
com.sap.cds.impl.localized.LocaleUtils Maven / Gradle / Ivy
/************************************************************************
* © 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