blob: 082aaf6eafe6cc991a3f963232a6789f345b155b [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.feature.builder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import org.opengis.util.GenericName;
import org.opengis.util.NameFactory;
import org.opengis.util.FactoryException;
import org.apache.sis.feature.AbstractOperation;
import org.apache.sis.feature.DefaultFeatureType;
import org.apache.sis.feature.FeatureOperations;
import org.apache.sis.internal.system.DefaultFactories;
import org.apache.sis.internal.feature.AttributeConvention;
import org.apache.sis.util.CorruptedObjectException;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.ArraysExt;
// Branch-dependent imports
import org.apache.sis.internal.jdk7.JDK7;
import org.apache.sis.internal.jdk7.Objects;
import org.opengis.feature.AttributeType;
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
import org.opengis.feature.PropertyType;
import org.opengis.feature.FeatureAssociationRole;
/**
* Helper class for the creation of {@link FeatureType} instances.
* This builder can create the arguments to be given to the
* {@linkplain DefaultFeatureType#DefaultFeatureType feature type constructor}
* from simpler parameters given to this builder.
*
* <p>{@code FeatureTypeBuilder} should be short lived.
* After the {@code FeatureType} has been created, the builder should be discarded.</p>
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @since 0.8
* @version 0.8
* @module
*
* @see org.apache.sis.parameter.ParameterBuilder
*/
public class FeatureTypeBuilder extends TypeBuilder {
/**
* The factory to use for creating names.
*/
private final NameFactory nameFactory;
/**
* Builders for the properties (attributes, associations or operations) of this feature.
*/
private final List<PropertyTypeBuilder> properties;
/**
* The parent of the feature to create. By default, new features have no parent.
*/
private final List<FeatureType> superTypes;
/**
* Whether the feature type is abstract. The default value is {@code false}.
*
* @see #isAbstract()
* @see #setAbstract(boolean)
*/
private boolean isAbstract;
/**
* The default scope to use when {@link #name(String, String)} is invoked with a null scope.
*
* @see #getDefaultScope()
* @see #setDefaultScope(String)
*/
private String defaultScope;
/**
* The default minimum number of property values.
*
* @see #setDefaultCardinality(int, int)
*/
int defaultMinimumOccurs;
/**
* The default maximum number of property values.
*
* @see #setDefaultCardinality(int, int)
*/
int defaultMaximumOccurs;
/**
* An optional prefix or suffix to insert before or after the {@linkplain FeatureOperations#compound compound key}
* named {@code "@identifier"}.
*/
private String idPrefix, idSuffix;
/**
* The separator to insert between each single component in a {@linkplain FeatureOperations#compound compound key}
* named {@code "@identifier"}. This is ignored if {@link #identifierCount} is zero.
*/
private String idDelimiter;
/**
* Number of attribute that have been flagged as an identifier component.
*
* @see AttributeRole#IDENTIFIER_COMPONENT
* @see AttributeConvention#IDENTIFIER_PROPERTY
*/
int identifierCount;
/**
* The default geometry attribute, or {@code null} if none.
*
* @see AttributeRole#DEFAULT_GEOMETRY
* @see AttributeConvention#GEOMETRY_PROPERTY
*/
AttributeTypeBuilder<?> defaultGeometry;
/**
* The object created by this builder, or {@code null} if not yet created.
* This field must be cleared every time that a setter method is invoked on this builder.
*/
private transient FeatureType feature;
/**
* Creates a new builder instance using the default name factory.
*/
public FeatureTypeBuilder() {
this(null, null, null);
}
/**
* Creates a new builder instance using the given feature type as a template.
*
* @param template an existing feature type to use as a template, or {@code null} if none.
*/
public FeatureTypeBuilder(final FeatureType template) {
this(template, null, null);
}
/**
* Creates a new builder instance using the given name factory, template and locale for formatting error messages.
*
* @param template an existing feature type to use as a template, or {@code null} if none.
* @param factory the factory to use for creating names, or {@code null} for the default factory.
* @param locale the locale to use for formatting error messages, or {@code null} for the default locale.
*/
public FeatureTypeBuilder(final FeatureType template, NameFactory factory, final Locale locale) {
super(template, locale);
if (factory == null) {
factory = DefaultFactories.forBuildin(NameFactory.class);
}
nameFactory = factory;
properties = new ArrayList<PropertyTypeBuilder>();
superTypes = new ArrayList<FeatureType>();
idDelimiter = ":";
defaultMinimumOccurs = 1;
defaultMaximumOccurs = 1;
if (template != null) {
feature = template;
isAbstract = template.isAbstract();
superTypes.addAll(template.getSuperTypes());
for (final PropertyType p : template.getProperties(false)) {
final PropertyTypeBuilder builder;
if (p instanceof AttributeType<?>) {
builder = new AttributeTypeBuilder(this, (AttributeType<?>) p);
} else if (p instanceof FeatureAssociationRole) {
builder = new AssociationRoleBuilder(this, (FeatureAssociationRole) p);
} else {
continue; // Skip unknown types.
}
properties.add(builder);
}
}
}
/**
* If the {@code FeatureType} created by the last call to {@link #build()} has been cached,
* clears that cache. This method must be invoked every time that a setter method is invoked.
*/
@Override
final void clearCache() {
feature = null;
}
/**
* Returns {@code true} if the feature type to create will act as an abstract super-type.
* Abstract types can not be {@linkplain DefaultFeatureType#newInstance() instantiated}.
*
* @return {@code true} if the feature type to create will act as an abstract super-type.
*
* @see DefaultFeatureType#isAbstract()
*/
public boolean isAbstract() {
return isAbstract;
}
/**
* Sets whether the feature type to create will be abstract.
* If this method is not invoked, then the default value is {@code false}.
*
* @param isAbstract whether the feature type will be abstract.
* @return {@code this} for allowing method calls chaining.
*/
public FeatureTypeBuilder setAbstract(final boolean isAbstract) {
if (this.isAbstract != isAbstract) {
this.isAbstract = isAbstract;
clearCache();
}
return this;
}
/**
* Returns the direct parents of the feature type to create.
*
* @return the parents of the feature type to create, or an empty array if none.
*
* @see DefaultFeatureType#getSuperTypes()
*/
public FeatureType[] getSuperTypes() {
return superTypes.toArray(new FeatureType[superTypes.size()]);
}
/**
* Sets the parent types (or super-type) from which to inherit properties.
* If this method is not invoked, then the default value is to have no parent.
*
* @param parents the parent types from which to inherit properties, or an empty array if none.
* @return {@code this} for allowing method calls chaining.
*/
public FeatureTypeBuilder setSuperTypes(final FeatureType... parents) {
ensureNonNull("parents", parents);
final List<FeatureType> asList = Arrays.asList(parents);
if (!superTypes.equals(asList)) {
superTypes.clear();
superTypes.addAll(asList);
clearCache();
}
return this;
}
/**
* Sets the {@code FeatureType} name as a generic name.
* If another name was defined before this method call, that previous value will be discarded.
*
* <div class="note"><b>Note for subclasses:</b>
* all {@code setName(…)} convenience methods in this builder delegate to this method.
* Consequently this method can be used as a central place where to control the creation of all names.</div>
*
* @return {@code this} for allowing method calls chaining.
*/
@Override
public FeatureTypeBuilder setName(final GenericName name) {
super.setName(name);
return this;
}
/**
* Sets the {@code FeatureType} name as a simple string with the default scope.
* The default scope is the value specified by the last call to {@link #setDefaultScope(String)}.
* The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName local name} if no default scope
* has been specified, or a {@linkplain org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
*
* <p>This convenience method creates a {@link GenericName} instance,
* then delegates to {@link #setName(GenericName)}.</p>
*
* @return {@code this} for allowing method calls chaining.
*/
@Override
public FeatureTypeBuilder setName(final String localPart) {
super.setName(localPart);
return this;
}
/**
* Sets the {@code FeatureType} name as a string in the given scope.
* The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName local name} if the given scope is
* {@code null} or empty, or a {@linkplain org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
* If a {@linkplain #setDefaultScope(String) default scope} has been specified, then the
* {@code scope} argument overrides it.
*
* <p>This convenience method creates a {@link GenericName} instance,
* then delegates to {@link #setName(GenericName)}.</p>
*
* @return {@code this} for allowing method calls chaining.
*/
@Override
public FeatureTypeBuilder setName(final String scope, final String localPart) {
super.setName(scope, localPart);
return this;
}
/**
* Invoked by {@link TypeBuilder} for creating new {@code LocalName} or {@code GenericName} instances.
*/
@Override
final GenericName name(String scope, final String localPart) {
if (scope == null) {
scope = getDefaultScope();
}
if (scope == null || scope.isEmpty()) {
return nameFactory.createLocalName(null, localPart);
} else {
return nameFactory.createGenericName(null, scope, localPart);
}
}
/**
* Returns the scope of the names created by {@code setName(String)} method calls.
*
* @return the scope to use by default when {@link #setName(String)} is invoked.
*/
public String getDefaultScope() {
return defaultScope;
}
/**
* Sets the scope of the next names created by {@code setName(String)} method calls.
* This method applies only to the next calls to {@code setName(String)};
* the result of all previous calls stay unmodified.
*
* <p>There is different conventions about the use of name scopes. ISO 19109 suggests that the scope of all
* {@code AttributeType} names is the name of the enclosing {@code FeatureType}, but this is not mandatory.
* Users who want to apply this convention can invoke {@code setDefaultScope(featureName)} after
* <code>{@linkplain #setName(String) FeatureTypeBuilder.setName}(featureName)</code> but before
* <code>{@linkplain AttributeTypeBuilder#setName(String) AttributeTypeBuilder.setName}(attributeName)</code>.</p>
*
* @param scope the new default scope, or {@code null} if none.
* @return {@code this} for allowing method calls chaining.
*/
public FeatureTypeBuilder setDefaultScope(final String scope) {
defaultScope = scope;
// No need to clear the cache because this change affects
// only the next names to be created, not the existing ones.
return this;
}
/**
* Sets the default minimum and maximum number of next attributes and associations to add.
* Those defaults will applied to newly created attributes or associations,
* for example in next calls to {@link #addAttribute(Class)}.
* Attributes and associations added before this method call are not modified.
*
* <p>If this method is not invoked, then the default cardinality is [1 … 1].</p>
*
* @param minimumOccurs new default minimum number of property values.
* @param maximumOccurs new default maximum number of property values.
* @return {@code this} for allowing method calls chaining.
*
* @see AttributeTypeBuilder#setCardinality(int, int)
*/
public FeatureTypeBuilder setDefaultCardinality(final int minimumOccurs, final int maximumOccurs) {
if (minimumOccurs < 0 || maximumOccurs < minimumOccurs) {
throw new IllegalArgumentException(errors().getString(Errors.Keys.IllegalRange_2, minimumOccurs, maximumOccurs));
}
defaultMinimumOccurs = minimumOccurs;
defaultMaximumOccurs = maximumOccurs;
// No need to clear the cache because this change affects only
// the next properties to be created, not the existing ones.
return this;
}
/**
* Sets the prefix, suffix and delimiter to use when formatting a compound identifier made of two or more attributes.
* The delimiter will be used only if at least two attributes have the {@linkplain AttributeRole#IDENTIFIER_COMPONENT
* identifier component role}.
*
* <p>If this method is not invoked, then the default values are the {@code ":"} delimiter and no prefix or suffix.</p>
*
* @param delimiter the characters to use as delimiter between each single property value.
* @param prefix characters to use at the beginning of the concatenated string, or {@code null} if none.
* @param suffix characters to use at the end of the concatenated string, or {@code null} if none.
* @return {@code this} for allowing method calls chaining.
*
* @see AttributeRole#IDENTIFIER_COMPONENT
* @see FeatureOperations#compound(Map, String, String, String, PropertyType...)
*/
public FeatureTypeBuilder setIdentifierDelimiters(final String delimiter, final String prefix, final String suffix) {
ensureNonEmpty("delimiter", delimiter);
if (!delimiter.equals(idDelimiter) || !Objects.equals(prefix, idPrefix) || !Objects.equals(suffix, idSuffix)) {
idDelimiter = delimiter;
idPrefix = prefix;
idSuffix = suffix;
clearCache();
}
return this;
}
/**
* Returns a view of all attributes and associations added to the {@code FeatureType} to build.
* The returned list is <cite>live</cite>: changes in this builder are reflected in that list and conversely.
* However the returned list allows only {@linkplain List#remove(Object) remove} operations;
* new attributes or associations can be added only by calls to one of the {@code addAttribute(…)}
* or {@code addAssociation(…)} methods.
*
* @return a live list over the properties declared to this builder.
*
* @see #addAttribute(Class)
* @see #addAttribute(AttributeType)
* @see #addAssociation(FeatureType)
* @see #addAssociation(GenericName)
* @see #addAssociation(FeatureAssociationRole)
*/
public List<PropertyTypeBuilder> properties() {
return new RemoveOnlyList<PropertyTypeBuilder>(properties);
}
/**
* Creates a new {@code AttributeType} builder for values of the given class.
* The default attribute name is the name of the given type, but callers should invoke one
* of the {@code AttributeTypeBuilder.setName(…)} methods on the returned instance with a better name.
*
* <p>Usage example:</p>
* {@preformat java
* builder.addAttribute(String.class).setName("City").setDefaultValue("Metropolis");
* }
*
* The value class can not be {@code Feature.class} since features shall be handled
* as {@linkplain #addAssociation(FeatureType) associations} instead than attributes.
*
* @param <V> the compile-time value of {@code valueClass} argument.
* @param valueClass the class of attribute values (can not be {@code Feature.class}).
* @return a builder for an {@code AttributeType}.
*
* @see #properties()
*/
public <V> AttributeTypeBuilder<V> addAttribute(final Class<V> valueClass) {
ensureNonNull("valueClass", valueClass);
if (Feature.class.isAssignableFrom(valueClass)) {
// We disallow Feature.class because that type shall be handled as association instead than attribute.
throw new IllegalArgumentException(errors().getString(Errors.Keys.IllegalArgumentValue_2, "valueClass", valueClass));
}
final AttributeTypeBuilder<V> property = new AttributeTypeBuilder<V>(this, valueClass);
properties.add(property);
clearCache();
return property;
}
/**
* Creates a new {@code AttributeType} builder initialized to the same characteristics than the given template.
*
* @param <V> the compile-time type of values in the {@code template} argument.
* @param template an existing attribute type to use as a template.
* @return a builder for an {@code AttributeType}, initialized with the values of the given template.
*
* @see #properties()
*/
public <V> AttributeTypeBuilder<V> addAttribute(final AttributeType<V> template) {
ensureNonNull("template", template);
final AttributeTypeBuilder<V> property = new AttributeTypeBuilder<V>(this, template);
properties.add(property);
clearCache();
return property;
}
/**
* Creates a new {@code FeatureAssociationRole} builder for features of the given type.
* The default association name is the name of the given type, but callers should invoke one
* of the {@code AssociationRoleBuilder.setName(…)} methods on the returned instance with a better name.
*
* @param type the type of feature values.
* @return a builder for a {@code FeatureAssociationRole}.
*
* @see #properties()
*/
public AssociationRoleBuilder addAssociation(final FeatureType type) {
ensureNonNull("type", type);
final AssociationRoleBuilder property = new AssociationRoleBuilder(this, type, type.getName());
properties.add(property);
clearCache();
return property;
}
/**
* Creates a new {@code FeatureAssociationRole} builder for features of a type of the given name.
* This method can be invoked as an alternative to {@link #addAssociation(FeatureType)} when the
* {@code FeatureType} instance is not yet available because of cyclic dependency.
*
* @param type the name of the type of feature values.
* @return a builder for a {@code FeatureAssociationRole}.
*
* @see #properties()
*/
public AssociationRoleBuilder addAssociation(final GenericName type) {
ensureNonNull("type", type);
final AssociationRoleBuilder property = new AssociationRoleBuilder(this, null, type);
properties.add(property);
clearCache();
return property;
}
/**
* Creates a new {@code FeatureAssociationRole} builder initialized to the same characteristics
* than the given template.
*
* @param template an existing feature association to use as a template.
* @return a builder for an {@code FeatureAssociationRole}, initialized with the values of the given template.
*
* @see #properties()
*/
public AssociationRoleBuilder addAssociation(final FeatureAssociationRole template) {
ensureNonNull("template", template);
final AssociationRoleBuilder property = new AssociationRoleBuilder(this, template);
properties.add(property);
clearCache();
return property;
}
/**
* {@inheritDoc}
*/
@Override
public FeatureTypeBuilder setDefinition(final CharSequence definition) {
super.setDefinition(definition);
return this;
}
/**
* {@inheritDoc}
*/
@Override
public FeatureTypeBuilder setDesignation(final CharSequence designation) {
super.setDesignation(designation);
return this;
}
/**
* {@inheritDoc}
*/
@Override
public FeatureTypeBuilder setDescription(final CharSequence description) {
super.setDescription(description);
return this;
}
/**
* Builds the feature type from the information and properties specified to this builder.
* One of the {@code setName(…)} methods must have been invoked before this {@code build()} method (mandatory).
* All other methods are optional, but some calls to a {@code add} method are usually needed.
*
* @return the new feature type.
* @throws IllegalStateException if the feature type contains incompatible
* {@linkplain AttributeTypeBuilder#setCRS CRS characteristics}.
*/
public FeatureType build() throws IllegalStateException {
if (feature == null) {
/*
* Creates an initial array of property types with up to 3 slots reserved for @identifier, @geometry
* and @envelope operations. At first we presume that there is always an identifier. The identifier
* slot will be removed later if there is none.
*/
final int numSpecified = properties.size(); // Number of explicitely specified properties.
int numSynthetic; // Number of synthetic properties that may be generated.
int envelopeIndex = -1;
int geometryIndex = -1;
final PropertyType[] identifierTypes;
if (identifierCount == 0) {
numSynthetic = 0;
identifierTypes = null;
} else {
numSynthetic = 1;
identifierTypes = new PropertyType[identifierCount];
}
if (defaultGeometry != null) {
envelopeIndex = numSynthetic;
geometryIndex = numSynthetic + 1;
numSynthetic += 2;
}
final PropertyType[] propertyTypes = new PropertyType[numSynthetic + numSpecified];
int propertyCursor = numSynthetic;
int identifierCursor = 0;
for (int i=0; i<numSpecified; i++) {
final PropertyTypeBuilder builder = properties.get(i);
final PropertyType instance = builder.build();
propertyTypes[propertyCursor] = instance;
/*
* Collect the attributes to use as identifier components while we loop over all properties.
* A NullPointerException or an ArrayIndexOutOfBoundsException in this block would mean that
* identifierCount field has not been updated correctly by an addRole(AttributeRole) method.
*/
if (builder.isIdentifier()) {
identifierTypes[identifierCursor++] = instance;
}
/*
* If there is a default geometry, add a link named "@geometry" to that geometry.
* It may happen that the property created by the user is already named "@geometry",
* in which case we will avoid to duplicate the property.
*/
if (builder == defaultGeometry) {
if (propertyTypes[geometryIndex] != null) {
// Assuming that there is no bug in our implementation, this error could happen if the user
// has modified this FeatureTypeBuilder in another thread during this build() execution.
throw new CorruptedObjectException();
}
if (AttributeConvention.GEOMETRY_PROPERTY.equals(instance.getName())) {
System.arraycopy(propertyTypes, geometryIndex, propertyTypes, geometryIndex-1, (numSynthetic - geometryIndex) + i);
geometryIndex = -1;
numSynthetic--;
continue; // Skip the increment of propertyCursor.
}
propertyTypes[geometryIndex] = FeatureOperations.link(name(AttributeConvention.GEOMETRY_PROPERTY), instance);
}
propertyCursor++;
}
/*
* Create the "envelope" operation only after we created all other properties.
* Actually it is okay if the 'propertyTypes' array still contains null elements not needed for envelope calculation
* like "@identifier", since FeatureOperations.envelope(…) constructor ignores any property which is not for a value.
*/
if (envelopeIndex >= 0) try {
propertyTypes[envelopeIndex] = FeatureOperations.envelope(name(AttributeConvention.ENVELOPE_PROPERTY), null, propertyTypes);
} catch (FactoryException e) {
throw new IllegalStateException(e);
}
/*
* If a synthetic identifier need to be created, create it now as the first property.
* It may happen that the user provided a single identifier component already named
* "@identifier", in which case we avoid to duplicate the property.
*/
if (identifierTypes != null) {
if (identifierCursor != identifierTypes.length) {
// Assuming that there is no bug in our implementation, this error could happen if the user
// has modified this FeatureTypeBuilder in another thread during this build() execution.
throw new CorruptedObjectException();
}
if (identifierCursor == 1 && AttributeConvention.IDENTIFIER_PROPERTY.equals(identifierTypes[0].getName())) {
System.arraycopy(propertyTypes, 1, propertyTypes, 0, --propertyCursor);
} else {
propertyTypes[0] = FeatureOperations.compound(name(AttributeConvention.IDENTIFIER_PROPERTY),
idDelimiter, idPrefix, idSuffix, identifierTypes);
}
}
feature = new DefaultFeatureType(identification(), isAbstract(),
superTypes.toArray(new FeatureType[superTypes.size()]),
ArraysExt.resize(propertyTypes, propertyCursor));
}
return feature;
}
/**
* Helper method for creating identification info of synthetic attributes.
*/
static Map<String,?> name(final GenericName name) {
return Collections.singletonMap(AbstractOperation.NAME_KEY, name);
}
/**
* Formats a string representation of this builder for debugging purpose.
*/
@Override
final void toStringInternal(final StringBuilder buffer) {
if (isAbstract()) {
buffer.insert(buffer.indexOf("[") + 1, "abstract ");
}
String separator = " : ";
for (final FeatureType parent : superTypes) {
buffer.append(separator).append('“').append(parent.getName()).append('”');
separator = ", ";
}
buffer.append(" {");
separator = JDK7.lineSeparator();
for (final PropertyTypeBuilder p : properties) {
p.toString(buffer.append(separator).append(" ").append(p.getClass().getSimpleName()));
}
buffer.append(separator).append('}');
}
}