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.uon;
018
019import static org.apache.juneau.commons.httppart.HttpPartType.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022
023import java.util.*;
024
025import org.apache.juneau.commons.httppart.*;
026import org.apache.juneau.commons.lang.*;
027import org.apache.juneau.marshall.collections.*;
028import org.apache.juneau.marshall.httppart.*;
029import org.apache.juneau.marshall.parser.*;
030
031/**
032 * Utility methods for the UON and UrlEncoding serializers and parsers.
033 *
034 * <h5 class='section'>See Also:</h5><ul>
035 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UonSupport">UON</a>
036 * </ul>
037 */
038public class UonUtils {
039
040   /**
041    * Prevents instantiation.
042    */
043   private UonUtils() {}
044
045   private static final AsciiSet needsQuoteChars = AsciiSet.of("),=\n\t\r\b\f ");
046   private static final AsciiSet maybeNeedsQuotesFirstChar = AsciiSet.of("),=\n\t\r\b\f tfn+-.#0123456789");
047
048   /**
049    * Returns <jk>true</jk> if the specified string needs to be quoted per UON notation.
050    *
051    * <p>
052    * For example, strings that start with '(' or '@' or look like boolean or numeric values need to be quoted.
053    *
054    * @param s The string to test.
055    *    <br>Must not be <jk>null</jk>.
056    * @return <jk>true</jk> if the specified string needs to be quoted per UON notation.
057    */
058   public static boolean needsQuotes(String s) {
059      var c0 = s.isEmpty() ? 0 : s.charAt(0);
060      // @formatter:off
061      return (
062         s.isEmpty()
063         || c0 == '@'
064         || c0 == '('
065         || needsQuoteChars.contains(s)
066         || (
067            maybeNeedsQuotesFirstChar.contains(c0)
068            && (
069               "true".equals(s)
070               || "false".equals(s)
071               || "null".equals(s)
072               || isNumeric(s)
073            )
074         )
075      );
076      // @formatter:on
077   }
078
079   /**
080    * Merges MULTI <c>key=uonValue</c> tokens into a new {@link JsonMap}.
081    *
082    * <p>
083    * Each token is one top-level pair (for example <c>environment=prod</c> or <c>flags=(a,b,c)</c>).
084    * A parenthesized UON object as a single token (<c>(a=1,b=2)</c>) is expanded.
085    * Later keys overwrite earlier ones.  Blank tokens are ignored.
086    * </p>
087    *
088    * @param tokens The raw tokens.  Can be <jk>null</jk>.
089    * @return A new map, never <jk>null</jk>.
090    * @throws ParseException Malformed UON value.
091    * @throws SchemaValidationException If a UON value fails schema validation.
092    */
093   public static JsonMap mergePairs(String...tokens) throws ParseException, SchemaValidationException {
094      var dest = JsonMap.create();
095      if (tokens != null)
096         for (var t : tokens)
097            mergePair(dest, t);
098      return dest;
099   }
100
101   /**
102    * Merges one MULTI <c>key=uonValue</c> token into <jv>dest</jv>.
103    *
104    * @param dest The destination map.  Must not be <jk>null</jk>.
105    * @param token The raw token.  Blank tokens are ignored.
106    * @throws ParseException Malformed UON value.
107    * @throws SchemaValidationException If a UON value fails schema validation.
108    */
109   public static void mergePair(JsonMap dest, String token) throws ParseException, SchemaValidationException {
110      if (isBlank(token))
111         return;
112      var t = token.trim();
113      if (t.startsWith("(") && t.endsWith(")")) {
114         for (var piece : splitTopLevelCommas(t.substring(1, t.length() - 1)))
115            mergePair(dest, piece);
116         return;
117      }
118      var eq = t.indexOf('=');
119      if (eq <= 0)
120         return;
121      var key = t.substring(0, eq);
122      var val = t.substring(eq + 1);
123      dest.put(key, isEmpty(val) ? "" : parseValue(val));
124   }
125
126   /**
127    * Parses a UON value from the right-hand side of a <c>key=uonValue</c> pair.
128    *
129    * <p>
130    * List-shaped values authored as <c>(a,b,c)</c> (without the UON <c>@</c> array prefix)
131    * are treated as UON arrays.  Unquoted strings that the parser would otherwise coerce to
132    * a number are kept as strings unless they are a UON number literal.
133    * </p>
134    *
135    * @param raw The raw UON value.  Must not be <jk>null</jk>.
136    * @return The parsed value.
137    * @throws ParseException Malformed UON value.
138    * @throws SchemaValidationException If the UON value fails schema validation.
139    */
140   public static Object parseValue(String raw) throws ParseException, SchemaValidationException {
141      var original = raw.trim();
142      var t = original;
143      // UON arrays are "@(a,b,c)"; list-shaped values are authored without the "@".
144      if (t.startsWith("(") && t.endsWith(")") && t.indexOf('=') < 0)
145         t = "@" + t;
146      var parsed = UonParser.DEFAULT.read(QUERY, HttpPartSchema.DEFAULT, t, Object.class);
147      if (parsed instanceof Number && ! isUonNumberLiteral(original))
148         return original;
149      return parsed;
150   }
151
152   private static List<String> splitTopLevelCommas(String s) {
153      List<String> out = l();
154      var depth = 0;
155      var start = 0;
156      for (var i = 0; i < s.length(); i++) {
157         var c = s.charAt(i);
158         if (c == '(')
159            depth++;
160         else if (c == ')')
161            depth--;
162         else if (c == ',' && depth == 0) {
163            out.add(s.substring(start, i).trim());
164            start = i + 1;
165         }
166      }
167      out.add(s.substring(start).trim());
168      out.removeIf(String::isEmpty);
169      return out;
170   }
171
172   @SuppressWarnings({
173      "java:S3776" // Single-pass number-literal scanner; splitting would obscure the grammar.
174   })
175   private static boolean isUonNumberLiteral(String s) {
176      if (isEmpty(s))
177         return false;
178      var i = 0;
179      if (s.charAt(0) == '-' || s.charAt(0) == '+')
180         i = 1;
181      var digits = 0;
182      var dot = false;
183      var exp = false;
184      for (; i < s.length(); i++) {
185         var c = s.charAt(i);
186         if (c >= '0' && c <= '9') {
187            digits++;
188         } else if (c == '.' && ! dot && ! exp) {
189            dot = true;
190         } else if ((c == 'e' || c == 'E') && ! exp && digits > 0) {
191            exp = true;
192            if (i + 1 < s.length() && (s.charAt(i + 1) == '+' || s.charAt(i + 1) == '-'))
193               i++;
194         } else {
195            return false;
196         }
197      }
198      return digits > 0;
199   }
200}