/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.referencing.factory;
import java.io.Serializable;
import java.io.ObjectStreamException;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.LinkedHashSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import org.opengis.metadata.Identifier;
import org.opengis.referencing.AuthorityFactory;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.NoSuchIdentifierException;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.operation.CoordinateOperationAuthorityFactory; // For javadoc
import org.geotools.util.Utilities;
/**
* A lazy set of {@linkplain IdentifiedObject identified objects}. This set creates
* {@link IdentifiedObject}s from authority codes only when first needed. This class
* is typically used as the set returned by implementations of the
* {@link CoordinateOperationAuthorityFactory#createFromCoordinateReferenceSystemCodes
* createFromCoordinateReferenceSystemCodes} method. Deferred creation in this case may
* have great performance impact since a set may contains about 40 entries (e.g.
* transformations from "ED50" (EPSG:4230) to "WGS 84" (EPSG:4326)) while some users
* only want to look for the first entry (e.g. the default
* {@link org.geotools.referencing.operation.AuthorityBackedFactory} implementation).
* <p>
* <h3>Exception handling</h3>
* If the underlying factory failed to creates an object because of an unsupported
* operation method ({@link NoSuchIdentifierException}), the exception is logged with
* the {@link Level#FINE FINE} level (because this is a recoverable failure) and
* the iteration continue. If the operation creation failed for any other kind of
* reason ({@link FactoryException}), then the exception is rethrown as an unchecked
* {@link BackingStoreException}. This default behavior can be changed if a subclass
* overrides the {@link #isRecoverableFailure isRecoverableFailure} method.
* <p>
* <h3>Serialization</h3>
* Serialization of this class forces the immediate creation of all
* {@linkplain IdentifiedObject identified objects} not yet created.
* The serialized set is disconnected from the {@linkplain #factory underlying factory}.
* <p>
* <h3>Thread safety</h3>
* This class is not thread-safe.
*
* @since 2.2
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public class IdentifiedObjectSet extends AbstractSet implements Serializable {
/**
* For cross-version compatibility during serialisation.
*/
private static final long serialVersionUID = -4221260663706882719L;
/**
* The map of object codes (keys), and the actual identified objects (values)
* when it has been created. Each entry has a null value until the corresponding
* object is created.
*/
private final Map/*<String,IdentifiedObject>*/ objects = new LinkedHashMap();
/**
* The factory to use for creating {@linkplain IdentifiedObject identified objects}
* when first needed.
*/
protected final AuthorityFactory factory;
/**
* Creates an initially empty set. The {@linkplain IdentifiedObject}s
* will be created when first needed using the specified factory.
*
* @param factory The factory to use for deferred {@link IdentifiedObject}s creations.
*/
public IdentifiedObjectSet(final AuthorityFactory factory) {
this.factory = factory;
}
/**
* Removes all of the elements from this collection.
*/
@Override
public void clear() {
objects.clear();
}
/**
* Returns the number of objects available in this set. Note that this
* number may decrease during the iteration process if the creation of
* some {@linkplain IdentifiedObject identified objects} failed.
*/
public int size() {
return objects.size();
}
/**
* Ensures that this collection contains an object for the specified authority code.
* The {@linkplain IdentifiedObject identified object} will be created from the specified
* code only when first needed. This method returns {@code true} if this set changed as a
* result of this call.
*/
public boolean addAuthorityCode(final String code) {
final boolean already = objects.containsKey(code);
final IdentifiedObject old = (IdentifiedObject) objects.put(code, null);
if (old != null) {
// A fully created object was already there. Keep it.
objects.put(code, old);
return false;
}
return !already;
}
/**
* Ensures that this collection contains the specified object. This set do not allows multiple
* objects for the same {@linkplain #getAuthorityCode authority code}. If this set already
* contains an object using the same {@linkplain #getAuthorityCode authority code} than the
* specified one, then the old object is replaced by the new one even if the objects are not
* otherwise identical.
*/
public boolean add(final Object object) {
final String code = getAuthorityCode((IdentifiedObject) object);
return !Utilities.equals(objects.put(code, object), object);
}
/**
* Returns the identified object for the specified value, {@linkplain #createObject creating}
* it if needed.
*
* @throws BackingStoreException if the object creation failed.
*/
private IdentifiedObject get(final String code) throws BackingStoreException {
IdentifiedObject object = (IdentifiedObject) objects.get(code);
if (object==null && objects.containsKey(code)) {
try {
object = createObject(code);
objects.put(code, object);
} catch (FactoryException exception) {
if (!isRecoverableFailure(exception)) {
throw new BackingStoreException(exception);
}
log(exception, code);
objects.remove(code);
}
}
return object;
}
/**
* Returns {@code true} if this collection contains the specified object.
*/
public boolean contains(final Object object) {
final String code = getAuthorityCode((IdentifiedObject) object);
final IdentifiedObject current = get(code);
return object.equals(current);
}
/**
* Removes a single instance of the specified element from this collection,
* if it is present.
*/
public boolean remove(final Object object) {
final String code = getAuthorityCode((IdentifiedObject) object);
final IdentifiedObject current = get(code);
if (object.equals(current)) {
objects.remove(code);
return true;
}
return false;
}
/**
* Removes from this collection all of its elements that are contained in
* the specified collection.
*/
public boolean removeAll(final Collection collection) {
boolean modified = false;
for (final Iterator it=collection.iterator(); it.hasNext();) {
if (remove(it.next())) {
modified = true;
}
}
return modified;
}
/**
* Returns an iterator over the objects in this set. If the iteration encounter any
* kind of {@link FactoryException} other than {@link NoSuchIdentifierException}, then
* the exception will be rethrown as an unchecked {@link BackingStoreException}.
*/
public Iterator iterator() {
return new Iter(objects.entrySet().iterator());
}
/**
* Ensures that the <var>n</var> first objects in this set are created. This method is
* typically invoked after some calls to {@link #addAuthorityCode} in order to make sure
* that the {@linkplain #factory underlying factory} is really capable to create at least
* one object. {@link FactoryException} (except the ones accepted as
* {@linkplain #isRecoverableFailure recoverable failures}) are thrown as if they were never
* wrapped into {@link BackingStoreException}.
*
* @param n The number of object to resolve. If this number is equals or greater than the
* {@linkplain #size set's size}, then the creation of all objects is garantee
* successful.
* @throws FactoryException if an {@linkplain #createObject object creation} failed.
*/
public void resolve(int n) throws FactoryException {
if (n > 0) try {
for (final Iterator it=iterator(); it.hasNext();) {
it.next();
if (--n == 0) {
break;
}
}
} catch (BackingStoreException exception) {
final Throwable cause = exception.getCause();
if (cause instanceof FactoryException) {
throw (FactoryException) cause;
}
throw exception;
}
}
/**
* Returns the {@linkplain #getAuthorityCode authority code} of all objects in this set.
* The returned array contains the codes in iteration order. This method do not trig the
* {@linkplain #createObject creation} of any new object.
* <p>
* This method is typically used together with {@link #setAuthorityCodes} for altering the
* iteration order on the basis of authority codes.
*/
public String[] getAuthorityCodes() {
final Set codes = objects.keySet();
return (String[]) codes.toArray(new String[codes.size()]);
}
/**
* Set the content of this set as an array of authority codes. For any code in the given list,
* this method will preserve the corresponding {@linkplain IdentifiedObject identified object}
* if it was already created. Other objects will be {@linkplain #createObject created} only
* when first needed, as usual in this {@code IdentifiedObjectSet} implementation.
* <p>
* This method is typically used together with {@link #getAuthorityCodes} for altering the
* iteration order on the basis of authority codes. If the specified {@code codes} array
* contains the same elements than {@link #getAuthorityCodes} in a different order, then
* this method just set the new ordering.
*
* @see #addAuthorityCode
*/
public void setAuthorityCodes(final String[] codes) {
final Map copy = new HashMap(objects);
objects.clear();
for (int i=0; i<codes.length; i++) {
final String code = codes[i];
objects.put(code, (IdentifiedObject) copy.get(code));
}
}
/**
* Returns the code to uses as a key for the specified object. The default implementation
* returns the code of the first {@linkplain IdentifiedObject#getIdentifiers identifier},
* if any, or the code of the{@linkplain IdentifiedObject#getName primary name} otherwise.
* Subclasses may overrides this method if they want to use a different key for this set.
*/
protected String getAuthorityCode(final IdentifiedObject object) {
final Identifier id;
final Set identifiers = object.getIdentifiers();
if (identifiers!=null && !identifiers.isEmpty()) {
id = (Identifier) identifiers.iterator().next();
} else {
id = object.getName();
}
return id.getCode();
}
/**
* Creates an object for the specified authority code. This method is invoked during the
* iteration process if an object was not already created. The default implementation invokes
* <code>{@linkplain #factory}.{@link AuthorityFactory#createObject createObject}(code)</code>.
* Subclasses may override this method if they want to invoke a more specific method.
*/
protected IdentifiedObject createObject(final String code) throws FactoryException {
return factory.createObject(code);
}
/**
* Returns {@code true} if the specified exception should be handled as a recoverable failure.
* This method is invoked during the iteration process if the factory failed to create some
* object. If this method returns {@code true} for the given exception, then the exception
* will be logged in the {@linkplain AbstractAuthorityFactory#LOGGER Geotools factory logger}
* with the {@link Level#FINE FINE} level. If this method returns {@code false}, then the
* exception will be retrown as a {@link BackingStoreException}. The default implementation
* returns {@code true} only for {@link NoSuchIdentifierException} (not to be confused with
* {@link NoSuchAuthorityCodeException}).
*/
protected boolean isRecoverableFailure(final FactoryException exception) {
return (exception instanceof NoSuchIdentifierException);
}
/**
* Log an message for the specified exception.
*
* @todo Localize.
*/
static void log(final FactoryException exception, final String code) {
final LogRecord record = new LogRecord(Level.FINE,
"Failed to create an object for code \"" + code + "\".");
record.setSourceClassName(IdentifiedObjectSet.class.getName());
record.setSourceMethodName("createObject");
record.setThrown(exception);
final Logger logger = AbstractAuthorityFactory.LOGGER;
record.setLoggerName(logger.getName());
logger.log(record);
}
/**
* Returns a serializable copy of this set. This method is invoked automatically during
* serialization. The serialised set of identified objects is disconnected from the
* {@linkplain #factory underlying factory}.
*/
protected Object writeReplace() throws ObjectStreamException {
return new LinkedHashSet(this);
}
/**
* The iterator over the entries in the enclosing set. This iterator will creates the
* {@linkplain IdentifiedObject identified objects} when first needed.
*
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
private final class Iter implements Iterator {
/**
* The iterator over the entries from the underlying map.
*/
private final Iterator iterator;
/**
* The next object to returns, or {@code null} if the iteration is over.
*/
private IdentifiedObject element;
/**
* Creates a new instance of this iterator.
*/
public Iter(final Iterator iterator) {
this.iterator = iterator;
toNext();
}
/**
* Moves to the next element.
*
* @throws BackingStoreException if the underlying factory failed to creates the
* coordinate operation.
*/
private void toNext() throws BackingStoreException {
while (iterator.hasNext()) {
final Map.Entry entry = (Map.Entry) iterator.next();
element = (IdentifiedObject) entry.getValue();
if (element == null) {
final String code = (String) entry.getKey();
try {
element = createObject(code);
} catch (FactoryException exception) {
if (!isRecoverableFailure(exception)) {
throw new BackingStoreException(exception);
}
log(exception, code);
iterator.remove();
continue;
}
entry.setValue(element);
}
return; // Element found.
}
element = null; // No more element found.
}
/**
* Returns {@code true} if there is more elements.
*/
public boolean hasNext() {
return element != null;
}
/**
* Returns the next element.
*
* @throws NoSuchElementException if there is no more operations in the set.
*/
public Object next() throws NoSuchElementException {
final IdentifiedObject next = element;
if (next == null) {
throw new NoSuchElementException();
}
toNext();
return next;
}
/**
* Removes the last element from the underlying set.
*/
public void remove() {
iterator.remove();
}
}
}