QuerydslPredicateArgumentResolverBeanPostProcessor.java

/*******************************************************************************
 * Copyright (c) 2018 @gt_tech
 *
 * 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.bitbucket.gt_tech.spring.data.querydsl.value.operators.experimental;

import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.bitbucket.gt_tech.spring.data.querydsl.value.operators.ExpressionProviderFactory;
import org.bitbucket.gt_tech.spring.data.querydsl.value.operators.Operator;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.querydsl.binding.QuerydslBindingsFactory;
import org.springframework.data.web.querydsl.QuerydslPredicateArgumentResolver;

import java.sql.Timestamp;
import java.time.LocalDate;
import java.util.Date;
import java.util.Optional;

/**
 * Advanced experimental feature of this component - an implementation of
 * {@link BeanPostProcessor} that overrides the default
 * {@link QuerydslPredicateArgumentResolver} by providing it a no-op
 * {@link ConversionService} and in-turn disabling it's strong type-conversion.
 * This allows for String values decorated with value-operators by client to
 * reach expression-provided even for non StringPath.
 *
 * <p>
 *     Note that by providing a delegate ConversionService and explicit Class
 *     types for delegated conversions, a high degree of control can be achieved
 *     when users are using direct bindings for certain fields (for e.g. Date)
 * </p>
 * 
 * <p>
 * If this isn't available, then the {@link QuerydslPredicateArgumentResolver}
 * will attempt to perform type-conversion which will fail for non-StringPath
 * (for. e.g. EnumPath) when values are decorated with value-operators -
 * {@link Operator}
 * </p>
 * 
 * @author gt_tech
 *
 */
public class QuerydslPredicateArgumentResolverBeanPostProcessor implements BeanPostProcessor {

	private final QuerydslBindingsFactory querydslBindingsFactory;

	private final ConversionService conversionServiceDelegate;

	private final Class[] delegatedConversions;

	/*
	 * No-op conversion service
	 */
	private final ConversionService delegationAwareConversionService = new ConversionService() {

		@Override
		public boolean canConvert(Class<?> sourceType, Class<?> targetType) {
			if ( isDelegatedConversion(sourceType) || isDelegatedConversion(targetType))
				return conversionServiceDelegate.canConvert(sourceType, targetType);

			return false;
		}

		@Override
		public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
			if ( isDelegatedConversion(sourceType.getType()) || isDelegatedConversion(targetType.getType()))
				return conversionServiceDelegate.canConvert(sourceType, targetType);

			return false;
		}

		@Override
		public <T> T convert(Object source, Class<T> targetType) {

			if ( isDelegatedConversion(source.getClass()) || isDelegatedConversion(targetType))
				return conversionServiceDelegate.convert(source, targetType);

			throw new UnsupportedOperationException("Overridden ConversionService in "
					+ "QuerydslPredicateArgumentResolver does not " + "support conversion");
		}

		@Override
		public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {

			if ( isDelegatedConversion(sourceType.getType()) || isDelegatedConversion(targetType.getType()))
				return conversionServiceDelegate.convert(source, sourceType, targetType);

			throw new UnsupportedOperationException("Overridden ConversionService in "
					+ "QuerydslPredicateArgumentResolver does not " + "support conversion");
		}

		private boolean isDelegatedConversion(Class<?> type) {
			boolean result = false;

			if ( conversionServiceDelegate != null && type != null && delegatedConversions != null ) {
				for (Class c : delegatedConversions) {
					if (c.equals(type)) {
						result = true;
						break;
					}
				}
			}
			return result;
		}
	};

	/**
	 * Constructor: Replaces {@link QuerydslPredicateArgumentResolver} with a no-op conversion service
	 * @param querydslBindingsFactory
	 */
	public QuerydslPredicateArgumentResolverBeanPostProcessor(QuerydslBindingsFactory querydslBindingsFactory) {
		this(querydslBindingsFactory, null, new Class[]{});
	}


	/**
	 * Constructor: Replaces {@link QuerydslPredicateArgumentResolver} with a no-op conversion service with the exception of following types
	 * conversion that would be handed over to provided delegated service - {@link Date}, {@link LocalDate},
	 * {@link Timestamp}
	 *
	 * @param querydslBindingsFactory
	 */
	public QuerydslPredicateArgumentResolverBeanPostProcessor(QuerydslBindingsFactory querydslBindingsFactory, ConversionService conversionServiceDelegate) {
		this(querydslBindingsFactory, conversionServiceDelegate, new Class[]{Date.class, LocalDate.class, Timestamp.class});
	}

	public QuerydslPredicateArgumentResolverBeanPostProcessor(QuerydslBindingsFactory querydslBindingsFactory, ConversionService conversionServiceDelegate, Class[] delegatedConversions) {
		Validate.notNull(querydslBindingsFactory, "QuerydslBindingsFactory must not be null");
		this.querydslBindingsFactory = querydslBindingsFactory;
		this.conversionServiceDelegate = conversionServiceDelegate;
		this.delegatedConversions = delegatedConversions;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		Object target = bean;
		if (target != null && QuerydslPredicateArgumentResolver.class.isAssignableFrom(target.getClass())) {
			ExpressionProviderFactory.setSupportsUnTypedValues(true);
			try {
				try {
					// Spring Boot 2.x
					return ConstructorUtils.invokeConstructor(QuerydslPredicateArgumentResolver.class,
							new Object[] { querydslBindingsFactory, Optional.of(delegationAwareConversionService) });
				} catch (NoSuchMethodException | NoSuchMethodError e) {
					// Spring boot 1.5.x
					return ConstructorUtils.invokeConstructor(QuerydslPredicateArgumentResolver.class,
							new Object[] { querydslBindingsFactory, delegationAwareConversionService });
				}
			} catch (Throwable t) {
				// phew
				throw new RuntimeException("Failed to post-process QuerydslPredicateArgumentResolver", t);
			}
		}
		return target;
	}

	/**
	 * Implementing default method as-is since Spring Boot 1.5.x specific
	 * dependencies don't have default methods so if library users use this with
	 * an older spring, the runtime would fail. This is implemented as a
	 * fail-safe mechanism.
	 */
	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
}