diff options
8 files changed, 538 insertions, 98 deletions
diff --git a/backend/src/main/java/org/luxons/sevenwonders/doc/builders/JSONDocApiObjectDocBuilder.java b/backend/src/main/java/org/luxons/sevenwonders/doc/builders/JSONDocApiObjectDocBuilder.java new file mode 100644 index 00000000..8a59451f --- /dev/null +++ b/backend/src/main/java/org/luxons/sevenwonders/doc/builders/JSONDocApiObjectDocBuilder.java @@ -0,0 +1,57 @@ +package org.luxons.sevenwonders.doc.builders; + +import java.lang.reflect.Field; +import java.util.Set; +import java.util.TreeSet; + +import org.jsondoc.core.annotation.ApiObject; +import org.jsondoc.core.annotation.ApiObjectField; +import org.jsondoc.core.pojo.ApiObjectDoc; +import org.jsondoc.core.pojo.ApiObjectFieldDoc; +import org.jsondoc.core.scanner.DefaultJSONDocScanner; +import org.jsondoc.core.scanner.builder.JSONDocApiVersionDocBuilder; + +public class JSONDocApiObjectDocBuilder { + + public static ApiObjectDoc build(Class<?> clazz) { + ApiObject apiObject = clazz.getAnnotation(ApiObject.class); + ApiObjectDoc apiObjectDoc = new ApiObjectDoc(); + + Set<ApiObjectFieldDoc> fieldDocs = new TreeSet<ApiObjectFieldDoc>(); + for (Field field : clazz.getDeclaredFields()) { + if (field.getAnnotation(ApiObjectField.class) != null) { + ApiObjectFieldDoc fieldDoc = + JSONDocApiObjectFieldDocBuilder.build(field.getAnnotation(ApiObjectField.class), field); + fieldDoc.setSupportedversions(JSONDocApiVersionDocBuilder.build(field)); + fieldDocs.add(fieldDoc); + } + } + + Class<?> c = clazz.getSuperclass(); + if (c != null) { + if (c.isAnnotationPresent(ApiObject.class)) { + ApiObjectDoc objDoc = build(c); + fieldDocs.addAll(objDoc.getFields()); + } + } + + if (clazz.isEnum()) { + apiObjectDoc.setAllowedvalues(DefaultJSONDocScanner.enumConstantsToStringArray(clazz.getEnumConstants())); + } + + if (apiObject.name().trim().isEmpty()) { + apiObjectDoc.setName(clazz.getSimpleName()); + } else { + apiObjectDoc.setName(apiObject.name()); + } + + apiObjectDoc.setDescription(apiObject.description()); + apiObjectDoc.setFields(fieldDocs); + apiObjectDoc.setGroup(apiObject.group()); + apiObjectDoc.setVisibility(apiObject.visibility()); + apiObjectDoc.setStage(apiObject.stage()); + apiObjectDoc.setShow(apiObject.show()); + + return apiObjectDoc; + } +} diff --git a/backend/src/main/java/org/luxons/sevenwonders/doc/builders/JSONDocApiObjectFieldDocBuilder.java b/backend/src/main/java/org/luxons/sevenwonders/doc/builders/JSONDocApiObjectFieldDocBuilder.java new file mode 100644 index 00000000..a1c9d7c6 --- /dev/null +++ b/backend/src/main/java/org/luxons/sevenwonders/doc/builders/JSONDocApiObjectFieldDocBuilder.java @@ -0,0 +1,42 @@ +package org.luxons.sevenwonders.doc.builders; + +import java.lang.reflect.Field; + +import org.jsondoc.core.annotation.ApiObjectField; +import org.jsondoc.core.pojo.ApiObjectFieldDoc; +import org.jsondoc.core.scanner.DefaultJSONDocScanner; +import org.jsondoc.core.util.JSONDocHibernateValidatorProcessor; +import org.jsondoc.core.util.JSONDocType; + +public class JSONDocApiObjectFieldDocBuilder { + + public static ApiObjectFieldDoc build(ApiObjectField annotation, Field field) { + ApiObjectFieldDoc apiPojoFieldDoc = new ApiObjectFieldDoc(); + if (!annotation.name().trim().isEmpty()) { + apiPojoFieldDoc.setName(annotation.name()); + } else { + apiPojoFieldDoc.setName(field.getName()); + } + apiPojoFieldDoc.setDescription(annotation.description()); + apiPojoFieldDoc.setJsondocType( + JSONDocTypeBuilder.build(new JSONDocType(), field.getType(), field.getGenericType())); + // if allowedvalues property is populated on an enum field, then the enum values are overridden with the + // allowedvalues ones + if (field.getType().isEnum() && annotation.allowedvalues().length == 0) { + apiPojoFieldDoc.setAllowedvalues( + DefaultJSONDocScanner.enumConstantsToStringArray(field.getType().getEnumConstants())); + } else { + apiPojoFieldDoc.setAllowedvalues(annotation.allowedvalues()); + } + apiPojoFieldDoc.setRequired(String.valueOf(annotation.required())); + apiPojoFieldDoc.setOrder(annotation.order()); + + if (!annotation.format().isEmpty()) { + apiPojoFieldDoc.addFormat(annotation.format()); + } + + JSONDocHibernateValidatorProcessor.processHibernateValidatorAnnotations(field, apiPojoFieldDoc); + + return apiPojoFieldDoc; + } +} diff --git a/backend/src/main/java/org/luxons/sevenwonders/doc/builders/JSONDocTemplateBuilder.java b/backend/src/main/java/org/luxons/sevenwonders/doc/builders/JSONDocTemplateBuilder.java index 78c86584..f518f319 100644 --- a/backend/src/main/java/org/luxons/sevenwonders/doc/builders/JSONDocTemplateBuilder.java +++ b/backend/src/main/java/org/luxons/sevenwonders/doc/builders/JSONDocTemplateBuilder.java @@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory; public class JSONDocTemplateBuilder { private static final Logger log = LoggerFactory.getLogger(org.jsondoc.core.util.JSONDocTemplateBuilder.class); + private static final Map<Class<?>, Class<?>> primitives = new HashMap<Class<?>, Class<?>>(); static { @@ -38,7 +39,7 @@ public class JSONDocTemplateBuilder { public static JSONDocTemplate build(Class<?> clazz, Set<Class<?>> jsondocObjects) { final JSONDocTemplate jsonDocTemplate = new JSONDocTemplate(); - if(jsondocObjects.contains(clazz)) { + if (jsondocObjects.contains(clazz)) { try { Set<JSONDocFieldWrapper> fields = getAllDeclaredFields(clazz); @@ -53,7 +54,8 @@ public class JSONDocTemplateBuilder { Object value; // This condition is to avoid StackOverflow in case class "A" // contains a field of type "A" - if (field.getType().equals(clazz) || (apiObjectField != null && !apiObjectField.processtemplate())) { + if (field.getType().equals(clazz) || (apiObjectField != null + && !apiObjectField.processtemplate())) { value = getValue(Object.class, field.getGenericType(), fieldName, jsondocObjects); } else { value = getValue(field.getType(), field.getGenericType(), fieldName, jsondocObjects); @@ -61,7 +63,6 @@ public class JSONDocTemplateBuilder { jsonDocTemplate.put(fieldName, value); } - } catch (Exception e) { log.error("Error in JSONDocTemplate creation for class [" + clazz.getCanonicalName() + "]", e); } @@ -70,30 +71,24 @@ public class JSONDocTemplateBuilder { return jsonDocTemplate; } - private static Object getValue(Class<?> fieldClass, Type fieldGenericType, String fieldName, Set<Class<?>> jsondocObjects) { + private static Object getValue(Class<?> fieldClass, Type fieldGenericType, String fieldName, + Set<Class<?>> jsondocObjects) { if (fieldClass.isPrimitive()) { return getValue(wrap(fieldClass), null, fieldName, jsondocObjects); - } else if (Map.class.isAssignableFrom(fieldClass)) { return new HashMap<Object, Object>(); - } else if (Number.class.isAssignableFrom(fieldClass)) { return new Integer(0); - } else if (String.class.isAssignableFrom(fieldClass) || fieldClass.isEnum()) { return new String(""); - } else if (Boolean.class.isAssignableFrom(fieldClass)) { return new Boolean("true"); - } else if (fieldClass.isArray() || Collection.class.isAssignableFrom(fieldClass)) { return new ArrayList<Object>(); - } else { return build(fieldClass, jsondocObjects); } - } private static Set<JSONDocFieldWrapper> getAllDeclaredFields(Class<?> clazz) { @@ -133,5 +128,4 @@ public class JSONDocTemplateBuilder { private static <T> Class<T> wrap(Class<T> clazz) { return clazz.isPrimitive() ? (Class<T>) primitives.get(clazz) : clazz; } - } diff --git a/backend/src/main/java/org/luxons/sevenwonders/doc/builders/JSONDocTypeBuilder.java b/backend/src/main/java/org/luxons/sevenwonders/doc/builders/JSONDocTypeBuilder.java new file mode 100644 index 00000000..4584e7cf --- /dev/null +++ b/backend/src/main/java/org/luxons/sevenwonders/doc/builders/JSONDocTypeBuilder.java @@ -0,0 +1,120 @@ +package org.luxons.sevenwonders.doc.builders; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Collection; +import java.util.Map; + +import org.jsondoc.core.annotation.ApiObject; +import org.jsondoc.core.util.JSONDocDefaultType; +import org.jsondoc.core.util.JSONDocType; + +public class JSONDocTypeBuilder { + + private static final String WILDCARD = "wildcard"; + + private static final String UNDEFINED = "undefined"; + + private static final String ARRAY = "array"; + + public static JSONDocType build(JSONDocType jsondocType, Class<?> clazz, Type type) { + if (clazz.isAssignableFrom(JSONDocDefaultType.class)) { + jsondocType.addItemToType(UNDEFINED); + return jsondocType; + } + + if (Map.class.isAssignableFrom(clazz)) { + jsondocType.addItemToType(getCustomClassName(clazz)); + + if (type instanceof ParameterizedType) { + Type mapKeyType = ((ParameterizedType) type).getActualTypeArguments()[0]; + Type mapValueType = ((ParameterizedType) type).getActualTypeArguments()[1]; + + jsondocType.setMapKey(new JSONDocType()); + jsondocType.setMapValue(new JSONDocType()); + + if (mapKeyType instanceof Class) { + jsondocType.setMapKey(new JSONDocType(((Class<?>) mapKeyType).getSimpleName())); + } else if (mapKeyType instanceof WildcardType) { + jsondocType.setMapKey(new JSONDocType(WILDCARD)); + } else if (mapKeyType instanceof TypeVariable<?>) { + jsondocType.setMapKey(new JSONDocType(((TypeVariable<?>) mapKeyType).getName())); + } else { + jsondocType.setMapKey( + build(jsondocType.getMapKey(), (Class<?>) ((ParameterizedType) mapKeyType).getRawType(), + mapKeyType)); + } + + if (mapValueType instanceof Class) { + jsondocType.setMapValue(new JSONDocType(((Class<?>) mapValueType).getSimpleName())); + } else if (mapValueType instanceof WildcardType) { + jsondocType.setMapValue(new JSONDocType(WILDCARD)); + } else if (mapValueType instanceof TypeVariable<?>) { + jsondocType.setMapValue(new JSONDocType(((TypeVariable<?>) mapValueType).getName())); + } else { + jsondocType.setMapValue( + build(jsondocType.getMapValue(), (Class<?>) ((ParameterizedType) mapValueType).getRawType(), + mapValueType)); + } + } + } else if (Collection.class.isAssignableFrom(clazz)) { + if (type instanceof ParameterizedType) { + Type parametrizedType = ((ParameterizedType) type).getActualTypeArguments()[0]; + jsondocType.addItemToType(getCustomClassName(clazz)); + + if (parametrizedType instanceof Class) { + jsondocType.addItemToType(getCustomClassName((Class<?>) parametrizedType)); + } else if (parametrizedType instanceof WildcardType) { + jsondocType.addItemToType(WILDCARD); + } else if (parametrizedType instanceof TypeVariable<?>) { + jsondocType.addItemToType(((TypeVariable<?>) parametrizedType).getName()); + } else { + return build(jsondocType, (Class<?>) ((ParameterizedType) parametrizedType).getRawType(), + parametrizedType); + } + } else if (type instanceof GenericArrayType) { + return build(jsondocType, clazz, ((GenericArrayType) type).getGenericComponentType()); + } else { + jsondocType.addItemToType(getCustomClassName(clazz)); + } + } else if (clazz.isArray()) { + jsondocType.addItemToType(ARRAY); + Class<?> componentType = clazz.getComponentType(); + return build(jsondocType, componentType, type); + } else { + jsondocType.addItemToType(getCustomClassName(clazz)); + if (type instanceof ParameterizedType) { + Type parametrizedType = ((ParameterizedType) type).getActualTypeArguments()[0]; + + if (parametrizedType instanceof Class) { + jsondocType.addItemToType(getCustomClassName((Class<?>) parametrizedType)); + } else if (parametrizedType instanceof WildcardType) { + jsondocType.addItemToType(WILDCARD); + } else if (parametrizedType instanceof TypeVariable<?>) { + jsondocType.addItemToType(((TypeVariable<?>) parametrizedType).getName()); + } else { + return build(jsondocType, (Class<?>) ((ParameterizedType) parametrizedType).getRawType(), + parametrizedType); + } + } + } + + return jsondocType; + } + + private static String getCustomClassName(Class<?> clazz) { + if (clazz.isAnnotationPresent(ApiObject.class)) { + ApiObject annotation = clazz.getAnnotation(ApiObject.class); + if (annotation.name().isEmpty()) { + return clazz.getSimpleName().toLowerCase(); + } else { + return annotation.name(); + } + } else { + return clazz.getSimpleName(); + } + } +} diff --git a/backend/src/main/java/org/luxons/sevenwonders/doc/builders/SpringObjectBuilder.java b/backend/src/main/java/org/luxons/sevenwonders/doc/builders/SpringObjectBuilder.java new file mode 100644 index 00000000..46f38e8b --- /dev/null +++ b/backend/src/main/java/org/luxons/sevenwonders/doc/builders/SpringObjectBuilder.java @@ -0,0 +1,52 @@ +package org.luxons.sevenwonders.doc.builders; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Set; +import java.util.TreeSet; + +import org.jsondoc.core.pojo.ApiObjectDoc; +import org.jsondoc.core.pojo.ApiObjectFieldDoc; +import org.jsondoc.core.scanner.DefaultJSONDocScanner; +import org.jsondoc.core.util.JSONDocHibernateValidatorProcessor; +import org.jsondoc.core.util.JSONDocType; + +public class SpringObjectBuilder { + + public static ApiObjectDoc buildObject(Class<?> clazz) { + ApiObjectDoc apiObjectDoc = new ApiObjectDoc(); + apiObjectDoc.setName(clazz.getSimpleName()); + + Set<ApiObjectFieldDoc> fieldDocs = new TreeSet<ApiObjectFieldDoc>(); + + for (Field field : clazz.getDeclaredFields()) { + ApiObjectFieldDoc fieldDoc = new ApiObjectFieldDoc(); + fieldDoc.setName(field.getName()); + fieldDoc.setOrder(Integer.MAX_VALUE); + fieldDoc.setRequired(DefaultJSONDocScanner.UNDEFINED.toUpperCase()); + fieldDoc.setJsondocType( + JSONDocTypeBuilder.build(new JSONDocType(), field.getType(), field.getGenericType())); + + JSONDocHibernateValidatorProcessor.processHibernateValidatorAnnotations(field, fieldDoc); + + fieldDocs.add(fieldDoc); + } + + Class<?> superclass = clazz.getSuperclass(); + if (superclass != null) { + ApiObjectDoc parentObjectDoc = buildObject(superclass); + fieldDocs.addAll(parentObjectDoc.getFields()); + } + + if (clazz.isEnum()) { + apiObjectDoc.setAllowedvalues(DefaultJSONDocScanner.enumConstantsToStringArray(clazz.getEnumConstants())); + } + + apiObjectDoc.setFields(fieldDocs); + if (Modifier.isAbstract(clazz.getModifiers())) { + apiObjectDoc.setShow(false); + } + + return apiObjectDoc; + } +} diff --git a/backend/src/main/java/org/luxons/sevenwonders/doc/builders/SpringResponseBuilder.java b/backend/src/main/java/org/luxons/sevenwonders/doc/builders/SpringResponseBuilder.java new file mode 100644 index 00000000..4d8c0ed9 --- /dev/null +++ b/backend/src/main/java/org/luxons/sevenwonders/doc/builders/SpringResponseBuilder.java @@ -0,0 +1,31 @@ +package org.luxons.sevenwonders.doc.builders; + +import java.lang.reflect.Method; + +import org.jsondoc.core.pojo.ApiResponseObjectDoc; +import org.jsondoc.core.util.JSONDocType; +import org.springframework.http.ResponseEntity; + +public class SpringResponseBuilder { + + /** + * Builds the ApiResponseObjectDoc from the method's return type and checks if the first type corresponds to a + * ResponseEntity class. In that case removes the "responseentity" string from the final list because it's not + * important to the documentation user. + * + * @param method + * the method to create the response object for + * + * @return the created {@link ApiResponseObjectDoc} + */ + public static ApiResponseObjectDoc buildResponse(Method method) { + ApiResponseObjectDoc apiResponseObjectDoc = new ApiResponseObjectDoc( + JSONDocTypeBuilder.build(new JSONDocType(), method.getReturnType(), method.getGenericReturnType())); + + if (method.getReturnType().isAssignableFrom(ResponseEntity.class)) { + apiResponseObjectDoc.getJsondocType().getType().remove(0); + } + + return apiResponseObjectDoc; + } +} diff --git a/backend/src/main/java/org/luxons/sevenwonders/doc/scanner/JsonDocWebSocketScanner.java b/backend/src/main/java/org/luxons/sevenwonders/doc/scanner/JsonDocWebSocketScanner.java index 0f5f5557..d7523577 100644 --- a/backend/src/main/java/org/luxons/sevenwonders/doc/scanner/JsonDocWebSocketScanner.java +++ b/backend/src/main/java/org/luxons/sevenwonders/doc/scanner/JsonDocWebSocketScanner.java @@ -1,21 +1,18 @@ package org.luxons.sevenwonders.doc.scanner; -import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.Type; import java.net.URL; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; -import com.google.common.collect.Sets; import org.jsondoc.core.annotation.Api; import org.jsondoc.core.annotation.ApiMethod; +import org.jsondoc.core.annotation.ApiObject; import org.jsondoc.core.pojo.ApiMethodDoc; +import org.jsondoc.core.pojo.ApiObjectDoc; import org.jsondoc.core.pojo.JSONDoc; import org.jsondoc.core.pojo.JSONDoc.MethodDisplay; import org.jsondoc.core.pojo.JSONDocTemplate; @@ -27,12 +24,14 @@ import org.jsondoc.springmvc.scanner.builder.SpringHeaderBuilder; import org.jsondoc.springmvc.scanner.builder.SpringPathVariableBuilder; import org.jsondoc.springmvc.scanner.builder.SpringProducesBuilder; import org.jsondoc.springmvc.scanner.builder.SpringQueryParamBuilder; -import org.jsondoc.springmvc.scanner.builder.SpringResponseBuilder; import org.jsondoc.springmvc.scanner.builder.SpringResponseStatusBuilder; import org.jsondoc.springmvc.scanner.builder.SpringVerbBuilder; +import org.luxons.sevenwonders.doc.builders.JSONDocApiObjectDocBuilder; import org.luxons.sevenwonders.doc.builders.JSONDocTemplateBuilder; +import org.luxons.sevenwonders.doc.builders.SpringObjectBuilder; import org.luxons.sevenwonders.doc.builders.SpringPathBuilder; import org.luxons.sevenwonders.doc.builders.SpringRequestBodyBuilder; +import org.luxons.sevenwonders.doc.builders.SpringResponseBuilder; import org.reflections.Reflections; import org.reflections.scanners.MethodAnnotationsScanner; import org.reflections.util.ClasspathHelper; @@ -46,11 +45,9 @@ import org.springframework.web.bind.annotation.RequestMapping; public class JsonDocWebSocketScanner extends Spring4JSONDocScanner { - /** - * Returns the main <code>ApiDoc</code>, containing <code>ApiMethodDoc</code> and <code>ApiObjectDoc</code> objects - * @return An <code>ApiDoc</code> object - */ - public JSONDoc getJSONDoc(String version, String basePath, List<String> packages, boolean playgroundEnabled, MethodDisplay displayMethodAs) { + @Override + public JSONDoc getJSONDoc(String version, String basePath, List<String> packages, boolean playgroundEnabled, + MethodDisplay displayMethodAs) { Set<URL> urls = new HashSet<URL>(); FilterBuilder filter = new FilterBuilder(); @@ -61,7 +58,9 @@ public class JsonDocWebSocketScanner extends Spring4JSONDocScanner { filter.includePackage(pkg); } - reflections = new Reflections(new ConfigurationBuilder().filterInputsBy(filter).setUrls(urls).addScanners(new MethodAnnotationsScanner())); + reflections = new Reflections(new ConfigurationBuilder().filterInputsBy(filter) + .setUrls(urls) + .addScanners(new MethodAnnotationsScanner())); JSONDoc jsondocDoc = new JSONDoc(version, basePath); jsondocDoc.setPlaygroundEnabled(playgroundEnabled); @@ -138,84 +137,20 @@ public class JsonDocWebSocketScanner extends Spring4JSONDocScanner { @Override public Set<Class<?>> jsondocObjects(List<String> packages) { - Set<Class<?>> candidates = getRootApiObjects(); - Set<Class<?>> subCandidates = Sets.newHashSet(); - - // This is to get objects' fields that are not returned nor part of the body request of a method, but that - // are a field of an object returned or a body of a request of a method - for (Class<?> clazz : candidates) { - appendSubCandidates(clazz, subCandidates); - } - candidates.addAll(subCandidates); - - return candidates.stream().filter(clazz -> inWhiteListedPackages(packages, clazz)).collect(Collectors.toSet()); - } - - private Set<Class<?>> getRootApiObjects() { - Set<Class<?>> candidates = Sets.newHashSet(); - Set<Method> methodsToDocument = getMethodsToDocument(); - for (Method method : methodsToDocument) { - addReturnType(candidates, method); - addBodyParam(candidates, method); - } - return candidates; - } - - private void addReturnType(Set<Class<?>> candidates, Method method) { - Class<?> returnValueClass = method.getReturnType(); - if (returnValueClass.isPrimitive() || returnValueClass.equals(JSONDoc.class)) { - return; - } - buildJSONDocObjectsCandidates(candidates, returnValueClass, method.getGenericReturnType(), - reflections); - } - - private void addBodyParam(Set<Class<?>> candidates, Method method) { - int bodyParamIndex = SpringRequestBodyBuilder.getIndexOfBodyParam(method); - if (bodyParamIndex >= 0) { - Class<?> bodyParamClass = method.getParameterTypes()[bodyParamIndex]; - Type bodyParamType = method.getGenericParameterTypes()[bodyParamIndex]; - buildJSONDocObjectsCandidates(candidates, bodyParamClass, bodyParamType, reflections); - } + return new ObjectsScanner(reflections).findJsondocObjects(packages); } - private Set<Method> getMethodsToDocument() { - Set<Method> methodsAnnotatedWith = reflections.getMethodsAnnotatedWith(RequestMapping.class); - methodsAnnotatedWith.addAll(reflections.getMethodsAnnotatedWith(SubscribeMapping.class)); - methodsAnnotatedWith.addAll(reflections.getMethodsAnnotatedWith(MessageMapping.class)); - return methodsAnnotatedWith; - } - - private boolean inWhiteListedPackages(List<String> packages, Class<?> clazz) { - Package p = clazz.getPackage(); - return p != null && packages.stream().anyMatch(whiteListedPkg -> p.getName().startsWith(whiteListedPkg)); + @Override + public ApiObjectDoc initApiObjectDoc(Class<?> clazz) { + return SpringObjectBuilder.buildObject(clazz); } - private void appendSubCandidates(Class<?> clazz, Set<Class<?>> subCandidates) { - if (clazz.isPrimitive() || clazz.equals(Class.class)) { - return; - } - - for (Field field : clazz.getDeclaredFields()) { - if (!isValidForRecursion(field)) { - continue; - } - - Class<?> fieldClass = field.getType(); - Set<Class<?>> fieldCandidates = new HashSet<>(); - buildJSONDocObjectsCandidates(fieldCandidates, fieldClass, field.getGenericType(), reflections); - - for (Class<?> candidate : fieldCandidates) { - if (!subCandidates.contains(candidate)) { - subCandidates.add(candidate); - - appendSubCandidates(candidate, subCandidates); - } - } + @Override + public ApiObjectDoc mergeApiObjectDoc(Class<?> clazz, ApiObjectDoc apiObjectDoc) { + if (clazz.isAnnotationPresent(ApiObject.class)) { + ApiObjectDoc jsondocApiObjectDoc = JSONDocApiObjectDocBuilder.build(clazz); + BeanUtils.copyProperties(jsondocApiObjectDoc, apiObjectDoc); } - } - - private static boolean isValidForRecursion(Field field) { - return !field.isSynthetic() && !field.getType().isPrimitive() && !Modifier.isTransient(field.getModifiers()); + return apiObjectDoc; } } diff --git a/backend/src/main/java/org/luxons/sevenwonders/doc/scanner/ObjectsScanner.java b/backend/src/main/java/org/luxons/sevenwonders/doc/scanner/ObjectsScanner.java new file mode 100644 index 00000000..e20406ef --- /dev/null +++ b/backend/src/main/java/org/luxons/sevenwonders/doc/scanner/ObjectsScanner.java @@ -0,0 +1,209 @@ +package org.luxons.sevenwonders.doc.scanner; + +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.Sets; +import org.jsondoc.core.pojo.JSONDoc; +import org.luxons.sevenwonders.doc.builders.SpringRequestBodyBuilder; +import org.reflections.Reflections; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.simp.annotation.SubscribeMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +public class ObjectsScanner { + + private final Reflections reflections; + + public ObjectsScanner(Reflections reflections) { + this.reflections = reflections; + } + + public Set<Class<?>> findJsondocObjects(List<String> packages) { + Set<Class<?>> candidates = getRootApiObjects(); + Set<Class<?>> subCandidates = Sets.newHashSet(); + + // This is to get objects' fields that are not returned nor part of the body request of a method, but that + // are a field of an object returned or a body of a request of a method + for (Class<?> clazz : candidates) { + appendSubCandidates(clazz, subCandidates); + } + candidates.addAll(subCandidates); + + return candidates.stream().filter(clazz -> inWhiteListedPackages(packages, clazz)).collect(Collectors.toSet()); + } + + private Set<Class<?>> getRootApiObjects() { + Set<Class<?>> candidates = Sets.newHashSet(); + Set<Method> methodsToDocument = getMethodsToDocument(); + for (Method method : methodsToDocument) { + addReturnType(candidates, method); + addBodyParam(candidates, method); + } + return candidates; + } + + private void addReturnType(Set<Class<?>> candidates, Method method) { + Class<?> returnValueClass = method.getReturnType(); + if (returnValueClass.isPrimitive() || returnValueClass.equals(JSONDoc.class)) { + return; + } + buildJSONDocObjectsCandidates(candidates, returnValueClass, method.getGenericReturnType(), reflections); + } + + private void addBodyParam(Set<Class<?>> candidates, Method method) { + int bodyParamIndex = SpringRequestBodyBuilder.getIndexOfBodyParam(method); + if (bodyParamIndex >= 0) { + Class<?> bodyParamClass = method.getParameterTypes()[bodyParamIndex]; + Type bodyParamType = method.getGenericParameterTypes()[bodyParamIndex]; + buildJSONDocObjectsCandidates(candidates, bodyParamClass, bodyParamType, reflections); + } + } + + private Set<Method> getMethodsToDocument() { + Set<Method> methodsAnnotatedWith = reflections.getMethodsAnnotatedWith(RequestMapping.class); + methodsAnnotatedWith.addAll(reflections.getMethodsAnnotatedWith(SubscribeMapping.class)); + methodsAnnotatedWith.addAll(reflections.getMethodsAnnotatedWith(MessageMapping.class)); + return methodsAnnotatedWith; + } + + private boolean inWhiteListedPackages(List<String> packages, Class<?> clazz) { + Package p = clazz.getPackage(); + return p != null && packages.stream().anyMatch(whiteListedPkg -> p.getName().startsWith(whiteListedPkg)); + } + + private void appendSubCandidates(Class<?> clazz, Set<Class<?>> subCandidates) { + if (clazz.isPrimitive() || clazz.equals(Class.class)) { + return; + } + + for (Field field : clazz.getDeclaredFields()) { + if (!isValidForRecursion(field)) { + continue; + } + + Class<?> fieldClass = field.getType(); + Set<Class<?>> fieldCandidates = new HashSet<>(); + buildJSONDocObjectsCandidates(fieldCandidates, fieldClass, field.getGenericType(), reflections); + + for (Class<?> candidate : fieldCandidates) { + if (!subCandidates.contains(candidate)) { + subCandidates.add(candidate); + + appendSubCandidates(candidate, subCandidates); + } + } + } + } + + private static boolean isValidForRecursion(Field field) { + // return !field.isSynthetic() && !field.getType().isPrimitive() && !Modifier.isTransient(field + // .getModifiers()); + return true; + } + + public static Set<Class<?>> buildJSONDocObjectsCandidates(Set<Class<?>> candidates, Class<?> clazz, Type type, + Reflections reflections) { + + if (Map.class.isAssignableFrom(clazz)) { + + if (type instanceof ParameterizedType) { + Type mapKeyType = ((ParameterizedType) type).getActualTypeArguments()[0]; + Type mapValueType = ((ParameterizedType) type).getActualTypeArguments()[1]; + + if (mapKeyType instanceof Class) { + candidates.add((Class<?>) mapKeyType); + } else if (mapKeyType instanceof WildcardType) { + candidates.add(Void.class); + } else { + if (mapKeyType instanceof ParameterizedType) { + candidates.addAll(buildJSONDocObjectsCandidates(candidates, + (Class<?>) ((ParameterizedType) mapKeyType).getRawType(), mapKeyType, reflections)); + } + } + + if (mapValueType instanceof Class) { + candidates.add((Class<?>) mapValueType); + } else if (mapValueType instanceof WildcardType) { + candidates.add(Void.class); + } else { + if (mapValueType instanceof ParameterizedType) { + candidates.addAll(buildJSONDocObjectsCandidates(candidates, + (Class<?>) ((ParameterizedType) mapValueType).getRawType(), mapValueType, reflections)); + } + } + } + } else if (Collection.class.isAssignableFrom(clazz)) { + if (type instanceof ParameterizedType) { + Type parametrizedType = ((ParameterizedType) type).getActualTypeArguments()[0]; + candidates.add(clazz); + + if (parametrizedType instanceof Class) { + candidates.add((Class<?>) parametrizedType); + } else if (parametrizedType instanceof WildcardType) { + candidates.add(Void.class); + } else { + candidates.addAll(buildJSONDocObjectsCandidates(candidates, + (Class<?>) ((ParameterizedType) parametrizedType).getRawType(), parametrizedType, + reflections)); + } + } else if (type instanceof GenericArrayType) { + candidates.addAll(buildJSONDocObjectsCandidates(candidates, clazz, + ((GenericArrayType) type).getGenericComponentType(), reflections)); + } else { + candidates.add(clazz); + } + } else if (clazz.isArray()) { + Class<?> componentType = clazz.getComponentType(); + candidates.addAll(buildJSONDocObjectsCandidates(candidates, componentType, type, reflections)); + } else { + if (type instanceof ParameterizedType) { + Type parametrizedType = ((ParameterizedType) type).getActualTypeArguments()[0]; + + if (parametrizedType instanceof Class) { + Class<?> candidate = (Class<?>) parametrizedType; + if (candidate.isInterface()) { + for (Class<?> implementation : reflections.getSubTypesOf(candidate)) { + buildJSONDocObjectsCandidates(candidates, implementation, parametrizedType, reflections); + } + } else { + candidates.add(candidate); + candidates.addAll(buildJSONDocObjectsCandidates(candidates, + (Class<?>) ((ParameterizedType) type).getRawType(), parametrizedType, reflections)); + } + } else if (parametrizedType instanceof WildcardType) { + candidates.add(Void.class); + candidates.addAll(buildJSONDocObjectsCandidates(candidates, + (Class<?>) ((ParameterizedType) type).getRawType(), parametrizedType, reflections)); + } else if (parametrizedType instanceof TypeVariable<?>) { + candidates.add(Void.class); + candidates.addAll(buildJSONDocObjectsCandidates(candidates, + (Class<?>) ((ParameterizedType) type).getRawType(), parametrizedType, reflections)); + } else { + candidates.addAll(buildJSONDocObjectsCandidates(candidates, + (Class<?>) ((ParameterizedType) parametrizedType).getRawType(), parametrizedType, + reflections)); + } + } else if (clazz.isInterface()) { + for (Class<?> implementation : reflections.getSubTypesOf(clazz)) { + candidates.addAll(buildJSONDocObjectsCandidates(candidates, implementation, type, reflections)); + } + } else { + candidates.add(clazz); + } + } + + return candidates; + } +} |