ResultLoaderMap.java

  1. /*
  2.  *    Copyright 2009-2024 the original author or authors.
  3.  *
  4.  *    Licensed under the Apache License, Version 2.0 (the "License");
  5.  *    you may not use this file except in compliance with the License.
  6.  *    You may obtain a copy of the License at
  7.  *
  8.  *       https://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  *    Unless required by applicable law or agreed to in writing, software
  11.  *    distributed under the License is distributed on an "AS IS" BASIS,
  12.  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  *    See the License for the specific language governing permissions and
  14.  *    limitations under the License.
  15.  */
  16. package org.apache.ibatis.executor.loader;

  17. import java.io.Serializable;
  18. import java.lang.reflect.Method;
  19. import java.lang.reflect.Modifier;
  20. import java.security.AccessController;
  21. import java.security.PrivilegedActionException;
  22. import java.security.PrivilegedExceptionAction;
  23. import java.sql.SQLException;
  24. import java.util.HashMap;
  25. import java.util.List;
  26. import java.util.Locale;
  27. import java.util.Map;
  28. import java.util.Set;

  29. import org.apache.ibatis.cursor.Cursor;
  30. import org.apache.ibatis.executor.BaseExecutor;
  31. import org.apache.ibatis.executor.BatchResult;
  32. import org.apache.ibatis.executor.ExecutorException;
  33. import org.apache.ibatis.logging.Log;
  34. import org.apache.ibatis.logging.LogFactory;
  35. import org.apache.ibatis.mapping.BoundSql;
  36. import org.apache.ibatis.mapping.MappedStatement;
  37. import org.apache.ibatis.reflection.MetaObject;
  38. import org.apache.ibatis.session.Configuration;
  39. import org.apache.ibatis.session.ResultHandler;
  40. import org.apache.ibatis.session.RowBounds;

  41. /**
  42.  * @author Clinton Begin
  43.  * @author Franta Mejta
  44.  */
  45. public class ResultLoaderMap {

  46.   private final Map<String, LoadPair> loaderMap = new HashMap<>();

  47.   public void addLoader(String property, MetaObject metaResultObject, ResultLoader resultLoader) {
  48.     String upperFirst = getUppercaseFirstProperty(property);
  49.     if (!upperFirst.equalsIgnoreCase(property) && loaderMap.containsKey(upperFirst)) {
  50.       throw new ExecutorException("Nested lazy loaded result property '" + property + "' for query id '"
  51.           + resultLoader.mappedStatement.getId()
  52.           + " already exists in the result map. The leftmost property of all lazy loaded properties must be unique within a result map.");
  53.     }
  54.     loaderMap.put(upperFirst, new LoadPair(property, metaResultObject, resultLoader));
  55.   }

  56.   public final Map<String, LoadPair> getProperties() {
  57.     return new HashMap<>(this.loaderMap);
  58.   }

  59.   public Set<String> getPropertyNames() {
  60.     return loaderMap.keySet();
  61.   }

  62.   public int size() {
  63.     return loaderMap.size();
  64.   }

  65.   public boolean isEmpty() {
  66.     return loaderMap.isEmpty();
  67.   }

  68.   public boolean hasLoader(String property) {
  69.     return loaderMap.containsKey(property.toUpperCase(Locale.ENGLISH));
  70.   }

  71.   public boolean load(String property) throws SQLException {
  72.     LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
  73.     if (pair != null) {
  74.       pair.load();
  75.       return true;
  76.     }
  77.     return false;
  78.   }

  79.   public void remove(String property) {
  80.     loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
  81.   }

  82.   public void loadAll() throws SQLException {
  83.     final Set<String> methodNameSet = loaderMap.keySet();
  84.     String[] methodNames = methodNameSet.toArray(new String[methodNameSet.size()]);
  85.     for (String methodName : methodNames) {
  86.       load(methodName);
  87.     }
  88.   }

  89.   private static String getUppercaseFirstProperty(String property) {
  90.     String[] parts = property.split("\\.");
  91.     return parts[0].toUpperCase(Locale.ENGLISH);
  92.   }

  93.   /**
  94.    * Property which was not loaded yet.
  95.    */
  96.   public static class LoadPair implements Serializable {

  97.     private static final long serialVersionUID = 20130412;
  98.     /**
  99.      * Name of factory method which returns database connection.
  100.      */
  101.     private static final String FACTORY_METHOD = "getConfiguration";
  102.     /**
  103.      * Object to check whether we went through serialization..
  104.      */
  105.     private final transient Object serializationCheck = new Object();
  106.     /**
  107.      * Meta object which sets loaded properties.
  108.      */
  109.     private transient MetaObject metaResultObject;
  110.     /**
  111.      * Result loader which loads unread properties.
  112.      */
  113.     private transient ResultLoader resultLoader;
  114.     /**
  115.      * Wow, logger.
  116.      */
  117.     private transient Log log;
  118.     /**
  119.      * Factory class through which we get database connection.
  120.      */
  121.     private Class<?> configurationFactory;
  122.     /**
  123.      * Name of the unread property.
  124.      */
  125.     private final String property;
  126.     /**
  127.      * ID of SQL statement which loads the property.
  128.      */
  129.     private String mappedStatement;
  130.     /**
  131.      * Parameter of the sql statement.
  132.      */
  133.     private Serializable mappedParameter;

  134.     private LoadPair(final String property, MetaObject metaResultObject, ResultLoader resultLoader) {
  135.       this.property = property;
  136.       this.metaResultObject = metaResultObject;
  137.       this.resultLoader = resultLoader;

  138.       /* Save required information only if original object can be serialized. */
  139.       if (metaResultObject != null && metaResultObject.getOriginalObject() instanceof Serializable) {
  140.         final Object mappedStatementParameter = resultLoader.parameterObject;

  141.         /* @todo May the parameter be null? */
  142.         if (mappedStatementParameter instanceof Serializable) {
  143.           this.mappedStatement = resultLoader.mappedStatement.getId();
  144.           this.mappedParameter = (Serializable) mappedStatementParameter;

  145.           this.configurationFactory = resultLoader.configuration.getConfigurationFactory();
  146.         } else {
  147.           Log log = this.getLogger();
  148.           if (log.isDebugEnabled()) {
  149.             log.debug("Property [" + this.property + "] of [" + metaResultObject.getOriginalObject().getClass()
  150.                 + "] cannot be loaded " + "after deserialization. Make sure it's loaded before serializing "
  151.                 + "forenamed object.");
  152.           }
  153.         }
  154.       }
  155.     }

  156.     public void load() throws SQLException {
  157.       /*
  158.        * These field should not be null unless the loadpair was serialized. Yet in that case this method should not be
  159.        * called.
  160.        */
  161.       if (this.metaResultObject == null) {
  162.         throw new IllegalArgumentException("metaResultObject is null");
  163.       }
  164.       if (this.resultLoader == null) {
  165.         throw new IllegalArgumentException("resultLoader is null");
  166.       }

  167.       this.load(null);
  168.     }

  169.     public void load(final Object userObject) throws SQLException {
  170.       if (this.metaResultObject == null || this.resultLoader == null) {
  171.         if (this.mappedParameter == null) {
  172.           throw new ExecutorException("Property [" + this.property + "] cannot be loaded because "
  173.               + "required parameter of mapped statement [" + this.mappedStatement + "] is not serializable.");
  174.         }

  175.         final Configuration config = this.getConfiguration();
  176.         final MappedStatement ms = config.getMappedStatement(this.mappedStatement);
  177.         if (ms == null) {
  178.           throw new ExecutorException(
  179.               "Cannot lazy load property [" + this.property + "] of deserialized object [" + userObject.getClass()
  180.                   + "] because configuration does not contain statement [" + this.mappedStatement + "]");
  181.         }

  182.         this.metaResultObject = config.newMetaObject(userObject);
  183.         this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,
  184.             metaResultObject.getSetterType(this.property), null, null);
  185.       }

  186.       /*
  187.        * We are using a new executor because we may be (and likely are) on a new thread and executors aren't thread
  188.        * safe. (Is this sufficient?) A better approach would be making executors thread safe.
  189.        */
  190.       if (this.serializationCheck == null) {
  191.         final ResultLoader old = this.resultLoader;
  192.         this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement,
  193.             old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
  194.       }

  195.       this.metaResultObject.setValue(property, this.resultLoader.loadResult());
  196.     }

  197.     private Configuration getConfiguration() {
  198.       if (this.configurationFactory == null) {
  199.         throw new ExecutorException("Cannot get Configuration as configuration factory was not set.");
  200.       }

  201.       Object configurationObject;
  202.       try {
  203.         final Method factoryMethod = this.configurationFactory.getDeclaredMethod(FACTORY_METHOD);
  204.         if (!Modifier.isStatic(factoryMethod.getModifiers())) {
  205.           throw new ExecutorException("Cannot get Configuration as factory method [" + this.configurationFactory + "]#["
  206.               + FACTORY_METHOD + "] is not static.");
  207.         }

  208.         if (!factoryMethod.isAccessible()) {
  209.           configurationObject = AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
  210.             try {
  211.               factoryMethod.setAccessible(true);
  212.               return factoryMethod.invoke(null);
  213.             } finally {
  214.               factoryMethod.setAccessible(false);
  215.             }
  216.           });
  217.         } else {
  218.           configurationObject = factoryMethod.invoke(null);
  219.         }
  220.       } catch (final ExecutorException ex) {
  221.         throw ex;
  222.       } catch (final NoSuchMethodException ex) {
  223.         throw new ExecutorException("Cannot get Configuration as factory class [" + this.configurationFactory
  224.             + "] is missing factory method of name [" + FACTORY_METHOD + "].", ex);
  225.       } catch (final PrivilegedActionException ex) {
  226.         throw new ExecutorException("Cannot get Configuration as factory method [" + this.configurationFactory + "]#["
  227.             + FACTORY_METHOD + "] threw an exception.", ex.getCause());
  228.       } catch (final Exception ex) {
  229.         throw new ExecutorException("Cannot get Configuration as factory method [" + this.configurationFactory + "]#["
  230.             + FACTORY_METHOD + "] threw an exception.", ex);
  231.       }

  232.       if (!(configurationObject instanceof Configuration)) {
  233.         throw new ExecutorException("Cannot get Configuration as factory method [" + this.configurationFactory + "]#["
  234.             + FACTORY_METHOD + "] didn't return [" + Configuration.class + "] but ["
  235.             + (configurationObject == null ? "null" : configurationObject.getClass()) + "].");
  236.       }

  237.       return Configuration.class.cast(configurationObject);
  238.     }

  239.     private Log getLogger() {
  240.       if (this.log == null) {
  241.         this.log = LogFactory.getLog(this.getClass());
  242.       }
  243.       return this.log;
  244.     }
  245.   }

  246.   private static final class ClosedExecutor extends BaseExecutor {

  247.     public ClosedExecutor() {
  248.       super(null, null);
  249.     }

  250.     @Override
  251.     public boolean isClosed() {
  252.       return true;
  253.     }

  254.     @Override
  255.     protected int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
  256.       throw new UnsupportedOperationException("Not supported.");
  257.     }

  258.     @Override
  259.     protected List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
  260.       throw new UnsupportedOperationException("Not supported.");
  261.     }

  262.     @Override
  263.     protected <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,
  264.         ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  265.       throw new UnsupportedOperationException("Not supported.");
  266.     }

  267.     @Override
  268.     protected <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
  269.         throws SQLException {
  270.       throw new UnsupportedOperationException("Not supported.");
  271.     }
  272.   }
  273. }