View Javadoc

1   package com.atlassian.plugins.rest.common.expand;
2   
3   import java.lang.annotation.Annotation;
4   import java.lang.reflect.Field;
5   import java.util.Collection;
6   import java.util.Collections;
7   import java.util.List;
8   import java.util.stream.Collectors;
9   import java.util.stream.Stream;
10  
11  import javax.annotation.Nonnull;
12  import javax.xml.bind.annotation.XmlAttribute;
13  import javax.xml.bind.annotation.XmlElement;
14  
15  import com.atlassian.annotations.tenancy.TenantAware;
16  import com.atlassian.plugins.rest.common.expand.parameter.ExpandParameter;
17  import com.atlassian.plugins.rest.common.expand.resolver.EntityExpanderResolver;
18  import com.atlassian.plugins.rest.common.util.ReflectionUtils;
19  
20  import com.google.common.base.Optional;
21  import com.google.common.base.Predicate;
22  import com.google.common.cache.CacheBuilder;
23  import com.google.common.cache.CacheLoader;
24  import com.google.common.cache.LoadingCache;
25  import com.google.common.collect.Iterables;
26  
27  import org.apache.commons.lang.StringUtils;
28  
29  import static com.atlassian.annotations.tenancy.TenancyScope.TENANTLESS;
30  import static com.atlassian.plugins.rest.common.util.ReflectionUtils.getFieldValue;
31  import static com.atlassian.plugins.rest.common.util.ReflectionUtils.setFieldValue;
32  import static com.google.common.collect.ImmutableList.copyOf;
33  
34  /**
35   * This allows for crawling the fields of any arbitrary object, looking for fields that should be expanded.
36   */
37  public class EntityCrawler {
38  
39      private final Collection<? extends AdditionalExpandsProvider> additionalExpandsProviders;
40      @TenantAware(TENANTLESS)
41      private LoadingCache<Class, List<Field>> declaredFields = CacheBuilder.newBuilder().build(new CacheLoader<Class, List<Field>>() {
42          @Override
43          public List<Field> load(@Nonnull final Class cls) throws Exception {
44              return copyOf(ReflectionUtils.getDeclaredFields(cls));
45          }
46      });
47  
48      @TenantAware(TENANTLESS)
49      private LoadingCache<Class, Optional<Field>> expandFields = CacheBuilder.newBuilder().build(new CacheLoader<Class, Optional<Field>>() {
50          @Override
51          public Optional<Field> load(@Nonnull final Class cls) throws Exception {
52              for (Field field : declaredFields.getUnchecked(cls)) {
53                  if (field.getType().equals(String.class)) {
54                      final XmlAttribute annotation = field.getAnnotation(XmlAttribute.class);
55                      if (annotation != null && (field.getName().equals("expand") || "expand".equals(annotation.name()))) {
56                          return Optional.of(field);
57                      }
58                  }
59              }
60              return Optional.absent();
61          }
62      });
63  
64      public EntityCrawler() {
65          this.additionalExpandsProviders = Collections.emptyList();
66      }
67  
68      public EntityCrawler(Collection<? extends AdditionalExpandsProvider> additionalExpandsProviders) {
69          this.additionalExpandsProviders = additionalExpandsProviders;
70      }
71  
72      /**
73       * Crawls an entity for fields that should be expanded and expands them.
74       *
75       * @param entity           the object to crawl, can be {@code null}.
76       * @param expandParameter  the parameters to match for expansion
77       * @param expanderResolver the resolver to lookup {@link EntityExpander} for fields to be expanded.
78       */
79      public void crawl(Object entity, ExpandParameter expandParameter, EntityExpanderResolver expanderResolver) {
80          if (entity == null) {
81              return;
82          }
83  
84          final Collection<Field> expandableFields = getExpandableFields(entity);
85          setExpandParameter(expandableFields, entity);
86          expandFields(expandableFields, entity, expandParameter, expanderResolver);
87      }
88  
89      private Collection<String> getAdditionalExpands(Object entity) {
90          return additionalExpandsProviders.stream()
91                  .filter(provider -> provider.getSupportedType().isInstance(entity))
92                  .flatMap(filteredProvider -> getAdditionalExpandFromProvider(entity, filteredProvider).stream())
93                  .collect(Collectors.toList());
94      }
95  
96      @SuppressWarnings("unchecked")
97      private List<String> getAdditionalExpandFromProvider(Object entity, AdditionalExpandsProvider filteredProvider) {
98          return filteredProvider.getAdditionalExpands(filteredProvider.getSupportedType().cast(entity));
99      }
100 
101     private void setExpandParameter(Collection<Field> expandableFields, Object entity) {
102         final Optional<Field> expand = expandFields.getUnchecked(entity.getClass());
103         if (expand != null && expand.isPresent() && !expandableFields.isEmpty()) {
104             String expandValue = createExpandString(expandableFields, getAdditionalExpands(entity));
105             setFieldValue(expand.get(), entity, expandValue);
106         }
107     }
108 
109     private String createExpandString(Collection<Field> expandableFields, final Collection<String> additionalExpands) {
110         return Stream.concat(
111                     expandableFields.stream()
112                         .map(this::getExpandable)
113                         .map(Expandable::value),
114                     additionalExpands.stream())
115                 .distinct()
116                 .collect(Collectors.joining(","));
117     }
118 
119     private Collection<Field> getExpandableFields(final Object entity) {
120         return copyOf(Iterables.filter(declaredFields.getUnchecked(entity.getClass()), new Predicate<Field>() {
121             public boolean apply(Field field) {
122                 return getExpandable(field) != null && ReflectionUtils.getFieldValue(field, entity) != null;
123             }
124         }));
125     }
126 
127     private void expandFields(Collection<Field> expandableFields, Object entity, ExpandParameter expandParameter, EntityExpanderResolver expanderResolver) {
128         for (Field field : expandableFields) {
129             final Expandable expandable = getExpandable(field);
130             if (expandParameter.shouldExpand(expandable) && expanderResolver.hasExpander(field.getType())) {
131                 // we know the expander is not null, as per ExpanderResolver contract
132                 final EntityExpander<Object> entityExpander = expanderResolver.getExpander(field.getType());
133 
134                 final ExpandContext<Object> context = new DefaultExpandContext<Object>(getFieldValue(field, entity), expandable, expandParameter);
135                 setFieldValue(field, entity, entityExpander.expand(context, expanderResolver, this));
136             }
137         }
138     }
139 
140     /**
141      * Returns the expandable annotation with the properly set value. The value is defined as the first valid point in the following list:
142      * <ol>
143      * <li>the value of the {@link Expandable} annotation if it is set</li>
144      * <li>the name of an {@link XmlElement} if the annotation is present on the field and its name is not {@code ##default}</li>
145      * <li>the name of the field</li>
146      * <ol>
147      *
148      * @param field the field to look up the Expandable for
149      * @return {@code null} if the field is null, {@code null} if the field doesn't have an expandable annotation,
150      * an expandable annotation with a properly set value.
151      */
152     Expandable getExpandable(final Field field) {
153         if (field == null) {
154             return null;
155         }
156 
157         final Expandable expandable = field.getAnnotation(Expandable.class);
158         if (expandable == null) {
159             return null;
160         }
161 
162         if (StringUtils.isNotEmpty(expandable.value())) {
163             return expandable;
164         }
165 
166         final XmlElement xmlElement = field.getAnnotation(XmlElement.class);
167         if (xmlElement != null && StringUtils.isNotEmpty(xmlElement.name()) && !StringUtils.equals("##default", xmlElement.name())) {
168             return new ExpandableWithValue(xmlElement.name());
169         }
170 
171         return new ExpandableWithValue(field.getName());
172     }
173 
174     private static class ExpandableWithValue implements Expandable {
175         private final String value;
176 
177         public ExpandableWithValue(String value) {
178             this.value = value;
179         }
180 
181         public String value() {
182             return value;
183         }
184 
185         public Class<? extends Annotation> annotationType() {
186             return Expandable.class;
187         }
188     }
189 }