001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.rest;
014
015import static org.apache.juneau.internal.ReflectionUtils.*;
016import static org.apache.juneau.internal.StringUtils.*;
017
018import java.lang.reflect.Method;
019import java.util.*;
020import java.util.concurrent.*;
021
022import org.apache.juneau.*;
023import org.apache.juneau.dto.swagger.*;
024import org.apache.juneau.http.*;
025import org.apache.juneau.internal.*;
026import org.apache.juneau.json.*;
027import org.apache.juneau.parser.*;
028import org.apache.juneau.rest.annotation.*;
029import org.apache.juneau.svl.*;
030import org.apache.juneau.utils.*;
031
032/**
033 * Default implementation of {@link RestInfoProvider}.
034 * 
035 * <p>
036 * Subclasses can override these methods to tailor how HTTP REST resources are documented.
037 * 
038 * <h5 class='section'>See Also:</h5>
039 * <ul>
040 *    <li class='jf'>{@link RestContext#REST_infoProvider}
041 *    <li class='link'><a class="doclink" href="../../../../overview-summary.html#juneau-rest-server.OptionsPages">Overview &gt; juneau-rest-server &gt; OPTIONS Pages</a>
042 * </ul>
043 */
044public class BasicRestInfoProvider implements RestInfoProvider {
045
046   private final RestContext context;
047   private final String
048      siteName,
049      title,
050      description;
051   private final ConcurrentHashMap<Locale,Swagger> swaggers = new ConcurrentHashMap<>();
052
053   /**
054    * Constructor.
055    * 
056    * @param context The resource context.
057    */
058   public BasicRestInfoProvider(RestContext context) {
059      this.context = context;
060
061      Builder b = new Builder(context);
062      this.siteName = b.siteName;
063      this.title = b.title;
064      this.description = b.description;
065   }
066
067   private static final class Builder {
068      String
069         siteName,
070         title,
071         description;
072      
073      Builder(RestContext context) {
074
075         LinkedHashMap<Class<?>,RestResource> restResourceAnnotationsParentFirst = findAnnotationsMapParentFirst(RestResource.class, context.getResource().getClass());
076
077         for (RestResource r : restResourceAnnotationsParentFirst.values()) {
078            if (! r.siteName().isEmpty())
079               siteName = r.siteName();
080            if (! r.title().isEmpty())
081               title = r.title();
082            if (! r.description().isEmpty())
083               description = r.description();
084         }
085      }
086   }
087
088   /**
089    * Returns the localized swagger for this REST resource.
090    * 
091    * <p>
092    * Subclasses can override this method to customize the Swagger.
093    * 
094    * @param req The incoming HTTP request.
095    * @return 
096    *    A new Swagger instance.
097    *    <br>Never <jk>null</jk>.
098    * @throws Exception
099    */
100   @Override /* RestInfoProvider */
101   public Swagger getSwagger(RestRequest req) throws Exception {
102      
103      Locale locale = req.getLocale();
104      
105      Swagger s = swaggers.get(locale);
106      if (s != null)
107         return s;
108
109      VarResolverSession vr = req.getVarResolverSession();
110      JsonParser jp = JsonParser.DEFAULT;
111      MessageBundle mb = context.getMessages();
112      
113      ObjectMap om = context.getClasspathResource(ObjectMap.class, MediaType.JSON, getClass().getSimpleName() + ".json", locale);
114      if (om == null)
115         om = new ObjectMap();
116      
117      LinkedHashMap<Class<?>,RestResource> restResourceAnnotationsParentFirst = findAnnotationsMapParentFirst(RestResource.class, context.getResource().getClass());
118
119      for (RestResource r : restResourceAnnotationsParentFirst.values()) {
120         if (r.swagger().length > 0) {
121            try {
122               String json = vr.resolve(StringUtils.join(r.swagger(), '\n').trim());
123               if (! (json.startsWith("{") && json.endsWith("}")))
124                  json = "{\n" + json + "\n}";
125               om.putAll(new ObjectMap(json));
126            } catch (ParseException e) {
127               throw new ParseException("Malformed swagger JSON encountered in @RestResource(swagger) on class "+context.getResource().getClass().getName()+".").initCause(e);
128            }
129         }
130      }
131
132      String title = this.title;
133      if (title == null)
134         title = mb.findFirstString(locale, "title");
135      if (title != null) 
136         getInfo(om).put("title", vr.resolve(title));
137
138      String description = this.description;
139      if (description == null)
140         description = mb.findFirstString(locale, "description");
141      if (description != null) 
142         getInfo(om).put("description", vr.resolve(description));
143      
144      String version = mb.findFirstString(locale, "version");
145      if (version != null) 
146         getInfo(om).put("version", vr.resolve(version));
147      
148      String contact = mb.findFirstString(locale, "contact");
149      if (contact != null) 
150         getInfo(om).put("contact", jp.parse(vr.resolve(contact), ObjectMap.class));
151      
152      String license = mb.findFirstString(locale, "license");
153      if (license != null) 
154         getInfo(om).put("license", jp.parse(vr.resolve(license), ObjectMap.class));
155      
156      String termsOfService = mb.findFirstString(locale, "termsOfService");
157      if (termsOfService != null) 
158         getInfo(om).put("termsOfService", vr.resolve(termsOfService));
159      
160      if (! om.containsKey("consumes")) {
161         List<MediaType> consumes = req.getContext().getConsumes();
162         if (! consumes.isEmpty())
163            om.put("consumes", consumes);
164      }
165
166      if (! om.containsKey("produces")) {
167         List<MediaType> produces = req.getContext().getProduces();
168         if (! produces.isEmpty())
169            om.put("produces", produces);
170      }
171         
172      String tags = mb.findFirstString(locale, "tags");
173      if (tags != null)
174         om.put("tags", jp.parse(vr.resolve(tags), ObjectList.class));
175
176      String externalDocs = mb.findFirstString(locale, "externalDocs");
177      if (externalDocs != null)
178         om.put("externalDocs", jp.parse(vr.resolve(externalDocs), ObjectMap.class));
179      
180      for (RestJavaMethod sm : context.getCallMethods().values()) {
181         if (sm.isRequestAllowed(req)) {
182            Method m = sm.method;
183            RestMethod rm = m.getAnnotation(RestMethod.class);
184            String mn = m.getName(), cn = m.getClass().getName();
185            
186            ObjectMap mom = getOperation(om, sm.getPathPattern(), sm.getHttpMethod().toLowerCase());
187            
188            if (rm.swagger().length > 0) {
189               try {
190                  String json = vr.resolve(StringUtils.join(rm.swagger(), '\n').trim());
191                  if (! (json.startsWith("{") && json.endsWith("}")))
192                     json = "{\n" + json + "\n}";
193                  mom.putAll(new ObjectMap(json));
194               } catch (ParseException e) {
195                  throw new ParseException("Malformed swagger JSON encountered in @RestMethod(swagger) on method "+mn+" on class "+cn+".").initCause(e);
196               }
197            }
198
199            mom.put("operationId", mn);
200            
201            String mDescription = rm.description();
202            if (mDescription.isEmpty())
203               mDescription = mb.findFirstString(locale, mn + ".description");
204            if (mDescription != null)
205               mom.put("description", vr.resolve(mDescription));
206            
207            String mTags = mb.findFirstString(locale, mn + ".tags");
208            if (mTags != null) {
209               mTags = vr.resolve(mTags);
210               if (StringUtils.isObjectList(mTags)) 
211                  mom.put("tags", jp.parse(mTags, ArrayList.class, String.class));
212               else
213                  mom.put("tags", Arrays.asList(StringUtils.split(mTags)));
214            }
215            
216            String mSummary = mb.findFirstString(locale, mn + ".summary");
217            if (mSummary != null)
218               mom.put("summary", vr.resolve(mSummary));
219
220            String mExternalDocs = mb.findFirstString(locale, mn + ".externalDocs");
221            if (mExternalDocs != null) 
222               mom.put("externalDocs", jp.parse(vr.resolve(s), ObjectMap.class));
223            
224            Map<String,ObjectMap> paramMap = new LinkedHashMap<>();
225
226            ObjectList parameters = mom.getObjectList("parameters");
227            if (parameters != null) {
228               for (ObjectMap param : parameters.elements(ObjectMap.class)) {
229                  String key = param.getString("in") + '.' + param.getString("name");
230                  paramMap.put(key, param);
231               }
232            }
233         
234            String mParameters = mb.findFirstString(locale, mn + ".parameters");
235            if (mParameters != null) {
236               ObjectList ol = jp.parse(vr.resolve(mParameters), ObjectList.class);
237               for (ObjectMap param : ol.elements(ObjectMap.class)) {
238                  String key = param.getString("in") + '.' + param.getString("name");
239                  if (paramMap.containsKey(key))
240                     paramMap.get(key).putAll(param);
241                  else
242                     paramMap.put(key, param);
243               }
244            }
245            
246            // Finally, look for parameters defined on method.
247            for (RestParam mp : context.getRestParams(m)) {
248               RestParamType in = mp.getParamType();
249               if (in != RestParamType.OTHER) {
250                  String key = in.toString() + '.' + (in == RestParamType.BODY ? null : mp.getName());
251                  ObjectMap param = new ObjectMap().append("in", in);
252                  if (in != RestParamType.BODY)
253                     param.append("name", mp.name);
254                  if (paramMap.containsKey(key)) {
255                     paramMap.get(key).putAll(param);
256                  } else {
257                     paramMap.put(key, param);
258                  }
259               }
260            }
261            
262            if (! paramMap.isEmpty())
263               mom.put("parameters", paramMap.values());
264            
265            String mResponses = mb.findFirstString(locale, mn + ".responses");
266            if (mResponses != null) 
267               mom.put("responses", jp.parse(vr.resolve(mResponses), ObjectMap.class));
268
269            if (! mom.containsKey("consumes")) {
270               List<MediaType> mConsumes = req.getParsers().getSupportedMediaTypes();
271               if (! mConsumes.equals(om.get("consumes")))
272                  mom.put("consumes", mConsumes);
273            }
274   
275            if (! mom.containsKey("produces")) {
276               List<MediaType> mProduces = req.getSerializers().getSupportedMediaTypes();
277               if (! mProduces.equals(om.get("produces")))
278                  mom.put("produces", mProduces);
279            }
280         }
281      }
282      
283      s = jp.parse(vr.resolve(om.toString()), Swagger.class);
284      swaggers.put(locale, s);
285      
286      return s;
287   }
288   
289   private ObjectMap getInfo(ObjectMap om) {
290      if (! om.containsKey("info"))
291         om.put("info", new ObjectMap());
292      return om.getObjectMap("info");
293   }
294
295   private ObjectMap getOperation(ObjectMap om, String path, String httpMethod) {
296      if (! om.containsKey("paths"))
297         om.put("paths", new ObjectMap());
298      om = om.getObjectMap("paths");
299      if (! om.containsKey(path))
300         om.put(path, new ObjectMap());
301      om = om.getObjectMap(path);
302      if (! om.containsKey(httpMethod))
303         om.put(httpMethod, new ObjectMap());
304      return om.getObjectMap(httpMethod);
305   }
306
307   /**
308    * Returns the localized summary of the specified java method on this servlet.
309    * 
310    * <p>
311    * Subclasses can override this method to provide their own summary.
312    * 
313    * <p>
314    * The default implementation returns the value from the following locations (whichever matches first):
315    * <ol class='spaced-list'>
316    *    <li>{@link RestMethod#summary() @RestMethod.summary()} annotation.
317    *       <h5 class='figure'>Examples:</h5>
318    *       <p class='bcode'>
319    *    <cc>// Direct value</cc>
320    *    <ja>@RestMethod</ja>(summary=<js>"Summary of my method"</js>)
321    *    <jk>public</jk> Object myMethod() {...}
322    *    
323    *    <cc>// Pulled from some other location</cc>
324    *    <ja>@RestMethod</ja>(summary=<js>"$L{myLocalizedSummary}"</js>)
325    *    <jk>public</jk> Object myMethod() {...}
326    *       </p>
327    *    <li>Localized string from resource bundle identified by {@link RestResource#messages() @RestResource.messages()}
328    *       on the resource class, then any parent classes.
329    *       <ol>
330    *          <li><ck>[ClassName].[javaMethodName].summary</ck>
331    *          <li><ck>[javaMethodName].summary</ck>
332    *       </ol>
333    *       <br>Value can contain any SVL variables defined on the {@link RestMethod#summary() @RestMethod.summary()} annotation.
334    *       <h5 class='figure'>Examples:</h5>
335    *       <p class='bcode'>
336    *    <cc>// Direct value</cc>
337    *    <ck>MyClass.myMethod.summary</ck> = <cv>Summary of my method.</cv>
338    *    
339    *    <cc>// Pulled from some other location</cc>
340    *    <ck>MyClass.myMethod.summary</ck> = <cv>$C{MyStrings/MyClass.myMethod.summary}</cv>
341    *       </p>
342    * </ol>
343    * 
344    * @param method The Java method annotated with {@link RestMethod @RestMethod}.
345    * @param req The current request.
346    * @return The localized summary of the method, or <jk>null</jk> if none was found.
347    * @throws Exception 
348    */
349   @Override /* RestInfoProvider */
350   public String getMethodSummary(Method method, RestRequest req) throws Exception {
351      VarResolverSession vr = req.getVarResolverSession();
352
353      String s = method.getAnnotation(RestMethod.class).summary();
354      if (s.isEmpty()) {
355         Operation o = getSwaggerOperation(method, req);
356         if (o != null)
357            s = o.getSummary();
358      }
359      
360      return isEmpty(s) ? null : vr.resolve(s);
361   }
362
363   /**
364    * Returns the localized description of the specified java method on this servlet.
365    * 
366    * <p>
367    * Subclasses can override this method to provide their own description.
368    * 
369    * <p>
370    * The default implementation returns the value from the following locations (whichever matches first):
371    * <ol class='spaced-list'>
372    *    <li>{@link RestMethod#description() @RestMethod.description()} annotation.
373    *       <h5 class='figure'>Examples:</h5>
374    *       <p class='bcode'>
375    *    <cc>// Direct value</cc>
376    *    <ja>@RestMethod</ja>(description=<js>"Description of my method"</js>)
377    *    <jk>public</jk> Object myMethod() {...}
378    *    
379    *    <cc>// Pulled from some other location</cc>
380    *    <ja>@RestMethod</ja>(description=<js>"$L{myLocalizedDescription}"</js>)
381    *    <jk>public</jk> Object myMethod() {...}
382    *       </p>
383    *    <li>Localized string from resource bundle identified by {@link RestResource#messages() @RestResource.messages()}
384    *       on the resource class, then any parent classes.
385    *       <ol>
386    *          <li><ck>[ClassName].[javaMethodName].description</ck>
387    *          <li><ck>[javaMethodName].description</ck>
388    *       </ol>
389    *       <br>Value can contain any SVL variables defined on the {@link RestMethod#description() @RestMethod.description()} annotation.
390    *       <h5 class='figure'>Examples:</h5>
391    *       <p class='bcode'>
392    *    <cc>// Direct value</cc>
393    *    <ck>MyClass.myMethod.description</ck> = <cv>Description of my method.</cv>
394    *    
395    *    <cc>// Pulled from some other location</cc>
396    *    <ck>MyClass.myMethod.description</ck> = <cv>$C{MyStrings/MyClass.myMethod.description}</cv>
397    *       </p>
398    * </ol>
399    * 
400    * @param method The Java method annotated with {@link RestMethod @RestMethod}.
401    * @param req The current request.
402    * @return The localized description of the method, or <jk>null</jk> if none was found.
403    * @throws Exception 
404    */
405   @Override /* RestInfoProvider */
406   public String getMethodDescription(Method method, RestRequest req) throws Exception {
407      VarResolverSession vr = req.getVarResolverSession();
408      
409      String s = method.getAnnotation(RestMethod.class).description();
410      if (s.isEmpty()) {
411         Operation o = getSwaggerOperation(method, req);
412         if (o != null)
413            s = o.getDescription();
414      }
415      
416      return isEmpty(s) ? null : vr.resolve(s);
417   }
418
419   /**
420    * Returns the localized site name of this REST resource.
421    * 
422    * <p>
423    * Subclasses can override this method to provide their own site name.
424    * 
425    * <p>
426    * The default implementation returns the value from the following locations (whichever matches first):
427    * <ol class='spaced-list'>
428    *    <li>{@link RestResource#siteName() @RestResource.siteName()} annotation on this class, and then any parent classes.
429    *       <h5 class='figure'>Examples:</h5>
430    *       <p class='bcode'>
431    *    <jc>// Direct value</jc>
432    *    <ja>@RestResource</ja>(siteName=<js>"My Site"</js>)
433    *    <jk>public class</jk> MyResource {...}
434    *    
435    *    <jc>// Pulled from some other location</jc>
436    *    <ja>@RestResource</ja>(siteName=<js>"$L{myLocalizedSiteName}"</js>)
437    *    <jk>public class</jk> MyResource {...}
438    *       </p>
439    *    <li>Localized strings from resource bundle identified by {@link RestResource#messages() @RestResource.messages()}
440    *       on the resource class, then any parent classes.
441    *       <ol>
442    *          <li><ck>[ClassName].siteName</ck>
443    *          <li><ck>siteName</ck>
444    *       </ol>
445    *       <br>Value can contain any SVL variables defined on the {@link RestResource#siteName() @RestResource.siteName()} annotation.
446    *       <h5 class='figure'>Examples:</h5>
447    *       <p class='bcode'>
448    *    <cc>// Direct value</cc>
449    *    <ck>MyClass.siteName</ck> = <cv>My Site</cv>
450    *    
451    *    <cc>// Pulled from some other location</cc>
452    *    <ck>MyClass.siteName</ck> = <cv>$C{MyStrings/MyClass.siteName}</cv>
453    *       </p>
454    * </ol>
455    * 
456    * @param req The current request.
457    * @return The localized site name of this REST resource, or <jk>null</jk> if none was found.
458    * @throws Exception 
459    */
460   @Override /* RestInfoProvider */
461   public String getSiteName(RestRequest req) throws Exception {
462      VarResolverSession vr = req.getVarResolverSession();
463      if (siteName != null)
464         return vr.resolve(siteName);
465      String siteName = context.getMessages().findFirstString(req.getLocale(), "siteName");
466      if (siteName != null)
467         return vr.resolve(siteName);
468      return null;
469   }
470
471   /**
472    * Returns the localized title of this REST resource.
473    * 
474    * <p>
475    * Subclasses can override this method to provide their own title.
476    * 
477    * <p>
478    * The default implementation returns the value from the following locations (whichever matches first):
479    * <ol class='spaced-list'>
480    *    <li>{@link RestResource#title() @RestResource.siteName()} annotation on this class, and then any parent classes.
481    *       <h5 class='figure'>Examples:</h5>
482    *       <p class='bcode'>
483    *    <jc>// Direct value</jc>
484    *    <ja>@RestResource</ja>(title=<js>"My Resource"</js>)
485    *    <jk>public class</jk> MyResource {...}
486    *    
487    *    <jc>// Pulled from some other location</jc>
488    *    <ja>@RestResource</ja>(title=<js>"$L{myLocalizedTitle}"</js>)
489    *    <jk>public class</jk> MyResource {...}
490    *       </p>
491    *    <li>Localized strings from resource bundle identified by {@link RestResource#messages() @RestResource.messages()}
492    *       on the resource class, then any parent classes.
493    *       <ol>
494    *          <li><ck>[ClassName].title</ck>
495    *          <li><ck>title</ck>
496    *       </ol>
497    *       <br>Value can contain any SVL variables defined on the {@link RestResource#title() @RestResource.title()} annotation.
498    *       <h5 class='figure'>Examples:</h5>
499    *       <p class='bcode'>
500    *    <cc>// Direct value</cc>
501    *    <ck>MyClass.title</ck> = <cv>My Resource</cv>
502    *    
503    *    <cc>// Pulled from some other location</cc>
504    *    <ck>MyClass.title</ck> = <cv>$C{MyStrings/MyClass.title}</cv>
505    *       </p>
506    *    <li><ck>/info/title</ck> entry in swagger file.
507    * </ol>
508    * 
509    * @param req The current request.
510    * @return The localized title of this REST resource, or <jk>null</jk> if none was found.
511    * @throws Exception 
512    */
513   @Override /* RestInfoProvider */
514   public String getTitle(RestRequest req) throws Exception {
515      VarResolverSession vr = req.getVarResolverSession();
516      if (title != null)
517         return vr.resolve(title);
518      String title = context.getMessages().findFirstString(req.getLocale(), "title");
519      if (title != null)
520         return vr.resolve(title);
521      Swagger s = getSwagger(req);
522      if (s != null && s.getInfo() != null)
523         return s.getInfo().getTitle();
524      return null;
525   }
526
527   /**
528    * Returns the localized description of this REST resource.
529    * 
530    * <p>
531    * Subclasses can override this method to provide their own description.
532    * 
533    * <p>
534    * The default implementation returns the value from the following locations (whichever matches first):
535    * <ol class='spaced-list'>
536    *    <li>{@link RestResource#description() @RestResource.description()} annotation on this class, and then any parent classes.
537    *       <h5 class='figure'>Examples:</h5>
538    *       <p class='bcode'>
539    *    <jc>// Direct value</jc>
540    *    <ja>@RestResource</ja>(description=<js>"My Resource"</js>)
541    *    <jk>public class</jk> MyResource {...}
542    *    
543    *    <jc>// Pulled from some other location</jc>
544    *    <ja>@RestResource</ja>(description=<js>"$L{myLocalizedDescription}"</js>)
545    *    <jk>public class</jk> MyResource {...}
546    *       </p>
547    *    <li>Localized strings from resource bundle identified by {@link RestResource#messages() @RestResource.messages()}
548    *       on the resource class, then any parent classes.
549    *       <ol>
550    *          <li><ck>[ClassName].description</ck>
551    *          <li><ck>description</ck>
552    *       </ol>
553    *       <br>Value can contain any SVL variables defined on the {@link RestResource#description() @RestResource.description()} annotation.
554    *       <h5 class='figure'>Examples:</h5>
555    *       <p class='bcode'>
556    *    <cc>// Direct value</cc>
557    *    <ck>MyClass.description</ck> = <cv>My Resource</cv>
558    *    
559    *    <cc>// Pulled from some other location</cc>
560    *    <ck>MyClass.description</ck> = <cv>$C{MyStrings/MyClass.description}</cv>
561    *       </p>
562    *    <li><ck>/info/description</ck> entry in swagger file.
563    * </ol>
564    * 
565    * @param req The current request.
566    * @return The localized description of this REST resource, or <jk>null</jk> if none was was found.
567    * @throws Exception 
568    */
569   @Override /* RestInfoProvider */
570   public String getDescription(RestRequest req) throws Exception {
571      VarResolverSession vr = req.getVarResolverSession();
572      if (description != null)
573         return vr.resolve(description);
574      String description = context.getMessages().findFirstString(req.getLocale(), "description");
575      if (description != null)
576         return vr.resolve(description);
577      Swagger s = getSwagger(req);
578      if (s != null && s.getInfo() != null)
579         return s.getInfo().getDescription();
580      return null;
581   }
582
583   private Operation getSwaggerOperation(Method method, RestRequest req) throws Exception {
584
585      Swagger s = getSwagger(req);
586      if (s != null) {
587         Map<String,Map<String,Operation>> sp = s.getPaths();
588         if (sp != null) {
589            Map<String,Operation> spp = sp.get(method.getAnnotation(RestMethod.class).path());
590            if (spp != null)
591               return spp.get(req.getMethod());
592         }
593      }
594      return null;
595   }
596}