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

com.sap.cds.reflect.impl.CdsModelReader Maven / Gradle / Ivy

There is a newer version: 3.6.1
Show newest version
/************************************************************************
 * (C) 2020-2023 SAP SE or an SAP affiliate company. All rights reserved. *
 ************************************************************************/
package com.sap.cds.reflect.impl;

import static com.sap.cds.reflect.impl.CdsOperationReader.readParameterList;
import static com.sap.cds.reflect.impl.CdsOperationReader.readReturnType;
import static com.sap.cds.reflect.impl.CdsStructuredTypeReader.readElementList;
import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.common.io.CharStreams;
import com.sap.cds.CdsException;
import com.sap.cds.reflect.CdsAction;
import com.sap.cds.reflect.CdsFunction;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsParameter;
import com.sap.cds.reflect.impl.CdsEntityReader.CdsParameterReader;
import com.sap.cds.reflect.impl.reader.issuecollector.IssueCollector;
import com.sap.cds.reflect.impl.reader.issuecollector.IssueCollectorFactory;
import com.sap.cds.reflect.impl.reader.model.CdsConstants;
import com.sap.cds.util.NameResolver;
import com.sap.cds.util.StructuredTypeResolver;

public class CdsModelReader implements CdsModel.Reader {
	private static final IssueCollector issueCollector = IssueCollectorFactory.getIssueCollector(CdsModelReader.class);
	private static final ObjectMapper jackson = new ObjectMapper();

	private static final HashFunction hasher = Hashing.goodFastHash(160);
	private static final Cache nonDraftModels = Caffeine.newBuilder().weakValues().maximumSize(5)
			.build();
	private static final Cache draftModels = Caffeine.newBuilder().weakValues().maximumSize(5)
			.build();

	private final CdsModelBuilder cdsModel = CdsModelBuilder.create();

	private NameResolver nameResolver;
	private StructuredTypeResolver structResolver;
	private final Config config;

	// called by service loader for public API
	public CdsModelReader() {
		this(new CdsModelReader.Config.Builder().build());
	}

	private CdsModelReader(Config config) {
		this.config = config;
	}

	@Override
	public CdsModel readCsn(InputStream is) {
		return read(is);
	}

	@Override
	public CdsModel readCsn(String csn) {
		return read(new CdsModelReader.Config.Builder().build(), csn, false);
	}

	public static CdsModel read(InputStream is) {
		return read(new CdsModelReader.Config.Builder().build(), is, false);
	}

	/**
	 * @deprecated use
	 *             {@link CdsModelReader#read(CdsModelReader.Config config, InputStream is, boolean adaptDraftEntities)}
	 *             instead
	 * @param is                 the csn as InputStream
	 * @param adaptDraftEntities whether to add draft transformations
	 * @return the CDS model
	 */
	@Deprecated
	public static CdsModel read(InputStream is, boolean adaptDraftEntities) {
		return read(new CdsModelReader.Config.Builder().setIncludeUIAnnotations(true).build(), is, adaptDraftEntities);
	}

	/**
	 * @deprecated use
	 *             {@link CdsModelReader#read(CdsModelReader.Config config, String csn, boolean adaptDraftEntities)}
	 *             instead
	 * @param csn                the csn as String
	 * @param adaptDraftEntities whether to add draft transformations
	 * @return the CDS model
	 */
	@Deprecated
	public static CdsModel read(String csn, boolean adaptDraftEntities) {
		return read(new CdsModelReader.Config.Builder().setIncludeUIAnnotations(true).build(), csn, adaptDraftEntities);
	}

	public static CdsModel read(CdsModelReader.Config config, InputStream is, boolean adaptDraftEntities) {
		if (is == null) {
			throw new CdsException("Cannot read CDS model: InputStream must not be null");
		}
		try (final Reader reader = new InputStreamReader(is, UTF_8)) {
			return read(config, CharStreams.toString(reader), adaptDraftEntities);
		} catch (Exception e) {
			throw new CdsException("Cannot read CDS model: ", e);
		}
	}

	public static CdsModel read(CdsModelReader.Config config, JsonNode jObject) {
		return read(config, jObject, false);
	}

	public static CdsModel read(CdsModelReader.Config config, JsonNode jObject, boolean adaptDraftEntities) {
		return read(config, jObject.toString(), adaptDraftEntities);
	}

	public static CdsModel read(CdsModelReader.Config config, String csn, boolean adaptDraftEntities) {
		return read(config, Collections.singletonList(csn), adaptDraftEntities);
	}

	public static CdsModel read(CdsModelReader.Config config, List csnList, boolean adaptDraftEntities) {
		return readCached(config, csnList, adaptDraftEntities);
	}

	private static CdsModel readCached(CdsModelReader.Config config, List csnList, boolean adaptDraftEntities) {
		HashCode hash = hasher.hashString(csnList.stream().collect(Collectors.joining()), UTF_8);

		Cache cache = adaptDraftEntities ? draftModels : nonDraftModels;
		try {
			return cache.get(hash, h -> parse(config, csnList, adaptDraftEntities));
		} catch (RuntimeException e) {
			throw new CdsException("Cannot load CDS model: ", e);
		}
	}

	private static CdsModel parse(CdsModelReader.Config config, List csnList, boolean adaptDraftEntities) {
		try {
			List jObjects = new ArrayList<>();
			for (String csn : csnList) {
				jObjects.add(jackson.readTree(csn));
			}
			return new CdsModelReader(config).parseNodes(jObjects, adaptDraftEntities);
		} catch (Exception e) { // NOSONAR
			throw new CdsException("Cannot parse CDS model: ", e);
		}
	}

	private static JsonNode asObject(JsonNode element) {
		if (element != null) {
			return element;
		}
		return new ObjectNode(null);
	}

	private CdsModel parseNodes(List csnList, boolean adaptDraftEntities) {
		Set qualifiedDefinitionNames = new HashSet();
		Map typeObjects = new HashMap<>();
		Map contextObjects = new HashMap<>();
		Map serviceObjects = new HashMap<>();
		Map entityObjects = new HashMap<>();
		Map actionObjects = new HashMap<>();
		Map functionObjects = new HashMap<>();
		Map eventObjects = new HashMap<>();
		Map structuredObjects = new HashMap<>();
		DraftAdapter draftAdapter = new DraftAdapter(adaptDraftEntities, entityObjects, serviceObjects);

		for (JsonNode csn : csnList) {
			JsonNode definitions = asObject(csn.get(CdsConstants.DEFINITIONS));
			Iterator> fields = definitions.fields();
			while (fields.hasNext()) {
				String name = fields.next().getKey();
				JsonNode object = definitions.get(name);
				String kind = object.get(CdsConstants.KIND).asText();
				switch (kind) {
				case CdsConstants.ASPECT:
				case CdsConstants.TYPE:
					typeObjects.put(name, object);
					break;
				case CdsConstants.ENTITY:
					entityObjects.put(name, object);
					draftAdapter.processEntity(name, object);
					break;
				case CdsConstants.ACTION:
					actionObjects.put(name, object);
					break;
				case CdsConstants.FUNCTION:
					functionObjects.put(name, object);
					break;
				case CdsConstants.CONTEXT:
					contextObjects.put(name, object);
					break;
				case CdsConstants.SERVICE:
					serviceObjects.put(name, object);
					break;
				case CdsConstants.EVENT:
					eventObjects.put(name, object);
					break;
				default:
					issueCollector.unrecognized(name,
							"The CDS model contains a definition with name '%s' that has an unrecognized type '%s'.",
							name, kind);
				}
			}
		}
		qualifiedDefinitionNames.addAll(entityObjects.keySet());
		qualifiedDefinitionNames.addAll(eventObjects.keySet());
		qualifiedDefinitionNames.addAll(typeObjects.keySet());
		qualifiedDefinitionNames.addAll(actionObjects.keySet());
		qualifiedDefinitionNames.addAll(functionObjects.keySet());
		nameResolver = new NameResolver(qualifiedDefinitionNames, serviceObjects.keySet());

		draftAdapter.adaptDraftEntities();
		// meta is always read from first CSN
		readMetaInfo(csnList.get(0));
		readEntities(entityObjects);
		readServices(serviceObjects);
		readUnboundActions(actionObjects);
		readUnboundFunctions(functionObjects);
		readTypes(typeObjects);
		readAnnotations(contextObjects);

		structuredObjects.putAll(entityObjects);
		structuredObjects.putAll(eventObjects);
		structuredObjects.putAll(typeObjects);
		structResolver = new StructuredTypeResolver(structuredObjects);
		readEvents(eventObjects);
		addElementsToTypes(typeObjects);
		addTypesToArrayedTypes(typeObjects);
		addElementsAndParamsToEntities(entityObjects);
		addElementsToUnboundActions(actionObjects);
		addElementsToUnboundFunctions(functionObjects);
		addElementsToBoundActions(entityObjects);
		addElementsToBoundFunctions(entityObjects);

		return cdsModel.build();
	}

	private void readMetaInfo(JsonNode csn) {
		if (csn.has(CdsConstants.VERSION)) {
			JsonNode version = asObject(csn.get(CdsConstants.VERSION));
			cdsModel.addMeta(CdsConstants.VERSION, version.get(CdsConstants.CSN).asText());
		}
		if (csn.has(CdsConstants.META)) {
			JsonNode meta = asObject(csn.get(CdsConstants.META));
			meta.fields().forEachRemaining(e -> {
				Object value;
				try {
					value = jackson.readValue(e.getValue().toString(), TypeFactory.unknownType());
				} catch (IOException e1) {
					value = e.getValue().toString();
				}
				cdsModel.addMeta(e.getKey(), value);
			});
		}
	}

	private void readEntities(Map entityObjects) {
		entityObjects.forEach((qualifiedName, o) -> {
			CdsEntityBuilder entity = CdsEntityReader.read(config, nameResolver.getDefinitionName(qualifiedName),
					qualifiedName, o);
			cdsModel.addEntity(entity);
		});
	}

	private void readServices(Map serviceDefs) {
		for (Entry entry : serviceDefs.entrySet()) {
			CdsServiceBuilder service = new CdsServiceBuilder(CdsAnnotationReader.read(config, entry.getValue()),
					entry.getKey());
			cdsModel.addService(service);
		}
	}

	private void readTypes(Map typeObjects) {
		for (Entry entry : typeObjects.entrySet()) {
			CdsTypeBuilder type = readTypeDefinition(entry.getKey(), entry.getValue(), typeObjects);
			cdsModel.addType(entry.getKey(), type);
		}
	}

	private CdsTypeBuilder readTypeDefinition(String path, JsonNode csn, Map typeDefs) {
		if (csn.has(CdsConstants.ELEMENTS)) {
			// return empty struct as a placeholder
			return CdsStructuredTypeReader.readWithoutElements(config, path, nameResolver.getDefinitionName(path), csn);
		}
		if (csn.has(CdsConstants.ITEMS)) {
			return new CdsArrayedTypeReader(config).readWithoutType(path, nameResolver.getDefinitionName(path), csn);
		}
		if (csn.has(CdsConstants.TYPE)) {
			JsonNode typeName = csn.get(CdsConstants.TYPE);
			String type = typeName.asText();
			if (type.equals(CdsConstants.ASSOCIATION) || type.equals(CdsConstants.COMPOSITION)) {
				return new CdsAssociationReader(config, cdsModel, null).read(path, csn);
			}
			if (type.startsWith("cds.")) {
				return CdsSimpleTypeReader.read(config, path, nameResolver.getDefinitionName(path), csn);
			}
			if (typeDefs.containsKey(type)) {
				// Resolve types that are based on other custom types
				// only relevant if CSN has not been transformed 4 odata
				// TODO support derived types
				JsonNode jsonNode = typeDefs.get(type);
				return readTypeDefinition(type, jsonNode, typeDefs);
			}
		}
		throw new InvalidCsnException("Failed to read type " + path);
	}

	private void readAnnotations(Map contextObjects) {
		contextObjects.forEach((key, value) -> cdsModel.addAnnotations(key, CdsAnnotationReader.read(config, value)));
	}

	private void addElementsToTypes(Map typeObjects) {
		for (Entry entry : typeObjects.entrySet()) {
			Optional> typeDefinition = cdsModel.findType(entry.getKey());
			if (typeDefinition.isPresent()) {
				CdsTypeBuilder type = typeDefinition.get();
				if (type.isStructured()) {
					CdsStructuredTypeBuilder structuredType = (CdsStructuredTypeBuilder) type;
					List> elements = readElementList(config, entry.getKey(), entry.getValue(),
							cdsModel, structResolver);
					structuredType.addElements(elements);
				}
			}
		}
	}

	private void addTypesToArrayedTypes(Map typeObjects) {
		for (Entry entry : typeObjects.entrySet()) {
			Optional> typeDefinition = cdsModel.findType(entry.getKey());
			if (typeDefinition.isPresent()) {
				CdsTypeBuilder type = typeDefinition.get();
				if (type.isArrayed()) {
					CdsArrayedTypeBuilder arrayedType = (CdsArrayedTypeBuilder) type;
					JsonNode itemsTypeJSON = entry.getValue().get(CdsConstants.ITEMS);
					CdsTypeBuilder itemsType = findType(itemsTypeJSON, cdsModel)
							.orElseGet(() -> readType(config, "", itemsTypeJSON, cdsModel, structResolver));
					arrayedType.setItemsType(itemsType);
				}
			}
		}
	}

	private void readEvents(Map eventObjects) {
		for (Entry entry : eventObjects.entrySet()) {
			CdsEventBuilder event = CdsEventReader.read(config, entry.getKey(),
					nameResolver.getDefinitionName(entry.getKey()), entry.getValue(), cdsModel, structResolver);
			cdsModel.addEvent(event);
		}
	}

	private void addElementsAndParamsToEntities(Map entityObjects) {
		for (Entry entry : entityObjects.entrySet()) {
			cdsModel.findEntity(entry.getKey()).ifPresent(entity -> {
				List> elements = readElementList(config, entry.getKey(), entry.getValue(),
						cdsModel, structResolver);
				entity.addElements(elements);

				List params = CdsParameterReader.read(config, entity.getQualifiedName(), entry.getValue(),
						cdsModel::findType);
				entity.addParams(params);
			});
		}
	}

	private void addElementsToBoundActions(Map entityObjects) {
		Stream boundedActionEntities = cdsModel.concreteEntities()
				.filter(e -> e.actions().findFirst().isPresent());

		boundedActionEntities.forEach(e -> {
			e.actions().forEach(a -> {
				JsonNode actionNode = entityObjects.get(e.getQualifiedName()).get(CdsConstants.ACTIONS)
						.get(a.getQualifiedName());
				List params = readParameterList(config, a.getQualifiedName(), actionNode, cdsModel,
						structResolver);
				a.addParameters(params);
				a.setReturnType(readReturnType(config, actionNode, cdsModel, structResolver));
			});
		});
	}

	private void addElementsToBoundFunctions(Map entityObjects) {
		Stream boundedFunctionEntities = cdsModel.concreteEntities()
				.filter(e -> e.functions().findFirst().isPresent());

		boundedFunctionEntities.forEach(e -> {
			e.functions().forEach(f -> {
				JsonNode functionNode = entityObjects.get(e.getQualifiedName()).get(CdsConstants.ACTIONS)
						.get(f.getQualifiedName());
				List params = readParameterList(config, f.getQualifiedName(), functionNode,
						cdsModel, structResolver);
				f.addParameters(params);
				f.setReturnType(readReturnType(config, functionNode, cdsModel, structResolver));
			});
		});
	}

	private void addElementsToUnboundActions(Map actionObjects) {
		for (Entry entry : actionObjects.entrySet()) {
			Optional> actionOptional = cdsModel.findAction(entry.getKey());
			if (actionOptional.isPresent()) {
				CdsOperationBuilder action = actionOptional.get();
				List params = readParameterList(config, entry.getKey(), entry.getValue(), cdsModel,
						structResolver);
				action.addParameters(params);
				action.setReturnType(readReturnType(config, entry.getValue(), cdsModel, structResolver));
			}
		}
	}

	private void addElementsToUnboundFunctions(Map functionObjects) {
		for (Entry entry : functionObjects.entrySet()) {
			Optional> functionOptional = cdsModel.findFunction(entry.getKey());
			if (functionOptional.isPresent()) {
				CdsOperationBuilder function = functionOptional.get();
				List params = readParameterList(config, entry.getKey(), entry.getValue(), cdsModel,
						structResolver);
				function.addParameters(params);
				function.setReturnType(readReturnType(config, entry.getValue(), cdsModel, structResolver));
			}
		}
	}

	private void readUnboundActions(Map actionDefs) {
		for (Entry entry : actionDefs.entrySet()) {
			CdsOperationBuilder action = CdsOperationReader.readAction(config, entry.getKey(),
					nameResolver.getDefinitionName(entry.getKey()), entry.getValue());
			cdsModel.addAction(action);
		}
	}

	private void readUnboundFunctions(Map functionDefs) {
		for (Entry entry : functionDefs.entrySet()) {
			CdsOperationBuilder function = CdsOperationReader.readFunction(config, entry.getKey(),
					nameResolver.getDefinitionName(entry.getKey()), entry.getValue());
			cdsModel.addFunction(function);
		}
	}

	public static CdsTypeBuilder readType(CdsModelReader.Config config, String pathToElement, JsonNode csn,
			CdsModelBuilder model, StructuredTypeResolver structResolver) {
		if (csn.has(CdsConstants.TYPE)) {
			JsonNode typeName = csn.get(CdsConstants.TYPE);
			String type = typeName.asText();
			if (type.equals(CdsConstants.ASSOCIATION) || type.equals(CdsConstants.COMPOSITION)) {
				// inline defined association
				return new CdsAssociationReader(config, model, structResolver).read(pathToElement, csn);
			}
			if (type.startsWith("cds.")) {
				// TODO -> type does not start with cds*?
				return CdsSimpleTypeReader.read(config, pathToElement, "", csn);
			}
			if (typeName.has("ref")) {
				ArrayNode refNode = typeName.withArray("ref");
				if (refNode.size() == 0) {
					throw new CdsException("Empty ref object encoutered for Element " + pathToElement);
				}
				JsonNode elementCsn = structResolver.getElementNode(refNode);
				return findType(elementCsn, model)
						.orElseGet(() -> readType(config, pathToElement, elementCsn, model, structResolver));
			}
		}

		if (csn.has(CdsConstants.ELEMENTS) || csn.has(CdsConstants.PAYLOAD)) {
			// inline defined struct
			return CdsStructuredTypeReader.read(config, "", csn, model, structResolver);
		}

		if (csn.has(CdsConstants.ITEMS)) {
			// inline defined array
			return new CdsArrayedTypeReader(config, model, structResolver).read(pathToElement, csn);
		}

		// computed element without type, e.g. result of function call
		return CdsSimpleTypeReader.read(config, pathToElement, pathToElement, csn);
	}

	public static Optional> findType(JsonNode csn, CdsModelBuilder model) {
		JsonNode typeName = csn.get(CdsConstants.TYPE);
		if (typeName != null && CdsConstants.SELF.equals(typeName.asText())) {
			return Optional.of(CdsSymbolicTypeBuilder.SELF);
		}

		if (typeName != null && !typeName.has("ref")) {
			return model.findType(typeName.asText());
		}
		return Optional.empty();
	}

	/**
	 * Contains configuration options for the {@link CdsModelReader}.
	 */
	public static class Config {

		private boolean readDocs;
		private boolean includeUIAnnotations;

		private Config() {
		}

		public boolean readDocs() {
			return readDocs;
		}

		public boolean includeUIAnnotations() {
			return includeUIAnnotations;
		}

		public static class Builder {

			private boolean readDocs;
			private boolean includeUIAnnotations;

			public Builder setReadDocs(boolean readDocs) {
				this.readDocs = readDocs;
				return this;
			}

			public Builder setIncludeUIAnnotations(boolean includeUIAnnotations) {
				this.includeUIAnnotations = includeUIAnnotations;
				return this;
			}

			public CdsModelReader.Config build() {
				CdsModelReader.Config config = new Config();
				config.readDocs = this.readDocs;
				config.includeUIAnnotations = this.includeUIAnnotations;
				return config;
			}
		}

	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy