Package org.springframework.data.repository.core.support

Source Code of org.springframework.data.repository.core.support.RepositoryFactorySupport$DefaultMethodInvokingMethodInterceptor

/*
* Copyright 2008-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.repository.core.support;

import static org.springframework.util.ReflectionUtils.*;

import java.io.Serializable;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.ProxyMethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.DefaultEvaluationContextProvider;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.util.ClassUtils;
import org.springframework.data.repository.util.NullableWrapper;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

/**
* Factory bean to create instances of a given repository interface. Creates a proxy implementing the configured
* repository interface and apply an advice handing the control to the {@code QueryExecuterMethodInterceptor}. Query
* detection strategy can be configured by setting {@link QueryLookupStrategy.Key}.
*
* @author Oliver Gierke
*/
public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {

  private static final TypeDescriptor WRAPPER_TYPE = TypeDescriptor.valueOf(NullableWrapper.class);
  private static final boolean IS_JAVA_8 = org.springframework.util.ClassUtils.isPresent("java.util.Optional",
      RepositoryFactorySupport.class.getClassLoader());

  private final Map<RepositoryInformationCacheKey, RepositoryInformation> repositoryInformationCache = new HashMap<RepositoryInformationCacheKey, RepositoryInformation>();
  private final List<RepositoryProxyPostProcessor> postProcessors = new ArrayList<RepositoryProxyPostProcessor>();

  private QueryLookupStrategy.Key queryLookupStrategyKey;
  private List<QueryCreationListener<?>> queryPostProcessors = new ArrayList<QueryCreationListener<?>>();
  private NamedQueries namedQueries = PropertiesBasedNamedQueries.EMPTY;
  private ClassLoader classLoader = org.springframework.util.ClassUtils.getDefaultClassLoader();
  private EvaluationContextProvider evaluationContextProvider = DefaultEvaluationContextProvider.INSTANCE;

  private QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener();

  public RepositoryFactorySupport() {
    this.queryPostProcessors.add(collectingListener);
  }

  /**
   * Sets the strategy of how to lookup a query to execute finders.
   *
   * @param key
   */
  public void setQueryLookupStrategyKey(Key key) {
    this.queryLookupStrategyKey = key;
  }

  /**
   * Configures a {@link NamedQueries} instance to be handed to the {@link QueryLookupStrategy} for query creation.
   *
   * @param namedQueries the namedQueries to set
   */
  public void setNamedQueries(NamedQueries namedQueries) {
    this.namedQueries = namedQueries == null ? PropertiesBasedNamedQueries.EMPTY : namedQueries;
  }

  /*
   * (non-Javadoc)
   * @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader)
   */
  @Override
  public void setBeanClassLoader(ClassLoader classLoader) {
    this.classLoader = classLoader == null ? org.springframework.util.ClassUtils.getDefaultClassLoader() : classLoader;
  }

  /**
   * Sets the {@link EvaluationContextProvider} to be used to evaluate SpEL expressions in manually defined queries.
   *
   * @param evaluationContextProvider can be {@literal null}, defaults to
   *          {@link DefaultEvaluationContextProvider#INSTANCE}.
   */
  public void setEvaluationContextProvider(EvaluationContextProvider evaluationContextProvider) {
    this.evaluationContextProvider = evaluationContextProvider == null ? DefaultEvaluationContextProvider.INSTANCE
        : evaluationContextProvider;
  }

  /**
   * Adds a {@link QueryCreationListener} to the factory to plug in functionality triggered right after creation of
   * {@link RepositoryQuery} instances.
   *
   * @param listener
   */
  public void addQueryCreationListener(QueryCreationListener<?> listener) {

    Assert.notNull(listener);
    this.queryPostProcessors.add(listener);
  }

  /**
   * Adds {@link RepositoryProxyPostProcessor}s to the factory to allow manipulation of the {@link ProxyFactory} before
   * the proxy gets created. Note that the {@link QueryExecutorMethodInterceptor} will be added to the proxy
   * <em>after</em> the {@link RepositoryProxyPostProcessor}s are considered.
   *
   * @param processor
   */
  public void addRepositoryProxyPostProcessor(RepositoryProxyPostProcessor processor) {

    Assert.notNull(processor);
    this.postProcessors.add(processor);
  }

  /**
   * Returns a repository instance for the given interface.
   *
   * @param <T>
   * @param repositoryInterface
   * @return
   */
  public <T> T getRepository(Class<T> repositoryInterface) {
    return getRepository(repositoryInterface, null);
  }

  /**
   * Returns a repository instance for the given interface backed by an instance providing implementation logic for
   * custom logic.
   *
   * @param <T>
   * @param repositoryInterface
   * @param customImplementation
   * @return
   */
  @SuppressWarnings({ "unchecked" })
  public <T> T getRepository(Class<T> repositoryInterface, Object customImplementation) {

    RepositoryMetadata metadata = getRepositoryMetadata(repositoryInterface);
    Class<?> customImplementationClass = null == customImplementation ? null : customImplementation.getClass();
    RepositoryInformation information = getRepositoryInformation(metadata, customImplementationClass);

    validate(information, customImplementation);

    Object target = getTargetRepository(information);

    // Create proxy
    ProxyFactory result = new ProxyFactory();
    result.setTarget(target);
    result.setInterfaces(new Class[] { repositoryInterface, Repository.class });

    for (RepositoryProxyPostProcessor processor : postProcessors) {
      processor.postProcess(result, information);
    }

    if (IS_JAVA_8) {
      result.addAdvice(new DefaultMethodInvokingMethodInterceptor());
    }

    result.addAdvice(new QueryExecutorMethodInterceptor(information, customImplementation, target));

    return (T) result.getProxy(classLoader);
  }

  /**
   * Returns the {@link RepositoryMetadata} for the given repository interface.
   *
   * @param repositoryInterface
   * @return
   */
  RepositoryMetadata getRepositoryMetadata(Class<?> repositoryInterface) {
    return AbstractRepositoryMetadata.getMetadata(repositoryInterface);
  }

  /**
   * Returns the {@link RepositoryInformation} for the given repository interface.
   *
   * @param metadata
   * @param customImplementationClass
   * @return
   */
  protected RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata,
      Class<?> customImplementationClass) {

    RepositoryInformationCacheKey cacheKey = new RepositoryInformationCacheKey(metadata, customImplementationClass);
    RepositoryInformation repositoryInformation = repositoryInformationCache.get(cacheKey);

    if (repositoryInformation != null) {
      return repositoryInformation;
    }

    repositoryInformation = new DefaultRepositoryInformation(metadata, getRepositoryBaseClass(metadata),
        customImplementationClass);
    repositoryInformationCache.put(cacheKey, repositoryInformation);
    return repositoryInformation;
  }

  protected List<QueryMethod> getQueryMethods() {
    return collectingListener.getQueryMethods();
  }

  /**
   * Returns the {@link EntityInformation} for the given domain class.
   *
   * @param <T> the entity type
   * @param <ID> the id type
   * @param domainClass
   * @return
   */
  public abstract <T, ID extends Serializable> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass);

  /**
   * Create a repository instance as backing for the query proxy.
   *
   * @param metadata
   * @return
   */
  protected abstract Object getTargetRepository(RepositoryMetadata metadata);

  /**
   * Returns the base class backing the actual repository instance. Make sure
   * {@link #getTargetRepository(RepositoryMetadata)} returns an instance of this class.
   *
   * @param metadata
   * @return
   */
  protected abstract Class<?> getRepositoryBaseClass(RepositoryMetadata metadata);

  /**
   * Returns the {@link QueryLookupStrategy} for the given {@link Key}.
   *
   * @deprecated favor {@link #getQueryLookupStrategy(Key, EvaluationContextProvider)}
   * @param key can be {@literal null}
   * @return the {@link QueryLookupStrategy} to use or {@literal null} if no queries should be looked up.
   */
  protected QueryLookupStrategy getQueryLookupStrategy(Key key) {
    return null;
  }

  /**
   * Returns the {@link QueryLookupStrategy} for the given {@link Key} and {@link EvaluationContextProvider}.
   *
   * @param key can be {@literal null}.
   * @param evaluationContextProvider will never be {@literal null}.
   * @return the {@link QueryLookupStrategy} to use or {@literal null} if no queries should be looked up.
   * @since 1.9
   */
  protected QueryLookupStrategy getQueryLookupStrategy(Key key, EvaluationContextProvider evaluationContextProvider) {
    return null;
  }

  /**
   * Validates the given repository interface as well as the given custom implementation.
   *
   * @param repositoryInformation
   * @param customImplementation
   */
  private void validate(RepositoryInformation repositoryInformation, Object customImplementation) {

    if (null == customImplementation && repositoryInformation.hasCustomMethod()) {

      throw new IllegalArgumentException(String.format(
          "You have custom methods in %s but not provided a custom implementation!",
          repositoryInformation.getRepositoryInterface()));
    }

    validate(repositoryInformation);
  }

  protected void validate(RepositoryMetadata repositoryMetadata) {

  }

  /**
   * This {@code MethodInterceptor} intercepts calls to methods of the custom implementation and delegates the to it if
   * configured. Furthermore it resolves method calls to finders and triggers execution of them. You can rely on having
   * a custom repository implementation instance set if this returns true.
   *
   * @author Oliver Gierke
   */
  public class QueryExecutorMethodInterceptor implements MethodInterceptor {

    private final Map<Method, RepositoryQuery> queries = new ConcurrentHashMap<Method, RepositoryQuery>();

    private final Object customImplementation;
    private final RepositoryInformation repositoryInformation;
    private final GenericConversionService conversionService;
    private final Object target;

    /**
     * Creates a new {@link QueryExecutorMethodInterceptor}. Builds a model of {@link QueryMethod}s to be invoked on
     * execution of repository interface methods.
     */
    public QueryExecutorMethodInterceptor(RepositoryInformation repositoryInformation, Object customImplementation,
        Object target) {

      Assert.notNull(repositoryInformation, "RepositoryInformation must not be null!");
      Assert.notNull(target, "Target must not be null!");

      DefaultConversionService conversionService = new DefaultConversionService();
      QueryExecutionConverters.registerConvertersIn(conversionService);
      this.conversionService = conversionService;

      this.repositoryInformation = repositoryInformation;
      this.customImplementation = customImplementation;
      this.target = target;

      QueryLookupStrategy lookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey,
          RepositoryFactorySupport.this.evaluationContextProvider);
      lookupStrategy = lookupStrategy == null ? getQueryLookupStrategy(queryLookupStrategyKey) : lookupStrategy;
      Iterable<Method> queryMethods = repositoryInformation.getQueryMethods();

      if (lookupStrategy == null) {

        if (queryMethods.iterator().hasNext()) {
          throw new IllegalStateException("You have defined query method in the repository but "
              + "you don't have any query lookup strategy defined. The "
              + "infrastructure apparently does not support query methods!");
        }

        return;
      }

      for (Method method : queryMethods) {
        RepositoryQuery query = lookupStrategy.resolveQuery(method, repositoryInformation, namedQueries);
        invokeListeners(query);
        queries.put(method, query);
      }
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private void invokeListeners(RepositoryQuery query) {

      for (QueryCreationListener listener : queryPostProcessors) {
        Class<?> typeArgument = GenericTypeResolver.resolveTypeArgument(listener.getClass(),
            QueryCreationListener.class);
        if (typeArgument != null && typeArgument.isAssignableFrom(query.getClass())) {
          listener.onCreation(query);
        }
      }
    }

    /*
     * (non-Javadoc)
     * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
     */
    public Object invoke(MethodInvocation invocation) throws Throwable {

      Object result = doInvoke(invocation);

      Method method = invocation.getMethod();

      // Looking up the TypeDescriptor for the return type - yes, this way o.O
      MethodParameter parameter = new MethodParameter(method, -1);
      TypeDescriptor methodReturnTypeDescriptor = TypeDescriptor.nested(parameter, 0);

      Class<?> expectedReturnType = method.getReturnType();

      if (result != null && expectedReturnType.isInstance(result)) {
        return result;
      }

      if (QueryExecutionConverters.supports(expectedReturnType)
          && conversionService.canConvert(WRAPPER_TYPE, methodReturnTypeDescriptor)
          && !conversionService.canBypassConvert(WRAPPER_TYPE, TypeDescriptor.valueOf(expectedReturnType))) {
        return conversionService.convert(new NullableWrapper(result), expectedReturnType);
      }

      if (result == null) {
        return null;
      }

      return conversionService.canConvert(result.getClass(), expectedReturnType) ? conversionService.convert(result,
          expectedReturnType) : result;
    }

    private Object doInvoke(MethodInvocation invocation) throws Throwable {

      Method method = invocation.getMethod();
      Object[] arguments = invocation.getArguments();

      if (isCustomMethodInvocation(invocation)) {
        Method actualMethod = repositoryInformation.getTargetClassMethod(method);
        makeAccessible(actualMethod);
        return executeMethodOn(customImplementation, actualMethod, arguments);
      }

      if (hasQueryFor(method)) {
        return queries.get(method).execute(arguments);
      }

      // Lookup actual method as it might be redeclared in the interface
      // and we have to use the repository instance nevertheless
      Method actualMethod = repositoryInformation.getTargetClassMethod(method);
      return executeMethodOn(target, actualMethod, arguments);
    }

    /**
     * Executes the given method on the given target. Correctly unwraps exceptions not caused by the reflection magic.
     *
     * @param target
     * @param method
     * @param parameters
     * @return
     * @throws Throwable
     */
    private Object executeMethodOn(Object target, Method method, Object[] parameters) throws Throwable {

      try {
        return method.invoke(target, parameters);
      } catch (Exception e) {
        ClassUtils.unwrapReflectionException(e);
      }

      throw new IllegalStateException("Should not occur!");
    }

    /**
     * Returns whether we know of a query to execute for the given {@link Method};
     *
     * @param method
     * @return
     */
    private boolean hasQueryFor(Method method) {
      return queries.containsKey(method);
    }

    /**
     * Returns whether the given {@link MethodInvocation} is considered to be targeted as an invocation of a custom
     * method.
     *
     * @param method
     * @return
     */
    private boolean isCustomMethodInvocation(MethodInvocation invocation) {

      if (null == customImplementation) {
        return false;
      }

      return repositoryInformation.isCustomMethod(invocation.getMethod());
    }
  }

  /**
   * Method interceptor to invoke default methods on the repository proxy.
   *
   * @author Oliver Gierke
   */
  private static class DefaultMethodInvokingMethodInterceptor implements MethodInterceptor {

    private final Constructor<MethodHandles.Lookup> constructor;

    /**
     * Creates a new {@link DefaultMethodInvokingMethodInterceptor}.
     */
    public DefaultMethodInvokingMethodInterceptor() {

      try {
        this.constructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);

        if (!constructor.isAccessible()) {
          constructor.setAccessible(true);
        }
      } catch (Exception o_O) {
        throw new IllegalStateException(o_O);
      }
    }

    /*
     * (non-Javadoc)
     * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
     */
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {

      Method method = invocation.getMethod();

      if (!org.springframework.data.util.ReflectionUtils.isDefaultMethod(method)) {
        return invocation.proceed();
      }

      Object[] arguments = invocation.getArguments();
      Class<?> declaringClass = method.getDeclaringClass();
      Object proxy = ((ProxyMethodInvocation) invocation).getProxy();

      return constructor.newInstance(declaringClass, MethodHandles.Lookup.PRIVATE)
          .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(arguments);
    }
  }

  /**
   * {@link QueryCreationListener} collecting the {@link QueryMethod}s created for all query methods of the repository
   * interface.
   *
   * @author Oliver Gierke
   */
  private static class QueryCollectingQueryCreationListener implements QueryCreationListener<RepositoryQuery> {

    private List<QueryMethod> queryMethods = new ArrayList<QueryMethod>();

    /**
     * Returns all {@link QueryMethod}s.
     *
     * @return
     */
    public List<QueryMethod> getQueryMethods() {
      return queryMethods;
    }

    /* (non-Javadoc)
     * @see org.springframework.data.repository.core.support.QueryCreationListener#onCreation(org.springframework.data.repository.query.RepositoryQuery)
     */
    public void onCreation(RepositoryQuery query) {
      this.queryMethods.add(query.getQueryMethod());
    }
  }

  /**
   * Simple value object to build up keys to cache {@link RepositoryInformation} instances.
   *
   * @author Oliver Gierke
   */
  private static class RepositoryInformationCacheKey {

    private final String repositoryInterfaceName;
    private final String customImplementationClassName;

    /**
     * Creates a new {@link RepositoryInformationCacheKey} for the given {@link RepositoryMetadata} and cuytom
     * implementation type.
     *
     * @param repositoryInterfaceName must not be {@literal null}.
     * @param customImplementationClassName
     */
    public RepositoryInformationCacheKey(RepositoryMetadata metadata, Class<?> customImplementationType) {
      this.repositoryInterfaceName = metadata.getRepositoryInterface().getName();
      this.customImplementationClassName = customImplementationType == null ? null : customImplementationType.getName();
    }

    /*
     * (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {

      if (!(obj instanceof RepositoryInformationCacheKey)) {
        return false;
      }

      RepositoryInformationCacheKey that = (RepositoryInformationCacheKey) obj;
      return this.repositoryInterfaceName.equals(that.repositoryInterfaceName)
          && ObjectUtils.nullSafeEquals(this.customImplementationClassName, that.customImplementationClassName);
    }

    /*
     * (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {

      int result = 31;

      result += 17 * repositoryInterfaceName.hashCode();
      result += 17 * ObjectUtils.nullSafeHashCode(customImplementationClassName);

      return result;
    }
  }
}
TOP

Related Classes of org.springframework.data.repository.core.support.RepositoryFactorySupport$DefaultMethodInvokingMethodInterceptor

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.