blob: e4650f69838707491bc13a847997cc45c52dbe06 [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.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.
*
* <div class="section">Limitations</div>
* <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};
}
}
}