blob: 1fb3f6ce1fb338601e34d0265e7d6fe6298ee433 [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.List;
import java.util.Arrays;
import java.util.Locale;
import java.util.ArrayList;
import java.util.Collection;
import org.opengis.metadata.Identifier;
import org.opengis.util.TypeName;
import org.opengis.util.NameSpace;
import org.opengis.util.LocalName;
import org.opengis.util.MemberName;
import org.opengis.util.GenericName;
import org.opengis.util.NameFactory;
import org.opengis.util.InternationalString;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.collection.WeakHashSet;
import org.apache.sis.internal.util.Strings;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
import static org.apache.sis.util.iso.DefaultNameSpace.DEFAULT_SEPARATOR_STRING;
/**
* A factory for creating {@link AbstractName} objects.
* This factory provides the following methods for creating name instances:
*
* <ul>
* <li>{@link #createTypeName(NameSpace, CharSequence)}</li>
* <li>{@link #createMemberName(NameSpace, CharSequence, TypeName)}</li>
* <li>{@link #createLocalName(NameSpace, CharSequence)}</li>
* <li>{@link #createGenericName(NameSpace, CharSequence[])} – for local or scoped names</li>
* </ul>
*
* The following methods for creating miscellaneous name-related objects:
*
* <ul>
* <li>{@link #createNameSpace(GenericName, Map)}</li>
* <li>{@link #createInternationalString(Map)}</li>
* </ul>
*
* And the following methods for performing some analysis:
*
* <ul>
* <li>{@link #parseGenericName(NameSpace, CharSequence)}</li>
* <li>{@link #toGenericNames(Object)}</li>
* <li>{@link #toTypeName(Class)}</li>
* </ul>
*
* <h2>Thread safety</h2>
* The same {@code DefaultNameFactory} instance can be safely used by many threads without synchronization
* on the part of the caller. Subclasses should make sure that any overridden methods remain safe to call
* from multiple threads.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.5
*
* @see Names
* @see DefaultNameSpace
* @see DefaultScopedName
* @see DefaultLocalName
* @see DefaultTypeName
* @see DefaultMemberName
*
* @since 0.3
* @module
*/
public class DefaultNameFactory extends AbstractFactory implements NameFactory {
/**
* The key for name separator.
*/
static final String SEPARATOR_KEY = "separator";
/**
* The key or the separator after the first name.
*/
static final String HEAD_SEPARATOR_KEY = "separator.head";
/**
* Weak references to the name created by this factory.
*/
private final WeakHashSet<GenericName> pool;
/**
* Helper class for mapping {@link Class} to {@link TypeName}, created when first needed.
*/
private transient volatile TypeNames typeNames;
/**
* Creates a new factory.
*/
public DefaultNameFactory() {
pool = new WeakHashSet<>(GenericName.class);
}
/**
* Creates an international string from a set of strings in different locales.
*
* @param strings string value for each locale key.
* @return the international string.
*
* @see Types#toInternationalString(CharSequence)
*/
@Override
public InternationalString createInternationalString(final Map<Locale,String> strings) {
ensureNonNull("strings", strings);
switch (strings.size()) {
case 0: throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyDictionary));
case 1: return new SimpleInternationalString(strings.values().iterator().next());
default: return new DefaultInternationalString(strings);
}
// Do not cache in the pool, because not all instances are immutable.
}
/**
* Returns the value for the given key in the given properties map, or {@code null} if none.
*/
private static String getString(final Map<String,?> properties, final String key) {
if (properties != null) {
final Object value = properties.get(key);
if (value != null) {
return value.toString();
}
}
return null;
}
/**
* Returns a namespace having the given name. Despite the "create" name, this method tries to
* return an existing instance when possible. The namespace is characterized by the given name,
* and optionally by the following properties:
*
* <blockquote><table class="sis">
* <caption>Recognized properties</caption>
* <tr>
* <th>Property name</th>
* <th>Purpose</th>
* </tr>
* <tr>
* <td>{@code "separator"}</td>
* <td>The separator to insert between {@linkplain AbstractName#getParsedNames() parsed names}
* in that namespace.</td>
* </tr>
* <tr>
* <td>{@code "separator.head"}</td>
* <td>The separator to insert between the namespace and the {@linkplain AbstractName#head() head}.<br>
* If omitted, then the default is the same value than {@code "separator"}.</td>
* </tr>
* </table></blockquote>
*
* <div class="note"><b>Examples:</b>
* <ul>
* <li>For URN namespace, {@code separator} = {@code ":"} is typically sufficient.</li>
* <li>For HTTP namespace, {@code separator.head} = {@code "://"} and {@code separator} = {@code "."}.</li>
* </ul></div>
*
* @param name the name of the namespace to be returned. This argument can be created using
* <code>{@linkplain #createGenericName(NameSpace, CharSequence[]) createGenericName}(null, namespace)</code>.
* @param properties an optional map of properties to be assigned to the namespace, or {@code null} if none.
* @return a namespace having the given name and separator.
*
* @see Names#createLocalName(CharSequence, String, CharSequence)
*/
@Override
public NameSpace createNameSpace(final GenericName name, final Map<String,?> properties) {
ensureNonNull("name", name);
String separator = getString(properties, SEPARATOR_KEY);
if (separator == null) {
separator = DefaultNameSpace.DEFAULT_SEPARATOR_STRING;
}
String headSeparator = getString(properties, HEAD_SEPARATOR_KEY);
if (headSeparator == null) {
headSeparator = separator;
}
final boolean isEmpty = separator.isEmpty();
if (isEmpty || headSeparator.isEmpty()) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.EmptyProperty_1, isEmpty ? SEPARATOR_KEY : HEAD_SEPARATOR_KEY));
}
return DefaultNameSpace.forName(name.toFullyQualifiedName(), headSeparator, separator);
}
/**
* Creates a type name from the given character sequence.
* The default implementation returns a new or an existing {@link DefaultTypeName} instance.
*
* @param scope the {@linkplain AbstractName#scope() scope} of the type name to be created,
* or {@code null} for a global namespace.
* @param name the type name as a string or an international string.
* @return the type name for the given character sequence.
*
* @see #toTypeName(Class)
* @see Names#createTypeName(CharSequence, String, CharSequence)
*/
@Override
public TypeName createTypeName(final NameSpace scope, final CharSequence name) {
return pool.unique(new DefaultTypeName(scope, name));
}
/**
* Creates a member name from the given character sequence and attribute type.
* The default implementation returns a new or an existing {@link DefaultMemberName} instance.
*
* @param scope the {@linkplain AbstractName#scope() scope} of the member name to be created,
* or {@code null} for a global namespace.
* @param name the member name as a string or an international string.
* @param attributeType the type of the data associated with the record member.
* @return the member name for the given character sequence.
*
* @see Names#createMemberName(CharSequence, String, CharSequence, Class)
*/
@Override
public MemberName createMemberName(final NameSpace scope, final CharSequence name, final TypeName attributeType) {
return pool.unique(new DefaultMemberName(scope, name, attributeType));
}
/**
* Creates a local name from the given character sequence.
* The default implementation returns a new or an existing {@link DefaultLocalName} instance.
*
* @param scope the {@linkplain AbstractName#scope() scope} of the local name to be created,
* or {@code null} for a global namespace.
* @param name the local name as a string or an international string.
* @return the local name for the given character sequence.
*
* @see Names#createLocalName(CharSequence, String, CharSequence)
*/
@Override
public LocalName createLocalName(final NameSpace scope, final CharSequence name) {
/*
* Maintenance note: if the body of this method is modified (except for the use of the cache),
* consider updating DefaultLocalName.castOrCopy(LocalName) method accordingly.
*/
if (scope instanceof DefaultNameSpace) {
// Following may return a cached instance.
return ((DefaultNameSpace) scope).local(name, null);
}
return pool.unique(new DefaultLocalName(scope, name));
}
/**
* Creates a local or scoped name from an array of parsed names. The default implementation
* returns an instance of {@link DefaultLocalName} if the length of the {@code parsedNames}
* array is 1, or an instance of {@link DefaultScopedName} if the length of the array is 2
* or more.
*
* @param scope the {@linkplain AbstractName#scope() scope} of the generic name to be created,
* or {@code null} for a global namespace.
* @param parsedNames the local names as an array of {@link String} or {@link InternationalString} instances.
* This array shall contain at least one element.
* @return the generic name for the given parsed names.
*
* @see #parseGenericName(NameSpace, CharSequence)
*/
@Override
public GenericName createGenericName(final NameSpace scope, final CharSequence... parsedNames) {
ensureNonNull("parsedNames", parsedNames);
switch (parsedNames.length) {
default: return pool.unique(new DefaultScopedName(scope, Arrays.asList(parsedNames)));
case 1: return createLocalName(scope, parsedNames[0]); // User may override.
case 0: throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, "parsedNames"));
}
}
/**
* Constructs a generic name from a qualified name.
* This method splits the given name around a separator inferred from the given scope, or the
* {@link DefaultNameSpace#DEFAULT_SEPARATOR ':'} separator if the given scope is null.
*
* @param scope the {@linkplain AbstractName#scope() scope} of the generic name to be created,
* or {@code null} for a global namespace.
* @param name the qualified name, as a sequence of names separated by a scope-dependent separator.
* @return a name parsed from the given string.
*
* @see Names#parseGenericName(CharSequence, String, CharSequence)
*/
@Override
public GenericName parseGenericName(final NameSpace scope, final CharSequence name) {
final String separator;
if (scope instanceof DefaultNameSpace) {
separator = ((DefaultNameSpace) scope).separator;
} else {
separator = DEFAULT_SEPARATOR_STRING;
}
final int s = separator.length();
final List<String> names = new ArrayList<>();
int lower = 0;
final String string = name.toString();
while (true) {
final int upper = string.indexOf(separator, lower);
if (upper >= 0) {
names.add(string.substring(lower, upper));
lower = upper + s;
} else {
names.add(string.substring(lower));
break;
}
}
if (names.size() == 1) {
/*
* Preserves the InternationalString (current implementation of
* the parsing code above has lost the internationalization).
*/
return createLocalName(scope, name);
}
return createGenericName(scope, names.toArray(new String[names.size()]));
}
/**
* Converts the given value to an array of generic names. If the given value is an instance of
* {@link GenericName}, {@link String} or any other type enumerated below, then it is converted
* and returned in an array of length 1. If the given value is an array or a collection, then an
* array of same length is returned where each element has been converted.
*
* <p>Allowed types or element types are:</p>
* <ul>
* <li>{@link GenericName}, to be casted and returned as-is.</li>
* <li>{@link CharSequence} (usually a {@link String} or an {@link InternationalString}),
* to be parsed as a generic name using the {@link DefaultNameSpace#DEFAULT_SEPARATOR ':'} separator.</li>
* <li>{@link Identifier}, its {@linkplain Identifier#getCode() code} to be parsed as a generic name
* using the {@link DefaultNameSpace#DEFAULT_SEPARATOR ':'} separator.</li>
* </ul>
*
* If {@code value} is an array or a collection containing {@code null} elements,
* then the corresponding element in the returned array will also be {@code null}.
*
* @param value the object to cast into an array of generic names, or {@code null}.
* @return the generic names, or {@code null} if the given {@code value} was null.
* Note that it may be the {@code value} reference itself casted to {@code GenericName[]}.
* @throws ClassCastException if {@code value} can't be casted.
*
* @since 0.5
*/
public GenericName[] toGenericNames(Object value) throws ClassCastException {
if (value == null) {
return null;
}
GenericName name = toGenericName(value);
if (name != null) {
return new GenericName[] {
name
};
}
/*
* Above code checked for a singleton. Now check for a collection or an array.
*/
final Object[] values;
if (value instanceof Object[]) {
values = (Object[]) value;
if (values instanceof GenericName[]) {
return (GenericName[]) values;
}
} else if (value instanceof Collection<?>) {
values = ((Collection<?>) value).toArray();
} else {
throw new ClassCastException(Errors.format(Errors.Keys.IllegalArgumentClass_2,
"value", value.getClass()));
}
final GenericName[] names = new GenericName[values.length];
for (int i=0; i<values.length; i++) {
value = values[i];
if (value != null) {
name = toGenericName(value);
if (name == null) {
throw new ClassCastException(Errors.format(Errors.Keys.IllegalArgumentClass_2,
Strings.toIndexed("value", i), value.getClass()));
}
names[i] = name;
}
}
return names;
}
/**
* Creates a generic name from the given value. The value may be an instance of
* {@link GenericName}, {@link Identifier}, {@link CharSequence} or {@link Class}.
* If the given object is not recognized, then this method returns {@code null}.
*
* @param value the object to convert.
* @return the converted object, or {@code null} if {@code value} is not convertible.
*/
private GenericName toGenericName(final Object value) {
if (value instanceof GenericName) {
return (GenericName) value;
}
if (value instanceof Identifier) {
return parseGenericName(null, ((Identifier) value).getCode());
}
if (value instanceof CharSequence) {
return parseGenericName(null, (CharSequence) value);
}
if (value instanceof Class<?>) {
return toTypeName((Class<?>) value);
}
return null;
}
/**
* Suggests a type name for the given class. Apache SIS provides a mapping between {@code Class}
* and {@code TypeName} objects as documented in the {@link DefaultTypeName} javadoc.
*
* <p>In order to protect against potential changes in the {@code Class} ↔ {@code TypeName} mapping, users are
* encouraged to retrieve the {@code valueClass} by invoking the {@link Names#toClass(TypeName)} method instead
* than parsing the name.</p>
*
* @param valueClass the Java class for which to get a type name, or {@code null}.
* @return a suggested type name, or {@code null} if the given class was null.
*
* @see DefaultTypeName#toClass()
* @see Names#toClass(TypeName)
*
* @since 0.5
*/
public TypeName toTypeName(final Class<?> valueClass) {
if (!TypeNames.isValid(valueClass)) {
return null;
}
/*
* Note: we do not cache the TypeName for the valueClass argument because:
*
* - It is not needed (at least in the default implementation) for getting unique instance.
* - It is not the best place for performance improvement, since TypeName are usually only
* a step in the creation of bigger object (typically AttributeType). Users are better to
* cache the bigger object instead.
*/
TypeNames t = typeNames;
if (t == null) {
/*
* Create TypeNames outide the synchronized block because the TypeNames constructor will call back
* methods from this class. Since those methods are overrideable, this could invoke user's code.
* Note also that methods in this class use the 'pool', which is itself synchronized, so we are
* better to avoid double synchronization for reducing the risk of dead-lock.
*/
final TypeNames c = new TypeNames(this);
synchronized (this) { // Double-check strategy is ok if 'typeNames' is volatile.
t = typeNames;
if (t == null) {
typeNames = t = c;
}
}
}
return t.toTypeName(this, valueClass);
}
}