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}