/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.metadata;
import java.util.HashMap;
import java.util.Map;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
/**
* Enumeration of some metadata standards. A standard is defined by a set of Java interfaces
* in a specific package or subpackages. For example the {@linkplain #ISO_19115 ISO 19115}
* standard is defined by <A HREF="http://geoapi.sourceforge.net">GeoAPI</A> interfaces in
* the {@link org.opengis.metadata} package and subpackages.
* <p>
* This class provides some methods operating on metadata instances through
* {@linkplain java.lang.reflect Java reflection}. The following rules are
* assumed:
* <p>
* <ul>
* <li>Properties (or metadata attributes) are defined by the set of {@code get*()}
* (arbitrary return type) or {@code is*()} (boolean return type) methods found
* in the <strong>interface</strong>. Getters declared in the implementation
* only are ignored.</li>
* <li>A property is <cite>writable</cite> if a {@code set*(...)} method is defined
* in the implementation class for the corresponding {@code get*()} method. The
* setter don't need to be defined in the interface.</li>
* </ul>
*
* @since 2.4
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (Geomatys)
*/
public final class MetadataStandard {
/**
* An instance working on ISO 19111 standard as defined by
* <A HREF="http://geoapi.sourceforge.net">GeoAPI</A> interfaces
* in the {@link org.opengis.referencing} package and subpackages.
*
* @since 2.5
*/
public static final MetadataStandard ISO_19111 = new MetadataStandard("org.opengis.referencing.");
/**
* An instance working on ISO 19115 standard as defined by
* <A HREF="http://geoapi.sourceforge.net">GeoAPI</A> interfaces
* in the {@link org.opengis.metadata} package and subpackages.
*/
public static final MetadataStandard ISO_19115 = new MetadataStandard("org.opengis.metadata.");
/**
* An instance working on ISO 19119 standard as defined by
* <A HREF="http://geoapi.sourceforge.net">GeoAPI</A> interfaces
* in the {@link org.opengis.service} package and subpackages.
*
* @since 2.5
*/
public static final MetadataStandard ISO_19119 = new MetadataStandard("org.opengis.service.");
/**
* The root packages for metadata interfaces. Must ends with {@code "."}.
*/
private final String interfacePackage;
/**
* Accessors for the specified implementations.
*/
private final Map<Class<?>,PropertyAccessor> accessors = new HashMap<Class<?>,PropertyAccessor>();
/**
* Shared pool of {@link PropertyTree} instances, once for each thread
* (in order to avoid the need for thread synchronization).
*/
private final ThreadLocal<PropertyTree> treeBuilders = new ThreadLocal<PropertyTree>() {
@Override
protected PropertyTree initialValue() {
return new PropertyTree(MetadataStandard.this);
}
};
/**
* Creates a new instance working on implementation of interfaces defined
* in the specified package. For the ISO 19115 standard reflected by GeoAPI
* interfaces, it should be the {@link org.opengis.metadata} package.
*
* @param interfacePackage The root package for metadata interfaces.
*/
public MetadataStandard(String interfacePackage) {
if (!interfacePackage.endsWith(".")) {
interfacePackage += '.';
}
this.interfacePackage = interfacePackage;
}
/**
* Returns the accessor for the specified implementation.
*
* @throws ClassCastException if the specified implementation class do
* not implements a metadata interface of the expected package.
*/
private PropertyAccessor getAccessor(final Class<?> implementation)
throws ClassCastException
{
final PropertyAccessor accessor = getAccessorOptional(implementation);
if (accessor == null) {
throw new ClassCastException(Errors.format(ErrorKeys.UNKNOW_TYPE_$1,
implementation.getName()));
}
return accessor;
}
/**
* Returns the accessor for the specified implementation, or {@code null} if none.
*/
final PropertyAccessor getAccessorOptional(final Class<?> implementation) {
synchronized (accessors) {
PropertyAccessor accessor = accessors.get(implementation);
if (accessor == null) {
Class<?> type = getType(implementation);
if (type != null) {
accessor = new PropertyAccessor(implementation, type);
accessors.put(implementation, accessor);
}
}
return accessor;
}
}
/**
* Returns the metadata interface implemented by the specified implementation.
* Only one metadata interface can be implemented.
*
* @param metadata The metadata implementation to wraps.
* @return The single interface, or {@code null} if none where found.
*/
private Class<?> getType(final Class<?> implementation) {
return PropertyAccessor.getType(implementation, interfacePackage);
}
/**
* Returns the metadata interface implemented by the specified implementation class.
*
* @param implementation The implementation class.
* @return The interface implemented by the given implementation class.
* @throws ClassCastException if the specified implementation class do
* not implements a metadata interface of the expected package.
*
* @see AbstractMap#getInterface
*/
public Class<?> getInterface(final Class<?> implementation) throws ClassCastException {
return getAccessor(implementation).type;
}
/**
* Returns a view of the specified metadata object as a {@linkplain Map map}.
* The map is backed by the metadata object using Java reflection, so changes
* in the underlying metadata object are immediately reflected in the map.
* The keys are the property names as determined by the list of {@code get*()}
* methods declared in the {@linkplain #getInterface metadata interface}.
* <p>
* The map supports the {@link Map#put put} operations if the underlying
* metadata object contains {@link #set*(...)} methods.
*
* @param metadata The metadata object to view as a map.
* @return A map view over the metadata object.
* @throws ClassCastException if at the metadata object don't
* implements a metadata interface of the expected package.
*
* @see AbstractMap#asMap
*/
public Map<String,Object> asMap(final Object metadata) throws ClassCastException {
return new PropertyMap(metadata, getAccessor(metadata.getClass()));
}
/**
* Returns a view of the specified metadata as a tree. Note that while {@link TreeModel}
* is defined in the {@link javax.swing.tree} package, it can be seen as a data structure
* independent of Swing. It will not force class loading of Swing framework.
* <p>
* In current implementation, the tree is not live (i.e. changes in metadata are not
* reflected in the tree). However it may be improved in a future Geotools implementation.
*
* @param metadata The metadata object to formats as a string.
* @return A tree representation of the specified metadata.
* @throws ClassCastException if at the metadata object don't
* implements a metadata interface of the expected package.
*
* @see AbstractMap#asTree
*/
public TreeModel asTree(final Object metadata) throws ClassCastException {
final PropertyTree builder = treeBuilders.get();
return new DefaultTreeModel(builder.asTree(metadata), true);
}
/**
* Returns {@code true} if this metadata is modifiable. This method is not public because it
* uses heuristic rules. In case of doubt, this method conservatively returns {@code true}.
*
* @throws ClassCastException if the specified implementation class do
* not implements a metadata interface of the expected package.
*
* @see AbstractMap#isModifiable
*/
final boolean isModifiable(final Class implementation) throws ClassCastException {
return getAccessor(implementation).isModifiable();
}
/**
* Replaces every properties in the specified metadata by their
* {@linkplain ModifiableMetadata#unmodifiable unmodifiable variant.
*
* @throws ClassCastException if the specified implementation class do
* not implements a metadata interface of the expected package.
*
* @see ModifiableMetadata#freeze()
*/
final void freeze(final Object metadata) throws ClassCastException {
getAccessor(metadata.getClass()).freeze(metadata);
}
/**
* Copies all metadata from source to target. The source must implements the same
* metadata interface than the target.
*
* @param source The metadata to copy.
* @param target The target metadata.
* @param skipNulls If {@code true}, only non-null values will be copied.
* @throws ClassCastException if the source or target object don't
* implements a metadata interface of the expected package.
* @throws UnmodifiableMetadataException if the target metadata is unmodifiable,
* or if at least one setter method was required but not found.
*
* @see AbstractMap#AbstractMap(Object)
*/
public void shallowCopy(final Object source, final Object target, final boolean skipNulls)
throws ClassCastException, UnmodifiableMetadataException
{
ensureNonNull("target", target);
final PropertyAccessor accessor = getAccessor(target.getClass());
if (!accessor.type.isInstance(source)) {
ensureNonNull("source", source);
throw new ClassCastException(Errors.format(ErrorKeys.ILLEGAL_CLASS_$2,
source.getClass(), accessor.type));
}
if (!accessor.shallowCopy(source, target, skipNulls)) {
throw new UnmodifiableMetadataException(Errors.format(ErrorKeys.UNMODIFIABLE_METADATA));
}
}
/**
* Compares the two specified metadata objects. The comparaison is <cite>shallow</cite>,
* i.e. all metadata attributes are compared using the {@link Object#equals} method without
* recursive call to this {@code shallowEquals(...)} method for child metadata.
* <p>
* This method can optionaly excludes null values from the comparaison. In metadata,
* null value often means "don't know", so in some occasion we want to consider two
* metadata as different only if an attribute value is know for sure to be different.
* <p>
* The first arguments must be an implementation of a metadata interface, otherwise an
* exception will be thrown. The two argument do not need to be the same implementation
* however.
*
* @param metadata1 The first metadata object to compare.
* @param metadata2 The second metadata object to compare.
* @param skipNulls If {@code true}, only non-null values will be compared.
* @return {@code true} if the given metadata objects are equals.
* @throws ClassCastException if at least one metadata object don't
* implements a metadata interface of the expected package.
*
* @see AbstractMetadata#equals
*/
public boolean shallowEquals(final Object metadata1, final Object metadata2, final boolean skipNulls)
throws ClassCastException
{
if (metadata1 == metadata2) {
return true;
}
if (metadata1 == null || metadata2 == null) {
return false;
}
final PropertyAccessor accessor = getAccessor(metadata1.getClass());
if (!accessor.type.equals(getType(metadata2.getClass()))) {
return false;
}
return accessor.shallowEquals(metadata1, metadata2, skipNulls);
}
/**
* Computes a hash code for the specified metadata. The hash code is defined as the
* sum of hash code values of all non-null properties. This is the same contract than
* {@link java.util.Set#hashCode} and ensure that the hash code value is insensitive
* to the ordering of properties.
*
* @param metadata The metadata object to compute hash code.
* @return A hash code value for the specified metadata.
* @throws ClassCastException if at the metadata object don't
* implements a metadata interface of the expected package.
*
* @see AbstractMap#hashCode
*/
public int hashCode(final Object metadata) throws ClassCastException {
return getAccessor(metadata.getClass()).hashCode(metadata);
}
/**
* Returns a string representation of the specified metadata.
*
* @param metadata The metadata object to formats as a string.
* @return A string representation of the specified metadata.
* @throws ClassCastException if at the metadata object don't
* implements a metadata interface of the expected package.
*
* @see AbstractMap#toString
*/
public String toString(final Object metadata) throws ClassCastException {
final PropertyTree builder = treeBuilders.get();
return PropertyTree.toString(builder.asTree(metadata));
}
/**
* Ensures that the specified argument is non-null.
*/
private static void ensureNonNull(final String name, final Object value) {
if (value == null) {
throw new NullPointerException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, name));
}
}
}