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