View Javadoc
1   package com.exsoinn.util.epf;
2   
3   import net.jcip.annotations.Immutable;
4   
5   import java.util.*;
6   import java.util.concurrent.ConcurrentHashMap;
7   import java.util.stream.Collectors;
8   
9   /**
10   * Provides unmodifiable instance of {@code SearchPath} objects, via the {@link SearchPath#valueOf(String)} constructor. Once
11   * constructed, you can traverse the nodes by invoking {@link SearchPath#advanceToNextNode()}. If already at last node
12   * and you invoke {@link SearchPath#advanceToNextNode()} again, then it will circle back to the first node. In other words
13   * this class is modular in that way, so be mindful of this when you write code that invokes it.
14   * Methods such as {@link SearchPath#currentNode()} and {@link SearchPath#isAtEndOfSearchPath()}, allow you to query
15   * the current state of the {@code SearchPath} object. Also because this class implements {@link List}, all the read-only
16   * methods are available for you to invoke. However invoking methods that modify state will throw
17   * {@link UnsupportedOperationException}.
18   *
19   * Last but not least, for space efficiency, instances of this class are internally cached.
20   *
21   * Created by QuijadaJ on 5/3/2017.
22   */
23  @Immutable
24  public final class SearchPath implements List<String> {
25      private final List<String> searchPath;
26      private final String searchPathAsString;
27      private final int currentNodeIndex;
28      private final boolean atEndOfSearchPath;
29      private static final String KEY_SEP = "__";
30      private static final int INIT_SP = -1;
31      private final static Map<String, SearchPath> cachedSearchPaths = new ConcurrentHashMap<>();
32  
33  
34  
35      static String generateCacheKey(String pSearchPath, int pIdx, boolean pAtEnd) {
36          StringBuilder key = new StringBuilder();
37          key.append(pSearchPath);
38          key.append(KEY_SEP);
39          key.append(pIdx);
40          key.append(KEY_SEP);
41          key.append(pAtEnd);
42          return key.toString();
43      }
44  
45      SearchPath(String pSearchPath, int pNodeIdx, boolean pAtEnd) {
46          if (pNodeIdx < 0 && pAtEnd) {
47              searchPath = Collections.emptyList();
48          } else {
49              searchPath = parseElementSearchPath(pSearchPath);
50          }
51          searchPathAsString = pSearchPath;
52          currentNodeIndex = pNodeIdx;
53          atEndOfSearchPath = pAtEnd;
54      }
55  
56  
57      /**
58       * This public static method builds a {@code SearchPath}. The format is a dot (.) separated string of tokens, for example:
59       *
60       * someNode.innerNode[0].finalNode
61       *
62       * The search path specifies what information in the underlying hierarchical data the caller is interested in finding. Notice in the
63       * example above one of the tokens contains square brackets. This is used to indicate if an array-like structure will
64       * be encountered somewhere along the search path.
65       * @param pSearchPath - pSearchPath
66       * @return - TODO
67       */
68      public static SearchPath valueOf(final String pSearchPath) {
69          final boolean atEnd = false;
70          String key = generateCacheKey(pSearchPath, INIT_SP, atEnd);
71          SearchPath sp = cachedSearchPaths.get(key);
72          if (null == sp) {
73              sp = new SearchPath(pSearchPath, INIT_SP, atEnd);
74              SearchPath spFromCache = cachedSearchPaths.putIfAbsent(key, sp);
75              sp = (null == spFromCache) ? sp : spFromCache;
76          }
77  
78          return sp;
79      }
80  
81  
82      /**
83       * Traverses the {@code SearchPath} object in circular/modular manner, meaning that when you have reached
84       * the last node by advancing, if you invoke {@link SearchPath#advanceToNextNode()} again it will go back
85       * to first note. This means you can traverse the {@code SearchPath} object indefinitely.
86       * @return - TODO
87       */
88      final SearchPath advanceToNextNode() {
89          /*
90           * If this go around is advancing to the last node in search path, set "atEndOfSearchPath"
91           * to true. If we've already advanced to last node, and are trying to advance again, set
92           * current index to -1. That's the reason why we're "pre-checking" the effect of advancing to
93           * next node index via "(currentNodeIndex + 1) <= searchPath.size()-1"
94           */
95          int nextNodeIdx = (currentNodeIndex + 1) <= searchPath.size()-1 ? (currentNodeIndex + 1) : -1 ;
96  
97          /**
98           * Set flag which indicates that this time we advanced to last node of search path. If caller again
99           * invokes advanceToNextNode() after having already advanced to last node, then "atEnd" gets set to false and
100          * again you can traverse the search path object.
101          */
102         boolean atEnd = (nextNodeIdx >= searchPath.size()-1);
103 
104         String key = generateCacheKey(searchPathAsString, nextNodeIdx, atEnd);
105         SearchPath sp = cachedSearchPaths.get(key);
106         if (null == sp) {
107             sp = new SearchPath(searchPathAsString, nextNodeIdx, atEnd);
108             SearchPath spFromCache = cachedSearchPaths.putIfAbsent(key, sp);
109             sp = (null == spFromCache) ? sp : spFromCache;
110         }
111         return sp;
112     }
113 
114 
115     public String lastNode() {
116         return searchPath.get(searchPath.size() - 1);
117     }
118 
119     public static void main(String[] args) {
120         SearchPath sp = valueOf("node1.node2.node3.node4");
121         SearchPath newSp = sp.advanceToNextNode();
122         newSp = newSp.advanceToNextNode();
123         newSp = newSp.advanceToNextNode();
124         newSp = newSp.advanceToNextNode();
125         newSp = newSp.advanceToNextNode();
126         newSp = newSp.advanceToNextNode();
127         newSp = newSp.advanceToNextNode();
128         newSp = newSp.advanceToNextNode();
129         newSp = newSp.advanceToNextNode();
130     }
131 
132     final String currentNode() {
133         return get(currentNodeIndex);
134     }
135 
136     final int currentNodeIndex() {
137         return currentNodeIndex;
138     }
139 
140     @Override
141     public int size() {
142         return searchPath.size();
143     }
144 
145     @Override
146     public boolean isEmpty() {
147         return searchPath.isEmpty();
148     }
149 
150     @Override
151     public boolean contains(Object o) {
152         return searchPath.contains(o);
153     }
154 
155     @Override
156     public Iterator<String> iterator() {
157         return new ArrayList<>(searchPath).iterator();
158     }
159 
160     @Override
161     public Object[] toArray() {
162         return searchPath.toArray();
163     }
164 
165     @Override
166     public <T> T[] toArray(T[] a) {
167         return searchPath.toArray(a);
168     }
169 
170     @Override
171     public boolean add(String s) {
172         throw new UnsupportedOperationException();
173     }
174 
175     @Override
176     public boolean remove(Object o) {
177         throw new UnsupportedOperationException();
178     }
179 
180     @Override
181     public boolean containsAll(Collection<?> c) {
182         return searchPath.containsAll(c);
183     }
184 
185     @Override
186     public boolean addAll(Collection<? extends String> c) {
187         throw new UnsupportedOperationException();
188     }
189 
190     @Override
191     public boolean addAll(int index, Collection<? extends String> c) {
192         throw new UnsupportedOperationException();
193     }
194 
195     @Override
196     public boolean removeAll(Collection<?> c) {
197         throw new UnsupportedOperationException();
198     }
199 
200     @Override
201     public boolean retainAll(Collection<?> c) {
202         throw new UnsupportedOperationException();
203     }
204 
205     @Override
206     public void clear() {
207         throw new UnsupportedOperationException();
208     }
209 
210     @Override
211     public String get(int index) {
212         return searchPath.get(index);
213     }
214 
215     @Override
216     public String set(int index, String element) {
217         throw new UnsupportedOperationException();
218     }
219 
220     @Override
221     public void add(int index, String element) {
222         throw new UnsupportedOperationException();
223     }
224 
225     @Override
226     public String remove(int index) {
227         throw new UnsupportedOperationException();
228     }
229 
230     @Override
231     public int indexOf(Object o) {
232         return searchPath.indexOf(o);
233     }
234 
235     @Override
236     public int lastIndexOf(Object o) {
237         return searchPath.lastIndexOf(o);
238     }
239 
240     @Override
241     public ListIterator<String> listIterator() {
242         return new ArrayList<>(searchPath).listIterator();
243     }
244 
245     @Override
246     public ListIterator<String> listIterator(int index) {
247         return new ArrayList<>(searchPath).listIterator(index);
248     }
249 
250     @Override
251     public List<String> subList(int fromIndex, int toIndex) {
252         return new ArrayList<>(searchPath).subList(fromIndex, toIndex);
253     }
254 
255 
256     /**
257      * Express the search path as a dot separated {@link String} of tokens, which was the original
258      * format provided when this {@code SearchPath} instance was constructed.
259      * @return - Search path string
260      */
261     @Override
262     public String toString() {
263         return searchPathAsString;
264     }
265 
266     @Override
267     public boolean equals(Object o) {
268         return searchPath.equals(o);
269     }
270 
271     @Override
272     public int hashCode() {
273         return searchPath.hashCode();
274     }
275 
276     private static List<String> parseElementSearchPath(String pElemSearchPath) {
277         String[] nodes = pElemSearchPath.split("\\.");
278         return Arrays.stream(nodes).collect(Collectors.toCollection(() -> new ArrayList(nodes.length)));
279     }
280 
281     boolean isAtEndOfSearchPath() {
282         return atEndOfSearchPath;
283     }
284 }