/*
* 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.Map;
import java.util.logging.Logger;
import javax.swing.tree.TreeModel;
import org.geotools.util.logging.Logging;
/**
* Base class for metadata implementations. Subclasses must implement the interfaces
* of some {@linkplain MetadataStandard metadata standard}. This class uses
* {@linkplain java.lang.reflect Java reflection} in order to provide default
* implementation of {@linkplain #AbstractMetadata(Object) copy constructor},
* {@link #equals} and {@link #hashCode} methods.
*
* @since 2.4
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (Geomatys)
*/
public abstract class AbstractMetadata {
/**
* The logger for metadata implementation.
*/
protected static final Logger LOGGER = Logging.getLogger("org.geotools.metadata");
/**
* Hash code value, or 0 if not yet computed. This field is reset to 0 by
* {@link #invalidate} in order to account for a change in metadata content.
*/
private transient int hashCode;
/**
* A view of this metadata as a map. Will be created only when first needed.
*/
private transient Map<String,Object> asMap;
/**
* Creates an initially empty metadata.
*/
protected AbstractMetadata() {
}
/**
* Constructs a metadata entity initialized with the values from the specified metadata.
* The {@code source} metadata must implements the same metadata interface (defined by
* the {@linkplain #getStandard standard}) than this class, but don't need to be the same
* implementation class. The copy is performed using Java reflections.
*
* @param source The metadata to copy values from.
* @throws ClassCastException if the specified metadata don't implements the expected
* metadata interface.
* @throws UnmodifiableMetadataException if this class don't define {@code set} methods
* corresponding to the {@code get} methods found in the implemented interface,
* or if this instance is not modifiable for some other reason.
*/
protected AbstractMetadata(final Object source)
throws ClassCastException, UnmodifiableMetadataException
{
getStandard().shallowCopy(source, this, true);
}
/**
* Returns the metadata standard implemented by subclasses.
*
* @return The metadata standard implemented.
*/
public abstract MetadataStandard getStandard();
/**
* Returns the metadata interface implemented by this class. It should be one of the
* interfaces defined in the {@linkplain #getStandard metadata standard} implemented
* by this class.
*
* @return The standard interface implemented by this implementation class.
*/
public Class<?> getInterface() {
// No need to sychronize, since this method do not depends on property values.
return getStandard().getInterface(getClass());
}
/**
* Returns {@code true} if this metadata is modifiable. The default implementation
* uses heuristic rules which return {@code false} if and only if:
* <p>
* <ul>
* <li>this class do not contains any {@code set*(...)} method</li>
* <li>All {@code get*()} methods return a presumed immutable object.
* The maining of "<cite>presumed immutable</cite>" may vary in
* different Geotools versions.</li>
* </ul>
* <p>
* Otherwise, this method conservatively returns {@code true}. Subclasses
* should override this method if they can provide a more rigorous analysis.
*/
boolean isModifiable() {
return getStandard().isModifiable(getClass());
}
/**
* Invoked when the metadata changed. Some cached informations will need
* to be recomputed.
*/
void invalidate() {
assert Thread.holdsLock(this);
hashCode = 0; // Will recompute when needed.
}
/**
* Returns a view of this metadata object as a {@linkplain Map map}. The map is backed by this
* 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.
*
* @return A view of this metadata object as a map.
*/
public synchronized Map<String,Object> asMap() {
if (asMap == null) {
asMap = getStandard().asMap(this);
}
return asMap;
}
/**
* Returns a view of this 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.
*
* @return A view of this metadata object as a tree.
*/
public synchronized TreeModel asTree() {
return getStandard().asTree(this);
}
/**
* Compares this metadata with the specified object for equality. The default
* implementation uses Java reflection. Subclasses may override this method
* for better performances.
* <p>
* This method performs a <cite>deep</cite> comparaison (i.e. if this metadata contains
* other metadata, the comparaison will walk through the other metadata content as well)
* providing that every childs implement the {@link Object#equals} method as well. This
* is the case by default if every childs are subclasses of {@code AbstractMetadata}.
*
* @param object The object to compare with this metadata.
* @return {@code true} if the given object is equals to this metadata.
*/
@Override
public boolean equals(final Object object) {
if (object == this) {
return true;
}
if (object==null || !object.getClass().equals(getClass())) {
return false;
}
/*
* Opportunist usage of hash code if they are already computed. If they are not, we will
* not compute them - they are not sure to be faster than checking directly for equality,
* and hash code could be invalidated later anyway if the object change. Note that we
* don't need to synchronize since reading int fields are garanteed to be atomic in Java.
*/
final int c0 = hashCode;
if (c0 != 0) {
final int c1 = ((AbstractMetadata) object).hashCode;
if (c1 != 0 && c0 != c1) {
return false;
}
}
final MetadataStandard standard = getStandard();
/*
* DEADLOCK WARNING: A deadlock may occur if the same pair of objects is being compared
* in an other thread (see http://jira.codehaus.org/browse/GEOT-1777). Ideally we would
* synchronize on 'this' and 'object' atomically (RFE #4210659). Since we can't in Java
* a workaround is to always get the locks in the same order. Unfortunatly we have no
* garantee that the caller didn't looked the object himself. For now the safest approach
* is to not synchronize at all.
*/
return standard.shallowEquals(this, object, false);
}
/**
* Computes a hash code value for this metadata using Java reflection. 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.
*/
@Override
public synchronized int hashCode() {
int code = hashCode;
if (code == 0) {
code = getStandard().hashCode(this);
if (!isModifiable()) {
// In current implementation, we do not store the hash code if this metadata is
// modifiable because we can not track change in dependencies (e.g. a change in
// a metadata contained in this metadata).
hashCode = code;
}
}
return code;
}
/**
* Returns a string representation of this metadata.
*/
@Override
public synchronized String toString() {
return getStandard().toString(this);
}
}