DataObjectFactory.java

/*
** Module   : DataObjectFactory.java
** Abstract : A factory which is responsible for creating data objects (DMOs and DTOs). 
**
** Copyright (c) 2022-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 OM  20220331 First revision. Supports instantiating DMOs based on their interface and DTOs which
**                  extends DMO.
**     TJD 20220504 Upgrade do Java 11 minor changes
** 002 SBI 20230223 Fixed createDTO.
**         20230302 Implemented ProperiesDescriptor for created DTO objects.
** 003 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 004 SBI 20230627 Generalized createDTO to extend its field of usages, added marker interfaces for generated DTO. 
** 005 TJD 20240123 Java 17 compatibility update
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
**
**   0. Attribution Requirement.
**
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
**
**   1. No License To Use Trademarks.
**
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
**
**   2. No Misrepresentation of Affiliation.
**
**     You may not represent yourself as Golden Code Development Corporation or FWD.
**
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
**
**   3. No Misrepresentation of Source or Origin.
**
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.persist.orm;

import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.proxy.*;

import java.lang.reflect.*;
import java.util.*;
import java.util.logging.Level;

/**
 * A factory utility which creates two kind of proxies:
 * <ul>
 *    <li>DMOs: based on the interface received, and instance of it is created and returned. There are
 *              multiple overloaded methods which provide different level of customizations of the object to
 *              be returned;
 *    <li>DTOs which extends DMOs and use one instance to delegate the unimplemented methods to them. In this
 *             case, a proxy is created and returned. The methods declared by the DTO abstract class will be
 *             invoked directly while the methods inherited from the DMO interface are delegated to the DMO
 *             instance provided.
 * </ul>
 */
public class DataObjectFactory
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(DataObjectFactory.class);
   
   private Session session;
   
   public static DataObjectFactory getInstance(Session session)
   {
      DataObjectFactory ret = new DataObjectFactory();
      ret.session = session;
      return ret;
   }
   
   /**
    * Creates a new instance of a DMO. The returned object is NOT initialized and will not have a private key
    * set. That means the object can be used only as a POJO container for standard Java operations. Also, the
    * caller is responsible for filling ALL fields before using the object.
    *
    * @param   <T>
    *          The generic type of the object to be created. Inferred from {@code clazz} parameter.
    * @param   clazz
    *          The interface of the DMO object to be created. 
    *
    * @return  A new object of the class implementing the specified DMO interface.
    */
   public <T extends DataModelObject> T createDMO(Class<T> clazz)
   {
      return createDMO(clazz, false, false, null);
   }
   
   /**
    * Creates a new instance of a DMO. The returned object is NOT initialized. That means the object can be
    * used only as a POJO container for standard Java operations. Also, the caller is responsible for filling
    * ALL fields before using the object.
    *
    * @param   <T>
    *          The generic type of the object to be created. Inferred from {@code clazz} parameter.
    * @param   clazz
    *          The interface of the DMO object to be created. 
    * @param   pk
    *          If not {@code null}, the value is used as private key for the newly created record.
    *
    * @return  A new object of the class implementing the specified DMO interface.
    */
   public <T extends DataModelObject> T createDMO(Class<T> clazz, Long pk)
   {
      return createDMO(clazz, false, false, pk);
   }
   
   /**
    * Creates a new instance of a DMO.
    *
    * @param   <T>
    *          The generic type of the object to be created. Inferred from {@code clazz} parameter.
    * @param   clazz
    *          The interface of the DMO object to be created. 
    * @param   init
    *          If {@code true}, the returned object is initialized (registered with the {@code session},
    *          a change set and internal state are prepared) for complex usage of the object. <br>
    *          Otherwise, the object is to be used as a POJO container instead.
    * @param   defs
    *          Valid only if {@code init} is true. Sets the default values for fields as the record was
    *          defined with INITIAL value in table definition. <br>
    *          Otherwise the caller is responsible for filling ALL fields before using the object.
    * @param   pk
    *          If not {@code null}, the value is used as private key for the newly created record.
    *
    * @return  A new object of the class implementing the specified DMO interface.
    */
   @SuppressWarnings(value="unchecked")
   public <T extends DataModelObject> T createDMO(Class<T> clazz, boolean init, boolean defs, Long pk)
   {
      DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(clazz);
      
      Record record = null;
      try
      {
         record = dmoInfo.getImplementationClass().getDeclaredConstructor().newInstance();
      }
      catch (ReflectiveOperationException e)
      {
         LOG.severe("", e);
         return null;
      }
      
      if (init)
      {
         record.initialize(session, defs);
      }
      
      if (pk != null)
      {
         record.primaryKey(pk);
      }
   
      return (T) record;
   }
   
   /**
    * Creates a new instance of a DTO which implements a DMO interface in order to reuse the code of its
    * implementation. The returned objects wraps the DMO object and delegates the specific calls to it while
    * directly accessing the method the DTO declares and implements directly. 
    *
    * @param   clazz
    *          The DTO type of the object to be returned.
    * @param   dmo
    *          An instance of the DMO interface to which the calls to 'super' class will be delegated. 
    * @param   markerInterfaces
    *          The marker interfaces, can be null.
    * @param   <T>
    *          The DTO class of the object to be returned.
    *
    * @return  A proxy which 'responds' to all calls from both the abstract class and the delegated object.
    */
   public static <T> T createDTO(Class<T> clazz, Object dmo, Class<?>[] markerInterfaces)
   {
      DtoInvocationHandler handler = new DtoInvocationHandler(dmo, clazz);
      Class<?> dmoClass = dmo.getClass(); 
      Class<?>[] interfaces = dmoClass.getInterfaces();
      int countInterfaces = interfaces.length;
      int additionalInterfaces = 0;
      if (markerInterfaces != null)
      {
         additionalInterfaces = markerInterfaces.length;
      }
      Class<?>[] extendedInterfaces = new Class<?>[countInterfaces + additionalInterfaces + 1];
      System.arraycopy(interfaces, 0, extendedInterfaces, 0, countInterfaces);
      extendedInterfaces[countInterfaces] = PropertiesDescriptor.class;
      if (additionalInterfaces > 0)
      {
         System.arraycopy(markerInterfaces, 0, extendedInterfaces, countInterfaces + 1, additionalInterfaces);
      }
      T proxiedObject = ProxyFactory.getProxy(clazz, false, extendedInterfaces, true, null, null, handler);
      handler.setProxiedObject(proxiedObject);
      
      return proxiedObject;
   }
   
   /**
    * Creates a new instance of a DTO which implements a DMO interface in order to reuse the code of its
    * implementation. The returned objects wraps the DMO object and delegates the specific calls to it while
    * directly accessing the method the DTO declares and implements directly. 
    *
    * @param   clazz
    *          The DTO type of the object to be returned.
    * @param   dmo
    *          An instance of the DMO interface to which the calls to 'super' class will be delegated. 
    * @param   <T>
    *          The DTO class of the object to be returned.
    *
    * @return  A proxy which 'responds' to all calls from both the abstract class and the delegated object.
    */
   public static <T> T createDTO(Class<T> clazz, Object dmo)
   {
      return createDTO(clazz, dmo, null);
   }
   
   /**
    * The handler which delegates the calls unimplemented method in a DTO abstract class to a DMO instance
    * it receives as parameter.
    */
   private static class DtoInvocationHandler
   implements InvocationHandler,
              PropertiesDescriptor
   {
      /** The DMO which will handle unimplemented calls of the proxy object. */
      private final Object delegate;
      
      /** The class representing abstract DTO class */
      private final Class<?> dtoClass;
      
      /** The object created from DTO abstract class and DMO instance*/
      private Object dtoObject;
      
      /**
       * The constructor. Saves the delegate as a constant member.
       * 
       * @param   delegate
       *          The delegate DMO.
       * @param   dtoClass
       *          The object created from DTO abstract class and DMO instance
       */
      public DtoInvocationHandler(Object delegate, Class<?> dtoClass)
      {
         this.delegate = delegate;
         this.dtoClass = dtoClass;
      }
   
      /**
       * Processes a method invocation on a proxy instance and returns the result. This method will be
       * invoked on an invocation handler when a method is invoked on a proxy instance that it is associated
       * with.
       *  
       * @param   proxy
       *          the proxy instance that the method was invoked on.
       * @param   method
       *          the Method instance corresponding to the interface method invoked on the proxy instance. The
       *          proxy is constructed so that this method should be relayed to the {@code delegate} instead.
       * @param   args
       *          an array of objects containing the values of the arguments passed in the method invocation
       *          on the proxy instance, or null if interface method takes no arguments.
       *
       * @return  the value obtained from invocation of the method on DMO, if any.
       * 
       * @throws  Throwable
       *          the exception to throw from the method invocation on the DMO instance.
       */
      @Override
      public Object invoke(Object proxy, Method method, Object[] args)
      throws Throwable
      {
         if ("getPropertyValues".equals(method.getName()))
         {
            return getPropertyValues();
         }
         else
         {
            return method.invoke(delegate, args);
         }
      }

      /**
       * Get all the properties of this compound object by collecting all properties of the nested object,
       * an instance of dmo interface and the created proxy object, an instance of the dto abstract class.
       * 
       * @return   The map of properties to their values.
       */
      @Override
      public Map<String, Object> getPropertyValues()
      {
         Map<String, Object> valuesMap = new LinkedHashMap<String, Object>();
         if (delegate instanceof PropertiesDescriptor)
         {
            valuesMap.putAll(((PropertiesDescriptor) delegate).getPropertyValues());
         }
         else if (delegate != null)
         {
            setProperties(valuesMap, delegate, delegate.getClass());
         }
         
         if (dtoObject != null)
         {
            setProperties(valuesMap, dtoObject, dtoClass);
         }
         return valuesMap;
      }

      /**
       * Set the reference to the object created from DTO abstract class and DMO instance
       * 
       * @param    dtoObject
       *           The object created from DTO abstract class and DMO instance
       */
      public void setProxiedObject(Object dtoObject)
      {
         this.dtoObject = dtoObject;
      }
      
      /**
       * Fills the given properties map by expecting the target object properties implemented by the given type.
       * 
       * @param    valuesMap
       *           The given properties map
       * @param    obj
       *           The target object
       * @param    objClass
       *           The given type
       */
      @SuppressWarnings("deprecation")
      private static void setProperties(Map<String, Object> valuesMap, Object obj, Class<?> objClass)
      {
         Field[] fields = objClass.getDeclaredFields();
         
         for (Field field : fields)
         {
            try
            {
// Field.canAccess is not available in JDK 1.8, so until it has to be supported it needs to stay here
// in JDK 9+ it should be replaced with:
//             if (!field.canAccess(obj))
               if (!field.isAccessible())
               {
                  field.setAccessible(true);
               }
               valuesMap.put(field.getName(), field.get(obj));
            }
            catch (IllegalArgumentException | IllegalAccessException ex)
            {
               LOG.log(Level.SEVERE, "Failed to get value of the field '" + field.getName() + "'", ex);
            }
         }

      }
   }
}