View Javadoc
1   /**
2    * This file Copyright (c) 2019 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.config.resolver;
35  
36  import static java.util.function.Function.identity;
37  import static java.util.stream.Collectors.toMap;
38  
39  import info.magnolia.init.MagnoliaConfigurationProperties;
40  import info.magnolia.jcr.node2bean.TypeDescriptor;
41  
42  import java.lang.annotation.Annotation;
43  import java.lang.reflect.InvocationTargetException;
44  import java.lang.reflect.Method;
45  import java.util.Map;
46  import java.util.Optional;
47  import java.util.function.Function;
48  
49  import javax.inject.Inject;
50  import javax.inject.Singleton;
51  
52  import info.magnolia.objectfactory.annotation.Multibinding;
53  import info.magnolia.transformer.TypeResolver;
54  
55  import org.apache.commons.lang3.StringUtils;
56  import org.reflections.Reflections;
57  import org.slf4j.Logger;
58  import org.slf4j.LoggerFactory;
59  
60  /**
61   * {@link TypeResolver} implementation capable of deducing the type from a string alias instead of fully-qualified class name.
62   * In order to successfully conduct the resolution this resolver expects the <b>"{@value TYPE_PROPERTY}"</b> property value
63   * to contain the alias which is assigned to some class via annotation.
64   *
65   * <p><strong>NOTE:</strong> For the time being this class is meant for the internal use only
66   * and is a subject to change any moment!</p>
67   *
68   * @see TypeAlias
69   */
70  @Singleton
71  @Multibinding
72  public final class AnnotationProcessingTypeResolver implements TypeResolver {
73  
74      private static final Logger log = LoggerFactory.getLogger(AnnotationProcessingTypeResolver.class);
75  
76      private final Map<Class, AliasTypeResolver> delegates;
77      private static final String TYPE_PROPERTY = "$type";
78  
79      @Inject
80      public AnnotationProcessingTypeResolver(MagnoliaConfigurationProperties configurationProperties) {
81          Object[] whitelistedPackages = StringUtils.stripAll(StringUtils.split(configurationProperties.getProperty(MagnoliaConfigurationProperties.MAGNOLIA_DEFINITIONS_CLASSPATH), ","));
82          final Reflections reflections = new Reflections(whitelistedPackages);
83  
84          this.delegates = reflections.getTypesAnnotatedWith(TypeAlias.class).stream()
85                  .filter(Class::isAnnotation)
86                  .map(annotation -> new AliasTypeResolver(
87                          annotation.getAnnotation(TypeAlias.class).type(),
88                          resolveRelatedTypes((Class<? extends Annotation>) annotation, reflections)))
89                  .collect(toMap(AliasTypeResolver::baseType, Function.identity()));
90      }
91  
92      private Map<String, Class> resolveRelatedTypes(Class<? extends Annotation> annotation, Reflections reflections) {
93          return reflections
94                  .getTypesAnnotatedWith(annotation, true)
95                  .stream()
96                  .collect(toMap(type -> getAlias(type.getAnnotation(annotation)), identity()));
97      }
98  
99      @Override
100     public Optional<Class<?>> resolveType(TypeDescriptor typeDescriptor, Map<String, Object> properties) {
101         return delegates.values().stream()
102                 .map(value -> value.resolveType(typeDescriptor, properties))
103                 .filter(Optional::isPresent)
104                 .findFirst()
105                 .orElse(Optional.empty());
106     }
107 
108     private String getAlias(Annotation annotation) {
109         try {
110             final Method valueMethod = annotation.getClass().getMethod("value");
111             valueMethod.setAccessible(true);
112             return (String) valueMethod.invoke(annotation);
113         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
114             throw new RuntimeException("Failed to resolve alias", e);
115         }
116     }
117 
118     private final class AliasTypeResolver implements TypeResolver {
119 
120         private final Class<?> baseType;
121         private final Map<String, Class> aliasedTypes;
122 
123         AliasTypeResolver(Class<?> baseType, Map<String, Class> aliasedTypes) {
124             this.baseType = baseType;
125             this.aliasedTypes = aliasedTypes;
126         }
127 
128         Class<?> baseType() {
129             return baseType;
130         }
131 
132         @Override
133         public Optional<Class<?>> resolveType(TypeDescriptor typeDescriptor, Map<String, Object> properties) {
134             if (supportsType(typeDescriptor) && properties.containsKey(TYPE_PROPERTY)) {
135                 Optional result = Optional.of(properties.get(TYPE_PROPERTY)).map(aliasedTypes::get);
136                 if (result.isPresent()) {
137                     return result;
138                 } else {
139                     log.warn("Encountered the '{}' property but failed to resolved the type from its value: [{}]. Available types: [{}]", TYPE_PROPERTY, properties.get(TYPE_PROPERTY), String.join(",", aliasedTypes.keySet()));
140                 }
141             }
142             return Optional.empty();
143         }
144 
145         @Override
146         public boolean supportsType(TypeDescriptor typeDescriptor) {
147             return typeDescriptor != null && baseType.isAssignableFrom(typeDescriptor.getType());
148         }
149     }
150 }