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