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