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

com.sap.cds.impl.localized.LocaleUtils Maven / Gradle / Ivy

There is a newer version: 3.8.0
Show newest version
/************************************************************************
 * © 2019-2022 SAP SE or an SAP affiliate company. All rights reserved. *
 ************************************************************************/
package com.sap.cds.impl.localized;

import static com.sap.cds.reflect.impl.reader.model.CdsConstants.CDS_LOCALIZED;

import java.util.Collection;
import java.util.Locale;
import java.util.Optional;

import com.sap.cds.ql.cqn.CqnComparisonPredicate;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnExistsSubquery;
import com.sap.cds.ql.cqn.CqnFilterableStatement;
import com.sap.cds.ql.cqn.CqnFunc;
import com.sap.cds.ql.cqn.CqnInPredicate;
import com.sap.cds.ql.cqn.CqnMatchPredicate;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnReference;
import com.sap.cds.ql.cqn.CqnReference.Segment;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnSortSpecification;
import com.sap.cds.ql.cqn.CqnStatement;
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
import com.sap.cds.ql.cqn.CqnValue;
import com.sap.cds.ql.cqn.CqnVisitor;
import com.sap.cds.reflect.CdsAnnotation;
import com.sap.cds.reflect.CdsBaseType;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.util.CdsModelUtils;
import com.sap.cds.util.CqnStatementUtils;

/**
 * Utility class to handle the locale settings
 */
public class LocaleUtils {

	private final CdsModel model;

	public LocaleUtils(CdsModel model) {
		this.model = model;
	}

	/**
	 * Prefix for the localized entities and views
	 */
	private static final String LOCALIZED_PREFIX = "localized";

	/**
	 * Calculates the localized entity name that can be used to access localized
	 * fields
	 *
	 * @param entity the entity
	 * @return the localized entity name
	 */
	public static String localizedEntityName(CdsEntity entity) {
		return LOCALIZED_PREFIX + "." + entity.getQualifiedName();
	}

	/**
	 * Calculates the localized entity name that can be used to access localized
	 * fields
	 *
	 * @param entity the entity name
	 * @return the localized entity name
	 */
	public static String localizedEntityName(String entity) {
		return LOCALIZED_PREFIX + "." + entity;
	}

	/**
	 * Calculates the locale-specific view name for a given locale, which can be
	 * used to access localized fields
	 *
	 * @param entity the entity name
	 * @param locale the locale
	 * @return the locale-specific view name
	 */
	public static String localeSpecificViewName(String entity, Locale locale) {
		return LOCALIZED_PREFIX + "." + getLocaleString(locale) + "." + entity;
	}

	/**
	 * Checks if a given entity name is a localized entity name
	 *
	 * @param entity the entity name
	 * @return true if the given name if a localized entity name
	 */
	public static boolean isLocalizedEntityName(String entity) {
		return entity.startsWith(LOCALIZED_PREFIX + ".");
	}

	/**
	 * Calculates the locale String value from a specific locale that can be used to
	 * access localized attributes fields
	 *
	 * @param locale the locale
	 * @return the locale String value
	 */
	public static String getLocaleString(Locale locale) {
		if (locale != null) {
			String localeStr = locale.getLanguage();
			String country = locale.getCountry();
			if (!country.isEmpty()) {
				localeStr += "_" + country;
			}
			String extension = locale.getExtension('x');
			if (extension != null && !extension.isEmpty()) {
				localeStr += "_" + extension;
			}
			return localeStr;
		}
		return null;
	}

	/**
	 * Checks if an entity is annotated with @cds.localized
	 * 
	 * @param entity the entity to be checked
	 * @return true if the entity is annotated with @cds.localized
	 */
	public static boolean isLocalized(CdsEntity entity) {
		Optional> map = entity.findAnnotation(CDS_LOCALIZED);
		if (map.isPresent() && Boolean.FALSE.equals(map.get().getValue())) {
			return false;
		}
		return entity.concreteNonAssociationElements().anyMatch(CdsElement::isLocalized);
	}

	/**
	 * Checks if some element refs of a structured type has any localized elements
	 *
	 * @param targetType  the structured type to be checked for localized elements
	 * @param elementRefs the element refs to be checked
	 * @return true if the provided elementRefs contain localized elements
	 */
	public static boolean hasLocalizedElements(CdsStructuredType targetType, Collection elementRefs) {
		return elementRefs.stream().map(ref -> CdsModelUtils.element(targetType, ref))
				.anyMatch(CdsElement::isLocalized);
	}

	/**
	 * Checks whether the given statement (including subqueries) has a filter,
	 * WHERE, or HAVING clause, which has an "unsafe" predicate.
	 * 
	 * @param targetType
	 * @param statement  the statement to be checked
	 *
	 * @return true if the statement has a WHERE, GROUPBY or HAVING clause
	 */
	private boolean hasFilterWhereOrHavingClause(CdsStructuredType targetType, CqnFilterableStatement statement) {
		if (!statement.where().map(w -> isSafe(targetType, w)).orElse(true)) {
			return true;
		}

		if (statement.isSelect() && !statement.asSelect().having().map(h -> isSafe(targetType, h)).orElse(true)) {
			return true;
		}

		if (statement.isSelect() && statement.asSelect().from().isRef() || !statement.isSelect()) {
			CqnStructuredTypeRef ref = statement.ref();
			Segment root = ref.rootSegment();
			CdsEntity type = model.getEntity(root.id());
			for (CqnReference.Segment seg : ref.segments()) {
				if (seg != root) {
					type = type.getTargetOf(seg.id());
				}
				Optional filter = seg.filter();
				if (filter.isPresent() && !isSafe(type, filter.get())) {
					return true;
				}
			}
		}

		if (statement.isSelect() && statement.asSelect().from().isSelect()) {

			CqnSelect source = statement.asSelect().from().asSelect();

			hasFilterWhereOrHavingClause(target(source), source);
		}

		return false;
	}

	private CdsStructuredType target(CqnSelect source) {
		return CqnStatementUtils.targetType(model, source);
	}

	private boolean isSafe(CdsStructuredType targetType, CqnPredicate pred) {
		CheckForUnsafePredicatesVisitor v = new CheckForUnsafePredicatesVisitor(targetType);
		pred.accept(v);

		return v.isSafe();
	}

	private class CheckForUnsafePredicatesVisitor implements CqnVisitor {

		private final CdsStructuredType targetType;

		CheckForUnsafePredicatesVisitor(CdsStructuredType targetType) {
			this.targetType = targetType;
		}

		boolean isSafe = true;

		@Override
		public void visit(CqnPredicate pred) {
			isSafe = false;
		}

		@Override
		public void visit(CqnComparisonPredicate cmp) {
			switch (cmp.operator()) {
				case EQ:
				case NE:
				case IS:
				case IS_NOT:
					// safe
					break;
				default:
					if (couldBeString(cmp.left()) || couldBeString(cmp.right())) {
						isSafe = false;
					}
			}
		}

		private boolean couldBeString(CqnValue val) {
			if (val.type().isPresent() && !val.type().get().equals(CdsBaseType.STRING.cdsName())) {
				return false;
			}

			if (targetType != null && val.isRef()) {
				CdsElement element = CdsModelUtils.element(targetType, val.asRef());
				if (!element.getType().isSimpleType(CdsBaseType.STRING)) {
					return false;
				}
			}

			return true;
		}

		@Override
		public void visit(CqnFunc test) {
			// TODO could we relax on fuzzy search?
			// TODO could we relax on case in sensitive comparison
			isSafe = false;
		}

		@Override
		public void visit(CqnExistsSubquery sq) {
			CqnSelect query = sq.subquery();
			CdsStructuredType target = CdsModelUtils.entity(model, query.ref());
			if (hasFilterWhereOrHavingClause(target, query)) {
				isSafe = false;
			}
		}

		@Override
		public void visit(CqnMatchPredicate match) {
			if (!match.predicate().map(p -> LocaleUtils.this.isSafe(null, p)).orElse(true)) {
				isSafe = false; // TODO -> check this!
			}
		}

		@Override
		public void visit(CqnInPredicate in) {
			// safe
		}

		boolean isSafe() {
			return isSafe;
		}
	}

	/**
	 * Checks whether the given statement has a sort spec with a string element.
	 *
	 * The locale parameter is needed to perform locale specific sorting over string
	 * elements. Narrowing this to String and excluding UUID helps to avoid
	 * performance degradation due to not intended sorting over UUIDs
	 *
	 * @param targetType the target type of this select
	 * @param statement  the select statement to be checked
	 * @return true if a sort spec with a string element was found
	 */
	private static boolean hasSortSpecWithStringElement(CdsStructuredType targetType, CqnSelect select) {
		StringTypedSortSpecVisitor visitor = new StringTypedSortSpecVisitor(targetType);
		select.orderBy().forEach(s -> s.accept(visitor));

		return visitor.hasFoundStringTypedSortSpec();
	}

	/**
	 * Checks whether a given {@link CqnStatement} needs to be appended with a
	 * collate clause
	 *
	 * @param statement the statement to be checked
	 * @param locale    the {@link Locale} provided along the statement execution
	 * @return true if a collate clause needs to be appended
	 */
	public boolean collateClauseIsNeeded(CqnFilterableStatement statement, Locale locale) {
		if (locale == null) {
			return false;
		}

		CdsStructuredType targetType;
		if (statement.isSelect()) {
			targetType = CqnStatementUtils.targetType(model, statement.asSelect());
			if (hasSortSpecWithStringElement(targetType, statement.asSelect())) {
				return true;
			}
		} else {
			targetType = CdsModelUtils.entity(model, statement.ref());
		}

		return hasFilterWhereOrHavingClause(targetType, statement);

	}

	private static class StringTypedSortSpecVisitor implements CqnVisitor {
		private final CdsStructuredType targetType;

		public boolean hasFoundStringTypedSortSpec() {
			return foundStringTypedSortSpec;
		}

		private boolean foundStringTypedSortSpec = false;

		public StringTypedSortSpecVisitor(CdsStructuredType targetType) {
			this.targetType = targetType;
		}

		@Override
		public void visit(CqnSortSpecification sortSpec) {

			// inspect value with another visitor as it could sth else than a ref
			sortSpec.value().accept(new CqnVisitor() {
				@Override
				public void visit(CqnElementRef elementRef) {
					// don't overwrite if one was already found
					if (!foundStringTypedSortSpec) {
						CdsElement element = CdsModelUtils.element(targetType, elementRef);
						foundStringTypedSortSpec = element.getType().isSimpleType(CdsBaseType.STRING);
					}
				}
			});
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy