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`:
```xml
```xml<p>
<repositories>
<repository>
<id>gitlab-cloudflare</id>
@@ -51,13 +51,13 @@ The dependency is:
- 0.2.0:
- 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
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` →
`recordList`
- 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,
optimized string concatenation, removed mutable setters from CfDnsClient
- 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`
#### With API Token (recommended):
```java
CfDnsClient cfDnsClient = new CfDnsClient(
"email@example.com", "yourApiKey"
);
CfDnsClient cfDnsClient = new CfDnsClientBuilder()
.withApiTokenAuth("your-api-token")
.build();
```
#### With Email/Key (legacy):
```java
CfDnsClient cfDnsClient = new CfDnsClientBuilder()
.withEmailKeyAuth("email@example.com", "yourApiKey")
.build();
```
### `zoneList`
@@ -390,7 +398,9 @@ client.zone("example.com")
### Complete Example
```java
CfDnsClient client = new CfDnsClient("email@example.com", "yourApiKey");
CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("your-api-token")
.build();
// Create a new record
client.zone("example.com")
@@ -425,7 +435,10 @@ The `CfDnsClient` provides internal error-handling mechanisms through exceptions
To enable exception throwing for empty results:
```java
CfDnsClient client = new CfDnsClient(true, "email@example.com", "yourApiKey");
CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("your-api-token")
.withEmptyResultThrowsException(true)
.build();
```
## Example:
@@ -1,6 +1,5 @@
package codes.thischwa.cf;
import codes.thischwa.cf.auth.CfAuth;
import codes.thischwa.cf.model.AbstractResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -31,7 +30,7 @@ import org.jetbrains.annotations.NotNull;
abstract class CfBasicHttpClient {
private final String baseUrl;
private final CfAuth auth;
private final CfDnsClientBuilder.CfAuth auth;
private final ObjectMapper objectMapper;
/**
@@ -40,7 +39,7 @@ abstract class CfBasicHttpClient {
* @param baseUrl the base URL for the Cloudflare API
* @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.auth = auth;
this.objectMapper = JsonConf.initObjectMapper();
@@ -1,6 +1,5 @@
package codes.thischwa.cf;
import codes.thischwa.cf.auth.CfAuth;
import codes.thischwa.cf.fluent.ZoneOperations;
import codes.thischwa.cf.fluent.ZoneOperationsImpl;
import codes.thischwa.cf.model.AbstractResponse;
@@ -30,7 +29,9 @@ import org.jetbrains.annotations.Nullable;
* <p>Example with API token authentication (recommended):
* <pre><code>
* // 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
* ZoneEntity zone = cfDnsClient.zoneGet("example.com");
@@ -49,63 +50,35 @@ import org.jetbrains.annotations.Nullable;
*
* <p>Example with email/key authentication (legacy):
* <pre><code>
* CfDnsClient cfDnsClient = new CfDnsClient(
* CfAuthBuilder.build("email@example.com", "your-api-key")
* );
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
* .withEmailKeyAuth("email@example.com", "your-api-key")
* .build();
* </code></pre>
*
* <p>Example with exception throwing enabled:
* <pre><code>
* // 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>
*
* <p>Example with custom base URL:
* <pre><code>
* CfAuth auth = CfAuthBuilder.build("your-api-token");
* auth.setBaseUrl("https://custom-api.example.com");
* CfDnsClient cfDnsClient = new CfDnsClient(auth);
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
* .withApiTokenAuth("your-api-token")
* .withBaseUrl("https://custom-api.example.com")
* .build();
* </code></pre>
*/
@Slf4j
public class CfDnsClient extends CfBasicHttpClient {
public static final String DEFAULT_BASEURL = "https://api.cloudflare.com/client/v4";
private final ResponseValidator responseValidator;
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}.
*
@@ -115,7 +88,7 @@ public class CfDnsClient extends CfBasicHttpClient {
* @param baseUrl The base URL for the Cloudflare API endpoint.
* @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);
this.responseValidator = new ResponseValidator(emptyResultThrowsException);
this.emptyResultThrowsException = emptyResultThrowsException;
@@ -514,4 +487,5 @@ public class CfDnsClient extends CfBasicHttpClient {
throws CloudflareApiException {
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.Assumptions.assumeTrue;
import codes.thischwa.cf.auth.CfAuthBuilder;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.List;
@@ -31,14 +30,14 @@ public class CfClientPenTest {
}
private CfDnsClient newClient() {
return new CfDnsClient(true, CfAuthBuilder.build(API_TOKEN));
return new CfDnsClientBuilder().withEmptyResultThrowsException(true).withApiTokenAuth(API_TOKEN).build();
}
@Test
@DisplayName("Invalid credentials should not authenticate and must throw CloudflareApiException")
void testInvalidCredentialsShouldFail() {
// 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);
}
@@ -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.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.RecordEntity;
import codes.thischwa.cf.model.RecordType;
@@ -33,7 +30,7 @@ public class CfClientTest {
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
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
void testRecordEntityInvalidType() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,