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}