Skip to content
Merged
4 changes: 2 additions & 2 deletions src/main/java/io/neonbee/data/DataContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
@SuppressWarnings("TypeParameterUnusedInFormals")
public interface DataContext {
/**
* Key for raw body in map.
* Status Code Hint.
*/
String RAW_BODY_KEY = "rawBody";
String STATUS_CODE_HINT = "Status-Code";

/**
* Returns the correlation id of this request.
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/io/neonbee/data/DataRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ public enum ResolutionStrategy {

private boolean localPreferred = true;

private String originalUrl;

/**
* Request data from a DataSource.
*
Expand Down Expand Up @@ -282,6 +284,26 @@ public DataRequest setLocalPreferred(boolean localPreferred) {
return this;
}

/**
* Get the original URL of the request.
*
* @return the original URL
*/
public String getOriginalUrl() {
return originalUrl;
}

/**
* Set the original URL of the request.
*
* @param originalUrl the original URL to set
* @return this DataRequest for chaining
*/
public DataRequest setOriginalUrl(String originalUrl) {
this.originalUrl = originalUrl;
return this;
}

@Override
public String toString() {
return Optional.ofNullable(dataSource).map(Object::getClass).map(Class::getName)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.neonbee.endpoint;

import static io.neonbee.data.DataAction.CREATE;
import static io.neonbee.data.DataAction.DELETE;
import static io.neonbee.data.DataAction.READ;
import static io.neonbee.data.DataAction.UPDATE;
import static io.vertx.core.http.HttpMethod.GET;
import static io.vertx.core.http.HttpMethod.HEAD;
import static io.vertx.core.http.HttpMethod.PATCH;
import static io.vertx.core.http.HttpMethod.POST;
import static io.vertx.core.http.HttpMethod.PUT;

import io.neonbee.data.DataAction;
import io.vertx.core.http.HttpMethod;

/**
* Utility class to map HTTP methods to DataActions.
*/
public class HttpMethodToDataActionMapper {

/**
* Maps an HTTP method to a DataAction.
*
* @param method The HTTP method
* @return The corresponding DataAction, or null if no mapping exists
*/
public static DataAction mapMethodToAction(HttpMethod method) {
if (POST.equals(method)) {
return CREATE;
} else if (HEAD.equals(method) || GET.equals(method)) {
return READ;
} else if (PUT.equals(method) || PATCH.equals(method)) {
return UPDATE;
} else if (HttpMethod.DELETE.equals(method)) {
return DELETE;
} else {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.neonbee.endpoint.odatav4;

import static io.neonbee.endpoint.odatav4.ODataV4Endpoint.UriConversion.STRICT;

import org.apache.olingo.server.api.ServiceMetadata;

import com.sap.cds.reflect.CdsService;

import io.neonbee.config.EndpointConfig;
import io.neonbee.entity.EntityModel;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;

public class ODataProxyEndpoint extends ODataV4Endpoint {

private static final String BASE_PATH_SEGMENT = "odataproxy";

/**
* The default path the OData V4 endpoint is exposed by NeonBee.
*/
public static final String DEFAULT_BASE_PATH = "/" + BASE_PATH_SEGMENT + "/";

@Override
public EndpointConfig getDefaultConfig() {
// as the EndpointConfig stays mutable, do not extract this to a static variable, but return a new object
return new EndpointConfig()
.setType(ODataProxyEndpoint.class.getName())
.setBasePath(DEFAULT_BASE_PATH)
.setAdditionalConfig(new JsonObject().put("uriConversion", STRICT.name()));
}

/**
* Creates a new OData Proxy Endpoint.
*
* @param edmxModel The EDMX model to be used by the handler.
* @return The request handler.
*/
@Override
protected Handler<RoutingContext> getRequestHandler(ServiceMetadata edmxModel) {
return new ODataProxyEndpointHandler(edmxModel);
}

/**
* Filters the models to be included in the OData Proxy Endpoint. Includes only those models that have the
* "neonbee.endpoint" annotation with value "odataproxy".
*
* @param model The entity model to be checked.
* @return true if the model should be included, false otherwise.
*/
@Override
protected boolean filterModels(EntityModel model) {
return model.getCsnModel().services()
.flatMap(CdsService::annotations)
.filter(annotation -> NEONBEE_ENDPOINT_CDS_SERVICE_ANNOTATION.equals(annotation.getName()))
.anyMatch(annotation -> BASE_PATH_SEGMENT.equalsIgnoreCase(annotation.getValue().toString()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package io.neonbee.endpoint.odatav4;

import static io.neonbee.endpoint.HttpMethodToDataActionMapper.mapMethodToAction;
import static io.neonbee.endpoint.odatav4.internal.olingo.OlingoEndpointHandler.getStatusCode;
import static io.neonbee.endpoint.odatav4.internal.olingo.OlingoEndpointHandler.mapToODataRequest;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.olingo.commons.api.edm.FullQualifiedName;
import org.apache.olingo.server.api.ODataRequest;
import org.apache.olingo.server.api.ServiceMetadata;

import io.neonbee.data.DataAction;
import io.neonbee.data.DataContext;
import io.neonbee.data.DataQuery;
import io.neonbee.data.DataRequest;
import io.neonbee.data.internal.DataContextImpl;
import io.neonbee.entity.AbstractEntityVerticle;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.ext.web.RequestBody;
import io.vertx.ext.web.RoutingContext;

public final class ODataProxyEndpointHandler implements Handler<RoutingContext> {

// Regex to find last path segment
private static final Pattern ENTITY_NAME_PATTERN =
Pattern.compile("/(\\w+)(?:\\([^)]*\\))?(?:/(\\w+))?$");

private final ServiceMetadata serviceMetadata;

/**
* Returns the ODataProxyEndpointHandler.
*
* @param serviceMetadata The metadata of the service
*/
public ODataProxyEndpointHandler(ServiceMetadata serviceMetadata) {
this.serviceMetadata = serviceMetadata;
}

@SuppressWarnings("unused") // normale Java-Warnung
@Override
public void handle(RoutingContext routingContext) {

DataQuery dataQuery;
FullQualifiedName fullQualifiedName;

try {
ODataRequest odataRequest = mapToODataRequest(
routingContext,
serviceMetadata.getEdm().getEntityContainer().getNamespace());

DataAction action = mapMethodToAction(routingContext.request().method());
Buffer body = Optional.ofNullable(routingContext.body())
.map(RequestBody::buffer)
.orElse(Buffer.buffer());
dataQuery = odataRequestToQuery(odataRequest, action, body);
fullQualifiedName = getFullQualifiedName(odataRequest);
} catch (Exception cause) {
routingContext.fail(getStatusCode(cause), cause);
return;
}

DataRequest dataRequest = new DataRequest(fullQualifiedName, dataQuery);
DataContextImpl context = new DataContextImpl(routingContext);
Future<Buffer> bufferFuture = AbstractEntityVerticle.requestEntity(
Buffer.class,
routingContext.vertx(),
dataRequest,
context);

bufferFuture.onSuccess(buffer -> {
HttpServerResponse response = routingContext.response();
copyResponseDataToHttpResponse(context, response);
response.end(buffer);
}).onFailure(cause -> routingContext.fail(getStatusCode(cause), cause));
}

/**
* Copy all entries from {@code context.responseData()} into the provided {@code HttpServerResponse} headers.
*
* @param context the data context containing the response data map; may be null
* @param response the HTTP response where headers will be written; may be null
*/
private static void copyResponseDataToHttpResponse(DataContext context, HttpServerResponse response) {
Map<String, Object> respData = context.responseData();
for (Map.Entry<String, Object> e : respData.entrySet()) {
String name = e.getKey();
Object value = e.getValue();

if (DataContext.STATUS_CODE_HINT.equals(name)) {
response.setStatusCode((Integer) value);
continue;
}

if (value instanceof Iterable<?>) {
for (Object v : (Iterable<?>) value) {
if (v != null) {
response.putHeader(name, v.toString());
}
}
} else {
response.putHeader(name, value.toString());
}
}
}

/**
* Retrieves the EdmEntityType from the ODataRequest.
*
* @param odataRequest The ODataRequest
* @return The FullQualifiedName
*/
private FullQualifiedName getFullQualifiedName(ODataRequest odataRequest) {
Matcher matcher = ENTITY_NAME_PATTERN.matcher(odataRequest.getRawODataPath());
if (matcher.matches()) {
return new FullQualifiedName(odataRequest.getRawServiceResolutionUri(), matcher.group(1));
} else {
throw new IllegalArgumentException("Cannot determine entity name from OData path: "
+ odataRequest.getRawODataPath());
}
}

private static DataQuery odataRequestToQuery(ODataRequest request, DataAction action, Buffer body) {
// the uriPath without /odata root path and without query path
String uriPath = "/" + request.getRawServiceResolutionUri() + request.getRawODataPath();
// the raw query path
Map<String, List<String>> stringListMap = DataQuery.parseEncodedQueryString(request.getRawQueryPath());
return new DataQuery(action, uriPath, stringListMap, request.getAllHeaders(), body).addHeader("X-HTTP-Method",
request.getMethod().name());
}
}
Loading
Loading