diff --git a/api-data-store-server-java/src/main/java/ch/ethz/sis/afsclient/client/AfsClientV2.java b/api-data-store-server-java/src/main/java/ch/ethz/sis/afsclient/client/AfsClientV2.java index da2f281af73304a1c12433752f680c327147a034..aa456946a960d8562a75a5d28c8700730cfb07f8 100644 --- a/api-data-store-server-java/src/main/java/ch/ethz/sis/afsclient/client/AfsClientV2.java +++ b/api-data-store-server-java/src/main/java/ch/ethz/sis/afsclient/client/AfsClientV2.java @@ -39,7 +39,7 @@ public final class AfsClientV2 implements PublicAPI private final URI serverUri; - private final JsonObjectMapper jsonObjectMapper; + private static final JsonObjectMapper jsonObjectMapper = new JacksonObjectMapper(); public AfsClientV2(final URI serverUri) { @@ -51,7 +51,6 @@ public final class AfsClientV2 implements PublicAPI this.maxReadSizeInBytes = maxReadSizeInBytes; this.timeout = timeout; this.serverUri = serverUri; - this.jsonObjectMapper = new JacksonObjectMapper(); } public URI getServerUri() @@ -147,12 +146,12 @@ public final class AfsClientV2 implements PublicAPI @Override public @NonNull Boolean write(@NonNull final String owner, @NonNull final String source, - @NonNull final Long offset, final byte @NonNull [] data, - final byte @NonNull [] md5Hash) throws Exception + @NonNull final Long offset, @NonNull final byte[] data, + @NonNull final byte[] md5Hash) throws Exception { validateSessionToken(); return request("POST", "write", Boolean.class, Map.of("owner", owner, "source", source, - "offset", offset.toString(), "data", Base64.getEncoder().encodeToString(data), "md5Hash", getMd5HexString(md5Hash))); + "offset", offset.toString(), "data", Base64.getEncoder().encodeToString(data), "md5Hash", Base64.getEncoder().encodeToString(md5Hash))); } @Override @@ -304,20 +303,10 @@ public final class AfsClientV2 implements PublicAPI throw new IllegalArgumentException( "Server error HTTP response. Missing content-type"); } - String content = httpResponse.headers().map().get("content-type").get(0); + String contentType = httpResponse.headers().map().get("content-type").get(0); + byte[] responseBody = httpResponse.body(); - switch (content) - { - case "text/plain": - return parseFormDataResponse(responseType, httpResponse); - case "application/json": - return parseJsonResponse(httpResponse); - case "application/octet-stream": - return (T) httpResponse.body(); - default: - throw new IllegalArgumentException( - "Client error HTTP response. Unsupported content-type received."); - } + return getResponseResult(responseType, contentType, responseBody); } else if (statusCode >= 400 && statusCode < 500) { // jsonObjectMapper can't deserialize immutable lists sent in the error message. @@ -332,23 +321,40 @@ public final class AfsClientV2 implements PublicAPI } } - private <T> T parseFormDataResponse(Class<T> responseType, HttpResponse<byte[]> httpResponse) + public static <T> T getResponseResult(Class<T> responseType, String contentType, byte[] responseBody) + throws Exception + { + switch (contentType) + { + case "text/plain": + return AfsClientV2.parseFormDataResponse(responseType, responseBody); + case "application/json": + return AfsClientV2.parseJsonResponse(responseBody); + case "application/octet-stream": + return (T) responseBody; + default: + throw new IllegalArgumentException( + "Client error HTTP response. Unsupported content-type received."); + } + } + + private static <T> T parseFormDataResponse(Class<T> responseType, byte[] responseBody) { if (responseType == null) { return null; } else if (responseType == String.class) { - return responseType.cast(new String(httpResponse.body())); + return responseType.cast(new String(responseBody, StandardCharsets.UTF_8)); } else if (responseType == Boolean.class) { - return responseType.cast(Boolean.parseBoolean(new String(httpResponse.body()))); + return responseType.cast(Boolean.parseBoolean(new String(responseBody, StandardCharsets.UTF_8))); } throw new IllegalStateException("Unreachable statement!"); } - private <T> T parseJsonResponse(final HttpResponse<byte[]> httpResponse) throws Exception + private static <T> T parseJsonResponse(byte[] responseBody) throws Exception { final ApiResponse response = - jsonObjectMapper.readValue(new ByteArrayInputStream(httpResponse.body()), + jsonObjectMapper.readValue(new ByteArrayInputStream(responseBody), ApiResponse.class); if (response.getError() != null) @@ -367,13 +373,4 @@ public final class AfsClientV2 implements PublicAPI throw new IllegalStateException("No session information detected!"); } } - - private String getMd5HexString(byte[] md5) { - BigInteger no = new BigInteger(1, md5); - String hashtext = no.toString(16); - while (hashtext.length() < 32) { - hashtext = "0" + hashtext; - } - return hashtext; - } } diff --git a/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/HttpResponse.java b/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/HttpResponse.java index 64484a7568a3419cb1ae7414eafcea3c112a07f6..4071cc5463353049a9a89bb172e37edb1072c473 100644 --- a/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/HttpResponse.java +++ b/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/HttpResponse.java @@ -24,6 +24,7 @@ import lombok.Value; @Builder(toBuilder = true) @AllArgsConstructor(access = AccessLevel.PUBLIC) public class HttpResponse { + public static final String CONTENT_TYPE_TEXT = "text/plain"; public static final String CONTENT_TYPE_BINARY_DATA = "application/octet-stream"; public static final String CONTENT_TYPE_JSON = "application/json"; diff --git a/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/impl/NettyHttpHandlerV2.java b/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/impl/NettyHttpHandlerV2.java new file mode 100644 index 0000000000000000000000000000000000000000..01244f61ccee0f10755b69febd5139894d204359 --- /dev/null +++ b/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/impl/NettyHttpHandlerV2.java @@ -0,0 +1,147 @@ +/* + * Copyright ETH 2022 - 2023 Zürich, Scientific IT Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ch.ethz.sis.afsserver.http.impl; + +import ch.ethz.sis.afsserver.http.HttpResponse; +import ch.ethz.sis.afsserver.http.HttpServerHandler; +import ch.ethz.sis.shared.log.LogManager; +import ch.ethz.sis.shared.log.Logger; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.*; + +import java.nio.charset.StandardCharsets; +import java.util.Set; + +import static io.netty.handler.codec.http.HttpMethod.*; + +public class NettyHttpHandlerV2 extends ChannelInboundHandlerAdapter +{ + + private static final Logger logger = LogManager.getLogger(NettyHttpServer.class); + + private static final byte[] NOT_FOUND = "404 NOT FOUND".getBytes(); + + private static final ByteBuf NOT_FOUND_BUFFER = Unpooled.wrappedBuffer(NOT_FOUND); + + private static final Set<HttpMethod> allowedMethods = Set.of(GET, POST, PUT, DELETE); + + private final String uri; + + private final HttpServerHandler httpServerHandler; + + public NettyHttpHandlerV2(String uri, HttpServerHandler httpServerHandler) + { + this.uri = uri; + this.httpServerHandler = httpServerHandler; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception + { + if (msg instanceof FullHttpRequest) + { + final FullHttpRequest request = (FullHttpRequest) msg; + QueryStringDecoder queryStringDecoderForPath = new QueryStringDecoder(request.uri(), true); + + if (queryStringDecoderForPath.path().equals(uri) && + allowedMethods.contains(request.method())) + { + FullHttpResponse response = null; + ByteBuf content = request.content(); + try + { + QueryStringDecoder queryStringDecoderForParameters = null; + byte[] array = new byte[content.readableBytes()]; + content.readBytes(array); + + if (GET.equals(request.method())) { + queryStringDecoderForParameters = queryStringDecoderForPath; + } else { + queryStringDecoderForParameters = new QueryStringDecoder(new String(array, StandardCharsets.UTF_8), StandardCharsets.UTF_8, false); + } + + HttpResponse apiResponse = httpServerHandler.process(request.method(), + queryStringDecoderForParameters.parameters(), array); + HttpResponseStatus status = (!apiResponse.isError()) ? + HttpResponseStatus.OK : + HttpResponseStatus.BAD_REQUEST; + response = getHttpResponse( + status, + apiResponse.getContentType(), + Unpooled.wrappedBuffer(apiResponse.getBody()), + apiResponse.getBody().length); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } finally + { + content.release(); + } + } else + { + FullHttpResponse response = getHttpResponse( + HttpResponseStatus.NOT_FOUND, + "text/plain", + NOT_FOUND_BUFFER, + NOT_FOUND.length); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + } else + { + super.channelRead(ctx, msg); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception + { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception + { + logger.catching(cause); + byte[] causeBytes = cause.getMessage().getBytes(); + FullHttpResponse response = getHttpResponse( + HttpResponseStatus.INTERNAL_SERVER_ERROR, + "text/plain", + Unpooled.wrappedBuffer(causeBytes), + causeBytes.length + ); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + public FullHttpResponse getHttpResponse( + HttpResponseStatus status, + String contentType, + ByteBuf content, + int contentLength) + { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + status, + content + ); + response.headers().set(HttpHeaderNames.CONTENT_LENGTH, contentLength); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType); + response.headers().set(HttpHeaderNames.CONNECTION, "close"); + return response; + } +} diff --git a/server-data-store/src/test/java/ch/ethz/sis/afsserver/core/AbstractPublicAPIWrapperV2.java b/server-data-store/src/test/java/ch/ethz/sis/afsserver/core/AbstractPublicAPIWrapperV2.java new file mode 100644 index 0000000000000000000000000000000000000000..661e8a2258ae02b7c290a9765617f992aaab4b26 --- /dev/null +++ b/server-data-store/src/test/java/ch/ethz/sis/afsserver/core/AbstractPublicAPIWrapperV2.java @@ -0,0 +1,152 @@ +/* + * Copyright ETH 2022 - 2023 Zürich, Scientific IT Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ch.ethz.sis.afsserver.core; + +import ch.ethz.sis.afsapi.api.PublicAPI; +import ch.ethz.sis.afsapi.dto.File; +import lombok.NonNull; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public abstract class AbstractPublicAPIWrapperV2 implements PublicAPI +{ + + public abstract <E> E process(Class<E> responseType, String method, Map<String, Object> params); + + @Override + public List<File> list(@NonNull String owner, @NonNull String source, + @NonNull Boolean recursively) throws Exception + { + Map<String, Object> args = Map.of( + "owner", owner, + "source", source, + "recursively", recursively); + return process(List.class, "list", args); + } + + @Override + public byte[] read(@NonNull String owner, @NonNull String source, @NonNull Long offset, + @NonNull Integer limit) throws Exception + { + Map<String, Object> args = Map.of( + "owner", owner, + "source", source, + "offset", offset, + "limit", limit); + return process(byte[].class,"read", args); + } + + @Override + public Boolean write(@NonNull String owner, @NonNull String source, @NonNull Long offset, + @NonNull byte[] data, @NonNull byte[] md5Hash) throws Exception + { + Map<String, Object> args = Map.of( + "owner", owner, + "source", source, + "offset", offset, + "data", data, + "md5Hash", md5Hash); + return process(Boolean.class, "write", args); + } + + @Override + public Boolean delete(@NonNull String owner, @NonNull String source) throws Exception + { + Map<String, Object> args = Map.of( + "owner", owner, + "source", source); + return process(Boolean.class, "delete", args); + } + + @Override + public Boolean copy(@NonNull String sourceOwner, @NonNull String source, + @NonNull String targetOwner, @NonNull String target) throws Exception + { + Map<String, Object> args = Map.of( + "sourceOwner", sourceOwner, + "source", source, + "targetOwner", targetOwner, + "target", target); + return process(Boolean.class,"copy", args); + } + + @Override + public Boolean move(@NonNull String sourceOwner, @NonNull String source, + @NonNull String targetOwner, @NonNull String target) throws Exception + { + Map<String, Object> args = Map.of( + "sourceOwner", sourceOwner, + "source", source, + "targetOwner", targetOwner, + "target", target); + return process(Boolean.class, "move", args); + } + + @Override + public void begin(UUID transactionId) throws Exception + { + //TODO: Unused + } + + @Override + public Boolean prepare() throws Exception + { + //TODO: Unused + return true; + } + + @Override + public void commit() throws Exception + { + //TODO: Unused + } + + @Override + public void rollback() throws Exception + { + //TODO: Unused + } + + @Override + public List<UUID> recover() throws Exception + { + //TODO: Unused + return null; + } + + @Override + public String login(String userId, String password) throws Exception + { + //TODO: Unused + return null; + } + + @Override + public Boolean isSessionValid() throws Exception + { + //TODO: Unused + return null; + } + + @Override + public Boolean logout() throws Exception + { + //TODO: Unused + return null; + } +} diff --git a/server-data-store/src/test/java/ch/ethz/sis/afsserver/impl/APIServerAdapterWrapperV2.java b/server-data-store/src/test/java/ch/ethz/sis/afsserver/impl/APIServerAdapterWrapperV2.java new file mode 100644 index 0000000000000000000000000000000000000000..810d8460872b687d1ddf45997e3d369c2c7e9f03 --- /dev/null +++ b/server-data-store/src/test/java/ch/ethz/sis/afsserver/impl/APIServerAdapterWrapperV2.java @@ -0,0 +1,96 @@ +/* + * Copyright ETH 2022 - 2023 Zürich, Scientific IT Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ch.ethz.sis.afsserver.impl; + +import ch.ethz.sis.afsclient.client.AfsClientV2; +import ch.ethz.sis.afsserver.core.AbstractPublicAPIWrapperV2; +import ch.ethz.sis.afsserver.http.HttpResponse; +import ch.ethz.sis.afsserver.server.impl.ApiServerAdapter; +import ch.ethz.sis.shared.io.IOUtils; +import ch.ethz.sis.shared.log.LogManager; +import ch.ethz.sis.shared.log.Logger; +import io.netty.handler.codec.http.HttpMethod; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class APIServerAdapterWrapperV2 extends AbstractPublicAPIWrapperV2 +{ + + private static final Logger logger = LogManager.getLogger(APIServerAdapterWrapperV2.class); + + private ApiServerAdapter apiServerAdapter; + + + public APIServerAdapterWrapperV2(ApiServerAdapter apiServerAdapter) + { + this.apiServerAdapter = apiServerAdapter; + } + + public Map<String, List<String>> getURIParameters(Map<String, Object> args) + { + Map<String, List<String>> result = new HashMap<>(args.size()); + for (Map.Entry<String, Object> entry : args.entrySet()) + { + if (entry.getValue() instanceof byte[]) + { + result.put(entry.getKey(), List.of(IOUtils.encodeBase64((byte[]) entry.getValue()))); + } else + { + result.put(entry.getKey(), List.of(String.valueOf(entry.getValue()))); + } + } + return result; + } + + public <E> E process(Class<E> responseType, String apiMethod, Map<String, Object> params) + { + try + { + HttpMethod httpMethod = ApiServerAdapter.getHttpMethod(apiMethod); + Map<String, List<String>> requestParameters = getURIParameters(params); + requestParameters.put("sessionToken", List.of(UUID.randomUUID().toString())); + requestParameters.put("method", List.of(apiMethod)); + + byte[] requestBody = null; + + if (HttpMethod.GET.equals(httpMethod)) + { + // Do nothing + } else if (HttpMethod.POST.equals(httpMethod) || HttpMethod.DELETE.equals(httpMethod)) + { + // Do nothing + } else + { + throw new IllegalArgumentException("Not supported HTTP method type!"); + } + + + + HttpResponse response = apiServerAdapter.process(httpMethod, requestParameters, null); + String contentType = response.getContentType(); + byte[] body = response.getBody(); + + return AfsClientV2.getResponseResult(responseType, contentType, body); + } catch (Throwable throwable) + { + throw new RuntimeException(throwable); + } + } + +} diff --git a/server-data-store/src/test/java/ch/ethz/sis/afsserver/impl/APIServerWrapperV2.java b/server-data-store/src/test/java/ch/ethz/sis/afsserver/impl/APIServerWrapperV2.java new file mode 100644 index 0000000000000000000000000000000000000000..6384b1ed15c5df46478c54964aebc0d37534c411 --- /dev/null +++ b/server-data-store/src/test/java/ch/ethz/sis/afsserver/impl/APIServerWrapperV2.java @@ -0,0 +1,57 @@ +/* + * Copyright ETH 2022 - 2023 Zürich, Scientific IT Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ch.ethz.sis.afsserver.impl; + + +import ch.ethz.sis.afsserver.core.AbstractPublicAPIWrapperV2; +import ch.ethz.sis.afsserver.server.APIServer; +import ch.ethz.sis.afsserver.server.Response; +import ch.ethz.sis.afsserver.server.impl.ApiRequest; +import ch.ethz.sis.afsserver.server.impl.ApiResponseBuilder; +import ch.ethz.sis.afsserver.server.performance.PerformanceAuditor; +import ch.ethz.sis.shared.log.LogManager; +import ch.ethz.sis.shared.log.Logger; + +import java.util.Map; +import java.util.UUID; + +public class APIServerWrapperV2 extends AbstractPublicAPIWrapperV2 +{ + + private static final Logger logger = LogManager.getLogger(APIServerWrapperV2.class); + + private APIServer apiServer; + private final ApiResponseBuilder apiResponseBuilder; + + public APIServerWrapperV2(APIServer apiServer) { + this.apiServer = apiServer; + this.apiResponseBuilder = new ApiResponseBuilder(); + } + + public <E> E process(Class<E> responseType, String method, Map<String, Object> params) { + PerformanceAuditor performanceAuditor = new PerformanceAuditor(); + // Random Session token just works for tests with dummy authentication + ApiRequest request = new ApiRequest("test", method, params, UUID.randomUUID().toString(), null, null); + + try { + Response response = apiServer.processOperation(request, apiResponseBuilder, performanceAuditor); + return (E) response.getResult(); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + +}