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

com.sap.cds.generator.util.NamesUtils Maven / Gradle / Ivy

/************************************************************************
 * © 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
 ************************************************************************/
package com.sap.cds.generator.util;

import static com.sap.cds.generator.util.CaseFormatHelper.toLowerCamel;
import static com.sap.cds.generator.util.CaseFormatHelper.toUpperCamel;
import static com.sap.cds.generator.util.CaseFormatHelper.toUpperUnderscore;

import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.regex.Pattern;

import javax.lang.model.SourceVersion;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.sap.cds.generator.Configuration;
import com.sap.cds.reflect.CdsAnnotatable;
import com.sap.cds.reflect.CdsAnnotation;
import com.sap.cds.reflect.CdsAssociationType;
import com.sap.cds.reflect.CdsDefinition;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsEnumType;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsOperation;
import com.sap.cds.reflect.CdsParameter;
import com.sap.cds.reflect.CdsService;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.reflect.CdsType;
import com.sap.cds.reflect.impl.reader.model.CdsConstants;
import com.palantir.javapoet.ClassName;
import com.palantir.javapoet.NameAllocator;

public class NamesUtils {

	private static final String CDS_MODEL = "CdsModel";
	private static final String UP = "up_";
	private static final String DOT = ".";
	private static final Logger logger = LoggerFactory.getLogger(NamesUtils.class);
	private static final String CONTEXT = "Context";
	private final List excludes;
	private final List includes;
	public static final String ITEM_TYPE_NAME = "Item";
	public static final String OPTIONAL_ARGS_TYPE_NAME = "OptionalArguments";
	public static final String OPTIONAL_ARGS_PARAMETER_NAME = "optionalArguments";

	public NamesUtils(Configuration config) {
		this.excludes = config.getExcludes().stream().map(PatternMatcher::transformPattern).toList();
		this.includes = config.getIncludes().stream().map(PatternMatcher::transformPattern).toList();
	}

	public static String qualifiedWrapperBuilderName(Configuration configuration, CdsDefinition def, String classNameSuffix, boolean isWrapper) {
		boolean betterNames = configuration.getBetterNames();
		String name = (isWrapper ? toUpperCamel(betterNames, unqualifiedContextName(def.getQualifiedName(), def.getName()))
				: toUpperCamel(betterNames, def.getName())) + classNameSuffix;
		if (def.getQualifiedName().equals(def.getName())) {// Wrapper name is 'CdsModel_'
			return name;
		}
		return qualifiedContextName(def.getQualifiedName(), def.getName()) + DOT + name;
	}

	public static String qualifiedContextName(String qualifiedName, String name) {
		if (name.contains(DOT)) {
			return getQualifiedContextNameForDot(qualifiedName, name);
		} else {
			int lastDot = qualifiedName.lastIndexOf('.');
			if (lastDot != -1) {
				return qualifiedName.substring(0, lastDot);
			}
			return CDS_MODEL;
		}
	}

	static String unqualifiedContextName(String qualifiedName, String name) {
		if (name.contains(DOT)) {
			String substring = getQualifiedContextNameForDot(qualifiedName, name);
			if (substring.contains(DOT)) {
				String[] bits = substring.split("\\.");
				return bits[bits.length - 1];
			}
			return substring;
		} else if (qualifiedName.contains(DOT)) {
			String[] bits = qualifiedName.split("\\.");
			return bits[bits.length - 2];
		}
		return CDS_MODEL;
	}

	private static String getQualifiedContextNameForDot(String qualifiedName, String name) {
		String qualifiedContextName = null;
		int endIndex = qualifiedName.lastIndexOf(name) - 1;
		if (endIndex > 0) {
			qualifiedContextName = qualifiedName.substring(0, endIndex);
		}
		if (Strings.isNullOrEmpty(qualifiedContextName)) {
			return CDS_MODEL;
		}
		return qualifiedContextName;
	}

	public static String unqualifiedName(String qualifiedName) {
		int lastDot = qualifiedName.lastIndexOf(DOT);

		return qualifiedName.substring(lastDot + 1);
	}

	public static void warnOnJavaKeywords(String qualifiedName) {
		int lastDot = qualifiedName.lastIndexOf(DOT);
		if (lastDot != -1) {
			String[] split = qualifiedName.split("\\.");
			for (String word : split) {
				if (SourceVersion.isKeyword(word)) {
					logger.warn("The Entity {} contains a reserved Java keyword in its fully qualified name.",
							qualifiedName);
				}
			}
		}
	}

	public static boolean isValidTechnicalEntity(Configuration configuration, CdsModel model, CdsEntity entity) {
		if (configuration.getInterfacesForAspects()) {

			// Texts has no _up association, but generated
			if (entity.getQualifiedName().endsWith("_texts") || entity.getQualifiedName().endsWith(".texts")) {
				return true;
			}

			//REVISIT: This might change when the compiler will introduce
			//marker for generated entities
			boolean hasUpAssociation = hasUpElement(entity);
			if (hasUpAssociation) {
				String parentEntity = prefix(entity.getQualifiedName());
				String association = unqualifiedName(entity.getQualifiedName());
				Optional associationElement = model
					.findEntity(parentEntity).flatMap(e -> e.findElement(association));
				return associationElement
					.filter(e -> e.getType().isAssociation())
					.map(element -> {
						Optional targetAspect = element.getType().as(CdsAssociationType.class).getTargetAspect();
						return targetAspect.isPresent()
							&& !targetAspect.get().isAnonymous();
					}).orElse(false);
			}
			return true;
		} else {
			return !hasUpElement(entity);
		}
	}

	public static String getResolvedWrapperName(String qualifiedBuilderName, String classNameSuffix) {
		return qualifiedBuilderName.substring(0, qualifiedBuilderName.length() - 1) + "Model" + classNameSuffix;
	}

	/**
	 * Generates constant representing this definition without renaming support
	 *
	 * @param configuration the generator {@link Configuration}
	 * @param definition {@link CdsDefinition} to be represented
	 * @return constant name generated "as-is"
	 */
	public static String constantName(Configuration configuration, CdsDefinition definition) {
		return toUpperUnderscore(toUpperCamel(configuration.getBetterNames(), definition.getName()));
	}

	/**
	 * Returns the effective package name of the given {@link CdsDefinition definition}.
	 *
	 * @param definition         the {@link CdsDefinition}
	 * @param basePackage the base package
	 * @return the effective package name
	 */
	public static String packageName(CdsDefinition definition, String basePackage) {
		String packageName = prefix(definition.getQualifiedName(), definition.getName());
		return getPackageName(basePackage, packageName);
	}

	/**
	 * Returns the effective package name of the given {@link CdsService service} definition.
	 *
	 * @param service     the {@link CdsService}
	 * @param basePackage the base package
	 * @return the effective package name
	 */
	public static String packageName(CdsService service, String basePackage) {
		return getPackageName(basePackage, service.getQualifiedName());
	}

	/**
	 * Returns Java class name for given CDS type. The name is always qualified with the
	 * package derived from the namespace of the type and a base package.
	 * Uses @cds.java.this.name as override mechanism.
	 *
	 * @param configuration the instance of {@link Configuration}
	 * @param type the instance of {@link CdsType}
	 * @return instance of the {@link ClassName} representing the Java class name
	 */
	public static ClassName className(Configuration configuration, CdsType type) {
		return ClassName.get(packageName(type, configuration.getBasePackage()), toUpperCamel(configuration.getBetterNames(), getJavaName(type, type::getName)));
	}

	/**
	 * Returns Java class name for given CDS element as a nested class to some
	 * other class. This is useful only for classes that represent nested types.
	 *
	 * @param configuration the generator {@link Configuration}
	 * @param parent {@link ClassName} of the parent class
	 * @param element a {@link CdsElement} that is nested within the parent class
	 * @return instance of the {@link ClassName} representing the Java class name
	 */
	public static ClassName className(Configuration configuration, ClassName parent, CdsElement element) {
		return parent.nestedClass(toUpperCamel(configuration.getBetterNames(), getJavaName(element, element::getName)));
	}

	/**
	 * Returns Java class name for given CDS parameter as a nested class to some
	 * other class. This is useful only for classes that represent nested _anonymous_ types
	 * that never renamed.
	 *
	 * @param configuration the generator {@link Configuration}
	 * @param parent {@link ClassName} of the parent class
	 * @param parameter a {@link CdsParameter} that is nested within the parent class
	 * @return instance of the {@link ClassName} representing the Java class name
	 */
	public static ClassName className(Configuration configuration, ClassName parent, CdsParameter parameter) {
		// We had no support for @cds.java.name there. Not sure if we need one.
		return parent.nestedClass(toUpperCamel(configuration.getBetterNames(), parameter.getName()));
	}

	/**
	 * Returns Java class name for given CDS type with a suffix.
	 * Suffix is defined by the configuration, but is never configurable by the customer.
	 *
	 * @param configuration the instance of {@link Configuration}
	 * @param type the instance of {@link CdsType} that should be suffixed
	 * @return instance of the {@link ClassName} representing the Java class name
	 */
	public static ClassName suffixedClassName(Configuration configuration, CdsType type) {
		return ClassName.get(packageName(type, configuration.getBasePackage()),
			toUpperCamel(configuration.getBetterNames(), getJavaName(type, type::getName)) + configuration.getClassNameSuffix());
	}

	/**
	 * Returns suffixed class name that is nested within the parent class for a given element. E.g. Entity.ReturnType.
	 *
	 * @param configuration the instance of {@link Configuration}
	 * @param parent parent class name as {@link ClassName}
	 * @param element the element that should be nested within the parent class
	 * @return instance of the {@link ClassName} representing the Java class name
	 */
	public static ClassName suffixedClassName(Configuration configuration, ClassName parent, CdsElement element) {
		return parent.nestedClass(toUpperCamel(configuration.getBetterNames(), getJavaName(element, element::getName)) + configuration.getClassNameSuffix());
	}

	/**
	 * Returns qualified typed service class name for the given {@link CdsService service} definition.
	 * NB: This is exception from standard rule
	 *
	 * @param configuration     the generator {@link Configuration}
	 * @param service the {@link CdsService service} definition
	 * @return the full qualified typed service {@link ClassName}
	 */
	public static ClassName typedServiceClassName(Configuration configuration, CdsService service) {
		//NB: service package name "skips" the service name in itself
		String packageName = packageName(service, configuration.getBasePackage());

		return ClassName.get(packageName, toUpperCamel(configuration.getBetterNames(), getJavaName(service, service::getName)));
	}

	/**
	 * Returns qualified typed service class name for the given {@link CdsService service} definition.
	 *
	 * @param configuration the generator {@link Configuration}
	 * @param service the {@link CdsService service} definition
	 * @return the full qualified typed service {@link String}
	 */
	public static ClassName typedServiceBuilderName(Configuration configuration, CdsService service) {
		// Builder is never renamed. REVISIT this
		String packageName = NamesUtils.packageName(service, configuration.getBasePackage());
		return ClassName.get(packageName, toUpperCamel(configuration.getBetterNames(), service.getName()) + configuration.getClassNameSuffix());
	}

	/**
	 * Returns the name of the template handler class for an action or functions. Variant for unbound operation.
	 * Uses @cds.java.name as override mechanism.
	 *
	 * @param configuration the instance of {@link Configuration}
	 * @param operation the @{@link CdsOperation}
	 * @return instance of the {@link ClassName} representing the Java class name
	 */
	public static ClassName templateEventHandlerClassName(Configuration configuration, CdsOperation operation) {
		String operationName = getJavaName(operation, operation::getName);
		String handlerName = "%1$s_%2$s_handler".formatted(unqualifiedName(operation.getQualifier()), operationName);
		return ClassName.get(configuration.getHandlerPackageName(), toUpperCamel(configuration.getBetterNames(), handlerName));
	}

	/**
	 * Returns the name of the template handler class for an action or functions. Variant for bound operation.
	 * Uses @cds.java.name as override mechanism.
	 *
	 * @param configuration the instance of {@link Configuration}
	 * @param entity the bound @{@link CdsEntity}
	 * @param operation the @{@link CdsOperation}
	 * @return instance of the {@link ClassName} representing the Java class name
	 */
	public static ClassName templateEventHandlerClassName(Configuration configuration, CdsEntity entity, CdsOperation operation) {
		String operationName = getJavaName(operation, operation::getName);
		ClassName entityClassName = className(configuration, entity);
		String handlerName = "%1$s_%2$s_%3$s_handler".formatted(
			unqualifiedName(entity.getQualifier()),
			entityClassName.simpleName(),
			operationName);
		return ClassName.get(configuration.getHandlerPackageName(), toUpperCamel(configuration.getBetterNames(), handlerName));
	}

	/**
	 * Returns name for default on-handler method for operations
	 *
	 * @param configuration the instance of {@link Configuration}
	 * @param operation the instance of @{@link CdsOperation}
	 * @return name
	 */
	public static String getOnHandlerMethodName(Configuration configuration, CdsOperation operation) {
		return "handle" + toUpperCamel(configuration.getBetterNames(), getJavaName(operation, operation::getName));
	}

	/**
	 * Returns qualified event context class name for the given CDS definition.
	 *
	 * @param configuration the generator {@link Configuration}
	 * @param boundEntity an optionally bound {@link CdsEntity entity}
	 * @param def         the {@link CdsType}
	 * @return the full qualified event context {@link ClassName}
	 */
	public static ClassName eventContextClassName(Configuration configuration, CdsEntity boundEntity, CdsDefinition def) {
		String packageName = boundEntity != null ? packageName(boundEntity, configuration.getBasePackage())
				: packageName(def, configuration.getBasePackage());

		boolean betterName = configuration.getBetterNames();
		// NB: Keeping the Context within the argument of the toUpperCamel method is buggy,
		// but that was already used by stakeholders
		Optional annotation = def.findAnnotation(CdsConstants.ANNOTATION_CDS_JAVA_THIS_NAME)
				.or(() -> def.findAnnotation(CdsConstants.ANNOTATION_CDS_JAVA_NAME))
				.map(a -> (String) a.getValue());
		if (annotation.isPresent()) {
			String className = contextName(betterName, annotation.get());
			return ClassName.get(packageName, className);
		}

		// use optional entity as prefix, if it's available and the switch "getUniqueEventContexts" is enabled
		String ownName = contextName(betterName, def.getName());
		if (configuration.getUniqueEventContexts() && boundEntity != null) {
			ClassName prefix = className(configuration, boundEntity);
			return ClassName.get(packageName, prefix.simpleName() + ownName);
		} else {
			return ClassName.get(packageName, ownName);
		}
	}

	private static String contextName(boolean betterName, String value) {
		return betterName ? toUpperCamel(true, value) + CONTEXT
				: toUpperCamel(false, value + CONTEXT);
	}

	/**
	 * Returns method name that is derived from element name
	 *
	 * @param configuration the generator {@link Configuration}
	 * @param element the @{@link CdsElement}
	 * @return the name
	 */
	public static String methodName(Configuration configuration, CdsElement element) {
		return toLowerCamel(configuration.getBetterNames(), elementName(element, element::getName));
	}

	/**
	 * Returns class name that is derived from element name. This exists for compatibility reasons
	 * and does not apply transformation
	 *
	 * @param element the @{@link CdsElement}
	 * @return the name
	 */
	public static String rawName(CdsElement element) {
		return elementName(element, element::getName);
	}

	/**
	 * Returns method name that is derived from parameter name, should be used only in fluent mode
	 *
	 * @param configuration the generator {@link Configuration}
	 * @param parameter the @{@link CdsParameter}
	 * @return the name
	 */
	public static String methodName(Configuration configuration, CdsParameter parameter) {
		return toLowerCamel(configuration.getBetterNames(), elementName(parameter, parameter::getName));
	}

	/**
	 * Returns method name that is derived from element name, should be used only in fluent mode
	 *
	 * @param configuration the generator {@link Configuration}
	 * @param operation the @{@link CdsOperation}
	 * @return the name
	 */
	public static String methodName(Configuration configuration, CdsOperation operation) {
		if (!configuration.getBetterNames()) {
			return getJavaName(operation, operation::getName).replace('.', '_');
		} else {
			return toLowerCamel(configuration.getBetterNames(), getJavaName(operation, operation::getName));
		}
	}

	/**
	 * Returns classic setter name for an element
	 *
	 * @param configuration the generator {@link Configuration}
	 * @param element the {@link CdsElement}
	 * @return name
	 */
	public static String setterName(Configuration configuration, CdsElement element) {
		return "set" + toUpperCamel(
			configuration.getBetterNames(),
			elementName(element, element::getName));
	}

	/**
	 * Returns classic getter name for an element
	 *
	 * @param configuration the generator {@link Configuration}
	 * @param element the {@link CdsElement}
	 * @return name
	 */
	public static String getterName(Configuration configuration, CdsElement element) {
		return "get" + toUpperCamel(
			configuration.getBetterNames(),
			elementName(element, element::getName));
	}

	/**
	 * Returns classic setter name for a parameter
	 *
	 * @param configuration the generator {@link Configuration}
	 * @param parameter the {@link CdsParameter}
	 * @return name
	 */
	public static String setterName(Configuration configuration, CdsParameter parameter) {
		return "set" + toUpperCamel(configuration.getBetterNames(), elementName(parameter, parameter::getName));
	}

	/**
	 * Returns classic getter name for a parameter
	 *
	 * @param configuration the generator {@link Configuration}
	 * @param parameter the {@link CdsParameter}
	 * @return name
	 */
	public static String getterName(Configuration configuration, CdsParameter parameter) {
		return "get" + toUpperCamel(
			configuration.getBetterNames(),
			elementName(parameter, parameter::getName));
	}

	/**
	 * Returns argument name that is derived from element name
	 *
	 * @param element the @{@link CdsElement}
	 * @return the name
	 */
	public static String argumentName(Configuration configuration, CdsElement element) {
		return methodName(configuration, element);
	}

	/**
	 * Returns argument name that is derived from parameter name
	 *
	 * @param parameter the @{@link CdsElement}
	 * @return the name
	 */
	public static String argumentName(Configuration configuration, CdsParameter parameter) {
		return methodName(configuration, parameter);
	}

	public static String constantName(CdsElement element) {
		return toUpperUnderscore(elementName(element, element::getName));
	}

	public static String normalizedConstantName(CdsEnumType.Enumeral enumeral) {
		return javaSafeConstantName(getJavaName(enumeral, enumeral::name));
	}

	private static String elementName(CdsAnnotatable element, Supplier defaultValue) {
		Optional> cdsJavaName = Optional.empty();
		// Compatibility: FK setters are never renamed
		if (element.findAnnotation(CdsConstants.ANNOTATION_ODATA_FK4).isEmpty()) {
			// TODO support cds.java.this.name -> currently unexpectedly propagated in reflection API due to derived types handling
			cdsJavaName = element
				.findAnnotation(CdsConstants.ANNOTATION_CDS_JAVA_NAME);
		}

		// This is a compatible variant for method names as they are used in the builder interfaces
		// where the change might break the existing CQN queries.
		return cdsJavaName.map(CdsAnnotation::getValue).orElse(defaultValue.get());
	}

	private static String getJavaName(CdsAnnotatable definition, Supplier defaultNameSupplier) {
		return definition.findAnnotation(CdsConstants.ANNOTATION_CDS_JAVA_THIS_NAME)
				.or(() -> definition.findAnnotation(CdsConstants.ANNOTATION_CDS_JAVA_NAME))
				.map(a -> (String) a.getValue())
				.orElseGet(defaultNameSupplier);
	}

	private static String javaSafeConstantName(String name)	{
		if (SourceVersion.isName(name)) {
			return toUpperUnderscore(name);
		}
		return "_" + toUpperUnderscore(NameAllocator.toJavaIdentifier(name));
	}

	public boolean isExcluded(String qualifiedName) {
		boolean included = includes.isEmpty() || matchesAny(qualifiedName, includes);
		included &= !matchesAny(qualifiedName, excludes);
		return !included;
	}

	@VisibleForTesting
	static String prefix(String qualifiedName, String name) {
		if (qualifiedName.equals(name)) {
			return null;
		} else {
			return qualifiedName.substring(0, qualifiedName.lastIndexOf(name) - 1);
		}
	}

	private static String getPackageName(String basePackage, String packageName) {
		if (!Strings.isNullOrEmpty(basePackage)) {
			packageName = Joiner.on('.').skipNulls().join(basePackage, packageName);
		}
		if (Strings.isNullOrEmpty(packageName)) {
			packageName = "model";
		}
		return packageName.toLowerCase(Locale.US);
	}

	private static boolean matchesAny(String qualifiedName, List patternList) {
		return patternList.stream().anyMatch(pm -> pm.matcher(qualifiedName).matches());
	}

	private static boolean hasUpElement(CdsEntity entity) {
		return entity.elements().anyMatch(e -> e.getName().startsWith(UP) && e.getType().isAssociation() && e.isKey());
	}

	// Entity prefix e.g. Books for Books.texts
	private static String prefix(String qualifiedName) {
		int lastDot = qualifiedName.lastIndexOf('.');
		if (lastDot != -1) {
			return qualifiedName.substring(0, lastDot);
		}
		return null;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy