View Javadoc
1   /**
2    * This file Copyright (c) 2011-2018 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.objectfactory;
35  
36  import static java.util.stream.Collectors.joining;
37  
38  import java.lang.reflect.Constructor;
39  import java.lang.reflect.InvocationTargetException;
40  import java.lang.reflect.Modifier;
41  import java.util.Arrays;
42  import java.util.stream.IntStream;
43  import java.util.stream.Stream;
44  
45  import javax.inject.Inject;
46  
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  
51  /**
52   * Creates objects by dynamically resolving the parameters to use.
53   *
54   * <p>If the current class factory is of type {@link ReloadableClassFactory}, then it will first attempt to get the
55   * most recent version of the {@link Class} to instantiate.</p>
56   */
57  public class ObjectManufacturer {
58  
59      private static final Logger log = LoggerFactory.getLogger(ObjectManufacturer.class);
60  
61      /**
62       * Creates an object of the given type using parameters provided by a set of parameter resolvers. Will first look
63       * for a constructor annotated with &#64;Inject, it can be public, private, protected or package private. It will
64       * then look at public constructors and use the greediest. The greediest constructor is the one that has the most
65       * number of arguments that the parameter resolvers can fulfill. If there is more than one constructor that
66       * qualifies as greediest it is unspecified which one will be used.
67       */
68      public Object newInstance(Class<?> clazz, ParameterResolver... parameterResolvers) {
69          final ClassFactory cf = Classes.getClassFactory();
70          if (cf instanceof ReloadableClassFactory) {
71              try {
72                  clazz = ((ReloadableClassFactory) cf).reload(clazz);
73              } catch (ClassNotFoundException e) {
74                  throw new MgnlInstantiationException(e);
75              }
76          }
77  
78          Constructor<?>[] constructors = clazz.getDeclaredConstructors();
79  
80          Constructor<?> selectedConstructor = null;
81          for (Constructor<?> constructor : constructors) {
82              if (constructor.isAnnotationPresent(Inject.class)) {
83                  if (selectedConstructor != null) {
84                      throw new MgnlInstantiationException("Only one constructor can use @Inject [" + clazz + "]");
85                  }
86                  selectedConstructor = constructor;
87              }
88          }
89          if (selectedConstructor != null) {
90              selectedConstructor.setAccessible(true);
91              Object[] parameters = resolveParameters(selectedConstructor, parameterResolvers);
92              if (parameters == null) {
93                  String params = getUnresolvableParameters(selectedConstructor, parameterResolvers)
94                          .map(param -> String.format("%s at %d", param.getParameterType().getSimpleName(), param.getParameterIndex()))
95                          .collect(joining(", ", "[", "]"));
96  
97                  throw new MgnlInstantiationException("Unable to resolve parameters " + params + " of ctor [" + constructorToString(selectedConstructor)  + "]");
98              }
99              return newInstance(selectedConstructor, parameters);
100         }
101 
102         // Find greediest satisfiable constructor
103         int bestScore = -1;
104         Object[] bestParameters = null;
105         for (Constructor<?> constructor : constructors) {
106             if (!Modifier.isPublic(constructor.getModifiers())) {
107                 continue;
108             }
109             int score = constructor.getParameterTypes().length;
110             if (score < bestScore) {
111                 continue;
112             }
113             Object[] parameters = resolveParameters(constructor, parameterResolvers);
114             if (parameters == null) {
115                 continue;
116             }
117             selectedConstructor = constructor;
118             bestScore = score;
119             bestParameters = parameters;
120         }
121         if (selectedConstructor != null) {
122             return newInstance(selectedConstructor, bestParameters);
123         }
124         throw new MgnlInstantiationException("No suitable constructor found for class [" + clazz + "]");
125     }
126 
127     private Object newInstance(Constructor constructor, Object[] parameters) {
128         try {
129             return constructor.newInstance(parameters);
130         } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
131             throw new MgnlInstantiationException("Failed to create an instance with c-tor [" + constructorToString(constructor) + "]", e);
132         }
133     }
134 
135     private Object[] resolveParameters(Constructor<?> constructor, ParameterResolver[] parameterResolvers) {
136         Object[] parameters = new Object[constructor.getParameterTypes().length];
137         for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) {
138             ParameterInfoeterInfo">ParameterInfo constructorParameter = new ParameterInfo(constructor, parameterIndex);
139             try {
140                 Object parameter = resolveParameter(constructorParameter, parameterResolvers);
141                 if (parameter == ParameterResolver.UNRESOLVED) {
142                     return null;
143                 }
144                 parameters[parameterIndex] = parameter;
145             } catch (Exception e) {
146                 throw new MgnlInstantiationException("Failed to resolve param [" + constructorParameter.getParameterIndex() + "] of type [" + constructorParameter.getParameterType() + "]", e);
147             }
148         }
149         return parameters;
150     }
151 
152     private Stream<ParameterInfo> getUnresolvableParameters(Constructor<?> constructor, ParameterResolver[] parameterResolvers) {
153         return IntStream.range(0, constructor.getParameterTypes().length)
154                 .mapToObj(idx -> new ParameterInfo(constructor, idx))
155                 .filter(param -> resolveParameter(param, parameterResolvers) == ParameterResolver.UNRESOLVED);
156     }
157 
158     private Object resolveParameter(ParameterInfo constructorParameter, ParameterResolver[] parameterResolvers) {
159         for (ParameterResolver parameterResolver : parameterResolvers) {
160             Object parameter = parameterResolver.resolveParameter(constructorParameter);
161             if (parameter != ParameterResolver.UNRESOLVED) {
162                 return parameter;
163             }
164         }
165         log.debug("Failed to resolve {}. parameter {} in constructor of {}. Will use other constructor (if available).", (constructorParameter.getParameterIndex() + 1), constructorParameter.getParameterType(), constructorParameter.getDeclaringClass());
166         return ParameterResolver.UNRESOLVED;
167     }
168 
169     private String constructorToString(Constructor ctor) {
170         return ctor.getDeclaringClass().getSimpleName() +
171                 Arrays.stream(ctor.getParameterTypes()).map(Class::getSimpleName).collect(joining(", ", "(", ")"));
172     }
173 }