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