001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.configuration2.tree;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.Iterator;
022import java.util.LinkedList;
023import java.util.List;
024
025import org.apache.commons.lang3.builder.ToStringBuilder;
026
027/**
028 * <p>
029 * A class for selecting a specific node based on a key or a set of keys.
030 * </p>
031 * <p>
032 * An instance of this class is initialized with the key of a node. It is also
033 * possible to concatenate multiple keys - e.g. if a sub key is to be
034 * constructed from another sub key. {@code NodeSelector} provides the
035 * {@code select()} method which evaluates the wrapped keys on a specified root
036 * node and returns the resulting unique target node. The class expects that the
037 * key(s) stored in an instance select exactly one target node. If this is not
038 * the case, result is <b>null</b> indicating that the selection criteria are
039 * not sufficient.
040 * </p>
041 * <p>
042 * Implementation node: Instances of this class are immutable. They can be
043 * shared between arbitrary components.
044 * </p>
045 *
046 * @since 2.0
047 */
048public class NodeSelector
049{
050    /** Stores the wrapped keys. */
051    private final List<String> nodeKeys;
052
053    /**
054     * Creates a new instance of {@code NodeSelector} and initializes it with
055     * the key to the target node.
056     *
057     * @param key the key
058     */
059    public NodeSelector(final String key)
060    {
061        this(Collections.singletonList(key));
062    }
063
064    /**
065     * Creates a new instance of {@code NodeSelector} and initializes it with
066     * the list of keys to be used as selection criteria.
067     *
068     * @param keys the keys for selecting nodes
069     */
070    private NodeSelector(final List<String> keys)
071    {
072        nodeKeys = keys;
073    }
074
075    /**
076     * Applies this {@code NodeSelector} on the specified root node. This method
077     * applies the selection criteria stored in this object and tries to
078     * determine a single target node. If this is successful, the target node is
079     * returned. Otherwise, result is <b>null</b>.
080     *
081     * @param root the root node on which to apply this selector
082     * @param resolver the {@code NodeKeyResolver}
083     * @param handler the {@code NodeHandler}
084     * @return the selected target node or <b>null</b>
085     */
086    public ImmutableNode select(final ImmutableNode root,
087            final NodeKeyResolver<ImmutableNode> resolver,
088            final NodeHandler<ImmutableNode> handler)
089    {
090        List<ImmutableNode> nodes = new LinkedList<>();
091        final Iterator<String> itKeys = nodeKeys.iterator();
092        getFilteredResults(root, resolver, handler, itKeys.next(), nodes);
093
094        while (itKeys.hasNext())
095        {
096            final String currentKey = itKeys.next();
097            final List<ImmutableNode> currentResults =
098                    new LinkedList<>();
099            for (final ImmutableNode currentRoot : nodes)
100            {
101                getFilteredResults(currentRoot, resolver, handler, currentKey,
102                        currentResults);
103            }
104            nodes = currentResults;
105        }
106
107        return nodes.size() == 1 ? nodes.get(0) : null;
108    }
109
110    /**
111     * Creates a sub {@code NodeSelector} object which uses the key(s) of this
112     * selector plus the specified key as selection criteria. This is useful
113     * when another selection is to be performed on the results of a first
114     * selector.
115     *
116     * @param subKey the additional key for the sub selector
117     * @return the sub {@code NodeSelector} instance
118     */
119    public NodeSelector subSelector(final String subKey)
120    {
121        final List<String> keys = new ArrayList<>(nodeKeys.size() + 1);
122        keys.addAll(nodeKeys);
123        keys.add(subKey);
124        return new NodeSelector(keys);
125    }
126
127    /**
128     * Compares this object with another one. Two instances of
129     * {@code NodeSelector} are considered equal if they have the same keys as
130     * selection criteria.
131     *
132     * @param obj the object to be compared
133     * @return a flag whether these objects are equal
134     */
135    @Override
136    public boolean equals(final Object obj)
137    {
138        if (this == obj)
139        {
140            return true;
141        }
142        if (!(obj instanceof NodeSelector))
143        {
144            return false;
145        }
146
147        final NodeSelector c = (NodeSelector) obj;
148        return nodeKeys.equals(c.nodeKeys);
149    }
150
151    /**
152     * Returns a hash code for this object.
153     *
154     * @return a hash code
155     */
156    @Override
157    public int hashCode()
158    {
159        return nodeKeys.hashCode();
160    }
161
162    /**
163     * Returns a string representation for this object. This string contains the
164     * keys to be used as selection criteria.
165     *
166     * @return a string for this object
167     */
168    @Override
169    public String toString()
170    {
171        return new ToStringBuilder(this).append("keys", nodeKeys).toString();
172    }
173
174    /**
175     * Executes a query for a given key and filters the results for nodes only.
176     *
177     * @param root the root node for the query
178     * @param resolver the {@code NodeKeyResolver}
179     * @param handler the {@code NodeHandler}
180     * @param key the key
181     * @param nodes here the results are stored
182     */
183    private void getFilteredResults(final ImmutableNode root,
184            final NodeKeyResolver<ImmutableNode> resolver,
185            final NodeHandler<ImmutableNode> handler, final String key,
186            final List<ImmutableNode> nodes)
187    {
188        final List<QueryResult<ImmutableNode>> results =
189                resolver.resolveKey(root, key, handler);
190        for (final QueryResult<ImmutableNode> result : results)
191        {
192            if (!result.isAttributeResult())
193            {
194                nodes.add(result.getNode());
195            }
196        }
197    }
198}