View Javadoc

1   package com.atlassian.plugins.rest.doclet.generators.schema;
2   
3   import com.atlassian.rest.annotation.RestProperty;
4   import com.atlassian.rest.annotation.RestProperty.Scope;
5   import com.google.common.base.Function;
6   import com.google.common.base.Joiner;
7   import com.google.common.base.Objects;
8   import com.google.common.base.Strings;
9   import com.google.common.collect.HashMultiset;
10  import com.google.common.collect.ImmutableList;
11  import com.google.common.collect.ImmutableSet;
12  import com.google.common.collect.Iterables;
13  import com.google.common.collect.Lists;
14  import com.google.common.collect.Multiset;
15  import com.google.common.collect.Sets;
16  import org.reflections.Reflections;
17  
18  import java.beans.IntrospectionException;
19  import java.beans.Introspector;
20  import java.beans.PropertyDescriptor;
21  import java.lang.reflect.AnnotatedElement;
22  import java.lang.reflect.Field;
23  import java.lang.reflect.Method;
24  import java.lang.reflect.Modifier;
25  import java.util.Collections;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Optional;
29  import java.util.Set;
30  
31  import static com.atlassian.plugins.rest.doclet.generators.schema.Annotations.shouldFieldBeIncludedInSchema;
32  import static com.atlassian.plugins.rest.doclet.generators.schema.Types.isCollection;
33  import static com.atlassian.plugins.rest.doclet.generators.schema.Types.isPrimitive;
34  import static com.atlassian.plugins.rest.doclet.generators.schema.Types.resolveType;
35  import static java.lang.reflect.Modifier.isStatic;
36  import static java.util.stream.Collectors.toList;
37  
38  public final class ModelClass implements Comparable<ModelClass> {
39  
40      private final Class<?> actualClass;
41      private final RichClass richClass;
42      private final AnnotatedElement containingField;
43      private final Schema.Type schemaType;
44  
45      public ModelClass(RichClass richClass, AnnotatedElement containingField) {
46          this.actualClass = richClass.getActualClass();
47          this.richClass = richClass;
48          this.schemaType = resolveType(actualClass, containingField);
49          this.containingField = containingField;
50      }
51  
52      public Class<?> getActualClass() {
53          return actualClass;
54      }
55  
56      public Schema.Type getType() {
57          return schemaType;
58      }
59  
60      public String getDescription() {
61          return Annotations.getDescription(containingField);
62      }
63  
64      public String getTopLevelTitle() {
65          if (richClass.hasGenericType()) {
66              String wrappedTitle = Joiner.on("-and-").skipNulls().join(Iterables.transform(richClass.getGenericTypes(), new Function<RichClass, String>() {
67                  @Override
68                  public String apply(final RichClass input) {
69                      return new ModelClass(input, null).getTitle();
70                  }
71              }));
72  
73              return Strings.emptyToNull(wrappedTitle) != null ? (getTitle(actualClass) + " of " + wrappedTitle) : null;
74          } else {
75              return getTitle(actualClass);
76          }
77      }
78  
79      public String getTitle() {
80          if (isPrimitive(actualClass) || isCollection(actualClass) || actualClass.getCanonicalName().startsWith("java")) {
81              return null;
82          } else {
83              return getTitle(actualClass);
84          }
85      }
86  
87      private String getTitle(Class<?> actualClass) {
88          String simplifiedClassName = actualClass.getSimpleName().replaceAll("(Bean|Data|DTO|Dto|Json|JSON|Enum)+$", "").replaceAll("^Abstract", "");
89          return camelCaseToSpaces(simplifiedClassName);
90      }
91  
92      private String camelCaseToSpaces(String camelCaseName) {
93          String[] parts = camelCaseName.split("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])");
94          return Joiner.on(" ").join(parts);
95      }
96  
97      public Set<Property> getProperties(Scope scope) {
98          return schemaType == Schema.Type.Object ? Sets.newLinkedHashSet(getProperties(actualClass, scope)) : Collections.<Property>emptySet();
99      }
100 
101     public List<Property> getProperties(Class<?> actualClass, Scope scope) {
102         List<Property> result = Lists.newArrayList();
103 
104         if (schemaType == Schema.Type.Object && actualClass != Object.class && actualClass != null) {
105             for (Field field : actualClass.getDeclaredFields()) {
106                 if (!isStatic(field.getModifiers()) && shouldFieldBeIncludedInSchema(field, field.getName(), actualClass, scope)) {
107                     ModelClass propertyModel = new ModelClass(richClass.createContainedType(field.getGenericType()), field);
108                     result.add(new Property(propertyModel, Annotations.resolveFieldName(field, field.getName()), Annotations.isRequired(field)));
109                 }
110             }
111 
112             for (PropertyDescriptor descriptor : getPropertyDescriptors(actualClass)) {
113                 Method getter = descriptor.getReadMethod();
114                 if (getter != null && getter.getDeclaringClass() != Object.class && shouldFieldBeIncludedInSchema(getter, descriptor.getName(), actualClass, scope)) {
115                     ModelClass propertyModel = new ModelClass(richClass.createContainedType(getter.getGenericReturnType()), getter);
116                     if (!result.contains(propertyModel)) {
117                         result.add(new Property(propertyModel, Annotations.resolveFieldName(getter, descriptor.getName()), Annotations.isRequired(getter)));
118                     }
119                 }
120             }
121 
122             result.addAll(0, getProperties(actualClass.getSuperclass(), scope));
123         }
124 
125         return result;
126     }
127 
128     public Optional<PatternedProperties> getPatternedProperties() {
129         if (Map.class.isAssignableFrom(actualClass) && richClass.getGenericTypes().size() == 2) {
130             String pattern = containingField != null && containingField.isAnnotationPresent(RestProperty.class) ?
131                     containingField.getAnnotation(RestProperty.class).pattern() : ".+";
132 
133             return Optional.of(new PatternedProperties(pattern, new ModelClass(richClass.getGenericTypes().get(1), null)));
134         }
135         return Optional.empty();
136     }
137 
138     public Optional<ModelClass> getCollectionItemModel() {
139         if (getType() == Schema.Type.Array) {
140             return Optional.of(new ModelClass(richClass.getGenericTypes().get(0), null));
141         }
142         return Optional.empty();
143     }
144 
145     public boolean isAbstract() {
146         return Modifier.isAbstract(actualClass.getModifiers()) && !actualClass.isInterface() && !actualClass.getCanonicalName().startsWith("java") && actualClass.getPackage() != null;
147     }
148 
149     public List<ModelClass> getSubModels() {
150         Set<ModelClass> result = Sets.newTreeSet();
151 
152         if (isAbstract() && actualClass.getTypeParameters().length == 0) // generic abstract classes are not supported, because they are probably some kind of collections
153         {
154             Reflections reflections = new Reflections(actualClass.getPackage().getName()); // search only in the package class
155 
156             for (Class<?> aClass : reflections.getSubTypesOf(actualClass)) // get subclasses of this
157             {
158                 result.add(new ModelClass(RichClass.of(aClass), null));
159             }
160         }
161 
162         return ImmutableList.copyOf(result);
163     }
164 
165     public Set<ModelClass> getSchemasReferencedMoreThanOnce(final Scope scope) {
166         Multiset<ModelClass> referenceCount = HashMultiset.create();
167         computeSchemasReferencedMoreThanOnce(this, scope, referenceCount);
168 
169         ImmutableSet.Builder<ModelClass> result = ImmutableSet.builder();
170         for (ModelClass modelClass : referenceCount) {
171             if (modelClass.getTitle() != null && referenceCount.count(modelClass) > 1) {
172                 result.add(modelClass);
173             }
174         }
175 
176         return result.build();
177     }
178 
179     private static void computeSchemasReferencedMoreThanOnce(ModelClass currentNode, final Scope scope, Multiset<ModelClass> alreadyReferenced) {
180         List<ModelClass> propertyModels = currentNode.getProperties(scope).stream()
181                 .map(input -> input.model)
182                 .collect(toList());
183 
184 
185         List<ModelClass> subModels = currentNode.getSubModels();
186         for (ModelClass subClass : subModels) {
187             alreadyReferenced.add(subClass, 2); // let's write definitions for all sub models as it will be more readable in "oneOf" clauses
188         }
189 
190         List<ModelClass> firstLevelModels = Lists.newArrayList();
191         firstLevelModels.addAll(subModels);
192         firstLevelModels.addAll(propertyModels);
193         currentNode.getPatternedProperties().map(PatternedProperties::getValuesType).ifPresent(firstLevelModels::add);
194         currentNode.getCollectionItemModel().ifPresent(firstLevelModels::add);
195 
196         for (ModelClass firstLevelModel : firstLevelModels) {
197             alreadyReferenced.add(firstLevelModel);
198             if (alreadyReferenced.count(firstLevelModel) <= 1 || Types.isCollection(firstLevelModel.actualClass) || Map.class.isAssignableFrom(firstLevelModel.actualClass)) {
199                 computeSchemasReferencedMoreThanOnce(firstLevelModel, scope, alreadyReferenced);
200             }
201         }
202     }
203 
204     private PropertyDescriptor[] getPropertyDescriptors(final Class<?> actualClass) {
205         try {
206             return Introspector.getBeanInfo(actualClass).getPropertyDescriptors();
207         } catch (IntrospectionException e) {
208             throw new RuntimeException(e);
209         }
210     }
211 
212 
213     @Override
214     public boolean equals(Object o) {
215         if (this == o) {
216             return true;
217         }
218         if (o == null || getClass() != o.getClass()) {
219             return false;
220         }
221 
222         ModelClass that = (ModelClass) o;
223 
224         return Objects.equal(this.actualClass, that.actualClass);
225     }
226 
227     @Override
228     public int hashCode() {
229         return Objects.hashCode(actualClass);
230     }
231 
232     @Override
233     public int compareTo(final ModelClass o) {
234         return actualClass.getName().compareTo(o.getActualClass().getName());
235     }
236 }