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}