001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.swap;
018
019import static org.apache.juneau.internal.ClassUtils.*;
020
021import java.lang.reflect.*;
022
023import org.apache.juneau.*;
024import org.apache.juneau.reflect.*;
025
026/**
027 * Specialized transform for builder classes.
028 *
029 * <h5 class='section'>See Also:</h5><ul>
030 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/PojoBuilders">POJO Builders</a>
031 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SwapBasics">Swap Basics</a>
032
033 * </ul>
034 *
035 * @param <T> The bean class.
036 * @param <B> The builder class.
037 */
038@SuppressWarnings("unchecked")
039public class BuilderSwap<T,B> {
040
041   private final Class<T> objectClass;
042   private final Class<B> builderClass;
043   private final Constructor<T> objectConstructor;          // public Pojo(Builder);
044   private final Constructor<B> builderConstructor;       // protected Builder();
045   private final MethodInfo createBuilderMethod;          // Builder create();
046   private final MethodInfo createObjectMethod;             // Pojo build();
047   private ClassMeta<?> builderClassMeta;
048
049   /**
050    * Constructor.
051    *
052    * @param objectClass The object class created by the builder class.
053    * @param builderClass The builder class.
054    * @param objectConstructor The object constructor that takes in a builder as a parameter.
055    * @param builderConstructor The builder no-arg constructor.
056    * @param createBuilderMethod The static create() method on the object class.
057    * @param createObjectMethod The build() method on the builder class.
058    */
059   protected BuilderSwap(Class<T> objectClass, Class<B> builderClass, Constructor<T> objectConstructor, Constructor<B> builderConstructor, MethodInfo createBuilderMethod, MethodInfo createObjectMethod) {
060      this.objectClass = objectClass;
061      this.builderClass = builderClass;
062      this.objectConstructor = objectConstructor;
063      this.builderConstructor = builderConstructor;
064      this.createBuilderMethod = createBuilderMethod;
065      this.createObjectMethod = createObjectMethod;
066   }
067
068   /**
069    * The object class.
070    *
071    * @return The object class.
072    */
073   public Class<T> getObjectClass() {
074      return objectClass;
075   }
076
077   /**
078    * The builder class.
079    *
080    * @return The builder class.
081    */
082   public Class<B> getBuilderClass() {
083      return builderClass;
084   }
085
086   /**
087    * Returns the {@link ClassMeta} of the transformed class type.
088    *
089    * <p>
090    * This value is cached for quick lookup.
091    *
092    * @param session
093    *    The bean context to use to get the class meta.
094    *    This is always going to be the same bean context that created this swap.
095    * @return The {@link ClassMeta} of the transformed class type.
096    */
097   public ClassMeta<?> getBuilderClassMeta(BeanSession session) {
098      if (builderClassMeta == null)
099         builderClassMeta = session.getClassMeta(getBuilderClass());
100      return builderClassMeta;
101   }
102
103   /**
104    * Creates a new builder object.
105    *
106    * @param session The current bean session.
107    * @param hint A hint about the class type.
108    * @return A new object.
109    * @throws ExecutableException Exception occurred on invoked constructor/method/field.
110    */
111   public B create(BeanSession session, ClassMeta<?> hint) throws ExecutableException {
112      if (createBuilderMethod != null)
113         return (B)createBuilderMethod.invoke(null);
114      try {
115         return builderConstructor.newInstance();
116      } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
117         throw new ExecutableException(e);
118      }
119   }
120
121   /**
122    * Creates a new object from the specified builder.
123    *
124    * @param session The current bean session.
125    * @param builder The object builder.
126    * @param hint A hint about the class type.
127    * @return A new object.
128    * @throws ExecutableException Exception occurred on invoked constructor/method/field.
129    */
130   public T build(BeanSession session, B builder, ClassMeta<?> hint) throws ExecutableException {
131      if (createObjectMethod != null)
132         return (T)createObjectMethod.invoke(builder);
133      try {
134         return objectConstructor.newInstance(builder);
135      } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
136         throw new ExecutableException(e);
137      }
138   }
139
140   /**
141    * Creates a BuilderSwap from the specified builder class if it qualifies as one.
142    *
143    * @param builderClass The potential builder class.
144    * @param cVis Minimum constructor visibility.
145    * @param mVis Minimum method visibility.
146    * @return A new swap instance, or <jk>null</jk> if class wasn't a builder class.
147    */
148   @SuppressWarnings("rawtypes")
149   public static BuilderSwap<?,?> findSwapFromBuilderClass(Class<?> builderClass, Visibility cVis, Visibility mVis) {
150      ClassInfo bci = ClassInfo.of(builderClass);
151      if (bci.isNotPublic())
152         return null;
153
154      Class<?> objectClass = ClassInfo.of(builderClass).getParameterType(0, Builder.class);
155
156      MethodInfo createObjectMethod, createBuilderMethod;
157      ConstructorInfo objectConstructor;
158      ConstructorInfo builderConstructor;
159
160      createObjectMethod = getBuilderBuildMethod(bci);
161      if (createObjectMethod != null)
162         objectClass = createObjectMethod.getReturnType().inner();
163
164      if (objectClass == null)
165         return null;
166
167      ClassInfo pci = ClassInfo.of(objectClass);
168
169      objectConstructor = pci.getDeclaredConstructor(x -> x.isVisible(cVis) && x.hasParamTypes(builderClass));
170      if (objectConstructor == null)
171         return null;
172
173      builderConstructor = bci.getNoArgConstructor(cVis);
174      createBuilderMethod = getBuilderCreateMethod(pci);
175      if (builderConstructor == null && createBuilderMethod == null)
176         return null;
177
178      return new BuilderSwap(objectClass, builderClass, objectConstructor.inner(), builderConstructor == null ? null : builderConstructor.inner(), createBuilderMethod, createObjectMethod);
179   }
180
181
182   /**
183    * Creates a BuilderSwap from the specified object class if it has one.
184    *
185    * @param bc The bean context to use to look up annotations.
186    * @param objectClass The object class to check.
187    * @param cVis Minimum constructor visibility.
188    * @param mVis Minimum method visibility.
189    * @return A new swap instance, or <jk>null</jk> if class didn't have a builder class.
190    */
191   @SuppressWarnings("rawtypes")
192   public static BuilderSwap<?,?> findSwapFromObjectClass(BeanContext bc, Class<?> objectClass, Visibility cVis, Visibility mVis) {
193      Value<Class<?>> builderClass = Value.empty();
194      MethodInfo objectCreateMethod, builderCreateMethod;
195      ConstructorInfo objectConstructor = null;
196      ConstructorInfo builderConstructor;
197
198      bc.forEachAnnotation(org.apache.juneau.annotation.Builder.class, objectClass, x -> isNotVoid(x.value()), x -> builderClass.set(x.value()));
199
200      ClassInfo pci = ClassInfo.of(objectClass);
201
202      builderCreateMethod = getBuilderCreateMethod(pci);
203
204      if (builderClass.isEmpty() && builderCreateMethod != null)
205         builderClass.set(builderCreateMethod.getReturnType().inner());
206
207      if (builderClass.isEmpty()) {
208         ConstructorInfo cc = pci.getPublicConstructor(
209            x -> x.isVisible(cVis)
210            && x.hasNumParams(1)
211            && x.getParamType(0).isChildOf(Builder.class)
212         );
213         if (cc != null) {
214            objectConstructor = cc;
215            builderClass.set(cc.getParamType(0).inner());
216         }
217      }
218
219      if (builderClass.isEmpty())
220         return null;
221
222      ClassInfo bci = ClassInfo.of(builderClass.get());
223      builderConstructor = bci.getNoArgConstructor(cVis);
224      if (builderConstructor == null && builderCreateMethod == null)
225         return null;
226
227      objectCreateMethod = getBuilderBuildMethod(bci);
228      Class<?> builderClass2 = builderClass.get();
229      if (objectConstructor == null)
230         objectConstructor = pci.getDeclaredConstructor(x -> x.isVisible(cVis) && x.hasParamTypes(builderClass2));
231
232      if (objectConstructor == null && objectCreateMethod == null)
233         return null;
234
235      return new BuilderSwap(objectClass, builderClass.get(), objectConstructor == null ? null : objectConstructor.inner(), builderConstructor == null ? null : builderConstructor.inner(), builderCreateMethod, objectCreateMethod);
236   }
237
238   private static MethodInfo getBuilderCreateMethod(ClassInfo c) {
239      return c.getPublicMethod(
240         x -> x.isStatic()
241         && (x.hasName("create") || x.hasName("builder"))
242         && ! x.hasReturnType(c)
243         && hasConstructorThatTakesType(c, x.getReturnType())
244      );
245   }
246
247   private static boolean hasConstructorThatTakesType(ClassInfo c, ClassInfo argType) {
248      return c.getPublicConstructor(
249         x -> x.hasNumParams(1)
250         && x.hasParamTypes(argType)
251      ) != null;
252   }
253
254   private static MethodInfo getBuilderBuildMethod(ClassInfo c) {
255      return c.getDeclaredMethod(
256         x -> x.isNotStatic()
257         && x.hasNoParams()
258         && (!x.hasReturnType(void.class))
259         && x.hasName("build")
260      );
261   }
262}