| /* |
| * 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.util.iso; |
| |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.AbstractMap; |
| import java.util.AbstractSet; |
| import java.util.Iterator; |
| import java.util.NoSuchElementException; |
| import java.util.Collections; |
| import java.util.Objects; |
| import java.io.Serializable; |
| import java.lang.reflect.Array; |
| import javax.xml.bind.annotation.XmlValue; |
| import org.opengis.util.MemberName; |
| import org.opengis.util.Record; |
| import org.opengis.util.RecordType; |
| import org.apache.sis.util.Utilities; |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.internal.util.AbstractMapEntry; |
| import org.apache.sis.internal.metadata.RecordSchemaSIS; |
| |
| |
| /** |
| * A list of logically related elements as (<var>name</var>, <var>value</var>) pairs in a dictionary. |
| * By definition, all record members have a [1 … 1] multiplicity |
| * (for a more flexible construct, see {@linkplain org.apache.sis.feature features}). |
| * Since all members are expected to be assigned a value, the initial values on {@code DefaultRecord} |
| * instantiation are unspecified. Some may be null, or some may be zero. |
| * |
| * <h2>Limitations</h2> |
| * <ul> |
| * <li><b>Multi-threading:</b> {@code DefaultRecord} instances are <strong>not</strong> thread-safe. |
| * Synchronization, if needed, shall be done externally by the caller.</li> |
| * <li><b>Serialization:</b> this class is serializable if the associated {@code RecordType} and all |
| * values are also serializable. Note in particular that {@link DefaultRecordSchema} is currently |
| * <strong>not</strong> serializable, so users wanting serialization may need to define their own |
| * schema implementation.</li> |
| * </ul> |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @author Cullen Rombach (Image Matters) |
| * @version 1.0 |
| * |
| * @see DefaultRecordType |
| * @see DefaultRecordSchema |
| * |
| * @since 0.5 |
| * @module |
| */ |
| public class DefaultRecord implements Record, Serializable { |
| /** |
| * For cross-version compatibility. |
| */ |
| private static final long serialVersionUID = -5293250754663538325L; |
| |
| /** |
| * The type definition of this record. Can not be {@code null}. |
| */ |
| final RecordDefinition definition; |
| |
| /** |
| * The record values in an array. May be an array of primitive type for compactness, |
| * which is why the type is not {@code Object[]}. Should never be {@code null}, except |
| * temporarily during XML unmarshalling. |
| */ |
| private Object values; |
| |
| /** |
| * Creates a new record for the given record type. |
| * The initial values are unspecified - they may be null or zero. |
| * Callers can assign values by a call to {@link #setAll(Object[])}. |
| * |
| * @param type the type definition of the new record. |
| */ |
| public DefaultRecord(final RecordType type) { |
| ArgumentChecks.ensureNonNull("type", type); |
| if (type instanceof RecordDefinition) { |
| definition = (RecordDefinition) type; |
| } else { |
| definition = new RecordDefinition.Adapter(type); |
| } |
| values = Array.newInstance(definition.baseValueClass(), definition.size()); |
| } |
| |
| /** |
| * Creates a new record initialized to a shallow copy of the given record. |
| * The members contained in the given record are <strong>not</strong> recursively copied. |
| * |
| * @param record the record to copy (can not be null). |
| * |
| * @since 0.8 |
| */ |
| @SuppressWarnings("SuspiciousSystemArraycopy") |
| public DefaultRecord(final Record record) { |
| this(record.getRecordType()); |
| if (record instanceof DefaultRecord) { |
| final Object source = ((DefaultRecord) record).values; |
| System.arraycopy(source, 0, values, 0, Array.getLength(source)); |
| } else { |
| for (final Map.Entry<MemberName,Integer> entry : definition.memberIndices().entrySet()) { |
| final MemberName name = entry.getKey(); |
| final Object value = record.locate(name); |
| if (value != null) { |
| final int index = entry.getValue(); |
| final Class<?> valueClass = definition.getValueClass(index); |
| if (valueClass != null && !valueClass.isInstance(value)) { |
| throw new ClassCastException(Errors.format(Errors.Keys.IllegalPropertyValueClass_3, |
| name, valueClass, value.getClass())); |
| } |
| Array.set(values, index, value); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns a SIS implementation with the name and members of the given arbitrary implementation. |
| * This method performs the first applicable action in the following choices: |
| * |
| * <ul> |
| * <li>If the given object is {@code null}, then this method returns {@code null}.</li> |
| * <li>Otherwise if the given object is already an instance of {@code DefaultRecord}, |
| * then it is returned unchanged.</li> |
| * <li>Otherwise a new {@code DefaultRecord} instance is created using the |
| * {@linkplain #DefaultRecord(Record) copy constructor} and returned. |
| * Note that this is a shallow copy operation, since the members contained |
| * in the given object are not recursively copied.</li> |
| * </ul> |
| * |
| * @param other The object to get as a SIS implementation, or {@code null} if none. |
| * @return A SIS implementation containing the members of the given object |
| * (may be the given object itself), or {@code null} if the argument was {@code null}. |
| * |
| * @since 0.8 |
| */ |
| public static DefaultRecord castOrCopy(final Record other) { |
| if (other == null || other instanceof DefaultRecord) { |
| return (DefaultRecord) other; |
| } else { |
| return new DefaultRecord(other); |
| } |
| } |
| |
| /** |
| * Returns the type definition of this record. |
| * |
| * @return the type definition of this record. |
| */ |
| @Override |
| public RecordType getRecordType() { |
| return definition.getRecordType(); |
| } |
| |
| /** |
| * Returns the dictionary of all (<var>name</var>, <var>value</var>) pairs in this record. |
| * This method returns a view which will delegate all {@code get} and {@code put} operations to |
| * the {@link #locate(MemberName)} and {@link #set(MemberName, Object)} methods respectively. |
| * |
| * @return the dictionary of all (<var>name</var>, <var>value</var>) pairs in this record. |
| * |
| * @see RecordType#getMemberTypes() |
| */ |
| @Override |
| public Map<MemberName, Object> getAttributes() { |
| if (values == null) { // Should never be null, except temporarily at XML unmarshalling time. |
| return Collections.emptyMap(); |
| } |
| return new AbstractMap<MemberName, Object>() { |
| /** Returns the number of members in the record. */ |
| @Override |
| public int size() { |
| return definition.size(); |
| } |
| |
| /** Delegates to {@code DefaultRecord.locate(name)}. */ |
| @Override |
| public Object get(final Object name) { |
| return (name instanceof MemberName) ? locate((MemberName) name) : null; |
| } |
| |
| /** Delegates to {@code DefaultRecord.set(name, value)}. */ |
| @Override |
| public Object put(final MemberName name, final Object value) { |
| final Object previous = locate(name); |
| set(name, value); |
| return previous; |
| } |
| |
| /** Returns a set containing all (<var>name</var>, <var>value</var>) pairs in the record. */ |
| @Override |
| public Set<Map.Entry<MemberName, Object>> entrySet() { |
| return new Entries(); |
| } |
| }; |
| } |
| |
| /** |
| * The set of map entries to be returned by {@code DefaultRecord.getAttributes().entrySet()}. |
| * {@link AbstractMap} uses this set for providing a default implementation of most methods. |
| */ |
| private final class Entries extends AbstractSet<Map.Entry<MemberName,Object>> { |
| /** Returns the number of members in the record. */ |
| @Override |
| public int size() { |
| return definition.size(); |
| } |
| |
| /** Returns an iterator over all record members. */ |
| @Override |
| public Iterator<Map.Entry<MemberName, Object>> iterator() { |
| return new Iter(); |
| } |
| } |
| |
| /** |
| * The iterator to be returned by {@code DefaultRecord.getAttributes().entrySet().iterator()}. |
| * {@link AbstractMap} (indirectly) and {@link AbstractSet} use this iterator for providing a |
| * default implementation of most methods. |
| */ |
| private final class Iter implements Iterator<Map.Entry<MemberName,Object>> { |
| /** Index of the next record member to return in the iteration. */ |
| private int index; |
| |
| /** Returns {@code true} if there is more record members to iterate over. */ |
| @Override |
| public boolean hasNext() { |
| return index < definition.size(); |
| } |
| |
| /** Returns an entry containing the name and value of the next record member. */ |
| @Override |
| public Map.Entry<MemberName, Object> next() { |
| if (hasNext()) { |
| return new Entry(index++); |
| } |
| throw new NoSuchElementException(); |
| } |
| } |
| |
| /** |
| * A single entry in the map returned by {@code DefaultRecord.getAttributes()}. |
| * Operations on this entry delegate to {@link DefaultRecord#locate(MemberName)} |
| * and {@link DefaultRecord#set(MemberName, Object)} methods. |
| */ |
| private final class Entry extends AbstractMapEntry<MemberName,Object> { |
| /** Index of the record member represented by this entry. */ |
| private final int index; |
| |
| /** Creates a new entry for the record member at the given index. */ |
| Entry(final int index) { |
| this.index = index; |
| } |
| |
| /** Returns the name of the record member contained in this entry. */ |
| @Override |
| public MemberName getKey() { |
| return definition.getName(index); |
| } |
| |
| /** Returns the current record member value. */ |
| @Override |
| public Object getValue() { |
| return locate(getKey()); |
| } |
| |
| /** Sets the record member value and returns the previous value. */ |
| @Override |
| public Object setValue(final Object value) { |
| final MemberName name = getKey(); |
| final Object previous = locate(name); |
| set(name, value); |
| return previous; |
| } |
| } |
| |
| /** |
| * Returns the value for an attribute of the specified name. |
| * |
| * @param name the name of the attribute to lookup. |
| * @return the value of the attribute for the given name. |
| */ |
| @Override |
| public Object locate(final MemberName name) { |
| final Integer index = definition.indexOf(name); |
| return (index != null) ? Array.get(values, index) : null; |
| } |
| |
| /** |
| * Sets the value for the attribute of the specified name. |
| * |
| * @param name the name of the attribute to modify. |
| * @param value the new value for the attribute. |
| * @throws IllegalArgumentException if the given name is not a member of this record. |
| * @throws ClassCastException if the given value is not an instance of the expected type for this record. |
| */ |
| @Override |
| public void set(final MemberName name, final Object value) { |
| final Integer index = definition.indexOf(name); |
| if (index == null) { |
| throw new IllegalArgumentException(Errors.format(Errors.Keys.PropertyNotFound_2, |
| getRecordType().getTypeName(), name)); |
| } |
| if (value != null) { |
| final Class<?> valueClass = definition.getValueClass(index); |
| if (valueClass != null && !valueClass.isInstance(value)) { |
| throw new ClassCastException(Errors.format(Errors.Keys.IllegalPropertyValueClass_3, |
| name, valueClass, value.getClass())); |
| } |
| } |
| Array.set(values, index, value); |
| } |
| |
| /** |
| * Sets all attribute values in this record, in attribute order. |
| * |
| * @param newValues the attribute values. |
| * @throws IllegalArgumentException if the given number of values does not match the expected number. |
| * @throws ClassCastException if a value is not an instance of the expected type for this record. |
| */ |
| public void setAll(final Object... newValues) { |
| ArgumentChecks.ensureNonNull("values", newValues); |
| final int length = Array.getLength(values); |
| if (newValues.length != length) { |
| throw new IllegalArgumentException(Errors.format( |
| Errors.Keys.UnexpectedArrayLength_2, length, newValues.length)); |
| } |
| for (int i=0; i<newValues.length; i++) { |
| final Object value = newValues[i]; |
| if (value != null) { |
| final Class<?> valueClass = definition.getValueClass(i); |
| if (valueClass != null && !valueClass.isInstance(value)) { |
| throw new ClassCastException(Errors.format(Errors.Keys.IllegalPropertyValueClass_3, |
| definition.getName(i), valueClass, value.getClass())); |
| } |
| } |
| Array.set(values, i, value); |
| } |
| } |
| |
| /** |
| * Compares this record with the given object for equality. |
| * |
| * @param object the object to compare with this record for equality. |
| * @return {@code true} if both objects are equal. |
| */ |
| @Override |
| public boolean equals(final Object object) { |
| if (object == this) { |
| return true; // Slight optimization for a common case. |
| } |
| if (object != null && object.getClass() == getClass()) { |
| final DefaultRecord that = (DefaultRecord) object; |
| return definition.getRecordType().equals(that.definition.getRecordType()) && |
| Objects.deepEquals(values, that.values); |
| } |
| return false; |
| } |
| |
| /** |
| * Returns a hash code value for this record. |
| * |
| * @return a hash code value for this record. |
| */ |
| @Override |
| public int hashCode() { |
| return Utilities.deepHashCode(values) ^ definition.getRecordType().hashCode(); |
| } |
| |
| /** |
| * Returns a string representation of this record. |
| * The string representation is for debugging purpose and may change in any future SIS version. |
| * |
| * @return a string representation of this record. |
| */ |
| @Override |
| public String toString() { |
| return definition.toString("Record", values); |
| } |
| |
| |
| |
| |
| ////////////////////////////////////////////////////////////////////////////////////////////////// |
| //////// //////// |
| //////// XML support with JAXB //////// |
| //////// //////// |
| //////// The following methods are invoked by JAXB using reflection (even if //////// |
| //////// they are private) or are helpers for other methods invoked by JAXB. //////// |
| //////// Those methods can be safely removed if Geographic Markup Language //////// |
| //////// (GML) support is not needed. //////// |
| //////// //////// |
| ////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * Constructs an initially empty record expecting exactly one value as a string. |
| * See {@link #setValue(String)} for a description of the supported XML content. |
| */ |
| private DefaultRecord() { |
| definition = RecordSchemaSIS.STRING; |
| } |
| |
| /** |
| * Returns the record value as a string. |
| */ |
| @XmlValue |
| private String getValue() { |
| if (values != null) { |
| switch (Array.getLength(values)) { |
| case 0: break; |
| case 1: return String.valueOf(Array.get(values, 0)); |
| default: return definition.toString(null, values); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Sets the record value as a string. This method is invoked at unmarshalling time. |
| * A record can be anything, but usages that we have seen so far write a character |
| * sequence or a code list. Examples: |
| * |
| * {@preformat xml |
| * <gco:Record>Alphanumeric values: Product is alphanumeric.</gco:Record> |
| * <gco:Record>Alphanumeric Text: Message contains alphanumeric text.</gco:Record> |
| * <gco:Record>Part A: Reflectivity presented as a tabular listing of alphanumerics.</gco:Record> |
| * <gco:Record> |
| * <gmd:CodeListValue codelist="someURL#DataQualityAssessment" codeListValue="intermediate">intermediate</gmd:CodeListValue> |
| * </gco:Record> |
| * } |
| * |
| * @see <a href="https://issues.apache.org/jira/browse/SIS-419">SIS-419</a> |
| */ |
| private void setValue(String value) { |
| if (value != null && !(value = value.trim()).isEmpty()) { |
| values = new String[] {value}; |
| } |
| } |
| } |