    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}