blob: 556c88b18a320458ef2328f6e365eef829f040f3 [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.Map;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import org.opengis.util.ScopedName;
import org.opengis.util.GenericName;
import org.opengis.util.InternationalString;
import org.apache.sis.internal.feature.Resources;
import org.apache.sis.feature.AbstractIdentifiedType;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.NullArgumentException;
import org.apache.sis.util.Deprecable;
import org.apache.sis.util.Localized;
import org.apache.sis.util.Classes;
/**
* Information common to all kind of types (feature, association, characteristics).
* Those information are:
*
* <ul>
* <li>the name — a unique name which can be defined within a scope (or namespace).</li>
* <li>the definition — a concise definition of the element.</li>
* <li>the designation — a natural language designator for the element for user interfaces.</li>
* <li>the description — information beyond that required for concise definition of the element.</li>
* </ul>
*
* The name is mandatory and can be specified as either {@link org.opengis.util.LocalName},
* {@link org.opengis.util.ScopedName}, {@link String} or {@link InternationalString} instance.
* All other properties are optional.
*
* <div class="section">Default namespace</div>
* In many cases, the names of all {@code AttributeType}s and {@code AssociationRole}s to create
* within a {@code FeatureType} share the same namespace.
* For making name creations more convenient, the namespace can be
* {@linkplain FeatureTypeBuilder#setNameSpace specified once} and applied automatically
* to all names created by the {@link #setName(CharSequence)} method.
* Note that namespaces will not be visible in the name {@linkplain org.apache.sis.util.iso.DefaultLocalName#toString()
* string representation} unless the {@linkplain org.apache.sis.util.iso.DefaultLocalName#toFullyQualifiedName() fully
* qualified name} is requested.
* Example:
*
* {@preformat java
* FeatureTypeBuilder builder = new FeatureTypeBuilder().setNameSpace("MyNameSpace").setName("City");
* FeatureType city = builder.build();
*
* System.out.println(city.getName()); // Prints "City"
* System.out.println(city.getName().toFullyQualifiedName()); // Prints "MyNameSpace:City"
* }
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 0.8
* @since 0.8
* @module
*/
public abstract class TypeBuilder implements Localized {
/**
* The feature name, definition, designation and description.
* The name is mandatory; all other information are optional.
*/
private final Map<String,Object> identification;
/**
* Creates a new builder initialized to the values of the given builder.
* This constructor is for {@link AttributeTypeBuilder#setValueClass(Class)}
* and {@link CharacteristicTypeBuilder#setValueClass(Class)} implementations.
*
* @param builder the builder from which to copy information.
*/
TypeBuilder(final TypeBuilder builder) {
identification = new HashMap<>(builder.identification);
}
/**
* Creates a new builder initialized to the given configuration.
*/
TypeBuilder(final Locale locale) {
identification = new HashMap<>(4);
putIfNonNull(Errors.LOCALE_KEY, locale);
}
/**
* Resets the identification map. After invoking this method, this {@code TypeBuilder}
* is in same state that after it has been {@linkplain #TypeBuilder(Locale) constructed}.
*
* @see #clearCache()
*/
final void reset() {
final Object locale = identification.get(Errors.LOCALE_KEY);
identification.clear();
putIfNonNull(Errors.LOCALE_KEY, locale);
}
/**
* Initializes this builder to the value of the given type.
* The caller is responsible to invoke {@link #reset()} (if needed) before this method.
*/
final void initialize(final AbstractIdentifiedType template) {
putIfNonNull(AbstractIdentifiedType.NAME_KEY, template.getName());
putIfNonNull(AbstractIdentifiedType.DEFINITION_KEY, template.getDefinition());
putIfNonNull(AbstractIdentifiedType.DESIGNATION_KEY, template.getDesignation());
putIfNonNull(AbstractIdentifiedType.DESCRIPTION_KEY, template.getDescription());
if (template instanceof Deprecable && ((Deprecable) template).isDeprecated()) {
identification.put(AbstractIdentifiedType.DEPRECATED_KEY, Boolean.TRUE);
}
}
/**
* Puts the given value in the {@link #identification} map if the value is non-null.
* This method should be invoked only when the {@link #identification} map is known
* to not contain any value for the given key.
*/
private void putIfNonNull(final String key, final Object value) {
if (value != null) {
identification.put(key, value);
}
}
/**
* Returns the map of properties to give to the {@code FeatureType} or {@code PropertyType} constructor.
* If the map does not contains a name, a default name may be generated.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
final Map<String,Object> identification() {
if (identification.get(AbstractIdentifiedType.NAME_KEY) == null) {
String name = getDefaultName();
if (name != null) {
final int length = name.length();
if (length != 0) {
final int c = name.codePointAt(0);
final int lc = Character.toLowerCase(c);
if (c != lc) {
final int n = Character.charCount(c);
if (n >= length || Character.isLowerCase(name.codePointAt(n))) {
final StringBuilder buffer = new StringBuilder(length);
name = buffer.appendCodePoint(lc).append(name, n, length).toString();
}
}
identification.put(AbstractIdentifiedType.NAME_KEY, createLocalName(name));
}
}
}
return identification;
}
/**
* If the object created by the last call to {@code build()} has been cached, clears that cache.
*
* @see #reset()
*/
abstract void clearCache();
/**
* Creates a local name in the {@linkplain FeatureTypeBuilder#setNameSpace feature namespace}.
*/
abstract GenericName createLocalName(final CharSequence name);
/**
* Creates a generic name in the {@linkplain FeatureTypeBuilder#setNameSpace feature namespace}.
*/
abstract GenericName createGenericName(final CharSequence... names);
/**
* Returns the name of the {@code IdentifiedType} to create, or {@code null} if undefined.
* This method returns the value built from the last call to a {@code setName(…)} method,
* or a default name or {@code null} if no name has been explicitly specified.
*
* @return the name of the {@code IdentifiedType} to create (may be a default name or {@code null}).
*
* @see #setName(GenericName)
* @see AbstractIdentifiedType#getName()
* @see FeatureTypeBuilder#getNameSpace()
*/
public GenericName getName() {
return (GenericName) identification().get(AbstractIdentifiedType.NAME_KEY);
}
/**
* Returns a default name to use if the user did not specified a name. The first letter will be changed to
* lower case (unless the name looks like an acronym) for compliance with Java convention on property names.
*/
String getDefaultName() {
return null;
}
/**
* Returns the name to use for displaying error messages.
*/
final String getDisplayName() {
final GenericName name = getName();
return (name != null) ? name.toString() : Vocabulary.getResources(identification).getString(Vocabulary.Keys.Unnamed);
}
/**
* Sets the {@code IdentifiedType} 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>
*
* @param name the generic name (can not be {@code null}).
* @return {@code this} for allowing method calls chaining.
*
* @see #getName()
* @see #setName(CharSequence)
* @see AbstractIdentifiedType#NAME_KEY
*/
public TypeBuilder setName(final GenericName name) {
ensureNonNull("name", name);
if (!name.equals(identification.put(AbstractIdentifiedType.NAME_KEY, name))) {
clearCache();
}
return this;
}
/**
* Sets the {@code IdentifiedType} name as a simple string (local name).
* The namespace will be the value specified by the last call to {@link FeatureTypeBuilder#setNameSpace(CharSequence)},
* but that namespace will not be visible in the {@linkplain org.apache.sis.util.iso.DefaultLocalName#toString()
* string representation} unless the {@linkplain org.apache.sis.util.iso.DefaultLocalName#toFullyQualifiedName()
* fully qualified name} is requested.
*
* <p>This convenience method creates a {@link org.opengis.util.LocalName} instance from
* the given {@code CharSequence}, then delegates to {@link #setName(GenericName)}.</p>
*
* @param localPart the local part of the generic name as a {@link String} or {@link InternationalString}.
* @return {@code this} for allowing method calls chaining.
*
* @see #getName()
* @see #setName(CharSequence...)
* @see FeatureTypeBuilder#getNameSpace()
*/
public TypeBuilder setName(final CharSequence localPart) {
ensureNonEmpty("localPart", localPart);
return setName(createLocalName(localPart));
}
/**
* Sets the {@code IdentifiedType} name as a string in the given scope.
* The {@code components} array must contain at least one element.
* The last component (the {@linkplain org.apache.sis.util.iso.DefaultScopedName#tip() tip}) will be sufficient
* in many cases for calls to the {@link org.apache.sis.feature.AbstractFeature#getProperty(String)} method.
* The other elements before the last one are optional and can be used for resolving ambiguity.
* They will be visible as the name {@linkplain org.apache.sis.util.iso.DefaultScopedName#path() path}.
*
* <div class="note"><b>Example:</b>
* a call to {@code setName("A", "B", "C")} will create a "A:B:C" name.
* A property built with this name can be obtained from a feature by a call to {@code feature.getProperty("C")}
* if there is no ambiguity, or otherwise by a call to {@code feature.getProperty("B:C")} (if non-ambiguous) or
* {@code feature.getProperty("A:B:C")}.</div>
*
* In addition to the path specified by the {@code components} array, the name may also contain
* a namespace specified by the last call to {@link FeatureTypeBuilder#setNameSpace(CharSequence)}.
* But contrarily to the specified components, the namespace will not be visible in the name
* {@linkplain org.apache.sis.util.iso.DefaultScopedName#toString() string representation} unless the
* {@linkplain org.apache.sis.util.iso.DefaultScopedName#toFullyQualifiedName() fully qualified name} is requested.
*
* <p>This convenience method creates a {@link org.opengis.util.LocalName} or {@link org.opengis.util.ScopedName}
* instance depending on whether the {@code names} array contains exactly 1 element or more than 1 element, then
* delegates to {@link #setName(GenericName)}.</p>
*
* @param components the name components as an array of {@link String} or {@link InternationalString} instances.
* @return {@code this} for allowing method calls chaining.
*
* @see #getName()
* @see #setName(CharSequence)
* @see FeatureTypeBuilder#getNameSpace()
*/
public TypeBuilder setName(final CharSequence... components) {
ensureNonNull("components", components);
if (components.length == 0) {
throw new IllegalArgumentException(errors().getString(Errors.Keys.EmptyArgument_1, "components"));
}
return setName(createGenericName(components));
}
/**
* Returns a concise definition of the element.
*
* @return concise definition of the element, or {@code null} if none.
*
* @see AbstractIdentifiedType#getDefinition()
*/
public CharSequence getDefinition() {
return (CharSequence) identification.get(AbstractIdentifiedType.DEFINITION_KEY);
}
/**
* Sets a concise definition of the element.
*
* @param definition a concise definition of the element, or {@code null} if none.
* @return {@code this} for allowing method calls chaining.
*
* @see #getDefinition()
* @see AbstractIdentifiedType#DEFINITION_KEY
*/
public TypeBuilder setDefinition(final CharSequence definition) {
if (!Objects.equals(definition, identification.put(AbstractIdentifiedType.DEFINITION_KEY, definition))) {
clearCache();
}
return this;
}
/**
* Returns a natural language designator for the element.
* This can be used as an alternative to the {@linkplain #getName() name} in user interfaces.
*
* @return natural language designator for the element, or {@code null} if none.
*
* @see AbstractIdentifiedType#getDesignation()
*/
public CharSequence getDesignation() {
return (CharSequence) identification.get(AbstractIdentifiedType.DESIGNATION_KEY);
}
/**
* Sets a natural language designator for the element.
* This can be used as an alternative to the {@linkplain #getName() name} in user interfaces.
*
* @param designation a natural language designator for the element, or {@code null} if none.
* @return {@code this} for allowing method calls chaining.
*
* @see #getDesignation()
* @see AbstractIdentifiedType#DESIGNATION_KEY
*/
public TypeBuilder setDesignation(final CharSequence designation) {
if (!Objects.equals(designation, identification.put(AbstractIdentifiedType.DESIGNATION_KEY, designation))) {
clearCache();
}
return this;
}
/**
* Returns optional information beyond that required for concise definition of the element.
* The description may assist in understanding the element scope and application.
*
* @return information beyond that required for concise definition of the element, or {@code null} if none.
*
* @see AbstractIdentifiedType#getDescription()
*/
public CharSequence getDescription() {
return (CharSequence) identification.get(AbstractIdentifiedType.DESCRIPTION_KEY);
}
/**
* Sets optional information beyond that required for concise definition of the element.
* The description may assist in understanding the feature scope and application.
* If the type {@linkplain #isDeprecated() is deprecated}, then the description should
* give indication about the replacement (e.g. <cite>"superceded by …"</cite>).
*
* @param description information beyond that required for concise definition of the element, or {@code null} if none.
* @return {@code this} for allowing method calls chaining.
*
* @see #getDescription()
* @see AbstractIdentifiedType#DESCRIPTION_KEY
*/
public TypeBuilder setDescription(final CharSequence description) {
if (!Objects.equals(description, identification.put(AbstractIdentifiedType.DESCRIPTION_KEY, description))) {
clearCache();
}
return this;
}
/**
* Returns {@code true} if the type is deprecated.
* If this method returns {@code true}, then the {@linkplain #getDescription() description} should give
* indication about the replacement (e.g. <cite>"superceded by …"</cite>).
*
* @return whether this type is deprecated.
*
* @see AbstractIdentifiedType#isDeprecated()
*/
public boolean isDeprecated() {
return Boolean.TRUE.equals(identification.get(AbstractIdentifiedType.DEPRECATED_KEY));
}
/**
* Sets whether the type is deprecated.
* If the type is deprecated, then the {@linkplain #setDescription(CharSequence) description}
* should be set to an indication about the replacement (e.g. <cite>"superceded by …"</cite>).
*
* @param deprecated whether this type is deprecated.
* @return {@code this} for allowing method calls chaining.
*
* @see #isDeprecated()
* @see AbstractIdentifiedType#DEPRECATED_KEY
*/
public TypeBuilder setDeprecated(final boolean deprecated) {
final Boolean wrapper = deprecated;
Object previous = identification.put(AbstractIdentifiedType.DEPRECATED_KEY, wrapper);
if (previous == null) {
previous = Boolean.FALSE;
}
if (!Objects.equals(wrapper, previous)) {
clearCache();
}
return this;
}
/**
* Returns the element of the given name in the given list. The given name does not need to contains
* all elements of a {@link ScopedName}; it can be only the tip (for example {@code "myName"} instead
* of {@code "myScope:myName"}) provided that ignoring the name head does not create ambiguity.
*
* @param types the collection where to search for an element of the given name.
* @param name name of the element to search.
* @param nonAmbiguous whether to throw an exception if the given name is ambiguous.
* @return element of the given name, or {@code null} if none were found.
* @throws IllegalArgumentException if the given name is ambiguous.
*/
@SuppressWarnings("null")
final <E extends TypeBuilder> E forName(final List<E> types, final String name, final boolean nonAmbiguous) {
E best = null; // Best type found so far.
E ambiguity = null; // If two types are found at the same depth, the other type.
int depth = Integer.MAX_VALUE; // Number of path elements that we had to ignore in the GenericName.
for (final E type : types) {
GenericName candidate = type.getName();
for (int d=0; candidate != null; d++) {
if (name.equals(candidate.toString())) {
if (d < depth) {
best = type;
ambiguity = null;
depth = d;
break;
}
if (d == depth) {
ambiguity = type;
break;
}
}
if (!(candidate instanceof ScopedName)) break;
candidate = ((ScopedName) candidate).tail();
}
}
if (ambiguity != null && nonAmbiguous) {
throw new IllegalArgumentException(errors().getString(
Errors.Keys.AmbiguousName_3, best.getName(), ambiguity.getName(), name));
}
return best;
}
/**
* Returns the locale used for formatting error messages, or {@code null} if unspecified.
* If unspecified, the system default locale will be used.
*
* @return the locale used for formatting error messages, or {@code null} if unspecified.
*/
@Override
public Locale getLocale() {
return (Locale) identification.get(Errors.LOCALE_KEY);
}
/**
* Returns the resources for error messages.
*/
final Errors errors() {
return Errors.getResources(identification);
}
/**
* Returns the {@code sis-feature} specific resources for error messages.
*/
final Resources resources() {
return Resources.forProperties(identification);
}
/**
* Same as {@link org.apache.sis.util.ArgumentChecks#ensureNonNull(String, Object)},
* but uses the current locale in case of error.
*
* @param name the name of the argument to be checked. Used only if an exception is thrown.
* @param value the user argument to check against null value.
* @throws NullArgumentException if {@code object} is null.
*/
final void ensureNonNull(final String name, final Object value) {
if (value == null) {
throw new NullArgumentException(errors().getString(Errors.Keys.NullArgument_1, name));
}
}
/**
* Ensures that this instance is still alive.
*
* @param owner the owner of this instance. A value of null means that this instance should not be used any more.
*/
final void ensureAlive(final TypeBuilder owner) {
if (owner == null) {
throw new IllegalStateException(errors().getString(Errors.Keys.DisposedInstanceOf_1, getClass()));
}
}
/**
* Same as {@link org.apache.sis.util.ArgumentChecks#ensureNonEmpty(String, CharSequence)},
* but uses the current locale in case of error.
*
* @param name the name of the argument to be checked. Used only if an exception is thrown.
* @param text the user argument to check against null value and empty sequences.
* @throws NullArgumentException if {@code text} is null.
* @throws IllegalArgumentException if {@code text} is empty.
*/
final void ensureNonEmpty(final String name, final CharSequence text) {
if (text == null) {
throw new NullArgumentException(errors().getString(Errors.Keys.NullArgument_1, name));
}
if (text.length() == 0) {
throw new IllegalArgumentException(errors().getString(Errors.Keys.EmptyArgument_1, name));
}
}
/**
* Returns a string representation of this object.
* The returned string is for debugging purpose only and may change in any future SIS version.
*
* @return a string representation of this object for debugging purpose.
*/
@Override
public String toString() {
return appendStringTo(new StringBuilder(Classes.getShortClassName(this))).toString();
}
/**
* Partial implementation of {@link #toString()}. This method assumes that the class name
* has already be written in the buffer.
*/
final StringBuilder appendStringTo(final StringBuilder buffer) {
toStringInternal(buffer.append("[“").append(getDisplayName()).append('”'));
return buffer.append(']');
}
/**
* Appends a text inside the value returned by {@link #toString()}, before the closing bracket.
*/
void toStringInternal(StringBuilder buffer) {
}
/**
* Invoked when a type builder has been removed from its parent.
* Subclasses should override this method in a way that flag the builder as not usable anymore.
*/
void remove() {
}
/**
* Builds the feature or property type from the information specified to this builder.
* If a type has already been built and this builder state has not changed since the type creation,
* then the previously created {@code IdentifiedType} instance is returned.
*
* <div class="warning"><b>Warning:</b> In a future SIS version, the return type may be changed to the
* {@code org.opengis.feature.IdentifiedType} interface. This change is pending GeoAPI revision.</div>
*
* @return the feature or property type.
* @throws IllegalStateException if the builder contains inconsistent information.
*/
public abstract AbstractIdentifiedType build() throws IllegalStateException;
}