• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2019 The gRPC Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package io.grpc.internal;
18 
19 import static com.google.common.math.LongMath.checkedAdd;
20 
21 import java.text.ParseException;
22 import java.util.List;
23 import java.util.Locale;
24 import java.util.Map;
25 import java.util.concurrent.TimeUnit;
26 import javax.annotation.Nullable;
27 
28 /**
29  * Helper utility to work with JSON values in Java types. Includes the JSON dialect used by
30  * Protocol Buffers.
31  */
32 public class JsonUtil {
33   /**
34    * Gets a list from an object for the given key.  If the key is not present, this returns null.
35    * If the value is not a List, throws an exception.
36    */
37   @Nullable
getList(Map<String, ?> obj, String key)38   public static List<?> getList(Map<String, ?> obj, String key) {
39     assert key != null;
40     if (!obj.containsKey(key)) {
41       return null;
42     }
43     Object value = obj.get(key);
44     if (!(value instanceof List)) {
45       throw new ClassCastException(
46           String.format("value '%s' for key '%s' in '%s' is not List", value, key, obj));
47     }
48     return (List<?>) value;
49   }
50 
51   /**
52    * Gets a list from an object for the given key, and verifies all entries are objects.  If the key
53    * is not present, this returns null.  If the value is not a List or an entry is not an object,
54    * throws an exception.
55    */
56   @Nullable
getListOfObjects(Map<String, ?> obj, String key)57   public static List<Map<String, ?>> getListOfObjects(Map<String, ?> obj, String key) {
58     List<?> list = getList(obj, key);
59     if (list == null) {
60       return null;
61     }
62     return checkObjectList(list);
63   }
64 
65   /**
66    * Gets a list from an object for the given key, and verifies all entries are strings.  If the key
67    * is not present, this returns null.  If the value is not a List or an entry is not a string,
68    * throws an exception.
69    */
70   @Nullable
getListOfStrings(Map<String, ?> obj, String key)71   public static List<String> getListOfStrings(Map<String, ?> obj, String key) {
72     List<?> list = getList(obj, key);
73     if (list == null) {
74       return null;
75     }
76     return checkStringList(list);
77   }
78 
79   /**
80    * Gets an object from an object for the given key.  If the key is not present, this returns null.
81    * If the value is not a Map, throws an exception.
82    */
83   @SuppressWarnings("unchecked")
84   @Nullable
getObject(Map<String, ?> obj, String key)85   public static Map<String, ?> getObject(Map<String, ?> obj, String key) {
86     assert key != null;
87     if (!obj.containsKey(key)) {
88       return null;
89     }
90     Object value = obj.get(key);
91     if (!(value instanceof Map)) {
92       throw new ClassCastException(
93           String.format("value '%s' for key '%s' in '%s' is not object", value, key, obj));
94     }
95     return (Map<String, ?>) value;
96   }
97 
98   /**
99    * Gets a number from an object for the given key.  If the key is not present, this returns null.
100    * If the value does not represent a double, throws an exception.
101    */
102   @Nullable
getNumberAsDouble(Map<String, ?> obj, String key)103   public static Double getNumberAsDouble(Map<String, ?> obj, String key) {
104     assert key != null;
105     if (!obj.containsKey(key)) {
106       return null;
107     }
108     Object value = obj.get(key);
109     if (value instanceof Double) {
110       return (Double) value;
111     }
112     if (value instanceof String) {
113       try {
114         return Double.parseDouble((String) value);
115       } catch (NumberFormatException e) {
116         throw new IllegalArgumentException(
117             String.format("value '%s' for key '%s' is not a double", value, key));
118       }
119     }
120     throw new IllegalArgumentException(
121         String.format("value '%s' for key '%s' in '%s' is not a number", value, key, obj));
122   }
123 
124   /**
125    * Gets a number from an object for the given key.  If the key is not present, this returns null.
126    * If the value does not represent a float, throws an exception.
127    */
128   @Nullable
getNumberAsFloat(Map<String, ?> obj, String key)129   public static Float getNumberAsFloat(Map<String, ?> obj, String key) {
130     assert key != null;
131     if (!obj.containsKey(key)) {
132       return null;
133     }
134     Object value = obj.get(key);
135     if (value instanceof Float) {
136       return (Float) value;
137     }
138     if (value instanceof String) {
139       try {
140         return Float.parseFloat((String) value);
141       } catch (NumberFormatException e) {
142         throw new IllegalArgumentException(
143             String.format("string value '%s' for key '%s' cannot be parsed as a float", value,
144                 key));
145       }
146     }
147     throw new IllegalArgumentException(
148         String.format("value %s for key '%s' is not a float", value, key));
149   }
150 
151   /**
152    * Gets a number from an object for the given key, casted to an integer.  If the key is not
153    * present, this returns null.  If the value does not represent an integer, throws an exception.
154    */
155   @Nullable
getNumberAsInteger(Map<String, ?> obj, String key)156   public static Integer getNumberAsInteger(Map<String, ?> obj, String key) {
157     assert key != null;
158     if (!obj.containsKey(key)) {
159       return null;
160     }
161     Object value = obj.get(key);
162     if (value instanceof Double) {
163       Double d = (Double) value;
164       int i = d.intValue();
165       if (i != d) {
166         throw new ClassCastException("Number expected to be integer: " + d);
167       }
168       return i;
169     }
170     if (value instanceof String) {
171       try {
172         return Integer.parseInt((String) value);
173       } catch (NumberFormatException e) {
174         throw new IllegalArgumentException(
175             String.format("value '%s' for key '%s' is not an integer", value, key));
176       }
177     }
178     throw new IllegalArgumentException(
179         String.format("value '%s' for key '%s' is not an integer", value, key));
180   }
181 
182   /**
183    * Gets a number from an object for the given key, casted to an long.  If the key is not
184    * present, this returns null.  If the value does not represent a long integer, throws an
185    * exception.
186    */
getNumberAsLong(Map<String, ?> obj, String key)187   public static Long getNumberAsLong(Map<String, ?> obj, String key) {
188     assert key != null;
189     if (!obj.containsKey(key)) {
190       return null;
191     }
192     Object value = obj.get(key);
193     if (value instanceof Double) {
194       Double d = (Double) value;
195       long l = d.longValue();
196       if (l != d) {
197         throw new ClassCastException("Number expected to be long: " + d);
198       }
199       return l;
200     }
201     if (value instanceof String) {
202       try {
203         return Long.parseLong((String) value);
204       } catch (NumberFormatException e) {
205         throw new IllegalArgumentException(
206             String.format("value '%s' for key '%s' is not a long integer", value, key));
207       }
208     }
209     throw new IllegalArgumentException(
210         String.format("value '%s' for key '%s' is not a long integer", value, key));
211   }
212 
213   /**
214    * Gets a string from an object for the given key.  If the key is not present, this returns null.
215    * If the value is not a String, throws an exception.
216    */
217   @Nullable
getString(Map<String, ?> obj, String key)218   public static String getString(Map<String, ?> obj, String key) {
219     assert key != null;
220     if (!obj.containsKey(key)) {
221       return null;
222     }
223     Object value = obj.get(key);
224     if (!(value instanceof String)) {
225       throw new ClassCastException(
226           String.format("value '%s' for key '%s' in '%s' is not String", value, key, obj));
227     }
228     return (String) value;
229   }
230 
231   /**
232    * Gets a string from an object for the given key, parsed as a duration (defined by protobuf).  If
233    * the key is not present, this returns null.  If the value is not a String or not properly
234    * formatted, throws an exception.
235    */
getStringAsDuration(Map<String, ?> obj, String key)236   public static Long getStringAsDuration(Map<String, ?> obj, String key) {
237     String value = getString(obj, key);
238     if (value == null) {
239       return null;
240     }
241     try {
242       return parseDuration(value);
243     } catch (ParseException e) {
244       throw new RuntimeException(e);
245     }
246   }
247 
248   /**
249    * Gets a boolean from an object for the given key.  If the key is not present, this returns null.
250    * If the value is not a Boolean, throws an exception.
251    */
252   @Nullable
getBoolean(Map<String, ?> obj, String key)253   public static Boolean getBoolean(Map<String, ?> obj, String key) {
254     assert key != null;
255     if (!obj.containsKey(key)) {
256       return null;
257     }
258     Object value = obj.get(key);
259     if (!(value instanceof Boolean)) {
260       throw new ClassCastException(
261           String.format("value '%s' for key '%s' in '%s' is not Boolean", value, key, obj));
262     }
263     return (Boolean) value;
264   }
265 
266   /**
267    * Casts a list of unchecked JSON values to a list of checked objects in Java type.
268    * If the given list contains a value that is not a Map, throws an exception.
269    */
270   @SuppressWarnings("unchecked")
checkObjectList(List<?> rawList)271   public static List<Map<String, ?>> checkObjectList(List<?> rawList) {
272     for (int i = 0; i < rawList.size(); i++) {
273       if (!(rawList.get(i) instanceof Map)) {
274         throw new ClassCastException(
275             String.format(
276                 Locale.US, "value %s for idx %d in %s is not object", rawList.get(i), i, rawList));
277       }
278     }
279     return (List<Map<String, ?>>) rawList;
280   }
281 
282   /**
283    * Casts a list of unchecked JSON values to a list of String. If the given list
284    * contains a value that is not a String, throws an exception.
285    */
286   @SuppressWarnings("unchecked")
checkStringList(List<?> rawList)287   public static List<String> checkStringList(List<?> rawList) {
288     for (int i = 0; i < rawList.size(); i++) {
289       if (!(rawList.get(i) instanceof String)) {
290         throw new ClassCastException(
291             String.format(
292                 Locale.US,
293                 "value '%s' for idx %d in '%s' is not string", rawList.get(i), i, rawList));
294       }
295     }
296     return (List<String>) rawList;
297   }
298 
299   private static final long DURATION_SECONDS_MIN = -315576000000L;
300   private static final long DURATION_SECONDS_MAX = 315576000000L;
301 
302   /**
303    * Parse from a string to produce a duration.  Copy of
304    * {@link com.google.protobuf.util.Durations#parse}.
305    *
306    * @return A Duration parsed from the string.
307    * @throws ParseException if parsing fails.
308    */
parseDuration(String value)309   private static long parseDuration(String value) throws ParseException {
310     // Must ended with "s".
311     if (value.isEmpty() || value.charAt(value.length() - 1) != 's') {
312       throw new ParseException("Invalid duration string: " + value, 0);
313     }
314     boolean negative = false;
315     if (value.charAt(0) == '-') {
316       negative = true;
317       value = value.substring(1);
318     }
319     String secondValue = value.substring(0, value.length() - 1);
320     String nanoValue = "";
321     int pointPosition = secondValue.indexOf('.');
322     if (pointPosition != -1) {
323       nanoValue = secondValue.substring(pointPosition + 1);
324       secondValue = secondValue.substring(0, pointPosition);
325     }
326     long seconds = Long.parseLong(secondValue);
327     int nanos = nanoValue.isEmpty() ? 0 : parseNanos(nanoValue);
328     if (seconds < 0) {
329       throw new ParseException("Invalid duration string: " + value, 0);
330     }
331     if (negative) {
332       seconds = -seconds;
333       nanos = -nanos;
334     }
335     try {
336       return normalizedDuration(seconds, nanos);
337     } catch (IllegalArgumentException e) {
338       throw new ParseException("Duration value is out of range.", 0);
339     }
340   }
341 
342   /**
343    * Copy of {@link com.google.protobuf.util.Timestamps#parseNanos}.
344    */
parseNanos(String value)345   private static int parseNanos(String value) throws ParseException {
346     int result = 0;
347     for (int i = 0; i < 9; ++i) {
348       result = result * 10;
349       if (i < value.length()) {
350         if (value.charAt(i) < '0' || value.charAt(i) > '9') {
351           throw new ParseException("Invalid nanoseconds.", 0);
352         }
353         result += value.charAt(i) - '0';
354       }
355     }
356     return result;
357   }
358 
359   private static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1);
360 
361   /**
362    * Copy of {@link com.google.protobuf.util.Durations#normalizedDuration}.
363    */
364   @SuppressWarnings("NarrowingCompoundAssignment")
normalizedDuration(long seconds, int nanos)365   private static long normalizedDuration(long seconds, int nanos) {
366     if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) {
367       seconds = checkedAdd(seconds, nanos / NANOS_PER_SECOND);
368       nanos %= NANOS_PER_SECOND;
369     }
370     if (seconds > 0 && nanos < 0) {
371       nanos += NANOS_PER_SECOND; // no overflow since nanos is negative (and we're adding)
372       seconds--; // no overflow since seconds is positive (and we're decrementing)
373     }
374     if (seconds < 0 && nanos > 0) {
375       nanos -= NANOS_PER_SECOND; // no overflow since nanos is positive (and we're subtracting)
376       seconds++; // no overflow since seconds is negative (and we're incrementing)
377     }
378     if (!durationIsValid(seconds, nanos)) {
379       throw new IllegalArgumentException(String.format(
380           "Duration is not valid. See proto definition for valid values. "
381               + "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
382               + "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
383               + "Nanos must have the same sign as seconds", seconds, nanos));
384     }
385     return saturatedAdd(TimeUnit.SECONDS.toNanos(seconds), nanos);
386   }
387 
388   /**
389    * Returns true if the given number of seconds and nanos is a valid {@code Duration}. The {@code
390    * seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
391    * value must be in the range [-999,999,999, +999,999,999].
392    *
393    * <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
394    * and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
395    * value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
396    *
397    * <p>Copy of {@link com.google.protobuf.util.Duration#isValid}.</p>
398    */
durationIsValid(long seconds, int nanos)399   private static boolean durationIsValid(long seconds, int nanos) {
400     if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
401       return false;
402     }
403     if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
404       return false;
405     }
406     if (seconds < 0 || nanos < 0) {
407       if (seconds > 0 || nanos > 0) {
408         return false;
409       }
410     }
411     return true;
412   }
413 
414   /**
415    * Returns the sum of {@code a} and {@code b} unless it would overflow or underflow in which case
416    * {@code Long.MAX_VALUE} or {@code Long.MIN_VALUE} is returned, respectively.
417    *
418    * <p>Copy of {@link com.google.common.math.LongMath#saturatedAdd}.</p>
419    *
420    */
421   @SuppressWarnings("ShortCircuitBoolean")
saturatedAdd(long a, long b)422   private static long saturatedAdd(long a, long b) {
423     long naiveSum = a + b;
424     if ((a ^ b) < 0 | (a ^ naiveSum) >= 0) {
425       // If a and b have different signs or a has the same sign as the result then there was no
426       // overflow, return.
427       return naiveSum;
428     }
429     // we did over/under flow, if the sign is negative we should return MAX otherwise MIN
430     return Long.MAX_VALUE + ((naiveSum >>> (Long.SIZE - 1)) ^ 1);
431   }
432 }
433