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