blob: 174cf6c1374ef5e08b520bedbf8ab2e385e23003 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.referencing.factory;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Collection;
import java.util.AbstractSet;
import java.util.LinkedHashMap;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import org.opengis.util.FactoryException;
import org.opengis.util.NoSuchIdentifierException;
import org.opengis.metadata.Identifier;
import org.opengis.referencing.AuthorityFactory;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.internal.system.Loggers;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.resources.Messages;
import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.util.collection.CheckedContainer;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.Localized;
import org.apache.sis.util.Classes;
/**
* A lazy set of {@code IdentifiedObject} instances created from their authority codes only when first needed.
* This set delegates {@link IdentifiedObject} creation to the most appropriate {@code createFoo(String)} method
* of the {@link AuthorityFactory} given at construction time.
*
* <p>Elements can be added to this collection with calls to {@link #addAuthorityCode(String)} for deferred
* {@linkplain #createObject(String) object creation}, or to {@link #add(IdentifiedObject)} for objects
* that are already instantiated. This collection can not contain two {@code IdentifiedObject} instances
* having the same identifier. However the identifiers used by this class can be controlled by overriding
* {@link #getAuthorityCode(IdentifiedObject)}.</p>
*
* <p>Iterations over elements in this collection preserve insertion order.</p>
*
* <h2>Purpose</h2>
* {@code IdentifiedObjectSet} can be used as the set returned by implementations of the
* {@link GeodeticAuthorityFactory#createFromCoordinateReferenceSystemCodes(String, String)} method.
* Deferred creation can have great performance impact since some set may contain as much as 40 entries
* (e.g. transformations from <cite>"ED50"</cite> (EPSG:4230) to <cite>"WGS 84"</cite> (EPSG:4326))
* while some users only want to look for the first entry.
*
* <h2>Exception handling</h2>
* If the underlying factory failed to creates an object because of an unsupported operation method
* ({@link NoSuchIdentifierException}), the exception is logged at {@link Level#WARNING} and the iteration continue.
* If the operation creation failed for any other kind of reason ({@link FactoryException}), then the exception is
* re-thrown as an unchecked {@link BackingStoreException}. This default behavior can be changed by overriding
* the {@link #isRecoverableFailure(FactoryException)} method.
*
* <h2>Thread safety</h2>
* This class is thread-safe is the underlying {@linkplain #factory} is also thread-safe.
* However, implementers are encouraged to wrap in {@linkplain java.util.Collections#unmodifiableSet unmodifiable set}
* if they intent to cache {@code IdentifiedObjectSet} instances.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 0.7
*
* @param <T> the type of objects to be included in this set.
*
* @since 0.7
* @module
*/
public class IdentifiedObjectSet<T extends IdentifiedObject> extends AbstractSet<T> implements CheckedContainer<T>, Localized {
/**
* The map of object codes (keys), and the actual identified objects (values) when they have been created.
* Each entry has a null value until the corresponding object is created.
*
* <p><b>Note:</b> using {@code ConcurrentHahMap} would be more efficient.
* But the later does not support null values and does not preserve insertion order.</p>
*/
final Map<String,T> objects = new LinkedHashMap<>();
/**
* The {@link #objects} keys, created for iteration purpose when first needed and cleared when the map is modified.
* We need to use such array as a snapshot of the map state at the time the iterator was created because the map
* may be modified during iteration or by concurrent threads.
*/
private transient String[] codes;
/**
* The factory to use for creating {@code IdentifiedObject}s when first needed.
* This is the authority factory given at construction time.
*/
protected final AuthorityFactory factory;
/**
* A proxy to the most specific {@linkplain #factory} method to invoke
* for the {@linkplain #type} of objects included in this set.
*/
private final AuthorityFactoryProxy<? super T> proxy;
/**
* The type of objects included in this set.
*
* @see #getElementType()
*/
private final Class<T> type;
/**
* Creates an initially empty set. The set can be populated after construction by calls
* to {@link #addAuthorityCode(String)} for deferred {@code IdentifiedObject} creation,
* or to {@link #add(IdentifiedObject)} for already instantiated objects.
*
* @param factory the factory to use for deferred {@code IdentifiedObject} instances creation.
* @param type the type of objects included in this set.
*/
public IdentifiedObjectSet(final AuthorityFactory factory, final Class<T> type) {
ArgumentChecks.ensureNonNull("factory", factory);
ArgumentChecks.ensureNonNull("type", type);
proxy = AuthorityFactoryProxy.getInstance(type);
this.factory = factory;
this.type = type;
}
/**
* Returns the locale to use for error messages and warnings.
* The default implementation inherits the {@link #factory} locale, if any.
*
* @return the locale, or {@code null} if not explicitly defined.
*/
@Override
public Locale getLocale() {
return (factory instanceof Localized) ? ((Localized) factory).getLocale() : null;
}
/**
* Returns the type of {@code IdentifiedObject} included in this set.
*
* @return the type of {@code IdentifiedObject} included in this set.
*/
@Override
public Class<T> getElementType() {
return type;
}
/**
* Removes all of the elements from this collection.
*/
@Override
public void clear() {
synchronized (objects) {
codes = null;
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 {@code IdentifiedObject}s failed.
*
* @return the number of objects available in this set.
*/
@Override
public int size() {
synchronized (objects) {
return objects.size();
}
}
/**
* Returns the authority codes of all {@code IdentifiedObject}s contained in this collection, in insertion order.
* This method does not trig the {@linkplain #createObject(String) creation} of any object.
*
* @return the authority codes in iteration order.
*/
public String[] getAuthorityCodes() {
return codes().clone();
}
/**
* Returns the {@code codes} array, creating it if needed. This method does not clone the array.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
final String[] codes() {
synchronized (objects) {
if (codes == null) {
final Set<String> keys = objects.keySet();
codes = keys.toArray(new String[keys.size()]);
}
return codes;
}
}
/**
* Sets the content of this collection to the object identified by the given codes.
* For any code in the given sequence, this method will preserve the corresponding {@code IdentifiedObject}
* instance if it was already created. Otherwise objects will be {@linkplain #createObject(String) created}
* only when first needed.
*
* <div class="note"><b>Purpose:</b>
* 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} sequence contains the same elements than
* the ones in the array returned by {@link #getAuthorityCodes()} but in a different order, then this method
* just sets the new ordering.</div>
*
* @param codes the authority codes of identified objects to store in this set.
*
* @see #addAuthorityCode(String)
*/
public void setAuthorityCodes(final String... codes) {
synchronized (objects) {
this.codes = null;
final Map<String,T> copy = new HashMap<>(objects);
objects.clear();
for (final String code : codes) {
objects.put(code, copy.get(code));
}
}
}
/**
* Ensures that this collection contains an object for the specified authority code.
* If this collection does not contain any element for the given code, then this method
* will instantiate an {@code IdentifiedObject} for the given code only when first needed.
* Otherwise this collection is unchanged.
*
* @param code the code authority code of the {@code IdentifiedObject} to include in this set.
*/
public void addAuthorityCode(final String code) {
synchronized (objects) {
if (objects.putIfAbsent(code, null) == null) {
codes = null;
}
}
}
/**
* Ensures that this collection contains the specified object. This collection does not allow
* multiple objects for the same {@linkplain #getAuthorityCode(IdentifiedObject) authority code}.
* If this collection already contains an object using the same authority code than the given object,
* then the old object is replaced by the new one regardless of whether the objects themselves are equal or not.
*
* @param object the object to add to the set.
* @return {@code true} if this set changed as a result of this call.
*
* @see #getAuthorityCode(IdentifiedObject)
*/
@Override
public boolean add(final T object) {
final String code = getAuthorityCode(object);
final T previous;
synchronized (objects) {
codes = null;
previous = objects.put(code, object);
}
return !Objects.equals(previous, object);
}
/**
* Returns the identified object for the specified value, creating it if needed.
*
* @throws BackingStoreException if the object creation failed.
*
* @see #createObject(String)
*/
final T get(final String code) throws BackingStoreException {
T object;
boolean success;
synchronized (objects) {
object = objects.get(code);
success = (object != null || !objects.containsKey(code));
}
/*
* If we need to create the object, it should be done outside synchronized block.
* There is a risk that the same object is created twice in concurrent threads.
* If this happen, we will discard the duplicated value.
*/
if (!success) {
try {
object = createObject(code);
success = true; // Shall be set only after above line succeed.
} catch (FactoryException exception) {
if (!isRecoverableFailure(exception)) {
throw new BackingStoreException(exception);
}
final LogRecord record = Messages.getResources(getLocale()).getLogRecord(Level.WARNING,
Messages.Keys.CanNotInstantiateForIdentifier_3, type, code, getCause(exception));
record.setLoggerName(Loggers.CRS_FACTORY);
Logging.log(IdentifiedObjectSet.class, "createObject", record);
}
synchronized (objects) {
if (success) {
/*
* The check for 'containsKey' is a paranoiac check in case the element has been removed
* in another thread while we were creating the object. This is likely to be unnecessary
* in the vast majority of cases where the set of codes is never modified after this set
* has been published. However, if someone decided to do such concurrent modifications,
* not checking for concurrent removal could be a subtle and hard-to-find bug, so we are
* better to be safe. Note that if a concurrent removal happened, we still return the non-null
* object but we do not put it in this IdentifiedObjectSet. This behavior is as if this method
* has been invoked before the concurrent removal happened.
*/
if (objects.containsKey(code)) { // Needed because code may be associated to null value.
final T c = objects.putIfAbsent(code, object);
if (c != null) {
object = c; // The object has been created concurrently.
}
}
} else if (objects.remove(code, null)) { // Do not remove if a concurrent thread succeeded.
codes = null;
}
}
}
return object;
}
/**
* Returns {@code true} if this collection contains the specified {@code IdentifiedObject}.
*
* @param object the {@code IdentifiedObject} to test for presence in this set.
* @return {@code true} if the given object is presents in this set.
*/
@Override
public boolean contains(final Object object) {
return (object != null) && object.equals(get(getAuthorityCode(type.cast(object))));
}
/**
* Removes the object for the given code.
*
* @param code the code of the object to remove.
*/
final void removeAuthorityCode(final String code) {
synchronized (objects) {
objects.remove(code);
codes = null;
}
}
/**
* Removes the specified {@code IdentifiedObject} from this collection, if it is present.
*
* @param object the {@code IdentifiedObject} to remove from this set.
* @return {@code true} if this set changed as a result of this call.
*/
@Override
public boolean remove(final Object object) {
if (object != null) {
final String code = getAuthorityCode(type.cast(object));
final T current = get(code);
if (object.equals(current)) {
synchronized (objects) {
objects.remove(code);
codes = null;
}
return true;
}
}
return false;
}
/**
* Removes from this collection all of its elements that are contained in the specified collection.
*
* @param collection the {@code IdentifiedObject}s to remove from this set.
* @return {@code true} if this set changed as a result of this call.
*/
@Override
public boolean removeAll(final Collection<?> collection) {
boolean modified = false;
for (final Object object : collection) {
modified |= remove(object);
}
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 re-thrown as an unchecked {@link BackingStoreException}.
*
* <p>This iterator is <strong>not</strong> thread safe – iteration should be done in a single thread.
* However the iterator is robust to concurrent changes in {@code IdentifiedObjectSet} during iteration.</p>
*
* @return an iterator over all {@code IdentifiedObject} instances in this set, in insertion order.
* @throws BackingStoreException if an error occurred while creating the iterator.
*/
@Override
public Iterator<T> iterator() throws BackingStoreException {
return new Iterator<T>() {
/**
* The keys from the underlying map, as a snapshot taken at the time the iterator has been created.
* We need to take a snapshot because the underlying {@link IdentifiedObjectSet#objects} map may be
* modified concurrently in other threads.
*/
private final String[] keys = codes();
/**
* Index of the next element to obtain by a call to {@link IdentifiedObjectSet#get(String)}.
*/
private int index;
/**
* The next element to return, or {@code null} if not yet determined.
*/
private T next;
/**
* {@code true} if {@link #remove()} can remove the element identified by {@code keys[index - 1]}.
*/
private boolean canRemove;
/**
* Returns {@code true} if there is more elements.
*
* @throws BackingStoreException if the underlying factory failed to creates the {@code IdentifiedObject}.
*/
@Override
public boolean hasNext() throws BackingStoreException {
while (next == null) {
if (index >= keys.length) {
return false;
}
next = get(keys[index++]);
}
return true;
}
/**
* Returns the next element.
*
* @throws NoSuchElementException if there is no more {@code IdentifiedObject} to iterate.
*/
@Override
public T next() throws NoSuchElementException {
if (canRemove = hasNext()) {
final T e = next;
next = null;
return e;
}
throw new NoSuchElementException();
}
/**
* Removes the previous element from the underlying set.
*/
@Override
public void remove() {
if (!canRemove) {
throw new IllegalStateException();
}
removeAuthorityCode(keys[index - 1]);
canRemove = false;
}
};
}
/**
* Ensures that the <var>n</var> first objects in this set are created. This method can be invoked for
* making sure that the underlying {@linkplain #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 {@link #size()}, then
* this method ensures that all {@code IdentifiedObject} instances in this collection are created.
* @throws FactoryException if an {@linkplain #createObject(String) object creation} failed.
*/
public void resolve(int n) throws FactoryException {
if (n > 0) try {
for (final Iterator<T> it=iterator(); it.hasNext(); it.next()) {
if (--n == 0) {
break;
}
}
} catch (BackingStoreException exception) {
throw exception.unwrapOrRethrow(FactoryException.class);
}
}
/**
* Returns the identifier for the specified object.
* The default implementation takes the first of the following identifier which is found:
*
* <ol>
* <li>An identifier allocated by the authority given by
* <code>{@linkplain #factory}.{@linkplain GeodeticAuthorityFactory#getAuthority() getAuthority()}</code>.</li>
* <li>The first {@linkplain org.apache.sis.referencing.AbstractIdentifiedObject#getIdentifiers() object identifier},
* regardless its authority.</li>
* <li>The first {@linkplain org.apache.sis.referencing.AbstractIdentifiedObject#getName() object name},
* regardless its authority.</li>
* </ol>
*
* Subclasses may override this method if they want to use a different identifiers.
*
* @param object the object for which to get the authority code.
* @return the authority code of the given identified object.
*/
protected String getAuthorityCode(final T object) {
final Identifier id = IdentifiedObjects.getIdentifier(object, factory.getAuthority());
return (id != null) ? id.getCode() : IdentifiedObjects.getIdentifierOrName(object);
}
/**
* Creates an object for the specified authority code.
* This method is invoked during the iteration process if an object was not already created.
*
* @param code the code for which to create the identified object.
* @return the identified object created from the given code.
* @throws FactoryException if the object creation failed.
*/
protected T createObject(final String code) throws FactoryException {
return type.cast(proxy.createFromAPI(factory, 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 objects.
* If this method returns {@code true} for the given exception, then the exception will be logged
* at {@link Level#WARNING}. If this method returns {@code false}, then the exception will be re-thrown
* as a {@link BackingStoreException}.
*
* <p>The default implementation applies the following rules:</p>
* <ul>
* <li>If {@link NoSuchAuthorityCodeException}, returns {@code false} since failure to find a code declared
* in the collection would be an inconsistency. Note that this exception is a subtype of
* {@code NoSuchIdentifierException}, so it must be tested before the last case below.</li>
* <li>If {@link NoSuchIdentifierException}, returns {@code true} since this exception is caused by an attempt to
* {@linkplain org.opengis.referencing.operation.MathTransformFactory#createParameterizedTransform
* create a parameterized transform} for an unimplemented operation.</li>
* <li>If {@link MissingFactoryResourceException}, returns {@code true}.</li>
* <li>Otherwise returns {@code false}.</li>
* </ul>
*
* @param exception the exception that occurred while creating an object.
* @return {@code true} if the given exception should be considered recoverable,
* or {@code false} if it should be considered fatal.
*/
protected boolean isRecoverableFailure(final FactoryException exception) {
if (exception instanceof NoSuchIdentifierException) {
return !(exception instanceof NoSuchAuthorityCodeException);
}
return (exception instanceof MissingFactoryResourceException);
}
/**
* Returns the message to format below the logging for giving the cause of an error.
*/
private static String getCause(Throwable cause) {
final String lineSeparator = System.lineSeparator();
final StringBuilder trace = new StringBuilder(180);
while (cause != null) {
trace.append(lineSeparator).append(" • ").append(Classes.getShortClassName(cause));
final String message = cause.getMessage(); // Prefer the local of system administrator.
if (message != null) {
trace.append(": ").append(message);
}
cause = cause.getCause();
}
return trace.toString();
}
}