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