1 Commits
Author SHA1 Message Date
thischwa ca5c92bd36 Initial commit 2026-09-07 07:39:58 +02:00
47 changed files with 14 additions and 4518 deletions
-13
View File
@@ -1,13 +0,0 @@
/.settings/
/target/
/.classpath
/.project
/.factorypath
/.springBeans
/.idea
*.iml
# local files
/info.txt
cf.yml
/docs/apidocs/javadoc.sh
-75
View File
@@ -1,75 +0,0 @@
default:
image: maven:3-amazoncorretto-17-alpine
stages:
- build
- on_commit
- sonarcloud_scan
- deploy
- release
variables:
GITLAB_CLONE_DIR: "/builds/th-schwarz/CloudflareDNS-java"
GITLAB_USERNAME: $GITLAB_USERNAME
GITLAB_USEREMAIL: GITLAB_USEREMAIL
SONAR_HOST_URL: $SONAR_HOST_URL
SONAR_PROJECT_KEY: "th-schwarz_CloudflareDNS-java"
SONAR_ORGANIZATION: "th-schwarz"
SONAR_TOKEN: $SONAR_TOKEN
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task
API_EMAIL: $API_EMAIL
API_KEY: $API_KEY
API_TOKEN: $API_TOKEN
build:
stage: build
script:
- echo "Running package..."
- cd ${GITLAB_CLONE_DIR}
- mvn clean package
- echo "Preparing GitLab Pages from docs/ (javadoc)"
- mkdir public
- cp -rv docs/* public/
- mkdir public/apidocs
- cp -rv target/reports/apidocs public/
artifacts:
paths:
- target/surefire-reports/*.xml
- target/
- public/
except:
- tags
on_commits:
stage: on_commit
dependencies:
- build
script:
- echo "on_commit DONE"
only:
- /^feature.*$/
- merge_request
- develop
pages:
# triggers the page deployment of gitlab
stage: deploy
script:
- echo "Publishing to GitLab Pages ..."
artifacts:
paths:
- public
only:
- develop
sonarcloud_scan:
stage: sonarcloud_scan
dependencies:
- build
script:
- mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=${SONAR_PROJECT_KEY}
only:
- merge_requests
- develop
- tags
+13 -16
View File
@@ -1,21 +1,18 @@
MIT License
Copyright (c) 2025 Thilo Schwarz
Copyright (c) 2026 thischwa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
+1 -465
View File
@@ -1,467 +1,3 @@
# CloudflareDNS-java
![GitLab Pipeline Status](https://gitlab.com/th-schwarz/CloudflareDNS-java/badges/develop/pipeline.svg)
![GitLab License](https://img.shields.io/gitlab/license/th-schwarz%2FCloudflareDNS-java)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=thischwa_CloudflareDNS-java&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=thischwa_CloudflareDNS-java)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=thischwa_CloudflareDNS-java&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=thischwa_CloudflareDNS-java)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=thischwa_CloudflareDNS-java&metric=coverage)](https://sonarcloud.io/summary/new_code?id=thischwa_CloudflareDNS-java)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=thischwa_CloudflareDNS-java&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=thischwa_CloudflareDNS-java)
[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=thischwa_CloudflareDNS-java&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=thischwa_CloudflareDNS-java)
## Preface
This project provides a java client for minimalistic access to the Cloudflare API version 4, which is mainly used for
managing DNS settings such as creating, updating and deleting DNS records.
If you encounter any bugs or find missing features, feel free to report them on
the [GitLab Issues page](https://gitlab.com/th-schwarz/CloudflareDNS-java/-/issues).
---
## Disclaimer
This guide comes without any warranty. Use at your own risk. The author is not responsible for potential data loss, hardware damage or keyboard mishaps!
---
## Get It
The project has its own maven repository. It can be added to the `pom.xml`:
```xml<p>
<repositories>
<repository>
<id>gitlab-cloudflare</id>
<url>https://gitlab.com/api/v4/projects/68509751/packages/maven</url>
</repository>
</repositories>
```
The dependency is:
```xml
<dependency>
<groupId>codes.thischwa</groupId>
<artifactId>cloudflaredns</artifactId>
<version>[version]</version>
</dependency>
```
## Changelog
- 0.3.0-SNAPSHOT:
- **Breaking Change**:
- **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
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
- CfClient#recordList must return multiple RecordEntries
- add a missing source jar
- ResponseResultInfo#Errors: wrong object structure
- changing multiple records with put, post, patch and delete for dns-records
- 0.1.0:
- refactored / extended tests
- 0.1.0-beta.3:
- fixed json deserialization
- added logging of api errors
- 0.1.0-beta.1: 1st runnable version
## Methods Overview
The methods can be categorized as follows:
- `Zone`: list, info
- `Record`: list, info, create, update, delete
The API provides two styles for working with DNS records:
1. **Traditional API**: Direct method calls with explicit parameters
2. **Fluent API**: Chainable method calls for more readable code
The following text focuses on the basic methods. For further information, take a look at
the [javadoc of the CfDnsClient](https://cloudflaredns-java-f4ee3a.gitlab.io/apidocs/codes/thischwa/cf/CfDnsClient.html).
### Instantiation of `CfDnsClient`
#### With API Token (recommended):
```java
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`
Retrieve all zones within the Cloudflare account.
- **Returns**: A list of `ZoneEntity` objects.
```java
List<ZoneEntity> zones = cfDnsClient.zoneList();
zones.forEach(zone -> System.out.println("Zone: " + zone.getName()));
```
---
### `zoneGet`
Get detailed information about a specific zone by its name.
- **Parameters**:
- `String name` - The zone name (e.g., "example.com").
- **Returns**: A `ZoneEntity` object.
```java
ZoneEntity zone = cfDnsClient.zoneGet("example.com");
System.out.println("Zone ID: " + zone.getId());
```
---
### `recordList`
Retrieve records for a given zone. There are two variants:
#### List by SLD
Retrieve all records for a specific second-level domain (SLD) under a given zone.
- **Parameters**:
- `ZoneEntity zone` - The zone object.
- `String sld` - Second-level domain (e.g., "www" in "www.example.com").
- **Returns**: A list of `RecordEntity` objects.
```java
List<RecordEntity> records = cfDnsClient.recordList(zone, "sld");
records.forEach(record ->
System.out.println("Record Type: "+record.getType()
+", Value: "+record.getContent())
);
```
#### List by Zone (optional filtering)
Retrieve DNS record details for a zone or a specific SLD, optionally filtered by record types.
- **Parameters**:
- `ZoneEntity zone` - The zone object.
- `String sld` - (Optional) The second-level domain.
- `RecordType... types` - Optional record types to filter by (e.g., A, CNAME). If not specified, returns all record types.
- **Returns**: A list of `RecordEntity` objects matching the criteria.
```java
// Get all records for a specific SLD
List<RecordEntity> allSldRecords = cfDnsClient.recordList(zone, "www");
// Get only A records for a specific SLD
List<RecordEntity> aSldRecords = cfDnsClient.recordList(zone, "www", RecordType.A);
// Get all records of a zone
List<RecordEntity> allZoneRecords = cfDnsClient.recordList(zone);
// Get only A and AAAA records of a zone
List<RecordEntity> ipZoneRecords = cfDnsClient.recordList(zone, RecordType.A, RecordType.AAAA);
```
---
### `recordCreate`
Create a new DNS record in a specific zone.
- **Parameters**:
- `ZoneEntity zone` - DNS zone object.
- `String sld` - The sub-tld of the new record.
- `int ttl` - The time-to-live in seconds of the new rcord.
```java
RecordEntity created = client.recordCreateSld(zone, "api", 60, RecordType.A, "192.168.1.1");
System.out.println("Created Record ID: " + created.getId());
```
---
### `recordUpdate`
Update an existing DNS record.
- **Parameters**:
- `ZoneEntity zone` - The zone that contains the record.
- `RecordEntity rec` - Updated record data.
```java
record.setContent("192.168.1.2");
RecordEntity updated = cfDnsClient.recordUpdate(zone, record);
System.out.println("Updated Record: " + updated.getContent());
```
---
### `recordDelete`
Delete a DNS record from a zone.
- **Parameters**:
- `ZoneEntity zone` - The parent zone.
- `RecordEntity rec` - Record to delete.
```java
boolean isDeleted = cfDnsClient.recordDelete(zone, record);
System.out.println(isDeleted ? "Deletion successful." : "Deletion failed.");
```
---
### `recordDeleteTypeIfExists`
Delete a DNS record of a specific type if it exists.
- **Parameters**:
- `ZoneEntity zone` - Target zone.
- `String sld` - Second-level domain.
- `RecordType type` - Record type.
```java
cfDnsClient.recordDeleteTypeIfExists(zone, "api", RecordType.A);
System.out.println("Deletion attempt completed.");
```
### Batch Operations
Process multiple DNS record operations (POST, PUT, PATCH, DELETE) in a single batch request.
- **Parameters**:
- `ZoneEntity zone` - The target zone.
- `List<RecordEntity> postRecords` - Records to create (nullable). Can be built without IDs.
- `List<RecordEntity> putRecords` - Records to fully replace (nullable). **Requires record IDs**.
- `List<RecordEntity> patchRecords` - Records to partially update (nullable). **Requires record IDs**.
- `List<RecordEntity> deleteRecords` - Records to delete (nullable). **Requires record IDs**.
- **Returns**: A `BatchEntry` object containing the processed records.
**Important**: For UPDATE (PATCH), REPLACE (PUT), and DELETE operations, you must first retrieve the existing records to obtain their IDs.
#### Batch Create (POST)
Create new records, IDs are not required:
```java
List<RecordEntity> newRecords = Arrays.asList(
RecordEntity.build("cdn." + zone.getName(), RecordType.A, 60, "192.168.1.11"),
RecordEntity.build("mail." + zone.getName(), RecordType.A, 60, "192.168.1.12")
);
BatchEntry result = cfDnsClient.recordBatch(zone, newRecords, null, null, null);
System.out.println("Created "+result.getPosts().size() +" records.");
```
#### Batch Update (PATCH)
Partially update existing records. **Record IDs are required** - fetch them first:
```java
// Step 1: Fetch existing records to get their IDs
List<RecordEntity> recordsToUpdate = new ArrayList<>();
recordsToUpdate.add(cfDnsClient.recordList(zone, "api",RecordType.A).get(0));
recordsToUpdate.add(cfDnsClient.recordList(zone, "cdn",RecordType.A).get(0));
// Step 2: Modify only the fields you want to update
recordsToUpdate.forEach(record ->record.setContent("192.168.2.10"));
// Step 3: Send batch update request
BatchEntry result = cfDnsClient.recordBatch(zone, null, null, recordsToUpdate, null);
System.out.println("Updated "+result.getPatches().size() +" records.");
```
#### Batch Replace (PUT)
Fully replace existing records. **Record IDs are required** - fetch them first:
```java
// Step 1: Fetch existing records to get their IDs
List<RecordEntity> recordsToReplace = new ArrayList<>();
recordsToReplace.add(cfDnsClient.recordList(zone, "mail",RecordType.A).get(0));
recordsToReplace.add(cfDnsClient.recordList(zone, "cdn",RecordType.A).get(0));
// Step 2: Modify all fields as needed (full replacement)
recordsToReplace.get(0).setContent("192.168.3.10");
recordsToReplace.get(0).setTtl(120);
recordsToReplace.get(1).setContent("192.168.3.11");
recordsToReplace.get(1).setTtl(120);
// Step 3: Send batch replace request
BatchEntry result = cfDnsClient.recordBatch(zone, null, recordsToReplace, null, null);
System.out.println("Replaced "+result.getPuts().size() +" records.");
```
#### Batch Delete
Delete existing records. **Record IDs are required** - fetch them first:
```java
// Step 1: Fetch existing records to get their IDs
List<RecordEntity> recordsToDelete = new ArrayList<>();
recordsToDelete.add(cfDnsClient.recordList(zone, "cdn",RecordType.A).get(0));
recordsToDelete.add(cfDnsClient.recordList(zone, "mail",RecordType.A).get(0));
// Step 2: Send batch delete request
BatchEntry result = cfDnsClient.recordBatch(zone, null, null, null, recordsToDelete);
System.out.println("Deleted "+recordsToDelete.size() +" records.");
```
#### Combined Batch Operations
Combine multiple operations in a single batch request:
```java
// Create new records (no IDs needed)
List<RecordEntity> newRecords = Arrays.asList(
RecordEntity.build("new-api." + zone.getName(), RecordType.A, 60, "192.168.1.100")
);
// Fetch existing records for update/delete (IDs required)
List<RecordEntity> recordsToUpdate = Arrays.asList(
cfDnsClient.recordList(zone, "existing-api", RecordType.A).get(0)
);
recordsToUpdate.get(0).setContent("192.168.1.200");
List<RecordEntity> recordsToDelete = Arrays.asList(
cfDnsClient.recordList(zone, "old-api", RecordType.A).get(0)
);
// Execute all operations in a single batch request
BatchEntry result = cfDnsClient.recordBatch(
zone,
newRecords, // POST - create new records
null, // PUT - not used in this example
recordsToUpdate, // PATCH - update existing records
recordsToDelete // DELETE - remove records
);
System.out.println("Batch completed:");
System.out.println(" Created: "+result.getPosts().size());
System.out.println(" Updated: "+result.getPatches().size());
System.out.println(" Deleted: "+result.getDeletes().size());
```
---
## Fluent API
The fluent API provides a chainable, readable interface for DNS operations. It's an alternative to the traditional API
that reduces verbosity and improves code readability.
### Basic Usage
```java
// Create a DNS record
client.zone("example.com")
.record("api")
.create(RecordType.A, "192.168.1.1",60);
// Get DNS records of a subdomain
List<RecordEntity> records = client.zone("example.com")
.record("www", RecordType.A)
.get();
// Get all DNS records of a zone
List<RecordEntity> zoneRecords = client.zone("example.com")
.list(RecordType.A, RecordType.AAAA);
// Update a DNS record
RecordEntity updated = client.zone("example.com")
.record("api", RecordType.A)
.update("192.168.1.2");
// Delete DNS records
client.zone("example.com")
.record("old-service")
.delete(RecordType.A, RecordType.AAAA);
```
### Advantages of Fluent API
- **More Readable**: The chain of method calls reads like natural language
- **Less Verbose**: No need to pass zone objects between method calls
- **Type Safe**: Full compile-time type checking
- **IDE Friendly**: Excellent autocomplete support
### Complete Example
```java
CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("your-api-token")
.build();
// Create a new record
client.zone("example.com")
.record("api")
.create(RecordType.A, "192.168.100.1",60);
// Retrieve and verify
List<RecordEntity> records = client.zone("example.com")
.record("api", RecordType.A)
.get();
System.out.println("IP: "+records.get(0).getContent());
// Update the record
client.zone("example.com")
.record("api",RecordType.A)
.update("192.168.100.2");
// Clean up
client.zone("example.com")
.record("api")
.delete(RecordType.A);
```
---
# Notes on Error Handling
The `CfDnsClient` provides internal error-handling mechanisms through exceptions. For example:
- `CloudflareApiException` is thrown for errors during API communication or invalid responses.
- `CloudflareNotFoundException` is thrown when the requested resource (single or multiple) is not found, if enabled via the `emptyResultThrowsException` flag during initialization. **Default is `false`**, meaning empty results will be returned without throwing an exception.
To enable exception throwing for empty results:
```java
CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("your-api-token")
.withEmptyResultThrowsException(true)
.build();
```
## Example:
```java
try {
List<RecordEntity> records = cfDnsClient.recordList(zone, "www", RecordType.A);
System.out.println("Record IP: "+records.get(0).getContent());
} catch (CloudflareApiException e) {
if (e instanceof CloudflareNotFoundException) {
log.warn("Sld not found: www");
} else {
log.error("Error while getting sld info of www", e);
throw e;
}
}
```
---
# Summary
`CfDnsClient` offers a simple interface for managing DNS entries via Cloudflare's public API, allowing seamless CRUD operations and automation-friendly workflows.
This project focuses on minimalistic Cloudflare API access, enabling DNS management tasks such as creating, updating, and deleting records for types like A, CNAME, MX, TXT, and others.
-15
View File
@@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cloudflare DNS Client - java</title>
</head>
<body>
<h1>Cloudflare DNS Client - java</h1>
<p>A Java-based client for interacting with the Cloudflare DNS API.</p>
<p>
<a href="apidocs/index.html" target="_blank">View API Documentation</a>
</p>
</body>
</html>
-284
View File
@@ -1,284 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>codes.thischwa</groupId>
<artifactId>cloudflaredns</artifactId>
<version>0.3.0</version>
<name>CloudflareDNS-java</name>
<inceptionYear>2025</inceptionYear>
<packaging>jar</packaging>
<issueManagement>
<url>https://gitlab.com/th-schwarz/CloudflareDNS-java/-/issues</url>
<system>GitLab Issues</system>
</issueManagement>
<properties>
<java.version>17</java.version>
<file.encoding>UTF-8</file.encoding>
<project.build.sourceEncoding>${file.encoding}</project.build.sourceEncoding>
<project.reporting.outputEncoding>${file.encoding}</project.reporting.outputEncoding>
<!-- checkstyle -->
<checkstyle.version>12.3.0</checkstyle.version>
<checkstyle.plugin.version>3.6.0</checkstyle.plugin.version>
<checkstyle.config.location>${project.basedir}/src/checkstyle/google_custom_checks.xml
</checkstyle.config.location>
<checkstyle.includeTestResources>false</checkstyle.includeTestResources>
<checkstyle.violationSeverity>warning</checkstyle.violationSeverity>
<checkstyle.failOnViolation>false</checkstyle.failOnViolation>
<checkstyle.propertyExpansion>config_loc=${basedir}
</checkstyle.propertyExpansion>
<checkstyle.consoleOutput>true</checkstyle.consoleOutput>
<linkXRef>false</linkXRef>
<!-- 3rd party dependencies -->
<jackson.version>2.19.1</jackson.version>
<httpclient5.version>5.5.1</httpclient5.version>
<lombok.version>1.18.36</lombok.version>
<slf4j.version>2.0.17</slf4j.version>
<logback-classic.version>1.5.18</logback-classic.version>
<junit5.version>5.14.2</junit5.version>
<mockito-junit5.version>5.21.0</mockito-junit5.version>
<!-- sonarqube -->
<sonar.organization>th-schwarz</sonar.organization>
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
<sonar.sourceEncoding>${file.encoding}</sonar.sourceEncoding>
<sonar.projectKey>th-schwarz_CloudflareDNS-java</sonar.projectKey>
<sonar.projectName>CloudflareDNS-java</sonar.projectName>
<sonar.branch.name>develop</sonar.branch.name>
<sonar.test.exclusions>src/test/java/**/*</sonar.test.exclusions>
<lombok-maven-plugin.version>1.18.20.0</lombok-maven-plugin.version>
</properties>
<scm>
<developerConnection>scm:git:git@gitlab.com:th-schwarz/CloudflareDNS-java.git</developerConnection>
<connection>scm:git:git@gitlab.com:th-schwarz/CloudflareDNS-java.git</connection>
<url>https://gitlab.com/th-schwarz/CloudflareDNS-java</url>
<tag>v0.3.0</tag>
</scm>
<distributionManagement>
<repository>
<id>gitlab-cloudflaredns</id>
<name>GitLab Maven Packages</name>
<url>https://gitlab.com/api/v4/projects/68509751/packages/maven</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</distributionManagement>
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>${httpclient5.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>24.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit5.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito-junit5.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback-classic.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.11.2</version>
<configuration>
<failOnError>false</failOnError>
<locale>en</locale>
<source>${java.version}</source>
</configuration>
<executions>
<execution>
<id>build javadoc jar</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<tagNameFormat>v@{project.version}</tagNameFormat>
<arguments>-DskipTests</arguments>
</configuration>
</plugin>
<plugin>
<!-- checkstyle -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${checkstyle.plugin.version}</version>
<executions>
<execution>
<id>checkstyle-validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>${checkstyle.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<!-- generates the code coverage report for sonar cube -->
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.13</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report-code-coverage</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<formats>XML</formats>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<!-- to export test summary -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.3</version>
</plugin>
<plugin>
<!-- delombok -->
<groupId>org.projectlombok</groupId>
<artifactId>lombok-maven-plugin</artifactId>
<version>${lombok-maven-plugin.version}</version>
<executions>
<execution>
<id>delombok-sources</id>
<phase>generate-sources</phase>
<goals>
<goal>delombok</goal>
</goals>
</execution>
</executions>
<configuration>
<sourceDirectory>src/main/java</sourceDirectory>
<outputDirectory>${project.build.directory}/delombok</outputDirectory>
<addOutputDirectory>false</addOutputDirectory>
</configuration>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<executions>
<execution>
<id>attach-delomboked-sources</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<classesDirectory>${project.build.directory}/delombok</classesDirectory>
<classifier>sources</classifier>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
-440
View File
@@ -1,440 +0,0 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<!--
Checkstyle configuration that checks the Google coding conventions from Google Java Style
that can be found at https://google.github.io/styleguide/javaguide.html
Checkstyle is very configurable. Be sure to read the documentation at
http://checkstyle.org (or in your downloaded distribution).
To completely disable a check, just comment it out or delete it from the file.
To suppress certain violations please review suppression filters.
Authors: Max Vetrenko, Mauryan Kansara, Ruslan Diachenko, Roman Ivanov.
-->
<module name="Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="${org.checkstyle.google.severity}" default="warning"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Excludes all 'module-info.java' files -->
<!-- See https://checkstyle.org/filefilters/index.html -->
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<module name="SuppressWarningsFilter"/>
<!-- https://checkstyle.org/filters/suppressionfilter.html -->
<module name="SuppressionFilter">
<property name="file" value="${org.checkstyle.google.suppressionfilter.config}"
default="checkstyle-suppressions.xml" />
<property name="optional" value="true"/>
</module>
<!-- https://checkstyle.org/filters/suppresswithnearbytextfilter.html -->
<module name="SuppressWithNearbyTextFilter">
<property name="nearbyTextPattern"
value="CHECKSTYLE.SUPPRESS\: (\w+) for ([+-]\d+) lines"/>
<property name="checkPattern" value="$1"/>
<property name="lineRange" value="$2"/>
</module>
<!-- Checks for whitespace -->
<!-- See http://checkstyle.org/checks/whitespace/index.html -->
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
</module>
<module name="LineLength">
<property name="fileExtensions" value="java"/>
<property name="max" value="140"/>
<property name="ignorePattern"
value="^package.*|^import.*|href\s*=\s*&quot;[^&quot;]*&quot;|http://|https://|ftp://"/>
</module>
<module name="TreeWalker">
<module name="OuterTypeFilename"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
<property name="format"
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
<property name="message"
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<module name="AvoidStarImport"/>
<module name="OneTopLevelClass"/>
<module name="NoLineWrap">
<property name="tokens" value="PACKAGE_DEF, IMPORT, STATIC_IMPORT"/>
</module>
<module name="NeedBraces">
<property name="tokens"
value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE"/>
</module>
<module name="LeftCurly">
<property name="id" value="LeftCurlyEol"/>
<property name="tokens"
value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,
INTERFACE_DEF, LAMBDA, LITERAL_CATCH,
LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,
OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="LeftCurly">
<property name="id" value="LeftCurlyNl"/>
<property name="option" value="nl"/>
<property name="tokens"
value="LITERAL_CASE, LITERAL_DEFAULT"/>
</module>
<module name="SuppressionXpathSingleFilter">
<!-- LITERAL_CASE, LITERAL_DEFAULT are reused in SWITCH_RULE -->
<property name="id" value="LeftCurlyNl"/>
<property name="query" value="//SWITCH_RULE/SLIST"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlySame"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_CATCH, LITERAL_IF, LITERAL_ELSE,
LITERAL_DO"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlyAlone"/>
<property name="option" value="alone"/>
<property name="tokens"
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, INTERFACE_DEF, RECORD_DEF,
COMPACT_CTOR_DEF, LITERAL_SWITCH, LITERAL_CASE, LITERAL_FINALLY"/>
</module>
<module name="SuppressionXpathSingleFilter">
<!-- suppression is required till https://github.com/checkstyle/checkstyle/issues/7541 -->
<property name="id" value="RightCurlyAlone"/>
<property name="query" value="//RCURLY[parent::SLIST[count(./*)=1]
or preceding-sibling::*[last()][self::LCURLY]]"/>
</module>
<module name="WhitespaceAfter">
<property name="tokens"
value="COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE, LITERAL_RETURN,
LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, LITERAL_FINALLY, DO_WHILE, ELLIPSIS,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_CATCH, LAMBDA,
LITERAL_YIELD, LITERAL_CASE, LITERAL_WHEN"/>
</module>
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true"/>
<property name="allowEmptyLambdas" value="true"/>
<property name="allowEmptyMethods" value="true"/>
<property name="allowEmptyTypes" value="true"/>
<property name="allowEmptyLoops" value="true"/>
<property name="allowEmptySwitchBlockStatements" value="true"/>
<property name="ignoreEnhancedForColon" value="false"/>
<property name="tokens"
value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,
BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,
LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,
LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,
LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,
NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,
SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT,
TYPE_EXTENSION_AND, LITERAL_WHEN"/>
<message key="ws.notFollowed"
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks
may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
<message key="ws.notPreceded"
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="WhitespaceAround"/>
<property name="query" value="//*[self::LITERAL_IF or self::LITERAL_ELSE or self::STATIC_INIT
or self::LITERAL_TRY or self::LITERAL_CATCH]/SLIST[count(./*)=1]
| //*[self::STATIC_INIT or self::LITERAL_TRY or self::LITERAL_IF]
//*[self::RCURLY][parent::SLIST[count(./*)=1]]"/>
</module>
<module name="RegexpSinglelineJava">
<property name="format" value="\{[ ]+\}"/>
<property name="message" value="Empty blocks should have no spaces. Empty blocks
may only be represented as '{}' when not part of a
multi-block statement (4.1.3)"/>
</module>
<module name="OneStatementPerLine"/>
<module name="MultipleVariableDeclarations"/>
<module name="ArrayTypeStyle"/>
<module name="MissingSwitchDefault"/>
<module name="FallThrough"/>
<module name="UpperEll"/>
<module name="ModifierOrder"/>
<module name="EmptyLineSeparator">
<property name="tokens"
value="PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF, RECORD_DEF,
COMPACT_CTOR_DEF"/>
<property name="allowNoEmptyLineBetweenFields" value="true"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapDot"/>
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapComma"/>
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/259 -->
<property name="id" value="SeparatorWrapEllipsis"/>
<property name="tokens" value="ELLIPSIS"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/258 -->
<property name="id" value="SeparatorWrapArrayDeclarator"/>
<property name="tokens" value="ARRAY_DECLARATOR"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapMethodRef"/>
<property name="tokens" value="METHOD_REF"/>
<property name="option" value="nl"/>
</module>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern"
value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
ANNOTATION_DEF, RECORD_DEF"/>
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MemberName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LambdaParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="CatchParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LocalVariableName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="PatternVariableName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Pattern variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="RecordComponentName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Record component name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="RecordTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Record type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="InterfaceTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="NoFinalizer"/>
<module name="GenericWhitespace">
<message key="ws.followed"
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded"
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow"
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded"
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<module name="Indentation">
<property name="basicOffset" value="2"/>
<property name="braceAdjustment" value="2"/>
<property name="caseIndent" value="2"/>
<property name="throwsIndent" value="4"/>
<property name="lineWrappingIndentation" value="4"/>
<property name="arrayInitIndent" value="2"/>
</module>
<!-- Suppression for block code until we find a way to detect them properly
until https://github.com/checkstyle/checkstyle/issues/15769 -->
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="Indentation"/>
<property name="query" value="//SLIST[not(parent::CASE_GROUP)]/SLIST
| //SLIST[not(parent::CASE_GROUP)]/SLIST/RCURLY"/>
</module>
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="0"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,
PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF,
RECORD_COMPONENT_DEF"/>
</module>
<module name="NoWhitespaceBeforeCaseDefaultColon"/>
<module name="OverloadMethodsDeclarationOrder"/>
<module name="ConstructorsDeclarationGrouping"/>
<module name="VariableDeclarationUsageDistance"/>
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="true"/>
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
<property name="tokens" value="IMPORT, STATIC_IMPORT, PACKAGE_DEF"/>
</module>
<module name="MethodParamPad">
<property name="tokens"
value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF, CTOR_CALL,
SUPER_CTOR_CALL, ENUM_CONSTANT_DEF, RECORD_DEF, RECORD_PATTERN_DEF"/>
</module>
<module name="NoWhitespaceBefore">
<property name="tokens"
value="COMMA, SEMI, POST_INC, POST_DEC, DOT,
LABELED_STAT, METHOD_REF"/>
<property name="allowLineBreaks" value="true"/>
</module>
<module name="ParenPad">
<property name="tokens"
value="ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,
EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,
METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA,
RECORD_DEF, RECORD_PATTERN_DEF"/>
</module>
<module name="OperatorWrap">
<property name="option" value="NL"/>
<property name="tokens"
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF,
TYPE_EXTENSION_AND "/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF,
RECORD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="NonEmptyAtclauseDescription"/>
<module name="InvalidJavadocPosition"/>
<module name="JavadocTagContinuationIndentation"/>
<module name="SummaryJavadoc">
<property name="forbiddenSummaryFragments"
value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
</module>
<module name="JavadocParagraph">
<property name="allowNewlineParagraph" value="false"/>
</module>
<module name="RequireEmptyLineBeforeBlockTagGroup"/>
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="JavadocMethod">
<property name="accessModifiers" value="public"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="MissingJavadocMethod">
<property name="scope" value="protected"/>
<property name="allowMissingPropertyJavadoc" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF,
COMPACT_CTOR_DEF"/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="MissingJavadocMethod"/>
<property name="query" value="//*[self::METHOD_DEF or self::CTOR_DEF
or self::ANNOTATION_FIELD_DEF or self::COMPACT_CTOR_DEF]
[ancestor::*[self::INTERFACE_DEF or self::CLASS_DEF
or self::RECORD_DEF or self::ENUM_DEF]
[not(./MODIFIERS/LITERAL_PUBLIC)]]"/>
</module>
<module name="MissingJavadocType">
<property name="scope" value="protected"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
RECORD_DEF, ANNOTATION_DEF"/>
<property name="excludeScope" value="nothing"/>
</module>
<module name="MethodName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="MethodName"/>
<property name="query" value="//METHOD_DEF[
./MODIFIERS/ANNOTATION//IDENT[contains(@text, 'Test')]
]/IDENT"/>
<property name="message" value="'[a-z][a-z0-9][a-zA-Z0-9]*(?:_[a-z][a-z0-9][a-zA-Z0-9]*)*'"/>
</module>
<module name="SingleLineJavadoc"/>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected"/>
</module>
<module name="CommentsIndentation">
<property name="tokens" value="SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN"/>
</module>
<!-- https://checkstyle.org/filters/suppressionxpathfilter.html -->
<module name="SuppressionXpathFilter">
<property name="file" value="${org.checkstyle.google.suppressionxpathfilter.config}"
default="checkstyle-xpath-suppressions.xml" />
<property name="optional" value="true"/>
</module>
<module name="SuppressWarningsHolder" />
<module name="SuppressionCommentFilter">
<property name="offCommentFormat" value="CHECKSTYLE.OFF\: ([\w\|]+)" />
<property name="onCommentFormat" value="CHECKSTYLE.ON\: ([\w\|]+)" />
<property name="checkFormat" value="$1" />
</module>
<module name="SuppressWithNearbyCommentFilter">
<property name="commentFormat" value="CHECKSTYLE.SUPPRESS\: ([\w\|]+)"/>
<!-- $1 refers to the first match group in the regex defined in commentFormat -->
<property name="checkFormat" value="$1"/>
<!-- The check is suppressed in the next line of code after the comment -->
<property name="influenceFormat" value="1"/>
</module>
</module>
</module>
@@ -1,193 +0,0 @@
package codes.thischwa.cf;
import codes.thischwa.cf.model.AbstractResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.charset.StandardCharsets;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPatch;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpPut;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
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
* methods for handling GET and POST requests and includes utilities for constructing HTTP clients,
* managing authentication, and handling JSON serialization.
*/
@Slf4j
abstract class CfBasicHttpClient {
private final String baseUrl;
private final CfDnsClientBuilder.CfAuth auth;
private final ObjectMapper objectMapper;
/**
* Creates a new Cloudflare HTTP client with the specified base URL and authentication.
*
* @param baseUrl the base URL for the Cloudflare API
* @param auth the authentication mechanism to use
*/
CfBasicHttpClient(@NotNull String baseUrl, @NotNull CfDnsClientBuilder.CfAuth auth) {
this.baseUrl = baseUrl;
this.auth = auth;
this.objectMapper = JsonConf.initObjectMapper();
}
private CloseableHttpClient createHttpClient() {
return HttpClients.custom().addRequestInterceptorFirst((request, context, execChain) -> {
request.addHeader(HttpHeaders.ACCEPT_CHARSET, "UTF-8");
request.addHeader(HttpHeaders.ACCEPT_ENCODING, "gzip");
request.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.getMimeType());
request.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
if (request instanceof ClassicHttpRequest classicRequest) {
auth.applyAuth(classicRequest);
}
}).build();
}
private <T extends AbstractResponse> T executeRequest(ClassicHttpRequest request,
Class<T> responseType)
throws CloudflareApiException {
String logUri = null;
try (CloseableHttpClient client = createHttpClient()) {
ResultWrapper result = client.execute(request,
(ClassicHttpResponse response) -> new ResultWrapper(response.getCode(),
EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)));
T respObj = objectMapper.readValue(result.responseBody, responseType);
if (!respObj.getResponseResultInfo().isSuccess()) {
log.error("API error.");
StringBuilder errorMessage = new StringBuilder("API error");
if (!respObj.getResponseResultInfo().getErrors().isEmpty()) {
errorMessage.append(": ");
respObj.getResponseResultInfo().getErrors().forEach(e -> {
log.error(" - {}", e.toString());
errorMessage.append(e).append("; ");
});
// Remove trailing "; "
if (errorMessage.toString().endsWith("; ")) {
errorMessage.setLength(errorMessage.length() - 2);
}
}
throw new CloudflareApiException(errorMessage.toString());
}
logUri = request.getRequestUri();
if (result.statusCode >= 200 && result.statusCode < 300) {
return respObj;
} else {
log.error("{} request failed for URL {}: Status {}", request.getMethod(), request.getUri(),
result.statusCode);
throw new CloudflareApiException(
request.getMethod() + " request failed with status code: " + result.statusCode);
}
} catch (JsonProcessingException e) {
log.error("JSON parsing error for request to {}", logUri, e);
throw new CloudflareApiException("Error processing JSON response", e);
} catch (Exception e) {
throw new CloudflareApiException("Server error!", e);
}
}
/**
* Sends a GET request to the given endpoint and maps the response.
*/
<T extends AbstractResponse> T getRequest(String endpoint, Class<T> responseType)
throws CloudflareApiException {
HttpGet request = new HttpGet(buildUrl(endpoint));
return executeRequest(request, responseType);
}
/**
* Sends a DELETE request to the given endpoint and maps the response.
*
* @param endpoint the API endpoint path
* @param responseType the expected response type class
* @param <T> the response type extending AbstractResponse
* @return the parsed response object
* @throws CloudflareApiException if an error occurs during the request
*/
<T extends AbstractResponse> T deleteRequest(String endpoint, Class<T> responseType) throws CloudflareApiException {
HttpDelete request = new HttpDelete(buildUrl(endpoint));
return executeRequest(request, responseType);
}
/**
* Sends a POST request with a payload to the given endpoint and maps the response.
*/
<T extends AbstractResponse> T postRequest(String endpoint,
Object requestPayload,
Class<T> responseType)
throws CloudflareApiException {
HttpPost request = new HttpPost(buildUrl(endpoint));
setRequestPayload(request, requestPayload);
return executeRequest(request, responseType);
}
/**
* Sends a PUT request with a payload to the given endpoint and maps the response.
*/
<T extends AbstractResponse> T putRequest(String endpoint,
Object requestPayload,
Class<T> responseType)
throws CloudflareApiException {
HttpPut request = new HttpPut(buildUrl(endpoint));
setRequestPayload(request, requestPayload);
return executeRequest(request, responseType);
}
/**
* Sends a PATCH request with a payload to the given endpoint and maps the response.
*
* @param endpoint the API endpoint path
* @param requestPayload the payload to send
* @param responseType the expected response type class
* @param <T> the response type extending AbstractResponse
* @return the parsed response object
* @throws CloudflareApiException if an error occurs during the request
*/
<T extends AbstractResponse> T patchRequest(String endpoint,
Object requestPayload,
Class<T> responseType)
throws CloudflareApiException {
HttpPatch request = new HttpPatch(buildUrl(endpoint));
setRequestPayload(request, requestPayload);
return executeRequest(request, responseType);
}
/**
* Sets the JSON payload for a request.
*/
private void setRequestPayload(BasicClassicHttpRequest request,
Object requestPayload)
throws CloudflareApiException {
try {
String jsonPayload = objectMapper.writeValueAsString(requestPayload);
log.trace("Request methode [{}] payload: {}", request.getMethod(), jsonPayload);
request.setEntity(new StringEntity(jsonPayload,
ContentType.APPLICATION_JSON));
} catch (JsonProcessingException e) {
throw new CloudflareApiException("Error serializing JSON payload", e);
}
}
private String buildUrl(String endpoint) {
return baseUrl + endpoint;
}
private record ResultWrapper(int statusCode, String responseBody) {
}
}
@@ -1,496 +0,0 @@
package codes.thischwa.cf;
import codes.thischwa.cf.fluent.ZoneOperations;
import codes.thischwa.cf.fluent.ZoneOperationsImpl;
import codes.thischwa.cf.model.AbstractResponse;
import codes.thischwa.cf.model.BatchEntry;
import codes.thischwa.cf.model.BatchResponse;
import codes.thischwa.cf.model.PagingRequest;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordMultipleResponse;
import codes.thischwa.cf.model.RecordSingleResponse;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import codes.thischwa.cf.model.ZoneMultipleResponse;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
/**
* CfDnsClient is a client interface to interact with Cloudflare DNS service. It allows managing DNS
* records and zones within the Cloudflare system, including creating, updating, retrieving, and
* deleting DNS records.
*
* <p>Example with API token authentication (recommended):
* <pre><code>
* // Create a new CfDnsClient instance with API token
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
* .withApiTokenAuth("your-api-token")
* .build();
*
* // Retrieve a zone
* ZoneEntity zone = cfDnsClient.zoneGet("example.com");
* System.out.println("Zone ID: " + zone.getId());
*
* // Retrieve records of a subdomain
* List&lt;RecordEntity&gt; records = cfDnsClient.recordList(zone, "sld");
* records.forEach(record ->
* System.out.println("Record Type: " + record.getType() + ", Value: " + record.getContent())
* );
*
* // Create a record for the subdomain "api"
* RecordEntity created = cfDnsClient.recordCreateSld(zone, "api", 60, RecordType.A, "192.168.1.10");
* System.out.println("Created Record ID: " + created.getId());
* </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
public class CfDnsClient extends CfBasicHttpClient {
private final ResponseValidator responseValidator;
private final boolean emptyResultThrowsException;
/**
* 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 baseUrl The base URL for the Cloudflare API endpoint.
* @param auth The authentication mechanism to use (ApiTokenAuth or EmailKeyAuth)
*/
CfDnsClient(boolean emptyResultThrowsException, String baseUrl, CfDnsClientBuilder.CfAuth auth) {
super(baseUrl, auth);
this.responseValidator = new ResponseValidator(emptyResultThrowsException);
this.emptyResultThrowsException = emptyResultThrowsException;
}
private static String buildFqdn(ZoneEntity zone, String sld) {
return sld + "." + zone.getName();
}
/**
* Groups a list of DNS records by their fully qualified domain name (FQDN).
*
* @param records A list of {@link RecordEntity} objects to be grouped by FQDN.
* @return A map where the key is the FQDN (name field) and the value is a list of {@link RecordEntity}
* objects that share that FQDN.
*/
public static Map<String, List<RecordEntity>> groupRecordsByFqdn(List<RecordEntity> records) {
if (records == null) {
return new HashMap<>();
}
return records.stream()
.collect(Collectors.groupingBy(RecordEntity::getName));
}
/**
* Provides fluent API access to operations on a specific zone.
* This method returns a ZoneOperations interface that allows chaining operations
* on DNS records within the specified zone.
*
* <p>Example:
* <pre><code>
* client.zone("example.com")
* .record("api")
* .create(RecordType.A, "192.168.1.1", 60);
* </code></pre>
*
* @param zoneName the name of the DNS zone (e.g., "example.com")
* @return a ZoneOperations instance for chaining operations
* @throws CloudflareApiException if the zone cannot be found or accessed
*/
public ZoneOperations zone(String zoneName) throws CloudflareApiException {
ZoneEntity zoneEntity = zoneGet(zoneName);
return new ZoneOperationsImpl(this, zoneEntity);
}
/**
* Retrieves a list of all zones from the Cloudflare API.
*
* @return A list of ZoneEntity objects representing the zones retrieved from the Cloudflare API.
* @throws CloudflareApiException If an error occurs during the API request or response handling.
*/
public List<ZoneEntity> zoneList() throws CloudflareApiException {
return zoneList(PagingRequest.defaultPaging());
}
/**
* Retrieves a list of all DNS zones using the provided paging request parameters.
*
* @param pagingRequest the pagination request object containing parameters for paging and
* filtering zone data
* @return a list of {@code ZoneEntity} objects representing the DNS zones retrieved from the API
* @throws CloudflareApiException if there is an error during the API request or response
* processing
*/
public List<ZoneEntity> zoneList(PagingRequest pagingRequest) throws CloudflareApiException {
String endpoint = pagingRequest.addQueryString(CfRequest.ZONE_LIST.buildPath());
ZoneMultipleResponse response = getRequest(endpoint, ZoneMultipleResponse.class);
checkResponse(response);
return response.getResult();
}
/**
* Retrieves detailed information about a specific zone by its name.
*
* @param name The name of the zone to retrieve information for.
* @return A {@link ZoneEntity} object that contains details of the specified zone.
* @throws CloudflareApiException If an error occurs while making the API request or processing
* the response.
*/
public ZoneEntity zoneGet(String name) throws CloudflareApiException {
String endpoint = CfRequest.ZONE_INFO.buildPath(name);
ZoneMultipleResponse response = getRequest(endpoint, ZoneMultipleResponse.class);
checkResponse(response, true);
return response.getResult().get(0);
}
/**
* Retrieves DNS records for the specified second-level domain (SLD) within a zone.
*
* @param zone the zone entity representing the DNS zone to query
* @param sld the second-level domain (SLD) to filter the records
* @return a list of RecordEntity objects that match the specified SLD within the zone
* @throws CloudflareNotFoundException if the specified SLD is not found in the zone
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
*/
public List<RecordEntity> recordList(ZoneEntity zone, String sld) throws CloudflareApiException {
return recordList(zone, sld, (RecordType[]) null);
}
/**
* Retrieves DNS records for the specified second-level domain (SLD) within a zone.
* Optionally filters by one or more DNS record types.
*
* @param zone The zone entity containing information about the domain zone.
* @param sld The second-level domain (SLD) for which to retrieve DNS records.
* @param types Optional parameter specifying one or more DNS record types to filter the results.
* @return A list of {@code RecordEntity} objects representing the DNS records for the specified domain.
* @throws CloudflareNotFoundException if the specified SLD is not found in the zone
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
*/
public List<RecordEntity> recordList(ZoneEntity zone, String sld, @Nullable RecordType... types)
throws CloudflareApiException {
PagingRequest pagingRequest = PagingRequest.defaultPaging();
List<RecordEntity> recs = recordList(zone, sld, pagingRequest);
return filterAndSetZoneRecords(zone, types, recs);
}
/**
* Retrieves all record entities for a specific second-level domain (SLD) within a given DNS
* zone using the provided paging request parameters.
*
* @param zone The DNS zone entity for which the SLD records are to be fetched.
* @param sld The second-level domain name for which the records are retrieved.
* @param pagingRequest The paging request.
* @return A list of {@code RecordEntity} associated with the desired SLD.
* @throws CloudflareApiException If an error occurs while interacting with the Cloudflare API.
*/
public List<RecordEntity> recordList(ZoneEntity zone, String sld, PagingRequest pagingRequest)
throws CloudflareApiException {
String fqdn = buildFqdn(zone, sld);
String endpoint =
pagingRequest.addQueryString(CfRequest.RECORD_INFO_NAME.buildPath(zone.getId(), fqdn));
RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class);
checkResponse(resp);
return resp.getResult();
}
/**
* Retrieves a list of all DNS records for a given zone.
* Optionally filters by one or more DNS record types.
*
* @param zone The zone entity containing information about the domain zone.
* @param types Optional parameter specifying one or more DNS record types to filter the results.
* @return A list of {@code RecordEntity} objects representing the DNS records for the specified zone.
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
*/
public List<RecordEntity> recordList(ZoneEntity zone, RecordType... types)
throws CloudflareApiException {
String endpoint = CfRequest.RECORD_LIST.buildPath(zone.getId());
RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class);
checkResponse(resp, false);
List<RecordEntity> recs = resp.getResult();
return filterAndSetZoneRecords(zone, types, recs);
}
/**
* Creates a new DNS record for a given second-level domain (SLD) within the specified zone.
*
* @param zone The ZoneEntity representing the DNS zone where the record is to be created.
* @param sld The second-level domain (SLD) for which the DNS record is being created.
* @param ttl The time-to-live (TTL) value for the DNS record in seconds.
* @param type The RecordType specifying the type of the DNS record (e.g., A, AAAA, CNAME).
* @param content The content of the DNS record (e.g., IP address for A/AAAA records, target
* domain for CNAME).
* @return The created RecordEntity object containing details of the newly created DNS record.
* @throws CloudflareApiException If an error occurs while communicating with the Cloudflare API
* or creating the record.
*/
public RecordEntity recordCreateSld(ZoneEntity zone, String sld, int ttl, RecordType type,
String content) throws CloudflareApiException {
String fqdn = buildFqdn(zone, sld);
return recordCreate(zone, fqdn, ttl, type, content);
}
/**
* Creates a DNS record in the specified DNS zone with the provided details.
*
* @param zone the DNS zone in which the record will be created
* @param name the name of the DNS record (e.g., www.example.com)
* @param ttl the time-to-live (TTL) value for the DNS record
* @param type the type of the DNS record (e.g., A, AAAA, CNAME)
* @param content the content or value of the DNS record
* @return the created DNS record as a {@link RecordEntity} object
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
*/
public RecordEntity recordCreate(ZoneEntity zone, String name, int ttl, RecordType type,
String content) throws CloudflareApiException {
RecordEntity rec = RecordEntity.build(name, type, ttl, content);
return recordCreate(zone, rec);
}
/**
* Creates a new DNS record in the specified zone using the Cloudflare API.
*
* @param zone The zone entity where the record will be created. Contains details such as zone
* ID.
* @param rec The record entity representing the DNS record to be created, including its
* attributes.
* @return The created record entity as returned by the Cloudflare API.
* @throws CloudflareApiException If an error occurs while interacting with the Cloudflare API.
*/
public RecordEntity recordCreate(ZoneEntity zone, RecordEntity rec)
throws CloudflareApiException {
String endpoint = CfRequest.RECORD_CREATE.buildPath(zone.getId());
RecordSingleResponse resp = postRequest(endpoint, rec, RecordSingleResponse.class);
checkResponse(resp);
log.info("Record {} of type {} successful created.", rec.getSld(), rec.getType());
RecordEntity retRec = resp.getResult();
retRec.setZoneId(zone.getId());
return retRec;
}
/**
* Deletes a DNS record of the specified type within a given zone on the Cloudflare API.
*
* @param zone The zone entity that specifies the zone in which the record exists.
* @param rec The record entity that represents the DNS record to be deleted.
* @return {@code true} if the DNS record was successfully deleted; {@code false} otherwise.
* @throws CloudflareApiException if there is an issue during the API communication, or the
* request fails for any reason.
*/
public boolean recordDelete(ZoneEntity zone, RecordEntity rec) throws CloudflareApiException {
boolean changed = recordDelete(zone, rec.getId());
if (changed) {
log.debug("Record {} of the type [{}] successful deleted.", rec.getSld(), rec.getType());
} else {
log.warn("Record {} of the type [{}] was not deleted.", rec.getSld(), rec.getType());
}
return changed;
}
/**
* Deletes a DNS record of the specified type within a given zone on the Cloudflare API.
*
* @param zone The zone entity that specifies the zone in which the record exists.
* @param id The record entity that represents the DNS record to be deleted.
* @return {@code true} if the DNS record was successfully deleted; {@code false} otherwise.
* @throws CloudflareApiException if there is an issue during the API communication or the request
* fails for any reason.
*/
public boolean recordDelete(ZoneEntity zone, String id) throws CloudflareApiException {
String endpoint = CfRequest.RECORD_DELETE.buildPath(zone.getId(), id);
RecordSingleResponse resp = deleteRequest(endpoint, RecordSingleResponse.class);
checkResponse(resp);
log.debug("Record id#{} successful deleted.", id);
return resp.getResult().getId().equals(id);
}
/**
* Updates an existing DNS record in a specified Cloudflare zone.
*
* @param zone the zone entity containing the ID of the target zone
* @param rec the record entity containing the ID of the DNS record to be updated and its updated
* data
* @return the updated record entity as returned by the Cloudflare API
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
*/
public RecordEntity recordUpdate(ZoneEntity zone, RecordEntity rec)
throws CloudflareApiException {
// reset all dates, it causes an API issue
rec.setModifiedOn(null);
rec.setCreatedOn(null);
String endpoint = CfRequest.RECORD_UPDATE.buildPath(zone.getId(), rec.getId());
RecordSingleResponse resp = patchRequest(endpoint, rec, RecordSingleResponse.class);
checkResponse(resp);
log.info("Record {} of type {} successful updated.", rec.getSld(), rec.getType());
return resp.getResult();
}
/**
* Deletes DNS records of a specific type within a given zone if they exist. If no record of the
* specified type exists, it logs this occurrence without throwing an exception.
*
* @param zone The DNS zone entity in which the record exists.
* @param sld The second-level domain for which the record is being checked.
* @param recordTypes The types of DNS records that should be deleted if they exist.
* @throws CloudflareApiException If an error occurs during API communication.
*/
public void recordDeleteTypeIfExists(ZoneEntity zone, String sld, RecordType... recordTypes)
throws CloudflareApiException {
String fqdn = buildFqdn(zone, sld);
List<RecordEntity> recs;
try {
recs = recordList(zone, sld, recordTypes);
} catch (CloudflareNotFoundException e) {
log.trace("No record of type {} found for domain {}.", recordTypes, fqdn);
return;
}
for (RecordEntity rec : recs) {
try {
recordDelete(zone, rec);
log.info("Record {} of type {} successful deleted.", fqdn, recordTypes);
} catch (CloudflareApiException e) {
log.error("Failed to delete record {} of type {} for zone {}: {}", fqdn, recordTypes, zone.getName(), e.getMessage());
}
}
}
/**
* Processes a batch of DNS record operations (POST, PUT, PATCH, DELETE) for a specified zone.
* This method builds and cleans the input records, sends the batch request to the Cloudflare API,
* and returns a result containing processed batch entries.
*
* @param zone The zone entity to which the records belong.
* @param postRecords A list of DNS records to be created (POST). This parameter is nullable.
* @param putRecords A list of DNS records to be fully replaced (PUT). This parameter is nullable.
* @param patchRecords A list of DNS records to be partially updated (PATCH). This parameter is nullable.
* @param deleteRecords A list of DNS records to be deleted (DELETE). This parameter is nullable.
* @return The resulting {@link BatchEntry} containing the processed records after the batch operation.
* @throws CloudflareApiException If an error occurs while communicating with the Cloudflare API.
*/
public BatchEntry recordBatch(ZoneEntity zone, @Nullable List<RecordEntity> postRecords, @Nullable List<RecordEntity> putRecords,
@Nullable List<RecordEntity> patchRecords, @Nullable List<RecordEntity> deleteRecords)
throws CloudflareApiException {
BatchEntry batchEntry = new BatchEntry();
// build 'clean' record entries
if (postRecords != null) {
batchEntry.setPosts(cleanRecordsForPostOrPut(postRecords));
}
if (putRecords != null) {
batchEntry.setPuts(cleanRecordsForPostOrPut(putRecords));
}
if (patchRecords != null) {
batchEntry.setPatches(cleanRecordsForPatch(patchRecords));
}
if (deleteRecords != null) {
batchEntry.setDeletes(cleanRecordsForDelete(deleteRecords));
}
String endpoint = CfRequest.RECORD_BATCH.buildPath(zone.getId());
BatchResponse resp = postRequest(endpoint, batchEntry, BatchResponse.class);
checkResponse(resp);
// set zone id
BatchEntry result = resp.getResult();
setZoneIdForBatchResults(result, zone.getId());
return result;
}
private List<RecordEntity> filterAndSetZoneRecords(ZoneEntity zone, @Nullable RecordType[] types, List<RecordEntity> recs)
throws CloudflareNotFoundException {
List<RecordEntity> filtered;
if (types != null && types.length > 0) {
Set<RecordType> allowedTypes = new HashSet<>(Arrays.asList(types));
filtered = recs.stream()
.filter(rec -> allowedTypes.contains(RecordType.valueOf(rec.getType())))
.collect(Collectors.toList());;
} else {
filtered = new ArrayList<>(recs);
}
filtered.forEach(rec -> rec.setZoneId(zone.getId()));
// special exception for an empty result, normally it's done in the RecordValidator
if (filtered.isEmpty() && emptyResultThrowsException) {
throw new CloudflareNotFoundException("No records exist after filtering zone: " + zone.getName());
}
return filtered;
}
private List<RecordEntity> cleanRecordsForPostOrPut(List<RecordEntity> records) {
List<RecordEntity> cleaned = new ArrayList<>();
records.forEach(
rec -> cleaned.add(RecordEntity.build(rec.getId(), rec.getSld(), rec.getType(), rec.getTtl(), rec.getContent())));
return cleaned;
}
private List<RecordEntity> cleanRecordsForPatch(List<RecordEntity> records) {
List<RecordEntity> cleaned = new ArrayList<>();
records.forEach(rec -> cleaned.add(RecordEntity.build(rec.getId(), rec.getContent())));
return cleaned;
}
private List<RecordEntity> cleanRecordsForDelete(List<RecordEntity> records) {
List<RecordEntity> cleaned = new ArrayList<>();
records.forEach(
rec -> cleaned.add(RecordEntity.build(rec.getId(), rec.getSld(), rec.getType(), null, rec.getContent())));
return cleaned;
}
private void setZoneIdForBatchResults(BatchEntry result, String zoneId) {
if (result.getPosts() != null) {
result.getPosts().forEach(rec -> rec.setZoneId(zoneId));
}
if (result.getPuts() != null) {
result.getPuts().forEach(rec -> rec.setZoneId(zoneId));
}
if (result.getPatches() != null) {
result.getPatches().forEach(rec -> rec.setZoneId(zoneId));
}
}
private void checkResponse(AbstractResponse resp) throws CloudflareApiException {
checkResponse(resp, false);
}
private void checkResponse(AbstractResponse resp, boolean singleResultExpected)
throws CloudflareApiException {
responseValidator.validate(resp, singleResultExpected);
}
}
@@ -1,190 +0,0 @@
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);
}
}
}
@@ -1,86 +0,0 @@
package codes.thischwa.cf;
import lombok.Getter;
/**
* Enum CfRequest encapsulates various API endpoint paths for managing DNS zones and records in a
* cohesive and reusable manner. Each enum constant represents a specific API request path.
*/
@Getter
public enum CfRequest {
/**
* Represents the API endpoint path for retrieving the list of DNS zones.
*/
ZONE_LIST("/zones"),
/**
* Represents the API endpoint path for retrieving information about a specific DNS zone by its
* name. The endpoint path supports a placeholder for the zone name, which needs to be provided to
* construct the complete path.
*/
ZONE_INFO("/zones?name=%s"),
/**
* Represents the API endpoint path for retrieving information about DNS records within a
* specific DNS zone. The endpoint path includes a placeholder for the zone identifier, which
* needs to be provided to construct the complete path.
*/
RECORD_LIST("/zones/%s/dns_records"),
/**
* Represents the API endpoint path for creating a new DNS record within a specific DNS zone. The
* endpoint path includes a placeholder for the zone identifier, which needs to be provided to
* construct the complete path.
*/
RECORD_CREATE("/zones/%s/dns_records"),
/**
* Represents the API endpoint path for retrieving information about a DNS record within a
* specific DNS zone by its name. The endpoint path includes placeholders for the zone identifier
* and the record name, which need to be provided to construct the complete path.
*/
RECORD_INFO_NAME("/zones/%s/dns_records?name=%s"),
/**
* Represents the API endpoint path for updating an existing DNS record within a specific DNS
* zone. The endpoint path includes placeholders for the zone identifier and the record
* identifier, which need to be provided to construct the complete path.
*/
RECORD_UPDATE("/zones/%s/dns_records/%s"),
/**
* Represents the API endpoint path for performing batch operations on DNS records within a specific zone.
* The placeholder "%s" in the path is intended to be replaced by a zone identifier.
* This constant is used to construct the URL for interacting with the batch DNS records API.
*/
RECORD_BATCH("/zones/%s/dns_records/batch"),
/**
* Represents the API endpoint path for deleting an existing DNS record within a specific DNS
* zone. The endpoint path includes placeholders for the zone identifier and the record
* identifier, which need to be provided to construct the complete path.
*/
RECORD_DELETE("/zones/%s/dns_records/%s");
private static final char VAR_IDENTIFICATION = '%';
private final String path;
CfRequest(String path) {
this.path = path;
}
/**
* Constructs the complete API endpoint path by formatting the base path with the provided
* arguments.
*
* @param vars the arguments to format the path string with; these are typically specific
* identifiers or parameters required by the API endpoint.
* @return the fully constructed API endpoint path as a string.
*/
String buildPath(Object... vars) {
long varCount = path.chars().filter(c -> c == VAR_IDENTIFICATION).count();
if (varCount != vars.length) {
throw new IllegalArgumentException(
String.format(
"The number of variables (%d) does not match the number of parameters (%d) in the path string: %s",
vars.length, varCount, path));
}
return String.format(path, vars);
}
}
@@ -1,33 +0,0 @@
package codes.thischwa.cf;
import java.io.Serial;
/**
* Represents a custom exception for errors encountered while interacting with the Cloudflare API.
*/
public class CloudflareApiException extends Exception {
@Serial
private static final long serialVersionUID = 1L;
/**
* Constructs a new CloudflareApiException with the specified detail message.
*
* @param message the detail message, which provides more information about the exception.
*/
public CloudflareApiException(String message) {
super(message);
}
/**
* Constructs a new CloudflareApiException with the specified detail message and cause.
*
* @param message the detail message, which provides additional context or information about the
* exception.
* @param cause the cause of this exception, which is the underlying throwable that triggered this
* exception.
*/
public CloudflareApiException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -1,34 +0,0 @@
package codes.thischwa.cf;
/**
* This exception is thrown to indicate that a requested resource was not found during interaction
* with the Cloudflare API.
*
* <p>It extends {@link CloudflareApiException} to provide specific errors related to situations
* where Cloudflare responds with a "not found" operation.
*/
public class CloudflareNotFoundException extends CloudflareApiException {
/**
* Constructs a new CloudflareNotFoundException with the specified detail message.
*
* @param message the detail message, which provides additional context about the "not found"
* error encountered during interaction with the Cloudflare API.
*/
public CloudflareNotFoundException(String message) {
super(message);
}
/**
* Constructs a new CloudflareNotFoundException with the specified detail message and cause.
*
* @param message the detail message, which provides additional context about the "not found"
* error encountered during interaction with the Cloudflare API.
* @param cause the cause of this exception, which is the underlying throwable that triggered this
* exception.
*/
public CloudflareNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -1,26 +0,0 @@
package codes.thischwa.cf;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
/**
* The JsonConf class provides a utility method for initializing and configuring a shared
* {@link ObjectMapper} instance for JSON serialization and deserialization.
*/
class JsonConf {
private JsonConf() {
}
static ObjectMapper initObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
return mapper;
}
}
@@ -1,73 +0,0 @@
package codes.thischwa.cf;
import codes.thischwa.cf.model.AbstractResponse;
import codes.thischwa.cf.model.AbstractSingleResponse;
import codes.thischwa.cf.model.RecordMultipleResponse;
import codes.thischwa.cf.model.ResponseResultInfo;
import java.util.stream.Collectors;
/**
* Validates API responses to ensure compliance with expected conditions, such as response success
* and result count.
*
* <p>This class performs two primary validation tasks:
* <ul>
* <li>It checks whether the API response was successful by analyzing the associated response
* metadata. If the response indicates failure, an exception is thrown with descriptive error
* messages.
* <li>It validates the number of results in the API response payload to detect unexpected counts.
* For {@link RecordMultipleResponse}, it checks if results are empty or if more than one result
* was returned when a single result was expected. For {@link AbstractSingleResponse}, it checks
* if the result is null. Depending on the parameter 'emptyResultThrowsException', an exception
* will be triggered or an empty/null result will be returned.
* </ul>
*/
class ResponseValidator {
private final boolean emptyResultThrowsException;
ResponseValidator(boolean emptyResultThrowsException) {
this.emptyResultThrowsException = emptyResultThrowsException;
}
void validate(AbstractResponse resp, boolean singleResultExpected) throws CloudflareApiException {
validateResponseSuccess(resp);
validateResultCount(resp, singleResultExpected);
}
private void validateResponseSuccess(AbstractResponse resp) throws CloudflareApiException {
ResponseResultInfo resultInfo = resp.getResponseResultInfo();
if (!resultInfo.isSuccess()) {
String errors =
resultInfo.getErrors().stream().map(Object::toString).collect(Collectors.joining(", "));
throw new CloudflareApiException("Error in response: " + errors);
}
}
private void validateResultCount(AbstractResponse resp, boolean singleResultExpected)
throws CloudflareApiException {
if (resp instanceof RecordMultipleResponse respMulti) {
validateMultipleResponse(respMulti, singleResultExpected);
} else if (resp instanceof AbstractSingleResponse<?> respSingle) {
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");
}
}
private void validateSingleResponse(AbstractSingleResponse<?> response)
throws CloudflareNotFoundException {
if (emptyResultThrowsException && response.getResult() == null) {
throw new CloudflareNotFoundException("No result found");
}
}
}
@@ -1,49 +0,0 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
import java.util.List;
/**
* Fluent interface for record-level operations.
* Provides a chainable API for CRUD operations on DNS records.
*/
public interface RecordOperations {
/**
* Retrieves DNS records for the selected subdomain.
*
* @return a list of RecordEntity objects matching the criteria
* @throws CloudflareApiException if an error occurs while retrieving records
*/
List<RecordEntity> get() throws CloudflareApiException;
/**
* Creates a new DNS record with the specified parameters.
*
* @param type the DNS record type (e.g., A, AAAA, CNAME)
* @param content the content of the DNS record (e.g., IP address)
* @param ttl the time-to-live value in seconds
* @return the created RecordEntity
* @throws CloudflareApiException if an error occurs while creating the record
*/
RecordEntity create(RecordType type, String content, int ttl) throws CloudflareApiException;
/**
* Updates an existing DNS record with new content.
*
* @param newContent the new content for the DNS record
* @return the updated RecordEntity
* @throws CloudflareApiException if an error occurs while updating the record
*/
RecordEntity update(String newContent) throws CloudflareApiException;
/**
* Deletes DNS records of the specified types.
*
* @param types the DNS record types to delete
* @throws CloudflareApiException if an error occurs while deleting records
*/
void delete(RecordType... types) throws CloudflareApiException;
}
@@ -1,64 +0,0 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CfDnsClient;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.List;
import org.jetbrains.annotations.Nullable;
/**
* Implementation of RecordOperations for fluent API access to record-level operations.
*/
public class RecordOperationsImpl implements RecordOperations {
private final CfDnsClient client;
private final ZoneEntity zone;
private final String sld;
private final RecordType[] types;
/**
* Constructs a RecordOperationsImpl instance.
*
* @param client the CfDnsClient instance to use for operations
* @param zone the ZoneEntity representing the DNS zone
* @param sld the subdomain (second-level domain) name
* @param types optional array of RecordType to filter by
*/
public RecordOperationsImpl(CfDnsClient client, ZoneEntity zone, String sld, @Nullable RecordType[] types) {
this.client = client;
this.zone = zone;
this.sld = sld;
this.types = types;
}
@Override
public List<RecordEntity> get() throws CloudflareApiException {
return client.recordList(zone, sld, types);
}
@Override
public RecordEntity create(RecordType type, String content, int ttl) throws CloudflareApiException {
return client.recordCreateSld(zone, sld, ttl, type, content);
}
@Override
public RecordEntity update(String newContent) throws CloudflareApiException {
List<RecordEntity> recs = get();
if (recs.isEmpty()) {
throw new CloudflareApiException("No recs found to update for subdomain: " + sld);
}
if (recs.size() > 1) {
throw new CloudflareApiException("Multiple recs found. Please use recordUpdate() directly for precise control.");
}
RecordEntity rec = recs.get(0);
rec.setContent(newContent);
return client.recordUpdate(zone, rec);
}
@Override
public void delete(RecordType... types) throws CloudflareApiException {
client.recordDeleteTypeIfExists(zone, sld, types);
}
}
@@ -1,40 +0,0 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordType;
import org.jetbrains.annotations.Nullable;
/**
* Fluent interface for zone-level operations.
* Provides a chainable API for accessing and manipulating DNS records within a specific zone.
*/
public interface ZoneOperations {
/**
* Selects a record (subdomain) within the zone for further operations.
*
* @param sld the second-level domain (subdomain) name
* @return a RecordOperations instance for chaining record-specific operations
* @throws CloudflareApiException if the zone cannot be found or accessed
*/
RecordOperations record(String sld) throws CloudflareApiException;
/**
* Selects a record with specific types within the zone for further operations.
*
* @param sld the second-level domain (subdomain) name
* @param types optional DNS record types to filter by
* @return a RecordOperations instance for chaining record-specific operations
* @throws CloudflareApiException if the zone cannot be found or accessed
*/
RecordOperations record(String sld, @Nullable RecordType... types) throws CloudflareApiException;
/**
* Lists all DNS records within the zone, optionally filtered by types.
*
* @param types optional DNS record types to filter by
* @return a list of RecordEntity objects matching the criteria
* @throws CloudflareApiException if an error occurs while retrieving records
*/
java.util.List<codes.thischwa.cf.model.RecordEntity> list(@Nullable RecordType... types) throws CloudflareApiException;
}
@@ -1,44 +0,0 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CfDnsClient;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.List;
import org.jetbrains.annotations.Nullable;
/**
* Implementation of ZoneOperations for fluent API access to zone-level operations.
*/
public class ZoneOperationsImpl implements ZoneOperations {
private final CfDnsClient client;
private final ZoneEntity zone;
/**
* Constructs a ZoneOperationsImpl instance.
*
* @param client the CfDnsClient instance to use for operations
* @param zone the ZoneEntity representing the DNS zone
*/
public ZoneOperationsImpl(CfDnsClient client, ZoneEntity zone) {
this.client = client;
this.zone = zone;
}
@Override
public RecordOperations record(String sld) throws CloudflareApiException {
return new RecordOperationsImpl(client, zone, sld, null);
}
@Override
public RecordOperations record(String sld, @Nullable RecordType... types) throws CloudflareApiException {
return new RecordOperationsImpl(client, zone, sld, types);
}
@Override
public List<RecordEntity> list(@Nullable RecordType... types) throws CloudflareApiException {
return client.recordList(zone, types);
}
}
@@ -1,31 +0,0 @@
/**
* Fluent API interfaces and implementations for chainable DNS operations.
*
* <p>This package provides a fluent, chainable interface for interacting with Cloudflare DNS
* records, making code more readable and concise.
*
* <p>Example usage:
* <pre><code>
* // Create a DNS record
* client.zone("example.com")
* .record("api")
* .create(RecordType.A, "192.168.1.1", 60);
*
* // Get DNS records
* List&lt;RecordEntity&gt; records = client.zone("example.com")
* .record("www", RecordType.A)
* .get();
*
* // Update a DNS record
* client.zone("example.com")
* .record("api", RecordType.A)
* .update("192.168.1.2");
*
* // Delete DNS records
* client.zone("example.com")
* .record("old-service")
* .delete(RecordType.A, RecordType.AAAA);
* </code></pre>
*/
package codes.thischwa.cf.fluent;
@@ -1,18 +0,0 @@
package codes.thischwa.cf.model;
import lombok.Data;
/**
* Represents a base abstract entity class for modeling domain objects with a unique identifier.
*
* <p>This class provides a fundamental contract for entities by implementing the {@link
* ResponseEntity} interface. The primary attribute of this class is the `id` field, which serves as
* a unique identifier for all derived entities.
*
* <p>Commonly extended by other entity classes to maintain a consistent entity structure across the
* domain models. This encourages code reusability and consistency within the system.
*/
@Data
public class AbstractEntity implements ResponseEntity {
private String id;
}
@@ -1,45 +0,0 @@
package codes.thischwa.cf.model;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Abstract base class for response models that contain multiple result entries.
*
* <p>This class is designed to handle API responses where multiple entities are part of the result
* set, such as paginated or batched data. It extends {@link AbstractResponse} to include additional
* attributes specific to multi-entity responses.
*
* <p>Attributes:
*
* <ul>
* <li>`resultInfo`: Provides metadata about the result set, such as pagination details like page
* number, total count, number of results per page, etc.
* <li>`result`: A list of entities representing the main body of the response. The type of
* entities in the result list is determined by the generic parameter {@code T}, which must
* extend {@link ResponseEntity}.
* </ul>
*
* <p>Subclasses can be created by specifying the entity type that the response should handle.
*
* @param <T> Represents the type of entities contained within the response. For this class, it is
* expected to be {@code ResponseEntity}.
*/
@EqualsAndHashCode(callSuper = true)
@Data
public abstract class AbstractMultipleResponse<T extends ResponseEntity> extends AbstractResponse {
private ResultInfo resultInfo;
private List<T> result;
/**
* Default constructor for the {@code AbstractMultipleResponse} class.
*
* <p>Initializes a new instance of {@code AbstractMultipleResponse}, invoking the default
* constructor of the superclass {@code AbstractResponse}. This constructor is typically used for
* deserialization or subclass instantiation.
*/
AbstractMultipleResponse() {
super();
}
}
@@ -1,28 +0,0 @@
package codes.thischwa.cf.model;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import lombok.Data;
/**
* Abstract base class for API response models.
*
* <p>This class encapsulates common attributes used to represent the result of an API request. It
* can be extended to define more specific response structures.
*
* <p>Attributes:
*
* <ol>
* <li><b>success</b>: Indicates whether the API request was successful.
* <li><b>errors</b>: A list of error messages, if any, returned by the API.
* <li><b>messages</b>: A list of informational or status messages accompanying the response.
* </ol>
*
* <p>This structure is designed for consistency and ease of extending response models in
* applications that require uniform response structures.
*/
@Data
public abstract class AbstractResponse {
@JsonUnwrapped
private ResponseResultInfo responseResultInfo;
}
@@ -1,24 +0,0 @@
package codes.thischwa.cf.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Represents a base abstract response model for handling single response entities within an API
* response.
*
* <p>This class extends {@code AbstractResponse}, inheriting common response attributes such as
* success status, error messages, and informational messages. It introduces a single generic type
* parameter {@code <T>} which extends {@code ResponseEntity}, enforcing type consistency for the
* result attribute.
*
* <p>Primary Attribute: {@code result}: Represents the single entity result of the response. This
* is a generic type that ensures flexibility and adaptability for different entity models.
*
* @param <T> The type of the response entity that extends {@code ResponseEntity}.
*/
@EqualsAndHashCode(callSuper = true)
@Data
public abstract class AbstractSingleResponse<T extends ResponseEntity> extends AbstractResponse {
private T result;
}
@@ -1,39 +0,0 @@
package codes.thischwa.cf.model;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Represents a batch entry containing different types of operations on record entities.
*
* <p>A BatchEntry groups together collections of operations (patches, posts, puts, and deletes)
* intended to be performed as part of a single batch process. Each operation corresponds to a specific
* type of action on DNS record entities.
*
* <ul>
* <li><b>patches</b>: A list of {@link RecordEntity} objects representing partial updates to existing records.
* <li><b>posts</b>: A list of {@link RecordEntity} objects to be created as new DNS records.
* <li><b>puts</b>: A list of {@link RecordEntity} objects representing updates or replacements for existing records.
* <li><b>deletes</b>: A list of {@link RecordEntity} objects with name, type, and content to be removed.
* </ul>
*
* <p>This class is used as both a request body for batch operations and to represent the batch response.
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class BatchEntry extends AbstractEntity {
List<RecordEntity> patches;
List<RecordEntity> posts;
List<RecordEntity> puts;
List<RecordEntity> deletes;
@Override
public String getId() {
return "";
}
}
@@ -1,18 +0,0 @@
package codes.thischwa.cf.model;
/**
* Represents a response that contains a single {@link BatchEntry} as the result.
*
* <p>This class is used for API responses where the primary result is a batch entry,
* which includes collections of operations such as patches, posts, puts, and deletes
* performed on DNS record entities.
*
* <p>Extends {@code AbstractSingleResponse} with {@code BatchEntry} as the generic type,
* ensuring that the response result is a batch of operations.
*/
public class BatchResponse extends AbstractSingleResponse<BatchEntry> {
BatchResponse() {
super();
}
}
@@ -1,83 +0,0 @@
package codes.thischwa.cf.model;
import java.util.Map;
import lombok.Data;
/**
* Represents a request model for paginated data.
*
* <p>This class encapsulates the page number and the number of items per page for a paginated
* request, along with utility methods for constructing and retrieving pagination parameters.
*
* <p>Key functionalities:
*
* <ul>
* <li>Creating a {@code PagingRequest} instance with specific pagination values using the {@code
* of} method.
* <li>Creating a default {@code PagingRequest} with predefined pagination values.
* <li>Retrieving pagination parameters as a key-value map.
* <li>Generating a query string representation for the pagination parameters.
* </ul>
*/
@Data
public class PagingRequest {
/**
* Default page size for retrieving all records in a single request.
* Set to a very high value to effectively disable pagination when fetching all records.
*/
private static final int DEFAULT_ALL_RECORDS_PAGE_SIZE = 5_000_000;
private int page;
private int perPage;
PagingRequest(int page, int perPage) {
this.page = page;
this.perPage = perPage;
}
/**
* Creates a new {@code PagingRequest} instance with the specified page number and items per page.
*
* @param page the page number to be requested
* @param perPage the number of items to be included per page
* @return a new {@code PagingRequest} instance with the provided parameters
*/
public static PagingRequest of(int page, int perPage) {
return new PagingRequest(page, perPage);
}
/**
* Creates a default {@code PagingRequest} instance with a page number set to 1 and a high number
* of items per page to accommodate large dataset requests and effectively retrieve all records.
*
* @return a default {@code PagingRequest} instance with predefined pagination parameters
*/
public static PagingRequest defaultPaging() {
return new PagingRequest(1, DEFAULT_ALL_RECORDS_PAGE_SIZE);
}
/**
* Retrieves the pagination parameters in a key-value map format.
*
* @return a map containing the pagination parameters, where the key "page" indicates the current
* page number and the key "perPage" indicates the number of items per page.
*/
public Map<String, String> getPagingParams() {
return Map.of("page", String.valueOf(page), "perPage", String.valueOf(perPage));
}
/**
* Appends a query string with pagination parameters (page and perPage) to the provided endpoint.
*
* @param endpoint the base URL or API endpoint to which the query string will be appended
* @return the complete URL with the appended query string for pagination
*/
public String addQueryString(String endpoint) {
return endpoint + queryString(endpoint.contains("?"));
}
private String queryString(boolean add) {
String qs = "page=" + page + "&perPage=" + perPage;
return add ? "&" + qs : "?" + qs;
}
}
@@ -1,142 +0,0 @@
package codes.thischwa.cf.model;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.jetbrains.annotations.Nullable;
/**
* Represents a DNS record entity within a specific zone.
*
* <p>Attributes defined in this class include:
*
* <ul>
* <li>DNS record type such as "A" or "CNAME".
* <li>Name of the DNS record.
* <li>Content of the DNS record, such as an IP address.
* <li>Flags indicating whether the record is proxiable or proxied.
* <li>TTL (Time-To-Live) for the DNS record.
* <li>A locked status to indicate the immutability of the record.
* <li>Zone-specific metadata including zone ID and name.
* <li>Timestamps for creation and modification.
* </ul>
*
* <p>Provides a static factory method {@code build} for creating a DNS record with specific
* attributes.
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class RecordEntity extends AbstractEntity {
private String type;
private String name;
private String content;
private Boolean proxiable;
private Boolean proxied;
private Integer ttl;
private Boolean locked;
@Nullable
private String zoneId;
@Nullable
private String zoneName;
@Nullable
private LocalDateTime modifiedOn;
@Nullable
private LocalDateTime createdOn;
/**
* Initializes a new instance of the RecordEntity class and invokes the parent constructor from
* the AbstractEntity class. The RecordEntity class represents a DNS record entity within a
* specific zone, encapsulating attributes such as type, name, content, TTL, and other related
* metadata.
*/
public RecordEntity() {
super();
}
/**
* Builds and returns a {@link RecordEntity} instance with the specified attributes.
*
* @param name the name of the DNS record
* @param type the {@link RecordType} of the DNS record
* @param ttl the time-to-live (TTL) value for the DNS record
* @param content the content of the DNS record, typically an IP address
* @return a {@link RecordEntity} populated with the provided attributes
*/
public static RecordEntity build(String name, RecordType type, Integer ttl, String content) {
RecordEntity rec = new RecordEntity();
rec.name = name;
rec.type = type.getType();
rec.ttl = ttl;
rec.content = content;
return rec;
}
/**
* Builds and returns a {@link RecordEntity} instance with the specified ID and content.
*
* @param id the unique identifier for the DNS record
* @param content the content of the DNS record, typically an IP address or other record data
* @return a {@link RecordEntity} populated with the provided ID and content
*/
public static RecordEntity build(String id, String content) {
RecordEntity rec = new RecordEntity();
rec.setId(id);
rec.content = content;
return rec;
}
/**
* Builds and returns a {@link RecordEntity} instance with the specified attributes.
*
* @param id the unique identifier for the DNS record
* @param name the name of the DNS record
* @param type the type of the DNS record, represented as a string (e.g., "A", "CNAME")
* @param ttl the time-to-live (TTL) value for the DNS record
* @param content the content of the DNS record, typically an IP address or other record data
* @return a {@link RecordEntity} populated with the provided attributes
* @throws IllegalArgumentException if the type string is not a valid RecordType
*/
public static RecordEntity build(String id, String name, String type, Integer ttl, String content) {
RecordType recordType;
try {
recordType = RecordType.valueOf(type);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid record type: " + type + ". Must be one of: "
+ java.util.Arrays.toString(RecordType.values()), e);
}
RecordEntity rec = new RecordEntity();
rec.setId(id);
rec.name = name;
rec.type = recordType.getType();
rec.ttl = ttl;
rec.content = content;
return rec;
}
/**
* Retrieves the short name (subdomain) of the DNS record.
* If the name contains a dot ('.'), only the substring before the first dot is returned.
* This is useful for getting the subdomain part of a fully qualified domain name.
*
* @return the short name of the DNS record (substring before the first dot),
* or the full name if no dot is present
*/
public String getSld() {
if (name == null) {
return null;
}
if (zoneName != null && name.endsWith(zoneName)) {
int zoneNameLength = zoneName.length();
int dotSeparatorLength = 1;
return name.substring(0, name.length() - zoneNameLength - dotSeparatorLength);
}
int firstDotPosition = name.indexOf('.');
if (firstDotPosition > 0) {
return name.substring(0, firstDotPosition);
}
return name;
}
}
@@ -1,19 +0,0 @@
package codes.thischwa.cf.model;
/**
* Represents the API response of the Cloudflare API containing multiple {@link RecordEntity}
* instances.
*/
public class RecordMultipleResponse extends AbstractMultipleResponse<RecordEntity> {
/**
* Constructs an instance of RecordMultipleResponse.
*
* <p>This class represents a response containing multiple DNS record entities from the
* Cloudflare API. It inherits functionality from AbstractMultipleResponse to handle multiple
* records of type RecordEntity.
*/
public RecordMultipleResponse() {
super();
}
}
@@ -1,20 +0,0 @@
package codes.thischwa.cf.model;
/**
* Represents the API response of the Cloudflare API containing a single {@link RecordEntity}
* instance.
*/
public class RecordSingleResponse extends AbstractSingleResponse<RecordEntity> {
/**
* Constructs a new instance of the RecordSingleResponse class.
*
* <p>This constructor initializes the RecordSingleResponse object by invoking the superclass
* constructor. The RecordSingleResponse represents a specific API response structure that
* encapsulates a single DNS record entity, providing mechanisms to interact with such data in the
* context of the Cloudflare API.
*/
public RecordSingleResponse() {
super();
}
}
@@ -1,204 +0,0 @@
package codes.thischwa.cf.model;
import lombok.Getter;
/**
* Enum representing various DNS record types.
*
* <p>Each constant in this enum corresponds to a specific DNS record type, such as "A", "AAAA",
* "CNAME", or "TXT". This enum provides a means to standardize the representation of these record
* types throughout the application while allowing easy retrieval of their string representation.
*/
@Getter
public enum RecordType {
/**
* Represents the DNS A record type.
*
* <p>The "A" record type is used to map a domain name to an IPv4 address.
*/
A("A"),
/**
* Represents the DNS AAAA record type.
*
* <p>The "AAAA" record type maps a domain name to an IPv6 address.
*/
AAAA("AAAA"),
/**
* Represents the DNS CAA (Certificate Authority Authorization) record type.
*
* <p>The "CAA" record type is used to specify which certificate authorities (CAs) are allowed to
* issue SSL/TLS certificates for a domain.
*/
CAA("CAA"),
/**
* Represents the DNS CERT record type.
*
* <p>The "CERT" record type is used to store certificates and related certificate revocation
* lists or certificate authority data in DNS zones.
*/
CERT("CERT"),
/**
* Represents the DNS CNAME (Canonical Name) record type.
*
* <p>The "CNAME" record type is used to alias one domain name to another.
*/
CNAME("CNAME"),
/**
* Represents the DNSKEY record type.
*
* <p>The "DNSKEY" record type is used for storing public keys in DNS, as part of the DNS Security
* Extensions (DNSSEC). It helps in verifying the authenticity of DNS responses through digital
* signatures.
*/
DNSKEY("DNSKEY"),
/**
* Represents the DNS DS (Delegation Signer) record type.
*
* <p>The "DS" record type is used in the DNSSEC (Domain Name System Security Extensions)
* protocol. It contains a hash of a DNSKEY record which is utilized in establishing a chain of
* trust from a parent zone to a child zone.
*/
DS("DS"),
/**
* Represents the DNS HTTPS (HTTP Service) record type.
*
* <p>The "HTTPS" record type is used to specify information about the HTTP/3 and related services
* offered by a domain.
*/
HTTPS("HTTPS"),
/**
* Represents the DNS LOC (Location) record type.
*
* <p>The "LOC" record type is used to store geographical location information for a domain,
* including latitude, longitude, altitude, and other details. It enables associating domains with
* physical locations.
*/
LOC("LOC"),
/**
* Represents the DNS MX (Mail Exchange) record type.
*
* <p>The "MX" record type is used to specify the mail servers responsible for receiving email
* messages on behalf of a domain.
*/
MX("MX"),
/**
* Represents the NAPTR record type for DNS configurations.
*
* <p>This constant is used to specify NAPTR (Naming Authority Pointer) DNS records, which allow
* for service discovery through flexible DNS-based mechanisms. NAPTR records are commonly used in
* applications such as VoIP and ENUM (Telephone Number Mapping) for resolving information about
* available services.
*/
NAPTR("NAPTR"),
/**
* Represents the namespace or identifier `"NS"` within the domain model.
*
* <p>This variable typically designates elements related to name server operations or
* configurations in the context of the Cloudflare DNS system. It may be used as an identifier or
* constant throughout the application for operations involving name servers.
*/
NS("NS"),
/**
* Represents the "OPENPGPKEY" DNS record type.
*
* <p>This constant is primarily used to identify DNS records of the type "OPENPGPKEY". It may be
* utilized within the system for operations such as creating, retrieving, updating, or managing
* DNS records specific to the "OPENPGPKEY" type.
*/
OPENPGPKEY("OPENPGPKEY"),
/**
* Represents a DNS record type.
*
* <p>The `PTR` value specifically refers to a "pointer record" in the DNS system, which is
* typically used for reverse DNS lookups.
*/
PTR("PTR"),
/**
* Represents the SMIMEA DNS record type.
*
* <p>The SMIMEA resource record is used to associate a user's certificate information for email
* message signing or encryption. This type of DNS record is part of the DNS-based Authentication
* of Named Entities (DANE) protocol.
*
* <p>SMIMEA records provide a mechanism for utilizing certificates in email communication
* securely by publishing their information in DNS. They ensure integrity and authenticity of
* encrypted or signed email exchanges.
*
* <p>Key features include:
*
* <ul>
* <li>Use in Secure/Multipurpose Internet Mail Extensions (S/MIME)-based messaging.
* <li>Facilitating secure email communications by publishing certificates in DNS.
* <li>Enhancing security by eliminating dependency on third-party certificate authorities.
* </ul>
*/
SMIMEA("SMIMEA"),
/**
* Represents a service record (SRV) type in the DNS configuration model.
*
* <p>This constant may be used to identify and work with SRV record types in various DNS-related
* operations or integrations.
*/
SRV("SRV"),
/**
* Represents the DNS record type "SSHFP" (SSH Fingerprint), used in DNS to store cryptographic
* fingerprints associated with SSH host keys.
*
* <p>This DNS record type provides a mechanism for verifying the authenticity of an SSH server
* before initiating a connection, enhancing the security of SSH communications.
*/
SSHFP("SSHFP"),
/**
* Represents the Service Binding (SVCB) DNS record type.
*
* <p>The SVCB record is a DNS resource record used to indicate alternative endpoints or specific
* configuration details for services. It is commonly applied in service discovery and
* protocol-specific configurations.
*/
SVCB("SVCB"),
/**
* Represents a constant for the DNS-based Authentication of Named Entities (DANE) TLSA record
* type.
*
* <p>The TLSA record is used to associate a TLS server certificate or public key with the domain
* name (e.g., via DNSSEC). It enables cryptographically secured connections by attaching
* certificate and key constraints to the specific domain.
*/
TLSA("TLSA"),
/**
* Represents the TXT DNS record type.
*
* <p>The TXT DNS record type is commonly used to store text-based information for various
* verification and configuration purposes, such as domain ownership verification or email
* authentication protocols (e.g., SPF, DKIM).
*/
TXT("TXT"),
/**
* Represents a Uniform Resource Identifier (URI).
*
* <p>This variable is used to define and manage URIs, which are string identifiers commonly
* utilized to specify the location of resources in various network-based systems.
*
* <p>Features and usage:
*
* <ul>
* <li>Provides a standard way of identifying resources via URI syntax.
* <li>Can encapsulate support for schemes such as HTTP, HTTPS, FTP, etc.
* </ul>
*
* <p>This variable serves as a critical component for resource identification and communication
* operations within the application.
*/
URI("URI");
private final String type;
RecordType(String type) {
this.type = type;
}
@Override
public String toString() {
return type;
}
}
@@ -1,17 +0,0 @@
package codes.thischwa.cf.model;
/**
* Represents a contract for entities that have a unique identifier.
*
* <p>This interface is primarily used as a common abstraction for domain objects that require a
* unique identifier, enabling type consistency and code reusability.
*/
public interface ResponseEntity {
/**
* Retrieves the unique identifier of the entity.
*
* @return the unique identifier as a String
*/
String getId();
}
@@ -1,62 +0,0 @@
package codes.thischwa.cf.model;
import java.util.List;
import lombok.Data;
/**
* Represents the result of a response with metadata about its success and associated messages or
* errors.
*
* <p>This class provides a structure to capture the outcome of an operation, including:
* <ul>
* <li>Whether the operation was successful.
* <li>A list of error messages if the operation failed.
* <li>A list of informational or success messages.
* </ul>
* It can be used to standardize the response format in an application.
*/
@Data
public class ResponseResultInfo {
private boolean success;
private List<Error> errors;
private List<String> messages;
/**
* Represents an error with a specific code and message.
*
* <p>This class is used to encapsulate error information, including a numerical error code
* and a corresponding descriptive message. It is often used as part of a collection of errors
* to provide detailed diagnostics for failed operations or processes.
*/
@Data
public static class Error {
private int code;
private String message;
/**
* Constructs a new instance of the {@code Error} class with default values for its properties.
*
* <p>This no-argument constructor initializes an {@code Error} object without setting
* specific values for the error code or message. It is primarily used when an error needs to
* be created and set up later, or when default values are acceptable.
*/
public Error() {
}
/**
* Constructs an instance of the {@code Error} class with a specified error code and message.
*
* @param code the numerical code representing the error
* @param message the descriptive message providing details about the error
*/
public Error(int code, String message) {
this.code = code;
this.message = message;
}
@Override
public String toString() {
return String.format("%d: %s", code, message);
}
}
}
@@ -1,26 +0,0 @@
package codes.thischwa.cf.model;
/**
* Represents metadata for paginated results.
*
* <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.
*
* @param page The current page number.
* @param perPage The number of results per page.
* @param totalPages The total number of pages available.
* @param count The number of results on the current page.
* @param totalCount The total number of results across all pages.
*/
public record ResultInfo(int page, int perPage, int totalPages, int count, int totalCount) {
/**
* Constructs a ResultInfo instance with the specified total count and default values for other
* fields. Just to use in tests!
*
* @param totalCount the total number of results across all pages
*/
public ResultInfo(int totalCount) {
this(0, 0, 0, 0, totalCount);
}
}
@@ -1,51 +0,0 @@
package codes.thischwa.cf.model;
import java.time.LocalDateTime;
import java.util.Set;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Represents a DNS zone entity in the Cloudflare DNS system. <br>
*
* <p>This class encapsulates all relevant data and metadata associated with a zone, including but
* not limited to the following attributes:
*
* <ul>
* <li>Zone name.
* <li>Development mode status.
* <li>Active and original name servers linked to the zone.
* <li>Timestamps indicating when the zone was created, modified, or activated.
* <li>Current operational status of the zone (e.g., active, inactive).
* <li>Boolean flag indicating whether the zone is paused.
* <li>Zone type, representing the nature of the DNS zone (e.g., full, partial).
* </ul>
*
* <p>This class extends {@link AbstractEntity} to inherit basic entity properties and to provide a
* consistent interface across domain models.
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ZoneEntity extends AbstractEntity {
private String name;
private Integer developmentMode;
private Set<String> nameServers;
private Set<String> originalNameServers;
private LocalDateTime createdOn;
private LocalDateTime modifiedOn;
private LocalDateTime activatedOn;
private String status;
private Boolean paused;
private String type;
/**
* Default no-argument constructor for the ZoneEntity class.
*
* <p>This constructor initializes a new instance of the ZoneEntity class and invokes the parent
* constructor from the AbstractEntity class. The ZoneEntity class represents a domain model for a
* DNS zone within the Cloudflare DNS system.
*/
public ZoneEntity() {
super();
}
}
@@ -1,17 +0,0 @@
package codes.thischwa.cf.model;
/**
* Represents a response model that contains multiple {@link ZoneEntity} instances.
*/
public class ZoneMultipleResponse extends AbstractMultipleResponse<ZoneEntity> {
/**
* Constructs a new ZoneMultipleResponse object.
*
* <p>This constructor initializes an instance of ZoneMultipleResponse, which serves as a response
* model containing multiple {@link ZoneEntity} instances.
*/
public ZoneMultipleResponse() {
super();
}
}
@@ -1,5 +0,0 @@
/**
* The model of CloudflareDNS-java.
*/
package codes.thischwa.cf.model;
@@ -1,5 +0,0 @@
/**
* The base package of CloudflareDNS-java.
*/
package codes.thischwa.cf;
@@ -1,113 +0,0 @@
package codes.thischwa.cf;
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.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* Penetration-style tests with malicious and invalid inputs to ensure CfDnsClient behaves safely
* (throws appropriate exceptions, does not crash, and does not create unintended resources).
* <p>
* These tests will be skipped if API_EMAIL or API_KEY are not provided via environment variables.
*/
public class CfClientPenTest {
private static final String ZONE_STR = "mein-d-ns.de"; // existing baseline zone
private static final String API_TOKEN = System.getenv("API_TOKEN");
@BeforeAll
static void checkEnv() {
assumeTrue(API_TOKEN != null && !API_TOKEN.isBlank(), "API_TOKEN not set; skipping pen tests");
}
private CfDnsClient newClient() {
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 CfDnsClientBuilder().withEmailKeyAuth("invalid@example.com", UUID.randomUUID().toString()).build();
assertThrows(CloudflareApiException.class, badClient::zoneList);
}
@Test
@DisplayName("Malicious SLD inputs must not crash and should throw proper exception type")
void testMaliciousSldPatternsDoNotSucceed() throws Exception {
CfDnsClient client = newClient();
ZoneEntity zone = client.zoneGet(ZONE_STR);
List<String> syntacticallyInvalidSlds =
List.of("; rm -rf /", "| cat /etc/passwd", "`shutdown -h now`",
"<script>alert('x')</script>", "\"quoted\"");
List<String> syntacticallyValidOrNotAllowedFromCloudflare =
List.of(".", "..", "../..", "..%2F..%2F", "a".repeat(300), "$(reboot)", "emoji-🥷-忍者",
"doesnotexist", "abcdef12345", "unwahrscheinlich-" + System.currentTimeMillis());
for (String sld : syntacticallyInvalidSlds) {
assertThrows(IllegalArgumentException.class, () -> client.recordList(zone, sld),
"Should throw IllegalArgumentException for invalid SLD '" + sld + "'");
}
for (String sld : syntacticallyValidOrNotAllowedFromCloudflare) {
assertThrows(CloudflareNotFoundException.class, () -> client.recordList(zone, sld),
"Should throw CloudflareNotFoundException for valid but non-existing SLD '" + sld + "'");
}
}
@Test
@DisplayName("Invalid record content and TTL boundaries must be rejected by API")
void testInvalidRecordCreateInputsRejected() throws Exception {
CfDnsClient client = newClient();
ZoneEntity zone = client.zoneGet(ZONE_STR);
String sld = "pentest-" + System.currentTimeMillis();
String fqdn = sld + "." + ZONE_STR;
// Ensure clean state and guarantee cleanup later
try {
client.recordDeleteTypeIfExists(zone, sld, RecordType.A, RecordType.AAAA, RecordType.CNAME);
} catch (Exception ignored) {
}
try {
// A record with invalid IPv4
assertThrows(CloudflareApiException.class,
() -> client.recordCreate(zone, fqdn, 60, RecordType.A, "999.999.999.999"));
// AAAA record with non-IP content
assertThrows(CloudflareApiException.class,
() -> client.recordCreate(zone, fqdn, 60, RecordType.AAAA, "not-an-ipv6"));
// TTL boundary checks
assertThrows(CloudflareApiException.class,
() -> client.recordCreate(zone, fqdn, -1, RecordType.A, "130.0.0.3"));
} finally {
// Best-effort cleanup in case anything slipped through
assertDoesNotThrow(
() -> client.recordDeleteTypeIfExists(zone, sld, RecordType.A, RecordType.AAAA,
RecordType.CNAME));
}
}
@Test
@DisplayName("recordDeleteTypeIfExists must be safe on non-existing SLD and types")
void testDeleteTypeIfExistsOnNonExistingIsSafe() throws Exception {
CfDnsClient client = newClient();
ZoneEntity zone = client.zoneGet(ZONE_STR);
String randomSld = "nonexist-" + System.currentTimeMillis();
// Should not throw even if nothing exists
assertDoesNotThrow(
() -> client.recordDeleteTypeIfExists(zone, randomSld, RecordType.A, RecordType.AAAA));
}
}
@@ -1,448 +0,0 @@
package codes.thischwa.cf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
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.model.BatchEntry;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@Slf4j
public class CfClientTest {
private static final String ZONE_STR = "mein-d-ns.de";
private static final String SLD_STR = "devsld";
private static final int TTL = 60;
private static final String API_TOKEN = System.getenv("API_TOKEN");
private final CfDnsClient client = new CfDnsClientBuilder().withEmptyResultThrowsException(true).withApiTokenAuth(API_TOKEN).build();
@BeforeAll
static void checkEnv() {
assumeTrue(API_TOKEN != null && !API_TOKEN.isBlank(), "API_TOKEN not set; skipping client tests");
}
@Test
void testUnknownSld() throws Exception {
ZoneEntity zone = client.zoneGet(ZONE_STR);
assertThrows(CloudflareNotFoundException.class, () -> client.recordList(zone, "unknown", RecordType.A));
}
@Test
void testAddHost() throws Exception {
ZoneEntity zone = client.zoneGet(ZONE_STR);
try {
// clean-up
client.recordDeleteTypeIfExists(zone, SLD_STR, RecordType.A, RecordType.AAAA);
RecordEntity record = RecordEntity.build(SLD_STR, RecordType.A, TTL, "127.0.0.1");
RecordEntity createdRecord = client.recordCreate(zone, record);
assertNotNull(createdRecord.getId());
assertEquals(SLD_STR, createdRecord.getSld());
assertEquals(RecordType.A.getType(), createdRecord.getType());
assertEquals(TTL, createdRecord.getTtl());
assertEquals("127.0.0.1", createdRecord.getContent());
assertNotNull(createdRecord.getCreatedOn());
List<RecordEntity> records = client.recordList(zone, SLD_STR, RecordType.A);
assertEquals(1, records.size());
RecordEntity fetchedRecord = records.get(0);
assertEquals(createdRecord.getId(), fetchedRecord.getId());
assertEquals(createdRecord.getContent(), fetchedRecord.getContent());
assertEquals(createdRecord.getType(), fetchedRecord.getType());
client.recordDelete(zone, createdRecord);
// test A and AAAA records for the same SLD
RecordEntity recordA = RecordEntity.build(SLD_STR, RecordType.A, TTL, "127.0.0.2");
RecordEntity recordAAAA = RecordEntity.build(SLD_STR, RecordType.AAAA, TTL, "2001:db8::1");
RecordEntity createdRecordA = client.recordCreate(zone, recordA);
RecordEntity createdRecordAAAA = client.recordCreate(zone, recordAAAA);
assertNotNull(createdRecordA.getId());
assertNotNull(createdRecordAAAA.getId());
assertEquals(SLD_STR, createdRecordA.getSld());
assertEquals(SLD_STR, createdRecordAAAA.getSld());
assertEquals(RecordType.A.getType(), createdRecordA.getType());
assertEquals(RecordType.AAAA.getType(), createdRecordAAAA.getType());
client.recordDeleteTypeIfExists(zone, SLD_STR, RecordType.A, RecordType.AAAA);
assertThrows(CloudflareNotFoundException.class,
() -> client.recordList(zone, SLD_STR, RecordType.A, RecordType.AAAA));
} finally {
// cleanup in case of failures during test
try {
client.recordDeleteTypeIfExists(zone, SLD_STR, RecordType.A, RecordType.AAAA);
} catch (Exception e) { /* ignore */ }
}
}
@Test
void testZoneListAnlFailedSldList() throws Exception {
List<ZoneEntity> zList = client.zoneList();
assertEquals(1, zList.size());
assertThrows(CloudflareNotFoundException.class,
() -> client.recordList(zList.get(0), "not-existing"));
}
@Test
void testDns() throws Exception {
// starting point: already existing zone 'mein-d-ns.de'
ZoneEntity z = client.zoneGet(ZONE_STR);
assertEquals("cf9d8b12f61423f280e0a3ea2a96d921", z.getId());
assertEquals("mein-d-ns.de", z.getName());
assertEquals("active", z.getStatus());
assertEquals(2, z.getNameServers().size());
assertTrue(z.getNameServers().contains("rafe.ns.cloudflare.com"));
assertTrue(z.getOriginalNameServers().size() >= 2);
assertTrue(z.getOriginalNameServers().contains("blair.ns.cloudflare.com"));
assertNotNull(z.getActivatedOn());
assertNotNull(z.getModifiedOn());
assertNotNull(z.getCreatedOn());
// for all other functions use a newly created SLD and ensure cleanup at the end
String randomSld = SLD_STR + "-" + System.currentTimeMillis();
String domain = randomSld + "." + ZONE_STR;
RecordEntity r;
RecordEntity createdRe1;
RecordEntity createdRe2;
try {
// ensure clean state
client.recordDeleteTypeIfExists(z, randomSld, RecordType.A, RecordType.AAAA);
// create A record using recordCreate with full domain
createdRe1 =
client.recordCreate(z, RecordEntity.build(domain, RecordType.A, TTL, "130.0.0.3"));
assertNotNull(createdRe1.getId());
assertEquals(randomSld + "." + ZONE_STR, createdRe1.getName());
assertEquals(randomSld, createdRe1.getSld());
assertEquals(RecordType.A.getType(), createdRe1.getType());
assertEquals(z.getId(), createdRe1.getZoneId());
assertEquals(TTL, createdRe1.getTtl());
assertEquals("130.0.0.3", createdRe1.getContent());
assertNotNull(createdRe1.getCreatedOn());
assertNotNull(createdRe1.getModifiedOn());
// verify recordList for A
List<RecordEntity> aRecords = client.recordList(z, randomSld, RecordType.A);
assertEquals(1, aRecords.size());
r = aRecords.get(0);
assertEquals("130.0.0.3", r.getContent());
// create AAAA record using recordCreateSld
createdRe2 =
client.recordCreateSld(z, randomSld, TTL, RecordType.AAAA, "2a0a:4cc0:c0:2e4::1");
List<RecordEntity> aaaaRecords = client.recordList(z, randomSld, RecordType.AAAA);
assertEquals(1, aaaaRecords.size());
r = aaaaRecords.get(0);
assertEquals(z.getId(), r.getZoneId());
assertEquals("2a0a:4cc0:c0:2e4::1", r.getContent());
assertEquals(RecordType.AAAA.getType(), r.getType());
// test recordList
List<RecordEntity> rList = client.recordList(z, randomSld);
assertEquals(2, rList.size());
for (RecordEntity re : rList) {
assertEquals(z.getId(), re.getZoneId());
if (Objects.equals(re.getType(), RecordType.A.getType())) {
assertEquals("130.0.0.3", re.getContent());
} else if (Objects.equals(re.getType(), RecordType.AAAA.getType())) {
assertEquals("2a0a:4cc0:c0:2e4::1", re.getContent());
} else {
fail(String.format("Unexpected record type: %s", re.getType()));
}
}
// test recordList without SLD
List<RecordEntity> fullList = client.recordList(z);
assertTrue(fullList.size() >= 2);
assertTrue(fullList.stream().anyMatch(re -> re.getId().equals(createdRe1.getId())));
assertTrue(fullList.stream().anyMatch(re -> re.getId().equals(createdRe2.getId())));
assertTrue(
fullList.stream().allMatch(re -> re.getType().equals(RecordType.A.getType()) || re.getType().equals(RecordType.AAAA.getType())));
// test recordList with types without SLD
List<RecordEntity> aList = client.recordList(z, RecordType.A);
assertFalse(aList.isEmpty());
assertTrue(aList.size() >= 1);
assertTrue(aList.stream().anyMatch(re -> re.getId().equals(createdRe1.getId())));
assertTrue(aList.stream().noneMatch(re -> re.getId().equals(createdRe2.getId())));
assertTrue(aList.stream().allMatch(re -> re.getType().equals(RecordType.A.getType())));
// test fluent api list
List<RecordEntity> fluentList = client.zone(ZONE_STR).list(RecordType.A);
assertFalse(fluentList.isEmpty());
assertTrue(fluentList.stream().anyMatch(re -> re.getId().equals(createdRe1.getId())));
// update AAAA record
createdRe2.setContent("2a0a:4cc0:c0:2e4::2");
client.recordUpdate(z, createdRe2);
aaaaRecords = client.recordList(z, randomSld, RecordType.AAAA);
assertEquals(1, aaaaRecords.size());
r = aaaaRecords.get(0);
assertEquals("2a0a:4cc0:c0:2e4::2", r.getContent());
// verify A record still intact
aRecords = client.recordList(z, randomSld, RecordType.A);
assertEquals(1, aRecords.size());
r = aRecords.get(0);
assertEquals("130.0.0.3", r.getContent());
// delete AAAA record and verify it's gone
assertTrue(client.recordDelete(z, createdRe2));
assertThrows(CloudflareNotFoundException.class,
() -> client.recordList(z, randomSld, RecordType.AAAA));
// delete A record using helper and verify it's gone
client.recordDeleteTypeIfExists(z, randomSld, RecordType.A);
assertThrows(CloudflareNotFoundException.class,
() -> client.recordList(z, randomSld, RecordType.A));
} finally {
// cleanup in case of failures during test
try {
client.recordDeleteTypeIfExists(z, randomSld, RecordType.A, RecordType.AAAA);
} catch (Exception e) { /* ignore */ }
}
}
@Test
void testRecordEntityInvalidType() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> RecordEntity.build("id123", "example.com", "INVALID_TYPE", 60, "192.168.1.1"));
assertTrue(exception.getMessage().contains("Invalid record type: INVALID_TYPE"));
assertTrue(exception.getMessage().contains("Must be one of:"));
}
private static final String IP_PREFIX = "130.0.0.";
private static final String UPDATED_IP_PREFIX = "130.1.0.";
@Test
void testBatch() throws Exception {
// starting point: already existing zone 'mein-d-ns.de'
ZoneEntity zone = client.zoneGet(ZONE_STR);
List<String> sldNames = createSldNames();
List<RecordEntity> initialRecords = createInitialRecords(sldNames);
cleanupRecords(zone, sldNames);
try {
testBatchPost(zone, initialRecords, sldNames);
testBatchPatch(zone, sldNames);
testBatchDelete(zone, sldNames);
testBatchPut(zone, sldNames);
} finally {
cleanupRecords(zone, sldNames);
}
}
private List<String> createSldNames() {
return List.of(SLD_STR + "-1", SLD_STR + "-2", SLD_STR + "-3");
}
private List<RecordEntity> createInitialRecords(List<String> sldNames) {
List<RecordEntity> records = new ArrayList<>();
for (int i = 0; i < sldNames.size(); i++) {
records.add(RecordEntity.build(sldNames.get(i), RecordType.A, TTL, IP_PREFIX + (i + 1)));
}
return records;
}
private void cleanupRecords(ZoneEntity zone, List<String> sldNames) {
sldNames.forEach(sld -> {
try {
client.recordDeleteTypeIfExists(zone, sld, RecordType.A);
} catch (CloudflareApiException e) {
throw new RuntimeException(e);
}
});
}
private void testBatchPost(ZoneEntity zone, List<RecordEntity> records, List<String> sldNames) throws Exception {
// Use only first 2 records for POST
List<RecordEntity> postRecords = records.subList(0, 2);
BatchEntry batchEntry = client.recordBatch(zone, postRecords, null, null, null);
assertEquals(2, batchEntry.getPosts().size());
RecordEntity batchedRecord = batchEntry.getPosts().get(0);
assertValidBatchedRecord(batchedRecord, postRecords.get(0));
// Verify only the first 2 records
for (int i = 0; i < 2; i++) {
List<RecordEntity> records1 = client.recordList(zone, sldNames.get(i), RecordType.A);
assertEquals(1, records1.size());
assertEquals(IP_PREFIX + (i + 1), records1.get(0).getContent());
}
}
private void testBatchPatch(ZoneEntity zone, List<String> sldNames) throws Exception {
// Use first 2 records for PATCH
List<RecordEntity> patchRecords = new ArrayList<>();
for (int i = 0; i < 2; i++) {
List<RecordEntity> records = client.recordList(zone, sldNames.get(i), RecordType.A);
RecordEntity record = records.get(0);
record.setContent(UPDATED_IP_PREFIX + (i + 1));
patchRecords.add(record);
}
client.recordBatch(zone, null, null, patchRecords, null);
// Verify both records were updated
for (int i = 0; i < 2; i++) {
List<RecordEntity> updatedRecords = client.recordList(zone, sldNames.get(i), RecordType.A);
assertEquals(1, updatedRecords.size());
assertEquals(UPDATED_IP_PREFIX + (i + 1), updatedRecords.get(0).getContent());
}
}
private void testBatchDelete(ZoneEntity zone, List<String> sldNames) throws Exception {
// Delete first 2 records
List<RecordEntity> deleteRecords = new ArrayList<>();
for (int i = 0; i < 2; i++) {
List<RecordEntity> records = client.recordList(zone, sldNames.get(i), RecordType.A);
deleteRecords.add(records.get(0));
}
client.recordBatch(zone, null, null, null, deleteRecords);
// Verify both records are deleted
for (int i = 0; i < 2; i++) {
String sldName = sldNames.get(i);
assertThrows(CloudflareNotFoundException.class,
() -> client.recordList(zone, sldName, RecordType.A));
}
}
private void testBatchPut(ZoneEntity zone, List<String> sldNames) throws Exception {
// Create 2 new records first for PUT test
List<RecordEntity> newRecords = new ArrayList<>();
for (int i = 0; i < 2; i++) {
RecordEntity record = RecordEntity.build(sldNames.get(i), RecordType.A, TTL, IP_PREFIX + (i + 1));
newRecords.add(record);
}
client.recordBatch(zone, newRecords, null, null, null);
// Now use PUT to replace them
List<RecordEntity> putRecords = new ArrayList<>();
for (int i = 0; i < 2; i++) {
List<RecordEntity> records = client.recordList(zone, sldNames.get(i), RecordType.A);
RecordEntity record = records.get(0);
record.setContent(UPDATED_IP_PREFIX + (i + 1));
putRecords.add(record);
}
client.recordBatch(zone, null, putRecords, null, null);
// Verify both records were updated
for (int i = 0; i < 2; i++) {
List<RecordEntity> updatedRecords = client.recordList(zone, sldNames.get(i), RecordType.A);
assertEquals(1, updatedRecords.size());
assertEquals(UPDATED_IP_PREFIX + (i + 1), updatedRecords.get(0).getContent());
}
}
private void assertValidBatchedRecord(RecordEntity batchedRecord, RecordEntity originalRecord) {
assertNotNull(batchedRecord.getId());
assertEquals(originalRecord.getSld(), batchedRecord.getSld());
assertEquals(originalRecord.getType(), batchedRecord.getType());
assertNotNull(batchedRecord.getCreatedOn());
}
@Test
void testFluentApi() throws Exception {
ZoneEntity zone = client.zoneGet(ZONE_STR);
String fluentSld = "fluent-" + System.currentTimeMillis();
try {
// Test fluent create
RecordEntity created = client.zone(ZONE_STR)
.record(fluentSld)
.create(RecordType.A, "192.168.100.1", TTL);
assertNotNull(created.getId());
assertEquals(fluentSld, created.getSld());
assertEquals("192.168.100.1", created.getContent());
// Test fluent get
List<RecordEntity> records = client.zone(ZONE_STR)
.record(fluentSld, RecordType.A)
.get();
assertEquals(1, records.size());
assertEquals("192.168.100.1", records.get(0).getContent());
// Test fluent update
RecordEntity updated = client.zone(ZONE_STR)
.record(fluentSld, RecordType.A)
.update("192.168.100.2");
assertEquals("192.168.100.2", updated.getContent());
// Test fluent delete
client.zone(ZONE_STR)
.record(fluentSld)
.delete(RecordType.A);
assertThrows(CloudflareNotFoundException.class,
() -> client.zone(ZONE_STR).record(fluentSld, RecordType.A).get());
} finally {
try {
client.recordDeleteTypeIfExists(zone, fluentSld, RecordType.A);
} catch (Exception e) { /* ignore */ }
}
}
@Test
void testGroupRecordsByFqdn_withValidRecords() {
// Arrange
RecordEntity rec1 = RecordEntity.build("1", "example.com.", "A", 300, "192.168.1.1");
RecordEntity rec2 = RecordEntity.build("2", "example.com.", "AAAA", 300, "::1");
RecordEntity rec3 = RecordEntity.build("3", "sub.example.com.", "CNAME", 300, "example.com.");
List<RecordEntity> records = Arrays.asList(rec1, rec2, rec3);
// Act
Map<String, List<RecordEntity>> groupedRecords = CfDnsClient.groupRecordsByFqdn(records);
// Assert
assertNotNull(groupedRecords, "Resulting map should not be null.");
assertEquals(2, groupedRecords.size(), "The grouping should result in 2 FQDN keys.");
assertEquals(2, groupedRecords.get("example.com.").size(), "The key 'example.com.' should have 2 records.");
assertEquals(1, groupedRecords.get("sub.example.com.").size(), "The key 'sub.example.com.' should have 1 record.");
}
@Test
void testGroupRecordsByFqdn_withMultipleRecordsSameFqdn() {
// Arrange
RecordEntity rec1 = RecordEntity.build("1", "example.com.", "A", 300, "192.168.1.1");
RecordEntity rec2 = RecordEntity.build("2", "example.com.", "AAAA", 300, "::1");
List<RecordEntity> records = Arrays.asList(rec1, rec2);
// Act
Map<String, List<RecordEntity>> groupedRecords = CfDnsClient.groupRecordsByFqdn(records);
// Assert
assertNotNull(groupedRecords, "Resulting map should not be null.");
assertEquals(1, groupedRecords.size(), "The grouping should result in 1 FQDN key.");
assertEquals(2, groupedRecords.get("example.com.").size(), "The key 'example.com.' should have 2 records.");
}
}
@@ -1,58 +0,0 @@
package codes.thischwa.cf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
public class CfRequestTest {
@Test
public void testBuildPathWithSingleVariable() {
String result = CfRequest.RECORD_CREATE.buildPath("zone123");
assertEquals("/zones/zone123/dns_records", result);
}
@Test
public void testBuildPathWithMultipleVariables() {
String result = CfRequest.RECORD_UPDATE.buildPath("zone123", "record456");
assertEquals("/zones/zone123/dns_records/record456", result);
}
@Test
public void testBuildPathWithoutVariables() {
String result = CfRequest.ZONE_LIST.buildPath();
assertEquals("/zones", result);
}
@Test
public void testBuildRecordInfoName() {
String result = CfRequest.RECORD_INFO_NAME.buildPath("zone123", "sub.domain.com");
assertEquals("/zones/zone123/dns_records?name=sub.domain.com", result);
}
@Test
public void testBuildRecordDelete() {
String result = CfRequest.RECORD_DELETE.buildPath("zone123", "record789");
assertEquals("/zones/zone123/dns_records/record789", result);
}
@Test
public void testBuildZoneInfo() {
String result = CfRequest.ZONE_INFO.buildPath("zone123");
assertEquals("/zones?name=zone123", result);
}
@Test
public void testBuildRecordInfo() {
String result = CfRequest.RECORD_INFO_NAME.buildPath("zone123", "sld.domain.com");
assertEquals("/zones/zone123/dns_records?name=sld.domain.com", result);
}
@Test
public void testBuildPathInvalidArguments() {
assertThrows(
IllegalArgumentException.class,
() -> CfRequest.RECORD_UPDATE.buildPath("zone123"));
}
}
@@ -1,53 +0,0 @@
package codes.thischwa.cf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import codes.thischwa.cf.model.AbstractResponse;
import codes.thischwa.cf.model.BatchResponse;
import codes.thischwa.cf.model.RecordMultipleResponse;
import codes.thischwa.cf.model.RecordSingleResponse;
import codes.thischwa.cf.model.ResponseResultInfo;
import codes.thischwa.cf.model.ZoneMultipleResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import org.junit.jupiter.api.Test;
public class ObjectMapperTest {
private final ObjectMapper mapper = JsonConf.initObjectMapper();
@Test
void testObjectMapper() throws IOException {
ZoneMultipleResponse resp =
mapper.readValue(this.getClass().getResourceAsStream("/zone-list-response.json"),
ZoneMultipleResponse.class);
assertNotNull(resp.getResponseResultInfo());
}
@Test
void testErrorResponse() throws IOException {
List<Class<? extends AbstractResponse>> respClasses =
List.of(RecordSingleResponse.class, RecordMultipleResponse.class, ZoneMultipleResponse.class, BatchResponse.class);
respClasses.forEach(this::assertErrorResponse);
}
private void assertErrorResponse(Class<? extends AbstractResponse> clazz) {
InputStream in = this.getClass().getResourceAsStream("/error-response.json");
try {
AbstractResponse resp = mapper.readValue(in, clazz);
assertNotNull(resp);
assertNotNull(resp.getResponseResultInfo());
ResponseResultInfo resultInfo = resp.getResponseResultInfo();
assertFalse(resultInfo.isSuccess());
assertEquals(1, resultInfo.getErrors().size());
assertEquals(81053, resultInfo.getErrors().get(0).getCode());
} catch (IOException e) {
fail("fail for " + clazz + ": " + e.getMessage());
}
}
}
@@ -1,133 +0,0 @@
package codes.thischwa.cf;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
import codes.thischwa.cf.model.AbstractResponse;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordMultipleResponse;
import codes.thischwa.cf.model.RecordSingleResponse;
import codes.thischwa.cf.model.ResponseResultInfo;
import codes.thischwa.cf.model.ResultInfo;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class ResponseValidatorTest {
@Mock
private AbstractResponse mockResponse;
@Mock
private ResponseResultInfo mockResultInfo;
@Mock
private RecordMultipleResponse mockMultipleResponse;
@Mock
private RecordSingleResponse mockSingleResponse;
@Mock
private RecordEntity mockRecordEntity;
private ResponseValidator validatorWithException;
private ResponseValidator validatorWithoutException;
@BeforeEach
void setUp() {
validatorWithException = new ResponseValidator(true);
validatorWithoutException = new ResponseValidator(false);
}
@Test
void validateSuccessfulResponse() {
when(mockResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
assertDoesNotThrow(() -> validatorWithException.validate(mockResponse, false));
}
@Test
void validateFailedResponse() {
List<ResponseResultInfo.Error> errors = new ArrayList<>();
ResponseResultInfo.Error error = new ResponseResultInfo.Error(1, "Fehler 1");
errors.add(error);
error = new ResponseResultInfo.Error(2, "Fehler 2");
errors.add(error);
when(mockResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(false);
when(mockResultInfo.getErrors()).thenReturn(errors);
CloudflareApiException exception = assertThrows(CloudflareApiException.class,
() -> validatorWithException.validate(mockResponse, false));
assertTrue(exception.getMessage().contains("Fehler 1"));
assertTrue(exception.getMessage().contains("Fehler 2"));
}
@Test
void validateSingleResultExpectedButMultipleFound() {
when(mockMultipleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
when(mockMultipleResponse.getResultInfo()).thenReturn(new ResultInfo(2));
CloudflareApiException exception = assertThrows(CloudflareApiException.class,
() -> validatorWithException.validate(mockMultipleResponse, true));
assertTrue(exception.getMessage().contains("Unexpected result count: 2"));
}
@Test
void validateEmptyResultWithExceptionEnabled() {
when(mockMultipleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
when(mockMultipleResponse.getResultInfo()).thenReturn(new ResultInfo(0));
assertThrows(CloudflareNotFoundException.class,
() -> validatorWithException.validate(mockMultipleResponse, false));
}
@Test
void validateEmptyResultWithExceptionDisabled() {
when(mockMultipleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
when(mockMultipleResponse.getResultInfo()).thenReturn(new ResultInfo(0));
assertDoesNotThrow(() -> validatorWithoutException.validate(mockMultipleResponse, true));
}
@Test
void validateSingleResultWithNullResultAndExceptionEnabled() {
when(mockSingleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
when(mockSingleResponse.getResult()).thenReturn(null);
assertThrows(CloudflareNotFoundException.class,
() -> validatorWithException.validate(mockSingleResponse, false));
}
@Test
void validateSingleResultWithNullResultAndExceptionDisabled() {
when(mockSingleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
// mockSingleResponse.getResult() returns null by default (no stubbing needed)
assertDoesNotThrow(() -> validatorWithoutException.validate(mockSingleResponse, false));
}
@Test
void validateSingleResultWithValidResult() {
when(mockSingleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
when(mockSingleResponse.getResult()).thenReturn(mockRecordEntity);
assertDoesNotThrow(() -> validatorWithException.validate(mockSingleResponse, false));
assertDoesNotThrow(() -> validatorWithoutException.validate(mockSingleResponse, false));
}
}
@@ -1,21 +0,0 @@
package codes.thischwa.cf.model;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class PagingRequestTest {
@Test
public void testBuildPath() {
String result = PagingRequest.defaultPaging().addQueryString("/zones");
assertEquals("/zones?page=1&perPage=5000000", result);
}
@Test
public void testBuildPathAdditional() {
String result = new PagingRequest( 10, 100).addQueryString("/zones?foo=bar");
assertEquals("/zones?foo=bar&page=10&perPage=100", result);
}
}
-11
View File
@@ -1,11 +0,0 @@
{
"result": null,
"success": false,
"errors": [
{
"code": 81053,
"message": "An A, AAAA, or CNAME record with that host already exists. For more details, refer to \u003chttps://developers.cloudflare.com/dns/manage-dns-records/troubleshooting/records-with-same-name/\u003e."
}
],
"messages": []
}
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="current"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%t] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.apache.hc.client5.http" level="info"/>
<logger name="codes.thischwa.cf.CfBasicHttpClient" level="trace"/>
<root level="debug">
<appender-ref ref="current"/>
</root>
</configuration>
-174
View File
@@ -1,174 +0,0 @@
{
"result": [
{
"id": "0a83dd6e7f8c46039f2517bbded8115e",
"name": "mein-d-ns.de",
"status": "active",
"paused": false,
"type": "full",
"development_mode": -230181,
"name_servers": [
"blair.ns.cloudflare.com",
"sergi.ns.cloudflare.com"
],
"original_name_servers": [
"c.ns14.net",
"b.ns14.net",
"a.ns14.net",
"d.ns14.net"
],
"original_registrar": null,
"original_dnshost": null,
"modified_on": "2025-01-20T09:06:52.122538Z",
"created_on": "2025-01-20T08:56:34.736214Z",
"activated_on": "2025-01-20T09:06:52.122538Z",
"meta": {
"step": 2,
"custom_certificate_quota": 0,
"page_rule_quota": 3,
"phishing_detected": false
},
"owner": {
"id": null,
"type": "user",
"email": null
},
"account": {
"id": "563335e3d73549dbbc0620f4ce82d527",
"name": "myAccount"
},
"tenant": {
"id": null,
"name": null
},
"tenant_unit": {
"id": null
},
"permissions": [
"#image:read",
"#image:edit",
"#worker:edit",
"#analytics:read",
"#ssl:edit",
"#zone_settings:read",
"#organization:edit",
"#waf:read",
"#waf:edit",
"#logs:read",
"#member:read",
"#worker:read",
"#blocks:read",
"#blocks:edit",
"#access:read",
"#access:edit",
"#billing:read",
"#teams:read",
"#organization:read",
"#logs:edit",
"#zaraz:publish",
"#zone_settings:edit",
"#fbm:read",
"#subscription:read",
"#lb:read",
"#waitingroom:edit",
"#ssl:read",
"#subscription:edit",
"#dns_records:read",
"#dns_records:edit",
"#api_gateway:read",
"#api_gateway:edit",
"#cds_compute_account:edit",
"#ces_submissions:read",
"#fbm:edit",
"#healthchecks:edit",
"#stream:edit",
"#lb:edit",
"#healthchecks:read",
"#billing:edit",
"#member:edit",
"#cfone_read",
"#cfone_edit",
"#integration:read",
"#zaraz:edit",
"#magic:read",
"#magic:edit",
"#zaraz:read",
"#stream:read",
"#integration:install",
"#dash_sso:edit",
"#zone:read",
"#http_applications:read",
"#http_applications:edit",
"#ces_analytics:read",
"#resilience:edit",
"#teams:edit",
"#r2_bucket_warehouse:read",
"#r2_bucket_warehouse:edit",
"#query_cache:read",
"#query_cache:edit",
"#dex:read",
"#zone_versioning:read",
"#zone_versioning:edit",
"#cds:read",
"#ces_search:action",
"#ces_search:read",
"#ces_search:preview",
"#ces_search:raw",
"#ces_search:trace",
"#fbm_acc:edit",
"#teams:pii",
"#r2_bucket_item:read",
"#r2_bucket_item:edit",
"#integration:edit",
"#resilience:read",
"#zone:edit",
"#dash_sso:read",
"#legal:read",
"#ces_integration:edit",
"#ces_integration:read",
"#cache_purge:edit",
"#vectorize:read",
"#vectorize:edit",
"#auditlogs:read",
"#teams:report",
"#ces_settings:edit",
"#ces_settings:read",
"#waitingroom:read",
"#web3:read",
"#web3:edit",
"#dex:edit",
"#cds_compute_account:read",
"#cds:edit",
"#r2_bucket:read",
"#r2_bucket:edit",
"#ces_phishguard:read",
"#page_shield:read",
"#page_shield:edit",
"#legal:edit",
"#app:edit"
],
"plan": {
"id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"name": "Free Website",
"price": 0,
"currency": "USD",
"frequency": "",
"is_subscribed": false,
"can_subscribe": false,
"legacy_id": "free",
"legacy_discount": false,
"externally_managed": false
}
}
],
"result_info": {
"page": 1,
"per_page": 20,
"total_pages": 1,
"count": 1,
"total_count": 1
},
"success": true,
"errors": [],
"messages": []
}