| /* |
| * 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.internal.jaxb; |
| |
| import java.net.URI; |
| import java.util.Set; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.AbstractMap; |
| import java.util.NoSuchElementException; |
| import java.util.Objects; |
| import java.io.Serializable; |
| import org.opengis.metadata.Identifier; |
| import org.opengis.metadata.citation.Citation; |
| import org.apache.sis.xml.XLink; |
| import org.apache.sis.xml.IdentifierMap; |
| import org.apache.sis.xml.IdentifierSpace; |
| import org.apache.sis.internal.util.Strings; |
| import org.apache.sis.internal.util.SetOfUnknownSize; |
| |
| import static org.apache.sis.util.collection.Containers.hashMapCapacity; |
| |
| |
| /** |
| * Implementation of the map of identifiers associated to {@link org.apache.sis.xml.IdentifiedObject} instances. |
| * This base class implements an unmodifiable map, but the {@link ModifiableIdentifierMap} subclass add write |
| * capabilities. |
| * |
| * <p>This class works as a wrapper around a collection of identifiers. Because all operations |
| * are performed by an iteration over the collection elements, this implementation is suitable |
| * only for small maps (less than 10 elements). Given that objects typically have only one or |
| * two identifiers, this is considered acceptable.</p> |
| * |
| * <h2>Special cases</h2> |
| * The identifiers for the following authorities are handled in a special way: |
| * <ul> |
| * <li>{@link IdentifierSpace#HREF}: handled as a shortcut to {@link XLink#getHRef()}.</li> |
| * </ul> |
| * |
| * <h2>Handling of duplicated authorities</h2> |
| * The collection shall not contain more than one identifier for the same |
| * {@linkplain Identifier#getAuthority() authority}. However duplications may happen if the user |
| * has direct access to the list, for example through {@link Citation#getIdentifiers()}. If such |
| * duplication is found, then this map implementation applies the following rules: |
| * |
| * <ul> |
| * <li>All getter methods (including the iterators and the values returned by the {@code put} |
| * and {@code remove} methods) return only the identifier code associated to the first |
| * occurrence of each authority. Any subsequent occurrences of the same authorities are |
| * silently ignored.</li> |
| * <li>All setter methods <em>may</em> affect <em>all</em> identifiers previously associated to |
| * the given authority, not just the first occurrence. The only guarantee is that the list |
| * is update in such a way that the effect of setter methods are visible to subsequent calls |
| * to getter methods.</li> |
| * </ul> |
| * |
| * <h2>Handling of null identifiers</h2> |
| * The collection of identifiers shall not contain any null element. This is normally ensured by |
| * the {@link org.apache.sis.metadata.ModifiableMetadata} internal collection implementations. |
| * This class performs opportunist null checks as an additional safety, but consistency is not |
| * guaranteed. See {@link #size()} for more information. |
| * |
| * <h2>Thread safety</h2> |
| * This class is thread safe if the underlying identifier collection is thread safe. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 0.7 |
| * |
| * @see org.apache.sis.xml.IdentifiedObject |
| * |
| * @since 0.3 |
| * @module |
| */ |
| public class IdentifierMapAdapter extends AbstractMap<Citation,String> implements IdentifierMap, Serializable { |
| /** |
| * For cross-version compatibility. |
| */ |
| private static final long serialVersionUID = -1445849218952061605L; |
| |
| /** |
| * An immutable empty instance. |
| */ |
| public static final IdentifierMap EMPTY = new IdentifierMapAdapter(Collections.emptySet()); |
| |
| /** |
| * The identifiers to wrap in a map view. |
| */ |
| public final Collection<Identifier> identifiers; |
| |
| /** |
| * Creates a new map which will be a view over the given identifiers. |
| * |
| * @param identifiers the identifiers to wrap in a map view. |
| */ |
| public IdentifierMapAdapter(final Collection<Identifier> identifiers) { |
| this.identifiers = identifiers; |
| } |
| |
| /** |
| * If the given authority is a special case, returns its {@link NonMarshalledAuthority} integer enum. |
| * Otherwise returns -1. See javadoc for more information about special cases. |
| * |
| * @param authority a {@link Citation} constant. The type is relaxed to {@code Object} |
| * because the signature of some {@code Map} methods are that way. |
| */ |
| static int specialCase(final Object authority) { |
| if (authority == IdentifierSpace.HREF) return NonMarshalledAuthority.HREF; |
| // A future Apache SIS version may add more special cases here. |
| return -1; |
| } |
| |
| /** |
| * Extracts the {@code xlink:href} value from the {@link XLink} if presents. |
| * This method does not test if an explicit {@code xlink:href} identifier exists; |
| * this check must be done by the caller <strong>before</strong> to invoke this method. |
| * |
| * @see ModifiableIdentifierMap#setHRef(URI) |
| */ |
| private URI getHRef() { |
| final Identifier identifier = getIdentifier(IdentifierSpace.XLINK); |
| if (identifier instanceof SpecializedIdentifier<?>) { |
| final Object link = ((SpecializedIdentifier<?>) identifier).value; |
| if (link instanceof XLink) { |
| return ((XLink) link).getHRef(); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the string representation of the given value, or {@code null} if none. |
| * |
| * @param value the value returned be one of the above {@code getFoo()} methods. |
| */ |
| private static String toString(final Object value) { |
| return (value != null) ? value.toString() : null; |
| } |
| |
| |
| |
| |
| //////////////////////////////////////////////////////////////////////////////////////// |
| //////// //////// |
| //////// END OF SPECIAL CASES. //////// |
| //////// //////// |
| //////// Implementation of IdentifierMap methods follow. Each method may //////// |
| //////// have a switch statement over the special cases declared above. //////// |
| //////// //////// |
| //////////////////////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * Whether this map support {@code put} and {@code remove} operations. |
| */ |
| boolean isModifiable() { |
| return false; |
| } |
| |
| /** |
| * Returns {@code true} if the collection of identifiers contains at least one element. |
| * This method does not verify if the collection contains null element (it should not). |
| */ |
| @Override |
| public final boolean isEmpty() { |
| return identifiers.isEmpty(); |
| } |
| |
| /** |
| * Counts the number of entries, ignoring null elements and duplicated authorities. |
| * |
| * <p>Because {@code null} elements are ignored, this method may return 0 even if {@link #isEmpty()} |
| * returns {@code false}. However this inconsistency should not happen in practice because |
| * {@link org.apache.sis.metadata.ModifiableMetadata} internal collection implementations |
| * do not allow null values.</p> |
| */ |
| @Override |
| public final int size() { |
| final HashSet<Citation> done = new HashSet<>(hashMapCapacity(identifiers.size())); |
| for (final Identifier identifier : identifiers) { |
| if (identifier != null) { |
| done.add(identifier.getAuthority()); |
| } |
| } |
| return done.size(); |
| } |
| |
| /** |
| * Returns {@code true} if at least one identifier declares the given {@linkplain Identifier#getCode() code}. |
| * |
| * @param code the code to search, which should be an instance of {@link String}. |
| * @return {@code true} if at least one identifier uses the given code. |
| */ |
| @Override |
| public final boolean containsValue(final Object code) { |
| if (code instanceof String) { |
| for (final Identifier identifier : identifiers) { |
| if (identifier != null && code.equals(identifier.getCode())) { |
| return true; |
| } |
| } |
| return code.equals(toString(getHRef())); |
| // A future Apache SIS version may add more special cases here. |
| } |
| return false; |
| } |
| |
| /** |
| * Returns {@code true} if at least one identifier declares the given |
| * {@linkplain Identifier#getAuthority() authority}. |
| * |
| * @param authority the authority to search, which should be an instance of {@link Citation}. |
| * @return {@code true} if at least one identifier uses the given authority. |
| */ |
| @Override |
| public final boolean containsKey(final Object authority) { |
| if (authority instanceof Citation) { |
| if (getIdentifier((Citation) authority) != null) { |
| return true; |
| } |
| switch (specialCase(authority)) { |
| case NonMarshalledAuthority.HREF: return getHRef() != null; |
| // A future Apache SIS version may add more special cases here. |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns the identifier for the given key, or {@code null} if none. |
| */ |
| final Identifier getIdentifier(final Citation authority) { |
| for (final Identifier identifier : identifiers) { |
| if (identifier != null && Objects.equals(authority, identifier.getAuthority())) { |
| return identifier; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the identifier associated with the given authority, |
| * or {@code null} if no specialized identifier was found. |
| */ |
| @Override |
| @SuppressWarnings("unchecked") |
| public final <T> T getSpecialized(final IdentifierSpace<T> authority) { |
| final Identifier identifier = getIdentifier(authority); |
| if (identifier instanceof SpecializedIdentifier<?>) { |
| return ((SpecializedIdentifier<T>) identifier).value; |
| } |
| switch (specialCase(authority)) { |
| case NonMarshalledAuthority.HREF: return (T) getHRef(); |
| // A future Apache SIS version may add more special cases here. |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the code of the first identifier associated with the given |
| * {@linkplain Identifier#getAuthority() authority}, or {@code null} if no identifier was found. |
| * |
| * @param authority the authority to search, which should be an instance of {@link Citation}. |
| * @return the code of the identifier for the given authority, or {@code null} if none. |
| */ |
| @Override |
| public final String get(final Object authority) { |
| if (authority instanceof Citation) { |
| final Identifier identifier = getIdentifier((Citation) authority); |
| if (identifier != null) { |
| return identifier.getCode(); |
| } |
| switch (specialCase(authority)) { |
| case NonMarshalledAuthority.HREF: return toString(getHRef()); |
| // A future Apache SIS version may add more special cases here. |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Removes all identifiers associated with the given {@linkplain Identifier#getAuthority() authority}. |
| * |
| * @param authority the authority to search, which should be an instance of {@link Citation}. |
| * @return the code of the identifier for the given authority, or {@code null} if none. |
| * @throws UnsupportedOperationException if the collection of identifiers is unmodifiable. |
| */ |
| @Override |
| public String remove(Object authority) throws UnsupportedOperationException { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * Removes every entries in the underlying collection. |
| * |
| * @throws UnsupportedOperationException if the collection of identifiers is unmodifiable. |
| */ |
| @Override |
| public void clear() throws UnsupportedOperationException { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * Sets the code of the identifier having the given authority to the given value. |
| * If no identifier is found for the given authority, a new one is created. |
| * If more than one identifier is found for the given authority, then all previous identifiers may be removed |
| * in order to ensure that the new entry will be the first entry, so it can be find by the {@code get} method. |
| * |
| * @param authority the authority for which to set the code. |
| * @param code the new code for the given authority, or {@code null} for removing the entry. |
| * @return the previous code for the given authority, or {@code null} if none. |
| * @throws UnsupportedOperationException if the collection of identifiers is unmodifiable. |
| */ |
| @Override |
| public String put(Citation authority, String code) throws UnsupportedOperationException { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * Sets the identifier associated with the given authority, and returns the previous value. |
| */ |
| @Override |
| public <T> T putSpecialized(IdentifierSpace<T> authority, T value) throws UnsupportedOperationException { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * Returns a view over the collection of identifiers. This view supports removal operation |
| * if the underlying collection of identifiers supports the {@link Iterator#remove()} method. |
| * |
| * <p>If the backing identifier collection contains null entries, those entries will be ignored. |
| * If the backing collection contains many entries for the same authority, then only the first |
| * occurrence is included.</p> |
| * |
| * @return a view over the collection of identifiers. |
| */ |
| @Override |
| public Set<Entry<Citation,String>> entrySet() { |
| /* |
| * Do not cache the entries set because if is very cheap to create and not needed very often. |
| * Not caching allows this implementation to be thread-safe without synchronization or volatile |
| * fields if the underlying list is thread-safe. Furthermore, IdentifierMapAdapter are temporary |
| * objects anyway in the current ISOMetadata implementation. |
| */ |
| return new SetOfUnknownSize<Entry<Citation,String>>() { |
| /** Delegates to the enclosing class. */ |
| @Override public void clear() throws UnsupportedOperationException { |
| IdentifierMapAdapter.this.clear(); |
| } |
| |
| /** Delegates to the enclosing class. */ |
| @Override public boolean isEmpty() { |
| return IdentifierMapAdapter.this.isEmpty(); |
| } |
| |
| /** Delegates to the enclosing class. */ |
| @Override public int size() { |
| return IdentifierMapAdapter.this.size(); |
| } |
| |
| /** Returns an iterator over the (<var>citation</var>, <var>code</var>) entries. */ |
| @Override public Iterator<Entry<Citation, String>> iterator() { |
| return new Iter(identifiers, isModifiable()); |
| } |
| }; |
| } |
| |
| /** |
| * The iterator over the (<var>citation</var>, <var>code</var>) entries. This iterator is created by the |
| * {@link #entrySet()} collection. It extends {@link HashMap} as an opportunist implementation strategy, |
| * but users do not need to know this detail. |
| * |
| * <p>This iterator supports the {@link #remove()} operation if the underlying collection supports it.</p> |
| * |
| * <p>The map entries are used as a safety against duplicated authority values. The map values |
| * are non-null only after we iterated over an authority. Then the value is {@link Boolean#TRUE} |
| * if the identifier has been removed, of {@code Boolean#FALSE} otherwise.</p> |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 0.7 |
| * @since 0.3 |
| * @module |
| */ |
| @SuppressWarnings("serial") // Not intended to be serialized. |
| private static final class Iter extends HashMap<Citation,Boolean> implements Iterator<Entry<Citation,String>> { |
| /** |
| * An iterator over the {@link IdentifierMapAdapter#identifiers} collection, |
| * or (@code null} if we have reached the iteration end. |
| */ |
| private Iterator<? extends Identifier> identifiers; |
| |
| /** |
| * The next entry to be returned by {@link #next()}, or {@code null} if not yet computed. |
| * This field will be computed only when {@link #next()} or {@link #hasNext()} is invoked. |
| */ |
| private transient Entry<Citation,String> next; |
| |
| /** |
| * The current authority. Used only for removal operations. |
| */ |
| private transient Citation authority; |
| |
| /** |
| * {@code true} if the iterator should support the {@link #remove()} operation. |
| */ |
| private final boolean isModifiable; |
| |
| /** |
| * Creates a new iterator for the given collection of identifiers. |
| */ |
| Iter(final Collection<? extends Identifier> identifiers, final boolean isModifiable) { |
| super(hashMapCapacity(identifiers.size())); |
| this.identifiers = identifiers.iterator(); |
| this.isModifiable = isModifiable; |
| } |
| |
| /** |
| * Advances to the next non-null identifier, skips duplicated authorities, wraps the |
| * identifier in an entry if needed and stores the result in the {@link #next} field. |
| * If we reach the iteration end, then this method set the {@link #identifiers} |
| * iterator to {@code null}. |
| */ |
| private void toNext() { |
| final Iterator<? extends Identifier> it = identifiers; |
| if (it != null) { |
| while (it.hasNext()) { |
| final Identifier identifier = it.next(); |
| if (identifier != null) { |
| final Citation authority = identifier.getAuthority(); |
| final Boolean state = put(authority, Boolean.FALSE); |
| if (state == null) { |
| if (identifier instanceof IdentifierMapEntry) { |
| next = (IdentifierMapEntry) identifier; |
| } else { |
| next = new IdentifierMapEntry.Immutable(authority, identifier.getCode()); |
| } |
| this.authority = authority; |
| return; |
| } |
| if (state) { |
| // Found a duplicated entry, and user asked for the |
| // removal of that authority. |
| it.remove(); |
| } |
| } |
| } |
| identifiers = null; |
| } |
| } |
| |
| /** |
| * If we need to search for the next element, fetches it now. |
| * Then returns {@code true} if we didn't reached the iteration end. |
| */ |
| @Override |
| public boolean hasNext() { |
| if (next == null) { |
| toNext(); |
| } |
| return identifiers != null; |
| } |
| |
| /** |
| * If we need to search for the next element, searches it now. Then set {@link #next} |
| * to {@code null} as a flag meaning that next invocations will need to search again |
| * for an element, and returns the element that we got. |
| */ |
| @Override |
| public Entry<Citation,String> next() throws NoSuchElementException { |
| Entry<Citation,String> entry = next; |
| if (entry == null) { |
| toNext(); |
| entry = next; |
| } |
| next = null; |
| if (identifiers == null) { |
| throw new NoSuchElementException(); |
| } |
| return entry; |
| } |
| |
| /** |
| * Removes the last element returned by {@link #next()}. Note that if the {@link #next} |
| * field is non-null, that would mean that the iteration has moved since the last call |
| * to the {@link #next()} method, in which case the iterator is invalid. |
| */ |
| @Override |
| public void remove() throws IllegalStateException { |
| if (!isModifiable) { |
| throw new UnsupportedOperationException(); |
| } |
| final Iterator<? extends Identifier> it = identifiers; |
| if (it == null || next != null) { |
| throw new IllegalStateException(); |
| } |
| it.remove(); |
| put(authority, Boolean.TRUE); |
| } |
| |
| /** |
| * Iterators are not intended to be cloned. |
| */ |
| @Override |
| @SuppressWarnings("CloneDoesntCallSuperClone") |
| public Object clone() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * Returns the next value to be returned, for debugging purpose only. |
| */ |
| @Override |
| public String toString() { |
| return Strings.toString(Iter.class, "next", next); |
| } |
| } |
| |
| /** |
| * Overrides the string representation in order to use only the authority title as keys. |
| * We do that because the string representations of {@code DefaultCitation} objects are |
| * very big. |
| * |
| * <p>String examples:</p> |
| * <ul> |
| * <li>{gml:id=“myID”}</li> |
| * <li>{gco:uuid=“42924124-032a-4dfe-b06e-113e3cb81cf0”}</li> |
| * <li>{xlink:href=“http://www.mydomain.org/myHREF”}</li> |
| * </ul> |
| * |
| * @see SpecializedIdentifier#toString() |
| */ |
| @Override |
| public String toString() { |
| final StringBuilder buffer = new StringBuilder(50).append('{'); |
| for (final Entry<Citation,String> entry : entrySet()) { |
| if (buffer.length() != 1) { |
| buffer.append(", "); |
| } |
| SpecializedIdentifier.format(buffer, entry.getKey(), entry.getValue()); |
| } |
| return buffer.append('}').toString(); |
| } |
| } |