View Javadoc
1   package org.synchronoss.cpo.core.meta.domain;
2   
3   /*-
4    * [[
5    * core
6    * ==
7    * Copyright (C) 2003 - 2026 Exaxis LLC, Synchronoss Technologies Inc
8    * ==
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   *
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Lesser Public License for more details.
18   *
19   * You should have received a copy of the GNU General Lesser Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
22   * ]]
23   */
24  
25  import java.lang.reflect.Constructor;
26  import java.lang.reflect.InvocationTargetException;
27  import java.lang.reflect.Method;
28  import java.util.ArrayList;
29  import java.util.List;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  import org.synchronoss.cpo.core.CpoData;
33  import org.synchronoss.cpo.core.CpoException;
34  import org.synchronoss.cpo.core.helper.CpoClassLoader;
35  import org.synchronoss.cpo.core.helper.ExceptionHelper;
36  import org.synchronoss.cpo.core.meta.CpoMetaDescriptor;
37  import org.synchronoss.cpo.core.meta.bean.CpoAttributeBean;
38  import org.synchronoss.cpo.core.transform.CpoTransform;
39  
40  /**
41   * Runtime metadata for a single JavaBean-to-datastore attribute binding: extends {@link
42   * CpoAttributeBean}'s plain configuration data with the reflectively-resolved getter/setter {@link
43   * Method}s and, if configured, the {@link CpoTransform} instance and its transform methods. {@link
44   * #loadRunTimeInfo(CpoMetaDescriptor, Class)} performs that resolution against a concrete bean
45   * class and must be called before the getter/setter/transform accessors are used.
46   *
47   * @author dberry
48   */
49  public class CpoAttribute extends CpoAttributeBean {
50  
51    private static final long serialVersionUID = 1L;
52  
53    private static final Logger logger = LoggerFactory.getLogger(CpoAttribute.class);
54    protected static final String TRANSFORM_IN_NAME = "transformIn";
55    protected static final String TRANSFORM_OUT_NAME = "transformOut";
56  
57    /** The resolved name of the bean getter method. */
58    private String getterName_ = null;
59  
60    /** The resolved name of the bean setter method. */
61    private String setterName_ = null;
62  
63    /** The reflectively-resolved bean getter method. */
64    private Method getter_ = null;
65  
66    /** The reflectively-resolved bean setter method. */
67    private Method setter_ = null;
68  
69    /** The native datastore type code for this attribute. */
70    private int dataTypeInt = Integer.MIN_VALUE;
71  
72    // Transform attributes
73    /** The configured transform instance, if any. */
74    private CpoTransform cpoTransform = null;
75  
76    /** The resolved {@link #TRANSFORM_IN_NAME} method on {@link #cpoTransform}, if any. */
77    private Method transformInMethod = null;
78  
79    /** The resolved {@link #TRANSFORM_OUT_NAME} method on {@link #cpoTransform}, if any. */
80    private Method transformOutMethod = null;
81  
82    // cached because Method.getParameterTypes() clones its array on every call
83    /** The parameter type of {@link #transformInMethod}, cached to avoid repeated reflection. */
84    private Class<?> transformInParamType = null;
85  
86    /** Creates an empty instance. Call {@link #loadRunTimeInfo} before use. */
87    public CpoAttribute() {}
88  
89    /**
90     * Gets the transform instance configured for this attribute, if any.
91     *
92     * @param <D> the transform's datastore-side type
93     * @param <J> the transform's Java-side type
94     * @return the configured transform, or {@code null} if none is configured
95     */
96    public <D, J> CpoTransform<D, J> getCpoTransform() {
97      return cpoTransform;
98    }
99  
100   /**
101    * Gets the resolved {@code transformIn} method of the configured transform.
102    *
103    * @return the {@code transformIn} method, or {@code null} if no transform is configured
104    */
105   public Method getTransformInMethod() {
106     return transformInMethod;
107   }
108 
109   /**
110    * Gets the resolved {@code transformOut} method of the configured transform.
111    *
112    * @return the {@code transformOut} method, or {@code null} if no transform is configured
113    */
114   public Method getTransformOutMethod() {
115     return transformOutMethod;
116   }
117 
118   /**
119    * Gets the parameter type of the configured transform's {@code transformIn} method.
120    *
121    * @return the {@code transformIn} parameter type, or {@code null} if no transform is configured
122    */
123   public Class<?> getTransformInParamType() {
124     return transformInParamType;
125   }
126 
127   /**
128    * Gets the parameter type expected by the bean's setter method.
129    *
130    * @return the setter's parameter type
131    */
132   public Class<?> getSetterParamType() {
133     return getter_.getReturnType();
134   }
135 
136   /**
137    * Gets the return type of the bean's getter method.
138    *
139    * @return the getter's return type
140    */
141   public Class<?> getGetterReturnType() {
142     return getter_.getReturnType();
143   }
144 
145   protected Method getGetter() {
146     return getter_;
147   }
148 
149   protected Method getSetter() {
150     return setter_;
151   }
152 
153   protected void setGetter(Method getter) {
154     getter_ = getter;
155   }
156 
157   protected void setSetter(Method setter) {
158     setter_ = setter;
159   }
160 
161   protected String getGetterName() {
162     return getterName_;
163   }
164 
165   protected String getSetterName() {
166     return setterName_;
167   }
168 
169   protected void setGetterName(String getterName) {
170     getterName_ = getterName;
171   }
172 
173   protected void setSetterName(String setterName) {
174     setterName_ = setterName;
175   }
176 
177   /**
178    * Sets the integer identifier of this attribute's native data type.
179    *
180    * @param dataTypeInt the native data type identifier
181    */
182   public void setDataTypeInt(int dataTypeInt) {
183     this.dataTypeInt = dataTypeInt;
184   }
185 
186   /**
187    * Gets the integer identifier of this attribute's native data type.
188    *
189    * @return the native data type identifier
190    */
191   public int getDataTypeInt() {
192     return this.dataTypeInt;
193   }
194 
195   protected List<Method> findMethods(Class<?> clazz, String methodName, int args, boolean hasReturn)
196       throws CpoException {
197     List<Method> retMethods = new ArrayList<>();
198 
199     try {
200       Method[] methods = clazz.getMethods();
201 
202       // go through once and find the accessor methods that match the method name
203       for (Method m : methods) {
204         // The method name must match as well as the number of parameters and return types
205         if (!m.isSynthetic()
206             && !m.isBridge()
207             && m.getName().equals(methodName)
208             && m.getParameterTypes().length == args
209             && ((!hasReturn && m.getReturnType() == Void.TYPE)
210                 || (hasReturn && m.getReturnType() != Void.TYPE))) {
211           retMethods.add(m);
212         }
213       }
214     } catch (SecurityException e) {
215       throw new CpoException("findMethod() Failed: " + methodName);
216     }
217     return retMethods;
218   }
219 
220   protected String buildMethodName(String prefix, String base) {
221 
222     StringBuilder methodName = new StringBuilder();
223     methodName.append(prefix);
224     methodName.append(base);
225     methodName.setCharAt(3, Character.toUpperCase(methodName.charAt(3)));
226 
227     return methodName.toString();
228   }
229 
230   /**
231    * Invokes the bean's setter method, passing the value read from {@code cpoData}.
232    *
233    * @param instanceObject the bean instance to set the value on
234    * @param cpoData the source of the value, via {@link CpoData#invokeGetter()}
235    * @throws CpoException if the setter cannot be invoked
236    */
237   public void invokeSetter(Object instanceObject, CpoData cpoData) throws CpoException {
238     try {
239       setter_.invoke(instanceObject, cpoData.invokeGetter());
240     } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException e) {
241       Logger localLogger =
242           instanceObject == null ? logger : LoggerFactory.getLogger(instanceObject.getClass());
243       String msg =
244           "Error Invoking Setter Method: "
245               + getDataName()
246               + ":"
247               + getJavaName()
248               + ":"
249               + setterName_;
250       localLogger.debug(msg + ExceptionHelper.getLocalizedMessage(e));
251       throw new CpoException(msg, e);
252     }
253   }
254 
255   /**
256    * Invokes the bean's getter method to read this attribute's value.
257    *
258    * @param obj the bean instance to read the value from
259    * @return the value returned by the bean's getter
260    * @throws CpoException if the getter cannot be invoked
261    */
262   public Object invokeGetter(Object obj) throws CpoException {
263     try {
264       return getGetter().invoke(obj, (Object[]) null);
265     } catch (IllegalAccessException | InvocationTargetException e) {
266       Logger localLogger = LoggerFactory.getLogger(obj.getClass());
267       String msg = "invokeGetter: Could not invoke getter for " + obj.getClass();
268       localLogger.debug(msg);
269       throw new CpoException(msg, e);
270     }
271   }
272 
273   private void dumpMethod(Method m) {
274     logger.trace("========================");
275     logger.trace("===> Declaring Class: " + m.getDeclaringClass().getName());
276     logger.trace("===> Method Signature: " + m.toString());
277     logger.trace("===> Generic Signature: " + m.toGenericString());
278     logger.trace("===> Method isBridge: " + m.isBridge());
279     logger.trace("===> Method isSynthetic: " + m.isSynthetic());
280     logger.trace("========================");
281   }
282 
283   /**
284    * Gets whether one of {@code clazz}/{@code paramClass} is a primitive type and the other is its
285    * corresponding wrapper class (e.g. {@code int}/{@code Integer}), determined heuristically by
286    * name and by checking for a wrapper constructor that accepts the primitive.
287    *
288    * @param clazz one of the two classes being compared
289    * @param paramClass the other class being compared
290    * @return {@code true} if exactly one of the two is primitive and it is assignable to/from the
291    *     other via boxing, {@code false} otherwise
292    */
293   public static boolean isPrimitiveAssignableFrom(Class<?> clazz, Class<?> paramClass) {
294 
295     // check to see if one is primitive and one is a possible wrapper
296     if (clazz.isPrimitive() ^ paramClass.isPrimitive()) {
297       // identify the prim and the wrapper
298       Class<?> primClass, objClass;
299       if (clazz.isPrimitive()) {
300         primClass = clazz;
301         objClass = paramClass;
302       } else {
303         primClass = paramClass;
304         objClass = clazz;
305       }
306 
307       // Lets do a quick name check
308       if (objClass.getSimpleName().toLowerCase().startsWith(primClass.getSimpleName())) {
309         // go through the constructors of the wrapper to see if there one with a parameter type
310         // that is the same as the primitive
311         for (Constructor<?> ctor : objClass.getConstructors()) {
312           Class<?>[] types = ctor.getParameterTypes();
313           if (types.length > 0 && types[0].isAssignableFrom(primClass)) {
314             return true;
315           }
316         }
317       } else {
318         logger.debug(
319             "Wrapper Class:"
320                 + objClass.getName().toLowerCase()
321                 + "does not start with "
322                 + primClass.getName());
323       }
324     }
325 
326     return false;
327   }
328 
329   /**
330    * Resolves this attribute's reflective getter/setter methods against {@code metaClass}, and
331    * instantiates and resolves its configured transform (if any) via {@code metaDescriptor}. Must be
332    * called before {@link #invokeGetter(Object)}, {@link #invokeSetter(Object, CpoData)}, or any of
333    * the getter/setter/transform accessor methods are used.
334    *
335    * @param metaDescriptor the owning descriptor, used to instantiate the transform class
336    * @param metaClass the concrete bean class to resolve the getter/setter against, or {@code null}
337    *     to skip getter/setter resolution
338    * @throws CpoException if the transform cannot be instantiated, or the getter/setter cannot be
339    *     found
340    */
341   public void loadRunTimeInfo(CpoMetaDescriptor metaDescriptor, Class<?> metaClass)
342       throws CpoException {
343     StringBuilder failedMessage = new StringBuilder();
344     setGetterName(buildMethodName("get", getJavaName()));
345     setSetterName(buildMethodName("set", getJavaName()));
346 
347     try {
348       initTransformClass(metaDescriptor);
349     } catch (Exception ce2) {
350       failedMessage.append(ce2.getMessage());
351     }
352     if (metaClass != null) {
353       try {
354         List<Method> methods = findMethods(metaClass, getGetterName(), 0, true);
355         if (methods.isEmpty()) {
356           failedMessage.append(
357               "loadRunTimeInfo: Could not find a Getter:"
358                   + getGetterName()
359                   + "("
360                   + metaClass.getName()
361                   + ")");
362         } else {
363           setGetter(methods.get(0));
364           dumpMethod(getGetter());
365         }
366       } catch (CpoException ce1) {
367         failedMessage.append(ce1.getMessage());
368       }
369       try {
370         Class<?> actualClass = getGetterReturnType();
371 
372         for (Method setter : findMethods(metaClass, getSetterName(), 1, false)) {
373           if (setter.getParameterTypes()[0].isAssignableFrom(actualClass)
374               || isPrimitiveAssignableFrom(setter.getParameterTypes()[0], actualClass)) {
375             setSetter(setter);
376             dumpMethod(getSetter());
377           }
378         }
379         if (getSetter() == null) {
380           failedMessage.append(
381               "loadRunTimeInfo: Could not find a Setter:"
382                   + getSetterName()
383                   + "("
384                   + actualClass.getName()
385                   + ")");
386         }
387       } catch (Exception ce2) {
388         failedMessage.append(ce2.getMessage());
389       }
390     }
391     if (!failedMessage.isEmpty()) {
392       throw new CpoException(failedMessage.toString());
393     }
394   }
395 
396   protected void initTransformClass(CpoMetaDescriptor metaDescriptor) throws CpoException {
397     String className = getTransformClassName();
398     Class<?> transformClass;
399 
400     if (className != null && !className.isEmpty()) {
401       try {
402         transformClass = CpoClassLoader.forName(className);
403       } catch (ClassNotFoundException e) {
404         String msg = "Invalid Transform Class specified:<" + className + ">";
405         throw new CpoException(msg, e);
406       }
407 
408       Object transformObject;
409       try {
410         transformObject = transformClass.getDeclaredConstructor().newInstance();
411       } catch (InstantiationException
412           | IllegalAccessException
413           | InvocationTargetException
414           | NoSuchMethodException e) {
415         String msg = "Error Setting Transform Class: ";
416         throw new CpoException(msg, e);
417       }
418 
419       if (transformObject instanceof CpoTransform) {
420         cpoTransform = (CpoTransform) transformObject;
421         List<Method> methods = findMethods(transformClass, TRANSFORM_IN_NAME, 1, true);
422         if (methods.size() > 0) {
423           transformInMethod = methods.get(0);
424           transformInParamType = transformInMethod.getParameterTypes()[0];
425         }
426         methods = findMethods(transformClass, TRANSFORM_OUT_NAME, 1, true);
427         if (methods.size() > 0) {
428           transformOutMethod = methods.get(0);
429         }
430       } else {
431         throw new CpoException("Invalid CpoTransform Class specified:<" + className + ">");
432       }
433     }
434   }
435 
436   @Override
437   public String toString() {
438     return this.getJavaName();
439   }
440 
441   /**
442    * Gets the full field-by-field string representation of this attribute, as produced by {@link
443    * CpoAttributeBean#toString()}.
444    *
445    * @return the full string representation
446    */
447   public String toStringFull() {
448     return super.toString();
449   }
450 }