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

com.sap.cds.adapter.odata.v4.metadata.cds.CdsServiceEdmSchema Maven / Gradle / Ivy

There is a newer version: 3.6.0
Show newest version
/**************************************************************************
 * (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
 **************************************************************************/
package com.sap.cds.adapter.odata.v4.metadata.cds;

import static com.sap.cds.adapter.odata.v4.metadata.cds.CdsServiceEdmUtils.fqn;
import static com.sap.cds.adapter.odata.v4.metadata.cds.CdsServiceEdmUtils.isIncluded;
import static com.sap.cds.adapter.odata.v4.metadata.cds.CdsServiceEdmUtils.isParameterized;
import static com.sap.cds.adapter.odata.v4.metadata.cds.CdsServiceEdmUtils.name;
import static com.sap.cds.adapter.odata.v4.metadata.cds.CdsServiceEdmUtils.nameElement;
import static com.sap.cds.adapter.odata.v4.metadata.cds.CdsServiceEdmUtils.nameParameter;
import static com.sap.cds.adapter.odata.v4.metadata.cds.CdsServiceEdmUtils.nameReturn;
import static com.sap.cds.adapter.odata.v4.metadata.cds.CdsServiceEdmUtils.structuredTypeFqn;
import static com.sap.cds.adapter.odata.v4.metadata.cds.CdsServiceEdmUtils.target;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.olingo.commons.api.edm.FullQualifiedName;
import org.apache.olingo.commons.api.edm.geo.SRID;
import org.apache.olingo.commons.api.edm.provider.CsdlAction;
import org.apache.olingo.commons.api.edm.provider.CsdlComplexType;
import org.apache.olingo.commons.api.edm.provider.CsdlEntityContainer;
import org.apache.olingo.commons.api.edm.provider.CsdlEntityType;
import org.apache.olingo.commons.api.edm.provider.CsdlFunction;
import org.apache.olingo.commons.api.edm.provider.CsdlNamed;
import org.apache.olingo.commons.api.edm.provider.CsdlNavigationProperty;
import org.apache.olingo.commons.api.edm.provider.CsdlParameter;
import org.apache.olingo.commons.api.edm.provider.CsdlProperty;
import org.apache.olingo.commons.api.edm.provider.CsdlPropertyRef;
import org.apache.olingo.commons.api.edm.provider.CsdlReturnType;
import org.apache.olingo.commons.api.edm.provider.CsdlSchema;

import com.sap.cds.adapter.odata.v4.utils.mapper.EdmxFlavourMapper;
import com.sap.cds.adapter.odata.v4.utils.mapper.EdmxFlavourMapper.Mapping;
import com.sap.cds.reflect.CdsAction;
import com.sap.cds.reflect.CdsAnnotatable;
import com.sap.cds.reflect.CdsAnnotation;
import com.sap.cds.reflect.CdsArrayedType;
import com.sap.cds.reflect.CdsBaseType;
import com.sap.cds.reflect.CdsBoundAction;
import com.sap.cds.reflect.CdsBoundFunction;
import com.sap.cds.reflect.CdsDefinition;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsFunction;
import com.sap.cds.reflect.CdsKind;
import com.sap.cds.reflect.CdsOperation;
import com.sap.cds.reflect.CdsParameter;
import com.sap.cds.reflect.CdsService;
import com.sap.cds.reflect.CdsSimpleType;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.reflect.CdsType;
import com.sap.cds.services.draft.Drafts;
import com.sap.cds.services.utils.DraftUtils;
import com.sap.cds.services.utils.model.CdsAnnotations;
import com.sap.cds.util.CdsModelUtils;

class CdsServiceEdmSchema extends CsdlSchema {

	private enum EntityTypeKind { DEFAULT, PARAMETERIZED_PARAMETERS, PARAMETERIZED_TYPE }
	private static final record CdsEntityInfo(CdsEntity entity, EntityTypeKind kind) {}
	private static final record CdsOperationInfo(T operation, CdsEntity boundTo) {}

	private final CdsService service;
	private final EdmxFlavourMapper edmxFlavourMapper;

	private final Map entityLookup = new HashMap<>();
	private final Map>> actionLookup = new HashMap<>();
	private final Map>> functionLookup = new HashMap<>();
	private final Map complexTypeLookup = new HashMap<>();
	private final Map cached = new HashMap<>();

	CdsServiceEdmSchema(CdsService service, CsdlEntityContainer entityContainer, EdmxFlavourMapper edmxFlavourMapper) {
		this.service = service;
		this.edmxFlavourMapper = edmxFlavourMapper;

		service.entities().filter(CdsServiceEdmUtils::isIncluded).forEach(e -> {
			if (isParameterized(e)) {
				entityLookup.put(name(e) + "Parameters", new CdsEntityInfo(e, EntityTypeKind.PARAMETERIZED_PARAMETERS));
				entityLookup.put(name(e) + "Type", new CdsEntityInfo(e, EntityTypeKind.PARAMETERIZED_TYPE));
			} else {
				entityLookup.put(name(e), new CdsEntityInfo(e, EntityTypeKind.DEFAULT));
			}
			e.actions().forEach(a -> actionLookup.compute(name(a), (k, v) -> addOrInit(v, new CdsOperationInfo<>(a, e))));
			e.functions().forEach(f -> functionLookup.compute(name(f), (k, v) -> addOrInit(v, new CdsOperationInfo<>(f, e))));
		});
		service.actions().forEach(a -> actionLookup.compute(name(a), (k, v) -> addOrInit(v, new CdsOperationInfo<>(a, null))));
		service.functions().forEach(f -> functionLookup.compute(name(f), (k, v) -> addOrInit(v, new CdsOperationInfo<>(f, null))));

		setNamespace(service.getQualifiedName());
		setEntityContainer(entityContainer);
		setEntityTypes(null);
		setComplexTypes(null);
		setActions(null);
		setFunctions(null);
	}

	@Override
	public CsdlEntityType getEntityType(String name) {
		CdsEntityInfo info = entityLookup.get(name);
		if (info != null) {
			return buildEntityType(info);
		}
		return null;
	}

	@Override
	public List getEntityTypes() {
		return entityLookup.values().stream().map(this::buildEntityType).toList();
	}

	private CsdlEntityType buildEntityType(CdsEntityInfo info) {
		CdsEntity entity = info.entity();
		EntityTypeKind kind = info.kind();
		Function builder = e -> {
			CsdlEntityType entityType = new CsdlEntityType();
			entityType.setKey(new ArrayList<>());
			if (kind == EntityTypeKind.DEFAULT || kind == EntityTypeKind.PARAMETERIZED_TYPE) {
				entityType.setName(name(entity) + (kind == EntityTypeKind.PARAMETERIZED_TYPE ? "Type" : ""));
				entityType.setOpenType(entity.getAnnotationValue("open", false));

				edmxFlavourMapper.createMappings(entity).forEach(mapped -> {
					if (mapped.getTargetElement().getType().isAssociation()) {
						entityType.getNavigationProperties().add(createNavigationProperty(mapped.getEdmxName(), mapped.getTargetElement(), entity));
					} else if (isIncluded(mapped.getTargetElement())) {
						entityType.getProperties().add(createProperty(mapped, entityType.getName()));
						if (mapped.getRootElement().isKey()) {
							entityType.getKey().add(new CsdlPropertyRef().setName(mapped.getEdmxName()));
						}
					}
				});

				if (kind == EntityTypeKind.PARAMETERIZED_TYPE) {
					CsdlNavigationProperty parametersNavigation = new CsdlNavigationProperty();
					parametersNavigation.setName("Parameters");
					parametersNavigation.setType(fqn(entity) + "Parameters");
					entityType.getNavigationProperties().add(parametersNavigation);
				}
			} else {
				entityType.setName(name(entity) + "Parameters");
				entity.params().forEach(parameter -> {
					entityType.getKey().add(new CsdlPropertyRef().setName(parameter.getName()));
					CsdlProperty property = new CsdlProperty();
					property.setName(parameter.getName());
					property.setType(type(parameter.getType(), parameter, CdsServiceEdmUtils::name));
					property.setCollection(parameter.getType().isArrayed());
					property.setNullable(false);
					if (parameter.getType().isSimple()) {
						SimpleTypeProperties properties = simpleTypeProperties(parameter.getType().as(CdsSimpleType.class), property.getType(), parameter);
						property.setMaxLength(properties.maxLength());
						property.setPrecision(properties.precision());
						property.setScale(properties.scale());
						property.setScaleAsString(properties.scaleAsString());
						property.setSrid(properties.srid());
					}
					entityType.getProperties().add(property);
				});

				CsdlNavigationProperty parametersNavigation = new CsdlNavigationProperty();
				parametersNavigation.setName("Set");
				parametersNavigation.setType(fqn(entity) + "Type");
				parametersNavigation.setCollection(true);
				parametersNavigation.setContainsTarget(true);
				entityType.getNavigationProperties().add(parametersNavigation);
			}
			if (entityType.getKey().isEmpty()) {
				entityType.setKey(null);
			}
			return entityType;
		};
		if (kind == EntityTypeKind.DEFAULT) {
			return (CsdlEntityType) cached.computeIfAbsent(entity, builder);
		} else {
			// entity types from parameterized views are not stored in cached
			// as there are two csdl definitions created from the CDS definition
			return (CsdlEntityType) builder.apply(entity);
		}
	}

	@Override
	public CsdlComplexType getComplexType(String name) {
		CdsStructuredType structuredType = complexTypeLookup.get(name);
		if (structuredType != null) {
			return buildComplexType(name, structuredType);
		}
		return null;
	}

	@Override
	public List getComplexTypes() {
		// ensure complex types in all entities, actions and functions are found
		getEntityTypes();
		getActions();
		getFunctions();

		int previousSize;
		List complexTypes;
		do {
			previousSize = complexTypeLookup.size();
			// iteration might detect new complex types
			complexTypes = new HashSet<>(complexTypeLookup.entrySet()).stream().map(e -> buildComplexType(e.getKey(), e.getValue())).toList();
		} while (previousSize != complexTypeLookup.size());
		return complexTypes;
	}

	private CsdlComplexType buildComplexType(String name, CdsStructuredType type) {
		return (CsdlComplexType) cached.computeIfAbsent(type, s -> {
			CsdlComplexType complexType = new CsdlComplexType();
			complexType.setName(name);
			complexType.setOpenType(type.getAnnotationValue("open", false));
			edmxFlavourMapper.createMappings(type).forEach(mapped -> {
				if (mapped.getTargetElement().getType().isAssociation()) {
					complexType.getNavigationProperties().add(createNavigationProperty(mapped.getEdmxName(), mapped.getTargetElement(), type));
				} else if (isIncluded(mapped.getTargetElement())) {
					complexType.getProperties().add(createProperty(mapped, complexType.getName()));
				}
			});
			return complexType;
		});
	}

	private CsdlProperty createProperty(Mapping mapping, String parentName) {
		CsdlProperty property = new CsdlProperty();
		property.setName(mapping.getEdmxName());

		CdsElement element = mapping.getTargetElement();
		property.setType(type(element.getType(), element, def -> nameElement(def, parentName, mapping.getEdmxName())));
		property.setCollection(element.getType().isArrayed());

		boolean notNull = mapping.getRootElement().isKey() || element.isNotNull() || property.isCollection();
		property.setNullable(!notNull); // TODO support explicit null for collections

		if (element.getType().isSimple()) {
			SimpleTypeProperties properties = simpleTypeProperties(element.getType().as(CdsSimpleType.class), property.getType(), element);
			property.setMaxLength(properties.maxLength());
			property.setPrecision(properties.precision());
			property.setScale(properties.scale());
			property.setScaleAsString(properties.scaleAsString());
			property.setSrid(properties.srid());
		}
		return property;
	}

	private CsdlNavigationProperty createNavigationProperty(String mappedName, CdsElement assoc, CdsStructuredType declarator) {
		CdsEntity target = target(declarator, assoc);
		CsdlNavigationProperty navProperty = new CsdlNavigationProperty();
		navProperty.setName(mappedName);
		navProperty.setType(fqn(target) + (isParameterized(target) ? "Parameters" : ""));
		navProperty.setCollection(CdsModelUtils.isToMany(assoc.getType()));
		navProperty.setNullable(!assoc.isNotNull());
		navProperty.setContainsTarget(declarator.getAnnotationValue("odata.contained",
				DraftUtils.isDraftEnabled(declarator) && assoc.getName().equals(Drafts.DRAFT_ADMINISTRATIVE_DATA)));
		return navProperty;
	}

	@Override
	public List getActions(String name) {
		List> actions = actionLookup.get(name);
		if (actions != null) {
			return actions.stream().map(this::buildAction).toList();
		}
		return Collections.emptyList();
	}

	@Override
	public List getActions() {
		return actionLookup.values().stream().flatMap(Collection::stream).map(this::buildAction).toList();
	}

	private CsdlAction buildAction(CdsOperationInfo info) {
		CdsAction action = info.operation();
		return (CsdlAction) cached.computeIfAbsent(action, a -> {
			CsdlAction csdlAction = new CsdlAction();
			csdlAction.setName(name(action));
			if (action instanceof CdsBoundAction boundAction) {
				csdlAction.setBound(true);
				csdlAction.getParameters().add(createBindingParameter(boundAction.getBindingParameter(), action, info.boundTo()));
			}
			action.parameters().forEach(parameter ->
				csdlAction.getParameters().add(createParameter(parameter, action))
			);
			action.returnType().ifPresent(type -> csdlAction.setReturnType(createReturnType(type, action)));
			return csdlAction;
		});
	}

	@Override
	public List getFunctions(String name) {
		List> functions = functionLookup.get(name);
		if (functions != null) {
			return functions.stream().map(this::buildFunction).toList();
		}
		return Collections.emptyList();
	}

	@Override
	public List getFunctions() {
		return functionLookup.values().stream().flatMap(Collection::stream).map(this::buildFunction).toList();
	}

	private CsdlFunction buildFunction(CdsOperationInfo info) {
		CdsFunction function = info.operation();
		return (CsdlFunction) cached.computeIfAbsent(function, f -> {
				CsdlFunction csdlFunction = new CsdlFunction();
			csdlFunction.setName(name(function));
			if (function instanceof CdsBoundFunction boundFunction) {
				csdlFunction.setBound(true);
				csdlFunction.getParameters().add(createBindingParameter(boundFunction.getBindingParameter(), function, info.boundTo()));
			}
			function.parameters().forEach(parameter ->
				csdlFunction.getParameters().add(createParameter(parameter, function))
			);
			csdlFunction.setReturnType(createReturnType(function.getReturnType(), function));
			return csdlFunction;
		});
	}

	private CsdlParameter createBindingParameter(CdsParameter bindingParameter, CdsOperation operation, CdsEntity boundTo) {
		String bindingParameterName;
		boolean isCollection;
		boolean notNull;
		if (bindingParameter != null) {
			bindingParameterName = bindingParameter.getName();
			isCollection = bindingParameter.getType().isArrayed();
			notNull = bindingParameter.isNotNull();
		} else {
			bindingParameterName = operation.getAnnotationValue("cds.odata.bindingparameter.name", "in");
			isCollection = operation.getAnnotationValue("cds.odata.bindingparameter.collection", false);
			notNull = false;
		}
		CsdlParameter csdlBindingParameter = new CsdlParameter();
		csdlBindingParameter.setName(bindingParameterName);
		csdlBindingParameter.setType(fqn(boundTo) + (isParameterized(boundTo) ? "Type" : ""));
		csdlBindingParameter.setCollection(isCollection);
		// TODO binding parameter should always be notNull -> CDS Compiler defaults to nullable however
		csdlBindingParameter.setNullable(!notNull && !csdlBindingParameter.isCollection());
		return csdlBindingParameter;
	}

	private CsdlParameter createParameter(CdsParameter parameter, CdsOperation operation) {
		CsdlParameter csdlParameter = new CsdlParameter();
		csdlParameter.setName(parameter.getName());
		csdlParameter.setType(type(parameter.getType(), parameter, def -> nameParameter(def, operation, parameter)));
		csdlParameter.setCollection(parameter.getType().isArrayed());
		csdlParameter.setNullable(!parameter.isNotNull() && !csdlParameter.isCollection()); // TODO support explicit null for collections
		if (parameter.getType().isSimple()) {
			SimpleTypeProperties properties = simpleTypeProperties(parameter.getType().as(CdsSimpleType.class), csdlParameter.getType(), parameter);
			csdlParameter.setMaxLength(properties.maxLength());
			csdlParameter.setPrecision(properties.precision());
			csdlParameter.setScale(properties.scale());
			csdlParameter.setSrid(properties.srid());
		}
		return csdlParameter;
	}

	private CsdlReturnType createReturnType(CdsType type, CdsOperation operation) {
		CsdlReturnType returnType = new CsdlReturnType();
		returnType.setType(type(type, type, def -> nameReturn(def, operation)));
		returnType.setCollection(type.isArrayed());
		returnType.setNullable(!returnType.isCollection()); // TODO isNotNull() on return type + support explicit null for collections?
		if (type.isSimple()) {
			SimpleTypeProperties properties = simpleTypeProperties(type.as(CdsSimpleType.class), returnType.getType(), type);
			returnType.setMaxLength(properties.maxLength());
			returnType.setPrecision(properties.precision());
			returnType.setScale(properties.scale());
			returnType.setSrid(properties.srid());
		}
		return returnType;
	}

	private FullQualifiedName type(CdsType type, CdsAnnotatable declarator, Function anonymousNameSupplier) {
		if (type.isStructured()) {
			CdsStructuredType structuredType = type.as(CdsStructuredType.class);
			FullQualifiedName fqn = structuredTypeFqn(service, structuredType, anonymousNameSupplier);
			if (type.getKind() != CdsKind.ENTITY) {
				complexTypeLookup.put(fqn.getName(), structuredType);
			}
			return fqn;
		} else if (type.isArrayed()) {
			return type(type.as(CdsArrayedType.class).getItemsType(), declarator, anonymousNameSupplier);
		} else if (type.isSimple()) {
			String edmType;
			if (isODataTypeAnnotated(declarator)) {
				edmType = declarator.getAnnotationValue("odata.Type", null);
			} else if (declarator != null && CdsAnnotations.CORE_MEDIA_TYPE.getOrDefault(declarator) != null) {
				edmType = "Edm.Stream";
			} else {
				edmType = simpleType(type.as(CdsSimpleType.class).getType());
			}
			return new FullQualifiedName(edmType);
		}
		return null;
	}

	private static String simpleType(CdsBaseType type) {
		return switch (type) {
			case UUID -> "Edm.Guid";
			case BOOLEAN -> "Edm.Boolean";
			case UINT8, HANA_TINYINT -> "Edm.Byte";
			case INT16, HANA_SMALLINT -> "Edm.Int16";
			case INT32, INTEGER -> "Edm.Int32";
			case INT64, INTEGER64 -> "Edm.Int64";
			case DECIMAL, HANA_SMALLDECIMAL -> "Edm.Decimal";
			case HANA_REAL -> "Edm.Single";
			case DOUBLE -> "Edm.Double";
			case DATE -> "Edm.Date";
			case TIME -> "Edm.TimeOfDay";
			case DATETIME, TIMESTAMP -> "Edm.DateTimeOffset";
			case STRING, LARGE_STRING, HANA_CHAR, HANA_NCHAR, HANA_VARCHAR, HANA_CLOB -> "Edm.String";
			case BINARY, LARGE_BINARY, HANA_BINARY -> "Edm.Binary";
			case HANA_ST_POINT -> "Edm.GeometryPoint";
			case HANA_ST_GEOMETRY -> "Edm.Geometry";
			default -> null;
		};
	}

	private static final Set MAX_LENGTH_FACET_TYPES = Set.of("Edm.String", "Edm.Stream", "Edm.Binary");
	private static final Set PRECISION_FACET_TYPES = Set.of("Edm.Decimal", "Edm.DateTimeOffset", "Edm.Duration", "Edm.TimeOfDay");
	private static final Set SRID_FACET_TYPES = Set.of("Edm.Geography", "Edm.GeographyPoint", "Edm.GeographyLineString",
			"Edm.GeographyPolygon", "Edm.GeographyMultiPoint", "Edm.GeographyMultiLineString", "Edm.GeographyMultiPolygon",
			"Edm.GeographyCollection", "Edm.Geometry", "Edm.GeometryPoint", "Edm.GeometryLineString", "Edm.GeometryPolygon",
			"Edm.GeometryMultiPoint", "Edm.GeometryMultiLineString", "Edm.GeometryMultiPolygon", "Edm.GeometryCollection");
	private static final Set ALL_TYPES = Stream.of(MAX_LENGTH_FACET_TYPES, PRECISION_FACET_TYPES, SRID_FACET_TYPES,
			Set.of("Edm.Boolean", "Edm.Byte", "Edm.Double", "Edm.Guid", "Edm.Int16", "Edm.Int32", "Edm.Int64", "Edm.SByte", "Edm.Single"))
			.flatMap(Set::stream).collect(Collectors.toSet());

	record SimpleTypeProperties(Integer maxLength, Integer precision, Integer scale, String scaleAsString, SRID srid) {}

	private static SimpleTypeProperties simpleTypeProperties(CdsSimpleType simpleType, String edmType, CdsAnnotatable declarator) {
		boolean hasOdataType = isODataTypeAnnotated(declarator);

		Integer maxLength = null;
		if (MAX_LENGTH_FACET_TYPES.contains(edmType)) {
			if (hasOdataType) {
				maxLength = declarator.getAnnotationValue("odata.MaxLength", null);
			} else {
				maxLength = simpleType.get("length");
			}
		}

		Integer precision = null;
		if (PRECISION_FACET_TYPES.contains(edmType)) {
			if (hasOdataType) {
				precision = declarator.getAnnotationValue("odata.Precision", null);
			} else {
				if (simpleType.getType() == CdsBaseType.TIMESTAMP) {
					precision = 7;
				} else {
					precision = simpleType.get("precision");
				}
			}
		}

		Integer scale = null;
		String scaleAsString = null;
		if ("Edm.Decimal".equals(edmType)) {
			if (hasOdataType) {
				Object scaleObj = declarator.getAnnotationValue("odata.Scale", null);
				if (scaleObj instanceof Integer s) {
					scale = s;
					scaleAsString = String.valueOf(s);
				} else if (scaleObj instanceof String s) {
					scaleAsString = s;
				}
			} else {
				scale = simpleType.get("scale");
				if (scale != null) {
					scaleAsString = String.valueOf(scale);
				}
				// default to variable scale if nothing is specified
				if (precision == null && scale == null) {
					scaleAsString = "variable";
				}
			}
		}

		SRID srid = null;
		if (SRID_FACET_TYPES.contains(edmType)) {
			Object sridObj;
			if (hasOdataType) {
				sridObj = declarator.getAnnotationValue("odata.SRID", null);
			} else {
				sridObj = simpleType.get("srid");
			}
			if (sridObj != null) {
				srid = SRID.valueOf(sridObj.toString());
			}
		}
		return new SimpleTypeProperties(maxLength, precision, scale, scaleAsString, srid);
	}

	private static boolean isODataTypeAnnotated(CdsAnnotatable element) {
		return element != null && element.findAnnotation("odata.Type")
				.map(CdsAnnotation::getValue).filter(ALL_TYPES::contains).isPresent();
	}

	private static  List addOrInit(List list, T value) {
		if (list == null) {
			list = new ArrayList<>();
		}
		list.add(value);
		return list;
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy