QuerydslHttpRequestContext.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.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.Path;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.bitbucket.gt_tech.spring.data.querydsl.value.operators.ExpressionProvider;
import org.bitbucket.gt_tech.spring.data.querydsl.value.operators.ExpressionProviderFactory;
import org.bitbucket.gt_tech.spring.data.querydsl.value.operators.Operator;
import org.bitbucket.gt_tech.spring.data.querydsl.value.operators.OperatorAndValue;
import org.springframework.data.web.querydsl.QuerydslPredicateArgumentResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.lang.reflect.Array;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Context is core to experimental features using
 * {@link QuerydslHttpRequestContextAwareServletFilter} as it decorates the
 * default {@link HttpServletRequest} by extracting all the value operators and
 * maintaining them locally against their respective {@link Path} in this
 * context. This allows Spring Web {@link QuerydslPredicateArgumentResolver} to
 * continue with its strong type-conversion hiding the fact that there were
 * value operators. Value operators enabled actual value is provided to
 * {@link ExpressionProvider} during later phases for their usage in actual
 * {@link Expression} for querying.
 * 
 * @author gt_tech
 *
 */
public final class QuerydslHttpRequestContext {

	private final EntityPath<?> root;
	private final HttpServletRequest servletRequest;
	private final Map<String, Collection<String>> original_parameters = new LinkedHashMap<>();
	private final Map<String, Collection<String>> transformed_parameters;

	/**
	 * Constructor
	 * 
	 * <p>
	 * This constructor extracts the original request parameters and also
	 * creates a local transformed parameter list devoid of any value operators
	 * this component supports.
	 * </p>
	 * 
	 * @param root
	 *            Root {@link EntityPath} for this context
	 * @param servletRequest
	 *            {@link HttpServletRequest}
	 */
	public QuerydslHttpRequestContext(EntityPath<?> root, HttpServletRequest servletRequest) {
		Validate.notNull(root, "EntityPath must not be null");
		Validate.notNull(servletRequest, "HttpServletRequest must not be null");
		this.root = root;
		this.servletRequest = servletRequest;

		this.servletRequest.getParameterMap()
				.keySet()
				.stream()
				.forEach(k -> original_parameters.put(String.valueOf(k),
						Arrays.asList(this.servletRequest.getParameterValues(String.valueOf(k)))));

		transformed_parameters = this.original_parameters.keySet()
				.stream()
				.collect(Collectors.toMap(key -> key, key -> original_parameters.get(key)
						.stream()
						.map(s -> extractTrueValue(s))
						.collect(Collectors.toList()), (e1, e2) -> e1, LinkedHashMap::new));
	}

	/**
	 * @return decorated {@link HttpServletRequest} object containing search
	 *         request parameters devoid of any value operators.
	 */
	HttpServletRequest getWrappedHttpServletRequest() {
		if (this.transformed_parameters == null || this.transformed_parameters.size() <= 0) {
			return getOriginalHttpServletRequest();
		}
		return new HttpServletRequestWrapper(this.servletRequest) {
			@Override
			public String getParameter(String name) {
				Collection<String> values = getParameterValuesAsList(name);
				if (CollectionUtils.isNotEmpty(values)) {
					return values.iterator()
							.next();
				}
				return super.getParameter(name);
			}

			@Override
			public Map<String, String[]> getParameterMap() {
				return transformed_parameters.keySet()
						.stream()
						.collect(Collectors.toMap(key -> key, key -> transformed_parameters.get(key)
								.toArray(new String[transformed_parameters.get(key)
										.size()]),
								(e1, e2) -> e1, LinkedHashMap::new));

			}

			@Override
			public Enumeration<String> getParameterNames() {
				return super.getParameterNames();
			}

			@Override
			public String[] getParameterValues(String name) {
				Collection<String> values = getParameterValuesAsList(name);
				return values.toArray(new String[values.size()]);
			}

			private Collection<String> getParameterValuesAsList(String name) {
				Validate.notNull(name, "Parameter name must not be blank");
				Collection<String> result = transformed_parameters.get(name);
				return result != null ? result : new ArrayList<>(0);
			}
		};
	}

	/**
	 * @return original {@link HttpServletRequest} containing inputs from client
	 *         request.
	 */
	HttpServletRequest getOriginalHttpServletRequest() {
		return this.servletRequest;
	}

	/**
	 * @param inPath
	 *            {@link Path} for which original search request single value is
	 *            required.
	 * @return Original value from original HttpServletRequest for given
	 *         {@link Path} if available, <code>null</code> otherwise
	 */
	public String getSingleValue(Path inPath) {
		return Optional.ofNullable(Optional.ofNullable(inPath)
				.map(p -> this.servletRequest.getParameter(findRequestParameterNameFromPath(inPath)))
				.orElseGet(() -> this.servletRequest.getParameter(inPath.toString())))
				.orElseGet(() -> ExpressionProviderFactory.findAlias(inPath)
						.map(s -> this.servletRequest.getParameter(s))
						.orElseGet(() -> null));

	}

	/**
	 * @param inPath
	 *            {@link Path} for which original search request all values are
	 *            required.
	 * @return Original values as {@link Array} of String from original
	 *         HttpServletRequest for given {@link Path} if available,
	 *         <code>null</code> otherwise
	 */
	public String[] getAllValues(Path inPath) {
		return Optional.ofNullable(Optional.ofNullable(inPath)
				.map(p -> this.servletRequest.getParameterValues(findRequestParameterNameFromPath(inPath)))
				.orElseGet(() -> this.servletRequest.getParameterValues(inPath.toString())))
				.orElseGet(() -> ExpressionProviderFactory.findAlias(inPath)
						.map(s -> this.servletRequest.getParameterValues(s))
						.orElseGet(() -> null));

	}

	/*
	 * Internal utility function to create actual search parameter name in
	 * request originating from request since provided path starts from root.
	 * notation unlike request parameter path which starts after root.
	 */
	private String findRequestParameterNameFromPath(Path inPath) {
		Validate.notNull(inPath, "Input path must not be null to lookup original request parameter value");
		String name = null;
		Validate.isTrue(inPath.getRoot()
				.getType()
				.equals(this.root.getType()), "Mismatch in type root in path and current context");
		return StringUtils.replace(inPath.toString(), this.root + ".", StringUtils.EMPTY, 1);
	}

	/*
	 * Utility function that strips the provided input from all operators and
	 * returns true value of input
	 */
	private String extractTrueValue(String input) {
		if (StringUtils.isNotBlank(input)) {
			while (true) {
				if (ExpressionProvider.isOperator(input)
						.isPresent()) {
					return extractTrueValue(
							new OperatorAndValue(input, Arrays.asList(Operator.values()), null).getValue());
				} else {
					return input;
				}
			}
		}
		return StringUtils.EMPTY;
	}
}