OffsetPageRequest.java

package org.oxerr.commons.ws.rs.data;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import javax.ws.rs.DefaultValue;
import javax.ws.rs.QueryParam;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * {@link Pageable} implementation using limit-offset mode.
 */
public class OffsetPageRequest implements Pageable, Serializable {

	public static OffsetPageRequest of() {
		return new OffsetPageRequest();
	}

	public static OffsetPageRequest of(int limit, long offset) {
		return new OffsetPageRequest(limit, offset);
	}

	public static OffsetPageRequest of(int limit, long offset, Sort sort) {
		return new OffsetPageRequest(limit, offset, sort);
	}

	private static final long serialVersionUID = 2017071101L;

	private static final int DEFAULT_MAX_LIMIT = 500;

	private static final int DEFAULT_LIMIT = 10;

	private static final String DEFAULT_LIMIT_STRING = "10";

	private static final String DEFAULT_PROPERTY_DELIMITER = ",";

	private int maxLimit = DEFAULT_MAX_LIMIT;

	private String propertyDelimiter = DEFAULT_PROPERTY_DELIMITER;

	private int limit;

	private long offset;

	private Sort sort;

	public OffsetPageRequest() {
		this(DEFAULT_LIMIT, 0, Sort.unsorted());
	}

	public OffsetPageRequest(int limit, long offset) {
		this(limit, offset, Sort.unsorted());
	}

	public OffsetPageRequest(int limit, long offset, Sort sort) {
		Assert.notNull(sort, "Sort must not be null!");

		this.check(limit);

		this.limit = limit;
		this.offset = offset;
		this.sort = sort;
	}

	public OffsetPageRequest defaultSort(Direction direction, String... properties) {
		final OffsetPageRequest ret;
		final Sort s = getSort();

		if (s.isSorted()) {
			ret = this;
		} else {
			ret = new OffsetPageRequest(getPageSize(), getOffset(), Sort.by(direction, properties));
		}

		return ret;
	}

	/**
	 * Sets limit.
	 *
	 * @param limit the size of the page to be returned.
	 */
	@QueryParam("limit")
	@DefaultValue(DEFAULT_LIMIT_STRING)
	public void setLimit(int limit) {
		this.check(limit);

		this.limit = limit;
	}

	/**
	 * Sets offset.
	 *
	 * @param offset number of records to be skipped from the result set.
	 */
	@QueryParam("offset")
	@DefaultValue("0")
	public void setOffset(long offset) {
		this.offset = offset;
	}

	/**
	 * Sets sorts.
	 *
	 * @param sorts the sorting strings.
	 * One sorting string is a comma separated string of property and direction,
	 * such as {@literal property,asc}, {@literal property,desc}.
	 */
	@QueryParam("sort[]")
	public void setSort(List<String> sorts) {
		Assert.notNull(sorts, "Sorts must not be null!");

		sort = parseSort(sorts);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int getPageNumber() {
		return (int) (limit > 0 ? offset / limit : 0);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int getPageSize() {
		return limit;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public long getOffset() {
		return offset;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Sort getSort() {
		return sort;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public OffsetPageRequest next() {
		return new OffsetPageRequest(limit, offset + limit, sort);
	}

	public OffsetPageRequest previous() {
		return getOffset() == 0 ? this : new OffsetPageRequest(limit, getOffset() - limit, getSort());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public OffsetPageRequest previousOrFirst() {
		return hasPrevious() ? previous() : first();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public OffsetPageRequest first() {
		return new OffsetPageRequest(limit, 0, sort);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public OffsetPageRequest withPage(int pageNumber) {
		return new OffsetPageRequest(this.limit, (long) this.limit * (long) pageNumber, getSort());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean hasPrevious() {
		return offset > 0;
	}

	protected int getMaxLimit() {
		return this.maxLimit;
	}

	protected String getPropertyDelimiter() {
		return this.propertyDelimiter;
	}

	protected String filterProperty(String property) {
		return StringUtils.replace(property, "'", "''");
	}

	protected Optional<Direction> parseDirection(String value) {
		return Direction.fromOptionalString(value);
	}

	protected List<Order> parseOrders(final List<String> source) {
		final List<String> parts = source.stream()
			.filter(java.util.Objects::nonNull)
			.collect(Collectors.toList());
		final String delimiter = this.getPropertyDelimiter();
		final List<Order> allOrders = new ArrayList<>();

		for (final String part : parts) {
			final String[] elements = part.split(delimiter);
			final Optional<Direction> direction = elements.length == 0
				? Optional.empty()
				: this.parseDirection(elements[elements.length - 1]);

			for (int i = 0; i < elements.length; i++) {

				if (i == elements.length - 1 && direction.isPresent()) {
					continue;
				}

				final String property = this.filterProperty(elements[i]);

				if (StringUtils.hasText(property)) {
					allOrders.add(new Order(direction.orElse(null), property));
				}
			}
		}

		return allOrders;
	}

	protected Sort parseSort(List<String> sorts) {
		final List<Order> orders = parseOrders(sorts);
		return Sort.by(orders);
	}

	private void check(int limit) {
		if (limit > this.getMaxLimit()) {
			throw new IllegalArgumentException("Limit is exceeded.");
		}
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + limit;
		result = prime * result + Long.hashCode(offset);
		result = prime * result + sort.hashCode();
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		OffsetPageRequest other = (OffsetPageRequest) obj;
		if (limit != other.limit) {
			return false;
		}
		if (offset != other.offset) {
			return false;
		}
		return sort.equals(other.sort);
	}

	@Override
	public String toString() {
		return String.format("Page request [number: %d, size %d, sort: %s]", getPageNumber(), getPageSize(), sort);
	}

}