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)
154 {
155 Reflections reflections = new Reflections(actualClass.getPackage().getName());
156
157 for (Class<?> aClass : reflections.getSubTypesOf(actualClass))
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);
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 }