13 Commits

Author SHA1 Message Date
thischwa 62b86ca22f [maven-release-plugin] prepare release v0.3.0 2026-02-15 17:33:34 +01:00
thischwa f12f5d6db1 Update .gitlab-ci.yml: Adjust apidocs path, and enhance documentation in ResultInfo and CfDnsClientBuilder. 2026-01-25 18:59:02 +01:00
thischwa c435a80966 Update pom.xml: Bump JUnit to 5.14.2, Mockito to 5.21.0, and Maven Release Plugin to 3.3.1. 2026-01-25 18:13:03 +01:00
thischwa 1e8fe53891 Update pom.xml: Bump dependency and plugin versions (Jackson: 2.19.1, HttpClient: 5.5.1, JUnit: 5.12.3, Maven Javadoc Plugin: 3.11.2, Release Plugin: 3.1.2, Jacoco Plugin: 0.8.13). 2026-01-25 17:57:27 +01:00
thischwa 10ad4bd7f3 Update version in pom.xml from 0.2.1-SNAPSHOT to 0.3.0-SNAPSHOT. 2026-01-25 14:39:49 +01:00
thischwa 84a454f656 fixed changelog 2026-01-25 14:30:59 +01:00
thischwa 4a3ec42fa5 reducing code smells 2026-01-23 18:32:54 +01:00
thischwa 9e09b12dc3 Update README.md: Remove duplicate "de-lombok the source jar" entry in changelog. 2026-01-23 17:55:44 +01:00
thischwa d0802fc01d Update pom.xml: Bump Checkstyle to 12.3.0, configure maven-jar-plugin with version 3.4.2, and adjust Checkstyle properties. 2026-01-23 17:40:53 +01:00
thischwa 607b634b35 issue #11 - Refactor CfDnsClient with a simplified CfDnsClientBuilder for authentication and configuration. Update tests and README for new builder. Added authtification with apiToken 2026-01-23 17:26:58 +01:00
thischwa b593ca7311 issue #11 -Implement authentication refactor: Add CfAuth interface, ApiTokenAuth and EmailKeyAuth implementations, and CfAuthBuilder for flexible authentication in CfDnsClient. Update tests and adjust related classes for compatibility. 2026-01-23 15:36:17 +01:00
thischwa 773573bd39 Update README.md: release 0.2.0 details and note de-lomboked source jar. 2026-01-19 18:21:45 +01:00
thischwa b890ae9d17 [maven-release-plugin] prepare for next development iteration 2026-01-19 18:03:50 +01:00
13 changed files with 346 additions and 151 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ build:
- mkdir public - mkdir public
- cp -rv docs/* public/ - cp -rv docs/* public/
- mkdir public/apidocs - mkdir public/apidocs
- cp -rv target/apidocs public/ - cp -rv target/reports/apidocs public/
artifacts: artifacts:
paths: paths:
- target/surefire-reports/*.xml - target/surefire-reports/*.xml
+26 -9
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>
@@ -49,14 +49,18 @@ The dependency is:
## Changelog ## Changelog
- 0.2.0-beta-SNAPSHOT: - 0.3.0-SNAPSHOT:
- **New Fluent API**: Added chainable method interface for more readable DNS operations ( - **Breaking Change**:
`client.zone().record()...`) - **New Fluent API**: Changed the initialization of the client(`new CfDnsClientBuilder().withApiTokenAuth("your-api-token").build()`)
- Authentication with API token.
- 0.2.0:
- **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
@@ -88,10 +92,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`
@@ -389,7 +401,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")
@@ -424,7 +438,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:
+14 -11
View File
@@ -4,7 +4,7 @@
<groupId>codes.thischwa</groupId> <groupId>codes.thischwa</groupId>
<artifactId>cloudflaredns</artifactId> <artifactId>cloudflaredns</artifactId>
<version>0.2.0</version> <version>0.3.0</version>
<name>CloudflareDNS-java</name> <name>CloudflareDNS-java</name>
<inceptionYear>2025</inceptionYear> <inceptionYear>2025</inceptionYear>
<packaging>jar</packaging> <packaging>jar</packaging>
@@ -21,24 +21,26 @@
<project.reporting.outputEncoding>${file.encoding}</project.reporting.outputEncoding> <project.reporting.outputEncoding>${file.encoding}</project.reporting.outputEncoding>
<!-- checkstyle --> <!-- checkstyle -->
<checkstyle.version>10.21.3</checkstyle.version> <checkstyle.version>12.3.0</checkstyle.version>
<checkstyle.plugin.version>3.6.0</checkstyle.plugin.version> <checkstyle.plugin.version>3.6.0</checkstyle.plugin.version>
<checkstyle.config.location>${project.basedir}/src/checkstyle/google_custom_checks.xml <checkstyle.config.location>${project.basedir}/src/checkstyle/google_custom_checks.xml
</checkstyle.config.location> </checkstyle.config.location>
<checkstyle.includeTestResources>false</checkstyle.includeTestResources> <checkstyle.includeTestResources>false</checkstyle.includeTestResources>
<checkstyle.violationSeverity>warning</checkstyle.violationSeverity> <checkstyle.violationSeverity>warning</checkstyle.violationSeverity>
<checkstyle.failOnViolation>false</checkstyle.failOnViolation> <checkstyle.failOnViolation>false</checkstyle.failOnViolation>
<checkstyle.propertyExpansion>config_loc=${basedir}
</checkstyle.propertyExpansion>
<checkstyle.consoleOutput>true</checkstyle.consoleOutput> <checkstyle.consoleOutput>true</checkstyle.consoleOutput>
<linkX-Ref>false</linkX-Ref> <linkXRef>false</linkXRef>
<!-- 3rd party dependencies --> <!-- 3rd party dependencies -->
<jackson.version>2.18.2</jackson.version> <jackson.version>2.19.1</jackson.version>
<httpclient5.version>5.4.3</httpclient5.version> <httpclient5.version>5.5.1</httpclient5.version>
<lombok.version>1.18.36</lombok.version> <lombok.version>1.18.36</lombok.version>
<slf4j.version>2.0.17</slf4j.version> <slf4j.version>2.0.17</slf4j.version>
<logback-classic.version>1.5.18</logback-classic.version> <logback-classic.version>1.5.18</logback-classic.version>
<junit5.version>5.12.2</junit5.version> <junit5.version>5.14.2</junit5.version>
<mockito-junit5.version>5.17.0</mockito-junit5.version> <mockito-junit5.version>5.21.0</mockito-junit5.version>
<!-- sonarqube --> <!-- sonarqube -->
<sonar.organization>th-schwarz</sonar.organization> <sonar.organization>th-schwarz</sonar.organization>
@@ -56,7 +58,7 @@
<developerConnection>scm:git:git@gitlab.com:th-schwarz/CloudflareDNS-java.git</developerConnection> <developerConnection>scm:git:git@gitlab.com:th-schwarz/CloudflareDNS-java.git</developerConnection>
<connection>scm:git:git@gitlab.com:th-schwarz/CloudflareDNS-java.git</connection> <connection>scm:git:git@gitlab.com:th-schwarz/CloudflareDNS-java.git</connection>
<url>https://gitlab.com/th-schwarz/CloudflareDNS-java</url> <url>https://gitlab.com/th-schwarz/CloudflareDNS-java</url>
<tag>v0.2.0</tag> <tag>v0.3.0</tag>
</scm> </scm>
<distributionManagement> <distributionManagement>
@@ -149,7 +151,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId> <artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.3</version> <version>3.11.2</version>
<configuration> <configuration>
<failOnError>false</failOnError> <failOnError>false</failOnError>
<locale>en</locale> <locale>en</locale>
@@ -168,7 +170,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId> <artifactId>maven-release-plugin</artifactId>
<version>3.1.1</version> <version>3.3.1</version>
<configuration> <configuration>
<tagNameFormat>v@{project.version}</tagNameFormat> <tagNameFormat>v@{project.version}</tagNameFormat>
<arguments>-DskipTests</arguments> <arguments>-DskipTests</arguments>
@@ -202,7 +204,7 @@
<!-- generates the code coverage report for sonar cube --> <!-- generates the code coverage report for sonar cube -->
<groupId>org.jacoco</groupId> <groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId> <artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version> <version>0.8.13</version>
<executions> <executions>
<execution> <execution>
<id>prepare-agent</id> <id>prepare-agent</id>
@@ -261,6 +263,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<executions> <executions>
<execution> <execution>
<id>attach-delomboked-sources</id> <id>attach-delomboked-sources</id>
@@ -19,6 +19,7 @@ import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
import org.jetbrains.annotations.NotNull;
/** /**
* Abstract base class for creating HTTP clients to interact with the Cloudflare API. Provides * Abstract base class for creating HTTP clients to interact with the Cloudflare API. Provides
@@ -27,23 +28,20 @@ import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
*/ */
@Slf4j @Slf4j
abstract class CfBasicHttpClient { abstract class CfBasicHttpClient {
private final String baseUrl;
private final String authEmail;
private final String authKey;
private final String baseUrl;
private final CfDnsClientBuilder.CfAuth auth;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
CfBasicHttpClient(String baseUrl, String authEmail, String authKey) /**
throws IllegalArgumentException { * Creates a new Cloudflare HTTP client with the specified base URL and authentication.
if (authEmail == null || authEmail.isBlank()) { *
throw new IllegalArgumentException("Authentication email must not be null or blank!"); * @param baseUrl the base URL for the Cloudflare API
} * @param auth the authentication mechanism to use
if (authKey == null || authKey.isBlank()) { */
throw new IllegalArgumentException("Authentication key must not be null or blank!"); CfBasicHttpClient(@NotNull String baseUrl, @NotNull CfDnsClientBuilder.CfAuth auth) {
}
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.authEmail = authEmail; this.auth = auth;
this.authKey = authKey;
this.objectMapper = JsonConf.initObjectMapper(); this.objectMapper = JsonConf.initObjectMapper();
} }
@@ -53,8 +51,9 @@ abstract class CfBasicHttpClient {
request.addHeader(HttpHeaders.ACCEPT_ENCODING, "gzip"); request.addHeader(HttpHeaders.ACCEPT_ENCODING, "gzip");
request.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.getMimeType()); request.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.getMimeType());
request.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType()); request.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
request.addHeader("X-Auth-Email", authEmail); if (request instanceof ClassicHttpRequest classicRequest) {
request.addHeader("X-Auth-Key", authKey); auth.applyAuth(classicRequest);
}
}).build(); }).build();
} }
@@ -15,8 +15,10 @@ import codes.thischwa.cf.model.ZoneMultipleResponse;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -26,74 +28,59 @@ import org.jetbrains.annotations.Nullable;
* records and zones within the Cloudflare system, including creating, updating, retrieving, and * records and zones within the Cloudflare system, including creating, updating, retrieving, and
* deleting DNS records. * deleting DNS records.
* *
* <p>Example: * <p>Example with API token authentication (recommended):
* <pre><code> * <pre><code>
* // Create a new CfDnsClient instance * // Create a new CfDnsClient instance with API token
* CfDnsClient cfDnsClient = new CfDnsClient( * CfDnsClient cfDnsClient = new CfDnsClientBuilder()
* "email@example.com", * .withApiTokenAuth("your-api-token")
* "yourApiKey" * .build();
* ); *
* // Retrieve a zone * // Retrieve a zone
* ZoneEntity zone = cfDnsClient.zoneGet("example.com"); * ZoneEntity zone = cfDnsClient.zoneGet("example.com");
* System.out.println("Zone ID: " + zone.getId()); * System.out.println("Zone ID: " + zone.getId());
*
* // Retrieve records of a subdomain * // Retrieve records of a subdomain
* List&lt;{@link RecordEntity}&gt; records = cfDnsClient.recordGet(zone, "sld"); * List&lt;RecordEntity&gt; records = cfDnsClient.recordList(zone, "sld");
* records.forEach(record -> * records.forEach(record ->
* System.out.println("Record Type: " + record.getType() + ", Value: " + record.getContent()) * System.out.println("Record Type: " + record.getType() + ", Value: " + record.getContent())
* ); * );
*
* // Create a record for the subdomain "api" * // Create a record for the subdomain "api"
* RecordEntity created = cfDnsClient.recordCreateSld(zone, "api", 60, RecordType.A, "192.168.1.10"); * RecordEntity created = cfDnsClient.recordCreateSld(zone, "api", 60, RecordType.A, "192.168.1.10");
* System.out.println("Created Record ID: " + created.getId()); * System.out.println("Created Record ID: " + created.getId());
* </code></pre> * </code></pre>
*
* <p>Example with email/key authentication (legacy):
* <pre><code>
* 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 CfDnsClientBuilder()
* .withApiTokenAuth("your-api-token")
* .withEmptyResultThrowsException(true)
* .build();
* </code></pre>
*
* <p>Example with custom base URL:
* <pre><code>
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
* .withApiTokenAuth("your-api-token")
* .withBaseUrl("https://custom-api.example.com")
* .build();
* </code></pre>
*/ */
@Slf4j @Slf4j
public class CfDnsClient extends CfBasicHttpClient { public class CfDnsClient extends CfBasicHttpClient {
private 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}.
*
* @param authEmail The email address associated with the Cloudflare account, used for
* authentication.
* @param authKey The API key of the Cloudflare account, used as part of the authentication
* process.
*/
public CfDnsClient(String authEmail, String authKey) {
this(DEFAULT_BASEURL, authEmail, authKey);
}
/**
* Constructs a new instance of {@code CfDnsClient}.
*
* @param baseUrl The base URL of the Cloudflare API to be used for requests.
* @param authEmail The email address associated with the Cloudflare account, used for
* authentication.
* @param authKey The API key of the Cloudflare account, used as part of the authentication
* process.
*/
public CfDnsClient(String baseUrl, String authEmail, String authKey) {
this(false, baseUrl, authEmail, authKey);
}
/**
* 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 authEmail The email address associated with the Cloudflare account,
* used for authentication.
* @param authKey The API key of the Cloudflare account, used as part of the
* authentication process.
*/
public CfDnsClient(boolean emptyResultThrowsException, String authEmail, String authKey) {
this(emptyResultThrowsException, DEFAULT_BASEURL, authEmail, authKey);
}
/** /**
* Constructs a new instance of {@code CfDnsClient}. * Constructs a new instance of {@code CfDnsClient}.
* *
@@ -101,14 +88,10 @@ public class CfDnsClient extends CfBasicHttpClient {
* thrown when the result is empty. Applies to both single and * thrown when the result is empty. Applies to both single and
* multiple result requests. Default is false. * multiple result requests. Default is false.
* @param baseUrl The base URL for the Cloudflare API endpoint. * @param baseUrl The base URL for the Cloudflare API endpoint.
* @param authEmail The email associated with the Cloudflare account for * @param auth The authentication mechanism to use (ApiTokenAuth or EmailKeyAuth)
* authentication.
* @param authKey The API key for authenticating the client with Cloudflare
* services.
*/ */
public CfDnsClient(boolean emptyResultThrowsException, String baseUrl, String authEmail, CfDnsClient(boolean emptyResultThrowsException, String baseUrl, CfDnsClientBuilder.CfAuth auth) {
String authKey) { super(baseUrl, auth);
super(baseUrl, authEmail, authKey);
this.responseValidator = new ResponseValidator(emptyResultThrowsException); this.responseValidator = new ResponseValidator(emptyResultThrowsException);
this.emptyResultThrowsException = emptyResultThrowsException; this.emptyResultThrowsException = emptyResultThrowsException;
} }
@@ -316,9 +299,9 @@ public class CfDnsClient extends CfBasicHttpClient {
RecordSingleResponse resp = postRequest(endpoint, rec, RecordSingleResponse.class); RecordSingleResponse resp = postRequest(endpoint, rec, RecordSingleResponse.class);
checkResponse(resp); checkResponse(resp);
log.info("Record {} of type {} successful created.", rec.getSld(), rec.getType()); log.info("Record {} of type {} successful created.", rec.getSld(), rec.getType());
RecordEntity record = resp.getResult(); RecordEntity retRec = resp.getResult();
record.setZoneId(zone.getId()); retRec.setZoneId(zone.getId());
return record; return retRec;
} }
/** /**
@@ -452,7 +435,10 @@ public class CfDnsClient extends CfBasicHttpClient {
throws CloudflareNotFoundException { throws CloudflareNotFoundException {
List<RecordEntity> filtered; List<RecordEntity> filtered;
if (types != null && types.length > 0) { if (types != null && types.length > 0) {
filtered = recs.stream().filter(rec -> Arrays.asList(types).contains(RecordType.valueOf(rec.getType()))).collect(Collectors.toList()); Set<RecordType> allowedTypes = new HashSet<>(Arrays.asList(types));
filtered = recs.stream()
.filter(rec -> allowedTypes.contains(RecordType.valueOf(rec.getType())))
.collect(Collectors.toList());;
} else { } else {
filtered = new ArrayList<>(recs); filtered = new ArrayList<>(recs);
} }
@@ -506,4 +492,5 @@ public class CfDnsClient extends CfBasicHttpClient {
throws CloudflareApiException { throws CloudflareApiException {
responseValidator.validate(resp, singleResultExpected); responseValidator.validate(resp, singleResultExpected);
} }
} }
@@ -0,0 +1,190 @@
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 {
/**
* The default base URL for the Cloudflare v4 API requests made by the {@code CfDnsClient}.
*/
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);
}
}
}
@@ -46,18 +46,28 @@ class ResponseValidator {
private void validateResultCount(AbstractResponse resp, boolean singleResultExpected) private void validateResultCount(AbstractResponse resp, boolean singleResultExpected)
throws CloudflareApiException { throws CloudflareApiException {
if (resp instanceof RecordMultipleResponse respMulti) { if (resp instanceof RecordMultipleResponse respMulti) {
if (singleResultExpected && respMulti.getResultInfo().totalCount() > 1) { validateMultipleResponse(respMulti, singleResultExpected);
throw new CloudflareApiException(
"Unexpected result count: " + respMulti.getResultInfo().totalCount());
}
if (emptyResultThrowsException && respMulti.getResultInfo().totalCount() == 0) {
throw new CloudflareNotFoundException("No result found");
}
} else if (resp instanceof AbstractSingleResponse<?> respSingle) { } else if (resp instanceof AbstractSingleResponse<?> respSingle) {
if (emptyResultThrowsException && respSingle.getResult() == null) { validateSingleResponse(respSingle);
}
}
private void validateMultipleResponse(RecordMultipleResponse response, boolean singleResultExpected)
throws CloudflareApiException {
int totalCount = response.getResultInfo().totalCount();
if (singleResultExpected && totalCount > 1) {
throw new CloudflareApiException("Unexpected result count: " + totalCount);
}
if (emptyResultThrowsException && totalCount == 0) {
throw new CloudflareNotFoundException("No result found"); throw new CloudflareNotFoundException("No result found");
} }
} }
private void validateSingleResponse(AbstractSingleResponse<?> response)
throws CloudflareNotFoundException {
if (emptyResultThrowsException && response.getResult() == null) {
throw new CloudflareNotFoundException("No result found");
}
} }
} }
@@ -45,16 +45,16 @@ public class RecordOperationsImpl implements RecordOperations {
@Override @Override
public RecordEntity update(String newContent) throws CloudflareApiException { public RecordEntity update(String newContent) throws CloudflareApiException {
List<RecordEntity> records = get(); List<RecordEntity> recs = get();
if (records.isEmpty()) { if (recs.isEmpty()) {
throw new CloudflareApiException("No records found to update for subdomain: " + sld); throw new CloudflareApiException("No recs found to update for subdomain: " + sld);
} }
if (records.size() > 1) { if (recs.size() > 1) {
throw new CloudflareApiException("Multiple records found. Please use recordUpdate() directly for precise control."); throw new CloudflareApiException("Multiple recs found. Please use recordUpdate() directly for precise control.");
} }
RecordEntity record = records.get(0); RecordEntity rec = recs.get(0);
record.setContent(newContent); rec.setContent(newContent);
return client.recordUpdate(zone, record); return client.recordUpdate(zone, rec);
} }
@Override @Override
@@ -64,10 +64,10 @@ public class RecordEntity extends AbstractEntity {
*/ */
public static RecordEntity build(String name, RecordType type, Integer ttl, String content) { public static RecordEntity build(String name, RecordType type, Integer ttl, String content) {
RecordEntity rec = new RecordEntity(); RecordEntity rec = new RecordEntity();
rec.setName(name); rec.name = name;
rec.setType(type.getType()); rec.type = type.getType();
rec.setTtl(ttl); rec.ttl = ttl;
rec.setContent(content); rec.content = content;
return rec; return rec;
} }
@@ -81,7 +81,7 @@ public class RecordEntity extends AbstractEntity {
public static RecordEntity build(String id, String content) { public static RecordEntity build(String id, String content) {
RecordEntity rec = new RecordEntity(); RecordEntity rec = new RecordEntity();
rec.setId(id); rec.setId(id);
rec.setContent(content); rec.content = content;
return rec; return rec;
} }
@@ -104,8 +104,12 @@ public class RecordEntity extends AbstractEntity {
throw new IllegalArgumentException("Invalid record type: " + type + ". Must be one of: " throw new IllegalArgumentException("Invalid record type: " + type + ". Must be one of: "
+ java.util.Arrays.toString(RecordType.values()), e); + java.util.Arrays.toString(RecordType.values()), e);
} }
RecordEntity rec = build(name, recordType, ttl, content); RecordEntity rec = new RecordEntity();
rec.setId(id); rec.setId(id);
rec.name = name;
rec.type = recordType.getType();
rec.ttl = ttl;
rec.content = content;
return rec; return rec;
} }
@@ -199,6 +199,6 @@ public enum RecordType {
@Override @Override
public String toString() { public String toString() {
return getType(); return type;
} }
} }
@@ -6,15 +6,12 @@ package codes.thischwa.cf.model;
* <p>This class contains information about the current page, page size, total pages, and result * <p>This class contains information about the current page, page size, total pages, and result
* counts, which can be utilized in managing and navigating through paginated data. * counts, which can be utilized in managing and navigating through paginated data.
* *
* <ul> * @param page The current page number.
* <li><b>page:</b> The current page number. * @param perPage The number of results per page.
* <li><b>perPage:</b> The number of results per page. * @param totalPages The total number of pages available.
* <li><b>totalPages:</b> The total number of pages available. * @param count The number of results on the current page.
* <li><b>count:</b> The number of results on the current page. * @param totalCount The total number of results across all pages.
* <li><b>totalCount:</b> The total number of results across all pages.
* </ul>
*/ */
public record ResultInfo(int page, int perPage, int totalPages, int count, int totalCount) { public record ResultInfo(int page, int perPage, int totalPages, int count, int totalCount) {
/** /**
@@ -22,24 +22,22 @@ public class CfClientPenTest {
private static final String ZONE_STR = "mein-d-ns.de"; // existing baseline zone private static final String ZONE_STR = "mein-d-ns.de"; // existing baseline zone
private static final String API_EMAIL = System.getenv("API_EMAIL"); private static final String API_TOKEN = System.getenv("API_TOKEN");
private static final String API_KEY = System.getenv("API_KEY");
@BeforeAll @BeforeAll
static void checkEnv() { static void checkEnv() {
assumeTrue(API_EMAIL != null && !API_EMAIL.isBlank(), "API_EMAIL not set; skipping pen tests"); assumeTrue(API_TOKEN != null && !API_TOKEN.isBlank(), "API_TOKEN not set; skipping pen tests");
assumeTrue(API_KEY != null && !API_KEY.isBlank(), "API_KEY not set; skipping pen tests");
} }
private CfDnsClient newClient() { private CfDnsClient newClient() {
return new CfDnsClient(true, API_EMAIL, API_KEY); 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("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);
} }
@@ -28,15 +28,13 @@ public class CfClientTest {
private static final String SLD_STR = "devsld"; private static final String SLD_STR = "devsld";
private static final int TTL = 60; private static final int TTL = 60;
private static final String API_EMAIL = System.getenv("API_EMAIL"); private static final String API_TOKEN = System.getenv("API_TOKEN");
private static final String API_KEY = System.getenv("API_KEY");
private final CfDnsClient client = new CfDnsClient(true, API_EMAIL, API_KEY); private final CfDnsClient client = new CfDnsClientBuilder().withEmptyResultThrowsException(true).withApiTokenAuth(API_TOKEN).build();
@BeforeAll @BeforeAll
static void checkEnv() { static void checkEnv() {
assumeTrue(API_EMAIL != null && !API_EMAIL.isBlank(), "API_EMAIL not set; skipping pen tests"); assumeTrue(API_TOKEN != null && !API_TOKEN.isBlank(), "API_TOKEN not set; skipping client tests");
assumeTrue(API_KEY != null && !API_KEY.isBlank(), "API_KEY not set; skipping pen tests");
} }
@Test @Test
@@ -106,13 +104,13 @@ public class CfClientTest {
void testDns() throws Exception { void testDns() throws Exception {
// starting point: already existing zone 'mein-d-ns.de' // starting point: already existing zone 'mein-d-ns.de'
ZoneEntity z = client.zoneGet(ZONE_STR); ZoneEntity z = client.zoneGet(ZONE_STR);
assertEquals("0a83dd6e7f8c46039f2517bbded8115e", z.getId()); assertEquals("cf9d8b12f61423f280e0a3ea2a96d921", z.getId());
assertEquals("mein-d-ns.de", z.getName()); assertEquals("mein-d-ns.de", z.getName());
assertEquals("active", z.getStatus()); assertEquals("active", z.getStatus());
assertEquals(2, z.getNameServers().size()); assertEquals(2, z.getNameServers().size());
assertTrue(z.getNameServers().contains("sergi.ns.cloudflare.com")); assertTrue(z.getNameServers().contains("rafe.ns.cloudflare.com"));
assertEquals(4, z.getOriginalNameServers().size()); assertTrue(z.getOriginalNameServers().size() >= 2);
assertTrue(z.getOriginalNameServers().contains("a.ns14.net")); assertTrue(z.getOriginalNameServers().contains("blair.ns.cloudflare.com"));
assertNotNull(z.getActivatedOn()); assertNotNull(z.getActivatedOn());
assertNotNull(z.getModifiedOn()); assertNotNull(z.getModifiedOn());
assertNotNull(z.getCreatedOn()); assertNotNull(z.getCreatedOn());
@@ -224,14 +222,6 @@ public class CfClientTest {
} }
} }
@Test
void testException() {
assertThrows(IllegalArgumentException.class, () -> new CfDnsClient(null, "key"));
assertThrows(IllegalArgumentException.class, () -> new CfDnsClient("email", null));
assertThrows(IllegalArgumentException.class, () -> new CfDnsClient("email", ""));
assertThrows(IllegalArgumentException.class, () -> new CfDnsClient("", "key"));
}
@Test @Test
void testRecordEntityInvalidType() { void testRecordEntityInvalidType() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,