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 > juneau-rest-server > 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}