blob: 1c068351b1ecd879fe0eb0cf8be6adf9b35f8276 [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.metadata;
import java.util.Map;
import java.util.Set;
import java.util.List;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Currency;
import java.util.NoSuchElementException;
import java.lang.reflect.Modifier;
import java.nio.charset.Charset;
import javax.xml.bind.annotation.XmlTransient;
import org.opengis.util.CodeList;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.collection.Containers;
import org.apache.sis.util.collection.CodeListSet;
import org.apache.sis.internal.util.CollectionsExt;
import org.apache.sis.internal.util.CheckedHashSet;
import org.apache.sis.internal.util.CheckedArrayList;
import org.apache.sis.internal.metadata.Resources;
import org.apache.sis.internal.system.Semaphores;
import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
import static org.apache.sis.internal.metadata.MetadataUtilities.valueIfDefined;
/**
* Base class of metadata having an editable content.
* Newly created {@code ModifiableMetadata} are initially in {@linkplain State#EDITABLE editable} state.
* The metadata can be populated using the setter methods provided by subclasses, then transition to the
* {@linkplain State#FINAL final} state for making it safe to share by many consumers.
*
* <h2>Tip for subclass implementations</h2>
* Subclasses can follow the pattern below for every {@code get} and {@code set} methods,
* with a different processing for singleton value or for {@linkplain Collection collections}.
*
* {@preformat java
* public class MyMetadata {
*
* // ==== Example for a singleton value =============================
*
* private Foo property;
*
* public Foo getProperty() {
* return property;
* }
*
* public void setProperty(Foo newValue) {
* checkWritePermission();
* property = newValue;
* }
*
* // ==== Example for a collection ==================================
*
* private Collection<Foo> properties;
*
* public Collection<Foo> getProperties() {
* return properties = nonNullCollection(properties, Foo.class);
* }
*
* public void setProperties(Collection<Foo> newValues) {
* // the call to checkWritePermission() is implicit
* properties = writeCollection(newValues, properties, Foo.class);
* }
* }
* }
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 0.3
* @module
*/
@XmlTransient
public abstract class ModifiableMetadata extends AbstractMetadata {
/**
* The {@link #state} value meaning that the metadata is modifiable.
* This is the default state when new {@link ModifiableMetadata} instances are created.
*/
private static final byte EDITABLE = 0;
/**
* See https://issues.apache.org/jira/browse/SIS-81 - not yet committed.
*/
private static final byte STAGED = 1;
/**
* A value for {@link #state} meaning that execution of {@code transitionTo(…)} is in progress.
* Must be greater than all other values except {@link #COMPLETABLE} and {@link #FINAL}.
*/
private static final byte FREEZING = 2;
/**
* The {@link #state} value meaning that missing properties can be set,
* but no existing properties can be modified (including collections).
* This is a kind of semi-final state.
*/
private static final byte COMPLETABLE = 3;
/**
* A value for {@link #state} meaning that {@code transitionTo(State.FINAL)} has been invoked.
* Must be greater than all other values.
*/
private static final byte FINAL = 4;
/**
* Whether this metadata has been made unmodifiable, as one of {@link #EDITABLE}, {@link #FREEZING}
* {@link #COMPLETABLE} or {@link #FINAL} values.
*
* <p>This field is not yet serialized because we are not sure to keep this information as a byte in
* the future. We could for example use an {@code int} and use remaining bits for caching hash-code
* value of final metadata.</p>
*/
private transient byte state;
/**
* Constructs an initially empty metadata.
* The initial state is {@link State#EDITABLE}.
*/
protected ModifiableMetadata() {
}
/**
* Whether the metadata is still editable or has been made final.
* New {@link ModifiableMetadata} instances are initially {@link #EDITABLE}
* and can be made {@link #FINAL} after construction by a call to {@link ModifiableMetadata#transitionTo(State)}.
*
* <div class="note"><b>Note:</b>
* more states may be added in future Apache SIS versions. On possible candidate is {@code STAGED}.
* See <a href="https://issues.apache.org/jira/browse/SIS-81">SIS-81</a>.</div>
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 1.0
* @module
*/
public enum State {
/**
* The metadata is modifiable.
* This is the default state when new {@link ModifiableMetadata} instances are created.
* Note that a modifiable metadata instance does <strong>not</strong> imply that all
* properties contained in that instance are also editable.
*/
EDITABLE(ModifiableMetadata.EDITABLE),
/**
* The metadata allows missing values to be set, but does not allow existing values to be modified.
* This state is not appendable, i.e. it does not allow adding elements in a collection.
*/
COMPLETABLE(ModifiableMetadata.COMPLETABLE),
/**
* The metadata is unmodifiable.
* When a metadata is final, it can not be moved back to an editable state
* (but it is still possible to create a modifiable copy with {@link MetadataCopier}).
* Invoking any setter method on an unmodifiable metadata cause an
* {@link UnmodifiableMetadataException} to be thrown.
*/
FINAL(ModifiableMetadata.FINAL);
/**
* Mapping from {@link ModifiableMetadata} private flags to {@code State} enumeration.
* A mapping exists because {@code ModifiableMetadata} does not use the same set of enumeration values
* (e.g. it has an internal {@link #FREEZING} value), and because future versions may use a bitmask.
*/
private static final State[] VALUES = new State[ModifiableMetadata.FINAL + 1];
static {
VALUES[ModifiableMetadata.EDITABLE] = EDITABLE;
VALUES[ModifiableMetadata.STAGED] = EDITABLE;
VALUES[ModifiableMetadata.FREEZING] = FINAL;
VALUES[ModifiableMetadata.COMPLETABLE] = COMPLETABLE;
VALUES[ModifiableMetadata.FINAL] = FINAL;
}
/**
* The numerical code associated to this enumeration value. It serves similar purpose to the
* {@link #ordinal()} value, but is nevertheless provided for the reasons given in {@link #VALUES}.
*/
final byte code;
/**
* Creates a new state associated to the given code numerical code.
*/
private State(final byte code) {
this.code = code;
}
}
/**
* Tells whether this instance of metadata is editable.
* This is initially {@link State#EDITABLE} for new {@code ModifiableMetadata} instances,
* but can be changed by a call to {@link #transitionTo(State)}.
*
* <p>{@link State#FINAL} implies that all properties are also final.
* This recursivity does not necessarily apply to other states. For example {@link State#EDITABLE}
* does <strong>not</strong> imply that all {@code ModifiableMetadata} children are also editable.</p>
*
* <div class="note"><b>API note:</b>
* the {@code ModifiableMetadata} state is not a metadata per se, but rather an information about
* this particular instance of a metadata class. Two metadata instances may be in different states
* but still have the same metadata content. For this reason, this method does not have {@code get}
* prefix for avoiding confusion with getter and setter methods of metadata properties.</div>
*
* @return the state (editable, completable or final) of this {@code ModifiableMetadata} instance.
*
* @since 1.0
*/
public State state() {
return State.VALUES[state];
}
/**
* Requests this metadata instance and (potentially) all its children to transition to a new state.
* The action performed by this method depends on the {@linkplain #state() source state} and the
* given target state, as listed in the following table:
*
* <table class="sis">
* <caption>State transitions</caption>
* <tr>
* <th>Current state</th>
* <th>Target state</th>
* <th>Action</th>
* </tr><tr>
* <td><var>Any</var></td>
* <td><var>Same</var></td>
* <td>Does nothing and returns {@code false}.</td>
* </tr><tr>
* <td>{@link State#EDITABLE}</td>
* <td>{@link State#COMPLETABLE}</td>
* <td>Marks this metadata and all children as completable.</td>
* </tr><tr>
* <td>Any</td>
* <td>{@link State#FINAL}</td>
* <td>Marks this metadata and all children as unmodifiable.</td>
* </tr><tr>
* <td>{@link State#FINAL}</td>
* <td>Any other</td>
* <td>Throws {@link UnmodifiableMetadataException}.</td>
* </tr>
* </table>
*
* The effect of invoking this method may be recursive. For example transitioning to {@link State#FINAL}
* implies transitioning all children {@code ModifiableMetadata} instances to the final state too.
*
* @param target the desired new state.
* @return {@code true} if the state of this {@code ModifiableMetadata} changed as a result of this method call.
* @throws UnmodifiableMetadataException if a transition to a less restrictive state
* (e.g. from {@link State#FINAL} to {@link State#EDITABLE}) was attempted.
*
* @since 1.0
*/
public boolean transitionTo(final State target) {
if (target.code < state) {
throw new UnmodifiableMetadataException(Resources.format(Resources.Keys.UnmodifiableMetadata));
}
if (target.code == state || state == FREEZING) {
return false;
}
byte result = state;
try {
state = FREEZING;
StateChanger.applyTo(target, this);
result = target.code;
} finally {
state = result;
}
return true;
}
/**
* Checks if changes in the metadata are allowed. All {@code setFoo(…)} methods in subclasses
* shall invoke this method (directly or indirectly) before to apply any change.
* The current property value should be specified in argument.
*
* @param current the current value, or {@code null} if none.
* @throws UnmodifiableMetadataException if this metadata is unmodifiable.
*
* @see #state()
*
* @since 1.0
*/
protected void checkWritePermission(Object current) throws UnmodifiableMetadataException {
if (state != COMPLETABLE) {
if (state == FINAL) {
throw new UnmodifiableMetadataException(Resources.format(Resources.Keys.UnmodifiableMetadata));
}
} else if (current != null) {
final MetadataStandard standard;
if (current instanceof AbstractMetadata) {
standard = ((AbstractMetadata) current).getStandard();
} else {
standard = getStandard();
}
final Object c = standard.getTitle(current);
if (c != null) current = c;
throw new UnmodifiableMetadataException(Resources.format(Resources.Keys.ElementAlreadyInitialized_1, current));
}
}
/**
* Writes the content of the {@code source} collection into the {@code target} list,
* creating it if needed. This method performs the following steps:
*
* <ul>
* <li>Invokes {@link #checkWritePermission(Object)} in order to ensure that this metadata is modifiable.</li>
* <li>If {@code source} is null or empty, returns {@code null}
* (meaning that the metadata property is not provided).</li>
* <li>If {@code target} is null, creates a new {@link List}.</li>
* <li>Copies the content of the given {@code source} into the target.</li>
* </ul>
*
* @param <E> the type represented by the {@code Class} argument.
* @param source the source list, or {@code null}.
* @param target the target list, or {@code null} if not yet created.
* @param elementType the base type of elements to put in the list.
* @return a list (possibly the {@code target} instance) containing the {@code source}
* elements, or {@code null} if the source was null.
* @throws UnmodifiableMetadataException if this metadata is unmodifiable.
*
* @see #nonNullList(List, Class)
*/
@SuppressWarnings("unchecked")
protected final <E> List<E> writeList(Collection<? extends E> source, List<E> target,
Class<E> elementType) throws UnmodifiableMetadataException
{
return (List<E>) write(source, target, elementType, Boolean.FALSE);
}
/**
* Writes the content of the {@code source} collection into the {@code target} set,
* creating it if needed. This method performs the following steps:
*
* <ul>
* <li>Invokes {@link #checkWritePermission(Object)} in order to ensure that this metadata is modifiable.</li>
* <li>If {@code source} is null or empty, returns {@code null}
* (meaning that the metadata property is not provided).</li>
* <li>If {@code target} is null, creates a new {@link Set}.</li>
* <li>Copies the content of the given {@code source} into the target.</li>
* </ul>
*
* @param <E> the type represented by the {@code Class} argument.
* @param source the source set, or {@code null}.
* @param target the target set, or {@code null} if not yet created.
* @param elementType the base type of elements to put in the set.
* @return a set (possibly the {@code target} instance) containing the {@code source} elements,
* or {@code null} if the source was null.
* @throws UnmodifiableMetadataException if this metadata is unmodifiable.
*
* @see #nonNullSet(Set, Class)
*/
protected final <E> Set<E> writeSet(Collection<? extends E> source, Set<E> target,
Class<E> elementType) throws UnmodifiableMetadataException
{
return (Set<E>) write(source, target, elementType, Boolean.TRUE);
}
/**
* Writes the content of the {@code source} collection into the {@code target} list or set,
* creating it if needed. This method performs the following steps:
*
* <ul>
* <li>Invokes {@link #checkWritePermission(Object)} in order to ensure that this metadata is modifiable.</li>
* <li>If {@code source} is null or empty, returns {@code null}
* (meaning that the metadata property is not provided).</li>
* <li>If {@code target} is null, creates a new {@link Set} or a new {@link List}
* depending on the value returned by {@link #collectionType(Class)}.</li>
* <li>Copies the content of the given {@code source} into the target.</li>
* </ul>
*
* <h4>Choosing a collection type</h4>
* Implementations shall invoke {@link #writeList writeList} or {@link #writeSet writeSet} methods
* instead than this method when the collection type is enforced by ISO specification.
* When the type is not enforced by the specification, some freedom are allowed at
* implementer choice. The default implementation invokes {@link #collectionType(Class)}
* in order to get a hint about whether a {@link List} or a {@link Set} should be used.
*
* @param <E> the type represented by the {@code Class} argument.
* @param source the source collection, or {@code null}.
* @param target the target collection, or {@code null} if not yet created.
* @param elementType the base type of elements to put in the collection.
* @return a collection (possibly the {@code target} instance) containing the {@code source} elements,
* or {@code null} if the source was null.
* @throws UnmodifiableMetadataException if this metadata is unmodifiable.
*/
protected final <E> Collection<E> writeCollection(Collection<? extends E> source, Collection<E> target,
Class<E> elementType) throws UnmodifiableMetadataException
{
return write(source, target, elementType, null);
}
/**
* Writes the content of the {@code source} collection into the {@code target} list or set,
* creating it if needed.
*
* @param useSet {@link Boolean#TRUE} for creating a set, {@link Boolean#FALSE} for creating a list,
* or null for automatic choice.
*/
@SuppressWarnings("unchecked")
private <E> Collection<E> write(final Collection<? extends E> source, Collection<E> target,
final Class<E> elementType, Boolean useSet) throws UnmodifiableMetadataException
{
/*
* It is not worth to copy the content if the current and the new instance are the
* same. This is safe only using the != operator, not the !equals(Object) method.
* This optimization is required for efficient working of PropertyAccessor.set(…)
* and JAXB unmarshalling.
*/
if (source != target) {
if (state == FREEZING) {
/*
* transitionTo(State.FINAL) is under progress. The source collection is already
* an unmodifiable instance created by StateChanger.
*/
assert (useSet != null) || collectionType(elementType).isInstance(source) : elementType;
return (Collection<E>) source;
}
checkWritePermission(valueIfDefined(target));
if (isNullOrEmpty(source)) {
target = null;
} else {
/*
* Reuse the existing collection if available, except in State.COMPLETABLE case
* since that collection may the Collection.EMPTY_SET or EMPTY_LIST.
*/
if (target != null && state != COMPLETABLE) {
target.clear();
} else {
if (useSet == null) {
useSet = useSet(elementType);
}
if (useSet) {
target = createSet(elementType, source);
} else {
target = createList(elementType, source);
}
}
target.addAll(source);
if (state == COMPLETABLE) {
if (useSet) {
target = CollectionsExt.unmodifiableOrCopy((Set<E>) target);
} else {
target = CollectionsExt.unmodifiableOrCopy((List<E>) target);
}
}
}
}
return target;
}
/**
* Writes the content of the {@code source} map into the {@code target} map,
* creating it if needed. This method performs the following steps:
*
* <ul>
* <li>Invokes {@link #checkWritePermission(Object)} in order to ensure that this metadata is modifiable.</li>
* <li>If {@code source} is null or empty, returns {@code null}
* (meaning that the metadata property is not provided).</li>
* <li>If {@code target} is null, creates a new {@link Map}.</li>
* <li>Copies the content of the given {@code source} into the target.</li>
* </ul>
*
* @param <K> the type of keys represented by the {@code Class} argument.
* @param <V> the type of values in the map.
* @param source the source map, or {@code null}.
* @param target the target map, or {@code null} if not yet created.
* @param keyType the base type of keys to put in the map.
* @return a map (possibly the {@code target} instance) containing the {@code source} entries,
* or {@code null} if the source was null.
* @throws UnmodifiableMetadataException if this metadata is unmodifiable.
*
* @see #nonNullMap(Map, Class)
*
* @since 1.0
*/
@SuppressWarnings("unchecked")
protected final <K,V> Map<K,V> writeMap(final Map<? extends K, ? extends V> source, Map<K,V> target,
Class<K> keyType) throws UnmodifiableMetadataException
{
/*
* Code in this method is a copy of write(Collection, Collection, Class) with some calls inlined.
* See the comments inside that write(…) method body for more information on the logic.
*/
if (source != target) {
if (state == FREEZING) {
return (Map<K,V>) source;
}
checkWritePermission((target == null) || target.isEmpty() ? null : target);
if (isNullOrEmpty(source)) {
target = null;
} else {
if (target != null && state != COMPLETABLE) {
target.clear();
} else {
target = createMap(keyType, source);
}
target.putAll(source);
if (state == COMPLETABLE) {
target = CollectionsExt.unmodifiableOrCopy(target);
}
}
}
return target;
}
/**
* Creates a list with the content of the {@code source} collection,
* or returns {@code null} if the source is {@code null} or empty.
* This is a convenience method for copying fields in subclass copy constructors.
*
* @param <E> the type represented by the {@code Class} argument.
* @param source the source collection, or {@code null}.
* @param elementType the base type of elements to put in the list.
* @return a list containing the {@code source} elements,
* or {@code null} if the source was null or empty.
*/
protected final <E> List<E> copyList(final Collection<? extends E> source, final Class<E> elementType) {
if (isNullOrEmpty(source)) {
return null;
}
final List<E> target = createList(elementType, source);
target.addAll(source);
return target;
}
/**
* Creates a set with the content of the {@code source} collection,
* or returns {@code null} if the source is {@code null} or empty.
* This is a convenience method for copying fields in subclass copy constructors.
*
* @param <E> the type represented by the {@code Class} argument.
* @param source the source collection, or {@code null}.
* @param elementType the base type of elements to put in the set.
* @return a set containing the {@code source} elements,
* or {@code null} if the source was null or empty.
*/
protected final <E> Set<E> copySet(final Collection<? extends E> source, final Class<E> elementType) {
if (isNullOrEmpty(source)) {
return null;
}
final Set<E> target = createSet(elementType, source);
target.addAll(source);
return target;
}
/**
* Creates a list or set with the content of the {@code source} collection,
* or returns {@code null} if the source is {@code null} or empty.
* This is a convenience method for copying fields in subclass copy constructors.
*
* <p>The collection type is selected as described in the
* {@link #nonNullCollection(Collection, Class)}.</p>
*
* @param <E> the type represented by the {@code Class} argument.
* @param source the source collection, or {@code null}.
* @param elementType the base type of elements to put in the collection.
* @return a collection containing the {@code source} elements,
* or {@code null} if the source was null or empty.
*/
protected final <E> Collection<E> copyCollection(final Collection<? extends E> source, final Class<E> elementType) {
if (isNullOrEmpty(source)) {
return null;
}
final Collection<E> target;
if (useSet(elementType)) {
target = createSet(elementType, source);
} else {
target = createList(elementType, source);
}
target.addAll(source);
return target;
}
/**
* Creates a map with the content of the {@code source} map,
* or returns {@code null} if the source is {@code null} or empty.
* This is a convenience method for copying fields in subclass copy constructors.
*
* @param <K> the type of keys represented by the {@code Class} argument.
* @param <V> the type of values in the map.
* @param source the source map, or {@code null}.
* @param keyType the base type of keys to put in the map.
* @return a map containing the {@code source} entries,
* or {@code null} if the source was null or empty.
*
* @since 1.0
*/
protected final <K,V> Map<K,V> copyMap(final Map<? extends K, ? extends V> source, final Class<K> keyType) {
if (isNullOrEmpty(source)) {
return null;
}
final Map<K,V> target = createMap(keyType, source);
target.putAll(source);
return target;
}
/**
* Creates a singleton list or set containing only the given value, if non-null.
* This is a convenience method for initializing fields in subclass constructors.
*
* <p>The collection type is selected as described in the
* {@link #nonNullCollection(Collection, Class)}.</p>
*
* @param <E> the type represented by the {@code Class} argument.
* @param value the singleton value to put in the returned collection, or {@code null}.
* @param elementType the element type (used only if {@code value} is non-null).
* @return a new modifiable collection containing the given value,
* or {@code null} if the given value was null.
*/
protected final <E> Collection<E> singleton(final E value, final Class<E> elementType) {
if (value == null) {
return null;
}
final Collection<E> collection;
if (useSet(elementType)) {
collection = createSet(elementType, null);
} else {
collection = new CheckedArrayList<>(elementType, 1);
}
collection.add(value);
return collection;
}
/**
* Returns {@code true} if empty collection should be returned as {@code null} value.
* This is usually not a behavior that we allow in public API. However this behavior
* is sometime desired internally, for example when marshalling with JAXB or when
* performing a {@code equals}, {@code isEmpty} or {@code prune} operation
* (for avoiding creating unnecessary collections).
*/
private static boolean emptyCollectionAsNull() {
return Semaphores.query(Semaphores.NULL_COLLECTION);
}
/**
* Returns the specified list, or a new one if {@code current} is null.
* This is a convenience method for implementation of {@code getFoo()} methods.
*
* @param <E> the type represented by the {@code Class} argument.
* @param current the existing list, or {@code null} if the list has not yet been created.
* @param elementType the element type (used only if {@code current} is null).
* @return {@code current}, or a new list if {@code current} is null.
*/
protected final <E> List<E> nonNullList(final List<E> current, final Class<E> elementType) {
if (current != null) {
return current.isEmpty() && emptyCollectionAsNull() ? null : current;
}
if (emptyCollectionAsNull()) {
return null;
}
if (state < FREEZING) {
return createList(elementType, current); // `current` given as a matter of principle even if null.
}
return Collections.emptyList();
}
/**
* Returns the specified set, or a new one if {@code current} is null.
* This is a convenience method for implementation of {@code getFoo()} methods.
*
* @param <E> the type represented by the {@code Class} argument.
* @param current the existing set, or {@code null} if the set has not yet been created.
* @param elementType the element type (used only if {@code current} is null).
* @return {@code current}, or a new set if {@code current} is null.
*/
protected final <E> Set<E> nonNullSet(final Set<E> current, final Class<E> elementType) {
if (current != null) {
return current.isEmpty() && emptyCollectionAsNull() ? null : current;
}
if (emptyCollectionAsNull()) {
return null;
}
if (state < FREEZING) {
return createSet(elementType, current); // `current` given as a matter of principle even if null.
}
return Collections.emptySet();
}
/**
* Returns the specified collection, or a new one if {@code current} is null.
* This is a convenience method for implementation of {@code getFoo()} methods.
*
* <h4>Choosing a collection type</h4>
* Implementations shall invoke {@link #nonNullList nonNullList(…)} or {@link #nonNullSet nonNullSet(…)}
* instead than this method when the collection type is enforced by ISO specification.
* When the type is not enforced by the specification, some freedom are allowed at implementer choice.
* The default implementation invokes {@link #collectionType(Class)} in order to get a hint about whether
* a {@link List} or a {@link Set} should be used.
*
* @param <E> the type represented by the {@code Class} argument.
* @param current the existing collection, or {@code null} if the collection has not yet been created.
* @param elementType the element type (used only if {@code current} is null).
* @return {@code current}, or a new collection if {@code current} is null.
*/
protected final <E> Collection<E> nonNullCollection(final Collection<E> current, final Class<E> elementType) {
if (current != null) {
assert collectionType(elementType).isInstance(current);
return current.isEmpty() && emptyCollectionAsNull() ? null : current;
}
if (emptyCollectionAsNull()) {
return null;
}
final boolean isModifiable = (state < FREEZING);
if (useSet(elementType)) {
if (isModifiable) {
return createSet(elementType, current); // `current` given as a matter of principle even if null.
} else {
return Collections.emptySet();
}
} else {
if (isModifiable) {
return createList(elementType, current); // `current` given as a matter of principle even if null.
} else {
return Collections.emptyList();
}
}
}
/**
* Returns the specified map, or a new one if {@code current} is null.
* This is a convenience method for implementation of {@code getFoo()} methods.
*
* @param <K> the type of keys represented by the {@code Class} argument.
* @param <V> the type of values in the map.
* @param current the existing map, or {@code null} if the map has not yet been created.
* @param keyType the key type (used only if {@code current} is null).
* @return {@code current}, or a new map if {@code current} is null.
*
* @since 1.0
*/
protected final <K,V> Map<K,V> nonNullMap(final Map<K,V> current, final Class<K> keyType) {
if (current != null) {
return current.isEmpty() && emptyCollectionAsNull() ? null : current;
}
if (emptyCollectionAsNull()) {
return null;
}
if (state < FREEZING) {
return createMap(keyType, current); // `current` given as a matter of principle even if null.
}
return Collections.emptyMap();
}
/**
* Creates a modifiable list for elements of the given type. This method is defined mostly
* for consistency with {@link #createSet(Class, Collection)}.
*
* @param source the collection to be copied in the new list. This method uses this information
* only for computing initial capacity; it does not perform the actual copy.
*/
private static <E> List<E> createList(final Class<E> elementType, final Collection<?> source) {
if (source == null) {
/*
* Do not specify an initial capacity, because the list will stay empty in a majority of cases
* (i.e. the users will want to iterate over the list elements more often than they will want
* to add elements). JDK implementation of ArrayList has a lazy instantiation mechanism for
* initially empty lists, but as of JDK 10 this lazy instantiation works only for list having
* the default capacity.
*/
return new CheckedArrayList<>(elementType);
}
return new CheckedArrayList<>(elementType, source.size());
}
/**
* Creates a modifiable set for elements of the given type. This method will create an {@link EnumSet},
* {@link CodeListSet} or {@link java.util.LinkedHashSet} depending on the {@code elementType} argument.
* The set must have a stable iteration order (this is needed by {@link TreeTableView}).
*
* @param source the collection to be copied in the new set, or {@code null} if unknown.
* This method uses this information only for computing initial capacity;
* it does not perform the actual copy.
*/
@SuppressWarnings({"unchecked","rawtypes"})
private static <E> Set<E> createSet(final Class<E> elementType, final Collection<?> source) {
if (Enum.class.isAssignableFrom(elementType)) {
return EnumSet.noneOf((Class) elementType);
}
if (CodeList.class.isAssignableFrom(elementType) && Modifier.isFinal(elementType.getModifiers())) {
return new CodeListSet(elementType);
}
/*
* If we can not compute an initial capacity from the size of an existing source, use an arbitrary
* small value (currently 4). We use a small value because collections will typically contain few
* elements (often just a singleton).
*/
return new CheckedHashSet<>(elementType, (source != null) ? Containers.hashMapCapacity(source.size()) : 4);
}
/**
* Creates a modifiable map for elements of the given type.
* The map must have a stable iteration order (this is needed by {@link TreeTableView}).
*
* @param source the map to be copied in the new map. This method uses this information
* only for computing initial capacity; it does not perform the actual copy.
*/
@SuppressWarnings({"unchecked","rawtypes"})
private static <K,V> Map<K,V> createMap(final Class<K> keyType, final Map<?,?> source) {
if (Enum.class.isAssignableFrom(keyType)) {
return new EnumMap(keyType);
} else {
// Must be LinkedHashMap, not HashMap, because TreeTableView needs stable iteration order.
return new LinkedHashMap<>((source != null) ? Containers.hashMapCapacity(source.size()) : 4);
}
}
/**
* Returns {@code true} if we should use a {@link Set} instead than a {@link List}
* for elements of the given type.
*/
private <E> boolean useSet(final Class<E> elementType) {
final Class<? extends Collection<E>> type = collectionType(elementType);
if (Set .class == (Class) type) return true;
if (List.class == (Class) type) return false;
throw new NoSuchElementException(Errors.format(Errors.Keys.UnsupportedType_1, type));
}
/**
* Returns the type of collection to use for the given type. The current implementation can
* return only two values: <code>{@linkplain Set}.class</code> if the property should not
* accept duplicated values, or <code>{@linkplain List}.class</code> otherwise. Future SIS
* versions may accept other types.
*
* <p>The default implementation returns <code>{@linkplain Set}.class</code> if the element type
* is assignable to {@link CodeList}, {@link Enum}, {@link String}, {@link Charset},
* {@link Locale} or {@link Currency}, and <code>{@linkplain List}.class</code> otherwise.
* Subclasses can override this method for choosing different kind of collections.
* <em>Note however that {@link Set} should be used only with immutable element types</em>,
* for {@linkplain Object#hashCode() hash code} stability.</p>
*
* @param <E> the type of elements in the collection to be created.
* @param elementType the type of elements in the collection to be created.
* @return {@code List.class} or {@code Set.class} depending on whether the
* property shall accept duplicated values or not.
*/
@SuppressWarnings({"rawtypes","unchecked"})
protected <E> Class<? extends Collection<E>> collectionType(final Class<E> elementType) {
return (Class) (CodeList.class.isAssignableFrom(elementType)
|| Enum.class.isAssignableFrom(elementType)
|| Charset.class.isAssignableFrom(elementType)
|| String.class == elementType
|| Locale.class == elementType
|| Currency.class == elementType
? Set.class : List.class);
}
}