    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.beans.BeanInfo;
    020import java.beans.IntrospectionException;
    021import java.beans.Introspector;
    022import java.beans.PropertyDescriptor;
    023import java.lang.reflect.Method;
    024import java.lang.reflect.Parameter;
    025import java.util.Arrays;
    026import java.util.List;
    027import java.util.Optional;
    028import java.util.Set;
    029import java.util.function.Function;
    030
    031import org.apache.tapestry5.annotations.RequestParameter;
    032import org.apache.tapestry5.annotations.RestInfo;
    033import org.apache.tapestry5.json.JSONArray;
    034import org.apache.tapestry5.json.JSONObject;
    035import org.apache.tapestry5.services.rest.MappedEntityManager;
    036import org.apache.tapestry5.services.rest.OpenApiTypeDescriber;
    037
    038/**
    039 * {@link OpenApiTypeDescriber} implementation that handles some basic types, mostly primitives and String.
    040 * Since this is the fallback, if the parameter doesn't have any handled type, it defaults
    041 * to give the <code>object</code> to it without providing properties.
    042 */
    043public class DefaultOpenApiTypeDescriber implements OpenApiTypeDescriber
    044{
    045    final Set<Class<?>> mappedEntities;
    046    private static final String ARRAY_TYPE = "array";
    047    private static final String OBJECT_TYPE = "object";
    048    private static final String STRING_TYPE = "string";
    049    private static final Function<Class<?>, String> TO_INTEGER = (c) -> "integer";
    050    private static final Function<Class<?>, String> TO_BOOLEAN = (c) -> "boolean";
    051    private static final Function<Class<?>, String> TO_NUMBER = (c) -> "number";
    052    private static final List<Handler> MAPPERS = Arrays.asList(
    053            new Handler(int.class, TO_INTEGER),
    054            new Handler(Integer.class, TO_INTEGER),
    055            new Handler(byte.class, TO_INTEGER),
    056            new Handler(Byte.class, TO_INTEGER),
    057            new Handler(short.class, TO_INTEGER),
    058            new Handler(Short.class, TO_INTEGER),
    059            new Handler(long.class, TO_INTEGER),
    060            new Handler(Long.class, TO_INTEGER),
    061            new Handler(float.class, TO_NUMBER),
    062            new Handler(Float.class, TO_NUMBER),
    063            new Handler(double.class, TO_NUMBER),
    064            new Handler(Double.class, TO_NUMBER),
    065            new Handler(boolean.class, TO_BOOLEAN),
    066            new Handler(Boolean.class, TO_BOOLEAN),
    067            new Handler(String.class, (c) -> STRING_TYPE),
    068            new Handler(char.class, (c) -> STRING_TYPE),
    069            new Handler(Character.class, (c) -> STRING_TYPE),
    070            new Handler(JSONObject.class, (c) -> OBJECT_TYPE),
    071            new Handler(JSONArray.class, (c) -> ARRAY_TYPE)
    072    );
    073
    074    public DefaultOpenApiTypeDescriber(final MappedEntityManager mappedEntityManager)
    075    {
    076        mappedEntities = mappedEntityManager.getEntities();
    077    }
    078
    079    @Override
    080    public void describe(JSONObject description, Parameter parameter)
    081    {
    082        describeType(description, parameter.getType());
    083
    084        // According to the OpenAPI 3 documentation, path parameters are always required.
    085        final RequestParameter requestParameter = parameter.getAnnotation(RequestParameter.class);
    086        if (requestParameter == null || requestParameter != null && !requestParameter.allowBlank())
    087        {
    088            description.put("required", true);
    089        }
    090
    091    }
    092
    093    @Override
    094    public void describeReturnType(JSONObject description, Method method)
    095    {
    096        Class<?> returnedType;
    097        final RestInfo restInfo = method.getAnnotation(RestInfo.class);
    098        if (restInfo != null)
    099        {
    100            returnedType = restInfo.returnType();
    101        }
    102        else
    103        {
    104            returnedType = method.getReturnType();
    105        }
    106        describeType(description, returnedType);
    107    }
    108
    109    private JSONObject describeType(JSONObject description, Class<?> type)
    110    {
    111        // If a schema is already provided, we leave it unchanged.
    112        JSONObject schema = description.getJSONObjectOrDefault("schema", null);
    113        if (schema == null)
    114        {
    115            final Optional<String> schemaType = getOpenApiType(type);
    116            if (schemaType.isPresent())
    117            {
    118                schema = description.put("schema", new JSONObject("type", schemaType.get()));
    119            }
    120            else if (mappedEntities.contains(type))
    121            {
    122                schema = description.put("schema",
    123                        new JSONObject("$ref", getSchemaReference(type)));
    124            }
    125        }
    126        return schema;
    127    }
    128
    129    private Optional<String> getOpenApiType(Class<?> type) {
    130        final Optional<String> schemaType = MAPPERS.stream()
    131                .filter(h -> h.type.equals(type))
    132                .map(h -> h.getMapper().apply(type))
    133                .findFirst();
    134        return schemaType;
    135    }
    136
    137    private static final class Handler
    138    {
    139        final private Class<?> type;
    140
    141        final private Function<Class<?>, String> mapper;
    142
    143        public Handler(Class<?> type, Function<Class<?>, String> mapper)
    144        {
    145            super();
    146            this.type = type;
    147            this.mapper = mapper;
    148        }
    149
    150        public Function<Class<?>, String> getMapper() {
    151            return mapper;
    152        }
    153
    154    }
    155
    156    @Override
    157    public void describeSchema(Class<?> entity, JSONObject schemas)
    158    {
    159
    160        final String name = getSchemaName(entity);
    161
    162        // Don't overwrite already provided schemas
    163        if (!schemas.containsKey(name))
    164        {
    165            JSONObject schema = new JSONObject();
    166            JSONObject properties = new JSONObject();
    167            final BeanInfo beanInfo;
    168
    169            try
    170            {
    171                beanInfo = Introspector.getBeanInfo(entity, Object.class);
    172            } catch (IntrospectionException e) {
    173                throw new RuntimeException(e);
    174            }
    175
    176            final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
    177            for (PropertyDescriptor propertyDescriptor : propertyDescriptors)
    178            {
    179                final String propertyName = propertyDescriptor.getName();
    180                final Class<?> type = propertyDescriptor.getPropertyType();
    181                Optional<String> schemaType = getOpenApiType(type);
    182                if (schemaType.isPresent())
    183                {
    184                    JSONObject propertyDescription = new JSONObject();
    185                    propertyDescription.put("type", schemaType.get());
    186                    properties.put(propertyName, propertyDescription);
    187                }
    188//                else if (mappedEntities.contains(entity))
    189//                {
    190//                    JSONObject propertyDescription = new JSONObject();
    191//                    propertyDescription.put("schema",
    192//                            new JSONObject("$ref", getSchemaReference(type)));
    193//                    properties.put(propertyName, propertyDescription);
    194//                }
    195            }
    196
    197            schema.put("properties", properties);
    198            schemas.put(name, schema);
    199        }
    200    }
    201
    202    private String getSchemaName(final Class<?> entity) {
    203        return entity.getSimpleName();
    204    }
    205
    206    private String getSchemaReference(final Class<?> entity) {
    207        return "#/components/schemas/" + getSchemaName(entity);
    208    }
    209
    210}