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}