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.juneau.marshall.collections;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023/**
024 * An <a class="doclink" href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a> JSON Pointer that addresses a
025 * single value within a {@link Map}/{@link List} tree (such as the {@link MarshalledMap}/{@link MarshalledList} family).
026 *
027 * <p>
028 * A JSON Pointer is a string of zero or more <c>/</c>-prefixed reference tokens. The empty string <js>""</js> references
029 * the whole document (the root). A non-empty pointer must begin with <c>/</c>; each token is the text between <c>/</c>
030 * separators with <c>~1</c> decoded to <c>/</c> and <c>~0</c> decoded to <c>~</c>. For arrays a token is either a
031 * base-10 non-negative integer index or the single character <c>-</c> (which references the nonexistent element after
032 * the last element, used to append on writes); for objects the decoded token is the member name.
033 *
034 * <h5 class='section'>Example:</h5>
035 * <p class='bjava'>
036 *    JsonMap <jv>doc</jv> = JsonMap.<jsm>of</jsm>(<js>"foo"</js>, JsonList.<jsm>of</jsm>(<js>"bar"</js>, <js>"baz"</js>));
037 *
038 *    <jc>// Read.</jc>
039 *    Object <jv>v</jv> = JsonPointer.<jsm>of</jsm>(<js>"/foo/0"</js>).eval(<jv>doc</jv>);  <jc>// "bar"</jc>
040 *
041 *    <jc>// Auto-vivifying write (creates intermediate containers).</jc>
042 *    JsonPointer.<jsm>of</jsm>(<js>"/foo/-"</js>).set(<jv>doc</jv>, <js>"qux"</js>);     <jc>// appends "qux"</jc>
043 * </p>
044 *
045 * <h5 class='section'>Notes:</h5><ul>
046 *    <li class='warn'>This class is not thread safe.
047 *    <li>Read-miss (missing key, out-of-range index, or type mismatch) resolves to <jk>null</jk> via {@link #eval(Object)}.
048 * </ul>
049 *
050 * <p>
051 * <b>Beta — API subject to change:</b> This type is part of the next-generation typed tree model layered over the
052 * {@code Marshalled*} collections and may change incompatibly in a future release.
053 */
054@SuppressWarnings({
055   "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants
056})
057public class JsonPointer {
058
059   /** Sentinel distinguishing an absent value from a present <jk>null</jk> value during traversal. */
060   private static final Object MISSING = new Object();
061
062   /**
063    * Parses and validates the specified RFC 6901 JSON Pointer string.
064    *
065    * @param pointer The pointer string. The empty string references the whole document; a non-empty pointer must begin with <c>/</c>.
066    * @return A new {@link JsonPointer}, never <jk>null</jk>.
067    * @throws IllegalArgumentException If the pointer is <jk>null</jk> or malformed (non-empty and not beginning with <c>/</c>).
068    */
069   public static JsonPointer of(String pointer) {
070      return new JsonPointer(parse(pointer));
071   }
072
073   /**
074    * Encodes a raw object member name into an RFC 6901 reference token (escapes <c>~</c> to <c>~0</c> and <c>/</c> to <c>~1</c>).
075    *
076    * @param rawMemberName The raw member name. Must not be <jk>null</jk>.
077    * @return The encoded reference token.
078    */
079   public static String encodeToken(String rawMemberName) {
080      reqnn("rawMemberName", rawMemberName);
081      return rawMemberName.replace("~", "~0").replace("/", "~1");
082   }
083
084   /**
085    * Decodes an RFC 6901 reference token into a raw object member name (unescapes <c>~1</c> to <c>/</c> then <c>~0</c> to <c>~</c>).
086    *
087    * @param token The reference token. Must not be <jk>null</jk>.
088    * @return The decoded member name.
089    */
090   public static String decodeToken(String token) {
091      reqnn("token", token);
092      return token.replace("~1", "/").replace("~0", "~");
093   }
094
095   private static List<String> parse(String pointer) {
096      if (pointer == null)
097         throw iaex("JSON Pointer cannot be null.");
098      if (pointer.isEmpty())
099         return List.of();
100      if (pointer.charAt(0) != '/')
101         throw iaex("Invalid JSON Pointer '%s': a non-empty pointer must begin with '/'.", pointer);
102      var parts = pointer.split("/", -1);
103      var tokens = new ArrayList<String>(parts.length - 1);
104      for (var i = 1; i < parts.length; i++)
105         tokens.add(decodeToken(parts[i]));
106      return tokens;
107   }
108
109   private final List<String> tokens;
110
111   private JsonPointer(List<String> tokens) {
112      this.tokens = tokens;
113   }
114
115   /**
116    * Reads the value addressed by this pointer from the specified root.
117    *
118    * <p>
119    * Walks the tokens from <c>root</c> (which may be a {@link Map}/{@link List} such as the {@link MarshalledMap}/
120    * {@link MarshalledList} family). The empty pointer returns <c>root</c>.
121    *
122    * @param root The root document. Can be <jk>null</jk> (treated as an empty document; the addressed value evaluates to <jk>null</jk>).
123    * @return The addressed value, or <jk>null</jk> on any missing key, out-of-range index, or type mismatch.
124    */
125   public Object eval(Object root) {
126      var o = evalOrMissing(root);
127      return o == MISSING ? null : o;
128   }
129
130   /**
131    * Writes the specified value at the location addressed by this pointer, auto-vivifying missing intermediate containers.
132    *
133    * <p>
134    * Intermediate containers are created based on the next token: a numeric or <c>-</c> token implies a {@link JsonList},
135    * otherwise a {@link JsonMap}. The <c>-</c> token on a list appends. The empty pointer cannot mutate <c>root</c> in
136    * place for the whole-document case, so it simply returns <c>value</c> as the new root.
137    *
138    * @param root The root document. For a non-empty pointer, must be a mutable {@link Map}/{@link List} tree.
139    * @param value The value to set.
140    * @return The (possibly newly-created) effective root.
141    */
142   public Object set(Object root, Object value) {
143      if (tokens.isEmpty())
144         return value;
145      var parent = navigateToParent(root);
146      rawSet(parent, tokens.get(tokens.size() - 1), value);
147      return root;
148   }
149
150   /**
151    * Removes the member or element addressed by this pointer.
152    *
153    * @param root The root document.  Can be <jk>null</jk> (treated as an empty document — nothing is found to remove).
154    * @return The removed value, or <jk>null</jk> if the addressed member/element was absent (or the pointer is the root pointer).
155    */
156   public Object remove(Object root) {
157      if (tokens.isEmpty())
158         return null;
159      var parent = root;
160      for (var i = 0; i < tokens.size() - 1; i++) {
161         parent = rawGet(parent, tokens.get(i));
162         if (parent == MISSING)
163            return null;
164      }
165      return rawRemove(parent, tokens.get(tokens.size() - 1));
166   }
167
168   @Override /* Overridden from Object */
169   public String toString() {
170      if (tokens.isEmpty())
171         return "";
172      var sb = new StringBuilder();
173      for (var t : tokens)
174         sb.append('/').append(encodeToken(t));
175      return sb.toString();
176   }
177
178   /**
179    * Returns <jk>true</jk> if this is the root pointer (the empty pointer addressing the whole document).
180    */
181   boolean isRoot() {
182      return tokens.isEmpty();
183   }
184
185   /**
186    * Same as {@link #eval(Object)} but returns the {@link #MISSING} sentinel (rather than <jk>null</jk>) for an absent
187    * value, so callers can distinguish a present <jk>null</jk> value from a read-miss.
188    */
189   Object evalOrMissing(Object root) {
190      var current = root;
191      for (var token : tokens) {
192         current = rawGet(current, token);
193         if (current == MISSING)
194            return MISSING;
195      }
196      return current;
197   }
198
199   /** Returns <jk>true</jk> if the specified value is the absent-value sentinel returned by {@link #evalOrMissing(Object)}. */
200   static boolean isMissing(Object o) {
201      return o == MISSING;
202   }
203
204   private Object navigateToParent(Object root) {
205      var current = root;
206      for (var i = 0; i < tokens.size() - 1; i++) {
207         var token = tokens.get(i);
208         var child = rawGet(current, token);
209         if (! (child instanceof Map || child instanceof List)) {
210            child = isIndexToken(tokens.get(i + 1)) ? new JsonList() : new JsonMap();
211            rawSet(current, token, child);
212         }
213         current = child;
214      }
215      return current;
216   }
217
218   private static Object rawGet(Object container, String token) {
219      if (container instanceof Map<?,?> container2)
220         return container2.containsKey(token) ? container2.get(token) : MISSING;
221      if (container instanceof List<?> container2) {
222         var idx = toArrayIndex(token);
223         return (idx >= 0 && idx < container2.size()) ? container2.get(idx) : MISSING;
224      }
225      return MISSING;
226   }
227
228   @SuppressWarnings({
229      "unchecked" // Object-node maps accept String keys / Object values; array-node lists accept Object elements.
230   })
231   private static void rawSet(Object container, String token, Object value) {
232      if (container instanceof Map) {
233         ((Map<String,Object>)container).put(token, value);
234         return;
235      }
236      if (container instanceof List) {
237         var list = (List<Object>)container;
238         if (eq(token, "-")) {
239            list.add(value);
240            return;
241         }
242         var idx = toArrayIndex(token);
243         if (idx < 0 || idx > list.size())
244            throw iaex("Invalid JSON Pointer array index '%s' for a list of size %s.", token, list.size());
245         if (idx == list.size())
246            list.add(value);
247         else
248            list.set(idx, value);
249         return;
250      }
251      throw iaex("Cannot set value at token '%s' on a non-container value.", token);
252   }
253
254   private static Object rawRemove(Object container, String token) {
255      if (container instanceof Map<?,?> container2)
256         return container2.containsKey(token) ? container2.remove(token) : null;
257      if (container instanceof List<?> container2) {
258         var idx = toArrayIndex(token);
259         return (idx >= 0 && idx < container2.size()) ? container2.remove(idx) : null;
260      }
261      return null;
262   }
263
264   private static boolean isIndexToken(String token) {
265      return eq(token, "-") || toArrayIndex(token) >= 0;
266   }
267
268   /*
269    * Returns the non-negative array index for the specified token, or -1 if the token is not a valid RFC 6901 array
270    * index (no leading zeros except "0" itself; "-" is not a numeric index).
271    */
272   private static int toArrayIndex(String token) {
273      var len = token.length();
274      if (len == 0)
275         return -1;
276      if (len > 1 && token.charAt(0) == '0')
277         return -1;
278      for (var i = 0; i < len; i++) {
279         var c = token.charAt(i);
280         if (c < '0' || c > '9')
281            return -1;
282      }
283      try {
284         return Integer.parseInt(token);
285      } catch (NumberFormatException e) {  // HTT - guards against overflow on very long digit strings.
286         return -1;
287      }
288   }
289}