| /* |
| * 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(); |
| } |
| } |