| /* |
| * 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); |
| } |
| } |
| } |