Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

public interface MtxSidecarClient {

String getOrdDocument();
byte[] getOrdDocument();

String getOrdResourceDefinition(String resource);
byte[] getOrdResourceDefinition(String resource);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import static com.sap.cds.services.request.RequestContext.getCurrent;
import static com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor.getHttpClient;
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor.getDestination;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.emptySet;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.io.IOUtils.toByteArray;
import static org.apache.hc.core5.http.ContentType.APPLICATION_JSON;

import com.sap.cds.CdsData;
Expand All @@ -22,7 +22,6 @@
import java.io.IOException;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.commons.io.IOUtils;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
Expand All @@ -38,24 +37,24 @@ public class MtxSidecarClientImpl implements MtxSidecarClient {
private final CdsRuntime cdsRuntime;

@Override
public String getOrdDocument() {
public byte[] getOrdDocument() {
return doPost(API_PATH_GET_ORD_DOCUMENT, asGetOrdDocumentRequestEntity(getCurrent(cdsRuntime)));
}

@Override
public String getOrdResourceDefinition(String resource) {
public byte[] getOrdResourceDefinition(String resource) {
return doPost(
API_PATH_GET_ORD_RESOURCE_DEFINITION,
asGetOrdResourceDefinitionRequestEntity(getCurrent(cdsRuntime), resource));
}

private String doPost(String path, HttpEntity entity) {
private byte[] doPost(String path, HttpEntity entity) {
try {
return getHttpClient(getDestination(MTX_PROVISIONING_SERVICE_DESTINATION))
.execute(
ClassicRequestBuilder.post(path).setEntity(entity).build(),
response -> IOUtils.toString(
assertSuccessful(response).getEntity().getContent(), UTF_8));
response -> toByteArray(
assertSuccessful(response).getEntity().getContent()));
} catch (IOException exception) {
throw new ServiceException("Request to sidecar failed", exception);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
import static com.sap.cds.feature.ord.common.Utils.Exceptions.asLocalizedErrorMessage;
import static com.sap.cds.services.utils.CdsErrorStatuses.ERROR_READING_ORD_DOCUMENT;
import static java.lang.Thread.currentThread;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.Spliterator.ORDERED;
import static java.util.Spliterators.spliteratorUnknownSize;
import static java.util.stream.StreamSupport.stream;

import com.sap.cds.feature.ord.processor.CdsOrdNodeProcessor;
import com.sap.cds.feature.ord.core.OrdJsonInputStream;
import com.sap.cds.feature.ord.core.customizers.CdsOrdNodeCustomizer;
import com.sap.cds.feature.ord.core.generators.CdsOrdNodeGenerator;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.environment.CdsProperties.Model.Provider;
import com.sap.cds.services.environment.CdsProperties.ODataV4;
Expand All @@ -28,7 +32,6 @@
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.core5.http.ClassicHttpResponse;
Expand Down Expand Up @@ -82,12 +85,13 @@ public static ClassicHttpResponse assertSuccessful(ClassicHttpResponse response)
public static class Streams {

public static <T> Stream<T> asStream(Iterable<T> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
return stream(iterable.spliterator(), false);
}

public static <T> List<T> asList(Iterator<T> iterator) {
return StreamSupport.stream(spliteratorUnknownSize(iterator, ORDERED), false)
.toList();
return isNull(iterator)
? List.of()
: stream(spliteratorUnknownSize(iterator, ORDERED), false).toList();
}
}

Expand All @@ -106,9 +110,9 @@ public static InputStream getResourceAsStream(String path) {
}

public static InputStream asOrdJsonInputStream(
InputStream inputStream, List<CdsOrdNodeProcessor> cdsOrdNodeProcessors) {
InputStream inputStream, List<CdsOrdNodeGenerator> generators, List<CdsOrdNodeCustomizer> customizers) {
try {
return new OrdJsonInputStream(inputStream, cdsOrdNodeProcessors);
return new OrdJsonInputStream(inputStream, generators, customizers);
} catch (IOException exception) {
throw new ErrorStatusException(ERROR_READING_ORD_DOCUMENT, "documents/ord-document", exception);
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
/*
* © 2026 SAP SE or an SAP affiliate company. All rights reserved.
*/
package com.sap.cds.feature.ord.common;
package com.sap.cds.feature.ord.core;

import static com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_SINGLE_QUOTES;
import static com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES;
import static com.fasterxml.jackson.core.JsonToken.END_ARRAY;
import static com.fasterxml.jackson.core.JsonToken.END_OBJECT;
import static com.fasterxml.jackson.core.JsonToken.START_ARRAY;
import static com.fasterxml.jackson.databind.DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS;
import static com.google.common.primitives.Bytes.asList;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.stream.Collectors.joining;
import static org.apache.commons.lang3.ObjectUtils.firstNonNull;
import static org.apache.commons.lang3.StringUtils.isEmpty;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonStreamContext;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sap.cds.feature.ord.processor.CdsOrdNodeProcessor;
import com.sap.cds.feature.ord.core.customizers.CdsOrdNodeCustomizer;
import com.sap.cds.feature.ord.core.generators.CdsOrdNodeGenerator;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -37,15 +40,19 @@ public class OrdJsonInputStream extends InputStream {

private final JsonParser parser;
private final InputStream inputStream;
private final List<CdsOrdNodeProcessor> nodeProcessors;
private final List<CdsOrdNodeGenerator> generators;
private final List<CdsOrdNodeCustomizer> customizers;

private JsonToken token;
private JsonToken previous;
private final Deque<Byte> buffer = new ArrayDeque<>();

public OrdJsonInputStream(InputStream inputStream, List<CdsOrdNodeProcessor> nodeProcessors) throws IOException {
public OrdJsonInputStream(
InputStream inputStream, List<CdsOrdNodeGenerator> generators, List<CdsOrdNodeCustomizer> customizers)
throws IOException {
this.previous = null;
this.nodeProcessors = List.copyOf(nodeProcessors);
this.generators = List.copyOf(generators);
this.customizers = List.copyOf(customizers);
this.inputStream = new BufferedInputStream(inputStream);
this.parser = MAPPER.getFactory().createParser(this.inputStream);
this.token = this.parser.nextToken();
Expand All @@ -68,70 +75,56 @@ public int read() throws IOException {
return -1;
}

if (isFieldName(token)) {
return handleFieldName();
}

if (isStructEnd(token)) {
return handleStructEnd();
}

if (isStructStart(token)) {
return handleStructStart();
}

return handleFieldValue();
return switch (token) {
case FIELD_NAME -> handleFieldName();
case END_ARRAY, END_OBJECT -> handleStructEnd();
case START_ARRAY, START_OBJECT -> handleStructStart();
default -> handleFieldValue();
};
}

private void appendToBuffer(String... values) {
Arrays.stream(values) //
.forEach(value -> buffer.addAll(asList(value.getBytes(UTF_8))));
.map(value -> value.getBytes(UTF_8)) //
.forEach(value -> buffer.addAll(asList(value)));
}

private int handleFieldName() throws IOException {
String current = parser.currentName();
List<CdsOrdNodeProcessor> processors = lookupCdsOrdNodeProcessors(current);
JsonToken next = processors.isEmpty() ? null : parser.nextToken();
List<CdsOrdNodeCustomizer> customizers = lookupCustomizers(parser.getParsingContext());

appendToBuffer(
isStructStart(previous) ? "" : ",",
"\"",
current,
"\": ",
processors.isEmpty() ? "" : process(current, parser.readValueAsTree(), processors));
appendToBuffer(isStructStart(previous) ? "" : ",", format("\"%s\": ", current));
if (!customizers.isEmpty() && nonNull(parser.nextToken())) {
appendToBuffer(process(current, parser.readValueAsTree(), customizers));
}

previous = next == null ? token : (START_ARRAY == next ? END_ARRAY : END_OBJECT);
previous = firstNonNull(parser.currentToken(), parser.getLastClearedToken());
token = parser.nextToken();

return buffer.removeFirst() & 0xFF;
}

private int handleStructEnd() throws IOException {
JsonToken next = parser.nextToken();

if (isNull(next)) { // We're at the end of the JSON file
nodeProcessors.stream().filter(CdsOrdNodeProcessor::canGenerate).forEach(processor -> processor
.<JsonNode>process(null, null)
.ifPresent(node ->
appendToBuffer(", \"" + processor.getGeneratedNodeName() + "\": ", node.toPrettyString())));
}
JsonStreamContext context = parser.getParsingContext();
String generated = generators.stream()
.filter(generator -> generator.predicate().test(context))
.map(generator -> generator.generate(context))
.collect(joining(", "));

appendToBuffer(token.asString());
appendToBuffer((isStructStart(previous) || isEmpty(generated) ? "" : ","), generated, token.asString());

previous = token;
token = (next != null) ? next : parser.nextToken();
previous = firstNonNull(parser.currentToken(), parser.getLastClearedToken());
token = parser.nextToken();

return buffer.removeFirst() & 0xFF;
}

private int handleFieldValue() throws IOException {
appendToBuffer(
isScalarValue(previous) ? "," : "",
isValueString(token) ? "\"" : "",
isValueString(token) ? parser.getValueAsString() : token.asString(),
isValueString(token) ? "\"" : "");
!isValueString(token) ? parser.getValueAsString() : format("\"%s\"", parser.getValueAsString()));

previous = token;
previous = firstNonNull(parser.currentToken(), parser.getLastClearedToken());
token = parser.nextToken();

return buffer.removeFirst() & 0xFF;
Expand All @@ -140,22 +133,18 @@ private int handleFieldValue() throws IOException {
private int handleStructStart() throws IOException {
appendToBuffer(isStructEnd(previous) ? "," : "", token.asString());

previous = token;
previous = firstNonNull(parser.currentToken(), parser.getLastClearedToken());
token = parser.nextToken();

return buffer.removeFirst() & 0xFF;
}

private List<CdsOrdNodeProcessor> lookupCdsOrdNodeProcessors(String node) {
return nodeProcessors.stream() //
.filter(processor -> processor.predicate().test(node)) //
private List<CdsOrdNodeCustomizer> lookupCustomizers(JsonStreamContext context) {
return customizers.stream() //
.filter(processor -> processor.predicate().test(context)) //
.toList();
}

private static boolean isFieldName(JsonToken token) {
return nonNull(token) && JsonToken.FIELD_NAME == token;
}

private static boolean isStructEnd(JsonToken token) {
return nonNull(token) && token.isStructEnd();
}
Expand All @@ -172,12 +161,9 @@ private static boolean isStructStart(JsonToken token) {
return nonNull(token) && token.isStructStart();
}

private static String process(String name, JsonNode node, List<CdsOrdNodeProcessor> processors) {
return processors.stream()
.reduce(
node,
(current, processor) -> processor.process(name, current).orElse(current),
(a, b) -> a)
private static String process(String name, JsonNode node, List<CdsOrdNodeCustomizer> customizers) {
return customizers.stream()
.reduce(node, (current, processor) -> processor.customize(name, current), (a, b) -> a)
.toPrettyString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* © 2026 SAP SE or an SAP affiliate company. All rights reserved.
*/
package com.sap.cds.feature.ord.core.customizers;

import com.fasterxml.jackson.core.JsonStreamContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.sap.cds.services.runtime.CdsRuntimeAware;
import java.util.function.Predicate;

public interface CdsOrdNodeCustomizer extends CdsRuntimeAware {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, CdsOrdNodeCustomizer is for modifying content while CdsOrdNodeGenerator is for contributing new things, correct?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. The customizers can only modify existing nodes in the JSON tree. Generators - on the contrary - they provide new nodes on arbitrary positions in the document.


/**
* Returns a predicate that determines whether the customizer should process a node with the given
* name.
*
* @return the predicate
*/
Predicate<JsonStreamContext> predicate();

/**
* Customize the given node.
*
* @param nodeName the name of the node
* @param node the node to process
* @return the processed node
*/
JsonNode customize(String nodeName, JsonNode node);
}
Loading
Loading