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}