001// Licensed to the Apache Software Foundation (ASF) under one 002// or more contributor license agreements. See the NOTICE file 003// distributed with this work for additional information 004// regarding copyright ownership. The ASF licenses this file 005// to you under the Apache License, Version 2.0 (the 006// "License"); you may not use this file except in compliance 007// with the License. You may obtain a copy of the License at 008// 009// http://www.apache.org/licenses/LICENSE-2.0 010// 011// Unless required by applicable law or agreed to in writing, 012// software distributed under the License is distributed on an 013// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 014// KIND, either express or implied. See the License for the 015// specific language governing permissions and limitations 016// under the License. 017package org.apache.tapestry5.internal.services.rest; 018 019import java.lang.reflect.Method; 020import java.lang.reflect.Parameter; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027import java.util.Set; 028import java.util.stream.Collectors; 029 030import javax.servlet.http.HttpServletResponse; 031 032import org.apache.tapestry5.SymbolConstants; 033import org.apache.tapestry5.annotations.ActivationContextParameter; 034import org.apache.tapestry5.annotations.OnEvent; 035import org.apache.tapestry5.annotations.RequestBody; 036import org.apache.tapestry5.annotations.RequestParameter; 037import org.apache.tapestry5.annotations.RestInfo; 038import org.apache.tapestry5.annotations.StaticActivationContextValue; 039import org.apache.tapestry5.commons.Messages; 040import org.apache.tapestry5.commons.util.CommonsUtils; 041import org.apache.tapestry5.http.services.BaseURLSource; 042import org.apache.tapestry5.http.services.Request; 043import org.apache.tapestry5.internal.InternalConstants; 044import org.apache.tapestry5.internal.services.PageSource; 045import org.apache.tapestry5.internal.structure.Page; 046import org.apache.tapestry5.ioc.services.SymbolSource; 047import org.apache.tapestry5.ioc.services.ThreadLocale; 048import org.apache.tapestry5.json.JSONArray; 049import org.apache.tapestry5.json.JSONObject; 050import org.apache.tapestry5.model.ComponentModel; 051import org.apache.tapestry5.runtime.Component; 052import org.apache.tapestry5.services.ComponentClassResolver; 053import org.apache.tapestry5.services.PageRenderLinkSource; 054import org.apache.tapestry5.services.messages.ComponentMessagesSource; 055import org.apache.tapestry5.services.rest.MappedEntityManager; 056import org.apache.tapestry5.services.rest.OpenApiDescriptionGenerator; 057import org.apache.tapestry5.services.rest.OpenApiTypeDescriber; 058import org.slf4j.Logger; 059import org.slf4j.LoggerFactory; 060 061/** 062 * {@linkplain OpenApiDescriptionGenerator} that generates lots, if not most, of the application's 063 * OpenAPI 3.0 documentation. 064 * 065 * @since 5.8.0 066 */ 067public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGenerator 068{ 069 070 final private static Logger LOGGER = LoggerFactory.getLogger(DefaultOpenApiDescriptionGenerator.class); 071 072 final private OpenApiTypeDescriber typeDescriber; 073 074 final private BaseURLSource baseUrlSource; 075 076 final private SymbolSource symbolSource; 077 078 final private ComponentMessagesSource componentMessagesSource; 079 080 final private ThreadLocale threadLocale; 081 082 final private PageSource pageSource; 083 084 final private ThreadLocal<Messages> messages; 085 086 final private ComponentClassResolver componentClassResolver; 087 088 final private PageRenderLinkSource pageRenderLinkSource; 089 090 final private Request request; 091 092 final Set<Class<?>> entities; 093 094 final private static String KEY_PREFIX = "openapi."; 095 096 final private String basePath; 097 098 private final Map<String, Class<?>> stringToClassMap = new HashMap<>(); 099 100 public DefaultOpenApiDescriptionGenerator( 101 final OpenApiTypeDescriber typeDescriber, 102 final MappedEntityManager mappedEntityManager, 103 final BaseURLSource baseUrlSource, 104 final SymbolSource symbolSource, 105 final ComponentMessagesSource componentMessagesSource, 106 final ThreadLocale threadLocale, 107 final PageSource pageSource, 108 final ComponentClassResolver componentClassResolver, 109 final PageRenderLinkSource pageRenderLinkSource, 110 final Request request) 111 { 112 super(); 113 114 this.typeDescriber = typeDescriber; 115 this.baseUrlSource = baseUrlSource; 116 this.symbolSource = symbolSource; 117 this.componentMessagesSource = componentMessagesSource; 118 this.threadLocale = threadLocale; 119 this.pageSource = pageSource; 120 this.componentClassResolver = componentClassResolver; 121 this.pageRenderLinkSource = pageRenderLinkSource; 122 this.request = request; 123 entities = mappedEntityManager.getEntities(); 124 125 messages = new ThreadLocal<>(); 126 basePath = symbolSource.valueForSymbol(SymbolConstants.OPENAPI_BASE_PATH); 127 128 if (!basePath.startsWith("/") || !basePath.endsWith("/")) 129 { 130 throw new RuntimeException(String.format( 131 "The value of the %s (%s) configuration symbol is '%s' is invalid. " 132 + "It should start with a slash and not end with one", 133 SymbolConstants.OPENAPI_BASE_PATH, 134 "SymbolConstants.OPENAPI_BASE_PATH", basePath)); 135 } 136 137 stringToClassMap.put("boolean", boolean.class); 138 stringToClassMap.put("byte", byte.class); 139 stringToClassMap.put("short", short.class); 140 stringToClassMap.put("int", int.class); 141 stringToClassMap.put("long", long.class); 142 stringToClassMap.put("float", float.class); 143 stringToClassMap.put("double", double.class); 144 stringToClassMap.put("char", char.class); 145 146 for (Class<?> entity : entities) { 147 stringToClassMap.put(entity.getName(), entity); 148 } 149 150 } 151 152 @Override 153 public JSONObject generate(JSONObject documentation) 154 { 155 156 // Making sure all pages have been loaded and transformed 157 for (String pageName : componentClassResolver.getPageNames()) 158 { 159 try 160 { 161 pageSource.getPage(pageName); 162 } 163 catch (Exception e) 164 { 165 // Ignoring exception, since some classes may not 166 // be instantiable. 167 LOGGER.warn(String.format( 168 "Exception while intantiating page %s for OpenAPI description generation,", 169 pageName), e); 170 e.printStackTrace(); 171 } 172 } 173 174 messages.set(componentMessagesSource.getApplicationCatalog(threadLocale.getLocale())); 175 176 if (documentation == null) 177 { 178 documentation = new JSONObject(); 179 } 180 181 documentation.put("openapi", symbolSource.valueForSymbol(SymbolConstants.OPENAPI_VERSION)); 182 183 generateInfo(documentation); 184 185 JSONArray servers = new JSONArray(); 186 servers.add(new JSONObject("url", baseUrlSource.getBaseURL(request.isSecure()) + 187 basePath.substring(0, basePath.length() - 1))); // removing the last slash 188 189 documentation.put("servers", servers); 190 191 try 192 { 193 addPaths(documentation); 194 } 195 catch (Exception e) 196 { 197 throw new RuntimeException(e); 198 } 199 200 generateSchemas(documentation); 201 202 return documentation; 203 204 } 205 206 private void generateInfo(JSONObject documentation) { 207 JSONObject info = new JSONObject(); 208 putIfNotEmpty(info, "title", SymbolConstants.OPENAPI_TITLE); 209 putIfNotEmpty(info, "description", SymbolConstants.OPENAPI_DESCRIPTION); 210 info.put("version", getValueFromSymbolNoPrefix(SymbolConstants.OPENAPI_APPLICATION_VERSION).orElse("?")); 211 documentation.put("info", info); 212 } 213 214 private void addPaths(JSONObject documentation) throws NoSuchMethodException, SecurityException 215 { 216 217 List<Page> pagesWithRestEndpoints = pageSource.getAllPages().stream() 218 .filter(DefaultOpenApiDescriptionGenerator::hasRestEndpoint) 219 .collect(Collectors.toList()); 220 221 JSONObject paths = new JSONObject(); 222 JSONArray tags = new JSONArray(); 223 224 for (Page page : pagesWithRestEndpoints) 225 { 226 processPageClass(page, paths, tags); 227 } 228 229 documentation.put("tags", tags); 230 documentation.put("paths", paths); 231 232 } 233 234 private void processPageClass(Page page, JSONObject paths, JSONArray tags) throws NoSuchMethodException { 235 final Class<?> pageClass = page.getRootComponent().getClass(); 236 237 final String tagName = addPageTag(tags, pageClass); 238 239 ComponentModel model = page.getRootComponent().getComponentResources().getComponentModel(); 240 241 JSONArray methodsAsJson = getMethodsAsJson(model); 242 243 List<Method> methods = toMethods(methodsAsJson, pageClass); 244 245 for (Method method : methods) 246 { 247 processMethod(method, pageClass, paths, tagName); 248 } 249 } 250 251 private String addPageTag(JSONArray tags, final Class<?> pageClass) 252 { 253 final String tagName = getValue(pageClass, "tag.name").orElse(pageClass.getSimpleName()); 254 JSONObject tag = new JSONObject(); 255 tag.put("name", tagName); 256 putIfNotEmpty(tag, "description", getValue(pageClass, "tag.description")); 257 tags.add(tag); 258 return tagName; 259 } 260 261 private JSONArray getMethodsAsJson(ComponentModel model) 262 { 263 JSONArray methodsAsJson = new JSONArray(); 264 while (model != null) 265 { 266 final String meta = model.getMeta( 267 InternalConstants.REST_ENDPOINT_EVENT_HANDLER_METHODS); 268 if (meta != null) 269 { 270 JSONArray thisMethodArray = new JSONArray(meta); 271 addElementsIfNotPresent(methodsAsJson, thisMethodArray); 272 } 273 model = model.getParentModel(); 274 } 275 return methodsAsJson; 276 } 277 278 private void processMethod(Method method, final Class<?> pageClass, JSONObject paths, final String tagName) 279 { 280 final String uri = getPath(method, pageClass); 281 final JSONObject path; 282 if (paths.containsKey(uri)) 283 { 284 path = paths.getJSONObject(uri); 285 } 286 else 287 { 288 path = new JSONObject(); 289 paths.put(uri, path); 290 } 291 292 final String httpMethod = getHttpMethod(method); 293 294 if (path.containsKey(httpMethod)) 295 { 296 throw new RuntimeException(String.format( 297 "There are at least two different REST endpoints for path %s and HTTP method %s in class %s", 298 uri, httpMethod.toUpperCase(), pageClass.getName())); 299 } 300 else 301 { 302 303 final JSONObject methodDescription = new JSONObject(); 304 305 putIfNotEmpty(methodDescription, "summary", getValue(method, uri, httpMethod, "summary")); 306 putIfNotEmpty(methodDescription, "description", getValue(method, uri, httpMethod, "description")); 307 308 JSONArray methodTags = new JSONArray(); 309 methodTags.add(tagName); 310 methodDescription.put("tags", methodTags); 311 312 processResponses(method, uri, httpMethod, methodDescription); 313 314 processParameters(method, uri, httpMethod, methodDescription); 315 316 path.put(httpMethod, methodDescription); 317 } 318 } 319 320 private void processParameters(Method method, final String uri, final String httpMethod, final JSONObject methodDescription) { 321 JSONArray parametersAsJsonArray = new JSONArray(); 322 for (Parameter parameter : method.getParameters()) 323 { 324 final JSONObject parameterDescription = new JSONObject(); 325 if (!isIgnored(parameter) && 326 !parameter.isAnnotationPresent(StaticActivationContextValue.class)) 327 { 328 parameterDescription.put("in", "path"); 329 } 330 else if (parameter.isAnnotationPresent(RequestParameter.class)) 331 { 332 parameterDescription.put("in", "query"); 333 } 334 else if (parameter.isAnnotationPresent(RequestBody.class)) 335 { 336 processRequestBody(method, uri, httpMethod, methodDescription, parametersAsJsonArray, parameter); 337 } 338 if (!parameterDescription.isEmpty()) 339 { 340// Optional<String> parameterName = getValue(method, uri, httpMethod, parameter, "name"); 341// parameterDescription.put("name", parameterName.orElse(parameter.getName())); 342 parameterDescription.put("name", getParameterName(parameter)); 343 getValue(method, uri, httpMethod, parameter, "description") 344 .ifPresent((v) -> parameterDescription.put("description", v)); 345 typeDescriber.describe(parameterDescription, parameter); 346 347 parametersAsJsonArray.add(parameterDescription); 348 } 349 } 350 351 if (!parametersAsJsonArray.isEmpty()) 352 { 353 methodDescription.put("parameters", parametersAsJsonArray); 354 } 355 } 356 357 private void processRequestBody(Method method, 358 final String uri, 359 final String httpMethod, 360 final JSONObject methodDescription, 361 JSONArray parametersAsJsonArray, 362 Parameter parameter) { 363 JSONObject requestBodyDescription = new JSONObject(); 364 requestBodyDescription.put("required", 365 !(parameter.getAnnotation(RequestBody.class).allowEmpty())); 366 getValue(method, uri, httpMethod, "requestbody.description") 367 .ifPresent((v) -> requestBodyDescription.put("description", v)); 368 369 RestInfo restInfo = method.getAnnotation(RestInfo.class); 370 if (restInfo != null) 371 { 372 JSONObject contentDescription = new JSONObject(); 373 for (String contentType : restInfo.consumes()) 374 { 375 JSONObject schemaDescription = new JSONObject(); 376 typeDescriber.describe(schemaDescription, parameter); 377 schemaDescription.remove("required"); 378 contentDescription.put(contentType, schemaDescription); 379 } 380 requestBodyDescription.put("content", contentDescription); 381 } 382 methodDescription.put("requestBody", requestBodyDescription); 383 } 384 385 private String getParameterName(Parameter parameter) { 386 String name = null; 387 final RequestParameter requestParameter = parameter.getAnnotation(RequestParameter.class); 388 if (requestParameter != null && !CommonsUtils.isBlank(requestParameter.value())) 389 { 390 name = requestParameter.value(); 391 } 392 ActivationContextParameter activationContextParameter = parameter.getAnnotation(ActivationContextParameter.class); 393 if (activationContextParameter != null && !CommonsUtils.isBlank(activationContextParameter.value())) 394 { 395 name = activationContextParameter.value(); 396 } 397 if (CommonsUtils.isBlank(name)) 398 { 399 name = parameter.getName(); 400 } 401 return name; 402 } 403 404 private void processResponses(Method method, final String uri, final String httpMethod, final JSONObject methodDescription) { 405 JSONObject responses = new JSONObject(); 406 JSONObject defaultResponse = new JSONObject(); 407 int statusCode = httpMethod.equals("post") || httpMethod.equals("put") ? 408 HttpServletResponse.SC_CREATED : HttpServletResponse.SC_OK; 409 putIfNotEmpty(defaultResponse, "description", getValue(method, uri, httpMethod, statusCode)); 410 responses.put(String.valueOf(statusCode), defaultResponse); 411 412 String[] produces = getProducedMediaTypes(method); 413 if (produces != null && produces.length > 0) 414 { 415 JSONObject contentDescription = new JSONObject(); 416 for (String mediaType : produces) 417 { 418 JSONObject responseTypeDescription = new JSONObject(); 419 typeDescriber.describeReturnType(responseTypeDescription, method); 420 contentDescription.put(mediaType, responseTypeDescription); 421 } 422 defaultResponse.put("content", contentDescription); 423 } 424 425 methodDescription.put("responses", responses); 426 } 427 428 private String[] getProducedMediaTypes(Method method) { 429 430 String[] produces = CommonsUtils.EMPTY_STRING_ARRAY; 431 RestInfo restInfo = method.getAnnotation(RestInfo.class); 432 if (isNonEmptyConsumes(restInfo)) 433 { 434 produces = restInfo.produces(); 435 } 436 else 437 { 438 restInfo = method.getDeclaringClass().getAnnotation(RestInfo.class); 439 if (isNonEmptyProduces(restInfo)) 440 { 441 produces = restInfo.produces(); 442 } 443 } 444 445 return produces; 446 } 447 448 private void addElementsIfNotPresent(JSONArray accumulator, JSONArray array) 449 { 450 if (array != null) 451 { 452 for (int i = 0; i < array.size(); i++) 453 { 454 JSONObject method = array.getJSONObject(i); 455 boolean present = isPresent(accumulator, method); 456 if (!present) 457 { 458 accumulator.add(method); 459 } 460 } 461 } 462 } 463 464 private boolean isNonEmptyConsumes(RestInfo restInfo) 465 { 466 return restInfo != null && !(restInfo.produces().length == 1 && "".equals(restInfo.produces()[0])); 467 } 468 469 private boolean isNonEmptyProduces(RestInfo restInfo) 470 { 471 return restInfo != null && !(restInfo.produces().length == 1 && "".equals(restInfo.produces()[0])); 472 } 473 474 private boolean isPresent(JSONArray array, JSONObject object) 475 { 476 boolean present = false; 477 for (int i = 0; i < array.size(); i++) 478 { 479 if (object.equals(array.getJSONObject(i))) 480 { 481 present = false; 482 } 483 } 484 return present; 485 } 486 487 private Optional<String> getValue(Class<?> clazz, String property) 488 { 489 Optional<String> value = getValue( 490 KEY_PREFIX + clazz.getName() + "." + property); 491 if (!value.isPresent()) 492 { 493 value = getValue( 494 KEY_PREFIX + clazz.getSimpleName() + "." + property); 495 } 496 return value; 497 } 498 499 private Optional<String> getValue(Method method, String path, String httpMethod, String property) 500 { 501 return getValue(method, path + "." + httpMethod + "." + property, false); 502 } 503 504 public Optional<String> getValue(Method method, String path, String httpMethod, Parameter parameter, String property) 505 { 506 return getValue(method, path, httpMethod, "parameter." + getParameterName(parameter), property); 507 } 508 509 public Optional<String> getValue(Method method, String path, String httpMethod, int statusCode) 510 { 511 return getValue(method, path, httpMethod, "response", String.valueOf(statusCode)); 512 } 513 514 public Optional<String> getValue(Method method, String path, String httpMethod, String middle, String propertyName) 515 { 516 Optional<String> value = getValue(method, path + "." + httpMethod + "." + middle + "." + String.valueOf(propertyName), true); 517 if (!value.isPresent()) 518 { 519 value = getValue(method, httpMethod + "." + middle + "." + propertyName, false); 520 } 521 if (!value.isPresent()) 522 { 523 value = getValue(method, middle + "." + propertyName, false); 524 } 525 if (!value.isPresent()) 526 { 527 value = getValue(middle + "." + propertyName); 528 } 529 return value; 530 } 531 532 public Optional<String> getValue(Method method, final String suffix, final boolean skipClassNameLookup) 533 { 534 Optional<String> value = Optional.empty(); 535 536 if (!skipClassNameLookup) 537 { 538 value = getValue( 539 KEY_PREFIX + method.getDeclaringClass().getName() + "." + suffix); 540 if (!value.isPresent()) 541 { 542 value = getValue( 543 KEY_PREFIX + method.getDeclaringClass().getSimpleName() + "." + suffix); 544 } 545 } 546 if (!value.isPresent()) 547 { 548 value = getValue(KEY_PREFIX + suffix); 549 } 550 return value; 551 } 552 553 private List<Method> toMethods(JSONArray methodsAsJson, Class<?> pageClass) throws NoSuchMethodException, SecurityException 554 { 555 List<Method> methods = new ArrayList<>(methodsAsJson.size()); 556 for (Object object : methodsAsJson) 557 { 558 JSONObject methodAsJason = (JSONObject) object; 559 final String name = methodAsJason.getString("name"); 560 final JSONArray parametersAsJson = methodAsJason.getJSONArray("parameters"); 561 @SuppressWarnings("rawtypes") 562 List<Class> parameterTypes = parametersAsJson.stream() 563 .map(o -> ((String) o)) 564 .map(s -> toClass(s)) 565 .collect(Collectors.toList()); 566 methods.add(findMethod(pageClass, name, parameterTypes)); 567 } 568 return methods; 569 } 570 571 @SuppressWarnings("rawtypes") 572 public Method findMethod(Class<?> pageClass, final String name, List<Class> parameterTypes) throws NoSuchMethodException 573 { 574 Method method = null; 575 try 576 { 577 method = pageClass.getDeclaredMethod(name, 578 parameterTypes.toArray(new Class[parameterTypes.size()])); 579 } 580 catch (NoSuchMethodException e) 581 { 582 // Let's try the supertypes 583 List<Class> superTypes = new ArrayList<>(); 584 superTypes.add(pageClass.getSuperclass()); 585 superTypes.addAll((Arrays.asList(pageClass.getInterfaces()))); 586 for (Class clazz : superTypes) 587 { 588 if (clazz != null && !clazz.equals(Object.class)) 589 { 590 method = findMethod(clazz, name, parameterTypes); 591 if (method != null) 592 { 593 break; 594 } 595 } 596 } 597 } 598// if (method == null && pageClass.getName().equals("org.apache.tapestry5.integration.app1.pages.rest.RestTypeDescriptionsDemo")) 599// { 600// System.out.println("WTF!"); 601// } 602 // In case of the same class being loaded from different classloaders, 603 // let's try to find the method in a different way. 604// if (method == null) 605// { 606// for (Method m : pageClass.getDeclaredMethods()) 607// { 608// if (name.equals(m.getName()) && parameterTypes.size() == m.getParameterCount()) 609// { 610// boolean matches = true; 611// for (int i = 0; i < parameterTypes.size(); i++) 612// { 613// if (!(parameterTypes.get(i)).getName().equals( 614// m.getParameterTypes()[i].getName())) 615// { 616// matches = false; 617// break; 618// } 619// } 620// if (matches) 621// { 622// method = m; 623// break; 624// } 625// } 626// } 627// } 628 return method; 629 } 630 631 private Class<?> toClass(String string) 632 { 633 Class<?> clasz = stringToClassMap.get(string); 634 if (clasz == null) 635 { 636 try 637 { 638 clasz = Thread.currentThread().getContextClassLoader().loadClass(string); 639 } catch (ClassNotFoundException e) 640 { 641 throw new RuntimeException(e); 642 } 643 stringToClassMap.put(string, clasz); 644 } 645 return clasz; 646 } 647 648 private String getPath(Method method, Class<?> pageClass) 649 { 650 final StringBuilder builder = new StringBuilder(); 651 builder.append(pageRenderLinkSource.createPageRenderLink(pageClass).toString()); 652 for (Parameter parameter : method.getParameters()) 653 { 654 if (!isIgnored(parameter)) 655 { 656 builder.append("/"); 657 final StaticActivationContextValue staticValue = parameter.getAnnotation(StaticActivationContextValue.class); 658 if (staticValue != null) 659 { 660 builder.append(staticValue.value()); 661 } 662 else 663 { 664 builder.append("{"); 665 builder.append(getParameterName(parameter)); 666 builder.append("}"); 667 } 668 } 669 } 670 String path = builder.toString(); 671 if (!path.startsWith(basePath)) 672 { 673 throw new RuntimeException(String.format("Method %s has path %s, which " 674 + "doesn't start with base path %s. It's likely you need to adjust the " 675 + "base path and/or the endpoint paths", 676 method, path, basePath)); 677 } 678 else 679 { 680 path = path.substring(basePath.length() - 1); // keep the slash 681 path = path.replace("//", "/"); // remove possible double slashes 682 } 683 return path; 684 } 685 686 @SuppressWarnings({ "rawtypes", "unchecked" }) 687 private static boolean isIgnored(Parameter parameter) 688 { 689 boolean ignored = false; 690 for (Class clazz : InternalConstants.INJECTED_PARAMETERS) 691 { 692 if (parameter.getAnnotation(clazz) != null) 693 { 694 ignored = true; 695 break; 696 } 697 } 698 return ignored; 699 } 700 701 private void putIfNotEmpty(JSONObject object, String propertyName, Optional<String> value) 702 { 703 value.ifPresent((v) -> object.put(propertyName, v)); 704 } 705 706 private void putIfNotEmpty(JSONObject object, String propertyName, String key) 707 { 708 getValue(key).ifPresent((value) -> object.put(propertyName, value)); 709 } 710 711 private Optional<String> getValue(String key) 712 { 713 Optional<String> value = getValueFromMessages(key); 714 return value.isPresent() ? value : getValueFromSymbol(key); 715 } 716 717 private Optional<String> getValueFromMessages(String key) 718 { 719 logMessageLookup(key); 720 final String value = messages.get().get(key.replace("tapestry.", "")).trim(); 721 return value.startsWith("[") && value.endsWith("]") ? Optional.empty() : Optional.of(value); 722 } 723 724 private void logSymbolLookup(String key) { 725 if (LOGGER.isDebugEnabled()) 726 { 727 LOGGER.debug("Looking up symbol " + key); 728 } 729 } 730 731 private void logMessageLookup(String key) { 732 if (LOGGER.isDebugEnabled()) 733 { 734 LOGGER.debug("Looking up message " + key); 735 } 736 } 737 738 private Optional<String> getValueFromSymbol(String key) 739 { 740 return getValueFromSymbolNoPrefix("tapestry." + key); 741 } 742 743 private Optional<String> getValueFromSymbolNoPrefix(final String symbol) { 744 String value; 745 logSymbolLookup(symbol); 746 try 747 { 748 value = symbolSource.valueForSymbol(symbol); 749 } 750 catch (RuntimeException e) 751 { 752 // value not found; 753 value = null; 754 } 755 return Optional.ofNullable(value); 756 } 757 758 private static final String PREFIX = InternalConstants.HTTP_METHOD_EVENT_PREFIX.toLowerCase(); 759 760 private static String getHttpMethod(Method method) 761 { 762 String httpMethod; 763 OnEvent onEvent = method.getAnnotation(OnEvent.class); 764 if (onEvent != null) 765 { 766 httpMethod = onEvent.value(); 767 } 768 else 769 { 770 httpMethod = method.getName().replace("on", ""); 771 } 772 httpMethod = httpMethod.toLowerCase(); 773 httpMethod = httpMethod.replace(PREFIX, ""); 774 return httpMethod; 775 } 776 777 private static boolean hasRestEndpoint(Page page) 778 { 779 return hasRestEndpoint(page.getRootComponent()); 780 } 781 782 private static boolean hasRestEndpoint(final Component component) 783 { 784 final ComponentModel componentModel = component.getComponentResources().getComponentModel(); 785 return InternalConstants.TRUE.equals(componentModel.getMeta( 786 InternalConstants.REST_ENDPOINT_EVENT_HANDLER_METHOD_PRESENT)); 787 } 788 789 private void generateSchemas(JSONObject documentation) 790 { 791 if (!entities.isEmpty()) 792 { 793 794 JSONObject components = new JSONObject(); 795 JSONObject schemas = new JSONObject(); 796 797 for (Class<?> entity : entities) { 798 typeDescriber.describeSchema(entity, schemas); 799 } 800 801 components.put("schemas", schemas); 802 documentation.put("components", components); 803 804 } 805 806 } 807 808}