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.StringUtils.*;
016import static org.apache.juneau.rest.util.RestUtils.*;
017
018import java.lang.reflect.Method;
019import java.util.*;
020import java.util.concurrent.*;
021
022import org.apache.juneau.dto.swagger.*;
023import org.apache.juneau.internal.*;
024import org.apache.juneau.reflect.*;
025import org.apache.juneau.rest.annotation.*;
026import org.apache.juneau.svl.*;
027
028/**
029 * Default implementation of {@link RestInfoProvider}.
030 *
031 * <p>
032 * Subclasses can override these methods to tailor how HTTP REST resources are documented.
033 *
034 * <ul class='seealso'>
035 *    <li class='jf'>{@link RestContext#REST_infoProvider}
036 *    <li class='link'>{@doc juneau-rest-server.Swagger}
037 * </ul>
038 */
039public class BasicRestInfoProvider implements RestInfoProvider {
040
041   private final RestContext context;
042   private final String
043      siteName,
044      title,
045      description;
046   private final ConcurrentHashMap<Locale,ConcurrentHashMap<Integer,Swagger>> swaggers = new ConcurrentHashMap<>();
047
048   /**
049    * Constructor.
050    *
051    * @param context The resource context.
052    */
053   public BasicRestInfoProvider(RestContext context) {
054      this.context = context;
055
056      Builder b = new Builder(context);
057      this.siteName = b.siteName;
058      this.title = b.title;
059      this.description = b.description;
060   }
061
062   private static final class Builder {
063      String
064         siteName,
065         title,
066         description;
067
068      @SuppressWarnings("deprecation")
069      Builder(RestContext context) {
070         ClassInfo ci = ClassInfo.of(context.getResource().getClass());
071         for (RestResource r : ci.getAnnotationsParentFirst(RestResource.class)) {
072            if (! r.siteName().isEmpty())
073               siteName = r.siteName();
074            if (r.title().length > 0)
075               title = joinnl(r.title());
076            if (r.description().length > 0)
077               description = joinnl(r.description());
078         }
079         for (Rest r : ci.getAnnotationsParentFirst(Rest.class)) {
080            if (! r.siteName().isEmpty())
081               siteName = r.siteName();
082            if (r.title().length > 0)
083               title = joinnl(r.title());
084            if (r.description().length > 0)
085               description = joinnl(r.description());
086         }
087      }
088   }
089
090   /**
091    * Returns the localized swagger for this REST resource.
092    *
093    * <p>
094    * Subclasses can override this method to customize the Swagger.
095    *
096    * @param req The incoming HTTP request.
097    * @return
098    *    A new Swagger instance.
099    *    <br>Never <jk>null</jk>.
100    * @throws Exception Error occurred.
101    */
102   @Override /* RestInfoProvider */
103   public Swagger getSwagger(RestRequest req) throws Exception {
104
105      Locale locale = req.getLocale();
106
107      // Find it in the cache.
108      // Swaggers are cached by user locale and an int hash of the @RestMethods they have access to.
109      HashCode userHash = HashCode.create();
110      for (RestMethodContext sm : context.getCallMethods().values())
111         if (sm.isRequestAllowed(req))
112            userHash.add(sm.hashCode());
113      int hashCode = userHash.get();
114
115      if (! swaggers.containsKey(locale))
116         swaggers.putIfAbsent(locale, new ConcurrentHashMap<Integer,Swagger>());
117
118      Swagger swagger = swaggers.get(locale).get(hashCode);
119      if (swagger != null)
120         return swagger;
121
122      // Wasn't cached...need to create one.
123      swagger = new SwaggerGenerator(req).getSwagger();
124
125      swaggers.get(locale).put(hashCode, swagger);
126
127      return swagger;
128   }
129
130   /**
131    * Returns the localized summary of the specified java method on this servlet.
132    *
133    * <p>
134    * Subclasses can override this method to provide their own summary.
135    *
136    * <p>
137    * The default implementation returns the value from the following locations (whichever matches first):
138    * <ol class='spaced-list'>
139    *    <li>{@link RestMethod#summary() @RestMethod(summary)} annotation.
140    *       <h5 class='figure'>Examples:</h5>
141    *       <p class='bcode w800'>
142    *    <cc>// Direct value</cc>
143    *    <ja>@RestMethod</ja>(summary=<js>"Summary of my method"</js>)
144    *    <jk>public</jk> Object myMethod() {...}
145    *
146    *    <cc>// Pulled from some other location</cc>
147    *    <ja>@RestMethod</ja>(summary=<js>"$L{myLocalizedSummary}"</js>)
148    *    <jk>public</jk> Object myMethod() {...}
149    *       </p>
150    *    <li>Localized string from resource bundle identified by {@link Rest#messages() @Rest(messages)}
151    *       on the resource class, then any parent classes.
152    *       <ol>
153    *          <li><ck>[ClassName].[javaMethodName].summary</ck>
154    *          <li><ck>[javaMethodName].summary</ck>
155    *       </ol>
156    *       <br>Value can contain any SVL variables defined on the {@link RestMethod#summary() @RestMethod(summary)} annotation.
157    *       <h5 class='figure'>Examples:</h5>
158    *       <p class='bcode w800'>
159    *    <cc>// Direct value</cc>
160    *    <ck>MyClass.myMethod.summary</ck> = <cv>Summary of my method.</cv>
161    *
162    *    <cc>// Pulled from some other location</cc>
163    *    <ck>MyClass.myMethod.summary</ck> = <cv>$C{MyStrings/MyClass.myMethod.summary}</cv>
164    *       </p>
165    * </ol>
166    *
167    * @param method The Java method annotated with {@link RestMethod @RestMethod}.
168    * @param req The current request.
169    * @return The localized summary of the method, or <jk>null</jk> if none was found.
170    * @throws Exception Error occurred.
171    */
172   @Override /* RestInfoProvider */
173   public String getMethodSummary(Method method, RestRequest req) throws Exception {
174      VarResolverSession vr = req.getVarResolverSession();
175
176      String s = MethodInfo.of(method).getAnnotation(RestMethod.class).summary();
177      if (s.isEmpty()) {
178         Operation o = getSwaggerOperation(method, req);
179         if (o != null)
180            s = o.getSummary();
181      }
182
183      return isEmpty(s) ? null : vr.resolve(s);
184   }
185
186   /**
187    * Returns the localized description of the specified java method on this servlet.
188    *
189    * <p>
190    * Subclasses can override this method to provide their own description.
191    *
192    * <p>
193    * The default implementation returns the value from the following locations (whichever matches first):
194    * <ol class='spaced-list'>
195    *    <li>{@link RestMethod#description() @RestMethod(description)} annotation.
196    *       <h5 class='figure'>Examples:</h5>
197    *       <p class='bcode w800'>
198    *    <cc>// Direct value</cc>
199    *    <ja>@RestMethod</ja>(description=<js>"Description of my method"</js>)
200    *    <jk>public</jk> Object myMethod() {...}
201    *
202    *    <cc>// Pulled from some other location</cc>
203    *    <ja>@RestMethod</ja>(description=<js>"$L{myLocalizedDescription}"</js>)
204    *    <jk>public</jk> Object myMethod() {...}
205    *       </p>
206    *    <li>Localized string from resource bundle identified by {@link Rest#messages() @Rest(messages)}
207    *       on the resource class, then any parent classes.
208    *       <ol>
209    *          <li><ck>[ClassName].[javaMethodName].description</ck>
210    *          <li><ck>[javaMethodName].description</ck>
211    *       </ol>
212    *       <br>Value can contain any SVL variables defined on the {@link RestMethod#description() @RestMethod(description)} annotation.
213    *       <h5 class='figure'>Examples:</h5>
214    *       <p class='bcode w800'>
215    *    <cc>// Direct value</cc>
216    *    <ck>MyClass.myMethod.description</ck> = <cv>Description of my method.</cv>
217    *
218    *    <cc>// Pulled from some other location</cc>
219    *    <ck>MyClass.myMethod.description</ck> = <cv>$C{MyStrings/MyClass.myMethod.description}</cv>
220    *       </p>
221    * </ol>
222    *
223    * @param method The Java method annotated with {@link RestMethod @RestMethod}.
224    * @param req The current request.
225    * @return The localized description of the method, or <jk>null</jk> if none was found.
226    * @throws Exception Error occurred.
227    */
228   @Override /* RestInfoProvider */
229   public String getMethodDescription(Method method, RestRequest req) throws Exception {
230      VarResolverSession vr = req.getVarResolverSession();
231
232      String s = joinnl(MethodInfo.of(method).getAnnotation(RestMethod.class).description());
233      if (s.isEmpty()) {
234         Operation o = getSwaggerOperation(method, req);
235         if (o != null)
236            s = o.getDescription();
237      }
238
239      return isEmpty(s) ? null : vr.resolve(s);
240   }
241
242   /**
243    * Returns the localized site name of this REST resource.
244    *
245    * <p>
246    * Subclasses can override this method to provide their own site name.
247    *
248    * <p>
249    * The default implementation returns the value from the following locations (whichever matches first):
250    * <ol class='spaced-list'>
251    *    <li>{@link Rest#siteName() @Rest(siteName)} annotation on this class, and then any parent classes.
252    *       <h5 class='figure'>Examples:</h5>
253    *       <p class='bcode w800'>
254    *    <jc>// Direct value</jc>
255    *    <ja>@Rest</ja>(siteName=<js>"My Site"</js>)
256    *    <jk>public class</jk> MyResource {...}
257    *
258    *    <jc>// Pulled from some other location</jc>
259    *    <ja>@Rest</ja>(siteName=<js>"$L{myLocalizedSiteName}"</js>)
260    *    <jk>public class</jk> MyResource {...}
261    *       </p>
262    *    <li>Localized strings from resource bundle identified by {@link Rest#messages() @Rest(messages)}
263    *       on the resource class, then any parent classes.
264    *       <ol>
265    *          <li><ck>[ClassName].siteName</ck>
266    *          <li><ck>siteName</ck>
267    *       </ol>
268    *       <br>Value can contain any SVL variables defined on the {@link Rest#siteName() @Rest(siteName)} annotation.
269    *       <h5 class='figure'>Examples:</h5>
270    *       <p class='bcode w800'>
271    *    <cc>// Direct value</cc>
272    *    <ck>MyClass.siteName</ck> = <cv>My Site</cv>
273    *
274    *    <cc>// Pulled from some other location</cc>
275    *    <ck>MyClass.siteName</ck> = <cv>$C{MyStrings/MyClass.siteName}</cv>
276    *       </p>
277    * </ol>
278    *
279    * @param req The current request.
280    * @return The localized site name of this REST resource, or <jk>null</jk> if none was found.
281    * @throws Exception Error occurred.
282    */
283   @Override /* RestInfoProvider */
284   public String getSiteName(RestRequest req) throws Exception {
285      VarResolverSession vr = req.getVarResolverSession();
286      if (siteName != null)
287         return vr.resolve(siteName);
288      String siteName = context.getMessages().findFirstString(req.getLocale(), "siteName");
289      if (siteName != null)
290         return vr.resolve(siteName);
291      return null;
292   }
293
294   /**
295    * Returns the localized title of this REST resource.
296    *
297    * <p>
298    * Subclasses can override this method to provide their own title.
299    *
300    * <p>
301    * The default implementation returns the value from the following locations (whichever matches first):
302    * <ol class='spaced-list'>
303    *    <li>{@link Rest#title() @Rest(siteName)} annotation on this class, and then any parent classes.
304    *       <h5 class='figure'>Examples:</h5>
305    *       <p class='bcode w800'>
306    *    <jc>// Direct value</jc>
307    *    <ja>@Rest</ja>(title=<js>"My Resource"</js>)
308    *    <jk>public class</jk> MyResource {...}
309    *
310    *    <jc>// Pulled from some other location</jc>
311    *    <ja>@Rest</ja>(title=<js>"$L{myLocalizedTitle}"</js>)
312    *    <jk>public class</jk> MyResource {...}
313    *       </p>
314    *    <li>Localized strings from resource bundle identified by {@link Rest#messages() @Rest(messages)}
315    *       on the resource class, then any parent classes.
316    *       <ol>
317    *          <li><ck>[ClassName].title</ck>
318    *          <li><ck>title</ck>
319    *       </ol>
320    *       <br>Value can contain any SVL variables defined on the {@link Rest#title() @Rest(title)} annotation.
321    *       <h5 class='figure'>Examples:</h5>
322    *       <p class='bcode w800'>
323    *    <cc>// Direct value</cc>
324    *    <ck>MyClass.title</ck> = <cv>My Resource</cv>
325    *
326    *    <cc>// Pulled from some other location</cc>
327    *    <ck>MyClass.title</ck> = <cv>$C{MyStrings/MyClass.title}</cv>
328    *       </p>
329    *    <li><ck>/info/title</ck> entry in swagger file.
330    * </ol>
331    *
332    * @param req The current request.
333    * @return The localized title of this REST resource, or <jk>null</jk> if none was found.
334    * @throws Exception Error occurred.
335    */
336   @Override /* RestInfoProvider */
337   public String getTitle(RestRequest req) throws Exception {
338      VarResolverSession vr = req.getVarResolverSession();
339      if (title != null)
340         return vr.resolve(title);
341      String title = context.getMessages().findFirstString(req.getLocale(), "title");
342      if (title != null)
343         return vr.resolve(title);
344      return null;
345   }
346
347   /**
348    * Returns the localized description of this REST resource.
349    *
350    * <p>
351    * Subclasses can override this method to provide their own description.
352    *
353    * <p>
354    * The default implementation returns the value from the following locations (whichever matches first):
355    * <ol class='spaced-list'>
356    *    <li>{@link Rest#description() @Rest(description)} annotation on this class, and then any parent classes.
357    *       <h5 class='figure'>Examples:</h5>
358    *       <p class='bcode w800'>
359    *    <jc>// Direct value</jc>
360    *    <ja>@Rest</ja>(description=<js>"My Resource"</js>)
361    *    <jk>public class</jk> MyResource {...}
362    *
363    *    <jc>// Pulled from some other location</jc>
364    *    <ja>@Rest</ja>(description=<js>"$L{myLocalizedDescription}"</js>)
365    *    <jk>public class</jk> MyResource {...}
366    *       </p>
367    *    <li>Localized strings from resource bundle identified by {@link Rest#messages() @Rest(messages)}
368    *       on the resource class, then any parent classes.
369    *       <ol>
370    *          <li><ck>[ClassName].description</ck>
371    *          <li><ck>description</ck>
372    *       </ol>
373    *       <br>Value can contain any SVL variables defined on the {@link Rest#description() @Rest(description)} annotation.
374    *       <h5 class='figure'>Examples:</h5>
375    *       <p class='bcode w800'>
376    *    <cc>// Direct value</cc>
377    *    <ck>MyClass.description</ck> = <cv>My Resource</cv>
378    *
379    *    <cc>// Pulled from some other location</cc>
380    *    <ck>MyClass.description</ck> = <cv>$C{MyStrings/MyClass.description}</cv>
381    *       </p>
382    *    <li><ck>/info/description</ck> entry in swagger file.
383    * </ol>
384    *
385    * @param req The current request.
386    * @return The localized description of this REST resource, or <jk>null</jk> if none was was found.
387    * @throws Exception Error occurred.
388    */
389   @Override /* RestInfoProvider */
390   public String getDescription(RestRequest req) throws Exception {
391      VarResolverSession vr = req.getVarResolverSession();
392      if (description != null)
393         return vr.resolve(description);
394      String description = context.getMessages().findFirstString(req.getLocale(), "description");
395      if (description != null)
396         return vr.resolve(description);
397      return null;
398   }
399
400   //-----------------------------------------------------------------------------------------------------------------
401   // Utility methods
402   //-----------------------------------------------------------------------------------------------------------------
403
404   private Operation getSwaggerOperation(Method method, RestRequest req) throws Exception {
405
406      Swagger s = getSwagger(req);
407      if (s != null) {
408         Map<String,OperationMap> sp = s.getPaths();
409         if (sp != null) {
410            Map<String,Operation> spp = sp.get(fixMethodPath(MethodInfo.of(method).getAnnotation(RestMethod.class).path()));
411            if (spp != null)
412               return spp.get(req.getMethod());
413         }
414      }
415      return null;
416   }
417
418   static String joinnl(String[] ss) {
419      if (ss.length == 0)
420         return "";
421      return StringUtils.joinnl(ss).trim();
422   }
423}