ex172000 commented on code in PR #2630: URL: https://github.com/apache/iggy/pull/2630#discussion_r2740124283
########## foreign/java/java-sdk/src/main/java/org/apache/iggy/IggyVersion.java: ########## @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iggy; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * Provides version information for the Iggy Java SDK. + * + * <p>Version information is read from a properties file generated at build time. + */ +public final class IggyVersion { + + private static final String PROPERTIES_FILE = "/iggy-version.properties"; + private static final String UNKNOWN = "unknown"; + + private static final IggyVersion INSTANCE; + + static { + String version = UNKNOWN; + String buildTime = UNKNOWN; + String gitCommit = UNKNOWN; + + try (InputStream is = IggyVersion.class.getResourceAsStream(PROPERTIES_FILE)) { + if (is != null) { + Properties props = new Properties(); + props.load(is); + version = props.getProperty("version", UNKNOWN); + buildTime = props.getProperty("buildTime", UNKNOWN); + gitCommit = props.getProperty("gitCommit", UNKNOWN); + } + } catch (IOException e) { + // Use default values Review Comment: Any logging needed? ########## foreign/java/java-sdk/src/main/java/org/apache/iggy/Iggy.java: ########## @@ -19,13 +19,135 @@ package org.apache.iggy; -import org.apache.iggy.client.blocking.IggyClientBuilder; +import org.apache.iggy.builder.HttpClientBuilder; +import org.apache.iggy.builder.TcpClientBuilder; +import org.apache.iggy.client.blocking.http.IggyHttpClient; +import org.apache.iggy.client.blocking.tcp.IggyTcpClient; +/** + * Main entry point for creating Iggy clients. + * + * <p>Iggy provides a fluent API for creating clients with protocol-first design: + * + * <h2>TCP Clients (recommended for performance)</h2> + * <pre>{@code + * // Blocking TCP client + * var client = Iggy.tcp().blocking() + * .host("localhost") + * .port(8090) + * .build(); + * client.connect(); + * client.users().login("iggy", "iggy"); + * + * // Async TCP client + * var asyncClient = Iggy.tcp().async() + * .host("localhost") + * .build(); + * asyncClient.connect().join(); + * asyncClient.users().login("iggy", "iggy").join(); + * }</pre> + * + * <h2>HTTP Clients</h2> + * <pre>{@code + * var httpClient = Iggy.http().blocking() + * .url("http://localhost:3000") + * .build(); + * + * // Login after creating the client + * httpClient.users().login("iggy", "iggy"); + * }</pre> + * + * <h2>Quick Factory Methods</h2> + * <pre>{@code + * // Local TCP client (localhost:8090) + * var client = Iggy.localTcp(); + * + * // Local HTTP client (localhost:3000) + * var httpClient = Iggy.localHttp(); + * }</pre> + * + * <h2>Version Information</h2> + * <pre>{@code + * String version = Iggy.version(); // e.g., "1.0.0" + * IggyVersion info = Iggy.versionInfo(); // Full version details + * }</pre> + * + * @see org.apache.iggy.builder.TcpClientBuilder + * @see org.apache.iggy.builder.HttpClientBuilder + * @see IggyVersion + */ public final class Iggy { + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_TCP_PORT = 8090; + private static final int DEFAULT_HTTP_PORT = 3000; + private Iggy() {} - public static IggyClientBuilder clientBuilder() { - return new IggyClientBuilder(); + /** + * Creates a builder for TCP clients. + * + * <p>TCP provides the best performance and is recommended for most use cases. + * + * @return a TCP client builder + */ + public static TcpClientBuilder tcp() { Review Comment: Not sure if this is too brevity, do we consider a more intuitive naming like `tcpBuilder()` or `tcpClientBuilder()`, or name the class to be `IggyClientBuilder` and then have it `tcp()` ? ########## foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyServerException.java: ########## @@ -0,0 +1,255 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iggy.exception; + +import org.apache.commons.lang3.StringUtils; + +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; + +/** + * Exception thrown when the server returns an error response. + * + * <p>This exception carries the error code, reason, and optional field information + * from the server response. The factory methods automatically map error codes to + * more specific exception subclasses where appropriate. + */ +public class IggyServerException extends IggyException { + + private static final Set<IggyErrorCode> NOT_FOUND_CODES = EnumSet.of( + IggyErrorCode.RESOURCE_NOT_FOUND, + IggyErrorCode.CANNOT_LOAD_RESOURCE, + IggyErrorCode.STREAM_ID_NOT_FOUND, + IggyErrorCode.STREAM_NAME_NOT_FOUND, + IggyErrorCode.TOPIC_ID_NOT_FOUND, + IggyErrorCode.TOPIC_NAME_NOT_FOUND, + IggyErrorCode.PARTITION_NOT_FOUND, + IggyErrorCode.SEGMENT_NOT_FOUND, + IggyErrorCode.CLIENT_NOT_FOUND, + IggyErrorCode.CONSUMER_GROUP_ID_NOT_FOUND, + IggyErrorCode.CONSUMER_GROUP_NAME_NOT_FOUND, + IggyErrorCode.CONSUMER_GROUP_NOT_JOINED, + IggyErrorCode.MESSAGE_NOT_FOUND); + + private static final Set<IggyErrorCode> AUTHENTICATION_CODES = EnumSet.of( + IggyErrorCode.UNAUTHENTICATED, + IggyErrorCode.INVALID_CREDENTIALS, + IggyErrorCode.INVALID_USERNAME, + IggyErrorCode.INVALID_PASSWORD, + IggyErrorCode.INVALID_PAT_TOKEN, + IggyErrorCode.PASSWORD_DOES_NOT_MATCH, + IggyErrorCode.PASSWORD_HASH_INTERNAL_ERROR); + + private static final Set<IggyErrorCode> AUTHORIZATION_CODES = EnumSet.of(IggyErrorCode.UNAUTHORIZED); + + private static final Set<IggyErrorCode> CONFLICT_CODES = EnumSet.of( + IggyErrorCode.USER_ALREADY_EXISTS, + IggyErrorCode.CLIENT_ALREADY_EXISTS, + IggyErrorCode.STREAM_ALREADY_EXISTS, + IggyErrorCode.TOPIC_ALREADY_EXISTS, + IggyErrorCode.CONSUMER_GROUP_ALREADY_EXISTS, + IggyErrorCode.PAT_NAME_ALREADY_EXISTS); + + private static final Set<IggyErrorCode> VALIDATION_CODES = EnumSet.of( + IggyErrorCode.INVALID_COMMAND, + IggyErrorCode.INVALID_FORMAT, + IggyErrorCode.FEATURE_UNAVAILABLE, + IggyErrorCode.CANNOT_PARSE_INT, + IggyErrorCode.CANNOT_PARSE_SLICE, + IggyErrorCode.CANNOT_PARSE_UTF8, + IggyErrorCode.INVALID_STREAM_NAME, + IggyErrorCode.CANNOT_CREATE_STREAM_DIRECTORY, + IggyErrorCode.INVALID_TOPIC_NAME, + IggyErrorCode.INVALID_REPLICATION_FACTOR, + IggyErrorCode.CANNOT_CREATE_TOPIC_DIRECTORY, + IggyErrorCode.CONSUMER_GROUP_MEMBER_NOT_FOUND, + IggyErrorCode.INVALID_CONSUMER_GROUP_NAME, + IggyErrorCode.TOO_MANY_MESSAGES, + IggyErrorCode.EMPTY_MESSAGES, + IggyErrorCode.TOO_BIG_MESSAGE, + IggyErrorCode.INVALID_MESSAGE_CHECKSUM); + + private final IggyErrorCode errorCode; + private final int rawErrorCode; + private final String reason; + private final Optional<String> field; + private final Optional<String> errorId; + + /** + * Constructs a new IggyServerException. + * + * @param errorCode the error code enum + * @param rawErrorCode the raw numeric error code from the server + * @param reason the error reason/message + * @param field the optional field related to the error + * @param errorId the optional error ID for correlation with server logs + */ + public IggyServerException( + IggyErrorCode errorCode, + int rawErrorCode, + String reason, + Optional<String> field, + Optional<String> errorId) { + super(buildMessage(errorCode, rawErrorCode, reason, field, errorId)); + this.errorCode = errorCode; + this.rawErrorCode = rawErrorCode; + this.reason = reason; + this.field = field; + this.errorId = errorId; + } + + /** + * Constructs a new IggyServerException with just a status code. + * + * @param rawErrorCode the raw numeric error code from the server + */ + public IggyServerException(int rawErrorCode) { + this(IggyErrorCode.fromCode(rawErrorCode), rawErrorCode, "Server error", Optional.empty(), Optional.empty()); + } + + /** + * Returns the error code enum. + * + * @return the error code + */ + public IggyErrorCode getErrorCode() { + return errorCode; + } + + /** + * Returns the raw numeric error code from the server. + * + * @return the raw error code + */ + public int getRawErrorCode() { + return rawErrorCode; + } + + /** + * Returns the error reason/message. + * + * @return the reason + */ + public String getReason() { + return reason; + } + + /** + * Returns the optional field related to the error. + * + * @return the field, if present + */ + public Optional<String> getField() { + return field; + } + + /** + * Returns the optional error ID for correlation with server logs. + * + * <p>This ID is only available for HTTP responses and can be used to find + * the corresponding error in server logs. + * + * @return the error ID, if present + */ + public Optional<String> getErrorId() { + return errorId; + } + + /** + * Creates an appropriate exception from a TCP response. + * + * @param status the status code from the TCP response + * @param payload the error payload bytes (may contain error message) + * @return an appropriate IggyServerException subclass + */ + public static IggyServerException fromTcpResponse(long status, byte[] payload) { + int errorCode = (int) status; + String reason = + payload != null && payload.length > 0 ? new String(payload, StandardCharsets.UTF_8) : "Server error"; + return createFromCode(errorCode, reason, Optional.empty(), Optional.empty()); + } + + /** + * Creates an appropriate exception from an HTTP response. + * + * @param id the error ID for correlation with server logs + * @param code the error code string + * @param reason the error reason + * @param field the optional field related to the error + * @return an appropriate IggyServerException subclass + */ + public static IggyServerException fromHttpResponse(String id, String code, String reason, String field) { + IggyErrorCode errorCode = IggyErrorCode.fromString(code); + int rawCode = errorCode.getCode(); + if (rawCode == -1) { + // Try parsing code as integer + try { + rawCode = Integer.parseInt(code); + } catch (NumberFormatException e) { + rawCode = -1; + } + } + Optional<String> fieldOpt = Optional.ofNullable(StringUtils.stripToNull(field)); + Optional<String> errorIdOpt = Optional.ofNullable(StringUtils.stripToNull(id)); + return createFromCode(rawCode, reason != null ? reason : "Server error", fieldOpt, errorIdOpt); Review Comment: `"Server error"` value is the default value, is it possible we just pass `reason` as is here, even if it's null ? ########## foreign/java/java-sdk/src/main/java/org/apache/iggy/config/RetryPolicy.java: ########## @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iggy.config; + +import java.time.Duration; + +/** + * Retry policy for client operations. + * + * <p>This class provides configuration for retry behavior including exponential backoff, + * fixed delay, and no-retry strategies. + */ +public final class RetryPolicy { Review Comment: Is it possible to use Failsafe instead? ########## foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/InternalHttpClient.java: ########## @@ -19,34 +19,98 @@ package org.apache.iggy.client.blocking.http; +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; -import org.apache.iggy.client.blocking.http.error.IggyHttpError; -import org.apache.iggy.client.blocking.http.error.IggyHttpException; +import org.apache.hc.core5.ssl.SSLContextBuilder; +import org.apache.hc.core5.util.Timeout; +import org.apache.iggy.exception.IggyConnectionException; +import org.apache.iggy.exception.IggyInvalidArgumentException; +import org.apache.iggy.exception.IggyServerException; +import org.apache.iggy.exception.IggyTlsException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import tools.jackson.core.type.TypeReference; import tools.jackson.databind.JavaType; import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.ObjectNode; +import java.io.Closeable; +import java.io.File; import java.io.IOException; +import java.security.GeneralSecurityException; +import java.time.Duration; import java.util.Optional; -final class InternalHttpClient { +final class InternalHttpClient implements Closeable { private static final Logger log = LoggerFactory.getLogger(InternalHttpClient.class); private static final String AUTHORIZATION = "Authorization"; private final String url; private final ObjectMapper objectMapper = ObjectMapperFactory.getInstance(); + private final CloseableHttpClient httpClient; private Optional<String> token = Optional.empty(); - InternalHttpClient(String url) { + InternalHttpClient( + String url, + Optional<Duration> connectionTimeout, + Optional<Duration> requestTimeout, + Optional<File> tlsCertificate) { + validateUrl(url); this.url = url; + this.httpClient = createHttpClient(connectionTimeout, requestTimeout, tlsCertificate); + } + + private static void validateUrl(String url) { + if (StringUtils.isBlank(url)) { + throw new IggyInvalidArgumentException("URL cannot be null or empty"); + } + if (!url.startsWith("http://") && !url.startsWith("https://")) { + throw new IggyInvalidArgumentException("URL must start with http:// or https://"); + } + } + + private static CloseableHttpClient createHttpClient( + Optional<Duration> connectionTimeout, Optional<Duration> requestTimeout, Optional<File> tlsCertificate) { + var connectionConfigBuilder = ConnectionConfig.custom(); Review Comment: It seems we partially used `var` in the past, maybe we can keep it consistent across the repo and use a type notion instead? ########## foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyErrorCode.java: ########## @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iggy.exception; + +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * Error codes returned by the Iggy server. + * + * <p>These codes correspond to the error codes defined in the Iggy server's iggy_error.rs. + */ +public enum IggyErrorCode { + // General errors + ERROR(1), + INVALID_COMMAND(3), + INVALID_FORMAT(4), + FEATURE_UNAVAILABLE(6), + CANNOT_PARSE_INT(7), + CANNOT_PARSE_SLICE(8), + CANNOT_PARSE_UTF8(9), + + // Resource errors + RESOURCE_NOT_FOUND(20), + CANNOT_LOAD_RESOURCE(100), + + // Authentication/Authorization errors + UNAUTHENTICATED(40), + UNAUTHORIZED(41), + INVALID_CREDENTIALS(42), + INVALID_USERNAME(43), + INVALID_PASSWORD(44), + CLEAR_TEXT_PASSWORD_REQUIRED(45), + USER_ALREADY_EXISTS(46), + USER_INACTIVE(47), + CANNOT_DELETE_USER_WITH_ACTIVE_PAT(48), + CANNOT_UPDATE_OWN_PERMISSIONS(49), + CANNOT_DELETE_YOURSELF(50), + CLIENT_ALREADY_EXISTS(51), + CLIENT_NOT_FOUND(52), + INVALID_PAT_TOKEN(53), + PAT_NAME_ALREADY_EXISTS(54), + PASSWORD_DOES_NOT_MATCH(77), + PASSWORD_HASH_INTERNAL_ERROR(78), + + // Stream errors + STREAM_ID_NOT_FOUND(1009), + STREAM_NAME_NOT_FOUND(1010), + STREAM_ALREADY_EXISTS(1012), + INVALID_STREAM_NAME(1013), + CANNOT_CREATE_STREAM_DIRECTORY(1014), + + // Topic errors + TOPIC_ID_NOT_FOUND(2010), + TOPIC_NAME_NOT_FOUND(2011), + TOPICS_COUNT_EXCEEDED(2012), + TOPIC_ALREADY_EXISTS(2013), + INVALID_TOPIC_NAME(2014), + INVALID_REPLICATION_FACTOR(2015), + CANNOT_CREATE_TOPIC_DIRECTORY(2016), + + // Partition errors + PARTITION_NOT_FOUND(3007), + + // Consumer group errors + CONSUMER_GROUP_ID_NOT_FOUND(5000), + CONSUMER_GROUP_MEMBER_NOT_FOUND(5002), + CONSUMER_GROUP_NAME_NOT_FOUND(5003), + CONSUMER_GROUP_ALREADY_EXISTS(5004), + INVALID_CONSUMER_GROUP_NAME(5005), + CONSUMER_GROUP_NOT_JOINED(5006), + + // Segment errors + SEGMENT_NOT_FOUND(4000), + SEGMENT_CLOSED(4001), + CANNOT_READ_SEGMENT(4002), + CANNOT_SAVE_SEGMENT(4003), + + // Message errors + TOO_MANY_MESSAGES(7000), + EMPTY_MESSAGES(7001), + TOO_BIG_MESSAGE(7002), + INVALID_MESSAGE_CHECKSUM(7003), + MESSAGE_NOT_FOUND(7004), + + // Unknown error code + UNKNOWN(-1); + + private static final Map<Integer, IggyErrorCode> CODE_MAP = new HashMap<>(); + + static { + for (IggyErrorCode errorCode : values()) { + CODE_MAP.put(errorCode.code, errorCode); + } + } + + private final int code; + + IggyErrorCode(int code) { + this.code = code; + } + + /** + * Returns the numeric error code. + * + * @return the error code + */ + public int getCode() { + return code; + } + + /** + * Returns the IggyErrorCode for the given numeric code. + * + * @param code the numeric error code + * @return the corresponding IggyErrorCode, or UNKNOWN if not found + */ + public static IggyErrorCode fromCode(int code) { + return CODE_MAP.getOrDefault(code, UNKNOWN); + } + + /** + * Returns the IggyErrorCode for the given string code. + * + * @param code the string error code (can be numeric or enum name) + * @return the corresponding IggyErrorCode, or UNKNOWN if not found + */ + public static IggyErrorCode fromString(String code) { Review Comment: Would love to see this part tested too! 😄 ########## foreign/java/java-sdk/src/main/java/org/apache/iggy/Iggy.java: ########## @@ -19,13 +19,135 @@ package org.apache.iggy; -import org.apache.iggy.client.blocking.IggyClientBuilder; +import org.apache.iggy.builder.HttpClientBuilder; +import org.apache.iggy.builder.TcpClientBuilder; +import org.apache.iggy.client.blocking.http.IggyHttpClient; +import org.apache.iggy.client.blocking.tcp.IggyTcpClient; +/** + * Main entry point for creating Iggy clients. + * + * <p>Iggy provides a fluent API for creating clients with protocol-first design: + * + * <h2>TCP Clients (recommended for performance)</h2> + * <pre>{@code + * // Blocking TCP client + * var client = Iggy.tcp().blocking() + * .host("localhost") + * .port(8090) + * .build(); + * client.connect(); + * client.users().login("iggy", "iggy"); + * + * // Async TCP client + * var asyncClient = Iggy.tcp().async() + * .host("localhost") + * .build(); + * asyncClient.connect().join(); + * asyncClient.users().login("iggy", "iggy").join(); + * }</pre> + * + * <h2>HTTP Clients</h2> + * <pre>{@code + * var httpClient = Iggy.http().blocking() + * .url("http://localhost:3000") + * .build(); + * + * // Login after creating the client + * httpClient.users().login("iggy", "iggy"); + * }</pre> + * + * <h2>Quick Factory Methods</h2> + * <pre>{@code + * // Local TCP client (localhost:8090) + * var client = Iggy.localTcp(); + * + * // Local HTTP client (localhost:3000) + * var httpClient = Iggy.localHttp(); + * }</pre> + * + * <h2>Version Information</h2> + * <pre>{@code + * String version = Iggy.version(); // e.g., "1.0.0" + * IggyVersion info = Iggy.versionInfo(); // Full version details + * }</pre> + * + * @see org.apache.iggy.builder.TcpClientBuilder + * @see org.apache.iggy.builder.HttpClientBuilder + * @see IggyVersion + */ public final class Iggy { + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_TCP_PORT = 8090; + private static final int DEFAULT_HTTP_PORT = 3000; + private Iggy() {} - public static IggyClientBuilder clientBuilder() { - return new IggyClientBuilder(); + /** + * Creates a builder for TCP clients. + * + * <p>TCP provides the best performance and is recommended for most use cases. + * + * @return a TCP client builder + */ + public static TcpClientBuilder tcp() { + return new TcpClientBuilder(); + } + + /** + * Creates a builder for HTTP clients. + * + * <p>HTTP is useful when TCP is blocked by firewalls or when HTTP semantics are preferred. + * + * @return an HTTP client builder + */ + public static HttpClientBuilder http() { + return new HttpClientBuilder(); + } + + /** + * Creates a local blocking TCP client connected to localhost:8090. + * + * <p>This is a convenience method for local development and testing. + * Call {@code client.users().login(username, password)} to authenticate. + * + * @return a connected IggyTcpClient + */ + public static IggyTcpClient localTcp() { + IggyTcpClient client = + tcp().blocking().host(DEFAULT_HOST).port(DEFAULT_TCP_PORT).build(); Review Comment: What happens if people want async tcp local client? I feel maybe we shouldn't expose this special API since we already allow users to set the host and port? ########## foreign/java/java-sdk/src/main/java/org/apache/iggy/IggyVersion.java: ########## @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iggy; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * Provides version information for the Iggy Java SDK. + * + * <p>Version information is read from a properties file generated at build time. + */ +public final class IggyVersion { + + private static final String PROPERTIES_FILE = "/iggy-version.properties"; + private static final String UNKNOWN = "unknown"; + + private static final IggyVersion INSTANCE; + + static { + String version = UNKNOWN; + String buildTime = UNKNOWN; + String gitCommit = UNKNOWN; + + try (InputStream is = IggyVersion.class.getResourceAsStream(PROPERTIES_FILE)) { + if (is != null) { + Properties props = new Properties(); + props.load(is); + version = props.getProperty("version", UNKNOWN); + buildTime = props.getProperty("buildTime", UNKNOWN); + gitCommit = props.getProperty("gitCommit", UNKNOWN); + } + } catch (IOException e) { + // Use default values + } + + INSTANCE = new IggyVersion(version, buildTime, gitCommit); + } + + private final String version; + private final String buildTime; + private final String gitCommit; + + private IggyVersion(String version, String buildTime, String gitCommit) { + this.version = version; + this.buildTime = buildTime; + this.gitCommit = gitCommit; + } + + /** + * Gets the singleton IggyVersion instance. + * + * @return the version information instance + */ + public static IggyVersion getInstance() { + return INSTANCE; + } + + /** + * Gets the SDK version string. + * + * @return the version string (e.g., "1.0.0") + */ + public String getVersion() { + return version; + } + + /** + * Gets the build timestamp. + * + * @return the build time as ISO-8601 string, or "unknown" if not available + */ + public String getBuildTime() { + return buildTime; + } + + /** + * Gets the Git commit hash. + * + * @return the short Git commit hash, or "unknown" if not available + */ + public String getGitCommit() { + return gitCommit; + } + + /** + * Gets a User-Agent string suitable for HTTP requests. + * + * @return the User-Agent string (e.g., "iggy-java-sdk/1.0.0") + */ + public String getUserAgent() { + return "iggy-java-sdk/" + version; Review Comment: Looks like we didn't add test for this file. Do we want to assert the string here, as well as the `toString()` below on L119? ########## foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/InternalHttpClient.java: ########## @@ -19,34 +19,98 @@ package org.apache.iggy.client.blocking.http; +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; -import org.apache.iggy.client.blocking.http.error.IggyHttpError; -import org.apache.iggy.client.blocking.http.error.IggyHttpException; +import org.apache.hc.core5.ssl.SSLContextBuilder; +import org.apache.hc.core5.util.Timeout; +import org.apache.iggy.exception.IggyConnectionException; +import org.apache.iggy.exception.IggyInvalidArgumentException; +import org.apache.iggy.exception.IggyServerException; +import org.apache.iggy.exception.IggyTlsException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import tools.jackson.core.type.TypeReference; import tools.jackson.databind.JavaType; import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.ObjectNode; +import java.io.Closeable; +import java.io.File; import java.io.IOException; +import java.security.GeneralSecurityException; +import java.time.Duration; import java.util.Optional; -final class InternalHttpClient { +final class InternalHttpClient implements Closeable { private static final Logger log = LoggerFactory.getLogger(InternalHttpClient.class); private static final String AUTHORIZATION = "Authorization"; private final String url; private final ObjectMapper objectMapper = ObjectMapperFactory.getInstance(); + private final CloseableHttpClient httpClient; private Optional<String> token = Optional.empty(); - InternalHttpClient(String url) { + InternalHttpClient( + String url, + Optional<Duration> connectionTimeout, + Optional<Duration> requestTimeout, + Optional<File> tlsCertificate) { + validateUrl(url); this.url = url; + this.httpClient = createHttpClient(connectionTimeout, requestTimeout, tlsCertificate); + } + + private static void validateUrl(String url) { + if (StringUtils.isBlank(url)) { + throw new IggyInvalidArgumentException("URL cannot be null or empty"); + } + if (!url.startsWith("http://") && !url.startsWith("https://")) { Review Comment: Do we want to parse it into URL/URI object to be certain? ########## foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/IggyHttpClientBuilder.java: ########## @@ -0,0 +1,234 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iggy.client.blocking.http; + +import org.apache.commons.lang3.StringUtils; +import org.apache.iggy.exception.IggyInvalidArgumentException; +import org.apache.iggy.exception.IggyMissingCredentialsException; + +import java.io.File; +import java.time.Duration; + +/** + * Builder for creating configured IggyHttpClient instances. + * + * <p>Example usage: + * <pre>{@code + * // Basic usage with explicit login + * var client = IggyHttpClient.builder() + * .url("http://localhost:3000") + * .build(); + * client.users().login("iggy", "iggy"); + * + * // Using host/port instead of full URL + * var client = IggyHttpClient.builder() + * .host("localhost") + * .port(3000) + * .build(); + * client.users().login("iggy", "iggy"); + * + * // Convenience method with auto-login + * var client = IggyHttpClient.builder() + * .host("localhost") + * .port(3000) + * .credentials("iggy", "iggy") + * .buildAndLogin(); + * + * // HTTPS with auto-login + * var client = IggyHttpClient.builder() + * .host("iggy-server.example.com") + * .port(443) + * .enableTls() + * .credentials("admin", "secret") + * .buildAndLogin(); + * }</pre> + * + * @see IggyHttpClient#builder() + */ +public final class IggyHttpClientBuilder { + private String url; + private String host = "localhost"; + private Integer port = IggyHttpClient.DEFAULT_HTTP_PORT; + private boolean enableTls = false; + private File tlsCertificate; + private Duration connectionTimeout; + private Duration requestTimeout; + private String username; + private String password; + + IggyHttpClientBuilder() {} + + /** + * Sets the full URL for the Iggy server. + * If set, this takes precedence over host/port/tls settings. + * + * @param url the full URL (e.g., "http://localhost:3000") + * @return this builder + */ + public IggyHttpClientBuilder url(String url) { + this.url = url; + return this; + } + + /** + * Sets the host address for the Iggy server. + * + * @param host the host address + * @return this builder + */ + public IggyHttpClientBuilder host(String host) { + this.host = host; + return this; + } + + /** + * Sets the port for the Iggy server. + * + * @param port the port number + * @return this builder + */ + public IggyHttpClientBuilder port(Integer port) { + this.port = port; + return this; + } + + /** + * Enables TLS for the HTTP connection. + * + * @return this builder + */ + public IggyHttpClientBuilder enableTls() { + this.enableTls = true; + return this; + } + + /** + * Enables or disables TLS for the HTTP connection. + * + * @param enableTls whether to enable TLS + * @return this builder + */ + public IggyHttpClientBuilder tls(boolean enableTls) { + this.enableTls = enableTls; + return this; + } + + /** + * Sets a custom trusted certificate (PEM file) to validate the server certificate. + * + * @param tlsCertificate the PEM file containing the certificate or CA chain + * @return this builder + */ + public IggyHttpClientBuilder tlsCertificate(File tlsCertificate) { + this.tlsCertificate = tlsCertificate; + return this; + } + + /** + * Sets a custom trusted certificate (PEM file path) to validate the server certificate. + * + * @param tlsCertificatePath the PEM file path containing the certificate or CA chain + * @return this builder + */ + public IggyHttpClientBuilder tlsCertificate(String tlsCertificatePath) { + this.tlsCertificate = StringUtils.isBlank(tlsCertificatePath) ? null : new File(tlsCertificatePath); + return this; + } + + /** + * Sets the connection timeout. + * + * @param connectionTimeout the connection timeout duration + * @return this builder + */ + public IggyHttpClientBuilder connectionTimeout(Duration connectionTimeout) { + this.connectionTimeout = connectionTimeout; + return this; + } + + /** + * Sets the request timeout. + * + * @param requestTimeout the request timeout duration + * @return this builder + */ + public IggyHttpClientBuilder requestTimeout(Duration requestTimeout) { + this.requestTimeout = requestTimeout; + return this; + } + + /** + * Sets the credentials for authentication. + * These credentials are stored and can be used with {@link IggyHttpClient#login()}. + * + * @param username the username + * @param password the password + * @return this builder + */ + public IggyHttpClientBuilder credentials(String username, String password) { + this.username = username; + this.password = password; + return this; + } + + /** + * Builds and returns a configured IggyHttpClient instance. + * The client will not be logged in. + * + * @return a new IggyHttpClient instance + * @throws IggyInvalidArgumentException if the host is null or empty (when url is not set), + * or if the port is not positive (when url is not set) + */ + public IggyHttpClient build() { + String finalUrl; + if (StringUtils.isNotBlank(url)) { + finalUrl = url; + } else { + if (StringUtils.isBlank(host)) { + throw new IggyInvalidArgumentException("Host cannot be null or empty"); + } + if (port == null || port <= 0) { + throw new IggyInvalidArgumentException("Port must be a positive integer"); + } + String protocol = enableTls ? "https" : "http"; Review Comment: Better to have protocols in constants too ########## foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyErrorCode.java: ########## @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iggy.exception; + +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * Error codes returned by the Iggy server. + * + * <p>These codes correspond to the error codes defined in the Iggy server's iggy_error.rs. + */ +public enum IggyErrorCode { + // General errors + ERROR(1), Review Comment: There are a lot of codes, we might have typos in the future. Is it possible we have it built into IDL and use the generated value here? ########## foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyServerException.java: ########## @@ -0,0 +1,255 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iggy.exception; + +import org.apache.commons.lang3.StringUtils; + +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; + +/** + * Exception thrown when the server returns an error response. + * + * <p>This exception carries the error code, reason, and optional field information + * from the server response. The factory methods automatically map error codes to + * more specific exception subclasses where appropriate. + */ +public class IggyServerException extends IggyException { + + private static final Set<IggyErrorCode> NOT_FOUND_CODES = EnumSet.of( + IggyErrorCode.RESOURCE_NOT_FOUND, + IggyErrorCode.CANNOT_LOAD_RESOURCE, + IggyErrorCode.STREAM_ID_NOT_FOUND, + IggyErrorCode.STREAM_NAME_NOT_FOUND, + IggyErrorCode.TOPIC_ID_NOT_FOUND, + IggyErrorCode.TOPIC_NAME_NOT_FOUND, + IggyErrorCode.PARTITION_NOT_FOUND, + IggyErrorCode.SEGMENT_NOT_FOUND, + IggyErrorCode.CLIENT_NOT_FOUND, + IggyErrorCode.CONSUMER_GROUP_ID_NOT_FOUND, + IggyErrorCode.CONSUMER_GROUP_NAME_NOT_FOUND, + IggyErrorCode.CONSUMER_GROUP_NOT_JOINED, + IggyErrorCode.MESSAGE_NOT_FOUND); + + private static final Set<IggyErrorCode> AUTHENTICATION_CODES = EnumSet.of( + IggyErrorCode.UNAUTHENTICATED, + IggyErrorCode.INVALID_CREDENTIALS, + IggyErrorCode.INVALID_USERNAME, + IggyErrorCode.INVALID_PASSWORD, + IggyErrorCode.INVALID_PAT_TOKEN, + IggyErrorCode.PASSWORD_DOES_NOT_MATCH, + IggyErrorCode.PASSWORD_HASH_INTERNAL_ERROR); + + private static final Set<IggyErrorCode> AUTHORIZATION_CODES = EnumSet.of(IggyErrorCode.UNAUTHORIZED); + + private static final Set<IggyErrorCode> CONFLICT_CODES = EnumSet.of( + IggyErrorCode.USER_ALREADY_EXISTS, + IggyErrorCode.CLIENT_ALREADY_EXISTS, + IggyErrorCode.STREAM_ALREADY_EXISTS, + IggyErrorCode.TOPIC_ALREADY_EXISTS, + IggyErrorCode.CONSUMER_GROUP_ALREADY_EXISTS, + IggyErrorCode.PAT_NAME_ALREADY_EXISTS); + + private static final Set<IggyErrorCode> VALIDATION_CODES = EnumSet.of( + IggyErrorCode.INVALID_COMMAND, + IggyErrorCode.INVALID_FORMAT, + IggyErrorCode.FEATURE_UNAVAILABLE, + IggyErrorCode.CANNOT_PARSE_INT, + IggyErrorCode.CANNOT_PARSE_SLICE, + IggyErrorCode.CANNOT_PARSE_UTF8, + IggyErrorCode.INVALID_STREAM_NAME, + IggyErrorCode.CANNOT_CREATE_STREAM_DIRECTORY, + IggyErrorCode.INVALID_TOPIC_NAME, + IggyErrorCode.INVALID_REPLICATION_FACTOR, + IggyErrorCode.CANNOT_CREATE_TOPIC_DIRECTORY, + IggyErrorCode.CONSUMER_GROUP_MEMBER_NOT_FOUND, + IggyErrorCode.INVALID_CONSUMER_GROUP_NAME, + IggyErrorCode.TOO_MANY_MESSAGES, + IggyErrorCode.EMPTY_MESSAGES, + IggyErrorCode.TOO_BIG_MESSAGE, + IggyErrorCode.INVALID_MESSAGE_CHECKSUM); + + private final IggyErrorCode errorCode; + private final int rawErrorCode; + private final String reason; + private final Optional<String> field; + private final Optional<String> errorId; + + /** + * Constructs a new IggyServerException. + * + * @param errorCode the error code enum + * @param rawErrorCode the raw numeric error code from the server + * @param reason the error reason/message + * @param field the optional field related to the error + * @param errorId the optional error ID for correlation with server logs + */ + public IggyServerException( + IggyErrorCode errorCode, + int rawErrorCode, + String reason, + Optional<String> field, + Optional<String> errorId) { + super(buildMessage(errorCode, rawErrorCode, reason, field, errorId)); + this.errorCode = errorCode; + this.rawErrorCode = rawErrorCode; + this.reason = reason; + this.field = field; + this.errorId = errorId; + } + + /** + * Constructs a new IggyServerException with just a status code. + * + * @param rawErrorCode the raw numeric error code from the server + */ + public IggyServerException(int rawErrorCode) { + this(IggyErrorCode.fromCode(rawErrorCode), rawErrorCode, "Server error", Optional.empty(), Optional.empty()); + } + + /** + * Returns the error code enum. + * + * @return the error code + */ + public IggyErrorCode getErrorCode() { + return errorCode; + } + + /** + * Returns the raw numeric error code from the server. + * + * @return the raw error code + */ + public int getRawErrorCode() { + return rawErrorCode; + } + + /** + * Returns the error reason/message. + * + * @return the reason + */ + public String getReason() { + return reason; + } + + /** + * Returns the optional field related to the error. + * + * @return the field, if present + */ + public Optional<String> getField() { + return field; + } + + /** + * Returns the optional error ID for correlation with server logs. + * + * <p>This ID is only available for HTTP responses and can be used to find + * the corresponding error in server logs. + * + * @return the error ID, if present + */ + public Optional<String> getErrorId() { + return errorId; + } + + /** + * Creates an appropriate exception from a TCP response. + * + * @param status the status code from the TCP response + * @param payload the error payload bytes (may contain error message) + * @return an appropriate IggyServerException subclass + */ + public static IggyServerException fromTcpResponse(long status, byte[] payload) { + int errorCode = (int) status; + String reason = + payload != null && payload.length > 0 ? new String(payload, StandardCharsets.UTF_8) : "Server error"; + return createFromCode(errorCode, reason, Optional.empty(), Optional.empty()); + } + + /** + * Creates an appropriate exception from an HTTP response. + * + * @param id the error ID for correlation with server logs + * @param code the error code string + * @param reason the error reason + * @param field the optional field related to the error + * @return an appropriate IggyServerException subclass + */ + public static IggyServerException fromHttpResponse(String id, String code, String reason, String field) { + IggyErrorCode errorCode = IggyErrorCode.fromString(code); + int rawCode = errorCode.getCode(); + if (rawCode == -1) { + // Try parsing code as integer + try { + rawCode = Integer.parseInt(code); + } catch (NumberFormatException e) { + rawCode = -1; + } + } + Optional<String> fieldOpt = Optional.ofNullable(StringUtils.stripToNull(field)); + Optional<String> errorIdOpt = Optional.ofNullable(StringUtils.stripToNull(id)); + return createFromCode(rawCode, reason != null ? reason : "Server error", fieldOpt, errorIdOpt); + } + + private static IggyServerException createFromCode( + int code, String reason, Optional<String> field, Optional<String> errorId) { + IggyErrorCode errorCode = IggyErrorCode.fromCode(code); + + if (NOT_FOUND_CODES.contains(errorCode)) { + return new IggyResourceNotFoundException(errorCode, code, reason, field, errorId); + } + if (AUTHENTICATION_CODES.contains(errorCode)) { + return new IggyAuthenticationException(errorCode, code, reason, field, errorId); + } + if (AUTHORIZATION_CODES.contains(errorCode)) { + return new IggyAuthorizationException(errorCode, code, reason, field, errorId); + } + if (CONFLICT_CODES.contains(errorCode)) { + return new IggyConflictException(errorCode, code, reason, field, errorId); + } + if (VALIDATION_CODES.contains(errorCode)) { + return new IggyValidationException(errorCode, code, reason, field, errorId); + } + + return new IggyServerException(errorCode, code, reason, field, errorId); + } + + private static String buildMessage( Review Comment: Would love to see this method validated 😄 ########## foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/InternalHttpClient.java: ########## @@ -140,8 +204,12 @@ private <T> T handleTypedResponse(ClassicHttpResponse response, JavaType type) t private void handleErrorResponse(ClassicHttpResponse response) throws IOException { if (!isSuccessful(response.getCode())) { - var error = objectMapper.readValue(response.getEntity().getContent(), IggyHttpError.class); - throw new IggyHttpException(error); + var errorNode = objectMapper.readValue(response.getEntity().getContent(), ObjectNode.class); Review Comment: We can do it later, but maybe this part can be in a dedicated class to make it more modular. ########## foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/TcpClientFactory.java: ########## @@ -28,12 +28,16 @@ final class TcpClientFactory { private TcpClientFactory() {} static IggyTcpClient create(GenericContainer<?> iggyServer) { + IggyTcpClient client; if (iggyServer == null) { // Server is running externally - return new IggyTcpClient("127.0.0.1", TCP_PORT); + client = new IggyTcpClient("127.0.0.1", TCP_PORT); Review Comment: Any plan to make 127.0.0.1 a constant? ########## foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyServerException.java: ########## @@ -0,0 +1,255 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iggy.exception; + +import org.apache.commons.lang3.StringUtils; + +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; + +/** + * Exception thrown when the server returns an error response. + * + * <p>This exception carries the error code, reason, and optional field information + * from the server response. The factory methods automatically map error codes to + * more specific exception subclasses where appropriate. + */ +public class IggyServerException extends IggyException { + + private static final Set<IggyErrorCode> NOT_FOUND_CODES = EnumSet.of( + IggyErrorCode.RESOURCE_NOT_FOUND, + IggyErrorCode.CANNOT_LOAD_RESOURCE, + IggyErrorCode.STREAM_ID_NOT_FOUND, + IggyErrorCode.STREAM_NAME_NOT_FOUND, + IggyErrorCode.TOPIC_ID_NOT_FOUND, + IggyErrorCode.TOPIC_NAME_NOT_FOUND, + IggyErrorCode.PARTITION_NOT_FOUND, + IggyErrorCode.SEGMENT_NOT_FOUND, + IggyErrorCode.CLIENT_NOT_FOUND, + IggyErrorCode.CONSUMER_GROUP_ID_NOT_FOUND, + IggyErrorCode.CONSUMER_GROUP_NAME_NOT_FOUND, + IggyErrorCode.CONSUMER_GROUP_NOT_JOINED, + IggyErrorCode.MESSAGE_NOT_FOUND); + + private static final Set<IggyErrorCode> AUTHENTICATION_CODES = EnumSet.of( + IggyErrorCode.UNAUTHENTICATED, + IggyErrorCode.INVALID_CREDENTIALS, + IggyErrorCode.INVALID_USERNAME, + IggyErrorCode.INVALID_PASSWORD, + IggyErrorCode.INVALID_PAT_TOKEN, + IggyErrorCode.PASSWORD_DOES_NOT_MATCH, + IggyErrorCode.PASSWORD_HASH_INTERNAL_ERROR); + + private static final Set<IggyErrorCode> AUTHORIZATION_CODES = EnumSet.of(IggyErrorCode.UNAUTHORIZED); + + private static final Set<IggyErrorCode> CONFLICT_CODES = EnumSet.of( + IggyErrorCode.USER_ALREADY_EXISTS, + IggyErrorCode.CLIENT_ALREADY_EXISTS, + IggyErrorCode.STREAM_ALREADY_EXISTS, + IggyErrorCode.TOPIC_ALREADY_EXISTS, + IggyErrorCode.CONSUMER_GROUP_ALREADY_EXISTS, + IggyErrorCode.PAT_NAME_ALREADY_EXISTS); + + private static final Set<IggyErrorCode> VALIDATION_CODES = EnumSet.of( Review Comment: Is it possible we place the codes into the Exception class and then leverage the exception class to determine if it is applicable? In this way we can avoid the if switches below. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
