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.marshall.swap.spi;
018
019import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021
022import java.lang.reflect.*;
023
024import org.apache.juneau.commons.lang.*;
025import org.apache.juneau.commons.reflect.*;
026import org.apache.juneau.commons.utils.*;
027import org.apache.juneau.marshall.*;
028
029/**
030 * Specialized transform for builder classes.
031 *
032 * <h5 class='section'>See Also:</h5><ul>
033 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/PojoBuilders">POJO Builders</a>
034 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Swaps">Swap</a>
035
036 * </ul>
037 *
038 * @param <T> The bean class.
039 * @param <B> The builder class.
040 */
041@SuppressWarnings({
042   "java:S1172", // Parameter kept to match ObjectSwap interface signature
043   "java:S1452", // findSwapFromBuilderClass()/findSwapFromObjectClass() return BuilderSwap<?,?> and getBuilderClassMeta() returns ClassMeta<?> because the object/builder types are discovered reflectively
044   "rawtypes", // Raw types necessary for ObjectSwap generic bridge methods
045   "unchecked" // Type erasure requires unchecked operations for builder pattern
046})
047public class BuilderSwap<T,B> {
048
049   /**
050    * Creates a BuilderSwap from the specified builder class if it qualifies as one.
051    *
052    * @param builderClass The potential builder class. Must not be <jk>null</jk>.
053    * @param cVis Minimum constructor visibility.
054    * @param mVis Minimum method visibility.
055    * @return A new swap instance, or <jk>null</jk> if class wasn't a builder class.
056    */
057   public static BuilderSwap<?,?> findSwapFromBuilderClass(Class<?> builderClass, Visibility cVis, Visibility mVis) {
058      var bci = info(builderClass);
059      if (bci.isNotPublic())
060         return null;
061
062      var objectClass = info(builderClass).getParameterType(0, PojoBuilder.class);
063
064      MethodInfo createObjectMethod;
065      MethodInfo createBuilderMethod;
066      ConstructorInfo objectConstructor;
067      ConstructorInfo builderConstructor;
068
069      createObjectMethod = getBuilderBuildMethod(bci);
070      if (nn(createObjectMethod))
071         objectClass = createObjectMethod.getReturnType().inner();
072
073      if (objectClass == null)
074         return null;
075
076      var pci = info(objectClass);
077
078      objectConstructor = pci.getDeclaredConstructor(x -> x.isVisible(cVis) && x.hasParameterTypes(builderClass)).orElse(null);
079      if (objectConstructor == null)
080         return null;
081
082      builderConstructor = bci.getNoArgConstructor(cVis).orElse(null);
083      createBuilderMethod = getBuilderCreateMethod(pci);
084      if (builderConstructor == null && createBuilderMethod == null)
085         return null;
086
087      return new BuilderSwap(objectClass, builderClass, objectConstructor.inner(), builderConstructor == null ? null : builderConstructor.inner(), createBuilderMethod, createObjectMethod);
088   }
089
090   /**
091    * Creates a BuilderSwap from the specified object class if it has one.
092    *
093    * @param bc The bean context to use to look up annotations. Must not be <jk>null</jk>.
094    * @param objectClass The object class to check. Must not be <jk>null</jk>.
095    * @param cVis Minimum constructor visibility.
096    * @param mVis Minimum method visibility.
097    * @return A new swap instance, or <jk>null</jk> if class didn't have a builder class.
098    */
099   public static BuilderSwap<?,?> findSwapFromObjectClass(MarshallingContext bc, Class<?> objectClass, Visibility cVis, Visibility mVis) {
100      var builderClass = Holder.<Class<?>>empty();
101      MethodInfo objectCreateMethod;
102      MethodInfo builderCreateMethod;
103      ConstructorInfo objectConstructor = null;
104      ConstructorInfo builderConstructor;
105      var pci = info(objectClass);
106
107      bc.getAnnotationProvider().find(org.apache.juneau.commons.Builder.class, pci).stream().map(x -> x.inner().value()).filter(ClassUtils::isNotVoid).forEach(builderClass::set);
108
109      builderCreateMethod = getBuilderCreateMethod(pci);
110
111      if (builderClass.isEmpty() && nn(builderCreateMethod))
112         builderClass.set(builderCreateMethod.getReturnType().inner());
113
114      if (builderClass.isEmpty()) {
115         // @formatter:off
116         var cc = pci.getPublicConstructor(
117            x -> x.isVisible(cVis)
118            && x.hasNumParameters(1)
119            && x.getParameter(0).getParameterType().isAssignableTo(PojoBuilder.class)
120         ).orElse(null);
121         // @formatter:on
122         if (nn(cc)) {
123            objectConstructor = cc;
124            builderClass.set(cc.getParameter(0).getParameterType().inner());
125         }
126      }
127
128      if (builderClass.isEmpty())
129         return null;
130
131      var bci = info(builderClass.get());
132      builderConstructor = bci.getNoArgConstructor(cVis).orElse(null);
133      if (builderConstructor == null && builderCreateMethod == null)
134         return null;
135
136      objectCreateMethod = getBuilderBuildMethod(bci);
137      var builderClass2 = builderClass.get();
138      if (objectConstructor == null)
139         objectConstructor = pci.getDeclaredConstructor(x -> x.isVisible(cVis) && x.hasParameterTypes(builderClass2)).orElse(null);
140
141      if (objectConstructor == null && objectCreateMethod == null)
142         return null;
143
144      return new BuilderSwap(objectClass, builderClass.get(), objectConstructor == null ? null : objectConstructor.inner(), builderConstructor == null ? null : builderConstructor.inner(),
145         builderCreateMethod, objectCreateMethod);
146   }
147
148   private static MethodInfo getBuilderBuildMethod(ClassInfo c) {
149      // @formatter:off
150      return c.getDeclaredMethod(
151         x -> x.isNotStatic()
152         && x.getParameterCount() == 0
153         && (!x.hasReturnType(void.class))
154         && x.hasName("build")
155      ).orElse(null);
156      // @formatter:on
157   }
158
159   private static MethodInfo getBuilderCreateMethod(ClassInfo c) {
160      // @formatter:off
161      return c.getPublicMethod(
162         x -> x.isStatic()
163         && (x.hasName("create") || x.hasName("builder"))
164         && ! x.hasReturnType(c)
165         && hasConstructorThatTakesType(c, x.getReturnType())
166      ).orElse(null);
167      // @formatter:on
168   }
169
170   private static boolean hasConstructorThatTakesType(ClassInfo c, ClassInfo argType) {
171      // @formatter:off
172      return nn(c.getPublicConstructor(
173         x -> x.hasNumParameters(1)
174         && x.hasParameterTypes(argType)
175      ));
176      // @formatter:on
177   }
178
179   private final Class<T> objectClass;
180   private final Class<B> builderClass;
181
182   private final Constructor<T> objectConstructor;
183
184   private final Constructor<B> builderConstructor;
185
186   private final MethodInfo createBuilderMethod;
187
188   private final MethodInfo createObjectMethod;
189
190   private ClassMeta<?> builderClassMeta;
191
192   /**
193    * Constructor.
194    *
195    * @param objectClass The object class created by the builder class.
196    * @param builderClass The builder class.
197    * @param objectConstructor
198    *    The object constructor that takes in a builder as a parameter.
199    *    Can be <jk>null</jk> if the object is created via {@code createObjectMethod} instead.
200    * @param builderConstructor
201    *    The builder no-arg constructor.
202    *    Can be <jk>null</jk> if the builder is created via {@code createBuilderMethod} instead.
203    * @param createBuilderMethod
204    *    The static create() method on the object class.
205    *    Can be <jk>null</jk> if the builder is created via {@code builderConstructor} instead.
206    * @param createObjectMethod
207    *    The build() method on the builder class.
208    *    Can be <jk>null</jk> if the object is created via {@code objectConstructor} instead.
209    */
210   protected BuilderSwap(Class<T> objectClass, Class<B> builderClass, Constructor<T> objectConstructor, Constructor<B> builderConstructor, MethodInfo createBuilderMethod,
211      MethodInfo createObjectMethod) {
212      this.objectClass = objectClass;
213      this.builderClass = builderClass;
214      this.objectConstructor = objectConstructor;
215      this.builderConstructor = builderConstructor;
216      this.createBuilderMethod = createBuilderMethod;
217      this.createObjectMethod = createObjectMethod;
218   }
219
220   /**
221    * Creates a new object from the specified builder.
222    *
223    * @param session The current bean session.
224    * @param builder The object builder.
225    * @param hint A hint about the class type.
226    * @return A new object.
227    * @throws ExecutableException Exception occurred on invoked constructor/method/field.
228    */
229   public T build(MarshallingSession session, B builder, ClassMeta<?> hint) throws ExecutableException {
230      if (nn(createObjectMethod))
231         return (T)createObjectMethod.invoke(builder);
232      try {
233         return objectConstructor.newInstance(builder);
234      } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
235         throw new ExecutableException(e);
236      }
237   }
238
239   /**
240    * Creates a new builder object.
241    *
242    * @param session The current bean session.
243    * @param hint A hint about the class type.
244    * @return A new object.
245    * @throws ExecutableException Exception occurred on invoked constructor/method/field.
246    */
247   public B create(MarshallingSession session, ClassMeta<?> hint) throws ExecutableException {
248      if (nn(createBuilderMethod))
249         return (B)createBuilderMethod.invoke(null);
250      try {
251         return builderConstructor.newInstance();
252      } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
253         throw new ExecutableException(e);
254      }
255   }
256
257   /**
258    * The builder class.
259    *
260    * @return The builder class.
261    */
262   public Class<B> getBuilderClass() { return builderClass; }
263
264   /**
265    * Returns the {@link ClassMeta} of the transformed class type.
266    *
267    * <p>
268    * This value is cached for quick lookup.
269    *
270    * @param session
271    *    The bean context to use to get the class meta.
272    *    This is always going to be the same bean context that created this swap.
273    *    Must not be <jk>null</jk> on the first (uncached) call.
274    * @return The {@link ClassMeta} of the transformed class type.
275    */
276   public ClassMeta<?> getBuilderClassMeta(MarshallingSession session) {
277      if (builderClassMeta == null)
278         builderClassMeta = session.getClassMeta(getBuilderClass());
279      return builderClassMeta;
280   }
281
282   /**
283    * The object class.
284    *
285    * @return The object class.
286    */
287   public Class<T> getObjectClass() { return objectClass; }
288}