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