blob: 6780fa9dd922f0194e511b049d27ae9bf9bde8cc [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.feature;
import java.util.Map;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.io.Serializable;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.InvalidObjectException;
import java.io.IOException;
import org.opengis.util.GenericName;
import org.opengis.metadata.quality.DataQuality;
import org.opengis.metadata.maintenance.ScopeCode;
import org.apache.sis.util.Classes;
import org.apache.sis.util.ArgumentChecks;
/**
* An instance of an {@linkplain DefaultAttributeType attribute type} containing the value of an attribute in a feature.
* {@code Attribute} holds three main information:
*
* <ul>
* <li>A {@linkplain #getType() reference to an attribute type}
* which defines the base Java type and domain of valid values.</li>
* <li>One or more {@linkplain #getValues() values}, which may be a singleton ([0 … 1] multiplicity)
* or multi-valued ([0 … ∞] multiplicity).</li>
* <li>Optional {@linkplain #characteristics() characteristics} about the attribute
* (e.g. a <var>temperature</var> attribute may have a characteristic holding the measurement <var>accuracy</var>).
* Characteristics are often, but not necessarily, constant for all attributes of the same type in a dataset.</li>
* </ul>
*
* {@code AbstractAttribute} can be instantiated by calls to {@link DefaultAttributeType#newInstance()}.
*
* <div class="section">Limitations</div>
* <ul>
* <li><b>Multi-threading:</b> {@code AbstractAttribute} instances are <strong>not</strong> thread-safe.
* Synchronization, if needed, shall be done externally by the caller.</li>
* <li><b>Serialization:</b> serialized objects of this class are not guaranteed to be compatible with future
* versions. Serialization should be used only for short term storage or RMI between applications running
* the same SIS version.</li>
* <li><b>Cloning:</b> despite providing a public {@link #clone()} method, this base class is <strong>not</strong>
* cloneable by default. Subclasses shall implement the {@link Cloneable} interface themselves if they choose
* to support cloning.</li>
* </ul>
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 0.8
*
* @param <V> the type of attribute values.
*
* @see AbstractFeature
* @see DefaultAttributeType
*
* @since 0.5
* @module
*/
@SuppressWarnings("CloneInNonCloneableClass") // Decision left to subclasses - see javadoc
public abstract class AbstractAttribute<V> extends Field<V> implements Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 7442739120526654676L;
/**
* Information about the attribute (base Java class, domain of values, <i>etc.</i>).
*/
final DefaultAttributeType<V> type;
/**
* Other attributes that describes this attribute, or {@code null} if not yet created.
*
* <div class="note"><b>Design note:</b>
* We could question if it is a good idea to put this field here, given that this field add a slight cost
* to all attribute implementations while only a small fraction of them will want attribute characteristics.
* Since attributes may exist in a very large amount, that question may be significant.
* However {@link AbstractFeature} tries hard to not create {@code Attribute} instances at all (it tries to
* store only their value instead), so we presume that peoples who ask for {@code Attribute} instances are
* willing to accept their cost.</div>
*
* @see #characteristics()
*/
private transient Map<String,AbstractAttribute<?>> characteristics;
/**
* Creates a new attribute of the given type.
*
* @param type information about the attribute (base Java class, domain of values, <i>etc.</i>).
*
* @see #create(DefaultAttributeType)
*/
protected AbstractAttribute(final DefaultAttributeType<V> type) {
this.type = type;
}
/**
* Creates a new attribute of the given type initialized to the
* {@linkplain DefaultAttributeType#getDefaultValue() default value}.
*
* @param <V> the type of attribute values.
* @param type information about the attribute (base Java class, domain of values, <i>etc.</i>).
* @return the new attribute.
*
* @see DefaultAttributeType#newInstance()
*/
public static <V> AbstractAttribute<V> create(final DefaultAttributeType<V> type) {
ArgumentChecks.ensureNonNull("type", type);
return isSingleton(type.getMaximumOccurs())
? new SingletonAttribute<>(type)
: new MultiValuedAttribute<>(type);
}
/**
* Creates a new attribute of the given type initialized to the given value.
* Note that a {@code null} value may not be the same as the default value.
*
* @param <V> the type of attribute values.
* @param type information about the attribute (base Java class, domain of values, <i>etc.</i>).
* @param value the initial value (may be {@code null}).
* @return the new attribute.
*/
static <V> AbstractAttribute<V> create(final DefaultAttributeType<V> type, final Object value) {
ArgumentChecks.ensureNonNull("type", type);
return isSingleton(type.getMaximumOccurs())
? new SingletonAttribute<>(type, value)
: new MultiValuedAttribute<>(type, value);
}
/**
* Invoked on serialization for saving the {@link #characteristics} field.
*
* @param out the output stream where to serialize this attribute.
* @throws IOException if an I/O error occurred while writing.
*/
private void writeObject(final ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
final AbstractAttribute<?>[] characterizedBy;
if (characteristics instanceof CharacteristicMap) {
characterizedBy = characteristics.values().toArray(new AbstractAttribute<?>[characteristics.size()]);
} else {
characterizedBy = null;
}
out.writeObject(characterizedBy);
}
/**
* Invoked on deserialization for restoring the {@link #characteristics} field.
*
* @param in the input stream from which to deserialize an attribute.
* @throws IOException if an I/O error occurred while reading or if the stream contains invalid data.
* @throws ClassNotFoundException if the class serialized on the stream is not on the classpath.
*/
private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
try {
final AbstractAttribute<?>[] characterizedBy = (AbstractAttribute<?>[]) in.readObject();
if (characterizedBy != null) {
characteristics = newCharacteristicsMap();
characteristics.values().addAll(Arrays.asList(characterizedBy));
}
} catch (RuntimeException e) {
// At least ClassCastException, NullPointerException, IllegalArgumentException and IllegalStateException.
throw (IOException) new InvalidObjectException(e.getLocalizedMessage()).initCause(e);
}
}
/**
* Returns the name of this attribute as defined by its {@linkplain #getType() type}.
* This convenience method delegates to {@link DefaultAttributeType#getName()}.
*
* @return the attribute name specified by its type.
*/
@Override
public GenericName getName() {
return type.getName();
}
/**
* Returns information about the attribute (base Java class, domain of values, <i>etc.</i>).
*
* <div class="warning"><b>Warning:</b> In a future SIS version, the return type may be changed
* to {@code org.opengis.feature.AttributeType}. This change is pending GeoAPI revision.</div>
*
* @return information about the attribute.
*/
public DefaultAttributeType<V> getType() {
return type;
}
/**
* Returns the attribute value, or {@code null} if none. This convenience method can be invoked in
* the common case where the {@linkplain DefaultAttributeType#getMaximumOccurs() maximum number}
* of attribute values is restricted to 1 or 0.
*
* @return the attribute value (may be {@code null}).
* @throws IllegalStateException if this attribute contains more than one value.
*
* @see AbstractFeature#getPropertyValue(String)
*/
@Override
public abstract V getValue() throws IllegalStateException;
/**
* Returns all attribute values, or an empty collection if none.
* The returned collection is <cite>live</cite>: changes in the returned collection
* will be reflected immediately in this {@code Attribute} instance, and conversely.
*
* <p>The default implementation returns a collection which will delegate its work to
* {@link #getValue()} and {@link #setValue(Object)}.</p>
*
* @return the attribute values in a <cite>live</cite> collection.
*/
@Override
public Collection<V> getValues() {
return super.getValues();
}
/**
* Sets the attribute value. All previous values are replaced by the given singleton.
*
* <div class="section">Validation</div>
* The amount of validation performed by this method is implementation dependent.
* Usually, only the most basic constraints are verified. This is so for performance reasons
* and also because some rules may be temporarily broken while constructing a feature.
* A more exhaustive verification can be performed by invoking the {@link #quality()} method.
*
* @param value the new value, or {@code null} for removing all values from this attribute.
* @throws IllegalArgumentException if this method verifies argument validity and the given value
* does not met the attribute constraints.
*
* @see AbstractFeature#setPropertyValue(String, Object)
*/
@Override
public abstract void setValue(final V value) throws IllegalArgumentException;
/**
* Sets the attribute values. All previous values are replaced by the given collection.
*
* <p>The default implementation ensures that the given collection contains at most one element,
* then delegates to {@link #setValue(Object)}.</p>
*
* @param values the new values.
* @throws IllegalArgumentException if the given collection contains too many elements.
*/
@Override
public void setValues(final Collection<? extends V> values) throws IllegalArgumentException {
super.setValues(values);
}
/**
* Other attributes that describes this attribute. For example if this attribute carries a measurement,
* then a characteristic of this attribute could be the measurement accuracy.
* See <cite>"Attribute characterization"</cite> in {@link DefaultAttributeType} Javadoc for more information.
*
* <p>The map returned by this method contains only the characteristics explicitly defined for this attribute.
* If the map contains no characteristic for a given name, a {@linkplain DefaultAttributeType#getDefaultValue()
* default value} may still exist.
* In such cases, callers may also need to inspect the {@link DefaultAttributeType#characteristics()}
* as shown in the <cite>Reading a characteristic</cite> section below.</p>
*
* <div class="note"><b>Rational:</b>
* Very often, all attributes of a given type in the same dataset have the same characteristics.
* For example it is very common that all temperature measurements in a dataset have the same accuracy,
* and setting a different accuracy for a single measurement is relatively rare.
* Consequently, {@code characteristics.isEmpty()} is a convenient way to check that an attribute have
* all the "standard" characteristics and need no special processing.</div>
*
* <div class="section">Reading a characteristic</div>
* The characteristic values are enumerated in the {@linkplain Map#values() map values}.
* The {@linkplain Map#keySet() map keys} are the {@code String} representations of characteristics
* {@linkplain DefaultAttributeType#getName() name}, for more convenient lookups.
*
* <p>If an attribute is known to be a measurement with a characteristic named "accuracy"
* of type {@link Float}, then the accuracy value could be read as below:</p>
*
* {@preformat java
* Float getAccuracy(Attribute<?> measurement) {
* Attribute<?> accuracy = measurement.characteristics().get("accuracy");
* if (accuracy != null) {
* return (Float) accuracy.getValue(); // Value may be null.
* } else {
* return (Float) measurement.getType().characteristics().get("accuracy").getDefaultValue();
* // A more sophisticated implementation would probably cache the default value somewhere.
* }
* }
* }
*
* <div class="section">Adding a characteristic</div>
* A new characteristic can be added in the map in three different ways:
* <ol>
* <li>Putting the (<var>name</var>, <var>characteristic</var>) pair explicitly.
* If an older characteristic existed for that name, it will be replaced.
* Example:
*
* {@preformat java
* Attribute<?> accuracy = ...; // To be created by the caller.
* characteristics.put("accuracy", accuracy);
* }</li>
*
* <li>Adding the new characteristic to the {@linkplain Map#values() values} collection.
* The name is inferred automatically from the characteristic type.
* If an older characteristic existed for the same name, an {@link IllegalStateException} will be thrown.
* Example:
*
* {@preformat java
* Attribute<?> accuracy = ...; // To be created by the caller.
* characteristics.values().add(accuracy);
* }</li>
*
* <li>Adding the characteristic name to the {@linkplain Map#keySet() key set}.
* If no characteristic existed for that name, a default one will be created.
* Example:
*
* {@preformat java
* characteristics.keySet().add("accuracy"); // Ensure that an entry will exist for that name.
* Attribute<?> accuracy = characteristics.get("accuracy");
* Features.cast(accuracy, Float.class).setValue(...); // Set new accuracy value here as a float.
* }</li>
* </ol>
*
* @return other attribute types that describes this attribute type, or an empty map if none.
*
* @see DefaultAttributeType#characteristics()
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public Map<String,AbstractAttribute<?>> characteristics() {
if (characteristics == null) {
characteristics = newCharacteristicsMap();
}
return characteristics; // Intentionally modifiable
}
/**
* Creates an initially empty map of characteristics for this attribute.
* This method does not store the new map in the {@link #characteristics} field;
* it is caller responsibility to do so if desired.
*/
private Map<String,AbstractAttribute<?>> newCharacteristicsMap() {
if (type instanceof DefaultAttributeType<?>) {
Map<String, DefaultAttributeType<?>> map = type.characteristics();
if (!map.isEmpty()) {
if (!(map instanceof CharacteristicTypeMap)) {
final Collection<DefaultAttributeType<?>> types = map.values();
map = CharacteristicTypeMap.create(type, types.toArray(new DefaultAttributeType<?>[types.size()]));
}
return new CharacteristicMap(this, (CharacteristicTypeMap) map);
}
}
return Collections.emptyMap();
}
/**
* Returns the characteristics, or an empty map if the characteristics have not yet been built.
* Contrarily to {@link #characteristics()}, this method does not create the map. This method
* is suitable when then caller only wants to read the map and does not plan to write anything.
*/
final Map<String,AbstractAttribute<?>> characteristicsReadOnly() {
return (characteristics != null) ? characteristics : Collections.emptyMap();
}
/**
* Evaluates the quality of this attribute at this method invocation time. The data quality reports
* may include information about whether the attribute value mets the constraints defined by the
* {@linkplain DefaultAttributeType attribute type}, or any other criterion at implementation choice.
*
* <p>The default implementation reports data quality with at least the following information:</p>
* <ul>
* <li>
* The {@linkplain org.apache.sis.metadata.iso.quality.DefaultDataQuality#getScope() scope}
* {@linkplain org.apache.sis.metadata.iso.quality.DefaultScope#getLevel() level} is set to
* {@link org.opengis.metadata.maintenance.ScopeCode#ATTRIBUTE}.
* </li><li>
* At most one {@linkplain org.apache.sis.metadata.iso.quality.DefaultDomainConsistency domain consistency}
* element is added to the {@linkplain org.apache.sis.metadata.iso.quality.DefaultDataQuality#getReports()
* reports} list (implementations are free to omit that element if they have nothing to report).
* If a report is provided, then it will contain at least the following information:
* <ul>
* <li>
* <p>The {@linkplain #getName() attribute name} as the data quality
* {@linkplain org.apache.sis.metadata.iso.quality.DefaultDomainConsistency#getMeasureIdentification()
* measure identification}.</p>
*
* <div class="note"><b>Note:</b> strictly speaking, {@code measureIdentification} identifies the
* <em>quality measurement</em>, not the “real” measurement itself. However this implementation
* uses the same set of identifiers for both for simplicity.</div>
* </li><li>
* <p>If the attribute {@linkplain #getValue() value} is not an {@linkplain Class#isInstance instance}
* of the expected {@linkplain DefaultAttributeType#getValueClass() value class}, or if the number
* of occurrences is not inside the multiplicity range, or if any other constraint is violated, then
* a {@linkplain org.apache.sis.metadata.iso.quality.DefaultConformanceResult conformance result} is
* added for each violation with an
* {@linkplain org.apache.sis.metadata.iso.quality.DefaultConformanceResult#getExplanation() explanation}
* set to the error message.</p>
*
* <div class="warning"><b>Note:</b> this is a departure from ISO intent, since {@code explanation}
* should be a statement about what a successful conformance means. This point may be reformulated
* in a future SIS version.</div>
* </li>
* </ul>
* </li>
* </ul>
*
* This attribute is valid if this method does not report any
* {@linkplain org.apache.sis.metadata.iso.quality.DefaultConformanceResult conformance result} having a
* {@linkplain org.apache.sis.metadata.iso.quality.DefaultConformanceResult#pass() pass} value of {@code false}.
*
* <div class="note"><b>Example:</b> given an attribute named “population” with [1 … 1] multiplicity,
* if no value has been assigned to that attribute, then this {@code quality()} method will return
* the following data quality report:
*
* {@preformat text
* Data quality
*   ├─Scope
*   │   └─Level………………………………………………… Attribute
*   └─Report
*       ├─Measure identification
*       │   └─Code………………………………………… population
*       ├─Evaluation method type…… Direct internal
*       └─Result
*           ├─Explanation……………………… Missing value for “population” property.
*           └─Pass………………………………………… false
* }
* </div>
*
* @return reports on all constraint violations found.
*
* @see AbstractFeature#quality()
*/
public DataQuality quality() {
final Validator v = new Validator(ScopeCode.ATTRIBUTE);
v.validate(type, getValues());
return v.quality;
}
/**
* Returns a string representation of this attribute.
* The returned string is for debugging purpose and may change in any future SIS version.
* The current implementation is like below:
*
* {@preformat text
* Attribute[“temperature” : Float] = {20.3, 17.8, 21.1}
* └─ characteristics: units=°C, accuracy=0.1
* }
*
* @return a string representation of this attribute for debugging purpose.
*/
@Override
public String toString() {
final StringBuilder buffer = FieldType.toString(isDeprecated(type), "Attribute", type.getName(),
Classes.getShortName(type.getValueClass()), getValues().iterator());
if (characteristics != null && !characteristics.isEmpty()) {
buffer.append(System.lineSeparator());
String separator = "└─ characteristics: ";
for (final Map.Entry<String,AbstractAttribute<?>> entry : characteristics.entrySet()) {
buffer.append(separator).append(entry.getKey()).append('=').append(entry.getValue().getValue());
separator = ", ";
}
}
return buffer.toString();
}
/**
* Returns a copy of this attribute if cloning is supported.
* The decision to support cloning or not is left to subclasses. If the subclass does not implement
* the {@link Cloneable} interface, then this method throws a {@link CloneNotSupportedException}.
* Otherwise the default implementation returns a <em>shallow</em> copy of this {@code Attribute}:
* the attribute {@linkplain #getValue() value} and {@linkplain #characteristics() characteristics}
* are <strong>not</strong> cloned.
* However subclasses may choose to do otherwise.
*
* @return a clone of this attribute.
* @throws CloneNotSupportedException if this attribute, the {@linkplain #getValue() value}
* or one of its {@linkplain #characteristics() characteristics} can not be cloned.
*/
@Override
@SuppressWarnings("unchecked")
public AbstractAttribute<V> clone() throws CloneNotSupportedException {
final AbstractAttribute<V> clone = (AbstractAttribute<V>) super.clone();
final Map<String,AbstractAttribute<?>> c = clone.characteristics;
if (c instanceof CharacteristicMap) {
clone.characteristics = ((CharacteristicMap) c).clone();
}
return clone;
}
}