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 * https://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 possible to concatenate multiple keys - 033 * for example if a sub key is to be constructed from another sub key. {@code NodeSelector} provides the {@code select()} 034 * method which evaluates the wrapped keys on a specified root node and returns the resulting unique target node. The 035 * class expects that the key(s) stored in an instance select exactly one target node. If this is not the case, result 036 * is <strong>null</strong> indicating that the selection criteria are not sufficient. 037 * </p> 038 * <p> 039 * Implementation node: Instances of this class are immutable. They can be shared between arbitrary components. 040 * </p> 041 * 042 * @since 2.0 043 */ 044public class NodeSelector { 045 046 /** Stores the wrapped keys. */ 047 private final List<String> nodeKeys; 048 049 /** 050 * Creates a new instance of {@code NodeSelector} and initializes it with the list of keys to be used as selection 051 * criteria. 052 * 053 * @param keys the keys for selecting nodes 054 */ 055 private NodeSelector(final List<String> keys) { 056 nodeKeys = keys; 057 } 058 059 /** 060 * Creates a new instance of {@code NodeSelector} and initializes it with the key to the target node. 061 * 062 * @param key the key 063 */ 064 public NodeSelector(final String key) { 065 this(Collections.singletonList(key)); 066 } 067 068 /** 069 * Compares this object with another one. Two instances of {@code NodeSelector} are considered equal if they have the 070 * same keys as selection criteria. 071 * 072 * @param obj the object to be compared 073 * @return a flag whether these objects are equal 074 */ 075 @Override 076 public boolean equals(final Object obj) { 077 if (this == obj) { 078 return true; 079 } 080 if (!(obj instanceof NodeSelector)) { 081 return false; 082 } 083 084 final NodeSelector c = (NodeSelector) obj; 085 return nodeKeys.equals(c.nodeKeys); 086 } 087 088 /** 089 * Executes a query for a given key and filters the results for nodes only. 090 * 091 * @param root the root node for the query 092 * @param resolver the {@code NodeKeyResolver} 093 * @param handler the {@code NodeHandler} 094 * @param key the key 095 * @param nodes here the results are stored 096 */ 097 private void getFilteredResults(final ImmutableNode root, final NodeKeyResolver<ImmutableNode> resolver, final NodeHandler<ImmutableNode> handler, 098 final String key, final List<ImmutableNode> nodes) { 099 final List<QueryResult<ImmutableNode>> results = resolver.resolveKey(root, key, handler); 100 results.forEach(result -> { 101 if (!result.isAttributeResult()) { 102 nodes.add(result.getNode()); 103 } 104 }); 105 } 106 107 /** 108 * Returns a hash code for this object. 109 * 110 * @return a hash code 111 */ 112 @Override 113 public int hashCode() { 114 return nodeKeys.hashCode(); 115 } 116 117 /** 118 * Applies this {@code NodeSelector} on the specified root node. This method applies the selection criteria stored in 119 * this object and tries to determine a single target node. If this is successful, the target node is returned. 120 * Otherwise, result is <strong>null</strong>. 121 * 122 * @param root the root node on which to apply this selector 123 * @param resolver the {@code NodeKeyResolver} 124 * @param handler the {@code NodeHandler} 125 * @return the selected target node or <strong>null</strong> 126 */ 127 public ImmutableNode select(final ImmutableNode root, final NodeKeyResolver<ImmutableNode> resolver, final NodeHandler<ImmutableNode> handler) { 128 List<ImmutableNode> nodes = new LinkedList<>(); 129 final Iterator<String> itKeys = nodeKeys.iterator(); 130 getFilteredResults(root, resolver, handler, itKeys.next(), nodes); 131 132 while (itKeys.hasNext()) { 133 final String currentKey = itKeys.next(); 134 final List<ImmutableNode> currentResults = new LinkedList<>(); 135 nodes.forEach(currentRoot -> getFilteredResults(currentRoot, resolver, handler, currentKey, currentResults)); 136 nodes = currentResults; 137 } 138 139 return nodes.size() == 1 ? nodes.get(0) : null; 140 } 141 142 /** 143 * Creates a sub {@code NodeSelector} object which uses the key(s) of this selector plus the specified key as selection 144 * criteria. This is useful when another selection is to be performed on the results of a first selector. 145 * 146 * @param subKey the additional key for the sub selector 147 * @return the sub {@code NodeSelector} instance 148 */ 149 public NodeSelector subSelector(final String subKey) { 150 final List<String> keys = new ArrayList<>(nodeKeys.size() + 1); 151 keys.addAll(nodeKeys); 152 keys.add(subKey); 153 return new NodeSelector(keys); 154 } 155 156 /** 157 * Returns a string representation for this object. This string contains the keys to be used as selection criteria. 158 * 159 * @return a string for this object 160 */ 161 @Override 162 public String toString() { 163 return new ToStringBuilder(this).append("keys", nodeKeys).toString(); 164 } 165}