JAL-3438 spotless for 2.11.2.0
[jalview.git] / src / org / json / JSONPointer.java
1 package org.json;
2
3 import static java.lang.String.format;
4
5 import java.io.UnsupportedEncodingException;
6 import java.net.URLDecoder;
7 import java.net.URLEncoder;
8 import java.util.ArrayList;
9 import java.util.Collections;
10 import java.util.List;
11
12 /*
13 Copyright (c) 2002 JSON.org
14
15 Permission is hereby granted, free of charge, to any person obtaining a copy
16 of this software and associated documentation files (the "Software"), to deal
17 in the Software without restriction, including without limitation the rights
18 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19 copies of the Software, and to permit persons to whom the Software is
20 furnished to do so, subject to the following conditions:
21
22 The above copyright notice and this permission notice shall be included in all
23 copies or substantial portions of the Software.
24
25 The Software shall be used for Good, not Evil.
26
27 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33 SOFTWARE.
34 */
35
36 /**
37  * A JSON Pointer is a simple query language defined for JSON documents by
38  * <a href="https://tools.ietf.org/html/rfc6901">RFC 6901</a>.
39  * 
40  * In a nutshell, JSONPointer allows the user to navigate into a JSON document
41  * using strings, and retrieve targeted objects, like a simple form of XPATH.
42  * Path segments are separated by the '/' char, which signifies the root of the
43  * document when it appears as the first char of the string. Array elements are
44  * navigated using ordinals, counting from 0. JSONPointer strings may be
45  * extended to any arbitrary number of segments. If the navigation is
46  * successful, the matched item is returned. A matched item may be a JSONObject,
47  * a JSONArray, or a JSON value. If the JSONPointer string building fails, an
48  * appropriate exception is thrown. If the navigation fails to find a match, a
49  * JSONPointerException is thrown.
50  * 
51  * @author JSON.org
52  * @version 2016-05-14
53  */
54 public class JSONPointer
55 {
56
57   // used for URL encoding and decoding
58   private static final String ENCODING = "utf-8";
59
60   /**
61    * This class allows the user to build a JSONPointer in steps, using exactly
62    * one segment in each step.
63    */
64   public static class Builder
65   {
66
67     // Segments for the eventual JSONPointer string
68     private final List<String> refTokens = new ArrayList<String>();
69
70     /**
71      * Creates a {@code JSONPointer} instance using the tokens previously set
72      * using the {@link #append(String)} method calls.
73      */
74     public JSONPointer build()
75     {
76       return new JSONPointer(this.refTokens);
77     }
78
79     /**
80      * Adds an arbitrary token to the list of reference tokens. It can be any
81      * non-null value.
82      * 
83      * Unlike in the case of JSON string or URI fragment representation of JSON
84      * pointers, the argument of this method MUST NOT be escaped. If you want to
85      * query the property called {@code "a~b"} then you should simply pass the
86      * {@code "a~b"} string as-is, there is no need to escape it as
87      * {@code "a~0b"}.
88      * 
89      * @param token
90      *          the new token to be appended to the list
91      * @return {@code this}
92      * @throws NullPointerException
93      *           if {@code token} is null
94      */
95     public Builder append(String token)
96     {
97       if (token == null)
98       {
99         throw new NullPointerException("token cannot be null");
100       }
101       this.refTokens.add(token);
102       return this;
103     }
104
105     /**
106      * Adds an integer to the reference token list. Although not necessarily,
107      * mostly this token will denote an array index.
108      * 
109      * @param arrayIndex
110      *          the array index to be added to the token list
111      * @return {@code this}
112      */
113     public Builder append(int arrayIndex)
114     {
115       this.refTokens.add(String.valueOf(arrayIndex));
116       return this;
117     }
118   }
119
120   /**
121    * Static factory method for {@link Builder}. Example usage:
122    * 
123    * <pre>
124    * <code>
125    * JSONPointer pointer = JSONPointer.builder()
126    *       .append("obj")
127    *       .append("other~key").append("another/key")
128    *       .append("\"")
129    *       .append(0)
130    *       .build();
131    * </code>
132    * </pre>
133    * 
134    * @return a builder instance which can be used to construct a
135    *         {@code JSONPointer} instance by chained
136    *         {@link Builder#append(String)} calls.
137    */
138   public static Builder builder()
139   {
140     return new Builder();
141   }
142
143   // Segments for the JSONPointer string
144   private final List<String> refTokens;
145
146   /**
147    * Pre-parses and initializes a new {@code JSONPointer} instance. If you want
148    * to evaluate the same JSON Pointer on different JSON documents then it is
149    * recommended to keep the {@code JSONPointer} instances due to performance
150    * considerations.
151    * 
152    * @param pointer
153    *          the JSON String or URI Fragment representation of the JSON
154    *          pointer.
155    * @throws IllegalArgumentException
156    *           if {@code pointer} is not a valid JSON pointer
157    */
158   public JSONPointer(final String pointer)
159   {
160     if (pointer == null)
161     {
162       throw new NullPointerException("pointer cannot be null");
163     }
164     if (pointer.isEmpty() || pointer.equals("#"))
165     {
166       this.refTokens = Collections.emptyList();
167       return;
168     }
169     String refs;
170     if (pointer.startsWith("#/"))
171     {
172       refs = pointer.substring(2);
173       try
174       {
175         refs = URLDecoder.decode(refs, ENCODING);
176       } catch (UnsupportedEncodingException e)
177       {
178         throw new RuntimeException(e);
179       }
180     }
181     else if (pointer.startsWith("/"))
182     {
183       refs = pointer.substring(1);
184     }
185     else
186     {
187       throw new IllegalArgumentException(
188               "a JSON pointer should start with '/' or '#/'");
189     }
190     this.refTokens = new ArrayList<String>();
191     int slashIdx = -1;
192     int prevSlashIdx = 0;
193     do
194     {
195       prevSlashIdx = slashIdx + 1;
196       slashIdx = refs.indexOf('/', prevSlashIdx);
197       if (prevSlashIdx == slashIdx || prevSlashIdx == refs.length())
198       {
199         // found 2 slashes in a row ( obj//next )
200         // or single slash at the end of a string ( obj/test/ )
201         this.refTokens.add("");
202       }
203       else if (slashIdx >= 0)
204       {
205         final String token = refs.substring(prevSlashIdx, slashIdx);
206         this.refTokens.add(unescape(token));
207       }
208       else
209       {
210         // last item after separator, or no separator at all.
211         final String token = refs.substring(prevSlashIdx);
212         this.refTokens.add(unescape(token));
213       }
214     } while (slashIdx >= 0);
215     // using split does not take into account consecutive separators or "ending
216     // nulls"
217     // for (String token : refs.split("/")) {
218     // this.refTokens.add(unescape(token));
219     // }
220   }
221
222   public JSONPointer(List<String> refTokens)
223   {
224     this.refTokens = new ArrayList<String>(refTokens);
225   }
226
227   private String unescape(String token)
228   {
229     return token.replace("~1", "/").replace("~0", "~").replace("\\\"", "\"")
230             .replace("\\\\", "\\");
231   }
232
233   /**
234    * Evaluates this JSON Pointer on the given {@code document}. The
235    * {@code document} is usually a {@link JSONObject} or a {@link JSONArray}
236    * instance, but the empty JSON Pointer ({@code ""}) can be evaluated on any
237    * JSON values and in such case the returned value will be {@code document}
238    * itself.
239    * 
240    * @param document
241    *          the JSON document which should be the subject of querying.
242    * @return the result of the evaluation
243    * @throws JSONPointerException
244    *           if an error occurs during evaluation
245    */
246   public Object queryFrom(Object document) throws JSONPointerException
247   {
248     if (this.refTokens.isEmpty())
249     {
250       return document;
251     }
252     Object current = document;
253     for (String token : this.refTokens)
254     {
255       if (current instanceof JSONObject)
256       {
257         current = ((JSONObject) current).opt(unescape(token));
258       }
259       else if (current instanceof JSONArray)
260       {
261         current = readByIndexToken(current, token);
262       }
263       else
264       {
265         throw new JSONPointerException(format(
266                 "value [%s] is not an array or object therefore its key %s cannot be resolved",
267                 current, token));
268       }
269     }
270     return current;
271   }
272
273   /**
274    * Matches a JSONArray element by ordinal position
275    * 
276    * @param current
277    *          the JSONArray to be evaluated
278    * @param indexToken
279    *          the array index in string form
280    * @return the matched object. If no matching item is found a
281    * @throws JSONPointerException
282    *           is thrown if the index is out of bounds
283    */
284   private Object readByIndexToken(Object current, String indexToken)
285           throws JSONPointerException
286   {
287     try
288     {
289       int index = Integer.parseInt(indexToken);
290       JSONArray currentArr = (JSONArray) current;
291       if (index >= currentArr.length())
292       {
293         throw new JSONPointerException(format(
294                 "index %d is out of bounds - the array has %d elements",
295                 index, currentArr.length()));
296       }
297       try
298       {
299         return currentArr.get(index);
300       } catch (JSONException e)
301       {
302         throw new JSONPointerException(
303                 "Error reading value at index position " + index, e);
304       }
305     } catch (NumberFormatException e)
306     {
307       throw new JSONPointerException(
308               format("%s is not an array index", indexToken), e);
309     }
310   }
311
312   /**
313    * Returns a string representing the JSONPointer path value using string
314    * representation
315    */
316   @Override
317   public String toString()
318   {
319     StringBuilder rval = new StringBuilder("");
320     for (String token : this.refTokens)
321     {
322       rval.append('/').append(escape(token));
323     }
324     return rval.toString();
325   }
326
327   /**
328    * Escapes path segment values to an unambiguous form. The escape char to be
329    * inserted is '~'. The chars to be escaped are ~, which maps to ~0, and /,
330    * which maps to ~1. Backslashes and double quote chars are also escaped.
331    * 
332    * @param token
333    *          the JSONPointer segment value to be escaped
334    * @return the escaped value for the token
335    */
336   private String escape(String token)
337   {
338     return token.replace("~", "~0").replace("/", "~1").replace("\\", "\\\\")
339             .replace("\"", "\\\"");
340   }
341
342   /**
343    * Returns a string representing the JSONPointer path value using URI fragment
344    * identifier representation
345    */
346   public String toURIFragment()
347   {
348     try
349     {
350       StringBuilder rval = new StringBuilder("#");
351       for (String token : this.refTokens)
352       {
353         rval.append('/').append(URLEncoder.encode(token, ENCODING));
354       }
355       return rval.toString();
356     } catch (UnsupportedEncodingException e)
357     {
358       throw new RuntimeException(e);
359     }
360   }
361
362 }