blob: bbe6738f8214889cca4434f2c41ba9929c368123 [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.metadata;
import java.util.Set;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.List;
import java.util.Arrays;
import java.util.Collection;
import java.lang.reflect.Constructor;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.Exceptions;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.collection.CodeListSet;
import org.apache.sis.metadata.internal.Resources;
/**
* Performs deep copies of given metadata instances. This class performs <em>copies</em>, not clones,
* since the copied metadata may not be instances of the same class as the original metadata.
* This class performs the following steps:
*
* <ul>
* <li>Get the {@linkplain MetadataStandard#getImplementation implementation class} of the given metadata instance.</li>
* <li>Create a {@linkplain Constructor#newInstance new instance} of the implementation class using the public no-argument constructor.</li>
* <li>Invoke all non-deprecated setter methods on the new instance with the corresponding value from the given metadata.</li>
* <li>If any of the values copied in above step is itself a metadata, recursively performs deep copy on those metadata instances too.</li>
* </ul>
*
* This copier may be used for converting metadata tree of unknown implementations (for example the result of a call to
* {@link org.apache.sis.metadata.sql.MetadataSource#lookup(Class, String)}) into instances of {@link AbstractMetadata}.
* The copier may also be used if a {@linkplain ModifiableMetadata.State#EDITABLE modifiable} metadata is desired after
* the original metadata has been made {@linkplain ModifiableMetadata.State#FINAL final}.
*
* <p>Default implementation copies all copiable children, regardless their {@linkplain ModifiableMetadata#state() state}.
* Static factory methods allow to construct some variants, for example skipping the copy of unmodifiable metadata instances
* since they can be safely shared.</p>
*
* <p>This class supports cyclic graphs in the metadata tree. It may return the given {@code metadata} object directly
* if the {@linkplain MetadataStandard#getImplementation implementation class} does not provide any setter method.</p>
*
* <p>This class is not thread-safe.
* In multi-threads environment, each thread should use its own {@code MetadataCopier} instance.</p>
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.2
* @since 0.8
*/
public class MetadataCopier extends MetadataVisitor<Object> {
/**
* The default metadata standard to use for object that are not {@link AbstractMetadata} instances,
* or {@code null} if none.
*/
private final MetadataStandard standard;
/**
* The current metadata instance where to copy the property values.
*/
private Object target;
/**
* Creates a new metadata copier.
*
* @param standard the default metadata standard to use for object that are not {@link AbstractMetadata} instances,
* or {@code null} if none.
*/
public MetadataCopier(final MetadataStandard standard) {
this.standard = standard;
}
/**
* Returns the metadata standard to use for the given metadata object, or {@code null} if unknown.
*/
private MetadataStandard getStandard(final Object metadata) {
if (metadata instanceof AbstractMetadata) {
final MetadataStandard std = ((AbstractMetadata) metadata).getStandard();
if (std != null) return std;
}
return standard;
}
/**
* Creates a new metadata copier which avoid copying unmodifiable metadata.
* More specifically, any {@link ModifiableMetadata} instance in
* {@linkplain ModifiableMetadata.State#FINAL final state} will be kept <i>as-is</i>;
* those final metadata will not be copied since they can be safely shared.
*
* @param standard the default metadata standard to use for object that are not {@link AbstractMetadata} instances,
* or {@code null} if none.
* @return a metadata copier which skip the copy of unmodifiable metadata.
*
* @since 1.0
*/
public static MetadataCopier forModifiable(final MetadataStandard standard) {
return new MetadataCopier(standard) {
@Override protected Object copyRecursively(final Class<?> type, final Object metadata) {
if (metadata instanceof ModifiableMetadata) {
final ModifiableMetadata.State state = ((ModifiableMetadata) metadata).state();
if (state != null && state.isUnmodifiable()) {
return metadata;
}
}
return super.copyRecursively(type, metadata);
}
};
}
/**
* Performs a potentially deep copy of a metadata object of unknown type.
* The return value does not need to be of the same class as the argument.
*
* @param metadata the metadata object to copy, or {@code null}.
* @return a copy of the given metadata object, or {@code null} if the given argument is {@code null}.
* @throws UnsupportedOperationException if there is no implementation class for a metadata to copy,
* or an implementation class does not provide a public default constructor.
*/
public Object copy(final Object metadata) {
return copyRecursively(null, metadata);
}
/**
* Performs a potentially deep copy of the given metadata object.
* This method is preferred to {@link #copy(Object)} when the type is known.
* The specified type should be an interface, not an implementation
* (for example {@link org.opengis.metadata.Metadata}, not
* {@link org.apache.sis.metadata.iso.DefaultMetadata}).
*
* @param <T> compile-time value of the {@code type} argument.
* @param type the interface of the metadata object to copy.
* @param metadata the metadata object to copy, or {@code null}.
* @return a copy of the given metadata object, or {@code null} if the given argument is {@code null}.
* @throws IllegalArgumentException if {@code type} is an implementation class instead of interface.
* @throws UnsupportedOperationException if there is no implementation class for a metadata to copy,
* or an implementation class does not provide a public default constructor.
*/
public <T> T copy(final Class<T> type, final T metadata) {
ArgumentChecks.ensureNonNull("type", type);
if (metadata instanceof AbstractMetadata) {
final Class<?> interfaceType = ((AbstractMetadata) metadata).getInterface();
if (!type.isAssignableFrom(interfaceType)) {
/*
* In case the user specified an implementation despite the documentation warning.
* We could replace `type` by `interfaceType` and it would work most of the time,
* but we would still have some ClassCastExceptions for example if the given type
* is a java.lang.reflect.Proxy. It is probably better to let users know soon that
* they should specify an interface.
*/
throw new IllegalArgumentException(Resources.format(Resources.Keys.ExpectedInterface_2, interfaceType, type));
}
}
return type.cast(copyRecursively(type, metadata));
}
/**
* Performs the actual copy operation on a single metadata instance.
* This method is invoked by all public {@code copy(…)} method with the root {@code metadata}
* object in argument, then is invoked recursively for all properties in that metadata object.
* If a metadata property is a collection, then this method is invoked for each element in the collection.
*
* <p>Subclasses can override this method if they need some control on the copy process.</p>
*
* @param type the interface of the metadata object to copy, or {@code null} if unspecified.
* @param metadata the metadata object to copy, or {@code null}.
* @return a copy of the given metadata object, or {@code null} if the given argument is {@code null}.
* @throws UnsupportedOperationException if there is no implementation class for a metadata to copy,
* or an implementation class does not provide a public default constructor.
*/
protected Object copyRecursively(final Class<?> type, final Object metadata) {
if (metadata != null) {
final MetadataStandard std = getStandard(metadata);
if (std != null) {
final Object result = walk(std, type, metadata, false);
if (result != null) {
return result;
}
}
}
return metadata;
}
/**
* Invoked before the properties of a metadata instance are visited. This method creates a new instance,
* to be returned by {@link #result()}, and returns {@link Filter#WRITABLE_RESULT} for notifying the caller
* that write operations need to be performed on that {@code result} object.
*/
@Override
final Filter preVisit(final PropertyAccessor accessor) {
if (accessor.isWritable()) try {
target = accessor.implementation.getConstructor().newInstance();
return Filter.WRITABLE_RESULT;
} catch (ReflectiveOperationException e) {
throw new UnsupportedOperationException(Errors.format(Errors.Keys.CanNotCopy_1, accessor.type), Exceptions.unwrap(e));
} else {
target = null;
return Filter.NONE;
}
}
/**
* Returns the metadata instance resulting from the copy. This method is invoked <strong>before</strong>
* metadata properties are visited. The returned value is a new, initially empty, metadata instance
* created by {@link #preVisit(PropertyAccessor)}.
*/
@Override
final Object result() {
return target;
}
/**
* Verifies if the given metadata value is a map or a collection before to invoke
* {@link #copyRecursively(Class, Object)} for metadata elements. This method is
* invoked by {@link PropertyAccessor#walkWritable(MetadataVisitor, Object, Object)}.
*/
@Override
final Object visit(final Class<?> type, final Object metadata) {
if (!type.isInstance(metadata)) {
if (metadata instanceof Collection<?>) {
Collection<?> c = (Collection<?>) metadata;
if (c.isEmpty()) {
return null;
}
if (c instanceof EnumSet<?> || c instanceof CodeListSet<?>) {
/*
* Enum and CodeList elements cannot be cloned. Do not clone their collection neither;
* we presume that the setter method (to be invoked by reflection) will do that itself.
*/
} else {
final Object[] array = c.toArray();
for (int i=0; i < array.length; i++) {
array[i] = copyRecursively(type, array[i]);
}
c = Arrays.asList(array);
if (metadata instanceof Set<?>) {
c = new LinkedHashSet<>(c);
}
}
return c;
}
/*
* Maps are rare in GeoAPI interfaces derived from ISO 19115. The main one
* is `Map<Locale,Charset>` returned by `Metadata.getLocalesAndCharsets()`.
* We cannot copy those entries because the `type` argument is `Map.Entry`,
* which is not enough information. Recursive copy should not be necessary
* anyway because we do not use `Map` for storing other metadata objects.
* We do not clone the map because it should be done by the setter method.
*/
if (metadata instanceof Map<?,?>) {
return metadata;
}
}
return copyRecursively(type, metadata);
}
/**
* Returns the path to the currently copied property.
* Each element in the list is the UML identifier of a property.
* Element at index 0 is the name of the property of the root metadata object being copied.
* Element at index 1 is the name of a property which is a children of above property, <i>etc.</i>
*
* <p>The returned list is valid only during {@link #copyRecursively(Class, Object)} method execution.
* The content of this list become undetermined after the {@code copyRecursively} method returned.</p>
*
* @return the path to the currently copied property.
*
* @since 1.0
*/
@Override
protected List<String> getCurrentPropertyPath() {
return super.getCurrentPropertyPath();
}
}