| /* |
| * 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; |
| } |
| } |