DynamoFilterUtil.java

package org.dynamoframework.filter;

/*-
 * #%L
 * Dynamo Framework
 * %%
 * Copyright (C) 2014 - 2024 Open Circle Solutions
 * %%
 * 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.
 * #L%
 */

import lombok.experimental.UtilityClass;
import org.dynamoframework.domain.model.AttributeModel;
import org.dynamoframework.domain.model.AttributeType;
import org.dynamoframework.domain.model.EntityModel;
import org.dynamoframework.exception.OCSRuntimeException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

/**
 * @author Bas Rutten
 */
@UtilityClass
public final class DynamoFilterUtil {

	/**
	 * Extracts a specific filter from a (possibly) composite filter
	 *
	 * @param filter     the filter from which to extract a certain part
	 * @param propertyId the propertyId of the filter to extract
	 * @return the extracted filter
	 */
	public static Filter extractFilter(Filter filter, String propertyId) {
		if (filter instanceof AbstractJunctionFilter junction) {
			for (Filter child : junction.getFilters()) {
				Filter found = extractFilter(child, propertyId);
				if (found != null) {
					return found;
				}
			}
		} else if (filter instanceof Compare compare) {
			if (compare.getPropertyId().equals(propertyId)) {
				return compare;
			}
		} else if (filter instanceof Like like) {
			if (like.getPropertyId().equals(propertyId)) {
				return like;
			}
		} else if (filter instanceof In in) {
			if (in.getPropertyId().equals(propertyId)) {
				return in;
			}
		} else if (filter instanceof Contains contains) {
			if (contains.getPropertyId().equals(propertyId)) {
				return contains;
			}
		} else if (filter instanceof Between between) {
			if (between.getPropertyId().equals(propertyId)) {
				return between;
			}
		} else if (filter instanceof Not not) {
			return extractFilter(not.getFilter(), propertyId);
		}
		return null;

	}

	/**
	 * Flattens the provided filter, removing any nested And-filters
	 *
	 * @param and the filter to flatten
	 * @return the result of the flattening
	 */
	public static List<Filter> flattenAnd(And and) {
		List<Filter> children = new ArrayList<>();

		for (Filter filter : and.getFilters()) {
			if (filter instanceof And childAnd) {
				List<Filter> temp = flattenAnd(childAnd);
				children.addAll(temp);
			} else {
				children.add(filter);
			}
		}
		return children;
	}

	/**
	 * Removes the specified filters from the provided junction filter
	 *
	 * @param junction    the junction filter
	 * @param propertyIds the propertyIds of the filters to remove
	 */
	private static void removeFilterFormJunction(AbstractJunctionFilter junction, String... propertyIds) {
		Iterator<Filter> it = junction.getFilters().iterator();
		while (it.hasNext()) {
			Filter child = it.next();
			if (child instanceof PropertyFilter propertyFilter) {
				for (String s : propertyIds) {
					if (propertyFilter.getPropertyId().equals(s)) {
						it.remove();
					}
				}
			}
		}

		// pass through to nested junction filters
		it = junction.getFilters().iterator();
		while (it.hasNext()) {
			Filter child = it.next();
			if (!(child instanceof PropertyFilter)) {
				removeFilters(child, propertyIds);
			}
		}
	}

	/**
	 * Remove any empty junction filters that don't contain any filters of their own
	 * anymore
	 *
	 * @param junction the junction filter to remove the empty filters from
	 */
	private static void cleanupEmptyFilters(AbstractJunctionFilter junction) {
		Iterator<Filter> it = junction.getFilters().iterator();
		while (it.hasNext()) {
			Filter child = it.next();
			if (child instanceof AbstractJunctionFilter junctionFilter) {
				if (junctionFilter.getFilters().isEmpty()) {
					it.remove();
				}
			} else if (child instanceof Not not) {
				if (not.getFilter() == null) {
					it.remove();
				}
			}
		}
	}

	/**
	 * Removes filters with the specified property IDs from a certain filter
	 *
	 * @param filter      the filter to remove the filters from
	 * @param propertyIds the property IDs of the filters to remove
	 */
	public static void removeFilters(Filter filter, String... propertyIds) {
		if (filter instanceof AbstractJunctionFilter junction) {
			// junction filter, iterate over its children
			removeFilterFormJunction(junction, propertyIds);
			cleanupEmptyFilters(junction);
		} else if (filter instanceof Not not) {
			// in case of a not-filter, propagate to the child

			if (not.getFilter() != null) {
				removeFilters(not.getFilter(), propertyIds);
			}

			Filter child = not.getFilter();
			if (child instanceof PropertyFilter propertyFilter) {
				for (String s : propertyIds) {
					if (propertyFilter.getPropertyId().equals(s)) {
						not.setFilter(null);
					}
				}
			} else if (child instanceof AbstractJunctionFilter junctionFilter) {
				if (junctionFilter.getFilters().isEmpty()) {
					not.setFilter(null);
				}
			}
		}
	}

	/**
	 * Replaces all filters that query a detail relation by the appropriate filters
	 *
	 * @param filter      the original filter
	 * @param entityModel the entity model used to determine which filters must be
	 *                    replaced
	 */
	public static void replaceMasterAndDetailFilters(Filter filter, EntityModel<?> entityModel) {

		// iterate over models and try to find filters that query DETAIL relations
		for (AttributeModel am : entityModel.getAttributeModels()) {
			replaceMasterDetailFilter(filter, am);
			if (am.getNestedEntityModel() != null) {
				replaceMasterAndDetailFilters(filter, am.getNestedEntityModel());
			}

		}
	}

	/**
	 * Replaces a "Compare.Equal" filter that searches on a master or detail field
	 * by a "Contains" or "In" filter
	 *
	 * @param filter the filter
	 * @param am     the attribute model
	 */
	private static void replaceMasterDetailFilter(Filter filter, AttributeModel am) {
		if (AttributeType.DETAIL.equals(am.getAttributeType())
			|| AttributeType.ELEMENT_COLLECTION.equals(am.getAttributeType())
			|| AttributeType.MASTER.equals(am.getAttributeType())
			|| (AttributeType.BASIC.equals(am.getAttributeType()) && am.isMultipleSearch())) {
			Filter detailFilter = extractFilter(filter, am.getPath());
			if (detailFilter instanceof Compare.Equal equal) {
				// check which property to use in the query
				String prop = am.getActualSearchPath();

				if (AttributeType.DETAIL.equals(am.getAttributeType())
					|| AttributeType.ELEMENT_COLLECTION.equals(am.getAttributeType())) {
					replaceDetailOrElementCollectionFilter(filter, am, prop, equal);
				} else {
					// master attribute - translate to an "in" filter
					replaceMasterFilter(filter, am, prop, equal);
				}
			}
		}
	}

	/**
	 * Replaces a filter on a master attribute
	 *
	 * @param filter the overall filter
	 * @param am     the attribute model
	 * @param prop   the name of the property
	 * @param equal  the "equal" filter to replace
	 */
	private static void replaceMasterFilter(Filter filter, AttributeModel am, String prop, Compare.Equal equal) {
		if (equal.getValue() instanceof Collection<?> col) {
			// multiple values supplied - construct an OR filter
			if (!col.isEmpty()) {
				In in = new In(prop, col);
				replaceFilter(null, filter, in, am.getPath(), false);
			} else {
				// filtering on an empty collection is a bad idea
				removeFilters(filter, am.getPath());
			}
		} else if (am.getReplacementSearchPath() != null) {
			// single value property implemented by means of a collection
			Object o = equal.getValue();
			Compare.Equal equals = new Compare.Equal(prop, o);
			replaceFilter(null, filter, equals, am.getPath(), false);
		}
	}

	/**
	 * Replaces a filter on a detail or element collection
	 *
	 * @param filter the overall filter
	 * @param am     the attribute model
	 * @param prop   the property
	 * @param equal  the "equal" filter to replace
	 */
	private static void replaceDetailOrElementCollectionFilter(Filter filter, AttributeModel am, String prop,
															   Compare.Equal equal) {
		if (equal.getValue() instanceof Collection<?> col) {
			// multiple values supplied - construct an OR filter

			if (!col.isEmpty()) {
				Or or = new Or();
				for (Object o : col) {
					or.or(new Contains(prop, o));
				}
				replaceFilter(filter, or, am.getPath(), false);
			} else {
				// filtering on an empty collection is a bad idea
				removeFilters(filter, am.getPath());
			}
		} else {
			// just a single value - construct a single contains filter
			replaceFilter(filter, new Contains(prop, equal.getValue()), am.getPath(), false);
		}
	}

	/**
	 * Replaces a filter by another filter
	 *
	 * @param original   the main filter that contains the filter to be replaced
	 * @param newFilter  the replacement filter
	 * @param propertyId the property ID of the filter to replace
	 * @param firstOnly  indicates whether to replace only the first instance
	 */
	public static void replaceFilter(Filter original, Filter newFilter, String propertyId, boolean firstOnly) {
		try {
			replaceFilter(null, original, newFilter, propertyId, firstOnly);
		} catch (RuntimeException ex) {
			// do nothing - only used to break out of loop
		}
	}

	/**
	 * Replaces a filter by another filter. This method only works for junction
	 * filters
	 *
	 * @param parent     the parent
	 * @param original   the original filter
	 * @param newFilter  the new filter
	 * @param propertyId the property id of the filter that must be replaced
	 * @param firstOnly  whether to only replace the first occurrence
	 */
	private static void replaceFilter(Filter parent, Filter original, Filter newFilter, String propertyId,
									  boolean firstOnly) {
		if (original instanceof AbstractJunctionFilter junction) {
			// junction filter, iterate over its children
			for (Filter child : junction.getFilters()) {
				replaceFilter(junction, child, newFilter, propertyId, firstOnly);
			}
		} else if (original instanceof PropertyFilter propertyFilter) {
			// filter has a property ID, see if it matches
			if (propertyFilter.getPropertyId().equals(propertyId)) {
				if (parent instanceof AbstractJunctionFilter junctionFilter) {
					junctionFilter.replace(original, newFilter, firstOnly);
				} else if (parent instanceof Not not) {
					not.setFilter(newFilter);
				}

				// throw exception to abort processing - this is nasty but better than
				// propagating
				// the state via parameters
				if (firstOnly) {
					throw new OCSRuntimeException();
				}
			}
		} else if (original instanceof Not not) {
			// in case of a not-filter, propagate to the child
			replaceFilter(not, not.getFilter(), newFilter, propertyId, firstOnly);
		}
	}

}