diff --git a/example.php b/example.php index 01a7cb8140..6cfc1a0410 100644 --- a/example.php +++ b/example.php @@ -27,6 +27,7 @@ use Appwrite\SDK\Language\ClaudePlugin; use Appwrite\SDK\Language\CursorPlugin; use Appwrite\SDK\Language\Rust; +use Appwrite\SDK\Language\Java; try { @@ -345,6 +346,15 @@ function configureSDK($sdk, $overrides = []) { configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/rust'); } + + // Java + if (!$requestedSdk || $requestedSdk === 'java') { + $sdk = new SDK(new Java(), new Swagger2($spec)); + configureSDK($sdk, [ + 'namespace' => 'io.appwrite', + ]); + $sdk->generate(__DIR__ . '/examples/java'); + } } catch (Exception $exception) { echo 'Error: ' . $exception->getMessage() . ' on ' . $exception->getFile() . ':' . $exception->getLine() . "\n"; diff --git a/src/SDK/Language/Java.php b/src/SDK/Language/Java.php new file mode 100644 index 0000000000..927f3f5a16 --- /dev/null +++ b/src/SDK/Language/Java.php @@ -0,0 +1,262 @@ +'; + } + + if (isset($parameter['enumName'])) { + return 'io.appwrite.enums.' . \ucfirst($parameter['enumName']); + } + if (!empty($parameter['enumValues'])) { + return 'io.appwrite.enums.' . \ucfirst($parameter['name']); + } + if (!empty($parameter['array']['model'])) { + return 'ListtoPascalCase($parameter['array']['model']) . '>'; + } + if (!empty($parameter['model'])) { + $modelType = 'io.appwrite.models.' . $this->toPascalCase($parameter['model']); + return $parameter['type'] === self::TYPE_ARRAY ? 'List<' . $modelType . '>' : $modelType; + } + if (isset($parameter['items'])) { + $parameter['array'] = $parameter['items']; + } + return match ($parameter['type']) { + self::TYPE_INTEGER => 'Long', + self::TYPE_NUMBER => 'Double', + self::TYPE_STRING => 'String', + self::TYPE_FILE => 'InputFile', + self::TYPE_BOOLEAN => 'Boolean', + self::TYPE_ARRAY => (!empty(($parameter['array'] ?? [])['type']) && !\is_array($parameter['array']['type'])) + ? 'List<' . $this->getTypeName($parameter['array']) . '>' + : 'List', + self::TYPE_OBJECT => 'Object', + default => $parameter['type'], + }; + } + + /** + * @param array $param + * @return string + */ + public function getParamDefault(array $param): string + { + $type = $param['type'] ?? ''; + $default = $param['default'] ?? ''; + $required = $param['required'] ?? ''; + + if ($required) { + return ''; + } + + $output = ' = '; + + if (empty($default) && $default !== 0 && $default !== false) { + switch ($type) { + case self::TYPE_INTEGER: + $output .= 'null'; + break; + case self::TYPE_NUMBER: + $output .= 'null'; + break; + case self::TYPE_ARRAY: + case self::TYPE_OBJECT: + $output .= 'null'; + break; + case self::TYPE_BOOLEAN: + $output .= 'null'; + break; + case self::TYPE_STRING: + $output .= 'null'; + break; + } + } else { + switch ($type) { + case self::TYPE_INTEGER: + $output .= $default . 'L'; + break; + case self::TYPE_NUMBER: + $output .= sprintf("%.1f", $default); + break; + case self::TYPE_BOOLEAN: + $output .= ($default) ? 'true' : 'false'; + break; + case self::TYPE_STRING: + $output .= "\"{$default}\""; + break; + case self::TYPE_ARRAY: + case self::TYPE_OBJECT: + $output .= 'null'; + break; + } + } + + return $output; + } + + /** + * @param array $param + * @param string $lang unused, always java + * @return string + */ + public function getParamExample(array $param, string $lang = 'java'): string + { + return parent::getParamExample($param, 'java'); + } + + /** + * @return array + */ + public function getFiles(): array + { + return [ + [ + 'scope' => 'default', + 'destination' => 'README.md', + 'template' => '/java/README.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'CHANGELOG.md', + 'template' => '/java/CHANGELOG.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'LICENSE.md', + 'template' => '/java/LICENSE.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'pom.xml', + 'template' => '/java/pom.xml.twig', + ], + [ + 'scope' => 'copy', + 'destination' => '.gitignore', + 'template' => '/java/.gitignore', + ], + [ + 'scope' => 'method', + 'destination' => 'docs/examples/{{ service.name | caseLower }}/{{ method.name | caseKebab }}.md', + 'template' => '/java/docs/example.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/java/{{ sdk.namespace | caseSlash }}/Client.java', + 'template' => '/java/src/main/java/io/appwrite/Client.java.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/java/{{ sdk.namespace | caseSlash }}/Permission.java', + 'template' => '/java/src/main/java/io/appwrite/Permission.java.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/java/{{ sdk.namespace | caseSlash }}/Role.java', + 'template' => '/java/src/main/java/io/appwrite/Role.java.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/java/{{ sdk.namespace | caseSlash }}/ID.java', + 'template' => '/java/src/main/java/io/appwrite/ID.java.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/java/{{ sdk.namespace | caseSlash }}/Query.java', + 'template' => '/java/src/main/java/io/appwrite/Query.java.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/java/{{ sdk.namespace | caseSlash }}/Operator.java', + 'template' => '/java/src/main/java/io/appwrite/Operator.java.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/java/{{ sdk.namespace | caseSlash }}/exceptions/{{ spec.title | caseUcfirst }}Exception.java', + 'template' => '/java/src/main/java/io/appwrite/exceptions/Exception.java.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/java/{{ sdk.namespace | caseSlash }}/models/InputFile.java', + 'template' => '/java/src/main/java/io/appwrite/models/InputFile.java.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/java/{{ sdk.namespace | caseSlash }}/models/UploadProgress.java', + 'template' => '/java/src/main/java/io/appwrite/models/UploadProgress.java.twig', + ], + [ + 'scope' => 'definition', + 'destination' => '/src/main/java/{{ sdk.namespace | caseSlash }}/models/{{ definition.name | caseUcfirst }}.java', + 'template' => '/java/src/main/java/io/appwrite/models/Model.java.twig', + ], + [ + 'scope' => 'requestModel', + 'destination' => '/src/main/java/{{ sdk.namespace | caseSlash }}/models/{{ requestModel.name | caseUcfirst }}.java', + 'template' => '/java/src/main/java/io/appwrite/models/RequestModel.java.twig', + ], + [ + 'scope' => 'enum', + 'destination' => '/src/main/java/{{ sdk.namespace | caseSlash }}/enums/{{ enum.name | caseUcfirst }}.java', + 'template' => '/java/src/main/java/io/appwrite/enums/Enum.java.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/java/{{ sdk.namespace | caseSlash }}/services/Service.java', + 'template' => '/java/src/main/java/io/appwrite/services/Service.java.twig', + ], + [ + 'scope' => 'service', + 'destination' => '/src/main/java/{{ sdk.namespace | caseSlash }}/services/{{ service.name | caseUcfirst }}.java', + 'template' => '/java/src/main/java/io/appwrite/services/ServiceTemplate.java.twig', + ], + ]; + } + + public function getFilters(): array + { + $parentFilters = parent::getFilters(); + + // Replace returnType filter: use Map instead of bare T for generic models + $overridden = []; + foreach ($parentFilters as $filter) { + if ($filter->getName() === 'returnType') { + $overridden[] = new TwigFilter('returnType', function (array $method, array $spec, string $namespace, string $generic = 'Map') { + return $this->getReturnType($method, $spec, $namespace, $generic); + }); + } else { + $overridden[] = $filter; + } + } + return $overridden; + } +} diff --git a/templates/java/.gitignore b/templates/java/.gitignore new file mode 100644 index 0000000000..823fb1f036 --- /dev/null +++ b/templates/java/.gitignore @@ -0,0 +1,8 @@ +target/ +*.class +*.jar +*.war +*.ear +.idea/ +*.iml +.DS_Store diff --git a/templates/java/CHANGELOG.md.twig b/templates/java/CHANGELOG.md.twig new file mode 100644 index 0000000000..9775df01b4 --- /dev/null +++ b/templates/java/CHANGELOG.md.twig @@ -0,0 +1,3 @@ +# Change Log + +{{ sdk.changelog | raw }} diff --git a/templates/java/LICENSE.md.twig b/templates/java/LICENSE.md.twig new file mode 100644 index 0000000000..d9437fba50 --- /dev/null +++ b/templates/java/LICENSE.md.twig @@ -0,0 +1 @@ +{{ sdk.licenseContent | raw }} diff --git a/templates/java/README.md.twig b/templates/java/README.md.twig new file mode 100644 index 0000000000..47bd0224e4 --- /dev/null +++ b/templates/java/README.md.twig @@ -0,0 +1,38 @@ +# {{ spec.title | caseUcfirst }} Java SDK + +{{ sdk.warning | raw }} + +{{ sdk.readme | raw }} + +## Installation + +Add the following dependency to your `pom.xml`: + +```xml + + {{ sdk.gitUserName }} + {{ sdk.gitRepoName }} + {{ sdk.version }} + +``` + +## Getting Started + +```java +import {{ sdk.namespace | caseDot }}.Client; +import {{ sdk.namespace | caseDot }}.services.*; + +Client client = new Client() + .setEndpoint("{{ spec.endpointDocs | raw }}") +{% for header in spec.global.headers %} + .set{{ header.key | caseUcfirst }}(""); +{% endfor %} +``` + +## Changelog + +{{ sdk.changelog | raw }} + +## Contributing + +All code changes happen through pull requests to the [GitHub repository](https://github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName }}). diff --git a/templates/java/docs/example.md.twig b/templates/java/docs/example.md.twig new file mode 100644 index 0000000000..44fc04ec1d --- /dev/null +++ b/templates/java/docs/example.md.twig @@ -0,0 +1,42 @@ +```java +import {{ sdk.namespace | caseDot }}.Client; +{% if method.parameters.all | filter((param) => param.type == 'file') | length > 0 %} +import {{ sdk.namespace | caseDot }}.models.InputFile; +{% endif %} +{% if method.parameters.all | hasPermissionParam %} +import {{ sdk.namespace | caseDot }}.Permission; +import {{ sdk.namespace | caseDot }}.Role; +{% endif %} +import {{ sdk.namespace | caseDot }}.services.{{ service.name | caseUcfirst }}; +{% set added = [] %} +{% for parameter in method.parameters.all %} +{% if parameter.enumValues is not empty %} +{% if parameter.enumName not in added %} +import {{ sdk.namespace | caseDot }}.enums.{{ parameter.enumName | caseUcfirst }}; +{% set added = added|merge([parameter.enumName]) %} +{% endif %} +{% endif %} +{% endfor %} + +Client client = new Client() +{% if method.auth|length > 0 %} + .setEndpoint("{{ spec.endpointDocs | raw }}") +{% for node in method.auth %} +{% for key,header in node|keys %} + .set{{ header | caseUcfirst }}("{{ node[header]['x-appwrite']['demo'] | raw }}"); // {{ node[header].description }} +{% endfor %}{% endfor %}{% endif %} + +{{ service.name | caseUcfirst }} {{ service.name | caseCamel }} = new {{ service.name | caseUcfirst }}(client); + +{{ service.name | caseCamel }}.{{ method.name | caseCamel }}( + {%~ for parameter in method.parameters.all %} + {% if parameter.enumValues | length > 0 %}{{ parameter | javaEnumExample }}{% else %}{{ parameter | javaParamExample }}{% endif %}{% if not loop.last %},{% endif %} // {{ parameter.name }}{% if not parameter.required %} (optional){% endif %} + + {%~ endfor %} +).thenAccept(result -> { + System.out.println(result); +}).exceptionally(e -> { + e.printStackTrace(); + return null; +}); +``` diff --git a/templates/java/pom.xml.twig b/templates/java/pom.xml.twig new file mode 100644 index 0000000000..eed0c2f34d --- /dev/null +++ b/templates/java/pom.xml.twig @@ -0,0 +1,60 @@ + + + 4.0.0 + + {{ sdk.gitUserName }} + {{ sdk.gitRepoName }} + {{ sdk.version }} + jar + + {{ spec.title | caseUcfirst }} Java SDK + {{ sdk.description }} + {{ sdk.url }} + + + 11 + 11 + UTF-8 + + + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + com.google.code.gson + gson + 2.10.1 + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.0 + + + attach-sources + + jar + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + ${exec.mainClass} + + + + + diff --git a/templates/java/src/main/java/io/appwrite/Client.java.twig b/templates/java/src/main/java/io/appwrite/Client.java.twig new file mode 100644 index 0000000000..a9b1170cc9 --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/Client.java.twig @@ -0,0 +1,343 @@ +package {{ sdk.namespace | caseDot }}; + +import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Exception; +import {{ sdk.namespace | caseDot }}.models.InputFile; +import {{ sdk.namespace | caseDot }}.models.UploadProgress; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import okhttp3.*; + +import javax.net.ssl.*; +import java.io.*; +import java.lang.reflect.Type; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +public class Client { + + private static final int CHUNK_SIZE = 5 * 1024 * 1024; + private static final Gson gson = new Gson(); + + private String endPoint; + private boolean selfSigned; + private OkHttpClient http; + private OkHttpClient httpForRedirect; + + private final Map headers = new LinkedHashMap<>(); + private final Map config = new LinkedHashMap<>(); + + public Client() { + this("{{ spec.endpoint }}", false); + } + + public Client(String endPoint) { + this(endPoint, false); + } + + public Client(String endPoint, boolean selfSigned) { + this.endPoint = endPoint; + headers.put("content-type", "application/json"); + headers.put("user-agent", "{{ spec.title | caseUcfirst }}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} " + System.getProperty("http.agent", "")); + headers.put("x-sdk-name", "{{ sdk.name }}"); + headers.put("x-sdk-platform", "{{ sdk.platform }}"); + headers.put("x-sdk-language", "{{ language.name | caseLower }}"); + headers.put("x-sdk-version", "{{ sdk.version }}"); + {%~ for key, header in spec.global.defaultHeaders %} + headers.put("{{ key | caseLower }}", "{{ header }}"); + {%~ endfor %} + setSelfSigned(selfSigned); + } + +{% for header in spec.global.headers %} + /** + * Set {{ header.key | caseUcfirst }} + *{% if header.description %} + * {{ header.description }} + *{% endif %} + * @param value the {{ header.key | caseLower }} + * @return this + */ + public Client set{{ header.key | caseUcfirst }}(String value) { + config.put("{{ header.key | caseCamel }}", value); + addHeader("{{ header.name | caseLower }}", value); + return this; + } + +{% endfor %} + public Client setSelfSigned(boolean status) { + this.selfSigned = status; + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + + if (!selfSigned) { + http = builder.build(); + httpForRedirect = builder.followRedirects(false).build(); + return this; + } + + try { + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } + } + }; + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAllCerts, new SecureRandom()); + SSLSocketFactory sslFactory = sslContext.getSocketFactory(); + builder.sslSocketFactory(sslFactory, (X509TrustManager) trustAllCerts[0]); + builder.hostnameVerifier((hostname, session) -> true); + http = builder.build(); + httpForRedirect = builder.followRedirects(false).build(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return this; + } + + public Client setEndpoint(String endPoint) { + if (!endPoint.startsWith("http://") && !endPoint.startsWith("https://")) { + throw new IllegalArgumentException("Invalid endpoint URL: " + endPoint); + } + this.endPoint = endPoint; + return this; + } + + public Client addHeader(String key, String value) { + headers.put(key, value); + return this; + } + + public Map getHeaders() { return Collections.unmodifiableMap(headers); } + public Map getConfig() { return Collections.unmodifiableMap(config); } + public String getEndpoint() { return endPoint; } + + private Request buildRequest(String method, String path, Map extraHeaders, Map params) { + Map filteredParams = new LinkedHashMap<>(); + if (params != null) { + for (Map.Entry e : params.entrySet()) { + if (e.getValue() != null) filteredParams.put(e.getKey(), e.getValue()); + } + } + + Headers.Builder hb = new Headers.Builder(); + for (Map.Entry e : headers.entrySet()) hb.add(e.getKey(), e.getValue()); + if (extraHeaders != null) for (Map.Entry e : extraHeaders.entrySet()) hb.add(e.getKey(), e.getValue()); + Headers requestHeaders = hb.build(); + + HttpUrl parsed = HttpUrl.parse(endPoint + path); + if (parsed == null) throw new IllegalArgumentException("Invalid URL: " + endPoint + path); + HttpUrl.Builder urlBuilder = parsed.newBuilder(); + + if ("GET".equalsIgnoreCase(method)) { + for (Map.Entry e : filteredParams.entrySet()) { + if (e.getValue() instanceof List) { + for (Object item : (List) e.getValue()) { + urlBuilder.addQueryParameter(e.getKey() + "[]", item.toString()); + } + } else { + urlBuilder.addQueryParameter(e.getKey(), e.getValue().toString()); + } + } + return new Request.Builder().url(urlBuilder.build()).headers(requestHeaders).get().build(); + } + + String contentType = (extraHeaders != null && extraHeaders.containsKey("content-type")) + ? extraHeaders.get("content-type") + : headers.getOrDefault("content-type", "application/json"); + RequestBody body; + if (MediaType.parse(MultipartBody.FORM.toString()) != null && "multipart/form-data".equals(contentType)) { + MultipartBody.Builder mb = new MultipartBody.Builder().setType(MultipartBody.FORM); + for (Map.Entry e : filteredParams.entrySet()) { + if (e.getValue() instanceof MultipartBody.Part) { + mb.addPart((MultipartBody.Part) e.getValue()); + } else if (e.getValue() instanceof List) { + for (Object item : (List) e.getValue()) mb.addFormDataPart(e.getKey() + "[]", item.toString()); + } else { + mb.addFormDataPart(e.getKey(), e.getValue().toString()); + } + } + body = mb.build(); + } else { + String json = gson.toJson(filteredParams); + body = RequestBody.create(json, MediaType.parse("application/json")); + } + + return new Request.Builder().url(urlBuilder.build()).headers(requestHeaders).method(method.toUpperCase(), body).build(); + } + + @SuppressWarnings("unchecked") + public CompletableFuture call( + String method, + String path, + Map headers, + Map params, + Class responseType, + Function converter + ) { + return CompletableFuture.supplyAsync(() -> { + Request request = buildRequest(method, path, headers, params); + try (Response response = http.newCall(request).execute()) { + if (!response.isSuccessful()) { + String body = response.body() != null ? response.body().string() : ""; + String contentType = response.header("content-type", ""); + if (contentType != null && contentType.contains("application/json")) { + Type mapType = new TypeToken>(){}.getType(); + Map map = gson.fromJson(body, mapType); + String msg = map.containsKey("message") ? (String) map.get("message") : body; + int code = map.containsKey("code") ? ((Number) map.get("code")).intValue() : response.code(); + String type = map.containsKey("type") ? (String) map.get("type") : ""; + throw new RuntimeException(new {{ spec.title | caseUcfirst }}Exception(msg, code, type, body)); + } + throw new RuntimeException(new {{ spec.title | caseUcfirst }}Exception(body, response.code(), "", body)); + } + + String warnings = response.header("x-{{ spec.title | lower }}-warning"); + if (warnings != null) { + for (String w : warnings.split(";")) System.err.println("Warning: " + w); + } + + if (responseType == Boolean.class) return (T) Boolean.TRUE; + + if (response.body() == null) return (T) Boolean.TRUE; + + if (responseType == byte[].class) return (T) response.body().bytes(); + + String body = response.body().string(); + if (body.isEmpty()) return (T) Boolean.TRUE; + + if (responseType == String.class) return (T) body; + + Type mapType = new TypeToken>(){}.getType(); + Map map = gson.fromJson(body, mapType); + if (converter != null) return converter.apply(map); + return (T) map; + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + public CompletableFuture call( + String method, + String path, + Map headers, + Map params, + Class responseType + ) { + return call(method, path, headers, params, responseType, null); + } + + public CompletableFuture redirect( + String method, + String path, + Map headers, + Map params + ) { + return CompletableFuture.supplyAsync(() -> { + Request request = buildRequest(method, path, headers, params); + try (Response response = httpForRedirect.newCall(request).execute()) { + return response.header("Location", ""); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + @SuppressWarnings("unchecked") + public CompletableFuture chunkedUpload( + String path, + Map headers, + Map params, + Class responseType, + Function converter, + String paramName, + String idParamName, + java.util.function.Consumer onProgress + ) { + return CompletableFuture.supplyAsync(() -> { + InputFile input = (InputFile) params.get(paramName); + long size; + RandomAccessFile file = null; + try { + if ("path".equals(input.sourceType) || "file".equals(input.sourceType)) { + file = new RandomAccessFile(input.path, "r"); + size = file.length(); + } else { + size = ((byte[]) input.data).length; + } + + if (size < CHUNK_SIZE) { + byte[] data; + if ("bytes".equals(input.sourceType)) { + data = (byte[]) input.data; + } else { + data = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(input.path)); + } + RequestBody fileBody = RequestBody.create(data, MediaType.parse(input.mimeType)); + params.put(paramName, MultipartBody.Part.createFormData(paramName, input.filename, fileBody)); + return call("POST", path, headers, params, responseType, converter).get(); + } + + byte[] buffer = new byte[CHUNK_SIZE]; + long offset = 0; + Map result = null; + + if (idParamName != null && !idParamName.isEmpty()) { + try { + Map current = (Map) call("GET", path + "/" + params.get(idParamName), headers, Collections.emptyMap(), Map.class).get(); + long chunksUploaded = ((Number) current.get("chunksUploaded")).longValue(); + offset = chunksUploaded * CHUNK_SIZE; + if (offset >= size) { + return converter.apply(current); + } + } catch (Exception ignored) { + // file not yet created on the server — start from offset 0 + } + } + + while (offset < size) { + int chunkLen = (int) Math.min(CHUNK_SIZE, size - offset); + byte[] chunk; + if ("bytes".equals(input.sourceType)) { + byte[] src = (byte[]) input.data; + chunk = Arrays.copyOfRange(src, (int) offset, (int) offset + chunkLen); + } else { + file.seek(offset); + chunk = new byte[chunkLen]; + file.readFully(chunk); + } + + long end = offset + chunkLen - 1; + headers.put("Content-Range", "bytes " + offset + "-" + end + "/" + size); + + RequestBody chunkBody = RequestBody.create(chunk, MediaType.parse("application/octet-stream")); + params.put(paramName, MultipartBody.Part.createFormData(paramName, input.filename, chunkBody)); + + result = (Map) call("POST", path, headers, params, Map.class).get(); + offset += chunkLen; + headers.put("x-{{ spec.title | caseLower }}-id", result.get("$id").toString()); + + if (onProgress != null) { + onProgress.accept(new UploadProgress( + result.get("$id").toString(), + (double) Math.min(offset, size) / size * 100, + Math.min(offset, size), + ((Number) result.get("chunksTotal")).intValue(), + ((Number) result.get("chunksUploaded")).intValue() + )); + } + } + + return converter.apply(result); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + if (file != null) try { file.close(); } catch (IOException ignored) {} + } + }); + } +} diff --git a/templates/java/src/main/java/io/appwrite/ID.java.twig b/templates/java/src/main/java/io/appwrite/ID.java.twig new file mode 100644 index 0000000000..f0b8eaac51 --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/ID.java.twig @@ -0,0 +1,28 @@ +package {{ sdk.namespace | caseDot }}; + +import java.time.Instant; +import java.util.Random; + +public class ID { + + private static String hexTimestamp() { + Instant now = Instant.now(); + long sec = now.getEpochSecond(); + long usec = now.getNano() / 1000; + return String.format("%08x%05x", sec, usec); + } + + public static String custom(String id) { return id; } + + public static String unique() { return unique(7); } + + public static String unique(int padding) { + String baseId = hexTimestamp(); + Random random = new Random(); + StringBuilder sb = new StringBuilder(baseId); + for (int i = 0; i < padding; i++) { + sb.append(Integer.toHexString(random.nextInt(16))); + } + return sb.toString(); + } +} diff --git a/templates/java/src/main/java/io/appwrite/Operator.java.twig b/templates/java/src/main/java/io/appwrite/Operator.java.twig new file mode 100644 index 0000000000..0033fbd847 --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/Operator.java.twig @@ -0,0 +1,143 @@ +package {{ sdk.namespace | caseDot }}; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Operator { + + private static final Gson gson = new Gson(); + + @SerializedName("method") + private final String method; + + @SerializedName("values") + private final List values; + + public Operator(String method, List values) { + this.method = method; + this.values = values; + } + + @Override + public String toString() { return gson.toJson(this); } + + public enum Condition { + EQUAL("equal"), + NOT_EQUAL("notEqual"), + GREATER_THAN("greaterThan"), + GREATER_THAN_EQUAL("greaterThanEqual"), + LESS_THAN("lessThan"), + LESS_THAN_EQUAL("lessThanEqual"), + CONTAINS("contains"), + IS_NULL("isNull"), + IS_NOT_NULL("isNotNull"); + + private final String value; + Condition(String value) { this.value = value; } + public String getValue() { return value; } + @Override public String toString() { return value; } + } + + public static String increment(Number value) { + return new Operator("increment", List.of(value)).toString(); + } + + public static String increment(Number value, Number max) { + return new Operator("increment", List.of(value, max)).toString(); + } + + public static String decrement(Number value) { + return new Operator("decrement", List.of(value)).toString(); + } + + public static String decrement(Number value, Number min) { + return new Operator("decrement", List.of(value, min)).toString(); + } + + public static String multiply(Number factor) { + return new Operator("multiply", List.of(factor)).toString(); + } + + public static String multiply(Number factor, Number max) { + return new Operator("multiply", List.of(factor, max)).toString(); + } + + public static String divide(Number divisor) { + return new Operator("divide", List.of(divisor)).toString(); + } + + public static String divide(Number divisor, Number min) { + return new Operator("divide", List.of(divisor, min)).toString(); + } + + public static String modulo(Number divisor) { + return new Operator("modulo", List.of(divisor)).toString(); + } + + public static String power(Number exponent) { + return new Operator("power", List.of(exponent)).toString(); + } + + public static String power(Number exponent, Number max) { + return new Operator("power", List.of(exponent, max)).toString(); + } + + public static String arrayAppend(List values) { + return new Operator("arrayAppend", new ArrayList(values)).toString(); + } + + public static String arrayPrepend(List values) { + return new Operator("arrayPrepend", new ArrayList(values)).toString(); + } + + public static String arrayInsert(int index, Object value) { + return new Operator("arrayInsert", List.of(index, value)).toString(); + } + + public static String arrayRemove(Object value) { + return new Operator("arrayRemove", List.of(value)).toString(); + } + + public static String arrayUnique() { + return new Operator("arrayUnique", List.of()).toString(); + } + + public static String arrayIntersect(List values) { + return new Operator("arrayIntersect", new ArrayList(values)).toString(); + } + + public static String arrayDiff(List values) { + return new Operator("arrayDiff", new ArrayList(values)).toString(); + } + + public static String arrayFilter(Condition condition, Object value) { + return new Operator("arrayFilter", List.of(condition.getValue(), value)).toString(); + } + + public static String stringConcat(Object value) { + return new Operator("stringConcat", List.of(value)).toString(); + } + + public static String stringReplace(String search, String replace) { + return new Operator("stringReplace", List.of(search, replace)).toString(); + } + + public static String toggle() { + return new Operator("toggle", List.of()).toString(); + } + + public static String dateAddDays(int days) { + return new Operator("dateAddDays", List.of(days)).toString(); + } + + public static String dateSubDays(int days) { + return new Operator("dateSubDays", List.of(days)).toString(); + } + + public static String dateSetNow() { + return new Operator("dateSetNow", List.of()).toString(); + } +} diff --git a/templates/java/src/main/java/io/appwrite/Permission.java.twig b/templates/java/src/main/java/io/appwrite/Permission.java.twig new file mode 100644 index 0000000000..9804055dbe --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/Permission.java.twig @@ -0,0 +1,24 @@ +package {{ sdk.namespace | caseDot }}; + +public class Permission { + + public static String read(String role) { + return "read(\"" + role + "\")"; + } + + public static String write(String role) { + return "write(\"" + role + "\")"; + } + + public static String create(String role) { + return "create(\"" + role + "\")"; + } + + public static String update(String role) { + return "update(\"" + role + "\")"; + } + + public static String delete(String role) { + return "delete(\"" + role + "\")"; + } +} diff --git a/templates/java/src/main/java/io/appwrite/Query.java.twig b/templates/java/src/main/java/io/appwrite/Query.java.twig new file mode 100644 index 0000000000..adf7f60da0 --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/Query.java.twig @@ -0,0 +1,129 @@ +package {{ sdk.namespace | caseDot }}; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Query { + + private static final Gson gson = new Gson(); + + @SerializedName("method") + private final String method; + + @SerializedName("attribute") + private final String attribute; + + @SerializedName("values") + private final List values; + + public Query(String method, String attribute, List values) { + this.method = method; + this.attribute = attribute; + this.values = values; + } + + public Query(String method, String attribute) { this(method, attribute, null); } + public Query(String method) { this(method, null, null); } + + private static List wrapCoords(List values) { + List l = new ArrayList<>(); + l.add(new ArrayList(values)); + return l; + } + + private static List withDistance(List values, Number distance) { + List inner = new ArrayList<>(); + inner.add(new ArrayList(values)); + inner.add(distance); + List outer = new ArrayList<>(); + outer.add(inner); + return outer; + } + + private static List withDistance(List values, Number distance, boolean meters) { + List inner = new ArrayList<>(); + inner.add(new ArrayList(values)); + inner.add(distance); + inner.add(meters); + List outer = new ArrayList<>(); + outer.add(inner); + return outer; + } + + @Override + public String toString() { return gson.toJson(this); } + + public static String equal(String attribute, Object value) { return new Query("equal", attribute, parseValue(value)).toString(); } + public static String notEqual(String attribute, Object value) { return new Query("notEqual", attribute, parseValue(value)).toString(); } + public static String regex(String attribute, String pattern) { return new Query("regex", attribute, parseValue(pattern)).toString(); } + public static String lessThan(String attribute, Object value) { return new Query("lessThan", attribute, parseValue(value)).toString(); } + public static String lessThanEqual(String attribute, Object value) { return new Query("lessThanEqual", attribute, parseValue(value)).toString(); } + public static String greaterThan(String attribute, Object value) { return new Query("greaterThan", attribute, parseValue(value)).toString(); } + public static String greaterThanEqual(String attribute, Object value) { return new Query("greaterThanEqual", attribute, parseValue(value)).toString(); } + public static String search(String attribute, String value) { return new Query("search", attribute, List.of(value)).toString(); } + public static String isNull(String attribute) { return new Query("isNull", attribute).toString(); } + public static String isNotNull(String attribute) { return new Query("isNotNull", attribute).toString(); } + public static String exists(List attributes) { return new Query("exists", null, new ArrayList(attributes)).toString(); } + public static String notExists(List attributes) { return new Query("notExists", null, new ArrayList(attributes)).toString(); } + public static String between(String attribute, Object start, Object end) { return new Query("between", attribute, List.of(start, end)).toString(); } + public static String startsWith(String attribute, String value) { return new Query("startsWith", attribute, List.of(value)).toString(); } + public static String endsWith(String attribute, String value) { return new Query("endsWith", attribute, List.of(value)).toString(); } + public static String select(List attributes) { return new Query("select", null, new ArrayList(attributes)).toString(); } + public static String orderAsc(String attribute) { return new Query("orderAsc", attribute).toString(); } + public static String orderDesc(String attribute) { return new Query("orderDesc", attribute).toString(); } + public static String orderRandom() { return new Query("orderRandom").toString(); } + public static String cursorBefore(String documentId) { return new Query("cursorBefore", null, List.of(documentId)).toString(); } + public static String cursorAfter(String documentId) { return new Query("cursorAfter", null, List.of(documentId)).toString(); } + public static String limit(int limit) { return new Query("limit", null, List.of(limit)).toString(); } + public static String offset(int offset) { return new Query("offset", null, List.of(offset)).toString(); } + public static String contains(String attribute, Object value) { return new Query("contains", attribute, parseValue(value)).toString(); } + public static String containsAny(String attribute, List value) { return new Query("containsAny", attribute, new ArrayList(value)).toString(); } + public static String containsAll(String attribute, List value) { return new Query("containsAll", attribute, new ArrayList(value)).toString(); } + public static String notContains(String attribute, Object value) { return new Query("notContains", attribute, parseValue(value)).toString(); } + public static String notSearch(String attribute, String value) { return new Query("notSearch", attribute, List.of(value)).toString(); } + public static String notBetween(String attribute, Object start, Object end) { return new Query("notBetween", attribute, List.of(start, end)).toString(); } + public static String notStartsWith(String attribute, String value) { return new Query("notStartsWith", attribute, List.of(value)).toString(); } + public static String notEndsWith(String attribute, String value) { return new Query("notEndsWith", attribute, List.of(value)).toString(); } + public static String createdBefore(String value) { return lessThan("$createdAt", value); } + public static String createdAfter(String value) { return greaterThan("$createdAt", value); } + public static String createdBetween(String start, String end) { return between("$createdAt", start, end); } + public static String updatedBefore(String value) { return lessThan("$updatedAt", value); } + public static String updatedAfter(String value) { return greaterThan("$updatedAt", value); } + public static String updatedBetween(String start, String end) { return between("$updatedAt", start, end); } + public static String or(List queries) { return new Query("or", null, parseQueries(queries)).toString(); } + public static String and(List queries) { return new Query("and", null, parseQueries(queries)).toString(); } + public static String elemMatch(String attribute, List queries) { return new Query("elemMatch", attribute, parseQueries(queries)).toString(); } + + public static String distanceEqual(String attribute, List values, Number distance) { return new Query("distanceEqual", attribute, withDistance(values, distance)).toString(); } + public static String distanceEqual(String attribute, List values, Number distance, boolean meters) { return new Query("distanceEqual", attribute, withDistance(values, distance, meters)).toString(); } + public static String distanceNotEqual(String attribute, List values, Number distance) { return new Query("distanceNotEqual", attribute, withDistance(values, distance)).toString(); } + public static String distanceNotEqual(String attribute, List values, Number distance, boolean meters) { return new Query("distanceNotEqual", attribute, withDistance(values, distance, meters)).toString(); } + public static String distanceGreaterThan(String attribute, List values, Number distance) { return new Query("distanceGreaterThan", attribute, withDistance(values, distance)).toString(); } + public static String distanceGreaterThan(String attribute, List values, Number distance, boolean meters) { return new Query("distanceGreaterThan", attribute, withDistance(values, distance, meters)).toString(); } + public static String distanceLessThan(String attribute, List values, Number distance) { return new Query("distanceLessThan", attribute, withDistance(values, distance)).toString(); } + public static String distanceLessThan(String attribute, List values, Number distance, boolean meters) { return new Query("distanceLessThan", attribute, withDistance(values, distance, meters)).toString(); } + + public static String intersects(String attribute, List values) { return new Query("intersects", attribute, wrapCoords(values)).toString(); } + public static String notIntersects(String attribute, List values) { return new Query("notIntersects", attribute, wrapCoords(values)).toString(); } + public static String crosses(String attribute, List values) { return new Query("crosses", attribute, wrapCoords(values)).toString(); } + public static String notCrosses(String attribute, List values) { return new Query("notCrosses", attribute, wrapCoords(values)).toString(); } + public static String overlaps(String attribute, List values) { return new Query("overlaps", attribute, wrapCoords(values)).toString(); } + public static String notOverlaps(String attribute, List values) { return new Query("notOverlaps", attribute, wrapCoords(values)).toString(); } + public static String touches(String attribute, List values) { return new Query("touches", attribute, wrapCoords(values)).toString(); } + public static String notTouches(String attribute, List values) { return new Query("notTouches", attribute, wrapCoords(values)).toString(); } + + private static List parseQueries(List queries) { + List l = new ArrayList<>(); + for (String q : queries) l.add(com.google.gson.JsonParser.parseString(q)); + return l; + } + + @SuppressWarnings("unchecked") + private static List parseValue(Object value) { + if (value instanceof List) return (List) value; + return List.of(value); + } +} diff --git a/templates/java/src/main/java/io/appwrite/Role.java.twig b/templates/java/src/main/java/io/appwrite/Role.java.twig new file mode 100644 index 0000000000..4480c70123 --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/Role.java.twig @@ -0,0 +1,43 @@ +package {{ sdk.namespace | caseDot }}; + +/** + * Helper class to generate role strings for {@link Permission}. + */ +public class Role { + + /** Grants access to anyone (authenticated and unauthenticated users). */ + public static String any() { return "any"; } + + /** Grants access to a specific user by user ID. */ + public static String user(String id) { return "user:" + id; } + + /** Grants access to a specific user with a status filter. */ + public static String user(String id, String status) { + return status.isEmpty() ? "user:" + id : "user:" + id + "/" + status; + } + + /** Grants access to any authenticated or anonymous user. */ + public static String users() { return "users"; } + + /** Grants access to any authenticated or anonymous user with a status filter. */ + public static String users(String status) { + return status.isEmpty() ? "users" : "users/" + status; + } + + /** Grants access to any guest user without a session. */ + public static String guests() { return "guests"; } + + /** Grants access to a team by team ID. */ + public static String team(String id) { return "team:" + id; } + + /** Grants access to a team member with the specified role. */ + public static String team(String id, String role) { + return role.isEmpty() ? "team:" + id : "team:" + id + "/" + role; + } + + /** Grants access to a specific member of a team. */ + public static String member(String id) { return "member:" + id; } + + /** Grants access to a user with the specified label. */ + public static String label(String name) { return "label:" + name; } +} diff --git a/templates/java/src/main/java/io/appwrite/enums/Enum.java.twig b/templates/java/src/main/java/io/appwrite/enums/Enum.java.twig new file mode 100644 index 0000000000..06daa6e485 --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/enums/Enum.java.twig @@ -0,0 +1,31 @@ +package {{ sdk.namespace | caseDot }}.enums; + +public enum {{ enum.name | caseUcfirst | overrideIdentifier }} { +{% for value in enum.enum %} +{% set key = enum.keys is empty ? value : enum.keys[loop.index0] %} + {{ key | caseEnumKey }}("{{ value }}"){% if not loop.last %},{% else %};{% endif %} + +{% endfor %} + + private final String value; + + {{ enum.name | caseUcfirst | overrideIdentifier }}(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } + + public static {{ enum.name | caseUcfirst | overrideIdentifier }} fromValue(String value) { + for ({{ enum.name | caseUcfirst | overrideIdentifier }} e : values()) { + if (e.value.equals(value)) return e; + } + throw new IllegalArgumentException("Unknown value: " + value); + } +} diff --git a/templates/java/src/main/java/io/appwrite/exceptions/Exception.java.twig b/templates/java/src/main/java/io/appwrite/exceptions/Exception.java.twig new file mode 100644 index 0000000000..cd5f96c6a0 --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/exceptions/Exception.java.twig @@ -0,0 +1,23 @@ +package {{ sdk.namespace | caseDot }}.exceptions; + +public class {{ spec.title | caseUcfirst }}Exception extends Exception { + + private final Integer code; + private final String type; + private final String response; + + public {{ spec.title | caseUcfirst }}Exception(String message, Integer code, String type, String response) { + super(message); + this.code = code; + this.type = type; + this.response = response; + } + + public {{ spec.title | caseUcfirst }}Exception(String message) { + this(message, null, null, null); + } + + public Integer getCode() { return code; } + public String getType() { return type; } + public String getResponse() { return response; } +} diff --git a/templates/java/src/main/java/io/appwrite/models/InputFile.java.twig b/templates/java/src/main/java/io/appwrite/models/InputFile.java.twig new file mode 100644 index 0000000000..5c02906feb --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/models/InputFile.java.twig @@ -0,0 +1,53 @@ +package {{ sdk.namespace | caseDot }}.models; + +import java.io.File; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class InputFile { + + public String path; + public String filename; + public String mimeType; + public String sourceType; + public Object data; + + private InputFile() {} + + public static InputFile fromFile(File file) { + InputFile input = new InputFile(); + try { + input.path = file.getCanonicalPath(); + input.filename = file.getName(); + String probed = Files.probeContentType(Paths.get(input.path)); + input.mimeType = probed != null ? probed + : URLConnection.guessContentTypeFromName(input.filename) != null + ? URLConnection.guessContentTypeFromName(input.filename) + : ""; + input.sourceType = "file"; + } catch (Exception e) { + throw new RuntimeException(e); + } + return input; + } + + public static InputFile fromPath(String path) { + InputFile input = fromFile(new File(path)); + input.sourceType = "path"; + return input; + } + + public static InputFile fromBytes(byte[] bytes, String filename, String mimeType) { + InputFile input = new InputFile(); + input.data = bytes; + input.filename = filename; + input.mimeType = mimeType; + input.sourceType = "bytes"; + return input; + } + + public static InputFile fromBytes(byte[] bytes) { + return fromBytes(bytes, "", ""); + } +} diff --git a/templates/java/src/main/java/io/appwrite/models/Model.java.twig b/templates/java/src/main/java/io/appwrite/models/Model.java.twig new file mode 100644 index 0000000000..fab555ee80 --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/models/Model.java.twig @@ -0,0 +1,118 @@ +package {{ sdk.namespace | caseDot }}.models; + +import java.util.*; +{% for property in definition.properties %} +{% if property.enum %} +import {{ sdk.namespace | caseDot }}.enums.{{ property.enumName | caseUcfirst }}; +{% endif %} +{% endfor %} + +/** + * {{ definition.description | replace({"\n": " "}) | raw }} + */ +public class {{ definition.name | caseUcfirst }}{% if definition.additionalProperties %}{% endif %} { + +{% for property in definition.properties %} + /** {{ property.description | replace({"\n": " "}) | raw }} */ + private {{ property | typeName }}{% if not property.required %}{% endif %} {{ property.name | caseCamel | removeDollarSign }}; + +{% endfor %} +{% if definition.additionalProperties %} + private T data; + +{% endif %} + public {{ definition.name | caseUcfirst }}( +{% for property in definition.properties %} + {{ property | typeName }} {{ property.name | caseCamel | removeDollarSign }}{% if not loop.last or definition.additionalProperties %},{% endif %} + +{% endfor %} +{% if definition.additionalProperties %} + T data +{% endif %} + ) { +{% for property in definition.properties %} + this.{{ property.name | caseCamel | removeDollarSign }} = {{ property.name | caseCamel | removeDollarSign }}; +{% endfor %} +{% if definition.additionalProperties %} + this.data = data; +{% endif %} + } + +{% for property in definition.properties %} + public {{ property | typeName }} get{{ property.name | caseUcfirst | removeDollarSign }}() { return {{ property.name | caseCamel | removeDollarSign }}; } + public void set{{ property.name | caseUcfirst | removeDollarSign }}({{ property | typeName }} {{ property.name | caseCamel | removeDollarSign }}) { this.{{ property.name | caseCamel | removeDollarSign }} = {{ property.name | caseCamel | removeDollarSign }}; } +{% endfor %} +{% if definition.additionalProperties %} + public T getData() { return data; } + public void setData(T data) { this.data = data; } +{% endif %} + + @SuppressWarnings("unchecked") + public static {% if definition.additionalProperties %} {{ definition.name | caseUcfirst }}{% else %}{{ definition.name | caseUcfirst }}{% endif %} from(Map map{% if definition.additionalProperties %}, Class nestedType{% endif %}) { + return new {{ definition.name | caseUcfirst }}{% if definition.additionalProperties %}{% endif %}( +{% for property in definition.properties %} + {% if property.sub_schema %} + {% if property.type == 'array' %} + ((java.util.function.Supplier>) () -> { + List> list = (List>) map.get("{{ property.name }}"); + List<{{ property.sub_schema | caseUcfirst }}> result = new ArrayList<>(); + if (list != null) { for (Map item : list) result.add({{ property.sub_schema | caseUcfirst }}.from(item)); } + return result; + }).get() + {% else %} + map.get("{{ property.name }}") != null ? {{ property.sub_schema | caseUcfirst }}.from((Map) map.get("{{ property.name }}")) : null + {% endif %} + {% elseif property.enum and property.type == 'array' %} + ((java.util.function.Supplier>) () -> { + List rawList = (List) map.get("{{ property.name }}"); + List<{{ property.enumName | caseUcfirst }}> result = new ArrayList<>(); + if (rawList != null) { for (String val : rawList) { for ({{ property.enumName | caseUcfirst }} e : {{ property.enumName | caseUcfirst }}.values()) { if (e.getValue().equals(val)) { result.add(e); break; } } } } + return result; + }).get() + {% elseif property.enum %} + ((java.util.function.Supplier<{{ property.enumName | caseUcfirst }}>) () -> { + String val = (String) map.get("{{ property.name }}"); + if (val == null) return null; + for ({{ property.enumName | caseUcfirst }} e : {{ property.enumName | caseUcfirst }}.values()) { + if (e.getValue().equals(val)) return e; + } + return null; + }).get() + {% elseif property.type == 'integer' %} + map.get("{{ property.name }}") != null ? ((Number) map.get("{{ property.name }}")).longValue() : {% if property.required %}0L{% else %}null{% endif %} + {% elseif property.type == 'number' %} + map.get("{{ property.name }}") != null ? ((Number) map.get("{{ property.name }}")).doubleValue() : {% if property.required %}0.0{% else %}null{% endif %} + {% else %} + ({{ property | typeName }}) map.get("{{ property.name }}") + {% endif %} + {% if not loop.last %},{% endif %} + +{% endfor %} +{% if definition.additionalProperties %} + , (T) map.get("data") +{% endif %} + ); + } + + public Map toMap() { + Map map = new LinkedHashMap<>(); +{% for property in definition.properties %} + map.put("{{ property.name }}", {% if property.sub_schema %}{% if property.type == 'array' %} + ((java.util.function.Supplier>>) () -> { List> l = new ArrayList<>(); if ({{ property.name | caseCamel | removeDollarSign }} != null) for (var i : {{ property.name | caseCamel | removeDollarSign }}) l.add(i.toMap()); return l; }).get(){% else %}{{ property.name | caseCamel | removeDollarSign }} != null ? {{ property.name | caseCamel | removeDollarSign }}.toMap() : null{% endif %}{% elseif property.enum and property.type == 'array' %}((java.util.function.Supplier>) () -> { List l = new ArrayList<>(); if ({{ property.name | caseCamel | removeDollarSign }} != null) for (var i : {{ property.name | caseCamel | removeDollarSign }}) l.add(i.getValue()); return l; }).get(){% elseif property.enum %}{{ property.name | caseCamel | removeDollarSign }} != null ? {{ property.name | caseCamel | removeDollarSign }}.getValue() : null{% else %}{{ property.name | caseCamel | removeDollarSign }}{% endif %}); +{% endfor %} +{% if definition.additionalProperties %} + map.put("data", data); +{% endif %} + return map; + } + + @Override + public String toString() { + return "{{ definition.name | caseUcfirst }}{" + +{% for property in definition.properties %} + "{{ property.name | caseCamel | removeDollarSign }}=" + {{ property.name | caseCamel | removeDollarSign }}{% if not loop.last %} + ", " +{% else %} +{% endif %} + +{% endfor %} + "}"; + } +} diff --git a/templates/java/src/main/java/io/appwrite/models/RequestModel.java.twig b/templates/java/src/main/java/io/appwrite/models/RequestModel.java.twig new file mode 100644 index 0000000000..6a93a6dc68 --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/models/RequestModel.java.twig @@ -0,0 +1,44 @@ +package {{ sdk.namespace | caseDot }}.models; + +import java.util.*; +{% for property in requestModel.properties %} +{% if property.enum %} +import {{ sdk.namespace | caseDot }}.enums.{{ property.enumName | caseUcfirst }}; +{% endif %} +{% endfor %} + +/** + * {{ requestModel.description | replace({"\n": " "}) | raw }} + */ +public class {{ requestModel.name | caseUcfirst }} { + +{% for property in requestModel.properties %} + /** {{ property.description | replace({"\n": " "}) | raw }} */ + private {{ property | typeName }} {{ property.name | caseCamel | removeDollarSign }}; + +{% endfor %} + public {{ requestModel.name | caseUcfirst }}( +{% for property in requestModel.properties %} + {{ property | typeName }} {{ property.name | caseCamel | removeDollarSign }}{% if not loop.last %},{% endif %} + +{% endfor %} + ) { +{% for property in requestModel.properties %} + this.{{ property.name | caseCamel | removeDollarSign }} = {{ property.name | caseCamel | removeDollarSign }}; +{% endfor %} + } + +{% for property in requestModel.properties %} + public {{ property | typeName }} get{{ property.name | caseUcfirst | removeDollarSign }}() { return {{ property.name | caseCamel | removeDollarSign }}; } + public void set{{ property.name | caseUcfirst | removeDollarSign }}({{ property | typeName }} {{ property.name | caseCamel | removeDollarSign }}) { this.{{ property.name | caseCamel | removeDollarSign }} = {{ property.name | caseCamel | removeDollarSign }}; } + +{% endfor %} + public Map toMap() { + Map map = new LinkedHashMap<>(); +{% for property in requestModel.properties %} + map.put("{{ property.name }}", {% if property.sub_schema %}{% if property.type == 'array' %} + ((java.util.function.Supplier>>) () -> { List> l = new ArrayList<>(); if ({{ property.name | caseCamel | removeDollarSign }} != null) for (var i : {{ property.name | caseCamel | removeDollarSign }}) l.add(i.toMap()); return l; }).get(){% else %}{{ property.name | caseCamel | removeDollarSign }} != null ? {{ property.name | caseCamel | removeDollarSign }}.toMap() : null{% endif %}{% elseif property.enum and property.type == 'array' %}((java.util.function.Supplier>) () -> { List l = new ArrayList<>(); if ({{ property.name | caseCamel | removeDollarSign }} != null) for (var i : {{ property.name | caseCamel | removeDollarSign }}) l.add(i.getValue()); return l; }).get(){% elseif property.enum %}{{ property.name | caseCamel | removeDollarSign }} != null ? {{ property.name | caseCamel | removeDollarSign }}.getValue() : null{% else %}{{ property.name | caseCamel | removeDollarSign }}{% endif %}); +{% endfor %} + return map; + } +} diff --git a/templates/java/src/main/java/io/appwrite/models/UploadProgress.java.twig b/templates/java/src/main/java/io/appwrite/models/UploadProgress.java.twig new file mode 100644 index 0000000000..bacea1e225 --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/models/UploadProgress.java.twig @@ -0,0 +1,31 @@ +package {{ sdk.namespace | caseDot }}.models; + +public class UploadProgress { + + private final String id; + private final double progress; + private final long sizeUploaded; + private final int chunksTotal; + private final int chunksUploaded; + + public UploadProgress(String id, double progress, long sizeUploaded, int chunksTotal, int chunksUploaded) { + this.id = id; + this.progress = progress; + this.sizeUploaded = sizeUploaded; + this.chunksTotal = chunksTotal; + this.chunksUploaded = chunksUploaded; + } + + public String getId() { return id; } + public double getProgress() { return progress; } + public long getSizeUploaded() { return sizeUploaded; } + public int getChunksTotal() { return chunksTotal; } + public int getChunksUploaded() { return chunksUploaded; } + + @Override + public String toString() { + return "UploadProgress{id='" + id + "', progress=" + progress + + ", sizeUploaded=" + sizeUploaded + ", chunksTotal=" + chunksTotal + + ", chunksUploaded=" + chunksUploaded + "}"; + } +} diff --git a/templates/java/src/main/java/io/appwrite/services/Service.java.twig b/templates/java/src/main/java/io/appwrite/services/Service.java.twig new file mode 100644 index 0000000000..1a3959468a --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/services/Service.java.twig @@ -0,0 +1,12 @@ +package {{ sdk.namespace | caseDot }}.services; + +import {{ sdk.namespace | caseDot }}.Client; + +public abstract class Service { + + protected final Client client; + + public Service(Client client) { + this.client = client; + } +} diff --git a/templates/java/src/main/java/io/appwrite/services/ServiceTemplate.java.twig b/templates/java/src/main/java/io/appwrite/services/ServiceTemplate.java.twig new file mode 100644 index 0000000000..54c8293588 --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/services/ServiceTemplate.java.twig @@ -0,0 +1,118 @@ +package {{ sdk.namespace | caseDot }}.services; + +import {{ sdk.namespace | caseDot }}.Client; +{% if spec.definitions is not empty %} +import {{ sdk.namespace | caseDot }}.models.*; +{% endif %} +{% if spec.requestEnums is not empty %} +import {{ sdk.namespace | caseDot }}.enums.*; +{% endif %} +import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Exception; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +/** + * {{ service.description | replace({"\n": " "}) | raw }} + */ +public class {{ service.name | caseUcfirst }} extends Service { + + public {{ service.name | caseUcfirst }}(Client client) { + super(client); + } + +{% for method in service.methods %} + /** + * {{ method.description | replace({"\n": "\n * "}) | raw }} + * + {%~ for parameter in method.parameters.all %} + * @param {{ parameter.name | caseCamel }} {{ parameter.description | raw }} + {%~ endfor %} + * @return CompletableFuture + */ + {%~ if method.deprecated %} + @Deprecated + {%~ endif %} + @SuppressWarnings("unchecked") + public CompletableFuture<{{ method | returnType(spec, sdk.namespace | caseDot) | raw }}> {{ method.name | caseCamel }}( + {%~ for parameter in method.parameters.all %} + {{ parameter | typeName }} {{ parameter.name | caseCamel }}{% if not loop.last %},{% endif %} + + {%~ endfor %} + {%~ if 'multipart/form-data' in method.consumes %} + {%~ if method.parameters.all | length > 0 %}, {% endif %}java.util.function.Consumer<{{ sdk.namespace | caseDot }}.models.UploadProgress> onProgress + {%~ endif %} + ) { + String apiPath = "{{ method.path }}" + {%~ for parameter in method.parameters.path %} + .replace("{{ '{' ~ parameter.name | caseCamel ~ '}' }}", {{ parameter.name | caseCamel }}{% if parameter.enumValues is not empty %}.getValue(){% endif %}) + {%~ endfor %} + ; + + Map apiParams = new LinkedHashMap<>(); + {%~ for parameter in method.parameters.query | merge(method.parameters.body) %} + apiParams.put("{{ parameter.name }}", {{ parameter.name | caseCamel }}); + {%~ endfor %} + + Map apiHeaders = new LinkedHashMap<>(); + {%~ for key, header in method.headers %} + apiHeaders.put("{{ key }}", "{{ header }}"); + {%~ endfor %} + + {%~ if method.type == 'location' %} + return (CompletableFuture) client.call("{{ method.method | caseUpper }}", apiPath, apiHeaders, apiParams, byte[].class); + {%~ elseif method.type == 'webAuth' %} + return (CompletableFuture) client.redirect("{{ method.method | caseUpper }}", apiPath, apiHeaders, apiParams); + {%~ elseif method.responseModel and method.responseModel != 'any' and (method | getValidResponseModels)|length <= 1 %} + {%~ if method.responseModel | hasGenericType(spec) %} + Function converter = + it -> ({{ method | returnType(spec, sdk.namespace | caseDot) | raw }}) {{ sdk.namespace | caseDot }}.models.{{ method.responseModel | caseUcfirst }}.from((Map) it, Map.class); + return (CompletableFuture) client.call("{{ method.method | caseUpper }}", apiPath, apiHeaders, apiParams, {{ sdk.namespace | caseDot }}.models.{{ method.responseModel | caseUcfirst }}.class, (Function) converter); + {%~ else %} + Function converter = + it -> {{ method | returnType(spec, sdk.namespace | caseDot) | raw }}.from((Map) it); + {%~ if 'multipart/form-data' in method.consumes %} + return client.chunkedUpload(apiPath, apiHeaders, apiParams, {{ method | returnType(spec, sdk.namespace | caseDot) | raw }}.class, (Function) converter, "{{ method.parameters.file[0].name ?? 'file' }}", {% if method.parameters.body[0].name ?? '' %}"{{ method.parameters.body[0].name }}"{% else %}null{% endif %}, onProgress); + {%~ else %} + return client.call("{{ method.method | caseUpper }}", apiPath, apiHeaders, apiParams, {{ method | returnType(spec, sdk.namespace | caseDot) | raw }}.class, (Function) converter); + {%~ endif %} + {%~ endif %} + {%~ else %} + return (CompletableFuture) client.call("{{ method.method | caseUpper }}", apiPath, apiHeaders, apiParams, Object.class); + {%~ endif %} + } + + {%~ if (method.parameters.all | reduce((carry, param) => carry or param.required, false)) and (method.parameters.all | reduce((carry, param) => carry or not param.required, false)) %} + {%~ set requiredParams = method.parameters.all | filter(p => p.required) %} + /** + * {{ method.description | replace({"\n": "\n * "}) | raw }} — optional params default to null + */ + {%~ if method.deprecated %} + @Deprecated + {%~ endif %} + public CompletableFuture<{{ method | returnType(spec, sdk.namespace | caseDot) | raw }}> {{ method.name | caseCamel }}( + {%~ for parameter in requiredParams %} + {{ parameter | typeName }} {{ parameter.name | caseCamel }}{% if not loop.last %},{% endif %} + + {%~ endfor %} + ) { + return {{ method.name | caseCamel }}( + {%~ for parameter in method.parameters.all %} + {%~ if parameter.required %} + {{ parameter.name | caseCamel }}{% if not loop.last or 'multipart/form-data' in method.consumes %},{% endif %} + + {%~ else %} + null{% if not loop.last or 'multipart/form-data' in method.consumes %},{% endif %} + + {%~ endif %} + {%~ endfor %} + {%~ if 'multipart/form-data' in method.consumes %} + null + {%~ endif %} + ); + } + {%~ endif %} + +{% endfor %} +} diff --git a/tests/Base.php b/tests/Base.php index 21813acab2..ef8bfe39a0 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -378,7 +378,7 @@ public function testHTTPSuccess(): void ]) ->setTest("true"); - if ($this->language === 'android' || $this->language === 'kotlin') { + if ($this->language === 'android' || $this->language === 'kotlin' || $this->language === 'java') { $sdk->setNamespace("io.appwrite"); } else { $sdk->setNamespace("appwrite"); diff --git a/tests/JavaJava11Test.php b/tests/JavaJava11Test.php new file mode 100644 index 0000000000..854ac9ae5d --- /dev/null +++ b/tests/JavaJava11Test.php @@ -0,0 +1,42 @@ +/dev/null"'; + + protected array $expectedOutput = [ + ...Base::PING_RESPONSE, + ...Base::FOO_RESPONSES, + ...Base::BAR_RESPONSES, + ...Base::GENERAL_RESPONSES, + ...Base::UPLOAD_RESPONSES, + ...Base::ENUM_RESPONSES, + ...Base::MODEL_RESPONSES, + ...Base::EXCEPTION_RESPONSES, + ...Base::OAUTH_RESPONSES, + ...Base::QUERY_HELPER_RESPONSES, + ...Base::PERMISSION_HELPER_RESPONSES, + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES, + ]; + + public function getLanguage(): \Appwrite\SDK\Language + { + $java = new \Appwrite\SDK\Language\Java(); + return $java; + } +} diff --git a/tests/JavaJava17Test.php b/tests/JavaJava17Test.php new file mode 100644 index 0000000000..1bf8c78ff7 --- /dev/null +++ b/tests/JavaJava17Test.php @@ -0,0 +1,41 @@ +/dev/null"'; + + protected array $expectedOutput = [ + ...Base::PING_RESPONSE, + ...Base::FOO_RESPONSES, + ...Base::BAR_RESPONSES, + ...Base::GENERAL_RESPONSES, + ...Base::UPLOAD_RESPONSES, + ...Base::ENUM_RESPONSES, + ...Base::MODEL_RESPONSES, + ...Base::EXCEPTION_RESPONSES, + ...Base::OAUTH_RESPONSES, + ...Base::QUERY_HELPER_RESPONSES, + ...Base::PERMISSION_HELPER_RESPONSES, + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES, + ]; + + public function getLanguage(): \Appwrite\SDK\Language + { + return new \Appwrite\SDK\Language\Java(); + } +} diff --git a/tests/languages/java/Tests.java b/tests/languages/java/Tests.java new file mode 100644 index 0000000000..770771f8aa --- /dev/null +++ b/tests/languages/java/Tests.java @@ -0,0 +1,330 @@ +package io.appwrite; + +import io.appwrite.exceptions.AppwriteException; +import io.appwrite.models.InputFile; +import io.appwrite.models.Mock; +import io.appwrite.models.Player; +import io.appwrite.enums.MockType; +import io.appwrite.services.Bar; +import io.appwrite.services.Foo; +import io.appwrite.services.General; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +public class Tests { + + private static PrintWriter out; + + public static void main(String[] args) throws Exception { + Files.deleteIfExists(Paths.get("result.txt")); + out = new PrintWriter(new java.io.FileWriter("result.txt", true)); + + write("Test Started"); + + Client client = new Client() + .setProject("123456") + .addHeader("Origin", "http://localhost") + .setSelfSigned(true); + + Map sdkHeaders = client.getHeaders(); + write("x-sdk-name: " + sdkHeaders.get("x-sdk-name") + + "; x-sdk-platform: " + sdkHeaders.get("x-sdk-platform") + + "; x-sdk-language: " + sdkHeaders.get("x-sdk-language") + + "; x-sdk-version: " + sdkHeaders.get("x-sdk-version")); + + // Ping + try { + String ping = (String) client.call("GET", "/ping", + Map.of("content-type", "application/json"), Map.of(), String.class).get(); + com.google.gson.Gson gson = new com.google.gson.Gson(); + @SuppressWarnings("unchecked") + Map pingMap = gson.fromJson(ping, Map.class); + write((String) pingMap.get("result")); + } catch (Exception e) { + write(e.getMessage()); + } + + client.setProject("123456"); + + Foo foo = new Foo(client); + Bar bar = new Bar(client); + General general = new General(client); + + // Foo Tests + Mock mock; + + mock = foo.get("string", 123L, List.of("string in array")).get(); + write(mock.getResult()); + + mock = foo.post("string", 123L, List.of("string in array")).get(); + write(mock.getResult()); + + mock = foo.put("string", 123L, List.of("string in array")).get(); + write(mock.getResult()); + + mock = foo.patch("string", 123L, List.of("string in array")).get(); + write(mock.getResult()); + + mock = foo.delete("string", 123L, List.of("string in array")).get(); + write(mock.getResult()); + + // Bar Tests + mock = bar.get("string", 123L, List.of("string in array")).get(); + write(mock.getResult()); + + mock = bar.post("string", 123L, List.of("string in array")).get(); + write(mock.getResult()); + + mock = bar.put("string", 123L, List.of("string in array")).get(); + write(mock.getResult()); + + mock = bar.patch("string", 123L, List.of("string in array")).get(); + write(mock.getResult()); + + mock = bar.delete("string", 123L, List.of("string in array")).get(); + write(mock.getResult()); + + // General Tests + try { + String result = general.redirect().get(); + write(result); + } catch (Exception e) { + write(e.getMessage()); + } + + // Upload Tests + try { + mock = general.upload("string", 123L, List.of("string in array"), + InputFile.fromPath("../../resources/file.png"), null).get(); + write(mock.getResult()); + } catch (Exception e) { + write(e.getCause() != null ? e.getCause().getMessage() : e.getMessage()); + } + + try { + mock = general.upload("string", 123L, List.of("string in array"), + InputFile.fromPath("../../resources/large_file.mp4"), null).get(); + write(mock.getResult()); + } catch (Exception e) { + write(e.getCause() != null ? e.getCause().getMessage() : e.getMessage()); + } + + try { + byte[] bytes = Files.readAllBytes(Paths.get("../../resources/file.png")); + mock = general.upload("string", 123L, List.of("string in array"), + InputFile.fromBytes(bytes, "file.png", "image/png"), null).get(); + write(mock.getResult()); + } catch (Exception e) { + write(e.getCause() != null ? e.getCause().getMessage() : e.getMessage()); + } + + try { + byte[] bytes = Files.readAllBytes(Paths.get("../../resources/large_file.mp4")); + mock = general.upload("string", 123L, List.of("string in array"), + InputFile.fromBytes(bytes, "large_file.mp4", "video/mp4"), null).get(); + write(mock.getResult()); + } catch (Exception e) { + write(e.getCause() != null ? e.getCause().getMessage() : e.getMessage()); + } + + // Enum Test + mock = general.enumTest(MockType.FIRST).get(); + write(mock.getResult()); + + // Model Tests + mock = general.createPlayer(new Player("player1", "John Doe", 100L)).get(); + write(mock.getResult()); + + mock = general.createPlayers(List.of( + new Player("player1", "John Doe", 100L), + new Player("player2", "Jane Doe", 200L) + )).get(); + write(mock.getResult()); + + // Exception Tests + try { + general.error400().get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause() != null ? e.getCause().getCause() : null; + if (cause instanceof AppwriteException) { + AppwriteException ex = (AppwriteException) cause; + write(ex.getMessage()); + write(ex.getResponse()); + } else { + write(e.getMessage()); + } + } + + try { + general.error500().get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause() != null ? e.getCause().getCause() : null; + if (cause instanceof AppwriteException) { + AppwriteException ex = (AppwriteException) cause; + write(ex.getMessage()); + write(ex.getResponse()); + } else { + write(e.getMessage()); + } + } + + try { + general.error502().get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause() != null ? e.getCause().getCause() : null; + if (cause instanceof AppwriteException) { + AppwriteException ex = (AppwriteException) cause; + write(ex.getMessage()); + write(ex.getResponse()); + } else { + write(e.getMessage()); + } + } + + try { + client.setEndpoint("htp://cloud.appwrite.io/v1"); + } catch (IllegalArgumentException e) { + write(e.getMessage()); + } + + general.empty().get(); + + // OAuth2 + String url = (String) general.oauth2("clientId", List.of("test"), "123456", + "https://localhost", "https://localhost").get(); + write(url); + + // Query helper tests + write(Query.equal("released", List.of(true))); + write(Query.equal("title", List.of("Spiderman", "Dr. Strange"))); + write(Query.notEqual("title", "Spiderman")); + write(Query.lessThan("releasedYear", 1990)); + write(Query.greaterThan("releasedYear", 1990)); + write(Query.search("name", "john")); + write(Query.isNull("name")); + write(Query.isNotNull("name")); + write(Query.between("age", 50, 100)); + write(Query.between("age", 50.5, 100.5)); + write(Query.between("name", "Anna", "Brad")); + write(Query.startsWith("name", "Ann")); + write(Query.endsWith("name", "nne")); + write(Query.select(List.of("name", "age"))); + write(Query.orderAsc("title")); + write(Query.orderDesc("title")); + write(Query.orderRandom()); + write(Query.cursorAfter("my_movie_id")); + write(Query.cursorBefore("my_movie_id")); + write(Query.limit(50)); + write(Query.offset(20)); + write(Query.contains("title", List.of("Spider"))); + write(Query.contains("labels", List.of("first"))); + write(Query.containsAny("labels", List.of("first", "second"))); + write(Query.containsAll("labels", List.of("first", "second"))); + write(Query.notContains("title", List.of("Spider"))); + write(Query.notSearch("name", "john")); + write(Query.notBetween("age", 50, 100)); + write(Query.notStartsWith("name", "Ann")); + write(Query.notEndsWith("name", "nne")); + write(Query.createdBefore("2023-01-01")); + write(Query.createdAfter("2023-01-01")); + write(Query.createdBetween("2023-01-01", "2023-12-31")); + write(Query.updatedBefore("2023-01-01")); + write(Query.updatedAfter("2023-01-01")); + write(Query.updatedBetween("2023-01-01", "2023-12-31")); + + // Spatial Distance queries + write(Query.distanceEqual("location", List.of(List.of(40.7128, -74), List.of(40.7128, -74)), 1000)); + write(Query.distanceEqual("location", List.of(40.7128, -74), 1000, true)); + write(Query.distanceNotEqual("location", List.of(40.7128, -74), 1000)); + write(Query.distanceNotEqual("location", List.of(40.7128, -74), 1000, true)); + write(Query.distanceGreaterThan("location", List.of(40.7128, -74), 1000)); + write(Query.distanceGreaterThan("location", List.of(40.7128, -74), 1000, true)); + write(Query.distanceLessThan("location", List.of(40.7128, -74), 1000)); + write(Query.distanceLessThan("location", List.of(40.7128, -74), 1000, true)); + + // Spatial queries + write(Query.intersects("location", List.of(40.7128, -74))); + write(Query.notIntersects("location", List.of(40.7128, -74))); + write(Query.crosses("location", List.of(40.7128, -74))); + write(Query.notCrosses("location", List.of(40.7128, -74))); + write(Query.overlaps("location", List.of(40.7128, -74))); + write(Query.notOverlaps("location", List.of(40.7128, -74))); + write(Query.touches("location", List.of(40.7128, -74))); + write(Query.notTouches("location", List.of(40.7128, -74))); + write(Query.contains("location", List.of(List.of(40.7128, -74), List.of(40.7128, -74)))); + write(Query.notContains("location", List.of(List.of(40.7128, -74), List.of(40.7128, -74)))); + write(Query.equal("location", List.of(List.of(40.7128, -74), List.of(40.7128, -74)))); + write(Query.notEqual("location", List.of(List.of(40.7128, -74), List.of(40.7128, -74)))); + + write(Query.or(List.of(Query.equal("released", List.of(true)), Query.lessThan("releasedYear", 1990)))); + write(Query.and(List.of(Query.equal("released", List.of(false)), Query.greaterThan("releasedYear", 2015)))); + + write(Query.regex("name", "pattern.*")); + write(Query.exists(List.of("attr1", "attr2"))); + write(Query.notExists(List.of("attr1", "attr2"))); + write(Query.elemMatch("friends", List.of( + Query.equal("name", "Alice"), + Query.greaterThan("age", 18) + ))); + + // Permission & Role helper tests + write(Permission.read(Role.any())); + write(Permission.write(Role.user(ID.custom("userid")))); + write(Permission.create(Role.users())); + write(Permission.update(Role.guests())); + write(Permission.delete(Role.team("teamId", "owner"))); + write(Permission.delete(Role.team("teamId"))); + write(Permission.create(Role.member("memberId"))); + write(Permission.update(Role.users("verified"))); + write(Permission.update(Role.user(ID.custom("userid"), "unverified"))); + write(Permission.create(Role.label("admin"))); + + // ID helper tests + write(ID.unique()); + write(ID.custom("custom_id")); + + // Operator helper tests + write(Operator.increment(1)); + write(Operator.increment(5, 100)); + write(Operator.decrement(1)); + write(Operator.decrement(3, 0)); + write(Operator.multiply(2)); + write(Operator.multiply(3, 1000)); + write(Operator.divide(2)); + write(Operator.divide(4, 1)); + write(Operator.modulo(5)); + write(Operator.power(2)); + write(Operator.power(3, 100)); + write(Operator.arrayAppend(List.of("item1", "item2"))); + write(Operator.arrayPrepend(List.of("first", "second"))); + write(Operator.arrayInsert(0, "newItem")); + write(Operator.arrayRemove("oldItem")); + write(Operator.arrayUnique()); + write(Operator.arrayIntersect(List.of("a", "b", "c"))); + write(Operator.arrayDiff(List.of("x", "y"))); + write(Operator.arrayFilter(Operator.Condition.EQUAL, "test")); + write(Operator.stringConcat("suffix")); + write(Operator.stringReplace("old", "new")); + write(Operator.toggle()); + write(Operator.dateAddDays(7)); + write(Operator.dateSubDays(3)); + write(Operator.dateSetNow()); + + out.flush(); + out.close(); + + // Print result to stdout + Files.readAllLines(Paths.get("result.txt")).forEach(System.out::println); + } + + private static void write(String line) { + out.println(line != null ? line : ""); + } +}