blob: 4c14e626325e69dcfbad34ff27ca95c921ba7dd9 [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.Iterator;
import java.util.Collection;
import org.opengis.util.ControlledVocabulary;
import org.apache.sis.util.Emptiable;
import org.apache.sis.internal.util.CollectionsExt;
import static org.apache.sis.metadata.ValueExistencePolicy.*;
/**
* Implementation of {@link AbstractMetadata#isEmpty()} and {@link ModifiableMetadata#prune()} methods.
*
* The {@link #visited} map inherited by this class is the thread-local map of metadata objects already tested.
* Keys are metadata instances, and values are the results of the {@code metadata.isEmpty()} operation.
* If the final operation requested by the user is {@code isEmpty()}, then this map will contain one of
* few {@code false} values since the walk in the tree will stop at the first {@code false} value found.
* If the final operation requested by the user is {@code prune()}, then this map will contain a mix of
* {@code false} and {@code true} values since the operation will unconditionally walk through the entire tree.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 0.3
* @module
*/
final class Pruner extends MetadataVisitor<Boolean> {
/**
* Provider of visitor instances.
*/
private static final ThreadLocal<Pruner> VISITORS = ThreadLocal.withInitial(Pruner::new);
/**
* {@code true} for removing empty properties.
*/
private boolean prune;
/**
* Whether the metadata is empty.
*/
private boolean isEmpty;
/**
* Creates a new object which will test or prune metadata properties.
*/
private Pruner() {
}
/**
* Returns the thread-local variable that created this {@code Pruner} instance.
*/
@Override
final ThreadLocal<Pruner> creator() {
return VISITORS;
}
/**
* Returns {@code true} if all properties in the given metadata are null or empty.
* This method is the entry point for the {@link AbstractMetadata#isEmpty()} and
* {@link ModifiableMetadata#prune()} public methods.
*
* @param metadata the metadata object.
* @param prune {@code true} for deleting empty entries.
* @return {@code true} if all metadata properties are null or empty.
*/
static boolean isEmpty(final AbstractMetadata metadata, final boolean prune) {
final Pruner visitor = VISITORS.get();
final boolean p = visitor.prune;
visitor.prune = prune;
final Boolean r = visitor.walk(metadata.getStandard(), metadata.getInterface(), metadata, false);
visitor.prune = p;
return (r != null) && r; // If there is a cycle (r == null), then the metadata is non-empty.
}
/**
* Marks a metadata instance as empty before we start visiting its non-null properties.
* If the metadata does not contain any property, then the {@link #isEmpty} field will stay {@code true}.
*
* @return {@link Filter#NON_EMPTY} since this visitor is not restricted to writable properties.
* We need to visit all readable properties even for pruning operation since we need to
* determine if the metadata is empty.
*/
@Override
Filter preVisit(final PropertyAccessor accessor) {
isEmpty = true;
return Filter.NON_EMPTY;
}
/**
* Invoked for each element in the metadata to test or prune. This method is invoked only for new elements
* not yet processed by {@code Pruner}. The element may be a value object or a collection. For convenience
* we will proceed as if we had only collections, wrapping value object in a singleton collection.
*
* @param type the type of elements. Note that this is not necessarily the type
* of given {@code element} argument if the later is a collection.
* @param value value of the metadata element being visited.
*/
@Override
Object visit(final Class<?> type, final Object value) {
final boolean isEmptyMetadata = isEmpty; // Save the value in case it is overwritten by recursive invocations.
boolean isEmptyValue = true;
final Collection<?> values = CollectionsExt.toCollection(value);
for (final Iterator<?> it = values.iterator(); it.hasNext();) {
final Object element = it.next();
if (!isNullOrEmpty(element)) {
/*
* At this point, 'element' is not an empty CharSequence, Collection or array.
* It may be another metadata, a Java primitive type or user-defined object.
*
* - For AbstractMetadata, delegate to the public API in case it has been overriden.
* - For user-defined Emptiable, delegate to the user's isEmpty() method. Note that
* we test at different times depending if 'prune' is true of false.
*/
boolean isEmptyElement = false;
if (element instanceof AbstractMetadata) {
final AbstractMetadata md = (AbstractMetadata) element;
if (prune) md.prune();
isEmptyElement = md.isEmpty();
} else if (!prune && element instanceof Emptiable) {
isEmptyElement = ((Emptiable) element).isEmpty();
// If 'prune' is true, we will rather test for Emptiable after our pruning attempt.
} else if (!(element instanceof ControlledVocabulary)) {
final MetadataStandard standard = MetadataStandard.forClass(element.getClass());
if (standard != null) {
/*
* For implementation that are not subtype of AbstractMetadata but nevertheless
* implement some metadata interfaces, we will invoke recursively this method.
*/
final Boolean r = walk(standard, type, element, false);
if (r != null) {
isEmptyElement = r;
if (!isEmptyElement && element instanceof Emptiable) {
isEmptyElement = ((Emptiable) element).isEmpty();
}
}
} else if (element instanceof Number) {
isEmptyElement = Double.isNaN(((Number) element).doubleValue());
} else if (element instanceof Boolean) {
// Typically methods of the kind 'isFooAvailable()'.
isEmptyElement = !((Boolean) element);
}
}
if (!isEmptyElement) {
/*
* At this point, we have determined that the property is not empty.
* If we are not removing empty nodes, there is no need to continue.
*/
if (!prune) {
isEmpty = false;
return SKIP_SIBLINGS;
}
isEmptyValue = false;
continue;
}
}
/*
* Found an empty element. Remove it if the element is part of a collection,
* then move to the next element in the collection (not yet the next property).
*/
if (prune && values == value) {
it.remove();
}
}
/*
* If all elements were empty, set the whole property to 'null'.
*/
isEmpty = isEmptyMetadata & isEmptyValue;
return isEmptyValue & prune ? null : value;
}
/**
* Returns the result of visiting all elements in the metadata.
*/
@Override
Boolean result() {
return isEmpty;
}
}