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 javax.servlet.http.HttpServletResponse.*; 016import static org.apache.juneau.BeanContext.*; 017import static org.apache.juneau.internal.ClassUtils.*; 018import static org.apache.juneau.internal.CollectionUtils.*; 019import static org.apache.juneau.internal.StringUtils.*; 020import static org.apache.juneau.internal.Utils.*; 021import static org.apache.juneau.rest.RestContext.*; 022 023import java.lang.annotation.*; 024import java.lang.reflect.*; 025import java.util.*; 026 027import javax.servlet.http.*; 028 029import org.apache.juneau.*; 030import org.apache.juneau.encoders.*; 031import org.apache.juneau.http.*; 032import org.apache.juneau.httppart.*; 033import org.apache.juneau.internal.*; 034import org.apache.juneau.parser.*; 035import org.apache.juneau.rest.annotation.*; 036import org.apache.juneau.rest.widget.*; 037import org.apache.juneau.serializer.*; 038import org.apache.juneau.svl.*; 039import org.apache.juneau.utils.*; 040 041/** 042 * Represents a single Java servlet/resource method annotated with {@link RestMethod @RestMethod}. 043 */ 044public class RestJavaMethod implements Comparable<RestJavaMethod> { 045 private final String httpMethod; 046 private final UrlPathPattern pathPattern; 047 final RestParam[] params; 048 private final RestGuard[] guards; 049 private final RestMatcher[] optionalMatchers; 050 private final RestMatcher[] requiredMatchers; 051 private final RestConverter[] converters; 052 private final RestMethodProperties properties; 053 private final Integer priority; 054 private final RestContext context; 055 final java.lang.reflect.Method method; 056 final SerializerGroup serializers; 057 final ParserGroup parsers; 058 final EncoderGroup encoders; 059 final HttpPartSerializer partSerializer; 060 final HttpPartParser partParser; 061 final Map<String,Object> 062 defaultRequestHeaders, 063 defaultQuery, 064 defaultFormData; 065 final String defaultCharset; 066 final long maxInput; 067 final BeanContext beanContext; 068 final Map<String,Widget> widgets; 069 final List<MediaType> 070 supportedAcceptTypes, 071 supportedContentTypes; 072 073 RestJavaMethod(Object servlet, java.lang.reflect.Method method, RestContext context) throws RestServletException { 074 Builder b = new Builder(servlet, method, context); 075 this.context = context; 076 this.method = method; 077 this.httpMethod = b.httpMethod; 078 this.pathPattern = b.pathPattern; 079 this.params = b.params; 080 this.guards = b.guards; 081 this.optionalMatchers = b.optionalMatchers; 082 this.requiredMatchers = b.requiredMatchers; 083 this.converters = b.converters; 084 this.serializers = b.serializers; 085 this.parsers = b.parsers; 086 this.encoders = b.encoders; 087 this.partParser = b.partParser; 088 this.partSerializer = b.partSerializer; 089 this.beanContext = b.beanContext; 090 this.properties = b.properties; 091 this.defaultRequestHeaders = b.defaultRequestHeaders; 092 this.defaultQuery = b.defaultQuery; 093 this.defaultFormData = b.defaultFormData; 094 this.defaultCharset = b.defaultCharset; 095 this.maxInput = b.maxInput; 096 this.priority = b.priority; 097 this.supportedAcceptTypes = b.supportedAcceptTypes; 098 this.supportedContentTypes = b.supportedContentTypes; 099 this.widgets = unmodifiableMap(b.widgets); 100 } 101 102 private static final class Builder { 103 String httpMethod, defaultCharset; 104 UrlPathPattern pathPattern; 105 RestParam[] params; 106 RestGuard[] guards; 107 RestMatcher[] optionalMatchers, requiredMatchers; 108 RestConverter[] converters; 109 SerializerGroup serializers; 110 ParserGroup parsers; 111 EncoderGroup encoders; 112 HttpPartParser partParser; 113 HttpPartSerializer partSerializer; 114 BeanContext beanContext; 115 RestMethodProperties properties; 116 Map<String,Object> defaultRequestHeaders, defaultQuery, defaultFormData; 117 long maxInput; 118 Integer priority; 119 Map<String,Widget> widgets; 120 List<MediaType> supportedAcceptTypes, supportedContentTypes; 121 122 Builder(Object servlet, java.lang.reflect.Method method, RestContext context) throws RestServletException { 123 String sig = method.getDeclaringClass().getName() + '.' + method.getName(); 124 125 try { 126 127 RestMethod m = method.getAnnotation(RestMethod.class); 128 if (m == null) 129 throw new RestServletException("@RestMethod annotation not found on method ''{0}''", sig); 130 131 VarResolver vr = context.getVarResolver(); 132 133 serializers = context.getSerializers(); 134 parsers = context.getParsers(); 135 partSerializer = context.getPartSerializer(); 136 partParser = context.getPartParser(); 137 beanContext = context.getBeanContext(); 138 encoders = context.getEncoders(); 139 properties = new RestMethodProperties(context.getProperties()); 140 defaultCharset = context.getDefaultCharset(); 141 maxInput = context.getMaxInput(); 142 143 if (! m.defaultCharset().isEmpty()) 144 defaultCharset = vr.resolve(m.defaultCharset()); 145 if (! m.maxInput().isEmpty()) 146 maxInput = StringUtils.parseLongWithSuffix(vr.resolve(m.maxInput())); 147 148 HtmlDocBuilder hdb = new HtmlDocBuilder(properties); 149 150 HtmlDoc hd = m.htmldoc(); 151 hdb.process(hd); 152 153 widgets = new HashMap<>(context.getWidgets()); 154 for (Class<? extends Widget> wc : hd.widgets()) { 155 Widget w = beanContext.newInstance(Widget.class, wc); 156 widgets.put(w.getName(), w); 157 hdb.script("INHERIT", "$W{"+w.getName()+".script}"); 158 hdb.style("INHERIT", "$W{"+w.getName()+".style}"); 159 } 160 161 ASet<String> inherit = new ASet<String>().appendAll(StringUtils.split(m.inherit())); 162 if (inherit.contains("*")) 163 inherit.appendAll("SERIALIZERS","PARSERS","TRANSFORMS","PROPERTIES","ENCODERS"); 164 165 SerializerGroupBuilder sgb = null; 166 ParserGroupBuilder pgb = null; 167 ParserBuilder uepb = null; 168 BeanContextBuilder bcb = null; 169 PropertyStore cps = context.getPropertyStore(); 170 171 if (m.serializers().length > 0 || m.parsers().length > 0 || m.properties().length > 0 || m.flags().length > 0 172 || m.beanFilters().length > 0 || m.pojoSwaps().length > 0 || m.bpi().length > 0 173 || m.bpx().length > 0) { 174 sgb = SerializerGroup.create(); 175 pgb = ParserGroup.create(); 176 uepb = Parser.create(); 177 bcb = beanContext.builder(); 178 179 if (inherit.contains("SERIALIZERS") || m.serializers().length == 0) 180 sgb.append(cps.getArrayProperty(REST_serializers, Object.class)); 181 182 if (inherit.contains("PARSERS") || m.parsers().length == 0) 183 pgb.append(cps.getArrayProperty(REST_parsers, Object.class)); 184 } 185 186 httpMethod = m.name().toUpperCase(Locale.ENGLISH); 187 if (httpMethod.equals("") && method.getName().startsWith("do")) 188 httpMethod = method.getName().substring(2).toUpperCase(Locale.ENGLISH); 189 if (httpMethod.equals("")) 190 httpMethod = "GET"; 191 if (httpMethod.equals("METHOD")) 192 httpMethod = "*"; 193 194 priority = m.priority(); 195 196 String p = m.path(); 197 converters = new RestConverter[m.converters().length]; 198 for (int i = 0; i < converters.length; i++) 199 converters[i] = beanContext.newInstance(RestConverter.class, m.converters()[i]); 200 201 guards = new RestGuard[m.guards().length]; 202 for (int i = 0; i < guards.length; i++) 203 guards[i] = beanContext.newInstance(RestGuard.class, m.guards()[i]); 204 205 List<RestMatcher> optionalMatchers = new LinkedList<>(), requiredMatchers = new LinkedList<>(); 206 for (int i = 0; i < m.matchers().length; i++) { 207 Class<? extends RestMatcher> c = m.matchers()[i]; 208 RestMatcher matcher = beanContext.newInstance(RestMatcher.class, c, true, servlet, method); 209 if (matcher.mustMatch()) 210 requiredMatchers.add(matcher); 211 else 212 optionalMatchers.add(matcher); 213 } 214 if (! m.clientVersion().isEmpty()) 215 requiredMatchers.add(new ClientVersionMatcher(context.getClientVersionHeader(), method)); 216 217 this.requiredMatchers = requiredMatchers.toArray(new RestMatcher[requiredMatchers.size()]); 218 this.optionalMatchers = optionalMatchers.toArray(new RestMatcher[optionalMatchers.size()]); 219 220 PropertyStore ps = context.getPropertyStore(); 221 if (! inherit.contains("TRANSFORMS")) 222 ps = ps.builder().set(BEAN_beanFilters, null).set(BEAN_pojoSwaps, null).build(); 223 224 if (sgb != null) { 225 sgb.append(m.serializers()); 226 227 if (! inherit.contains("PROPERTIES")) 228 sgb.beanFilters((Object[])ps.getClassArrayProperty(BEAN_beanFilters)).pojoSwaps(ps.getClassArrayProperty(BEAN_pojoSwaps)); 229 else 230 sgb.apply(ps); 231 for (Property p1 : m.properties()) 232 sgb.set(p1.name(), p1.value()); 233 for (String p1 : m.flags()) 234 sgb.set(p1, true); 235 if (m.bpi().length > 0) { 236 Map<String,String> bpiMap = new LinkedHashMap<>(); 237 for (String s : m.bpi()) { 238 for (String s2 : split(s, ';')) { 239 int i = s2.indexOf(':'); 240 if (i == -1) 241 throw new RestServletException( 242 "Invalid format for @RestMethod.bpi() on method ''{0}''. Must be in the format \"ClassName: comma-delimited-tokens\". \nValue: {1}", sig, s); 243 bpiMap.put(s2.substring(0, i).trim(), s2.substring(i+1).trim()); 244 } 245 } 246 sgb.includeProperties(bpiMap); 247 } 248 if (m.bpx().length > 0) { 249 Map<String,String> bpxMap = new LinkedHashMap<>(); 250 for (String s : m.bpx()) { 251 for (String s2 : split(s, ';')) { 252 int i = s2.indexOf(':'); 253 if (i == -1) 254 throw new RestServletException( 255 "Invalid format for @RestMethod.bpx() on method ''{0}''. Must be in the format \"ClassName: comma-delimited-tokens\". \nValue: {1}", sig, s); 256 bpxMap.put(s2.substring(0, i).trim(), s2.substring(i+1).trim()); 257 } 258 } 259 sgb.excludeProperties(bpxMap); 260 } 261 sgb.beanFilters((Object[])m.beanFilters()); 262 sgb.pojoSwaps(m.pojoSwaps()); 263 } 264 265 if (pgb != null) { 266 pgb.append(m.parsers()); 267 if (! inherit.contains("PROPERTIES")) 268 pgb.beanFilters((Object[])ps.getClassArrayProperty(BEAN_beanFilters)).pojoSwaps(ps.getClassArrayProperty(BEAN_pojoSwaps)); 269 else 270 pgb.apply(ps); 271 for (Property p1 : m.properties()) 272 pgb.set(p1.name(), p1.value()); 273 for (String p1 : m.flags()) 274 pgb.set(p1, true); 275 pgb.beanFilters((Object[])m.beanFilters()); 276 pgb.pojoSwaps(m.pojoSwaps()); 277 } 278 279 if (uepb != null) { 280 uepb.apply(ps); 281 for (Property p1 : m.properties()) 282 uepb.set(p1.name(), p1.value()); 283 for (String p1 : m.flags()) 284 uepb.set(p1, true); 285 uepb.beanFilters((Object[])m.beanFilters()); 286 uepb.pojoSwaps(m.pojoSwaps()); 287 } 288 289 if (bcb != null) { 290 bcb.apply(ps); 291 for (Property p1 : m.properties()) 292 bcb.set(p1.name(), p1.value()); 293 for (String p1 : m.flags()) 294 bcb.set(p1, true); 295 bcb.beanFilters((Object[])m.beanFilters()); 296 bcb.pojoSwaps(m.pojoSwaps()); 297 } 298 299 if (m.properties().length > 0 || m.flags().length > 0) { 300 properties = new RestMethodProperties(properties); 301 for (Property p1 : m.properties()) 302 properties.put(p1.name(), p1.value()); 303 for (String p1 : m.flags()) 304 properties.put(p1, true); 305 } 306 307 if (m.encoders().length > 0) { 308 EncoderGroupBuilder g = EncoderGroup.create().append(IdentityEncoder.INSTANCE); 309 if (inherit.contains("ENCODERS")) 310 g.append(encoders); 311 312 for (Class<? extends Encoder> c : m.encoders()) { 313 try { 314 g.append(c); 315 } catch (Exception e) { 316 throw new RestServletException( 317 "Exception occurred while trying to instantiate ConfigEncoder on method ''{0}'': ''{1}''", sig, c.getSimpleName()).initCause(e); 318 } 319 } 320 encoders = g.build(); 321 } 322 323 defaultRequestHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 324 for (String s : m.defaultRequestHeaders()) { 325 String[] h = RestUtils.parseKeyValuePair(vr.resolve(s)); 326 if (h == null) 327 throw new RestServletException( 328 "Invalid default request header specified on method ''{0}'': ''{1}''. Must be in the format: ''name[:=]value''", sig, s); 329 defaultRequestHeaders.put(h[0], h[1]); 330 } 331 332 defaultQuery = new LinkedHashMap<>(); 333 for (String s : m.defaultQuery()) { 334 String[] h = RestUtils.parseKeyValuePair(vr.resolve(s)); 335 if (h == null) 336 throw new RestServletException( 337 "Invalid default query parameter specified on method ''{0}'': ''{1}''. Must be in the format: ''name[:=]value''", sig, s); 338 defaultQuery.put(h[0], h[1]); 339 } 340 341 defaultFormData = new LinkedHashMap<>(); 342 for (String s : m.defaultFormData()) { 343 String[] h = RestUtils.parseKeyValuePair(vr.resolve(s)); 344 if (h == null) 345 throw new RestServletException( 346 "Invalid default form data parameter specified on method ''{0}'': ''{1}''. Must be in the format: ''name[:=]value''", sig, s); 347 defaultFormData.put(h[0], h[1]); 348 } 349 350 Type[] pt = method.getGenericParameterTypes(); 351 Annotation[][] pa = method.getParameterAnnotations(); 352 for (int i = 0; i < pt.length; i++) { 353 for (Annotation a : pa[i]) { 354 if (a instanceof Header) { 355 Header h = (Header)a; 356 if (! h.def().isEmpty()) 357 defaultRequestHeaders.put(firstNonEmpty(h.name(), h.value()), h.def()); 358 } else if (a instanceof Query) { 359 Query q = (Query)a; 360 if (! q.def().isEmpty()) 361 defaultQuery.put(firstNonEmpty(q.name(), q.value()), q.def()); 362 } else if (a instanceof FormData) { 363 FormData f = (FormData)a; 364 if (! f.def().isEmpty()) 365 defaultFormData.put(firstNonEmpty(f.name(), f.value()), f.def()); 366 } 367 } 368 } 369 370 pathPattern = new UrlPathPattern(p); 371 372 if (sgb != null) 373 serializers = sgb.build(); 374 if (pgb != null) 375 parsers = pgb.build(); 376 if (uepb != null && partParser instanceof Parser) { 377 Parser pp = (Parser)partParser; 378 partParser = (HttpPartParser)pp.builder().apply(uepb.getPropertyStore()).build(); 379 } 380 if (bcb != null) 381 beanContext = bcb.build(); 382 383 supportedAcceptTypes = 384 m.produces().length > 0 385 ? immutableList(MediaType.forStrings(resolveVars(vr, m.produces()))) 386 : serializers.getSupportedMediaTypes(); 387 supportedContentTypes = 388 m.consumes().length > 0 389 ? immutableList(MediaType.forStrings(resolveVars(vr, m.consumes()))) 390 : parsers.getSupportedMediaTypes(); 391 392 params = context.findParams(method, pathPattern, false); 393 394 // Need this to access methods in anonymous inner classes. 395 method.setAccessible(true); 396 } catch (RestServletException e) { 397 throw e; 398 } catch (Exception e) { 399 throw new RestServletException("Exception occurred while initializing method ''{0}''", sig).initCause(e); 400 } 401 } 402 } 403 404 /** 405 * Returns <jk>true</jk> if this Java method has any guards or matchers. 406 */ 407 boolean hasGuardsOrMatchers() { 408 return (guards.length != 0 || requiredMatchers.length != 0 || optionalMatchers.length != 0); 409 } 410 411 /** 412 * Returns the HTTP method name (e.g. <js>"GET"</js>). 413 */ 414 String getHttpMethod() { 415 return httpMethod; 416 } 417 418 /** 419 * Returns the path pattern for this method. 420 */ 421 String getPathPattern() { 422 return pathPattern.toString(); 423 } 424 425 /** 426 * Returns <jk>true</jk> if the specified request object can call this method. 427 */ 428 boolean isRequestAllowed(RestRequest req) { 429 for (RestGuard guard : guards) { 430 req.setJavaMethod(method); 431 if (! guard.isRequestAllowed(req)) 432 return false; 433 } 434 return true; 435 } 436 437 /** 438 * Workhorse method. 439 * 440 * @param pathInfo The value of {@link HttpServletRequest#getPathInfo()} (sorta) 441 * @return The HTTP response code. 442 */ 443 int invoke(String pathInfo, RestRequest req, RestResponse res) throws RestException { 444 445 String[] patternVals = pathPattern.match(pathInfo); 446 if (patternVals == null) 447 return SC_NOT_FOUND; 448 449 String remainder = null; 450 if (patternVals.length > pathPattern.getVars().length) 451 remainder = patternVals[pathPattern.getVars().length]; 452 for (int i = 0; i < pathPattern.getVars().length; i++) 453 req.getPathMatch().put(pathPattern.getVars()[i], patternVals[i]); 454 req.getPathMatch().pattern(pathPattern.getPatternString()).remainder(remainder); 455 456 RequestProperties requestProperties = new RequestProperties(req.getVarResolverSession(), properties); 457 458 req.init(this, requestProperties); 459 res.init(this, requestProperties); 460 461 // Class-level guards 462 for (RestGuard guard : context.getGuards()) 463 if (! guard.guard(req, res)) 464 return SC_UNAUTHORIZED; 465 466 // If the method implements matchers, test them. 467 for (RestMatcher m : requiredMatchers) 468 if (! m.matches(req)) 469 return SC_PRECONDITION_FAILED; 470 if (optionalMatchers.length > 0) { 471 boolean matches = false; 472 for (RestMatcher m : optionalMatchers) 473 matches |= m.matches(req); 474 if (! matches) 475 return SC_PRECONDITION_FAILED; 476 } 477 478 context.preCall(req, res); 479 480 Object[] args = new Object[params.length]; 481 for (int i = 0; i < params.length; i++) { 482 try { 483 args[i] = params[i].resolve(req, res); 484 } catch (RestException e) { 485 throw e; 486 } catch (Exception e) { 487 throw new RestException(SC_BAD_REQUEST, 488 "Invalid data conversion. Could not convert {0} ''{1}'' to type ''{2}'' on method ''{3}.{4}''.", 489 params[i].getParamType().name(), params[i].getName(), params[i].getType(), method.getDeclaringClass().getName(), method.getName() 490 ).initCause(e); 491 } 492 } 493 494 try { 495 496 for (RestGuard guard : guards) 497 if (! guard.guard(req, res)) 498 return SC_OK; 499 500 Object output = method.invoke(context.getResource(), args); 501 if (! method.getReturnType().equals(Void.TYPE)) 502 if (output != null || ! res.getOutputStreamCalled()) 503 res.setOutput(output); 504 505 context.postCall(req, res); 506 507 if (res.hasOutput()) { 508 output = res.getOutput(); 509 for (RestConverter converter : converters) 510 output = converter.convert(req, output); 511 res.setOutput(output); 512 } 513 } catch (IllegalArgumentException e) { 514 throw new RestException(SC_BAD_REQUEST, 515 "Invalid argument type passed to the following method: ''{0}''.\n\tArgument types: {1}", 516 method.toString(), getReadableClassNames(args) 517 ).initCause(e); 518 } catch (InvocationTargetException e) { 519 Throwable e2 = e.getTargetException(); // Get the throwable thrown from the doX() method. 520 if (e2 instanceof RestException) 521 throw (RestException)e2; 522 if (e2 instanceof ParseException) 523 throw new RestException(SC_BAD_REQUEST, e2); 524 if (e2 instanceof InvalidDataConversionException) 525 throw new RestException(SC_BAD_REQUEST, e2); 526 throw new RestException(SC_INTERNAL_SERVER_ERROR, e2); 527 } catch (RestException e) { 528 throw e; 529 } catch (Exception e) { 530 throw new RestException(SC_INTERNAL_SERVER_ERROR, e); 531 } 532 return SC_OK; 533 } 534 535 @Override /* Object */ 536 public String toString() { 537 return "SimpleMethod: name=" + httpMethod + ", path=" + pathPattern.getPatternString(); 538 } 539 540 /* 541 * compareTo() method is used to keep SimpleMethods ordered in the RestCallRouter list. 542 * It maintains the order in which matches are made during requests. 543 */ 544 @Override /* Comparable */ 545 public int compareTo(RestJavaMethod o) { 546 int c; 547 548 c = priority.compareTo(o.priority); 549 if (c != 0) 550 return c; 551 552 c = pathPattern.compareTo(o.pathPattern); 553 if (c != 0) 554 return c; 555 556 c = compare(o.requiredMatchers.length, requiredMatchers.length); 557 if (c != 0) 558 return c; 559 560 c = compare(o.optionalMatchers.length, optionalMatchers.length); 561 if (c != 0) 562 return c; 563 564 c = compare(o.guards.length, guards.length); 565 if (c != 0) 566 return c; 567 568 return 0; 569 } 570 571 @Override /* Object */ 572 public boolean equals(Object o) { 573 if (! (o instanceof RestJavaMethod)) 574 return false; 575 return (compareTo((RestJavaMethod)o) == 0); 576 } 577 578 @Override /* Object */ 579 public int hashCode() { 580 return super.hashCode(); 581 } 582 583 static String[] resolveVars(VarResolver vr, String[] in) { 584 String[] out = new String[in.length]; 585 for (int i = 0; i < in.length; i++) 586 out[i] = vr.resolve(in[i]); 587 return out; 588 } 589}