blob: cad46829f29129a765cb23ec44cec819492136b4 [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.LinkedHashMap;
import java.io.Serializable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Array;
import javax.xml.bind.annotation.XmlTransient;
import org.opengis.util.Type;
import org.opengis.util.RecordType;
import org.opengis.util.MemberName;
import org.opengis.feature.AttributeType;
import org.apache.sis.util.Classes;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.collection.Containers;
import org.apache.sis.internal.util.CollectionsExt;
/**
* Holds a {@code Record} definition in a way more convenient for Apache SIS than
* what the {@code RecordType} interface provides.
*
* <h2>Serialization</h2>
* This base class is intentionally not serializable, and all private fields are marked as transient for making
* this decision more visible. This is because the internal details of this class are quite arbitrary, so we do
* not want to expose them in serialization for compatibility reasons. Furthermore some information are redundant,
* so a serialization performed by subclasses may be more compact. Serialization of all necessary data shall be
* performed by subclasses, and the transient fields shall be reconstructed by a call to
* {@link #computeTransientFields(Map)}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 0.5
* @module
*/
@XmlTransient
abstract class RecordDefinition { // Intentionally not Serializable.
/**
* {@code RecordDefinition} implementation used as a fallback when the user-supplied {@link RecordType}
* is not an instance of {@link DefaultRecordType}. So this adapter is used only if Apache SIS is mixed
* with other implementations.
*
* <h4>Serialization</h4>
* This class is serializable if the {@code RecordType} given to the constructor is also serializable.
*/
static final class Adapter extends RecordDefinition implements Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 3739362257927222288L;
/**
* The wrapped record type.
*/
private final RecordType recordType; // This is the only serialized field in this file.
/**
* Creates a new adapter for the given record type.
*/
Adapter(final RecordType recordType) {
this.recordType = recordType;
computeTransientFields(recordType.getMemberTypes());
}
/**
* Invoked on deserialization for restoring the transient fields.
*
* @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();
computeTransientFields(recordType.getMemberTypes());
}
/**
* Returns the wrapped record type.
*/
@Override
RecordType getRecordType() {
return recordType;
}
}
/**
* Indices of member names. Created on construction or deserialization,
* and shall be considered final and unmodifiable after that point.
*
* @see #memberIndices()
*/
private transient Map<MemberName,Integer> memberIndices;
/**
* Member names. Created on construction or deserialization, and shall be considered
* final and unmodifiable after that point.
*/
private transient MemberName[] members;
/**
* Classes of expected values, or {@code null} if there is no restriction. Created on
* construction or deserialization, and shall be considered final and unmodifiable after that point.
*/
private transient Class<?>[] valueClasses;
/**
* The common parent of all value classes. May be a primitive type.
*/
private transient Class<?> baseValueClass;
/**
* Creates a new instance. Subclasses shall invoke {@link #computeTransientFields(Map)} in their constructor.
*/
RecordDefinition() {
}
/**
* Returns the record type for which this object is a definition.
*/
abstract RecordType getRecordType();
/**
* Invoked on construction or deserialization for computing the transient fields.
*
* @param memberTypes the (<var>name</var>, <var>type</var>) pairs in this record type.
* @return the values in the given map. This information is not stored in {@code RecordDefinition}
* because not needed by this class, but the {@link DefaultRecordType} subclass will store it.
*/
final Type[] computeTransientFields(final Map<? extends MemberName, ? extends Type> memberTypes) {
final int size = memberTypes.size();
members = new MemberName[size];
memberIndices = new LinkedHashMap<>(Containers.hashMapCapacity(size));
final Type[] types = new Type[size];
int i = 0;
for (final Map.Entry<? extends MemberName, ? extends Type> entry : memberTypes.entrySet()) {
final Type type = entry.getValue();
if (type instanceof AttributeType) {
final Class<?> c = ((AttributeType) type).getValueClass();
if (c != Object.class) {
if (valueClasses == null) {
valueClasses = new Class<?>[size];
}
valueClasses[i] = c;
baseValueClass = Classes.findCommonClass(baseValueClass, c);
}
}
final MemberName name = entry.getKey();
members[i] = name;
memberIndices.put(name, i);
types[i] = type;
i++;
}
memberIndices = CollectionsExt.unmodifiableOrCopy(memberIndices);
baseValueClass = (baseValueClass != null) ? Numbers.wrapperToPrimitive(baseValueClass) : Object.class;
return types;
}
/**
* Returns the common parent of all value classes. May be a primitive type.
*/
final Class<?> baseValueClass() {
return baseValueClass;
}
/**
* Read-only access to the map of member indices.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
final Map<MemberName,Integer> memberIndices() {
return memberIndices;
}
/**
* Returns the number of elements in records.
*/
final int size() {
// 'members' should not be null, but let be safe.
return (members != null) ? members.length : 0;
}
/**
* Returns the index of the given name, or {@code null} if none.
*/
final Integer indexOf(final MemberName memberName) {
return memberIndices.get(memberName);
}
/**
* Returns the name of the member at the given index.
*/
final MemberName getName(final int index) {
return members[index];
}
/**
* Returns the expected class of values in the given column, or {@code null} if unspecified.
*/
final Class<?> getValueClass(final int index) {
return (valueClasses != null) ? valueClasses[index] : null;
}
/**
* Returns a string representation of this object.
* The string representation is for debugging purpose and may change in any future SIS version.
*
* @return a string representation of this record type.
*/
@Override
public String toString() {
return toString("RecordType", null);
}
/**
* Returns a string representation of a {@code Record} or {@code RecordType}.
*
* @param head either {@code "Record"} or {@code "RecordType"} or {@code null}.
* @param values the values as an array, or {@code null} for writing the types instead.
* @return the string representation.
*/
final String toString(final String head, final Object values) {
final StringBuilder buffer = new StringBuilder(250);
final String lineSeparator = System.lineSeparator();
final String[] names = new String[size()];
final String margin;
int width = 0;
for (int i=0; i<names.length; i++) {
width = Math.max(width, (names[i] = members[i].toString()).length());
}
if (head == null) {
width = 0; // Ignore the width computation, but we still need the names in the array.
margin = "";
} else {
buffer.append(head).append("[“").append(getRecordType().getTypeName()).append("”] {").append(lineSeparator);
margin = " ";
}
for (int i=0; i<names.length; i++) {
final String name = names[i];
buffer.append(margin).append(name);
final Object value = (values != null) ? Array.get(values, i) : members[i].getAttributeType();
if (value != null) {
buffer.append(CharSequences.spaces(width - name.length())).append(" : ").append(value);
}
buffer.append(lineSeparator);
}
if (head != null) {
buffer.append('}').append(lineSeparator);
}
return buffer.toString();
}
}