001/*
002 * MIT License
003 *
004 * Copyright (c) 2023 IntellectualSites
005 *
006 * Permission is hereby granted, free of charge, to any person obtaining a copy
007 * of this software and associated documentation files (the "Software"), to deal
008 * in the Software without restriction, including without limitation the rights
009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010 * copies of the Software, and to permit persons to whom the Software is
011 * furnished to do so, subject to the following conditions:
012 *
013 * The above copyright notice and this permission notice shall be included in all
014 * copies or substantial portions of the Software.
015 *
016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022 * SOFTWARE.
023 */
024package com.intellectualsites.arkitektonika.v1;
025
026import com.google.gson.GsonBuilder;
027import com.google.gson.JsonObject;
028import com.intellectualsites.arkitektonika.ApiVersion;
029import com.intellectualsites.arkitektonika.ResourceStatus;
030import com.intellectualsites.arkitektonika.Schematic;
031import com.intellectualsites.arkitektonika.SchematicKeys;
032import com.intellectualsites.arkitektonika.exceptions.InvalidFormatException;
033import com.intellectualsites.arkitektonika.exceptions.ResourceRetrievalException;
034import com.intellectualsites.arkitektonika.exceptions.ResourceUploadException;
035import com.intellectualsites.http.ContentType;
036import com.intellectualsites.http.EntityMapper;
037import com.intellectualsites.http.HttpClient;
038import com.intellectualsites.http.HttpResponse;
039import com.intellectualsites.http.external.GsonMapper;
040import org.jetbrains.annotations.NotNull;
041
042import java.io.ByteArrayOutputStream;
043import java.io.File;
044import java.io.OutputStreamWriter;
045import java.io.PrintWriter;
046import java.nio.charset.StandardCharsets;
047import java.nio.file.Files;
048import java.util.Objects;
049import java.util.UUID;
050import java.util.concurrent.CompletableFuture;
051import java.util.concurrent.ExecutorService;
052
053public final class ApiClient implements com.intellectualsites.arkitektonika.ApiClient {
054
055    private final HttpClient httpClient;
056
057    public ApiClient(@NotNull final String url) {
058        this.httpClient = HttpClient.newBuilder().withBaseURL(url).withEntityMapper(EntityMapper.newInstance()
059            .registerDeserializer(JsonObject.class, GsonMapper.deserializer(JsonObject.class, new GsonBuilder().create()))
060            .registerSerializer(File.class, new SchematicSerializer())).build();
061    }
062
063    @NotNull @Override public ApiVersion getApiVersion() {
064        return ApiVersion.V1_0_0;
065    }
066
067    @NotNull @Override public CompletableFuture<Boolean> checkCompatibility(@NotNull final ExecutorService service) {
068        return CompletableFuture.supplyAsync(() -> {
069           final HttpResponse response =
070               httpClient.get("/").onStatus(200, ignore -> {})
071                              .onRemaining(r -> {
072                                  throw new ResourceRetrievalException("/", r.getStatusCode(), r.getStatus());
073                              }).execute();
074           final JsonObject object = Objects.requireNonNull(response, "Failed to retrieve response")
075               .getResponseEntity(JsonObject.class);
076           return object.has("version") && object.get("version").getAsString().startsWith("1.");
077        }, service);
078    }
079
080    @NotNull @Override public CompletableFuture<SchematicKeys> upload(@NotNull final File file,
081        @NotNull final ExecutorService service) {
082        return CompletableFuture.supplyAsync(() -> {
083           final HttpResponse response = httpClient.post("/upload").withInput(() -> file)
084               .onStatus(400, httpResponse -> {
085                    throw new InvalidFormatException("/upload", 400, httpResponse.getStatus());
086                })
087               .onStatus(200, httpResponse -> {})
088               .onRemaining(httpResponse -> {
089                   throw new ResourceUploadException("/upload", httpResponse.getStatusCode(), httpResponse.getStatus(), "Other");
090               }).execute();
091           final JsonObject object = Objects.requireNonNull(response, "Failed to get response").getResponseEntity(JsonObject.class);
092           return new SchematicKeys(object.get("download_key").getAsString(), object.get("delete_key").getAsString());
093        }, service);
094    }
095
096    @NotNull @Override public CompletableFuture<ResourceStatus> checkStatus(@NotNull final String key,
097        @NotNull final ExecutorService service) {
098        return CompletableFuture.supplyAsync(() -> {
099            final HttpResponse response = httpClient.head(String.format("/download/%s", key)).execute();
100            if (response == null) {
101                throw new ResourceRetrievalException(String.format("/download/%s", key), 0, "Could not fetch response");
102            } else if (response.getStatusCode() == 200) {
103                return ResourceStatus.OK;
104            } else if (response.getStatusCode() == 404) {
105                return ResourceStatus.NON_EXISTENT;
106             } else if (response.getStatusCode() == 410) {
107                return ResourceStatus.DELETED;
108            } else {
109                throw new ResourceRetrievalException(String.format("/download/%s", key), response.getStatusCode(), response.getStatus());
110            }
111        }, service);
112    }
113
114    @Override @NotNull public CompletableFuture<Boolean> delete(@NotNull String key,
115        @NotNull final ExecutorService service) {
116        return CompletableFuture.supplyAsync(() -> {
117            final HttpResponse response = httpClient.delete(String.format("/delete/%s", key))
118                .onStatus(200, httpResponse -> {})
119                .onRemaining(httpResponse -> {
120                    throw new ResourceRetrievalException(String.format("/delete/%s", key), httpResponse.getStatusCode(), httpResponse.getStatus());
121                }).execute();
122            if (response == null) {
123                throw new ResourceRetrievalException(String.format("/delete/%s", key), 0, "Could not fetch response");
124            }
125            return true;
126        }, service);
127    }
128
129    @Override @NotNull public CompletableFuture<Schematic> download(@NotNull String key,
130        @NotNull final ExecutorService service) {
131        return CompletableFuture.supplyAsync(() -> {
132            final HttpResponse response = httpClient.get(String.format("/download/%s", key))
133                .onStatus(200, httpResponse -> {})
134                .onRemaining(httpResponse -> {
135                    throw new ResourceRetrievalException(String.format("/download/%s", key), httpResponse.getStatusCode(), httpResponse.getStatus());
136                }).execute();
137            if (response == null) {
138                throw new ResourceRetrievalException(String.format("/download/%s", key), 0, "Could not fetch response");
139            }
140            return new Schematic(key, response.getRawResponse());
141        }, service);
142    }
143
144
145    private static final class SchematicSerializer implements EntityMapper.EntitySerializer<File> {
146
147        private final String boundary = UUID.randomUUID().toString();
148
149        @Override @NotNull public byte[] serialize(@NotNull final File file) {
150            try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
151                 final PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream,
152                     StandardCharsets.UTF_8), true)) {
153                printWriter.append("--").append(this.boundary).append("\r\n");
154                printWriter.append("Content-Disposition: form-data; name=\"schematic\"; filename=\"plot.schem\"\r\n");
155                printWriter.append("Content-Type: application/octet-stream\r\n\r\n").flush();
156                Files.copy(file.toPath(), byteArrayOutputStream);
157                byteArrayOutputStream.flush();
158                printWriter.append("\r\n").flush();
159                printWriter.append("--").append(this.boundary).append("--\r\n").flush();
160                return byteArrayOutputStream.toByteArray();
161            } catch (final Exception e) {
162                e.printStackTrace();
163            }
164            return new byte[0];
165        }
166
167        @Override public ContentType getContentType() {
168            return ContentType.of(String.format("multipart/form-data; boundary=%s", this.boundary));
169        }
170
171    }
172
173}