issue #11 - Refactor CfDnsClient with a simplified CfDnsClientBuilder for authentication and configuration. Update tests and README for new builder. Added authtification with apiToken

This commit is contained in:
2026-01-23 17:26:58 +01:00
parent b593ca7311
commit 607b634b35
11 changed files with 228 additions and 175 deletions
+21 -8
View File
@@ -29,7 +29,7 @@ This guide comes without any warranty. Use at your own risk. The author is not r
The project has its own maven repository. It can be added to the `pom.xml`: The project has its own maven repository. It can be added to the `pom.xml`:
```xml ```xml<p>
<repositories> <repositories>
<repository> <repository>
<id>gitlab-cloudflare</id> <id>gitlab-cloudflare</id>
@@ -51,13 +51,13 @@ The dependency is:
- 0.2.0: - 0.2.0:
- de-lombok the source jar - de-lombok the source jar
- **New Fluent API**: Added chainable method interface for more readable DNS operations (
`client.zone().record()...`)
- **Breaking Change**: `emptyResultThrowsException` default changed from `true` to `false`. Now applies to both - **Breaking Change**: `emptyResultThrowsException` default changed from `true` to `false`. Now applies to both
single and multiple result requests. Empty results will be returned by default without throwing exceptions. single and multiple result requests. Empty results will be returned by default without throwing exceptions.
- API method names refactored for consistency: `zoneListAll` → `zoneList`, `zoneInfo` → `zoneGet`, `sldListAll` → - API method names refactored for consistency: `zoneListAll` → `zoneList`, `zoneInfo` → `zoneGet`, `sldListAll` →
`recordList` `recordList`
- RecordEntity getter methods renamed for clarity: `getName()` → `getSld()` - RecordEntity getter methods renamed for clarity: `getName()` → `getSld()`
- **New Fluent API**: Changed the initialization of the client(`new CfDnsClientBuilder().withApiTokenAuth("your-api-token").build()`) and added chainable method interface for more readable DNS operations (
`client.zone().record()...`)
- Code quality improvements: eliminated duplication in batch operations, improved type safety in HTTP methods, - Code quality improvements: eliminated duplication in batch operations, improved type safety in HTTP methods,
optimized string concatenation, removed mutable setters from CfDnsClient optimized string concatenation, removed mutable setters from CfDnsClient
- Enhanced type validation in `RecordEntity.build()` with better error messages - Enhanced type validation in `RecordEntity.build()` with better error messages
@@ -89,10 +89,18 @@ the [javadoc of the CfDnsClient](https://cloudflaredns-java-f4ee3a.gitlab.io/api
### Instantiation of `CfDnsClient` ### Instantiation of `CfDnsClient`
#### With API Token (recommended):
```java ```java
CfDnsClient cfDnsClient = new CfDnsClient( CfDnsClient cfDnsClient = new CfDnsClientBuilder()
"email@example.com", "yourApiKey" .withApiTokenAuth("your-api-token")
); .build();
```
#### With Email/Key (legacy):
```java
CfDnsClient cfDnsClient = new CfDnsClientBuilder()
.withEmailKeyAuth("email@example.com", "yourApiKey")
.build();
``` ```
### `zoneList` ### `zoneList`
@@ -390,7 +398,9 @@ client.zone("example.com")
### Complete Example ### Complete Example
```java ```java
CfDnsClient client = new CfDnsClient("email@example.com", "yourApiKey"); CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("your-api-token")
.build();
// Create a new record // Create a new record
client.zone("example.com") client.zone("example.com")
@@ -425,7 +435,10 @@ The `CfDnsClient` provides internal error-handling mechanisms through exceptions
To enable exception throwing for empty results: To enable exception throwing for empty results:
```java ```java
CfDnsClient client = new CfDnsClient(true, "email@example.com", "yourApiKey"); CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("your-api-token")
.withEmptyResultThrowsException(true)
.build();
``` ```
## Example: ## Example:
@@ -1,6 +1,5 @@
package codes.thischwa.cf; package codes.thischwa.cf;
import codes.thischwa.cf.auth.CfAuth;
import codes.thischwa.cf.model.AbstractResponse; import codes.thischwa.cf.model.AbstractResponse;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -31,7 +30,7 @@ import org.jetbrains.annotations.NotNull;
abstract class CfBasicHttpClient { abstract class CfBasicHttpClient {
private final String baseUrl; private final String baseUrl;
private final CfAuth auth; private final CfDnsClientBuilder.CfAuth auth;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
/** /**
@@ -40,7 +39,7 @@ abstract class CfBasicHttpClient {
* @param baseUrl the base URL for the Cloudflare API * @param baseUrl the base URL for the Cloudflare API
* @param auth the authentication mechanism to use * @param auth the authentication mechanism to use
*/ */
CfBasicHttpClient(@NotNull String baseUrl, @NotNull CfAuth auth) { CfBasicHttpClient(@NotNull String baseUrl, @NotNull CfDnsClientBuilder.CfAuth auth) {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.auth = auth; this.auth = auth;
this.objectMapper = JsonConf.initObjectMapper(); this.objectMapper = JsonConf.initObjectMapper();
@@ -1,6 +1,5 @@
package codes.thischwa.cf; package codes.thischwa.cf;
import codes.thischwa.cf.auth.CfAuth;
import codes.thischwa.cf.fluent.ZoneOperations; import codes.thischwa.cf.fluent.ZoneOperations;
import codes.thischwa.cf.fluent.ZoneOperationsImpl; import codes.thischwa.cf.fluent.ZoneOperationsImpl;
import codes.thischwa.cf.model.AbstractResponse; import codes.thischwa.cf.model.AbstractResponse;
@@ -30,7 +29,9 @@ import org.jetbrains.annotations.Nullable;
* <p>Example with API token authentication (recommended): * <p>Example with API token authentication (recommended):
* <pre><code> * <pre><code>
* // Create a new CfDnsClient instance with API token * // Create a new CfDnsClient instance with API token
* CfDnsClient cfDnsClient = new CfDnsClient(CfAuthBuilder.build("your-api-token")); * CfDnsClient cfDnsClient = new CfDnsClientBuilder()
* .withApiTokenAuth("your-api-token")
* .build();
* *
* // Retrieve a zone * // Retrieve a zone
* ZoneEntity zone = cfDnsClient.zoneGet("example.com"); * ZoneEntity zone = cfDnsClient.zoneGet("example.com");
@@ -49,63 +50,35 @@ import org.jetbrains.annotations.Nullable;
* *
* <p>Example with email/key authentication (legacy): * <p>Example with email/key authentication (legacy):
* <pre><code> * <pre><code>
* CfDnsClient cfDnsClient = new CfDnsClient( * CfDnsClient cfDnsClient = new CfDnsClientBuilder()
* CfAuthBuilder.build("email@example.com", "your-api-key") * .withEmailKeyAuth("email@example.com", "your-api-key")
* ); * .build();
* </code></pre> * </code></pre>
* *
* <p>Example with exception throwing enabled: * <p>Example with exception throwing enabled:
* <pre><code> * <pre><code>
* // Throws exception when results are empty * // Throws exception when results are empty
* CfDnsClient cfDnsClient = new CfDnsClient(true, CfAuthBuilder.build("your-api-token")); * CfDnsClient cfDnsClient = new CfDnsClientBuilder()
* .withApiTokenAuth("your-api-token")
* .withEmptyResultThrowsException(true)
* .build();
* </code></pre> * </code></pre>
* *
* <p>Example with custom base URL: * <p>Example with custom base URL:
* <pre><code> * <pre><code>
* CfAuth auth = CfAuthBuilder.build("your-api-token"); * CfDnsClient cfDnsClient = new CfDnsClientBuilder()
* auth.setBaseUrl("https://custom-api.example.com"); * .withApiTokenAuth("your-api-token")
* CfDnsClient cfDnsClient = new CfDnsClient(auth); * .withBaseUrl("https://custom-api.example.com")
* .build();
* </code></pre> * </code></pre>
*/ */
@Slf4j @Slf4j
public class CfDnsClient extends CfBasicHttpClient { public class CfDnsClient extends CfBasicHttpClient {
public static final String DEFAULT_BASEURL = "https://api.cloudflare.com/client/v4";
private final ResponseValidator responseValidator; private final ResponseValidator responseValidator;
private final boolean emptyResultThrowsException; private final boolean emptyResultThrowsException;
/**
* Constructs a new instance of {@code CfDnsClient} with default configuration.
*
* @param auth The authentication mechanism to use (ApiTokenAuth or EmailKeyAuth)
*/
public CfDnsClient(CfAuth auth) {
this(false, DEFAULT_BASEURL, auth);
}
/**
* Constructs a new instance of {@code CfDnsClient}.
*
* @param baseUrl The base URL of the Cloudflare API to be used for requests.
* @param auth The authentication mechanism to use (ApiTokenAuth or EmailKeyAuth)
*/
public CfDnsClient(String baseUrl, CfAuth auth) {
this(false, baseUrl, auth);
}
/**
* Constructs a new instance of {@code CfDnsClient}.
*
* @param emptyResultThrowsException A boolean value indicating whether an exception should be
* thrown when the result is empty. Applies to both single and
* multiple result requests. Default is false.
* @param auth The authentication mechanism to use (ApiTokenAuth or EmailKeyAuth)
*/
public CfDnsClient(boolean emptyResultThrowsException, CfAuth auth) {
this(emptyResultThrowsException, DEFAULT_BASEURL, auth);
}
/** /**
* Constructs a new instance of {@code CfDnsClient}. * Constructs a new instance of {@code CfDnsClient}.
* *
@@ -115,7 +88,7 @@ public class CfDnsClient extends CfBasicHttpClient {
* @param baseUrl The base URL for the Cloudflare API endpoint. * @param baseUrl The base URL for the Cloudflare API endpoint.
* @param auth The authentication mechanism to use (ApiTokenAuth or EmailKeyAuth) * @param auth The authentication mechanism to use (ApiTokenAuth or EmailKeyAuth)
*/ */
public CfDnsClient(boolean emptyResultThrowsException, String baseUrl, CfAuth auth) { CfDnsClient(boolean emptyResultThrowsException, String baseUrl, CfDnsClientBuilder.CfAuth auth) {
super(baseUrl, auth); super(baseUrl, auth);
this.responseValidator = new ResponseValidator(emptyResultThrowsException); this.responseValidator = new ResponseValidator(emptyResultThrowsException);
this.emptyResultThrowsException = emptyResultThrowsException; this.emptyResultThrowsException = emptyResultThrowsException;
@@ -514,4 +487,5 @@ public class CfDnsClient extends CfBasicHttpClient {
throws CloudflareApiException { throws CloudflareApiException {
responseValidator.validate(resp, singleResultExpected); responseValidator.validate(resp, singleResultExpected);
} }
} }
@@ -0,0 +1,186 @@
package codes.thischwa.cf;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Builder class for configuring and creating instances of {@link CfDnsClient}.
* This class provides a fluent API for customizing the client settings,
* such as the base URL and authentication mechanism.
*/
public class CfDnsClientBuilder {
public static final String DEFAULT_BASEURL = "https://api.cloudflare.com/client/v4";
private boolean emptyResultThrowsException;
private CfAuth auth;
@Nullable
private String baseUrl;
/**
* Constructs a new instance of `CfDnsClientBuilder`.
*
* <p>This class serves as a builder for creating and configuring instances of a CfDnsClient. It provides
* a fluent API to set various optional configurations, such as API authentication methods and base
* URL, before constructing the client.
*
* <p>By using this constructor, you can initiate the building process with default settings, which can
* later be overridden using the provided builder methods.
*/
public CfDnsClientBuilder() {
}
/**
* Configures whether an exception should be thrown when an empty result is encountered
* during operations performed by the `CfDnsClient`.
*
* @param emptyResultThrowsException a boolean flag indicating if an exception should be thrown
* when an empty result is returned. If set to `true`, operations
* that result in an empty response will throw an exception;
* otherwise, they will not.
* @return the current instance of {@code CfDnsClientBuilder}, allowing for method chaining
* to further configure the builder.
*/
public CfDnsClientBuilder withEmptyResultThrowsException(boolean emptyResultThrowsException) {
this.emptyResultThrowsException = emptyResultThrowsException;
return this;
}
/**
* Sets the base URL to be used by the {@code CfDnsClient}.
* This method allows configuring the base URL for API requests, overriding any default value.
*
* @param baseUrl the base URL to be used for API requests
* @return the current instance of {@code CfDnsClientBuilder}, enabling method chaining
*/
public CfDnsClientBuilder withBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
return this;
}
/**
* Configures the authentication method for the {@code CfDnsClient} to use an API token.
* This is the recommended way to authenticate with the Cloudflare API, as it provides
* enhanced security and ease of use compared to other authentication mechanisms.
*
* @param apiToken the Cloudflare API token. This token is required for authenticating
* API requests and must not be null or blank.
* @return the current instance of {@code CfDnsClientBuilder}, allowing for method chaining
* to further configure the builder.
* @throws IllegalArgumentException if the {@code apiToken} is null or blank.
*/
public CfDnsClientBuilder withApiTokenAuth(String apiToken) {
this.auth = new ApiTokenAuth(apiToken);
return this;
}
/**
* Configures the authentication method for the {@code CfDnsClient} to use an email and API key.
* This approach uses the legacy authentication mechanism provided by Cloudflare, where requests
* are authenticated with a combination of an account's email address and API key.
*
* @param authEmail the email address associated with the Cloudflare account. This must not be null or blank.
* @param authKey the API key of the Cloudflare account. This must not be null or blank.
* @return the current instance of {@code CfDnsClientBuilder}, allowing for method chaining
* to further configure the builder.
* @throws IllegalArgumentException if {@code authEmail} or {@code authKey} is null or blank.
*/
public CfDnsClientBuilder withEmailKeyAuth(String authEmail, String authKey) {
this.auth = new EmailKeyAuth(authEmail, authKey);
return this;
}
/**
* Builds and returns a configured instance of {@code CfDnsClient}.
*
* <p>The method constructs a new {@code CfDnsClient} object based on the
* options set in the {@code CfDnsClientBuilder}. If no base URL has been
* explicitly configured, a default base URL will be used.
*
* @return a new instance of {@code CfDnsClient} configured with the
* specified options such as base URL, authentication details,
* and the exception-handling policy for empty results.
*/
public CfDnsClient build() {
String url = baseUrl == null ? DEFAULT_BASEURL : baseUrl;
return new CfDnsClient(emptyResultThrowsException, url, auth);
}
/**
* Interface for Cloudflare authentication mechanisms.
* Implementations of this interface provide different methods of authentication
* with the Cloudflare API (e.g., API token, email/key combination).
*/
interface CfAuth {
/**
* Applies authentication headers to the given HTTP request.
*
* @param request the HTTP request to authenticate
*/
void applyAuth(ClassicHttpRequest request);
}
/**
* Authentication mechanism using Cloudflare API token.
* This is the recommended authentication method for the Cloudflare API.
*/
static class ApiTokenAuth implements CfAuth {
private final String apiToken;
/**
* Creates a new API token authentication object.
*
* @param apiToken the Cloudflare API token
* @throws IllegalArgumentException if the API token is null or blank
*/
ApiTokenAuth(@NotNull String apiToken) {
if (apiToken.isBlank()) {
throw new IllegalArgumentException("API token must not be null or blank!");
}
this.apiToken = apiToken;
}
@Override
public void applyAuth(ClassicHttpRequest request) {
request.addHeader("Authorization", "Bearer " + apiToken);
}
}
/**
* Authentication mechanism using Cloudflare account email and API key.
* This is the legacy authentication method for the Cloudflare API.
*/
static class EmailKeyAuth implements CfAuth {
private final String authEmail;
private final String authKey;
/**
* Creates a new email/key authentication object.
*
* @param authEmail the email address associated with the Cloudflare account
* @param authKey the API key of the Cloudflare account
* @throws IllegalArgumentException if email or key is null or blank
*/
EmailKeyAuth(@NotNull String authEmail, @NotNull String authKey) {
if (authEmail.isBlank()) {
throw new IllegalArgumentException("Authentication email must not be null or blank!");
}
if (authKey.isBlank()) {
throw new IllegalArgumentException("Authentication key must not be null or blank!");
}
this.authEmail = authEmail;
this.authKey = authKey;
}
@Override
public void applyAuth(ClassicHttpRequest request) {
request.addHeader("X-Auth-Email", authEmail);
request.addHeader("X-Auth-Key", authKey);
}
}
}
@@ -1,31 +0,0 @@
package codes.thischwa.cf.auth;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.jetbrains.annotations.NotNull;
/**
* Authentication mechanism using Cloudflare API token.
* This is the recommended authentication method for the Cloudflare API.
*/
public class ApiTokenAuth implements CfAuth {
private final String apiToken;
/**
* Creates a new API token authentication object.
*
* @param apiToken the Cloudflare API token
* @throws IllegalArgumentException if the API token is null or blank
*/
public ApiTokenAuth(@NotNull String apiToken) {
if (apiToken.isBlank()) {
throw new IllegalArgumentException("API token must not be null or blank!");
}
this.apiToken = apiToken;
}
@Override
public void applyAuth(ClassicHttpRequest request) {
request.addHeader("Authorization", "Bearer " + apiToken);
}
}
@@ -1,19 +0,0 @@
package codes.thischwa.cf.auth;
import org.apache.hc.core5.http.ClassicHttpRequest;
/**
* Interface for Cloudflare authentication mechanisms.
* Implementations of this interface provide different methods of authentication
* with the Cloudflare API (e.g., API token, email/key combination).
*/
public interface CfAuth {
/**
* Applies authentication headers to the given HTTP request.
*
* @param request the HTTP request to authenticate
*/
void applyAuth(ClassicHttpRequest request);
}
@@ -1,12 +0,0 @@
package codes.thischwa.cf.auth;
public class CfAuthBuilder {
public static ApiTokenAuth build(String apiToken) {
return new ApiTokenAuth(apiToken);
}
public static EmailKeyAuth build(String authEmail, String authKey) {
return new EmailKeyAuth(authEmail, authKey);
}
}
@@ -1,38 +0,0 @@
package codes.thischwa.cf.auth;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.jetbrains.annotations.NotNull;
/**
* Authentication mechanism using Cloudflare account email and API key.
* This is the legacy authentication method for the Cloudflare API.
*/
public class EmailKeyAuth implements CfAuth {
private final String authEmail;
private final String authKey;
/**
* Creates a new email/key authentication object.
*
* @param authEmail the email address associated with the Cloudflare account
* @param authKey the API key of the Cloudflare account
* @throws IllegalArgumentException if email or key is null or blank
*/
public EmailKeyAuth(@NotNull String authEmail, @NotNull String authKey) {
if (authEmail.isBlank()) {
throw new IllegalArgumentException("Authentication email must not be null or blank!");
}
if (authKey.isBlank()) {
throw new IllegalArgumentException("Authentication key must not be null or blank!");
}
this.authEmail = authEmail;
this.authKey = authKey;
}
@Override
public void applyAuth(ClassicHttpRequest request) {
request.addHeader("X-Auth-Email", authEmail);
request.addHeader("X-Auth-Key", authKey);
}
}
@@ -1,5 +0,0 @@
/**
* The authentication package of CloudflareDNS-java.
*/
package codes.thischwa.cf.auth;
@@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue;
import codes.thischwa.cf.auth.CfAuthBuilder;
import codes.thischwa.cf.model.RecordType; import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity; import codes.thischwa.cf.model.ZoneEntity;
import java.util.List; import java.util.List;
@@ -31,14 +30,14 @@ public class CfClientPenTest {
} }
private CfDnsClient newClient() { private CfDnsClient newClient() {
return new CfDnsClient(true, CfAuthBuilder.build(API_TOKEN)); return new CfDnsClientBuilder().withEmptyResultThrowsException(true).withApiTokenAuth(API_TOKEN).build();
} }
@Test @Test
@DisplayName("Invalid credentials should not authenticate and must throw CloudflareApiException") @DisplayName("Invalid credentials should not authenticate and must throw CloudflareApiException")
void testInvalidCredentialsShouldFail() { void testInvalidCredentialsShouldFail() {
// Use syntactically valid but wrong credentials // Use syntactically valid but wrong credentials
CfDnsClient badClient = new CfDnsClient(CfAuthBuilder.build("invalid@example.com", UUID.randomUUID().toString())); CfDnsClient badClient = new CfDnsClientBuilder().withEmailKeyAuth("invalid@example.com", UUID.randomUUID().toString()).build();
assertThrows(CloudflareApiException.class, badClient::zoneList); assertThrows(CloudflareApiException.class, badClient::zoneList);
} }
@@ -8,9 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue;
import codes.thischwa.cf.auth.ApiTokenAuth;
import codes.thischwa.cf.auth.CfAuthBuilder;
import codes.thischwa.cf.auth.EmailKeyAuth;
import codes.thischwa.cf.model.BatchEntry; import codes.thischwa.cf.model.BatchEntry;
import codes.thischwa.cf.model.RecordEntity; import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType; import codes.thischwa.cf.model.RecordType;
@@ -33,7 +30,7 @@ public class CfClientTest {
private static final String API_TOKEN = System.getenv("API_TOKEN"); private static final String API_TOKEN = System.getenv("API_TOKEN");
private final CfDnsClient client = new CfDnsClient(true, CfAuthBuilder.build(API_TOKEN)); private final CfDnsClient client = new CfDnsClientBuilder().withEmptyResultThrowsException(true).withApiTokenAuth(API_TOKEN).build();
@BeforeAll @BeforeAll
static void checkEnv() { static void checkEnv() {
@@ -225,16 +222,6 @@ public class CfClientTest {
} }
} }
@Test
void testException() {
// Test EmailKeyAuth validation
assertThrows(IllegalArgumentException.class, () -> new EmailKeyAuth("email", ""));
assertThrows(IllegalArgumentException.class, () -> new EmailKeyAuth("", "key"));
// Test ApiTokenAuth validation;
assertThrows(IllegalArgumentException.class, () -> new ApiTokenAuth(""));
}
@Test @Test
void testRecordEntityInvalidType() { void testRecordEntityInvalidType() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,