PagedQuery.java

/*******************************************************************************
 * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
package org.eclipse.rdf4j.workbench.util;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.rdf4j.query.QueryLanguage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Dale Visser
 */
public class PagedQuery {

	private static final Logger LOGGER = LoggerFactory.getLogger(PagedQuery.class);

	private static final int FLAGS = Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL;

	private static final Pattern LIMIT_OR_OFFSET = Pattern.compile("((limit)|(offset))\\s+\\d+", FLAGS);

	private final String modifiedQuery;

	private final boolean inlineLimitAndOffset;

	private final int limitSubstitute;
	private final int offsetSubstitute;

	/***
	 * <p>
	 * Creates an object that adds or modifies the limit and offset clauses of the query to be executed so that only
	 * those results to be displayed are requested from the query engine.
	 * </p>
	 * <p>
	 * Implementation note: The new object contains the user's query with appended or modified LIMIT and OFFSET clauses.
	 * </p>
	 *
	 * @param query         as it was specified by the user
	 * @param language      a {@link QueryLanguage} as specified by the user
	 * @param requestLimit  maximum number of results to return, as specified by the URL query parameters or cookies
	 * @param requestOffset which result to start at when populating the result set
	 */
	public PagedQuery(final String query, final QueryLanguage language, final int requestLimit,
			final int requestOffset) {
		LOGGER.debug("Query Language: {}, requestLimit: " + requestLimit + ", requestOffset: " + requestOffset,
				language);
		LOGGER.debug("Query: {}", query);

		String rval = query;

		/*
		 * the matcher on the pattern will have a group for "limit l#" as well as a group for l#, similarly for
		 * "offset o#" and o#. If either exists, disable paging.
		 */
		final Matcher matcher = LIMIT_OR_OFFSET.matcher(query);
		// requestLimit <= 0 actually means don't limit display
		inlineLimitAndOffset = requestLimit > 0 && !matcher.find();
		// gracefully handle malicious value
		offsetSubstitute = (requestOffset < 0) ? 0 : requestOffset;
		limitSubstitute = requestLimit;
		if (inlineLimitAndOffset) {
			rval = modifyLimit(language, rval, limitSubstitute);
			rval = modifyOffset(language, offsetSubstitute, rval);
			LOGGER.debug("Modified Query: {}", rval);
		}

		this.modifiedQuery = rval;
	}

	public boolean isPaged() {
		return this.inlineLimitAndOffset;
	}

	public int getLimit() {
		return this.limitSubstitute;
	}

	public int getOffset() {
		return this.offsetSubstitute;
	}

	@Override
	public String toString() {
		return this.modifiedQuery;
	}

	private String modifyOffset(final QueryLanguage language, final int offset, final String query) {
		String rval = query;
		final String newOffsetClause = "offset " + offset;
		if (offset > 0) {
			rval = ensureNewlineAndAppend(rval, newOffsetClause);
		}
		return rval;
	}

	private static String ensureNewlineAndAppend(final String original, final String append) {
		final StringBuffer buffer = new StringBuffer(original.length() + append.length() + 1);
		buffer.append(original);
		if (buffer.charAt(buffer.length() - 1) != '\n') {
			buffer.append('\n');
		}

		return buffer.append(append).toString();
	}

	private static String modifyLimit(final QueryLanguage language, final String query, final int limitSubstitute) {
		return ensureNewlineAndAppend(query, "limit " + limitSubstitute);
	}

}