blob: 0747de265ef2d002223cd6c7486075933490b67f [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.Set;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.Arrays;
import java.util.Objects;
import java.io.Serializable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.InvalidObjectException;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlValue;
import org.opengis.util.Type;
import org.opengis.util.TypeName;
import org.opengis.util.LocalName;
import org.opengis.util.MemberName;
import org.opengis.util.GenericName;
import org.opengis.util.NameSpace;
import org.opengis.util.Record;
import org.opengis.util.RecordType;
import org.opengis.util.RecordSchema;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.collection.Containers;
import org.apache.sis.util.ObjectConverters;
import org.apache.sis.internal.converter.SurjectiveConverter;
import org.apache.sis.internal.metadata.RecordSchemaSIS;
/**
* An immutable definition of the type of a {@linkplain DefaultRecord record}.
* A {@code RecordType} is identified by a {@linkplain #getTypeName() type name} and contains an
* arbitrary amount of {@linkplain #getMembers() members} as (<var>name</var>, <var>type</var>) pairs.
* A {@code RecordType} may therefore contain another {@code RecordType} as a member.
*
* <div class="note"><b>Comparison with Java reflection:</b>
* {@code RecordType} instances can be though as equivalent to instances of the Java {@link Class} class.
* The set of members in a {@code RecordType} can be though as equivalent to the set of fields in a class.
* </div>
*
* <div class="section">Instantiation</div>
* The easiest way to create {@code DefaultRecordType} instances is to use the
* {@link DefaultRecordSchema#createRecordType(CharSequence, Map)} method.
* Example:
*
* <div class="note">
* {@preformat java
* DefaultRecordSchema schema = new DefaultRecordSchema(null, null, "MySchema");
* // The same instance can be reused for all records to create in that schema.
*
* Map<CharSequence,Class<?>> members = new LinkedHashMap<>();
* members.put("city", String .class);
* members.put("latitude", Double .class);
* members.put("longitude", Double .class);
* members.put("population", Integer.class);
* RecordType record = schema.createRecordType("MyRecordType", members);
* }
* </div>
*
* <div class="section">Immutability and thread safety</div>
* This class is immutable and thus inherently thread-safe if the {@link TypeName}, the {@link RecordSchema}
* and all ({@link MemberName}, {@link Type}) entries in the map given to the constructor are also immutable.
* Subclasses shall make sure that any overridden methods remain safe to call from multiple threads and do not change
* any public {@code RecordType} state.
*
* <div class="section">Serialization</div>
* This class is serializable if all elements given to the constructor are also serializable.
* Note in particular that {@link DefaultRecordSchema} is currently <strong>not</strong> serializable,
* so users wanting serialization may need to provide their own schema.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 1.0
*
* @see DefaultRecord
* @see DefaultRecordSchema
* @see DefaultMemberName
*
* @since 0.3
* @module
*/
@XmlType(name = "RecordType")
public class DefaultRecordType extends RecordDefinition implements RecordType, Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -1534515712654429099L;
/**
* The name that identifies this record type.
*
* @see #getTypeName()
*/
private final TypeName typeName;
/**
* The schema that contains this record type.
*
* @see #getContainer()
*/
private final RecordSchema container;
/**
* The type of each members.
*
* @see #getMemberTypes()
*/
private transient Type[] memberTypes;
/**
* Creates a new record with the same names and members than the given one.
*
* @param other the {@code RecordType} to copy.
*/
public DefaultRecordType(final RecordType other) {
typeName = other.getTypeName();
container = other.getContainer();
memberTypes = computeTransientFields(other.getMemberTypes());
}
/**
* Creates a new record in the given schema.
* It is caller responsibility to add the new {@code RecordType} in the container
* {@linkplain RecordSchema#getDescription() description} map, if desired.
*
* <p>This constructor is provided mostly for developers who want to create {@code DefaultRecordType}
* instances in their own {@code RecordSchema} implementation. Otherwise if the default record schema
* implementation is sufficient, the {@link DefaultRecordSchema#createRecordType(CharSequence, Map)}
* method provides an easier alternative.</p>
*
* @param typeName the name that identifies this record type.
* @param container the schema that contains this record type.
* @param members the name and type of the members to be included in this record type.
*
* @see DefaultRecordSchema#createRecordType(CharSequence, Map)
*/
public DefaultRecordType(final TypeName typeName, final RecordSchema container,
final Map<? extends MemberName, ? extends Type> members)
{
ArgumentChecks.ensureNonNull("typeName", typeName);
ArgumentChecks.ensureNonNull("container", container);
ArgumentChecks.ensureNonNull("members", members);
this.typeName = typeName;
this.container = container;
this.memberTypes = computeTransientFields(members);
/*
* Ensure that the record namespace is equals to the schema name. For example if the schema
* name is "MyNameSpace", then the record type name can be "MyNameSpace:MyRecordType".
*/
final LocalName schemaName = container.getSchemaName();
final GenericName fullTypeName = typeName.toFullyQualifiedName();
if (schemaName.compareTo(typeName.scope().name().tip()) != 0) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.InconsistentNamespace_2, schemaName, fullTypeName));
}
final int size = size();
for (int i=0; i<size; i++) {
final MemberName name = getName(i);
final Type type = this.memberTypes[i];
if (type == null || name.getAttributeType().compareTo(type.getTypeName()) != 0) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalMemberType_2, name, type));
}
if (fullTypeName.compareTo(name.scope().name()) != 0) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.InconsistentNamespace_2,
fullTypeName, name.toFullyQualifiedName()));
}
}
}
/**
* Creates a new record from member names specified as character sequence.
* This constructor builds the {@link MemberName} instance itself.
*
* @param typeName the name that identifies this record type.
* @param container the schema that contains this record type.
* @param members the name of the members to be included in this record type.
* @param nameFactory the factory to use for instantiating {@link MemberName}.
*/
DefaultRecordType(final TypeName typeName, final RecordSchema container,
final Map<? extends CharSequence, ? extends Type> members, final DefaultNameFactory nameFactory)
{
this.typeName = typeName;
this.container = container;
final NameSpace namespace = nameFactory.createNameSpace(typeName, null);
final Map<MemberName,Type> memberTypes = new LinkedHashMap<>(Containers.hashMapCapacity(members.size()));
for (final Map.Entry<? extends CharSequence, ? extends Type> entry : members.entrySet()) {
final Type type = entry.getValue();
final CharSequence name = entry.getKey();
final MemberName member = nameFactory.createMemberName(namespace, name, type.getTypeName());
if (memberTypes.put(member, type) != null) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.DuplicatedElement_1, member));
}
}
this.memberTypes = computeTransientFields(memberTypes);
}
/**
* Invoked on deserialization for restoring the transient fields.
* See {@link #writeObject(ObjectOutputStream)} for the stream data description.
*
* @param in the input stream from which to deserialize an object.
* @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();
final int size = in.readInt();
final Map<MemberName,Type> members = new LinkedHashMap<>(Containers.hashMapCapacity(size));
for (int i=0; i<size; i++) {
final MemberName member = (MemberName) in.readObject();
final Type type = (Type) in.readObject();
if (members.put(member, type) != null) {
throw new InvalidObjectException(Errors.format(Errors.Keys.DuplicatedElement_1, member));
}
}
memberTypes = computeTransientFields(members);
}
/**
* Invoked on serialization for writing the member names and their type.
*
* @param out the output stream where to serialize this object.
* @throws IOException if an I/O error occurred while writing.
*
* @serialData the number of members as an {@code int}, followed by a
* ({@code MemberName}, {@code Type}) pair for each member.
*/
private void writeObject(final ObjectOutputStream out) throws IOException {
final int size = size();
out.defaultWriteObject();
out.writeInt(size);
for (int i=0; i<size; i++) {
out.writeObject(getName(i));
out.writeObject(memberTypes[i]);
}
}
/**
* 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 DefaultRecordType},
* then it is returned unchanged.</li>
* <li>Otherwise a new {@code DefaultRecordType} instance is created using the
* {@linkplain #DefaultRecordType(RecordType) 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}.
*/
public static DefaultRecordType castOrCopy(final RecordType other) {
if (other == null || other instanceof DefaultRecordType) {
return (DefaultRecordType) other;
} else {
return new DefaultRecordType(other);
}
}
/**
* Returns {@code this} since {@link RecordDefinition} is the definition of this record type.
*/
@Override
final RecordType getRecordType() {
return this;
}
/**
* Returns the name that identifies this record type. If this {@code RecordType} is contained in a
* {@linkplain DefaultRecordSchema record schema}, then the record type name shall be valid in the
* {@linkplain DefaultNameSpace name space} of the record schema:
*
* {@preformat java
* NameSpace namespace = getContainer().getSchemaName().scope()
* }
*
* <div class="note"><b>Comparison with Java reflection:</b>
* If we think about this {@code RecordType} as equivalent to a {@code Class} instance,
* then this method can be think as the equivalent of the Java {@link Class#getName()} method.
* </div>
*
* @return the name that identifies this record type.
*/
@Override
public TypeName getTypeName() {
return typeName;
}
/**
* Returns the schema that contains this record type.
*
* @return the schema that contains this record type.
*/
@Override
public RecordSchema getContainer() {
return container;
}
/**
* Returns the dictionary of all (<var>name</var>, <var>type</var>) pairs in this record type.
* The returned map is unmodifiable.
*
* <div class="note"><b>Comparison with Java reflection:</b>
* If we think about this {@code RecordType} as equivalent to a {@code Class} instance, then
* this method can be though as the related to the Java {@link Class#getFields()} method.
* </div>
*
* @return the dictionary of (<var>name</var>, <var>type</var>) pairs, or an empty map if none.
*/
@Override
public Map<MemberName,Type> getMemberTypes() {
return ObjectConverters.derivedValues(memberIndices(), MemberName.class, new SurjectiveConverter<Integer,Type>() {
@Override public Class<Integer> getSourceClass() {return Integer.class;}
@Override public Class<Type> getTargetClass() {return Type.class;}
@Override public Type apply(final Integer index) {return getType(index);}
});
}
/**
* Returns the set of attribute names defined in this {@code RecordType}'s dictionary.
* This method is functionally equivalent to:
*
* {@preformat java
* getMemberTypes().keySet();
* }
*
* @return the set of attribute names, or an empty set if none.
*/
@Override
public Set<MemberName> getMembers() {
return memberIndices().keySet();
}
/**
* Returns the type at the given index.
*/
final Type getType(final int index) {
return memberTypes[index];
}
/**
* Returns the type associated to the given attribute name, or {@code null} if none.
* This method is functionally equivalent to (omitting the check for null value):
*
* {@preformat java
* getMemberTypes().get(memberName).getTypeName();
* }
*
* <div class="note"><b>Comparison with Java reflection:</b>
* If we think about this {@code RecordType} as equivalent to a {@code Class} instance, then
* this method can be though as related to the Java {@link Class#getField(String)} method.
* </div>
*
* @param memberName the attribute name for which to get the associated type name.
* @return the associated type name, or {@code null} if none.
*/
@Override
public TypeName locate(final MemberName memberName) {
final Integer index = indexOf(memberName);
return (index != null) ? getType(index).getTypeName() : null;
}
/**
* Determines if the given record is compatible with this record type. This method returns {@code true}
* if the given {@code record} argument is non-null and the following condition holds:
*
* {@preformat java
* Set<MemberName> attributeNames = record.getAttributes().keySet();
* boolean isInstance = getMembers().containsAll(attributeNames);
* }
*
* <div class="note"><b>Implementation note:</b>
* We do not require that {@code record.getRecordType() == this} in order to allow record
* "sub-types" to define additional fields, in a way similar to Java sub-classing.</div>
*
* @param record the record to test for compatibility.
* @return {@code true} if the given record is compatible with this {@code RecordType}.
*/
@Override
public boolean isInstance(final Record record) {
return (record != null) && getMembers().containsAll(record.getAttributes().keySet());
}
/**
* Compares the given object with this {@code RecordType} for equality.
*
* @param other the object to compare with this {@code RecordType}.
* @return {@code true} if both objects are equal.
*/
@Override
public boolean equals(final Object other) {
if (other == this) {
return true;
}
if (other != null && other.getClass() == getClass()) {
final DefaultRecordType that = (DefaultRecordType) other;
return Objects.equals(typeName, that.typeName) &&
Objects.equals(container, that.container) &&
Arrays .equals(memberTypes, that.memberTypes) &&
memberIndices().equals(that.memberIndices());
}
return false;
}
/**
* Returns a hash code value for this {@code RecordType}.
*/
@Override
public int hashCode() {
return Objects.hashCode(typeName) + 31*(memberIndices().hashCode() + 31*Arrays.hashCode(memberTypes));
}
//////////////////////////////////////////////////////////////////////////////////////////////////
//////// ////////
//////// 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 type describing exactly one value as a string.
* See {@link #setValue(String)} for a description of the supported XML content.
*/
@SuppressWarnings("unused")
private DefaultRecordType() {
final DefaultRecordType type = RecordSchemaSIS.STRING;
typeName = type.typeName;
container = type.container;
}
/**
* Returns the record type value as a string. Current implementation returns the members with
* one member per line, but it may change in any future version for adapting to common practice.
*/
@XmlValue
private String getValue() {
switch (size()) {
case 0: return null;
case 1: return String.valueOf(memberTypes[0]);
default: return toString(null, null);
}
}
/**
* Sets the record type value as a string. Current implementation expect one member per line.
* A record can be anything, but usages that we have seen so far write a character sequence
* of what seems <var>key</var>-<var>description</var> pairs. Examples:
*
* {@preformat xml
* <gco:RecordType>
* General Meteorological Products: General products include the baseline reflectivity and velocity,
* and also algorithmic graphic products spectrum width, vertical integrated liquid, and VAD wind profile.
* </gco:RecordType>
* }
*
* @see <a href="https://issues.apache.org/jira/browse/SIS-419">SIS-419</a>
*/
private void setValue(final String value) {
if (value != null) {
final Map<MemberName,Type> members = new LinkedHashMap<>();
for (CharSequence element : CharSequences.splitOnEOL(value)) {
final int s = ((String) element).indexOf(':');
if (s >= 0) {
element = element.subSequence(0, CharSequences.skipTrailingWhitespaces(element, 0, s));
// TODO: the part after ":" is the description. For now, we have no room for storing it.
}
final MemberName m = Names.createMemberName(null, null, element, String.class);
members.put(m, RecordSchemaSIS.INSTANCE.toAttributeType(String.class));
}
memberTypes = computeTransientFields(members);
}
}
}