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}