001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.rest.util; 014 015import static org.apache.juneau.internal.ArrayUtils.*; 016import static org.apache.juneau.internal.StringUtils.*; 017 018import java.util.*; 019import java.util.regex.*; 020 021import javax.servlet.http.*; 022 023import org.apache.juneau.*; 024import org.apache.juneau.internal.*; 025import org.apache.juneau.json.*; 026import org.apache.juneau.parser.*; 027import org.apache.juneau.rest.*; 028import org.apache.juneau.uon.*; 029import org.apache.juneau.utils.*; 030 031/** 032 * Various reusable utility methods. 033 */ 034public final class RestUtils { 035 036 /** 037 * Returns readable text for an HTTP response code. 038 * 039 * @param rc The HTTP response code. 040 * @return Readable text for an HTTP response code, or <jk>null</jk> if it's an invalid code. 041 */ 042 public static String getHttpResponseText(int rc) { 043 return httpMsgs.get(rc); 044 } 045 046 private static Map<Integer,String> httpMsgs = new AMap<Integer,String>() 047 .append(100, "Continue") 048 .append(101, "Switching Protocols") 049 .append(102, "Processing") 050 .append(103, "Early Hints") 051 .append(200, "OK") 052 .append(201, "Created") 053 .append(202, "Accepted") 054 .append(203, "Non-Authoritative Information") 055 .append(204, "No Content") 056 .append(205, "Reset Content") 057 .append(206, "Partial Content") 058 .append(300, "Multiple Choices") 059 .append(301, "Moved Permanently") 060 .append(302, "Temporary Redirect") 061 .append(303, "See Other") 062 .append(304, "Not Modified") 063 .append(305, "Use Proxy") 064 .append(307, "Temporary Redirect") 065 .append(400, "Bad Request") 066 .append(401, "Unauthorized") 067 .append(402, "Payment Required") 068 .append(403, "Forbidden") 069 .append(404, "Not Found") 070 .append(405, "Method Not Allowed") 071 .append(406, "Not Acceptable") 072 .append(407, "Proxy Authentication Required") 073 .append(408, "Request Time-Out") 074 .append(409, "Conflict") 075 .append(410, "Gone") 076 .append(411, "Length Required") 077 .append(412, "Precondition Failed") 078 .append(413, "Request Entity Too Large") 079 .append(414, "Request-URI Too Large") 080 .append(415, "Unsupported Media Type") 081 .append(500, "Internal Server Error") 082 .append(501, "Not Implemented") 083 .append(502, "Bad Gateway") 084 .append(503, "Service Unavailable") 085 .append(504, "Gateway Timeout") 086 .append(505, "HTTP Version Not Supported") 087 ; 088 089 /** 090 * Identical to {@link HttpServletRequest#getPathInfo()} but doesn't decode encoded characters. 091 * 092 * @param req The HTTP request 093 * @return The un-decoded path info. 094 */ 095 public static String getPathInfoUndecoded(HttpServletRequest req) { 096 String requestURI = req.getRequestURI(); 097 String contextPath = req.getContextPath(); 098 String servletPath = req.getServletPath(); 099 int l = contextPath.length() + servletPath.length(); 100 if (requestURI.length() == l) 101 return null; 102 return requestURI.substring(l); 103 } 104 105 /** 106 * Efficiently trims the path info part from a request URI. 107 * 108 * <p> 109 * The result is the URI of the servlet itself. 110 * 111 * @param requestURI The value returned by {@link HttpServletRequest#getRequestURL()} 112 * @param contextPath The value returned by {@link HttpServletRequest#getContextPath()} 113 * @param servletPath The value returned by {@link HttpServletRequest#getServletPath()} 114 * @return The same StringBuilder with remainder trimmed. 115 */ 116 public static StringBuffer trimPathInfo(StringBuffer requestURI, String contextPath, String servletPath) { 117 if (servletPath.equals("/")) 118 servletPath = ""; 119 if (contextPath.equals("/")) 120 contextPath = ""; 121 122 try { 123 // Given URL: http://hostname:port/servletPath/extra 124 // We want: http://hostname:port/servletPath 125 int sc = 0; 126 for (int i = 0; i < requestURI.length(); i++) { 127 char c = requestURI.charAt(i); 128 if (c == '/') { 129 sc++; 130 if (sc == 3) { 131 if (servletPath.isEmpty()) { 132 requestURI.setLength(i); 133 return requestURI; 134 } 135 136 // Make sure context path follows the authority. 137 for (int j = 0; j < contextPath.length(); i++, j++) 138 if (requestURI.charAt(i) != contextPath.charAt(j)) 139 throw new Exception("case=1"); 140 141 // Make sure servlet path follows the authority. 142 for (int j = 0; j < servletPath.length(); i++, j++) 143 if (requestURI.charAt(i) != servletPath.charAt(j)) 144 throw new Exception("case=2"); 145 146 // Make sure servlet path isn't a false match (e.g. /foo2 should not match /foo) 147 c = (requestURI.length() == i ? '/' : requestURI.charAt(i)); 148 if (c == '/' || c == '?') { 149 requestURI.setLength(i); 150 return requestURI; 151 } 152 153 throw new Exception("case=3"); 154 } 155 } else if (c == '?') { 156 if (sc != 2) 157 throw new Exception("case=4"); 158 if (servletPath.isEmpty()) { 159 requestURI.setLength(i); 160 return requestURI; 161 } 162 throw new Exception("case=5"); 163 } 164 } 165 if (servletPath.isEmpty()) 166 return requestURI; 167 throw new Exception("case=6"); 168 } catch (Exception e) { 169 throw new FormattedRuntimeException(e, "Could not find servlet path in request URI. URI=''{0}'', servletPath=''{1}''", requestURI, servletPath); 170 } 171 } 172 173 /** 174 * Parses HTTP header. 175 * 176 * @param s The string to parse. 177 * @return The parsed string. 178 */ 179 public static String[] parseHeader(String s) { 180 int i = s.indexOf(':'); 181 if (i == -1) 182 return null; 183 String name = s.substring(0, i).trim().toLowerCase(Locale.ENGLISH); 184 String val = s.substring(i+1).trim(); 185 return new String[]{name,val}; 186 } 187 188 /** 189 * Parses key/value pairs separated by either : or = 190 * 191 * @param s The string to parse. 192 * @return The parsed string. 193 */ 194 public static String[] parseKeyValuePair(String s) { 195 int i = -1; 196 for (int j = 0; j < s.length() && i < 0; j++) { 197 char c = s.charAt(j); 198 if (c == '=' || c == ':') 199 i = j; 200 } 201 if (i == -1) 202 return null; 203 String name = s.substring(0, i).trim(); 204 String val = s.substring(i+1).trim(); 205 return new String[]{name,val}; 206 } 207 208 static String resolveNewlineSeparatedAnnotation(String[] value, String fromParent) { 209 if (value.length == 0) 210 return fromParent; 211 212 List<String> l = new ArrayList<>(); 213 for (String v : value) { 214 if (! "INHERIT".equals(v)) 215 l.add(v); 216 else if (fromParent != null) 217 l.addAll(Arrays.asList(fromParent)); 218 } 219 return join(l, '\n'); 220 } 221 222 private static final Pattern INDEXED_LINK_PATTERN = Pattern.compile("(?s)(\\S*)\\[(\\d+)\\]\\:(.*)"); 223 224 static String[] resolveLinks(String[] links, String[] parentLinks) { 225 if (links.length == 0) 226 return parentLinks; 227 228 List<String> list = new ArrayList<>(); 229 for (String l : links) { 230 if ("INHERIT".equals(l)) 231 list.addAll(Arrays.asList(parentLinks)); 232 else if (l.indexOf('[') != -1 && INDEXED_LINK_PATTERN.matcher(l).matches()) { 233 Matcher lm = INDEXED_LINK_PATTERN.matcher(l); 234 lm.matches(); 235 String key = lm.group(1); 236 int index = Math.min(list.size(), Integer.parseInt(lm.group(2))); 237 String remainder = lm.group(3); 238 list.add(index, key.isEmpty() ? remainder : key + ":" + remainder); 239 } else { 240 list.add(l); 241 } 242 } 243 return list.toArray(new String[list.size()]); 244 } 245 246 static String[] resolveContent(String[] content, String[] parentContent) { 247 if (content.length == 0) 248 return parentContent; 249 250 List<String> list = new ArrayList<>(); 251 for (String l : content) { 252 if ("INHERIT".equals(l)) { 253 list.addAll(Arrays.asList(parentContent)); 254 } else if ("NONE".equals(l)) { 255 return new String[0]; 256 } else { 257 list.add(l); 258 } 259 } 260 return list.toArray(new String[list.size()]); 261 } 262 263 /** 264 * Parses a URL query string or form-data body. 265 * 266 * @param qs A reader or string containing the query string to parse. 267 * @return A new map containing the parsed query. 268 * @throws Exception 269 */ 270 public static Map<String,String[]> parseQuery(Object qs) throws Exception { 271 return parseQuery(qs, null); 272 } 273 274 /** 275 * Same as {@link #parseQuery(Object)} but allows you to specify the map to insert values into. 276 * 277 * @param qs A reader containing the query string to parse. 278 * @param map The map to pass the values into. 279 * @return The same map passed in, or a new map if it was <jk>null</jk>. 280 * @throws Exception 281 */ 282 public static Map<String,String[]> parseQuery(Object qs, Map<String,String[]> map) throws Exception { 283 284 Map<String,String[]> m = map; 285 if (m == null) 286 m = new TreeMap<>(); 287 288 if (qs == null || ((qs instanceof CharSequence) && isEmpty(qs))) 289 return m; 290 291 try (ParserPipe p = new ParserPipe(qs)) { 292 293 final int S1=1; // Looking for attrName start. 294 final int S2=2; // Found attrName start, looking for = or & or end. 295 final int S3=3; // Found =, looking for valStart or &. 296 final int S4=4; // Found valStart, looking for & or end. 297 298 try (UonReader r = new UonReader(p, true)) { 299 int c = r.peekSkipWs(); 300 if (c == '?') 301 r.read(); 302 303 int state = S1; 304 String currAttr = null; 305 while (c != -1) { 306 c = r.read(); 307 if (state == S1) { 308 if (c != -1) { 309 r.unread(); 310 r.mark(); 311 state = S2; 312 } 313 } else if (state == S2) { 314 if (c == -1) { 315 add(m, r.getMarked(), null); 316 } else if (c == '\u0001') { 317 m.put(r.getMarked(0,-1), null); 318 state = S1; 319 } else if (c == '\u0002') { 320 currAttr = r.getMarked(0,-1); 321 state = S3; 322 } 323 } else if (state == S3) { 324 if (c == -1 || c == '\u0001') { 325 add(m, currAttr, ""); 326 state = S1; 327 } else { 328 if (c == '\u0002') 329 r.replace('='); 330 r.unread(); 331 r.mark(); 332 state = S4; 333 } 334 } else if (state == S4) { 335 if (c == -1) { 336 add(m, currAttr, r.getMarked()); 337 } else if (c == '\u0001') { 338 add(m, currAttr, r.getMarked(0,-1)); 339 state = S1; 340 } else if (c == '\u0002') { 341 r.replace('='); 342 } 343 } 344 } 345 } 346 347 return m; 348 } 349 } 350 351 private static void add(Map<String,String[]> m, String key, String val) { 352 boolean b = m.containsKey(key); 353 if (val == null) { 354 if (! b) 355 m.put(key, null); 356 } else if (b && m.get(key) != null) { 357 m.put(key, append(m.get(key), val)); 358 } else { 359 m.put(key, new String[]{val}); 360 } 361 } 362 363 /** 364 * Parses a string that can consist of a simple string or JSON object/array. 365 * 366 * @param s The string to parse. 367 * @return The parsed value, or <jk>null</jk> if the input is null. 368 * @throws ParseException 369 */ 370 public static Object parseAnything(String s) throws ParseException { 371 if (isJson(s)) 372 return JsonParser.DEFAULT.parse(s, Object.class); 373 return s; 374 } 375 376 /** 377 * Merges the specified parent and child arrays. 378 * 379 * <p> 380 * The general concept is to allow child values to override parent values. 381 * 382 * <p> 383 * The rules are: 384 * <ul> 385 * <li>If the child array is not empty, then the child array is returned. 386 * <li>If the child array is empty, then the parent array is returned. 387 * <li>If the child array contains {@link None}, then an empty array is always returned. 388 * <li>If the child array contains {@link Inherit}, then the contents of the parent array are inserted into the position of the {@link Inherit} entry. 389 * </ul> 390 * 391 * @param fromParent The parent array. 392 * @param fromChild The child array. 393 * @return A new merged array. 394 */ 395 public static Object[] merge(Object[] fromParent, Object[] fromChild) { 396 397 if (ArrayUtils.contains(None.class, fromChild)) 398 return new Object[0]; 399 400 if (fromChild.length == 0) 401 return fromParent; 402 403 if (! ArrayUtils.contains(Inherit.class, fromChild)) 404 return fromChild; 405 406 List<Object> l = new ArrayList<>(fromParent.length + fromChild.length); 407 for (Object o : fromChild) { 408 if (o == Inherit.class) 409 l.addAll(Arrays.asList(fromParent)); 410 else 411 l.add(o); 412 } 413 return l.toArray(new Object[l.size()]); 414 } 415}