QuerydslHttpRequestContextAwareServletFilter.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 com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.dsl.EnumPath;
import org.bitbucket.gt_tech.spring.data.querydsl.value.operators.ExpressionProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.web.querydsl.QuerydslPredicateArgumentResolver;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.ExecutionException;

/**
 * Advanced experimental feature which allows request values to be stripped of
 * their value-operators and thus allowing
 * {@link QuerydslPredicateArgumentResolver} to perform type-conversion on
 * non-String paths. Original values decorated with value operator are managed
 * in a {@link QuerydslHttpRequestContext} which is available to
 * {@link ExpressionProvider} thru {@link QuerydslHttpRequestContextHolder}
 * though that's dependent on threading. See -
 * {@link QuerydslHttpRequestContextHolderStrategy}.
 * 
 * <p>
 * Note that this will still require request to only contain certain
 * value-operators, as the true value of search input (which is value wrapped
 * within operators) must still suffice type-conversion needs. For example, if
 * an {@link EnumPath} can have values - LOCKED, ACTIVE, the search input can
 * have ne(LOCKED) but can not have startsWith(LOC) as latter would fail
 * type-conversion. For advanced needs to support maximum possible range of
 * value-operators on individual path, it may be desired to disable the
 * type-conversion of these search input values which can be accomplished using
 * {@link QuerydslPredicateArgumentResolverBeanPostProcessor} in which case this
 * filter can be disabled by consuming application.
 * </p>
 * 
 * @author gt_tech
 *
 */
public class QuerydslHttpRequestContextAwareServletFilter implements Filter {

	private static final Logger logger = LoggerFactory.getLogger(QuerydslHttpRequestContextAwareServletFilter.class);

	private static final EntityPathResolver entityPathResolver = SimpleEntityPathResolver.INSTANCE;

	Map<String, Class<?>> URI_SEARCH_RESOURCE_TYPE_MAPPINGS = new TreeMap(new Comparator<String>() {
		@Override
		public int compare(String s1, String s2) {
			if (s1.length() > s2.length()) {
				return -1;
			} else if (s1.length() < s2.length()) {
				return 1;
			} else {
				/*
				 * Equal length's so lexicographically comparison
				 */
				return s1.compareTo(s2);
			}
		}
	});

	static LoadingCache<Class<?>, EntityPath<?>> loadingCache = CacheBuilder.newBuilder()
			.build(new CacheLoader<Class<?>, EntityPath<?>>() {
				@Override
				public EntityPath<?> load(Class<?> domainClass) throws Exception {
					return entityPathResolver.createPath(domainClass);
				}
			});

	/**
	 * Constructor
	 * 
	 * @param URI_SEARCH_RESOURCE_TYPE_MAPPINGS
	 *            - A mapping of URI to corresponding search resource class.
	 */
	public QuerydslHttpRequestContextAwareServletFilter(Map<String, Class<?>> URI_SEARCH_RESOURCE_TYPE_MAPPINGS) {
		if (URI_SEARCH_RESOURCE_TYPE_MAPPINGS != null) {
			this.URI_SEARCH_RESOURCE_TYPE_MAPPINGS.putAll(URI_SEARCH_RESOURCE_TYPE_MAPPINGS);
		}

		try {
			loadingCache.getAll(this.URI_SEARCH_RESOURCE_TYPE_MAPPINGS.values());
		} catch (ExecutionException ex) {
			throw new RuntimeException("Failed to instantiate filter, possible mis-configurations?", ex); // TODO:
																											// to
																											// have
																											// library
																											// specific
																											// exceptions.
		}
	}

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {

	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {

		try {
			HttpServletRequest req = (HttpServletRequest) request;
			final String request_uri = req.getRequestURI();

			Optional<EntityPath<?>> optionalPath = URI_SEARCH_RESOURCE_TYPE_MAPPINGS.keySet()
					.stream()
					.filter(k -> k.equalsIgnoreCase(request_uri))
					.findFirst()
					.map(k -> URI_SEARCH_RESOURCE_TYPE_MAPPINGS.get(k))
					.map(k -> {
						try {
							return loadingCache.get(k);
						} catch (Exception ex) {
							throw new RuntimeException("Failed to load Path for " + "request uri: " + request_uri);
						}
					});

			if (optionalPath.isPresent()) {
				logger.debug("Processing {} on URI: {} for EntityPath: {}",
						new Object[] { QuerydslHttpRequestContext.class, request_uri, optionalPath.get()
								.getClass()
								.getCanonicalName() });
				QuerydslHttpRequestContext context = new QuerydslHttpRequestContext(optionalPath.get(), req);
				QuerydslHttpRequestContextHolder.setContext(context);
				chain.doFilter(context.getWrappedHttpServletRequest(), response);
			} else {
				logger.error(
						"No EntityPath found on request_uri: {}, bad filter configurations (check filter url pattern and also the injected mappings), filter is turning into a no-op for this request",
						request_uri);
				chain.doFilter(req, response);
			}
		} finally {
			QuerydslHttpRequestContextHolder.clearContext();
		}
	}

	@Override
	public void destroy() {

	}
}