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}