All Downloads are FREE. Search and download functionalities are using the official Maven repository.

querymethods.springdata.mapping.PropertyPath Maven / Gradle / Ivy

/*
 * Copyright 2011-2019 the original author or authors.
 *
 * 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
 *
 *      https://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 querymethods.springdata.mapping;

import java.beans.Introspector;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import querymethods.springdata.util.Assert;
import querymethods.springdata.util.ObjectUtils;
import querymethods.springdata.util.StringUtils;

/**
 * Abstraction of a {@link PropertyPath} of a domain class.
 * 
 * @author Oliver Gierke
 */
public class PropertyPath implements Iterable {

	private static final String PARSE_DEPTH_EXCEEDED = "Trying to parse a path with depth greater than 1000! This has been disabled for security reasons to prevent parsing overflows.";

	private static final String DELIMITERS = "_\\.";
//	private static final String ALL_UPPERCASE = "[A-Z0-9._$]+";
	private static final Pattern SPLITTER = Pattern.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", DELIMITERS));

	private final String name;

	private PropertyPath next;

	/**
	 * Creates a leaf {@link PropertyPath} (no nested ones) with the given name
	 * inside the given owning type.
	 * 
	 * @param name       must not be {@literal null} or empty.
	 * @param owningType must not be {@literal null}.
	 */
	PropertyPath(String name) {
		this(name, Collections.emptyList());
	}

	/**
	 * Creates a leaf {@link PropertyPath} (no nested ones with the given name and
	 * owning type.
	 * 
	 * @param name       must not be {@literal null} or empty.
	 * @param owningType must not be {@literal null}.
	 * @param base       the {@link PropertyPath} previously found.
	 */
	PropertyPath(String name, List base) {

		Assert.hasText(name, "Name must not be null or empty!");
		Assert.notNull(base, "Perviously found properties must not be null!");

		String propertyName = Introspector.decapitalize(name);
		this.name = propertyName;
	}

	/**
	 * Returns the name of the {@link PropertyPath}.
	 * 
	 * @return the name will never be {@literal null}.
	 */
	public String getSegment() {
		return name;
	}

	/**
	 * Returns the leaf property of the {@link PropertyPath}.
	 * 
	 * @return will never be {@literal null}.
	 */
	public PropertyPath getLeafProperty() {

		PropertyPath result = this;

		while (result.hasNext()) {
			result = result.next();
		}

		return result;
	}

	/**
	 * Returns the next nested {@link PropertyPath}.
	 * 
	 * @return the next nested {@link PropertyPath} or {@literal null} if no nested
	 *         {@link PropertyPath} available.
	 * @see #hasNext()
	 */
	public PropertyPath next() {
		return next;
	}

	/**
	 * Returns whether there is a nested {@link PropertyPath}. If this returns
	 * {@literal true} you can expect {@link #next()} to return a non-
	 * {@literal null} value.
	 * 
	 * @return
	 */
	public boolean hasNext() {
		return next != null;
	}

	/**
	 * Returns the {@link PropertyPath} in dot notation.
	 * 
	 * @return
	 */
	public String toDotPath() {

		if (hasNext()) {
			return getSegment() + "." + next().toDotPath();
		}

		return getSegment();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {

		if (this == obj) {
			return true;
		}

		if (obj == null || !getClass().equals(obj.getClass())) {
			return false;
		}

		PropertyPath that = (PropertyPath) obj;

		return this.name.equals(that.name) && ObjectUtils.nullSafeEquals(this.next, that.next);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {

		int result = 17;

		result += 31 * name.hashCode();
		result += 31 * (next == null ? 0 : next.hashCode());

		return result;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Iterable#iterator()
	 */
	@Override
	public Iterator iterator() {
		return new Iterator() {

			private PropertyPath current = PropertyPath.this;

			@Override
			public boolean hasNext() {
				return current != null;
			}

			@Override
			public PropertyPath next() {
				PropertyPath result = current;
				this.current = current.next();
				return result;
			}

			@Override
			public void remove() {
				throw new UnsupportedOperationException();
			}
		};
	}

	/**
	 * Extracts the {@link PropertyPath} chain from the given source {@link String}.
	 * 
	 * @param source must not be {@literal null}.
	 * @param type
	 * @return
	 */
	public static PropertyPath from(String source) {

		Assert.hasText(source, "Source must not be null or empty!");

		List iteratorSource = new ArrayList();
		Matcher matcher = SPLITTER.matcher("_" + source);

		while (matcher.find()) {
			iteratorSource.add(matcher.group(1));
		}

		Iterator parts = iteratorSource.iterator();

		PropertyPath result = null;
		Stack current = new Stack();

		while (parts.hasNext()) {
			if (result == null) {
				result = createNoTail(parts.next(), current);
				current.push(result);
			} else {
				current.push(create(parts.next(), current));
			}
		}

		return result;
	}

	/**
	 * Creates a new {@link PropertyPath} as subordinary of the given
	 * {@link PropertyPath}.
	 * 
	 * @param source
	 * @param base
	 * @return
	 */
	private static PropertyPath create(String source, Stack base) {

		PropertyPath previous = base.peek();

		PropertyPath propertyPath = create(source, base);
		previous.next = propertyPath;
		return propertyPath;
	}

	/**
	 * Factory method to create a new {@link PropertyPath} for the given
	 * {@link String} and owning type. It will inspect the given source for
	 * camel-case parts and traverse the {@link String} along its parts starting
	 * with the entire one and chewing off parts from the right side then. Whenever
	 * a valid property for the given class is found, the tail will be traversed for
	 * subordinary properties of the just found one and so on.
	 * 
	 * @param source
	 * @param type
	 * @return
	 */
	private static PropertyPath createNoTail(String source, List base) {
		return create(source, "", base);
	}

	/**
	 * Tries to look up a chain of {@link PropertyPath}s by trying the givne source
	 * first. If that fails it will split the source apart at camel case borders
	 * (starting from the right side) and try to look up a {@link PropertyPath} from
	 * the calculated head and recombined new tail and additional tail.
	 * 
	 * @param source
	 * @param type
	 * @param addTail
	 * @return
	 */
	private static PropertyPath create(String source, String addTail, List base) {

		if (base.size() > 1000) {
			throw new IllegalArgumentException(PARSE_DEPTH_EXCEEDED);
		}

		PropertyPath current = null;

		current = new PropertyPath(source, base);

		if (!base.isEmpty()) {
			base.get(base.size() - 1).next = current;
		}

		List newBase = new ArrayList(base);
		newBase.add(current);

		if (StringUtils.hasText(addTail)) {
			current.next = createNoTail(addTail, newBase);
		}

		return current;

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return String.format("%s", toDotPath());
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy