blob: 80859d28373c52b4c7fcdf932576682ae097fdc6 [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.Arrays;
import java.util.List;
import java.util.Map;
import java.util.IdentityHashMap;
import java.util.ConcurrentModificationException;
import org.apache.sis.internal.system.Semaphores;
import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.apache.sis.util.Exceptions;
/**
* A visitor of metadata properties with a safety against infinite recursivity.
* The visitor may compute a result, for example a hash code value or a boolean
* testing whether the metadata is empty. Each {@code MetadataVisitor} instance
* is used by one thread; this class does not need to be thread-safe.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
*
* @param <R> the type of result of walking in the metadata.
*
* @since 1.0
* @module
*/
abstract class MetadataVisitor<R> {
/**
* Sentinel value that may be returned by {@link #visit(Class, Object)} for notifying the walker to stop.
* This value causes {@link #walk walk(…)} to stop its iteration, but does not stop iteration by the parent
* if {@code walk(…)} has been invoked recursively. The {@link #result()} method shall return a valid result
* even if the iteration has been terminated.
*/
static final Object SKIP_SIBLINGS = InterruptedException.class; // The choice of this type is arbitrary.
/**
* A guard against infinite recursivity in {@link #walk(MetadataStandard, Class, Object, boolean)}.
* Keys are visited metadata instances and values are computed value. The value may be null if
* the computation is in progress.
*
* <h4>The problem</h4>
* Cyclic associations can exist in ISO 19115 metadata. For example {@code Instrument} has a reference
* to the platform it is mounted on, and the {@code Platform} has a list of instruments mounted on it.
* Consequently walking down the metadata tree can cause infinite recursivity, unless we keep trace of
* previously visited metadata objects in order to avoid visiting them again.
*
* We use an {@link IdentityHashMap} for that purpose, since the recursivity problem exists only when revisiting
* the exact same instance. Furthermore, {@code HashMap} would not suit since it invokes {@code equals(Object)}
* and {@code hashCode()}, which are among the methods that we want to avoid invoking twice.
*/
private final Map<Object,R> visited;
/**
* The name of the property being visited as the last element of the queue. If {@code visit} method
* is invoked recursively, then the properties before the last one are the parent properties.
* The number of valid elements is {@link #nestedCount}.
*/
private String[] propertyPath;
/**
* Count of nested calls to {@link #walk(MetadataStandard, Class, Object, boolean)} method.
* When this count reach zero, the visitor should be removed from the thread local variable.
*/
private int nestedCount;
/**
* Value of the {@link Semaphores#NULL_COLLECTION} flag when we started the walk.
* The {@code NULL_COLLECTION} flag prevents creation of new empty collections by getter methods
* (a consequence of lazy instantiation). The intent is to avoid creation of unnecessary objects
* for all unused properties. Users should not see behavioral difference, except if they override
* some getters with an implementation invoking other getters. However in such cases, users would
* have been exposed to null values at XML marshalling time anyway.
*/
private boolean allowNull;
/**
* Creates a new visitor.
*/
MetadataVisitor() {
visited = new IdentityHashMap<>();
propertyPath = new String[6];
}
/**
* The thread-local variable that created this {@code MetadataVisitor} instance.
* This is usually a static final {@code VISITORS} constant defined in the subclass.
* May be {@code null} if this visitor does not use thread-local instances.
*/
ThreadLocal<? extends MetadataVisitor<?>> creator() {
return null;
}
/**
* Sets the name of the method being visited. This is invoked by {@code PropertyAccessor.walk} methods only.
*/
final void setCurrentProperty(final String name) {
propertyPath[nestedCount - 1] = name;
}
/**
* Returns the path to the currently visited 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 visited.
* 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 #visit(Class, Object)} method execution.
* The content of this list become undetermined after the {@code visit} method returned.</p>
*
* @return the path to the currently visited property.
*/
List<String> getCurrentPropertyPath() {
return UnmodifiableArrayList.wrap(propertyPath, 0, nestedCount);
}
/**
* Invokes {@link #visit(Class, Object)} for all elements of the given metadata if that metadata has not
* already been visited. The computation result is returned (may be the result of a previous computation).
*
* <p>This method is typically invoked recursively while we iterate down the metadata tree.
* It creates a map of visited nodes when the iteration begin, and deletes that map when the
* iteration ends.</p>
*
* @param standard the metadata standard implemented by the object to visit.
* @param type the standard interface implemented by the metadata object, or {@code null} if unknown.
* @param metadata the metadata to visit.
* @param mandatory {@code true} for throwing an exception for unsupported metadata type, or {@code false} for ignoring.
* @return the value of {@link #result()} after all elements of the given metadata have been visited.
* If the given metadata instance has already been visited, then this is the previously computed value.
* If the computation is in progress (e.g. a cyclic graph), then this method returns {@code null}.
*/
final R walk(final MetadataStandard standard, final Class<?> type, final Object metadata, final boolean mandatory) {
if (!visited.containsKey(metadata)) { // Reminder: the associated value may be null.
final PropertyAccessor accessor = standard.getAccessor(new CacheKey(metadata.getClass(), type), mandatory);
if (accessor != null) {
final Filter filter = preVisit(accessor);
final boolean preconstructed;
final R sentinel;
switch (filter) {
case NONE: return null;
case WRITABLE_RESULT: preconstructed = true; sentinel = result(); break;
default: preconstructed = false; sentinel = null; break;
}
if (visited.put(metadata, sentinel) != null) {
// Should never happen, unless this method is invoked concurrently in another thread.
throw new ConcurrentModificationException();
}
if (nestedCount >= propertyPath.length) {
propertyPath = Arrays.copyOf(propertyPath, nestedCount * 2);
}
if (nestedCount++ == 0) {
/*
* The NULL_COLLECTION semaphore prevents creation of new empty collections by getter methods
* (a consequence of lazy instantiation). The intent is to avoid creation of unnecessary objects
* for all unused properties. Users should not see behavioral difference, except if they override
* some getters with an implementation invoking other getters. However in such cases, users would
* have been exposed to null values at XML marshalling time anyway.
*/
allowNull = Semaphores.queryAndSet(Semaphores.NULL_COLLECTION);
}
try {
switch (filter) {
case NON_EMPTY: accessor.walkReadable(this, metadata); break;
case WRITABLE: accessor.walkWritable(this, metadata, metadata); break;
case WRITABLE_RESULT: accessor.walkWritable(this, metadata, sentinel); break;
}
} catch (MetadataVisitorException e) {
throw e;
} catch (Exception e) {
e = Exceptions.unwrap(e);
throw new MetadataVisitorException(Arrays.copyOf(propertyPath, nestedCount), accessor.type, e);
} finally {
if (--nestedCount == 0) {
if (!allowNull) {
Semaphores.clear(Semaphores.NULL_COLLECTION);
}
final ThreadLocal<? extends MetadataVisitor<?>> creator = creator();
if (creator != null) creator.remove();
}
}
final R result = preconstructed ? sentinel : result();
if (visited.put(metadata, result) != sentinel) {
throw new ConcurrentModificationException();
}
return result;
}
}
return visited.get(metadata);
}
/**
* Filter the properties to visit. A value of this enumeration is returned by {@link #preVisit(PropertyAccessor)}
* before the properties of a metadata instance are visited.
*/
enum Filter {
/**
* Do not visit any property (skip completely the metadata).
*/
NONE,
/**
* Visit all non-null and non-empty standard properties.
*/
NON_EMPTY,
/**
* Visit all writable properties. May include some non-standard properties.
*/
WRITABLE,
/**
* Same as {@link #WRITABLE}, but write properties in the object returned by {@link #result()}.
* This mode implies that {@code result()} is invoked <strong>before</strong> metadata properties
* are visited instead than after.
*/
WRITABLE_RESULT
}
/**
* Invoked when a new metadata is about to be visited. After this method has been invoked,
* {@link #visit(Class, Object)} will be invoked for each property in the metadata object.
*
* @param accessor information about the standard interface and implementation of the metadata being visited.
* @return most common values are {@code NON_EMPTY} for visiting all non-empty properties (the default),
* or {@code WRITABLE} for visiting only writable properties.
*/
Filter preVisit(PropertyAccessor accessor) {
return Filter.NON_EMPTY;
}
/**
* Invoked when a new metadata property is being visited. The current property value is given in argument.
* The return value is interpreted as below:
*
* <ul>
* <li>{@link #SKIP_SIBLINGS}: do not iterate over other properties of current metadata,
* but continue iteration over properties of the parent metadata.</li>
* <li>{@code value}: continue with next sibling property without setting any value.</li>
* <li>{@code null}: clear the property value, then continue with next sibling property.
* If the property type is a collection, then "null" value is interpreted as an instruction
* to {@linkplain java.util.Collection#clear() clear} the collection.</li>
* <li>Any other value: set the property value to the given value,
* then continue with next sibling property.</li>
* </ul>
*
* @param type the type of elements. Note that this is not necessarily the type
* of given {@code value} argument if the later is a collection.
* @param value value of the metadata property being visited.
* @return the new property value to set, or {@link #SKIP_SIBLINGS}.
* @throws Exception if the visit operation failed.
*/
abstract Object visit(Class<?> type, Object value) throws Exception;
/**
* Returns the result of visiting all elements in a metadata instance.
* This method is invoked zero or one time per metadata instance.
* The invocation time depends on the value returned by {@link #preVisit(PropertyAccessor)}:
*
* <ul>
* <li>If {@link Filter#NONE}, then this method is never invoked for the current metadata instance.</li>
* <li>If {@link Filter#NON_EMPTY} or {@link Filter#WRITABLE}, then this method is invoked after all properties
* have been visited or after {@link #visit(Class, Object)} returned {@link #SKIP_SIBLINGS}.</li>
* <li>If {@link Filter#WRITABLE_RESULT}, then this method is invoked <strong>before</strong> metadata
* properties are visited. In such case, this method should return an initially empty instance.</li>
* </ul>
*
* The value returned by this method will be cached in case the same metadata instance is revisited again.
* This value can be {@code null}.
*/
R result() {
return null;
}
}