From 39f114c2ad662a37eb7fbb709d80976b4cbc200b Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 01:24:37 +0530 Subject: [PATCH 01/22] feat: add Java SDK language support --- example.php | 10 + src/SDK/Language/Java.php | 262 ++++++++++++++ templates/java/.gitignore | 8 + templates/java/CHANGELOG.md.twig | 3 + templates/java/LICENSE.md.twig | 1 + templates/java/README.md.twig | 38 ++ templates/java/docs/example.md.twig | 42 +++ templates/java/pom.xml.twig | 60 ++++ .../main/java/io/appwrite/Client.java.twig | 332 ++++++++++++++++++ .../src/main/java/io/appwrite/ID.java.twig | 28 ++ .../main/java/io/appwrite/Operator.java.twig | 143 ++++++++ .../java/io/appwrite/Permission.java.twig | 24 ++ .../src/main/java/io/appwrite/Query.java.twig | 78 ++++ .../src/main/java/io/appwrite/Role.java.twig | 43 +++ .../java/io/appwrite/enums/Enum.java.twig | 31 ++ .../appwrite/exceptions/Exception.java.twig | 23 ++ .../io/appwrite/models/InputFile.java.twig | 53 +++ .../java/io/appwrite/models/Model.java.twig | 111 ++++++ .../io/appwrite/models/RequestModel.java.twig | 44 +++ .../appwrite/models/UploadProgress.java.twig | 31 ++ .../io/appwrite/services/Service.java.twig | 12 + .../services/ServiceTemplate.java.twig | 119 +++++++ tests/Base.php | 2 +- tests/JavaJava11Test.php | 42 +++ tests/JavaJava17Test.php | 41 +++ tests/languages/java/Tests.java | 331 +++++++++++++++++ 26 files changed, 1911 insertions(+), 1 deletion(-) create mode 100644 src/SDK/Language/Java.php create mode 100644 templates/java/.gitignore create mode 100644 templates/java/CHANGELOG.md.twig create mode 100644 templates/java/LICENSE.md.twig create mode 100644 templates/java/README.md.twig create mode 100644 templates/java/docs/example.md.twig create mode 100644 templates/java/pom.xml.twig create mode 100644 templates/java/src/main/java/io/appwrite/Client.java.twig create mode 100644 templates/java/src/main/java/io/appwrite/ID.java.twig create mode 100644 templates/java/src/main/java/io/appwrite/Operator.java.twig create mode 100644 templates/java/src/main/java/io/appwrite/Permission.java.twig create mode 100644 templates/java/src/main/java/io/appwrite/Query.java.twig create mode 100644 templates/java/src/main/java/io/appwrite/Role.java.twig create mode 100644 templates/java/src/main/java/io/appwrite/enums/Enum.java.twig create mode 100644 templates/java/src/main/java/io/appwrite/exceptions/Exception.java.twig create mode 100644 templates/java/src/main/java/io/appwrite/models/InputFile.java.twig create mode 100644 templates/java/src/main/java/io/appwrite/models/Model.java.twig create mode 100644 templates/java/src/main/java/io/appwrite/models/RequestModel.java.twig create mode 100644 templates/java/src/main/java/io/appwrite/models/UploadProgress.java.twig create mode 100644 templates/java/src/main/java/io/appwrite/services/Service.java.twig create mode 100644 templates/java/src/main/java/io/appwrite/services/ServiceTemplate.java.twig create mode 100644 tests/JavaJava11Test.php create mode 100644 tests/JavaJava17Test.php create mode 100644 tests/languages/java/Tests.java 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..024b5aa767 --- /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 .= '-1L'; + break; + case self::TYPE_NUMBER: + $output .= '1.0'; + break; + case self::TYPE_ARRAY: + case self::TYPE_OBJECT: + $output .= 'null'; + break; + case self::TYPE_BOOLEAN: + $output .= 'false'; + break; + case self::TYPE_STRING: + $output .= '""'; + 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..43d33db0f3 --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/Client.java.twig @@ -0,0 +1,332 @@ +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 okhttp3.logging.HttpLoggingInterceptor; + +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.Builder urlBuilder = HttpUrl.parse(endPoint + path).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.getOrDefault("content-type", "application/json") : "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 ("file".equals(e.getKey())) { + 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; + String body = response.body().string(); + if (body.isEmpty()) return (T) Boolean.TRUE; + + if (responseType == String.class) return (T) body; + + if (responseType == byte[].class) return (T) body.getBytes(); + + Type mapType = new TypeToken>(){}.getType(); + Map map = gson.fromJson(body, mapType); + if (converter != null) return converter.apply(map); + return (T) map; + } catch ({{ spec.title | caseUcfirst }}Exception e) { + throw new RuntimeException(e); + } 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 = new java.io.FileInputStream(input.path).readAllBytes(); + } + 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()) { + 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; + } + + while (offset < size) { + if ("bytes".equals(input.sourceType)) { + byte[] src = (byte[]) input.data; + int len = (int) Math.min(CHUNK_SIZE, size - offset); + System.arraycopy(src, (int) offset, buffer, 0, len); + } else { + file.seek(offset); + file.read(buffer); + } + + long end = Math.min(offset + CHUNK_SIZE - 1, size - 1); + headers.put("Content-Range", "bytes " + offset + "-" + end + "/" + size); + + RequestBody chunkBody = RequestBody.create(buffer, 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 += CHUNK_SIZE; + 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..d09d649aa4 --- /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 = (System.nanoTime() / 1000) % 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..68f7e6997b --- /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", values).toString(); + } + + public static String arrayPrepend(List values) { + return new Operator("arrayPrepend", 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", values).toString(); + } + + public static String arrayDiff(List values) { + return new Operator("arrayDiff", 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..077781a146 --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/Query.java.twig @@ -0,0 +1,78 @@ +package {{ sdk.namespace | caseDot }}; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +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); } + + @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, List.copyOf(attributes)).toString(); } + public static String notExists(List attributes) { return new Query("notExists", null, List.copyOf(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, List.copyOf(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, value).toString(); } + public static String containsAll(String attribute, List value) { return new Query("containsAll", attribute, 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, List.copyOf(queries)).toString(); } + public static String and(List queries) { return new Query("and", null, List.copyOf(queries)).toString(); } + + @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..e940916cef --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/models/Model.java.twig @@ -0,0 +1,111 @@ +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 %},{% 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' %} + (() -> { + 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; + }).call() + {% else %} + map.get("{{ property.name }}") != null ? {{ property.sub_schema | caseUcfirst }}.from((Map) map.get("{{ property.name }}")) : null + {% endif %} + {% elseif property.enum %} + (() -> { + 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; + }).call() + {% 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' %} + (() -> { List> l = new ArrayList<>(); if ({{ property.name | caseCamel | removeDollarSign }} != null) for (var i : {{ property.name | caseCamel | removeDollarSign }}) l.add(i.toMap()); return l; }).call(){% else %}{{ property.name | caseCamel | removeDollarSign }} != null ? {{ property.name | caseCamel | removeDollarSign }}.toMap() : null{% endif %}{% 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..a7bbd7dc33 --- /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' %} + (() -> { List> l = new ArrayList<>(); if ({{ property.name | caseCamel | removeDollarSign }} != null) for (var i : {{ property.name | caseCamel | removeDollarSign }}) l.add(i.toMap()); return l; }).call(){% else %}{{ property.name | caseCamel | removeDollarSign }} != null ? {{ property.name | caseCamel | removeDollarSign }}.toMap() : null{% endif %}{% 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..72675cec95 --- /dev/null +++ b/templates/java/src/main/java/io/appwrite/services/ServiceTemplate.java.twig @@ -0,0 +1,119 @@ +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 not param.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 method.parameters.all %} + {%~ if parameter.required %} + {{ parameter | typeName }} {{ parameter.name | caseCamel }}{% if not loop.last %},{% endif %} + + {%~ 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..658a4452cb --- /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..636f856e9f --- /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..c9aad5aea4 --- /dev/null +++ b/tests/languages/java/Tests.java @@ -0,0 +1,331 @@ +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 { + @SuppressWarnings("unchecked") + Map result = (Map) general.redirect().get(); + write((String) result.get("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(); + if (cause instanceof AppwriteException) { + AppwriteException ex = (AppwriteException) cause; + write(ex.getMessage()); + write(ex.getResponse()); + } else { + write(cause.getMessage()); + } + } + + try { + general.error500().get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof AppwriteException) { + AppwriteException ex = (AppwriteException) cause; + write(ex.getMessage()); + write(ex.getResponse()); + } else { + write(cause.getMessage()); + } + } + + try { + general.error502().get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof AppwriteException) { + AppwriteException ex = (AppwriteException) cause; + write(ex.getMessage()); + write(ex.getResponse()); + } else { + write(cause.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 : ""); + } +} From ae049086c9198a496bf98a2dee4e5cb741dfd947 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 08:35:26 +0530 Subject: [PATCH 02/22] fix(java): address PR review issues - Query: add spatial/geo methods (distanceEqual/NotEqual/GreaterThan/LessThan with distance+meters fields, intersects/notIntersects/crosses/notCrosses/overlaps/notOverlaps/touches/notTouches, elemMatch) - Client: null-check HttpUrl.parse() result to prevent NPE on bad URLs - Client: fix last-chunk data corruption by using Arrays.copyOfRange/readFully instead of fixed CHUNK_SIZE buffer - Client: replace FileInputStream resource leak with Files.readAllBytes(Paths.get(...)) - Tests: fix AppwriteException unwrap depth (ExecutionException -> RuntimeException -> AppwriteException requires two getCause() calls) Co-Authored-By: Claude Sonnet 4.6 --- .../main/java/io/appwrite/Client.java.twig | 17 +++++---- .../src/main/java/io/appwrite/Query.java.twig | 37 +++++++++++++++++-- tests/languages/java/Tests.java | 12 +++--- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/templates/java/src/main/java/io/appwrite/Client.java.twig b/templates/java/src/main/java/io/appwrite/Client.java.twig index 43d33db0f3..a3fe02a15f 100644 --- a/templates/java/src/main/java/io/appwrite/Client.java.twig +++ b/templates/java/src/main/java/io/appwrite/Client.java.twig @@ -6,7 +6,6 @@ import {{ sdk.namespace | caseDot }}.models.UploadProgress; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import okhttp3.*; -import okhttp3.logging.HttpLoggingInterceptor; import javax.net.ssl.*; import java.io.*; @@ -129,7 +128,9 @@ public class Client { if (extraHeaders != null) for (Map.Entry e : extraHeaders.entrySet()) hb.add(e.getKey(), e.getValue()); Headers requestHeaders = hb.build(); - HttpUrl.Builder urlBuilder = HttpUrl.parse(endPoint + path).newBuilder(); + 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()) { @@ -273,7 +274,7 @@ public class Client { if ("bytes".equals(input.sourceType)) { data = (byte[]) input.data; } else { - data = new java.io.FileInputStream(input.path).readAllBytes(); + 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)); @@ -291,19 +292,21 @@ public class Client { } while (offset < size) { + int chunkLen = (int) Math.min(CHUNK_SIZE, size - offset); + byte[] chunk; if ("bytes".equals(input.sourceType)) { byte[] src = (byte[]) input.data; - int len = (int) Math.min(CHUNK_SIZE, size - offset); - System.arraycopy(src, (int) offset, buffer, 0, len); + chunk = Arrays.copyOfRange(src, (int) offset, (int) offset + chunkLen); } else { file.seek(offset); - file.read(buffer); + chunk = new byte[chunkLen]; + file.readFully(chunk); } long end = Math.min(offset + CHUNK_SIZE - 1, size - 1); headers.put("Content-Range", "bytes " + offset + "-" + end + "/" + size); - RequestBody chunkBody = RequestBody.create(buffer, MediaType.parse("application/octet-stream")); + 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(); diff --git a/templates/java/src/main/java/io/appwrite/Query.java.twig b/templates/java/src/main/java/io/appwrite/Query.java.twig index 077781a146..eede5649ef 100644 --- a/templates/java/src/main/java/io/appwrite/Query.java.twig +++ b/templates/java/src/main/java/io/appwrite/Query.java.twig @@ -18,14 +18,26 @@ public class Query { @SerializedName("values") private final List values; - public Query(String method, String attribute, List values) { + @SerializedName("distance") + private final Number distance; + + @SerializedName("meters") + private final Boolean meters; + + private Query(String method, String attribute, List values, Number distance, Boolean meters) { this.method = method; this.attribute = attribute; this.values = values; + this.distance = distance; + this.meters = meters; + } + + public Query(String method, String attribute, List values) { + this(method, attribute, values, null, null); } - public Query(String method, String attribute) { this(method, attribute, null); } - public Query(String method) { this(method, null, null); } + public Query(String method, String attribute) { this(method, attribute, null, null, null); } + public Query(String method) { this(method, null, null, null, null); } @Override public String toString() { return gson.toJson(this); } @@ -69,6 +81,25 @@ public class Query { public static String updatedBetween(String start, String end) { return between("$updatedAt", start, end); } public static String or(List queries) { return new Query("or", null, List.copyOf(queries)).toString(); } public static String and(List queries) { return new Query("and", null, List.copyOf(queries)).toString(); } + public static String elemMatch(String attribute, List queries) { return new Query("elemMatch", attribute, List.copyOf(queries)).toString(); } + + public static String distanceEqual(String attribute, List values, Number distance) { return new Query("distanceEqual", attribute, values, distance, null).toString(); } + public static String distanceEqual(String attribute, List values, Number distance, boolean meters) { return new Query("distanceEqual", attribute, values, distance, meters).toString(); } + public static String distanceNotEqual(String attribute, List values, Number distance) { return new Query("distanceNotEqual", attribute, values, distance, null).toString(); } + public static String distanceNotEqual(String attribute, List values, Number distance, boolean meters) { return new Query("distanceNotEqual", attribute, values, distance, meters).toString(); } + public static String distanceGreaterThan(String attribute, List values, Number distance) { return new Query("distanceGreaterThan", attribute, values, distance, null).toString(); } + public static String distanceGreaterThan(String attribute, List values, Number distance, boolean meters) { return new Query("distanceGreaterThan", attribute, values, distance, meters).toString(); } + public static String distanceLessThan(String attribute, List values, Number distance) { return new Query("distanceLessThan", attribute, values, distance, null).toString(); } + public static String distanceLessThan(String attribute, List values, Number distance, boolean meters) { return new Query("distanceLessThan", attribute, values, distance, meters).toString(); } + + public static String intersects(String attribute, List values) { return new Query("intersects", attribute, values).toString(); } + public static String notIntersects(String attribute, List values) { return new Query("notIntersects", attribute, values).toString(); } + public static String crosses(String attribute, List values) { return new Query("crosses", attribute, values).toString(); } + public static String notCrosses(String attribute, List values) { return new Query("notCrosses", attribute, values).toString(); } + public static String overlaps(String attribute, List values) { return new Query("overlaps", attribute, values).toString(); } + public static String notOverlaps(String attribute, List values) { return new Query("notOverlaps", attribute, values).toString(); } + public static String touches(String attribute, List values) { return new Query("touches", attribute, values).toString(); } + public static String notTouches(String attribute, List values) { return new Query("notTouches", attribute, values).toString(); } @SuppressWarnings("unchecked") private static List parseValue(Object value) { diff --git a/tests/languages/java/Tests.java b/tests/languages/java/Tests.java index c9aad5aea4..bd472bef21 100644 --- a/tests/languages/java/Tests.java +++ b/tests/languages/java/Tests.java @@ -153,39 +153,39 @@ public static void main(String[] args) throws Exception { try { general.error400().get(); } catch (ExecutionException e) { - Throwable cause = e.getCause(); + 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(cause.getMessage()); + write(e.getMessage()); } } try { general.error500().get(); } catch (ExecutionException e) { - Throwable cause = e.getCause(); + 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(cause.getMessage()); + write(e.getMessage()); } } try { general.error502().get(); } catch (ExecutionException e) { - Throwable cause = e.getCause(); + 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(cause.getMessage()); + write(e.getMessage()); } } From 3ad035eaab0d909c9c944fe63070b2eb535bf8cb Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 09:30:58 +0530 Subject: [PATCH 03/22] fix(java): fix binary response corruption and overload condition - Client: read byte[] responses with body().bytes() to avoid charset re-encoding that corrupts binary data - ServiceTemplate: only generate no-optional-args overload when method has both required AND optional params; avoids empty-arg overload when all params are optional Co-Authored-By: Claude Sonnet 4.6 --- templates/java/src/main/java/io/appwrite/Client.java.twig | 5 +++-- .../main/java/io/appwrite/services/ServiceTemplate.java.twig | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/templates/java/src/main/java/io/appwrite/Client.java.twig b/templates/java/src/main/java/io/appwrite/Client.java.twig index a3fe02a15f..5d28e52d32 100644 --- a/templates/java/src/main/java/io/appwrite/Client.java.twig +++ b/templates/java/src/main/java/io/appwrite/Client.java.twig @@ -201,13 +201,14 @@ public class Client { 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; - if (responseType == byte[].class) return (T) body.getBytes(); - Type mapType = new TypeToken>(){}.getType(); Map map = gson.fromJson(body, mapType); if (converter != null) return converter.apply(map); 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 index 72675cec95..9173611392 100644 --- a/templates/java/src/main/java/io/appwrite/services/ServiceTemplate.java.twig +++ b/templates/java/src/main/java/io/appwrite/services/ServiceTemplate.java.twig @@ -83,7 +83,7 @@ public class {{ service.name | caseUcfirst }} extends Service { {%~ endif %} } - {%~ if method.parameters.all | reduce((carry, param) => carry or not param.required) %} + {%~ 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)) %} /** * {{ method.description | replace({"\n": "\n * "}) | raw }} — optional params default to null */ From e96d41412585dc4608312b102cb99c8544123648 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 09:41:48 +0530 Subject: [PATCH 04/22] fix(java): fix Maven image and overload trailing comma - Switch test Docker images from eclipse-temurin (JDK only) to maven:3.9-eclipse-temurin-11/17 which bundles both JDK and Maven - Fix trailing comma in overload method signature by iterating only required params using Twig filter instead of relying on loop.last over the full parameter list --- .../main/java/io/appwrite/services/ServiceTemplate.java.twig | 5 ++--- tests/JavaJava11Test.php | 2 +- tests/JavaJava17Test.php | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) 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 index 9173611392..54c8293588 100644 --- a/templates/java/src/main/java/io/appwrite/services/ServiceTemplate.java.twig +++ b/templates/java/src/main/java/io/appwrite/services/ServiceTemplate.java.twig @@ -84,6 +84,7 @@ public class {{ service.name | caseUcfirst }} extends Service { } {%~ 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 */ @@ -91,11 +92,9 @@ public class {{ service.name | caseUcfirst }} extends Service { @Deprecated {%~ endif %} public CompletableFuture<{{ method | returnType(spec, sdk.namespace | caseDot) | raw }}> {{ method.name | caseCamel }}( - {%~ for parameter in method.parameters.all %} - {%~ if parameter.required %} + {%~ for parameter in requiredParams %} {{ parameter | typeName }} {{ parameter.name | caseCamel }}{% if not loop.last %},{% endif %} - {%~ endif %} {%~ endfor %} ) { return {{ method.name | caseCamel }}( diff --git a/tests/JavaJava11Test.php b/tests/JavaJava11Test.php index 658a4452cb..854ac9ae5d 100644 --- a/tests/JavaJava11Test.php +++ b/tests/JavaJava11Test.php @@ -16,7 +16,7 @@ class JavaJava11Test extends Base 'cp tests/languages/java/Tests.java tests/sdks/java/src/main/java/io/appwrite/Tests.java', ]; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/java eclipse-temurin:11-jdk-jammy sh -c "mvn compile exec:java -Dexec.mainClass=io.appwrite.Tests -q 2>/dev/null"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/java maven:3.9-eclipse-temurin-11 sh -c "mvn compile exec:java -Dexec.mainClass=io.appwrite.Tests -q 2>/dev/null"'; protected array $expectedOutput = [ ...Base::PING_RESPONSE, diff --git a/tests/JavaJava17Test.php b/tests/JavaJava17Test.php index 636f856e9f..1bf8c78ff7 100644 --- a/tests/JavaJava17Test.php +++ b/tests/JavaJava17Test.php @@ -16,7 +16,7 @@ class JavaJava17Test extends Base 'cp tests/languages/java/Tests.java tests/sdks/java/src/main/java/io/appwrite/Tests.java', ]; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/java eclipse-temurin:17-jdk-jammy sh -c "mvn compile exec:java -Dexec.mainClass=io.appwrite.Tests -q 2>/dev/null"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/java maven:3.9-eclipse-temurin-17 sh -c "mvn compile exec:java -Dexec.mainClass=io.appwrite.Tests -q 2>/dev/null"'; protected array $expectedOutput = [ ...Base::PING_RESPONSE, From 1f6613c1d95d664f5b510e85a09818685d37ff50 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 09:57:14 +0530 Subject: [PATCH 05/22] fix(java): fix List/List mismatch and invalid IIFE lambdas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Query: replace List.copyOf(List) with new ArrayList(...) for or/and/elemMatch/exists/notExists/select to fix generics invariance compile error - Model/RequestModel: replace bare (() -> {}).call() with ((Supplier) () -> {}).get() — untyped IIFE lambdas are invalid Java (valid Kotlin only); affects sub_schema array and enum array properties in from() and toMap() --- .../java/src/main/java/io/appwrite/Query.java.twig | 12 ++++++------ .../src/main/java/io/appwrite/models/Model.java.twig | 10 +++++----- .../java/io/appwrite/models/RequestModel.java.twig | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/templates/java/src/main/java/io/appwrite/Query.java.twig b/templates/java/src/main/java/io/appwrite/Query.java.twig index eede5649ef..0a8a838f2d 100644 --- a/templates/java/src/main/java/io/appwrite/Query.java.twig +++ b/templates/java/src/main/java/io/appwrite/Query.java.twig @@ -52,12 +52,12 @@ public class Query { 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, List.copyOf(attributes)).toString(); } - public static String notExists(List attributes) { return new Query("notExists", null, List.copyOf(attributes)).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, List.copyOf(attributes)).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(); } @@ -79,9 +79,9 @@ public class Query { 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, List.copyOf(queries)).toString(); } - public static String and(List queries) { return new Query("and", null, List.copyOf(queries)).toString(); } - public static String elemMatch(String attribute, List queries) { return new Query("elemMatch", attribute, List.copyOf(queries)).toString(); } + public static String or(List queries) { return new Query("or", null, new ArrayList(queries)).toString(); } + public static String and(List queries) { return new Query("and", null, new ArrayList(queries)).toString(); } + public static String elemMatch(String attribute, List queries) { return new Query("elemMatch", attribute, new ArrayList(queries)).toString(); } public static String distanceEqual(String attribute, List values, Number distance) { return new Query("distanceEqual", attribute, values, distance, null).toString(); } public static String distanceEqual(String attribute, List values, Number distance, boolean meters) { return new Query("distanceEqual", attribute, values, distance, meters).toString(); } 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 index e940916cef..b1e20c5203 100644 --- a/templates/java/src/main/java/io/appwrite/models/Model.java.twig +++ b/templates/java/src/main/java/io/appwrite/models/Model.java.twig @@ -53,24 +53,24 @@ public class {{ definition.name | caseUcfirst }}{% if definition.additionalPrope {% 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; - }).call() + }).get() {% else %} map.get("{{ property.name }}") != null ? {{ property.sub_schema | caseUcfirst }}.from((Map) map.get("{{ property.name }}")) : null {% endif %} {% 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; - }).call() + }).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' %} @@ -91,7 +91,7 @@ public class {{ definition.name | caseUcfirst }}{% if definition.additionalPrope Map map = new LinkedHashMap<>(); {% for property in definition.properties %} map.put("{{ property.name }}", {% if property.sub_schema %}{% if property.type == 'array' %} - (() -> { List> l = new ArrayList<>(); if ({{ property.name | caseCamel | removeDollarSign }} != null) for (var i : {{ property.name | caseCamel | removeDollarSign }}) l.add(i.toMap()); return l; }).call(){% else %}{{ property.name | caseCamel | removeDollarSign }} != null ? {{ property.name | caseCamel | removeDollarSign }}.toMap() : null{% endif %}{% elseif property.enum %}{{ property.name | caseCamel | removeDollarSign }} != null ? {{ property.name | caseCamel | removeDollarSign }}.getValue() : null{% else %}{{ property.name | caseCamel | removeDollarSign }}{% endif %}); + ((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 %}{{ 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); 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 index a7bbd7dc33..3e6bdb1fa7 100644 --- a/templates/java/src/main/java/io/appwrite/models/RequestModel.java.twig +++ b/templates/java/src/main/java/io/appwrite/models/RequestModel.java.twig @@ -37,7 +37,7 @@ public class {{ requestModel.name | caseUcfirst }} { Map map = new LinkedHashMap<>(); {% for property in requestModel.properties %} map.put("{{ property.name }}", {% if property.sub_schema %}{% if property.type == 'array' %} - (() -> { List> l = new ArrayList<>(); if ({{ property.name | caseCamel | removeDollarSign }} != null) for (var i : {{ property.name | caseCamel | removeDollarSign }}) l.add(i.toMap()); return l; }).call(){% else %}{{ property.name | caseCamel | removeDollarSign }} != null ? {{ property.name | caseCamel | removeDollarSign }}.toMap() : null{% endif %}{% elseif property.enum %}{{ property.name | caseCamel | removeDollarSign }} != null ? {{ property.name | caseCamel | removeDollarSign }}.getValue() : null{% else %}{{ property.name | caseCamel | removeDollarSign }}{% endif %}); + ((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 %}{{ property.name | caseCamel | removeDollarSign }} != null ? {{ property.name | caseCamel | removeDollarSign }}.getValue() : null{% else %}{{ property.name | caseCamel | removeDollarSign }}{% endif %}); {% endfor %} return map; } From 23588794d397609699b0c95bdf3af22d64dd32cc Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 10:12:50 +0530 Subject: [PATCH 06/22] fix(java): add missing ArrayList import to Query.java.twig --- templates/java/src/main/java/io/appwrite/Query.java.twig | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/java/src/main/java/io/appwrite/Query.java.twig b/templates/java/src/main/java/io/appwrite/Query.java.twig index 0a8a838f2d..20ebb7a0aa 100644 --- a/templates/java/src/main/java/io/appwrite/Query.java.twig +++ b/templates/java/src/main/java/io/appwrite/Query.java.twig @@ -2,6 +2,7 @@ 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; From 7e18a52f7222ce810a1ce7575dd4f64d19ad5da1 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 10:23:54 +0530 Subject: [PATCH 07/22] fix(java): return early in chunkedUpload when file already fully uploaded Prevents NPE from converter.apply(null) when resuming an upload that is already complete (offset >= size means the while loop never runs) --- templates/java/src/main/java/io/appwrite/Client.java.twig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/templates/java/src/main/java/io/appwrite/Client.java.twig b/templates/java/src/main/java/io/appwrite/Client.java.twig index 5d28e52d32..a9542d6f3a 100644 --- a/templates/java/src/main/java/io/appwrite/Client.java.twig +++ b/templates/java/src/main/java/io/appwrite/Client.java.twig @@ -290,6 +290,9 @@ public class Client { 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); + } } while (offset < size) { From 6717cdc90a06b569bf16e1ca52fe317aecd8bd11 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 10:39:59 +0530 Subject: [PATCH 08/22] fix(java): widen List params to List across Query and Operator Java generics are invariant so List/List cannot be assigned to List. Tests.java passes typed lists (List.of(...)) to containsAny, containsAll, all spatial/distance methods in Query, and arrayAppend/arrayPrepend/arrayIntersect/arrayDiff in Operator. Changed all public method signatures to List and widen internally with new ArrayList(values). --- .../main/java/io/appwrite/Operator.java.twig | 16 ++++---- .../src/main/java/io/appwrite/Query.java.twig | 38 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/templates/java/src/main/java/io/appwrite/Operator.java.twig b/templates/java/src/main/java/io/appwrite/Operator.java.twig index 68f7e6997b..0033fbd847 100644 --- a/templates/java/src/main/java/io/appwrite/Operator.java.twig +++ b/templates/java/src/main/java/io/appwrite/Operator.java.twig @@ -85,12 +85,12 @@ public class Operator { return new Operator("power", List.of(exponent, max)).toString(); } - public static String arrayAppend(List values) { - return new Operator("arrayAppend", values).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", values).toString(); + public static String arrayPrepend(List values) { + return new Operator("arrayPrepend", new ArrayList(values)).toString(); } public static String arrayInsert(int index, Object value) { @@ -105,12 +105,12 @@ public class Operator { return new Operator("arrayUnique", List.of()).toString(); } - public static String arrayIntersect(List values) { - return new Operator("arrayIntersect", values).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", values).toString(); + public static String arrayDiff(List values) { + return new Operator("arrayDiff", new ArrayList(values)).toString(); } public static String arrayFilter(Condition condition, Object value) { diff --git a/templates/java/src/main/java/io/appwrite/Query.java.twig b/templates/java/src/main/java/io/appwrite/Query.java.twig index 20ebb7a0aa..5f18290317 100644 --- a/templates/java/src/main/java/io/appwrite/Query.java.twig +++ b/templates/java/src/main/java/io/appwrite/Query.java.twig @@ -67,8 +67,8 @@ public class Query { 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, value).toString(); } - public static String containsAll(String attribute, List value) { return new Query("containsAll", attribute, 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(); } @@ -84,23 +84,23 @@ public class Query { public static String and(List queries) { return new Query("and", null, new ArrayList(queries)).toString(); } public static String elemMatch(String attribute, List queries) { return new Query("elemMatch", attribute, new ArrayList(queries)).toString(); } - public static String distanceEqual(String attribute, List values, Number distance) { return new Query("distanceEqual", attribute, values, distance, null).toString(); } - public static String distanceEqual(String attribute, List values, Number distance, boolean meters) { return new Query("distanceEqual", attribute, values, distance, meters).toString(); } - public static String distanceNotEqual(String attribute, List values, Number distance) { return new Query("distanceNotEqual", attribute, values, distance, null).toString(); } - public static String distanceNotEqual(String attribute, List values, Number distance, boolean meters) { return new Query("distanceNotEqual", attribute, values, distance, meters).toString(); } - public static String distanceGreaterThan(String attribute, List values, Number distance) { return new Query("distanceGreaterThan", attribute, values, distance, null).toString(); } - public static String distanceGreaterThan(String attribute, List values, Number distance, boolean meters) { return new Query("distanceGreaterThan", attribute, values, distance, meters).toString(); } - public static String distanceLessThan(String attribute, List values, Number distance) { return new Query("distanceLessThan", attribute, values, distance, null).toString(); } - public static String distanceLessThan(String attribute, List values, Number distance, boolean meters) { return new Query("distanceLessThan", attribute, values, distance, meters).toString(); } - - public static String intersects(String attribute, List values) { return new Query("intersects", attribute, values).toString(); } - public static String notIntersects(String attribute, List values) { return new Query("notIntersects", attribute, values).toString(); } - public static String crosses(String attribute, List values) { return new Query("crosses", attribute, values).toString(); } - public static String notCrosses(String attribute, List values) { return new Query("notCrosses", attribute, values).toString(); } - public static String overlaps(String attribute, List values) { return new Query("overlaps", attribute, values).toString(); } - public static String notOverlaps(String attribute, List values) { return new Query("notOverlaps", attribute, values).toString(); } - public static String touches(String attribute, List values) { return new Query("touches", attribute, values).toString(); } - public static String notTouches(String attribute, List values) { return new Query("notTouches", attribute, values).toString(); } + public static String distanceEqual(String attribute, List values, Number distance) { return new Query("distanceEqual", attribute, new ArrayList(values), distance, null).toString(); } + public static String distanceEqual(String attribute, List values, Number distance, boolean meters) { return new Query("distanceEqual", attribute, new ArrayList(values), distance, meters).toString(); } + public static String distanceNotEqual(String attribute, List values, Number distance) { return new Query("distanceNotEqual", attribute, new ArrayList(values), distance, null).toString(); } + public static String distanceNotEqual(String attribute, List values, Number distance, boolean meters) { return new Query("distanceNotEqual", attribute, new ArrayList(values), distance, meters).toString(); } + public static String distanceGreaterThan(String attribute, List values, Number distance) { return new Query("distanceGreaterThan", attribute, new ArrayList(values), distance, null).toString(); } + public static String distanceGreaterThan(String attribute, List values, Number distance, boolean meters) { return new Query("distanceGreaterThan", attribute, new ArrayList(values), distance, meters).toString(); } + public static String distanceLessThan(String attribute, List values, Number distance) { return new Query("distanceLessThan", attribute, new ArrayList(values), distance, null).toString(); } + public static String distanceLessThan(String attribute, List values, Number distance, boolean meters) { return new Query("distanceLessThan", attribute, new ArrayList(values), distance, meters).toString(); } + + public static String intersects(String attribute, List values) { return new Query("intersects", attribute, new ArrayList(values)).toString(); } + public static String notIntersects(String attribute, List values) { return new Query("notIntersects", attribute, new ArrayList(values)).toString(); } + public static String crosses(String attribute, List values) { return new Query("crosses", attribute, new ArrayList(values)).toString(); } + public static String notCrosses(String attribute, List values) { return new Query("notCrosses", attribute, new ArrayList(values)).toString(); } + public static String overlaps(String attribute, List values) { return new Query("overlaps", attribute, new ArrayList(values)).toString(); } + public static String notOverlaps(String attribute, List values) { return new Query("notOverlaps", attribute, new ArrayList(values)).toString(); } + public static String touches(String attribute, List values) { return new Query("touches", attribute, new ArrayList(values)).toString(); } + public static String notTouches(String attribute, List values) { return new Query("notTouches", attribute, new ArrayList(values)).toString(); } @SuppressWarnings("unchecked") private static List parseValue(Object value) { From 6350db8eab40a12984dda3e62270d978505d1264 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 10:49:43 +0530 Subject: [PATCH 09/22] fix(java): fix redirect cast, Long default sentinel, content-type detection - Tests: redirect() returns CompletableFuture not Map; remove incorrect cast that caused ClassCastException at runtime - Java.php: change TYPE_INTEGER optional default from -1L to null so callers can distinguish unset from a valid negative value - Client: check extraHeaders first then fall back to global headers map for content-type detection instead of ignoring global headers --- src/SDK/Language/Java.php | 2 +- templates/java/src/main/java/io/appwrite/Client.java.twig | 4 +++- tests/languages/java/Tests.java | 5 ++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/SDK/Language/Java.php b/src/SDK/Language/Java.php index 024b5aa767..fcceac21d6 100644 --- a/src/SDK/Language/Java.php +++ b/src/SDK/Language/Java.php @@ -83,7 +83,7 @@ public function getParamDefault(array $param): string if (empty($default) && $default !== 0 && $default !== false) { switch ($type) { case self::TYPE_INTEGER: - $output .= '-1L'; + $output .= 'null'; break; case self::TYPE_NUMBER: $output .= '1.0'; diff --git a/templates/java/src/main/java/io/appwrite/Client.java.twig b/templates/java/src/main/java/io/appwrite/Client.java.twig index a9542d6f3a..8c3e5970c8 100644 --- a/templates/java/src/main/java/io/appwrite/Client.java.twig +++ b/templates/java/src/main/java/io/appwrite/Client.java.twig @@ -145,7 +145,9 @@ public class Client { return new Request.Builder().url(urlBuilder.build()).headers(requestHeaders).get().build(); } - String contentType = extraHeaders != null ? extraHeaders.getOrDefault("content-type", "application/json") : "application/json"; + 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); diff --git a/tests/languages/java/Tests.java b/tests/languages/java/Tests.java index bd472bef21..770771f8aa 100644 --- a/tests/languages/java/Tests.java +++ b/tests/languages/java/Tests.java @@ -93,9 +93,8 @@ public static void main(String[] args) throws Exception { // General Tests try { - @SuppressWarnings("unchecked") - Map result = (Map) general.redirect().get(); - write((String) result.get("result")); + String result = general.redirect().get(); + write(result); } catch (Exception e) { write(e.getMessage()); } From 8648c1e47b3d392b474c8aaed6450c39001759b7 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 11:15:18 +0530 Subject: [PATCH 10/22] fix(java): change optional Double default from 1.0 to null MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the Long fix — 1.0 is a valid API value and must not be sent silently when the caller omits the parameter. --- src/SDK/Language/Java.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SDK/Language/Java.php b/src/SDK/Language/Java.php index fcceac21d6..9640fcf503 100644 --- a/src/SDK/Language/Java.php +++ b/src/SDK/Language/Java.php @@ -86,7 +86,7 @@ public function getParamDefault(array $param): string $output .= 'null'; break; case self::TYPE_NUMBER: - $output .= '1.0'; + $output .= 'null'; break; case self::TYPE_ARRAY: case self::TYPE_OBJECT: From cdb0f14ed617fbaf19989c3163a58de5ed7d1b1e Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 11:23:24 +0530 Subject: [PATCH 11/22] fix(java): change optional String/Boolean defaults from sentinel to null Empty string and false are valid API values; using them as defaults causes unintended parameters to be sent on every call that omits them. Consistent with the null fix already applied to Long and Double. --- src/SDK/Language/Java.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SDK/Language/Java.php b/src/SDK/Language/Java.php index 9640fcf503..927f3f5a16 100644 --- a/src/SDK/Language/Java.php +++ b/src/SDK/Language/Java.php @@ -93,10 +93,10 @@ public function getParamDefault(array $param): string $output .= 'null'; break; case self::TYPE_BOOLEAN: - $output .= 'false'; + $output .= 'null'; break; case self::TYPE_STRING: - $output .= '""'; + $output .= 'null'; break; } } else { From f5b70778899d88c9e854020778e5364cfd9b2016 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 11:35:04 +0530 Subject: [PATCH 12/22] fix(java): detect multipart parts by type and handle array-of-enum in from() - Client: check instanceof MultipartBody.Part instead of hardcoded key name 'file' so any upload param name works correctly - Model: add array-of-enum branch before the scalar-enum branch in from() so List fields deserialize correctly instead of emitting a Supplier compile error --- templates/java/src/main/java/io/appwrite/Client.java.twig | 2 +- .../java/src/main/java/io/appwrite/models/Model.java.twig | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/templates/java/src/main/java/io/appwrite/Client.java.twig b/templates/java/src/main/java/io/appwrite/Client.java.twig index 8c3e5970c8..8e4f6ea0ce 100644 --- a/templates/java/src/main/java/io/appwrite/Client.java.twig +++ b/templates/java/src/main/java/io/appwrite/Client.java.twig @@ -152,7 +152,7 @@ public class Client { 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 ("file".equals(e.getKey())) { + 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()); 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 index b1e20c5203..67df256c0e 100644 --- a/templates/java/src/main/java/io/appwrite/models/Model.java.twig +++ b/templates/java/src/main/java/io/appwrite/models/Model.java.twig @@ -62,6 +62,13 @@ public class {{ definition.name | caseUcfirst }}{% if definition.additionalPrope {% 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 }}"); From b3e706689fbe25ad63e09f0baf39d43bb4fadbee Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 12:06:44 +0530 Subject: [PATCH 13/22] fix(java): add array-of-enum branch in toMap() for Model and RequestModel For List properties, iterate and call .getValue() on each element. Without this branch the scalar-enum path called .getValue() on the List itself, causing a compile error. --- templates/java/src/main/java/io/appwrite/models/Model.java.twig | 2 +- .../src/main/java/io/appwrite/models/RequestModel.java.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 67df256c0e..370c9062e3 100644 --- a/templates/java/src/main/java/io/appwrite/models/Model.java.twig +++ b/templates/java/src/main/java/io/appwrite/models/Model.java.twig @@ -98,7 +98,7 @@ public class {{ definition.name | caseUcfirst }}{% if definition.additionalPrope 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 %}{{ property.name | caseCamel | removeDollarSign }} != null ? {{ property.name | caseCamel | removeDollarSign }}.getValue() : null{% else %}{{ property.name | caseCamel | removeDollarSign }}{% endif %}); + ((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); 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 index 3e6bdb1fa7..6a93a6dc68 100644 --- a/templates/java/src/main/java/io/appwrite/models/RequestModel.java.twig +++ b/templates/java/src/main/java/io/appwrite/models/RequestModel.java.twig @@ -37,7 +37,7 @@ public class {{ requestModel.name | caseUcfirst }} { 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 %}{{ property.name | caseCamel | removeDollarSign }} != null ? {{ property.name | caseCamel | removeDollarSign }}.getValue() : null{% else %}{{ property.name | caseCamel | removeDollarSign }}{% endif %}); + ((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; } From de354d93a3882e4700cb41c8863436be95d22d85 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 12:34:33 +0530 Subject: [PATCH 14/22] fix(java): add missing comma after last property in additionalProperties constructor When a model has additionalProperties (generic T data param), the last regular property was missing its trailing comma, causing a compile error for all generic response models like Document. --- templates/java/src/main/java/io/appwrite/models/Model.java.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 370c9062e3..fab555ee80 100644 --- a/templates/java/src/main/java/io/appwrite/models/Model.java.twig +++ b/templates/java/src/main/java/io/appwrite/models/Model.java.twig @@ -23,7 +23,7 @@ public class {{ definition.name | caseUcfirst }}{% if definition.additionalPrope {% endif %} public {{ definition.name | caseUcfirst }}( {% for property in definition.properties %} - {{ property | typeName }} {{ property.name | caseCamel | removeDollarSign }}{% if not loop.last %},{% endif %} + {{ property | typeName }} {{ property.name | caseCamel | removeDollarSign }}{% if not loop.last or definition.additionalProperties %},{% endif %} {% endfor %} {% if definition.additionalProperties %} From e5cc1e76458a68386ce857e917b35802d261d216 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 13:13:28 +0530 Subject: [PATCH 15/22] fix(java): pack distance and meters into values array for spatial distance queries Remove top-level Gson-annotated distance/meters fields. Instead append them to the values list via withDistance() helpers so the serialized JSON matches the expected [[coords],distance,meters] structure that QUERY_HELPER_RESPONSES asserts. --- .../src/main/java/io/appwrite/Query.java.twig | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/templates/java/src/main/java/io/appwrite/Query.java.twig b/templates/java/src/main/java/io/appwrite/Query.java.twig index 5f18290317..be7e5b268a 100644 --- a/templates/java/src/main/java/io/appwrite/Query.java.twig +++ b/templates/java/src/main/java/io/appwrite/Query.java.twig @@ -19,26 +19,27 @@ public class Query { @SerializedName("values") private final List values; - @SerializedName("distance") - private final Number distance; - - @SerializedName("meters") - private final Boolean meters; - - private Query(String method, String attribute, List values, Number distance, Boolean meters) { + public Query(String method, String attribute, List values) { this.method = method; this.attribute = attribute; this.values = values; - this.distance = distance; - this.meters = meters; } - public Query(String method, String attribute, List values) { - this(method, attribute, values, null, null); + public Query(String method, String attribute) { this(method, attribute, null); } + public Query(String method) { this(method, null, null); } + + private static List withDistance(List values, Number distance) { + List l = new ArrayList(values); + l.add(distance); + return l; } - public Query(String method, String attribute) { this(method, attribute, null, null, null); } - public Query(String method) { this(method, null, null, null, null); } + private static List withDistance(List values, Number distance, boolean meters) { + List l = new ArrayList(values); + l.add(distance); + l.add(meters); + return l; + } @Override public String toString() { return gson.toJson(this); } @@ -84,14 +85,14 @@ public class Query { public static String and(List queries) { return new Query("and", null, new ArrayList(queries)).toString(); } public static String elemMatch(String attribute, List queries) { return new Query("elemMatch", attribute, new ArrayList(queries)).toString(); } - public static String distanceEqual(String attribute, List values, Number distance) { return new Query("distanceEqual", attribute, new ArrayList(values), distance, null).toString(); } - public static String distanceEqual(String attribute, List values, Number distance, boolean meters) { return new Query("distanceEqual", attribute, new ArrayList(values), distance, meters).toString(); } - public static String distanceNotEqual(String attribute, List values, Number distance) { return new Query("distanceNotEqual", attribute, new ArrayList(values), distance, null).toString(); } - public static String distanceNotEqual(String attribute, List values, Number distance, boolean meters) { return new Query("distanceNotEqual", attribute, new ArrayList(values), distance, meters).toString(); } - public static String distanceGreaterThan(String attribute, List values, Number distance) { return new Query("distanceGreaterThan", attribute, new ArrayList(values), distance, null).toString(); } - public static String distanceGreaterThan(String attribute, List values, Number distance, boolean meters) { return new Query("distanceGreaterThan", attribute, new ArrayList(values), distance, meters).toString(); } - public static String distanceLessThan(String attribute, List values, Number distance) { return new Query("distanceLessThan", attribute, new ArrayList(values), distance, null).toString(); } - public static String distanceLessThan(String attribute, List values, Number distance, boolean meters) { return new Query("distanceLessThan", attribute, new ArrayList(values), distance, meters).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, new ArrayList(values)).toString(); } public static String notIntersects(String attribute, List values) { return new Query("notIntersects", attribute, new ArrayList(values)).toString(); } From 51767f4b7c13835d6ba4c388c2caa447be5d66e7 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 13:24:43 +0530 Subject: [PATCH 16/22] fix(java): use chunkLen for Content-Range end and offset advance CHUNK_SIZE and chunkLen diverge on the final chunk. Use chunkLen so Content-Range header reflects actual bytes sent and offset advances by the exact number of bytes read, not a fixed block size. --- templates/java/src/main/java/io/appwrite/Client.java.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/java/src/main/java/io/appwrite/Client.java.twig b/templates/java/src/main/java/io/appwrite/Client.java.twig index 8e4f6ea0ce..096154492c 100644 --- a/templates/java/src/main/java/io/appwrite/Client.java.twig +++ b/templates/java/src/main/java/io/appwrite/Client.java.twig @@ -309,14 +309,14 @@ public class Client { file.readFully(chunk); } - long end = Math.min(offset + CHUNK_SIZE - 1, size - 1); + 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 += CHUNK_SIZE; + offset += chunkLen; headers.put("x-{{ spec.title | caseLower }}-id", result.get("$id").toString()); if (onProgress != null) { From 004eab1dc5c0cc8e7151fd3128e63e6630e6d8ff Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 14:02:00 +0530 Subject: [PATCH 17/22] fix(java): remove unreachable checked-exception catch and guard resume GET MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove catch(AppwriteException e) from call() — AppwriteException is only ever wrapped in RuntimeException inside the try body, so the catch clause was an unreachable checked-exception compile error - Wrap the chunkedUpload resume GET in try-catch so a 404 on brand-new uploads is silently ignored and the upload starts from offset 0 --- .../src/main/java/io/appwrite/Client.java.twig | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/templates/java/src/main/java/io/appwrite/Client.java.twig b/templates/java/src/main/java/io/appwrite/Client.java.twig index 096154492c..a9b1170cc9 100644 --- a/templates/java/src/main/java/io/appwrite/Client.java.twig +++ b/templates/java/src/main/java/io/appwrite/Client.java.twig @@ -215,8 +215,6 @@ public class Client { Map map = gson.fromJson(body, mapType); if (converter != null) return converter.apply(map); return (T) map; - } catch ({{ spec.title | caseUcfirst }}Exception e) { - throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } @@ -289,11 +287,15 @@ public class Client { Map result = null; if (idParamName != null && !idParamName.isEmpty()) { - 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); + 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 } } From 3cd44d94e8e5483e97f8d1537ce0e1c4b0808839 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 15:39:27 +0530 Subject: [PATCH 18/22] fix(java): deserialize query strings before nesting in or/and/elemMatch Storing raw JSON strings in the values list causes Gson to re-serialize them as escaped strings. Parse each query string back to Object via Gson first so they serialize as nested JSON objects. --- .../java/src/main/java/io/appwrite/Query.java.twig | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/templates/java/src/main/java/io/appwrite/Query.java.twig b/templates/java/src/main/java/io/appwrite/Query.java.twig index be7e5b268a..6ab19075f3 100644 --- a/templates/java/src/main/java/io/appwrite/Query.java.twig +++ b/templates/java/src/main/java/io/appwrite/Query.java.twig @@ -81,9 +81,9 @@ public class Query { 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, new ArrayList(queries)).toString(); } - public static String and(List queries) { return new Query("and", null, new ArrayList(queries)).toString(); } - public static String elemMatch(String attribute, List queries) { return new Query("elemMatch", attribute, new ArrayList(queries)).toString(); } + 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(); } @@ -103,6 +103,12 @@ public class Query { public static String touches(String attribute, List values) { return new Query("touches", attribute, new ArrayList(values)).toString(); } public static String notTouches(String attribute, List values) { return new Query("notTouches", attribute, new ArrayList(values)).toString(); } + private static List parseQueries(List queries) { + List l = new ArrayList<>(); + for (String q : queries) l.add(gson.fromJson(q, Object.class)); + return l; + } + @SuppressWarnings("unchecked") private static List parseValue(Object value) { if (value instanceof List) return (List) value; From 8f6cef7e9580484c3237b89131a854e110b00328 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 15:51:50 +0530 Subject: [PATCH 19/22] fix(java): use JsonParser.parseString in parseQueries to preserve key order gson.fromJson returns a LinkedTreeMap with unpredictable key ordering, causing re-serialized nested queries to mismatch expected output. JsonParser.parseString returns a JsonElement that Gson serializes verbatim, preserving the original key order. --- templates/java/src/main/java/io/appwrite/Query.java.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/java/src/main/java/io/appwrite/Query.java.twig b/templates/java/src/main/java/io/appwrite/Query.java.twig index 6ab19075f3..afcd21f3bc 100644 --- a/templates/java/src/main/java/io/appwrite/Query.java.twig +++ b/templates/java/src/main/java/io/appwrite/Query.java.twig @@ -105,7 +105,7 @@ public class Query { private static List parseQueries(List queries) { List l = new ArrayList<>(); - for (String q : queries) l.add(gson.fromJson(q, Object.class)); + for (String q : queries) l.add(com.google.gson.JsonParser.parseString(q)); return l; } From 5240561e9ba4ecfa5629c87601e55f4dfae81ba6 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sat, 25 Apr 2026 16:19:15 +0530 Subject: [PATCH 20/22] fix(java): wrap coordinate list as nested element in spatial predicates intersects/notIntersects/crosses/notCrosses/overlaps/notOverlaps/ touches/notTouches were storing flat coords [lat,lon] but expected output is [[lat,lon]]. Use wrapCoords() to nest the input list as a single values element. --- .../src/main/java/io/appwrite/Query.java.twig | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/templates/java/src/main/java/io/appwrite/Query.java.twig b/templates/java/src/main/java/io/appwrite/Query.java.twig index afcd21f3bc..41bd8afdbc 100644 --- a/templates/java/src/main/java/io/appwrite/Query.java.twig +++ b/templates/java/src/main/java/io/appwrite/Query.java.twig @@ -28,6 +28,12 @@ public class Query { 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 l = new ArrayList(values); l.add(distance); @@ -94,14 +100,14 @@ public class Query { 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, new ArrayList(values)).toString(); } - public static String notIntersects(String attribute, List values) { return new Query("notIntersects", attribute, new ArrayList(values)).toString(); } - public static String crosses(String attribute, List values) { return new Query("crosses", attribute, new ArrayList(values)).toString(); } - public static String notCrosses(String attribute, List values) { return new Query("notCrosses", attribute, new ArrayList(values)).toString(); } - public static String overlaps(String attribute, List values) { return new Query("overlaps", attribute, new ArrayList(values)).toString(); } - public static String notOverlaps(String attribute, List values) { return new Query("notOverlaps", attribute, new ArrayList(values)).toString(); } - public static String touches(String attribute, List values) { return new Query("touches", attribute, new ArrayList(values)).toString(); } - public static String notTouches(String attribute, List values) { return new Query("notTouches", attribute, new ArrayList(values)).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<>(); From c64f9f8ab291277794f681d1c1a37a26703b3a53 Mon Sep 17 00:00:00 2001 From: Gowri Date: Sun, 26 Apr 2026 08:33:14 +0530 Subject: [PATCH 21/22] fix(java): wrap [coords,distance,meters] as single values element in withDistance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous format spread coords+distance+meters as flat values elements. Expected format is values:[[[coords],distance,meters]] — the entire triplet as one nested element. Build an inner list [coords,distance,meters] and wrap it in an outer single-element list. --- .../src/main/java/io/appwrite/Query.java.twig | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/templates/java/src/main/java/io/appwrite/Query.java.twig b/templates/java/src/main/java/io/appwrite/Query.java.twig index 41bd8afdbc..adf7f60da0 100644 --- a/templates/java/src/main/java/io/appwrite/Query.java.twig +++ b/templates/java/src/main/java/io/appwrite/Query.java.twig @@ -35,16 +35,22 @@ public class Query { } private static List withDistance(List values, Number distance) { - List l = new ArrayList(values); - l.add(distance); - return l; + 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 l = new ArrayList(values); - l.add(distance); - l.add(meters); - return l; + 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 From 7215943038c3ac7ff88c3813424cebfc225d767c Mon Sep 17 00:00:00 2001 From: Gowri Date: Sun, 26 Apr 2026 08:52:55 +0530 Subject: [PATCH 22/22] fix(java): use wall-clock nanoseconds for ID microsecond field System.nanoTime() is a monotonic clock unrelated to wall time, giving wrong sub-second values. now.getNano()/1000 gives 0-999999 microseconds from the wall clock, correctly filling the 5-hex-digit field and preserving time-sortability of generated IDs. --- templates/java/src/main/java/io/appwrite/ID.java.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/java/src/main/java/io/appwrite/ID.java.twig b/templates/java/src/main/java/io/appwrite/ID.java.twig index d09d649aa4..f0b8eaac51 100644 --- a/templates/java/src/main/java/io/appwrite/ID.java.twig +++ b/templates/java/src/main/java/io/appwrite/ID.java.twig @@ -8,7 +8,7 @@ public class ID { private static String hexTimestamp() { Instant now = Instant.now(); long sec = now.getEpochSecond(); - long usec = (System.nanoTime() / 1000) % 1000; + long usec = now.getNano() / 1000; return String.format("%08x%05x", sec, usec); }