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; 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.parser.*; 025import org.apache.juneau.uon.*; 026import org.apache.juneau.utils.*; 027 028/** 029 * Various reusable utility methods. 030 */ 031public final class RestUtils { 032 033 /** 034 * Returns readable text for an HTTP response code. 035 * 036 * @param rc The HTTP response code. 037 * @return Readable text for an HTTP response code, or <jk>null</jk> if it's an invalid code. 038 */ 039 public static String getHttpResponseText(int rc) { 040 return httpMsgs.get(rc); 041 } 042 043 private static Map<Integer,String> httpMsgs = new AMap<Integer,String>() 044 .append(200, "OK") 045 .append(201, "Created") 046 .append(202, "Accepted") 047 .append(203, "Non-Authoritative Information") 048 .append(204, "No Content") 049 .append(205, "Reset Content") 050 .append(206, "Partial Content") 051 .append(300, "Multiple Choices") 052 .append(301, "Moved Permanently") 053 .append(302, "Temporary Redirect") 054 .append(303, "See Other") 055 .append(304, "Not Modified") 056 .append(305, "Use Proxy") 057 .append(307, "Temporary Redirect") 058 .append(400, "Bad Request") 059 .append(401, "Unauthorized") 060 .append(402, "Payment Required") 061 .append(403, "Forbidden") 062 .append(404, "Not Found") 063 .append(405, "Method Not Allowed") 064 .append(406, "Not Acceptable") 065 .append(407, "Proxy Authentication Required") 066 .append(408, "Request Time-Out") 067 .append(409, "Conflict") 068 .append(410, "Gone") 069 .append(411, "Length Required") 070 .append(412, "Precondition Failed") 071 .append(413, "Request Entity Too Large") 072 .append(414, "Request-URI Too Large") 073 .append(415, "Unsupported Media Type") 074 .append(500, "Internal Server Error") 075 .append(501, "Not Implemented") 076 .append(502, "Bad Gateway") 077 .append(503, "Service Unavailable") 078 .append(504, "Gateway Timeout") 079 .append(505, "HTTP Version Not Supported") 080 ; 081 082 /** 083 * Identical to {@link HttpServletRequest#getPathInfo()} but doesn't decode encoded characters. 084 * 085 * @param req The HTTP request 086 * @return The un-decoded path info. 087 */ 088 public static String getPathInfoUndecoded(HttpServletRequest req) { 089 String requestURI = req.getRequestURI(); 090 String contextPath = req.getContextPath(); 091 String servletPath = req.getServletPath(); 092 int l = contextPath.length() + servletPath.length(); 093 if (requestURI.length() == l) 094 return null; 095 return requestURI.substring(l); 096 } 097 098 /** 099 * Efficiently trims the path info part from a request URI. 100 * 101 * <p> 102 * The result is the URI of the servlet itself. 103 * 104 * @param requestURI The value returned by {@link HttpServletRequest#getRequestURL()} 105 * @param contextPath The value returned by {@link HttpServletRequest#getContextPath()} 106 * @param servletPath The value returned by {@link HttpServletRequest#getServletPath()} 107 * @return The same StringBuilder with remainder trimmed. 108 */ 109 public static StringBuffer trimPathInfo(StringBuffer requestURI, String contextPath, String servletPath) { 110 if (servletPath.equals("/")) 111 servletPath = ""; 112 if (contextPath.equals("/")) 113 contextPath = ""; 114 115 try { 116 // Given URL: http://hostname:port/servletPath/extra 117 // We want: http://hostname:port/servletPath 118 int sc = 0; 119 for (int i = 0; i < requestURI.length(); i++) { 120 char c = requestURI.charAt(i); 121 if (c == '/') { 122 sc++; 123 if (sc == 3) { 124 if (servletPath.isEmpty()) { 125 requestURI.setLength(i); 126 return requestURI; 127 } 128 129 // Make sure context path follows the authority. 130 for (int j = 0; j < contextPath.length(); i++, j++) 131 if (requestURI.charAt(i) != contextPath.charAt(j)) 132 throw new Exception("case=1"); 133 134 // Make sure servlet path follows the authority. 135 for (int j = 0; j < servletPath.length(); i++, j++) 136 if (requestURI.charAt(i) != servletPath.charAt(j)) 137 throw new Exception("case=2"); 138 139 // Make sure servlet path isn't a false match (e.g. /foo2 should not match /foo) 140 c = (requestURI.length() == i ? '/' : requestURI.charAt(i)); 141 if (c == '/' || c == '?') { 142 requestURI.setLength(i); 143 return requestURI; 144 } 145 146 throw new Exception("case=3"); 147 } 148 } else if (c == '?') { 149 if (sc != 2) 150 throw new Exception("case=4"); 151 if (servletPath.isEmpty()) { 152 requestURI.setLength(i); 153 return requestURI; 154 } 155 throw new Exception("case=5"); 156 } 157 } 158 if (servletPath.isEmpty()) 159 return requestURI; 160 throw new Exception("case=6"); 161 } catch (Exception e) { 162 throw new FormattedRuntimeException(e, "Could not find servlet path in request URI. URI=''{0}'', servletPath=''{1}''", requestURI, servletPath); 163 } 164 } 165 166 static String[] parseHeader(String s) { 167 int i = s.indexOf(':'); 168 if (i == -1) 169 return null; 170 String name = s.substring(0, i).trim().toLowerCase(Locale.ENGLISH); 171 String val = s.substring(i+1).trim(); 172 return new String[]{name,val}; 173 } 174 175 /** 176 * Parses key/value pairs separated by either : or = 177 */ 178 static String[] parseKeyValuePair(String s) { 179 int i = -1; 180 for (int j = 0; j < s.length() && i < 0; j++) { 181 char c = s.charAt(j); 182 if (c == '=' || c == ':') 183 i = j; 184 } 185 if (i == -1) 186 return null; 187 String name = s.substring(0, i).trim(); 188 String val = s.substring(i+1).trim(); 189 return new String[]{name,val}; 190 } 191 192 static String resolveNewlineSeparatedAnnotation(String[] value, String fromParent) { 193 if (value.length == 0) 194 return fromParent; 195 196 List<String> l = new ArrayList<>(); 197 for (String v : value) { 198 if (! "INHERIT".equals(v)) 199 l.add(v); 200 else if (fromParent != null) 201 l.addAll(Arrays.asList(fromParent)); 202 } 203 return join(l, '\n'); 204 } 205 206 private static final Pattern INDEXED_LINK_PATTERN = Pattern.compile("(?s)(\\S*)\\[(\\d+)\\]\\:(.*)"); 207 208 static String[] resolveLinks(String[] links, String[] parentLinks) { 209 if (links.length == 0) 210 return parentLinks; 211 212 List<String> list = new ArrayList<>(); 213 for (String l : links) { 214 if ("INHERIT".equals(l)) 215 list.addAll(Arrays.asList(parentLinks)); 216 else if (l.indexOf('[') != -1 && INDEXED_LINK_PATTERN.matcher(l).matches()) { 217 Matcher lm = INDEXED_LINK_PATTERN.matcher(l); 218 lm.matches(); 219 String key = lm.group(1); 220 int index = Math.min(list.size(), Integer.parseInt(lm.group(2))); 221 String remainder = lm.group(3); 222 list.add(index, key.isEmpty() ? remainder : key + ":" + remainder); 223 } else { 224 list.add(l); 225 } 226 } 227 return list.toArray(new String[list.size()]); 228 } 229 230 static String[] resolveContent(String[] content, String[] parentContent) { 231 if (content.length == 0) 232 return parentContent; 233 234 List<String> list = new ArrayList<>(); 235 for (String l : content) { 236 if ("INHERIT".equals(l)) { 237 list.addAll(Arrays.asList(parentContent)); 238 } else if ("NONE".equals(l)) { 239 return new String[0]; 240 } else { 241 list.add(l); 242 } 243 } 244 return list.toArray(new String[list.size()]); 245 } 246 247 /** 248 * Parses a URL query string or form-data body. 249 * 250 * @param qs A reader or string containing the query string to parse. 251 * @return A new map containing the parsed query. 252 * @throws Exception 253 */ 254 public static Map<String,String[]> parseQuery(Object qs) throws Exception { 255 return parseQuery(qs, null); 256 } 257 258 /** 259 * Same as {@link #parseQuery(Object)} but allows you to specify the map to insert values into. 260 * 261 * @param qs A reader containing the query string to parse. 262 * @param map The map to pass the values into. 263 * @return The same map passed in, or a new map if it was <jk>null</jk>. 264 * @throws Exception 265 */ 266 public static Map<String,String[]> parseQuery(Object qs, Map<String,String[]> map) throws Exception { 267 268 Map<String,String[]> m = map == null ? new TreeMap<String,String[]>() : map; 269 270 if (qs == null || ((qs instanceof CharSequence) && isEmpty(qs))) 271 return m; 272 273 try (ParserPipe p = new ParserPipe(qs, false, false, false, false, null, null)) { 274 275 final int S1=1; // Looking for attrName start. 276 final int S2=2; // Found attrName start, looking for = or & or end. 277 final int S3=3; // Found =, looking for valStart. 278 final int S4=4; // Found valStart, looking for & or end. 279 280 try (UonReader r = new UonReader(p, true)) { 281 int c = r.peekSkipWs(); 282 if (c == '?') 283 r.read(); 284 285 int state = S1; 286 String currAttr = null; 287 while (c != -1) { 288 c = r.read(); 289 if (state == S1) { 290 if (c != -1) { 291 r.unread(); 292 r.mark(); 293 state = S2; 294 } 295 } else if (state == S2) { 296 if (c == -1) { 297 add(m, r.getMarked(), null); 298 } else if (c == '\u0001') { 299 m.put(r.getMarked(0,-1), null); 300 state = S1; 301 } else if (c == '\u0002') { 302 currAttr = r.getMarked(0,-1); 303 state = S3; 304 } 305 } else if (state == S3) { 306 if (c == -1 || c == '\u0001') { 307 add(m, currAttr, ""); 308 } else { 309 if (c == '\u0002') 310 r.replace('='); 311 r.unread(); 312 r.mark(); 313 state = S4; 314 } 315 } else if (state == S4) { 316 if (c == -1) { 317 add(m, currAttr, r.getMarked()); 318 } else if (c == '\u0001') { 319 add(m, currAttr, r.getMarked(0,-1)); 320 state = S1; 321 } else if (c == '\u0002') { 322 r.replace('='); 323 } 324 } 325 } 326 } 327 328 return m; 329 } 330 } 331 332 private static void add(Map<String,String[]> m, String key, String val) { 333 boolean b = m.containsKey(key); 334 if (val == null) { 335 if (! b) 336 m.put(key, null); 337 } else if (b && m.get(key) != null) { 338 m.put(key, append(m.get(key), val)); 339 } else { 340 m.put(key, new String[]{val}); 341 } 342 } 343}