From 1e7c1b8e9eef9ce08f9a77cd9d3489f6c611b426 Mon Sep 17 00:00:00 2001 From: William Callahan Date: Thu, 15 Jan 2026 01:14:33 -0800 Subject: [PATCH 1/7] fix: switch to manual GPG signing configuration - Use Gradle signing plugin directly with useInMemoryPgpKeys() - Read GPG_PRIVATE_KEY and GPG_PASSPHRASE from environment - Update workflows to pass correct env vars --- .github/workflows/CI.yaml | 5 ++--- .github/workflows/Release.yaml | 5 ++--- build.gradle.kts | 12 +++++++++++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 87d7796..93a864a 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -39,9 +39,8 @@ jobs: env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} - ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }} - ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: ./gradlew publishAllPublicationsToMavenCentralRepository -x test - name: Update dependency graph diff --git a/.github/workflows/Release.yaml b/.github/workflows/Release.yaml index c5896a1..f42e4f6 100644 --- a/.github/workflows/Release.yaml +++ b/.github/workflows/Release.yaml @@ -13,9 +13,8 @@ jobs: env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} - ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }} - ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} steps: - uses: actions/checkout@v6 diff --git a/build.gradle.kts b/build.gradle.kts index 7a3214c..e8a41fe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,7 @@ import java.io.FileInputStream plugins { id("com.vanniktech.maven.publish") version "0.35.0" `java-library` + signing } // Load .env file if it exists @@ -162,7 +163,16 @@ tasks.withType().configureEach { mavenPublishing { publishToMavenCentral(automaticRelease = true) - signAllPublications() +} + +// Manual signing configuration using environment variables +signing { + val signingKey = providers.environmentVariable("GPG_PRIVATE_KEY").orNull + val signingPassword = providers.environmentVariable("GPG_PASSPHRASE").orNull + if (signingKey != null) { + useInMemoryPgpKeys(signingKey, signingPassword ?: "") + sign(publishing.publications) + } } // Fix task dependency issue with Gradle 9.x and vanniktech plugin From 49ef11fb6b549384eb4330abbddd98bacfd6fb9d Mon Sep 17 00:00:00 2001 From: William Callahan Date: Thu, 15 Jan 2026 01:17:27 -0800 Subject: [PATCH 2/7] chore: switch to nmcp + maven-publish, update action versions - Replace vanniktech plugin with nmcp + maven-publish (matches tui4j) - Use SONATYPE_USERNAME/PASSWORD env vars directly - Update to actions/checkout@v6, gradle/actions/setup-gradle@v5 - Use publishAllPublicationsToCentralPortal tasks --- .github/workflows/CI.yaml | 13 ++----- .github/workflows/Release.yaml | 6 +-- build.gradle.kts | 67 +++++++++++++++++++++++++++++----- 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 93a864a..74d2b4e 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -28,20 +28,15 @@ jobs: - name: Build and test run: ./gradlew build - - name: Read version - id: version - run: | - VERSION=$(grep '^VERSION_NAME=' gradle.properties | cut -d= -f2) - echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - name: Publish SNAPSHOT (main only) - if: github.event_name == 'push' && github.ref == 'refs/heads/main' && endsWith(steps.version.outputs.VERSION, 'SNAPSHOT') + if: github.event_name == 'push' && github.ref == 'refs/heads/main' env: - ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }} - ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - run: ./gradlew publishAllPublicationsToMavenCentralRepository -x test + run: ./gradlew publishAllPublicationsToCentralPortalSnapshots -x test - name: Update dependency graph uses: gradle/actions/dependency-submission@v5 diff --git a/.github/workflows/Release.yaml b/.github/workflows/Release.yaml index f42e4f6..64165ad 100644 --- a/.github/workflows/Release.yaml +++ b/.github/workflows/Release.yaml @@ -11,8 +11,8 @@ jobs: release: runs-on: ubuntu-latest env: - ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }} - ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} @@ -45,4 +45,4 @@ jobs: run: ./gradlew build -Pversion=${{ steps.version.outputs.VERSION }} - name: Deploy to Maven Central - run: ./gradlew publishAllPublicationsToMavenCentralRepository -Pversion=${{ steps.version.outputs.VERSION }} + run: ./gradlew publishAllPublicationsToCentralPortal -Pversion=${{ steps.version.outputs.VERSION }} diff --git a/build.gradle.kts b/build.gradle.kts index e8a41fe..626c2a2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,9 +14,10 @@ import java.util.Properties import java.io.FileInputStream plugins { - id("com.vanniktech.maven.publish") version "0.35.0" `java-library` + `maven-publish` signing + id("com.gradleup.nmcp") version "1.2.1" } // Load .env file if it exists @@ -161,21 +162,69 @@ tasks.withType().configureEach { (options as StandardJavadocDocletOptions).addStringOption("-release", javaTargetVersion.toString()) } -mavenPublishing { - publishToMavenCentral(automaticRelease = true) +publishing { + publications { + create("mavenJava") { + from(components["java"]) + artifactId = providers.gradleProperty("POM_ARTIFACT_ID").orNull ?: "apple-maps-java" + + pom { + name.set(providers.gradleProperty("POM_NAME").orNull ?: "Apple Maps Java") + description.set(providers.gradleProperty("POM_DESCRIPTION").orNull ?: "Apple Maps Java implements the Apple Maps Server API for use in JVMs.") + url.set(providers.gradleProperty("POM_URL").orNull ?: "https://github.com/WilliamAGH/apple-maps-java") + + licenses { + license { + name.set(providers.gradleProperty("POM_LICENSE_NAME").orNull ?: "MIT License") + url.set(providers.gradleProperty("POM_LICENSE_URL").orNull ?: "https://opensource.org/license/mit") + } + } + + developers { + developer { + id.set(providers.gradleProperty("POM_DEVELOPER_ID").orNull ?: "WilliamAGH") + name.set(providers.gradleProperty("POM_DEVELOPER_NAME").orNull ?: "William Callahan") + url.set(providers.gradleProperty("POM_DEVELOPER_URL").orNull ?: "https://github.com/WilliamAGH/") + } + } + + scm { + url.set(providers.gradleProperty("POM_SCM_URL").orNull ?: "https://github.com/WilliamAGH/apple-maps-java") + connection.set(providers.gradleProperty("POM_SCM_CONNECTION").orNull ?: "scm:git:git://github.com/WilliamAGH/apple-maps-java.git") + developerConnection.set(providers.gradleProperty("POM_SCM_DEV_CONNECTION").orNull ?: "scm:git:ssh://git@github.com/WilliamAGH/apple-maps-java.git") + } + } + } + } + + repositories { + maven { + val releasesUrl = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") + val snapshotsUrl = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") + url = if (version.toString().endsWith("SNAPSHOT")) snapshotsUrl else releasesUrl + + credentials { + username = providers.environmentVariable("SONATYPE_USERNAME").orNull + password = providers.environmentVariable("SONATYPE_PASSWORD").orNull + } + } + } } -// Manual signing configuration using environment variables signing { val signingKey = providers.environmentVariable("GPG_PRIVATE_KEY").orNull val signingPassword = providers.environmentVariable("GPG_PASSPHRASE").orNull - if (signingKey != null) { - useInMemoryPgpKeys(signingKey, signingPassword ?: "") + + if (signingKey != null && signingPassword != null) { + useInMemoryPgpKeys(signingKey, signingPassword) sign(publishing.publications) } } -// Fix task dependency issue with Gradle 9.x and vanniktech plugin -tasks.matching { it.name == "generateMetadataFileForMavenPublication" }.configureEach { - dependsOn(tasks.matching { it.name == "plainJavadocJar" }) +nmcp { + publishAllPublicationsToCentralPortal { + username.set(providers.environmentVariable("SONATYPE_USERNAME").orNull) + password.set(providers.environmentVariable("SONATYPE_PASSWORD").orNull) + publishingType.set("USER_MANAGED") + } } From f96b6f0e86c748bf4badae2714e7b60e3d767461 Mon Sep 17 00:00:00 2001 From: William Callahan Date: Thu, 15 Jan 2026 01:58:40 -0800 Subject: [PATCH 3/7] chore: bump version to 0.1.3 for release Increment version from 0.1.2 to 0.1.3 across all project configuration files and documentation in preparation for the next release. - Update VERSION_NAME to 0.1.3-SNAPSHOT in gradle.properties - Update fallback version to 0.1.3 in build.gradle.kts - Update installation examples in README.md to reference 0.1.3 --- README.md | 6 +++--- build.gradle.kts | 2 +- gradle.properties | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 33816e0..a83554f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A lightweight Java SDK for the Apple Maps Server API, with automatic access-toke ## Installation -Replace `0.1.2` with the latest release. +Replace `0.1.3` with the latest release. Note: this repo’s build uses a Gradle Java toolchain (Java 17). If you don’t have JDK 17 installed locally, Gradle will download it automatically. @@ -18,7 +18,7 @@ Note: this repo’s build uses a Gradle Java toolchain (Java 17). If you don’t ```groovy dependencies { - implementation("com.williamcallahan:apple-maps-java:0.1.2") + implementation("com.williamcallahan:apple-maps-java:0.1.3") } ``` @@ -28,7 +28,7 @@ dependencies { com.williamcallahan apple-maps-java - 0.1.2 + 0.1.3 ``` diff --git a/build.gradle.kts b/build.gradle.kts index 626c2a2..9fec39e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,7 +47,7 @@ val javaTargetVersion = 17 group = providers.gradleProperty("GROUP").orNull ?: "com.williamcallahan" version = providers.gradleProperty("version").orNull ?: providers.gradleProperty("VERSION_NAME").orNull - ?: "0.1.2" + ?: "0.1.3" repositories { mavenCentral() diff --git a/gradle.properties b/gradle.properties index b8dc14d..c744b7d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ GROUP=com.williamcallahan POM_ARTIFACT_ID=apple-maps-java -VERSION_NAME=0.1.2-SNAPSHOT +VERSION_NAME=0.1.3-SNAPSHOT POM_NAME=Apple Maps Java POM_DESCRIPTION=Apple Maps Java implements the Apple Maps Server API for use in JVMs. From 6a262756420e8a8dbfe7cb045fc28623b91e2726 Mon Sep 17 00:00:00 2001 From: William Callahan Date: Thu, 15 Jan 2026 01:58:50 -0800 Subject: [PATCH 4/7] docs: add comprehensive Javadoc across public API Add Javadoc documentation to all public classes, constructors, methods, and enum values to improve developer experience and enable proper API documentation generation. This includes the main client, gateway interface, domain models, request builders, and adapter layer. - Document AppleMaps client constructors and all public methods - Document AppleMapsGateway port interface methods - Add constructor Javadoc to domain records explaining parameter normalization - Document all PoiCategory, AddressCategory, and other enum values - Document request input builders and their fluent methods - Document adapter exceptions and their accessors - Document CLI entry point --- .../williamcallahan/applemaps/AppleMaps.java | 104 ++++++++++++++++ .../jackson/AppleMapsObjectMapperFactory.java | 5 + .../mapsserver/AppleMapsApiException.java | 23 ++++ .../AppleMapsAuthorizationService.java | 11 ++ .../mapsserver/AppleMapsClientException.java | 6 + .../mapsserver/HttpAppleMapsGateway.java | 6 + .../applemaps/cli/AppleMapsCli.java | 8 ++ .../domain/model/AddressCategory.java | 23 ++++ .../domain/model/AlternateIdsEntry.java | 6 + .../domain/model/AlternateIdsResponse.java | 6 + .../domain/model/AutocompleteResult.java | 8 ++ .../domain/model/DirectionsAvoid.java | 8 ++ .../domain/model/DirectionsEndpoint.java | 23 ++++ .../domain/model/DirectionsResponse.java | 9 ++ .../domain/model/DirectionsRoute.java | 10 ++ .../domain/model/DirectionsStep.java | 9 ++ .../applemaps/domain/model/ErrorResponse.java | 6 + .../applemaps/domain/model/EtaEstimate.java | 9 ++ .../applemaps/domain/model/EtaResponse.java | 5 + .../domain/model/PaginationInfo.java | 8 ++ .../applemaps/domain/model/Place.java | 13 ++ .../domain/model/PlaceLookupError.java | 6 + .../domain/model/PlaceLookupErrorCode.java | 9 ++ .../applemaps/domain/model/PlaceResults.java | 5 + .../domain/model/PlacesResponse.java | 6 + .../applemaps/domain/model/PoiCategory.java | 51 ++++++++ .../applemaps/domain/model/RouteLocation.java | 17 +++ .../domain/model/SearchACResultType.java | 10 ++ .../model/SearchAutocompleteResponse.java | 5 + .../domain/model/SearchLocation.java | 17 +++ .../applemaps/domain/model/SearchRegion.java | 25 ++++ .../domain/model/SearchRegionPriority.java | 7 ++ .../domain/model/SearchResponse.java | 7 ++ .../domain/model/SearchResponsePlace.java | 14 +++ .../domain/model/SearchResultType.java | 9 ++ .../domain/model/StructuredAddress.java | 15 +++ .../applemaps/domain/model/TokenResponse.java | 6 + .../applemaps/domain/model/TransportType.java | 9 ++ .../applemaps/domain/model/UserLocation.java | 17 +++ .../domain/port/AppleMapsGateway.java | 66 ++++++++++ .../domain/request/AlternateIdsInput.java | 24 ++++ .../domain/request/DirectionsInput.java | 89 ++++++++++++++ .../applemaps/domain/request/EtaInput.java | 47 +++++++ .../domain/request/GeocodeInput.java | 59 +++++++++ .../domain/request/PlaceLookupInput.java | 31 +++++ .../request/SearchAutocompleteInput.java | 101 +++++++++++++++ .../applemaps/domain/request/SearchInput.java | 115 ++++++++++++++++++ 47 files changed, 1073 insertions(+) diff --git a/src/main/java/com/williamcallahan/applemaps/AppleMaps.java b/src/main/java/com/williamcallahan/applemaps/AppleMaps.java index 30c18a7..3aaeed6 100644 --- a/src/main/java/com/williamcallahan/applemaps/AppleMaps.java +++ b/src/main/java/com/williamcallahan/applemaps/AppleMaps.java @@ -33,41 +33,93 @@ public final class AppleMaps implements AutoCloseable { private final AppleMapsGateway gateway; + /** + * Creates an {@link AppleMaps} client using the provided authorization token and a default timeout. + * + * @param authToken the Apple Maps Server API authorization token + */ public AppleMaps(String authToken) { this(authToken, DEFAULT_TIMEOUT); } + /** + * Creates an {@link AppleMaps} client using the provided authorization token and timeout. + * + * @param authToken the Apple Maps Server API authorization token + * @param timeout request timeout + */ public AppleMaps(String authToken, Duration timeout) { Objects.requireNonNull(authToken, "authToken"); Objects.requireNonNull(timeout, "timeout"); this.gateway = new HttpAppleMapsGateway(authToken, timeout); } + /** + * Creates an {@link AppleMaps} client backed by a custom gateway implementation. + * + * @param gateway the gateway to use for API operations + */ public AppleMaps(AppleMapsGateway gateway) { this.gateway = Objects.requireNonNull(gateway, "gateway"); } + /** + * Performs a geocode request. + * + * @param input geocode request parameters + * @return geocode results + */ public PlaceResults geocode(GeocodeInput input) { return gateway.geocode(input); } + /** + * Performs a search request. + * + * @param input search request parameters + * @return search results + */ public SearchResponse search(SearchInput input) { return gateway.search(input); } + /** + * Performs an autocomplete request. + * + * @param input autocomplete request parameters + * @return autocomplete results + */ public SearchAutocompleteResponse autocomplete(SearchAutocompleteInput input) { return gateway.autocomplete(input); } + /** + * Resolves a completion URL returned from an autocomplete response. + * + * @param completionUrl completion URL to resolve + * @return search results for the completion URL + */ public SearchResponse resolveCompletionUrl(String completionUrl) { return gateway.resolveCompletionUrl(completionUrl); } + /** + * Resolves all completion URLs from an autocomplete response. + * + * @param response an autocomplete response + * @return resolved search responses in the same order as the results + */ public List resolveCompletionUrls(SearchAutocompleteResponse response) { Objects.requireNonNull(response, "response"); return resolveCompletionUrls(response.results()); } + /** + * Resolves all completion URLs from an autocomplete result list. + * + * @param results autocomplete results + * @return resolved search responses in the same order as the results + */ public List resolveCompletionUrls(List results) { Objects.requireNonNull(results, "results"); if (results.isEmpty()) { @@ -92,36 +144,88 @@ public List resolveCompletionUrls(List resul } } + /** + * Performs a reverse geocode request using the default language ({@code en-US}). + * + * @param latitude latitude in decimal degrees + * @param longitude longitude in decimal degrees + * @return reverse geocode results + */ public PlaceResults reverseGeocode(double latitude, double longitude) { return gateway.reverseGeocode(latitude, longitude, DEFAULT_LANGUAGE); } + /** + * Performs a reverse geocode request. + * + * @param latitude latitude in decimal degrees + * @param longitude longitude in decimal degrees + * @param language response language (BCP 47); if {@code null} or blank, defaults to {@code en-US} + * @return reverse geocode results + */ public PlaceResults reverseGeocode(double latitude, double longitude, String language) { String resolvedLanguage = language == null || language.isBlank() ? DEFAULT_LANGUAGE : language; return gateway.reverseGeocode(latitude, longitude, resolvedLanguage); } + /** + * Performs a directions request. + * + * @param input directions request parameters + * @return directions results + */ public DirectionsResponse directions(DirectionsInput input) { return gateway.directions(input); } + /** + * Performs an ETA request. + * + * @param input ETA request parameters + * @return ETA results + */ public EtaResponse etas(EtaInput input) { return gateway.etas(input); } + /** + * Looks up a place by ID using the default language ({@code en-US}). + * + * @param placeId place identifier + * @return a place + */ public Place lookupPlace(String placeId) { return lookupPlace(placeId, DEFAULT_LANGUAGE); } + /** + * Looks up a place by ID. + * + * @param placeId place identifier + * @param language response language (BCP 47); if {@code null} or blank, defaults to {@code en-US} + * @return a place + */ public Place lookupPlace(String placeId, String language) { String resolvedLanguage = language == null || language.isBlank() ? DEFAULT_LANGUAGE : language; return gateway.lookupPlace(placeId, resolvedLanguage); } + /** + * Looks up places using a place lookup input. + * + * @param input place lookup request parameters + * @return places results + */ public PlacesResponse lookupPlaces(PlaceLookupInput input) { return gateway.lookupPlaces(input); } + /** + * Looks up alternate IDs for one or more places. + * + * @param input alternate IDs request parameters + * @return alternate IDs results + */ public AlternateIdsResponse lookupAlternateIds(AlternateIdsInput input) { return gateway.lookupAlternateIds(input); } diff --git a/src/main/java/com/williamcallahan/applemaps/adapters/jackson/AppleMapsObjectMapperFactory.java b/src/main/java/com/williamcallahan/applemaps/adapters/jackson/AppleMapsObjectMapperFactory.java index 0ad8071..c5a98d3 100644 --- a/src/main/java/com/williamcallahan/applemaps/adapters/jackson/AppleMapsObjectMapperFactory.java +++ b/src/main/java/com/williamcallahan/applemaps/adapters/jackson/AppleMapsObjectMapperFactory.java @@ -15,6 +15,11 @@ public final class AppleMapsObjectMapperFactory { private AppleMapsObjectMapperFactory() {} + /** + * Creates a new {@link ObjectMapper} configured for Apple Maps Server API models. + * + * @return an object mapper instance + */ public static ObjectMapper create() { return JsonMapper.builder() .addMixIn(Location.class, LocationMixin.class) diff --git a/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsApiException.java b/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsApiException.java index 78bbc7c..bba953c 100644 --- a/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsApiException.java +++ b/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsApiException.java @@ -6,19 +6,42 @@ * Indicates a non-successful response from the Apple Maps API. */ public final class AppleMapsApiException extends RuntimeException { + /** + * HTTP status code returned by the Apple Maps Server API. + */ private final int statusCode; + /** + * Response body returned by the Apple Maps Server API (may be empty). + */ private final String responseBody; + /** + * Creates an exception for a non-successful Apple Maps Server API response. + * + * @param operation a short operation name (for example, {@code "search"}) + * @param statusCode the HTTP status code + * @param responseBody the response body, if available + */ public AppleMapsApiException(String operation, int statusCode, String responseBody) { super("Apple Maps API request failed for " + operation + " (status " + statusCode + ")"); this.statusCode = statusCode; this.responseBody = Objects.requireNonNullElse(responseBody, ""); } + /** + * Returns the HTTP status code from the API response. + * + * @return the status code + */ public int statusCode() { return statusCode; } + /** + * Returns the API response body (may be empty). + * + * @return the response body + */ public String responseBody() { return responseBody; } diff --git a/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsAuthorizationService.java b/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsAuthorizationService.java index 8ae7578..e6fbe19 100644 --- a/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsAuthorizationService.java +++ b/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsAuthorizationService.java @@ -31,6 +31,12 @@ public final class AppleMapsAuthorizationService { private final ReentrantLock refreshLock = new ReentrantLock(); private final AtomicReference accessToken = new AtomicReference<>(); + /** + * Creates a service that exchanges an authorization token for access tokens. + * + * @param authToken the Apple Maps Server API authorization token + * @param timeout request timeout for token exchange + */ public AppleMapsAuthorizationService(String authToken, Duration timeout) { this(new Dependencies(authToken, timeout)); } @@ -44,6 +50,11 @@ public AppleMapsAuthorizationService(String authToken, Duration timeout) { this.clock = dependencies.clock(); } + /** + * Returns a cached access token, refreshing it when needed. + * + * @return the access token string + */ public String getAccessToken() { AccessToken cachedToken = accessToken.get(); if (cachedToken != null && !isExpiring(cachedToken)) { diff --git a/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsClientException.java b/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsClientException.java index e90f11d..a418188 100644 --- a/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsClientException.java +++ b/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsClientException.java @@ -6,6 +6,12 @@ * Indicates a client-side failure when calling the Apple Maps API. */ public final class AppleMapsClientException extends RuntimeException { + /** + * Creates an exception for a client-side failure. + * + * @param operation a short operation name (for example, {@code "search"}) + * @param cause the underlying exception + */ public AppleMapsClientException(String operation, Throwable cause) { super("Apple Maps request failed for " + Objects.requireNonNull(operation, "operation"), cause); } diff --git a/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/HttpAppleMapsGateway.java b/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/HttpAppleMapsGateway.java index 598e33b..95c2174 100644 --- a/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/HttpAppleMapsGateway.java +++ b/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/HttpAppleMapsGateway.java @@ -56,6 +56,12 @@ public final class HttpAppleMapsGateway implements AppleMapsGateway { private final Duration timeout; private final ExecutorService executorService; + /** + * Creates an HTTP gateway that calls the Apple Maps Server API. + * + * @param authToken the Apple Maps Server API authorization token + * @param timeout request timeout + */ public HttpAppleMapsGateway(String authToken, Duration timeout) { this(new Dependencies(authToken, timeout)); } diff --git a/src/main/java/com/williamcallahan/applemaps/cli/AppleMapsCli.java b/src/main/java/com/williamcallahan/applemaps/cli/AppleMapsCli.java index cd3adb9..4144710 100644 --- a/src/main/java/com/williamcallahan/applemaps/cli/AppleMapsCli.java +++ b/src/main/java/com/williamcallahan/applemaps/cli/AppleMapsCli.java @@ -19,6 +19,9 @@ import java.util.Locale; import java.util.Objects; +/** + * Command line interface for exercising Apple Maps Server API operations. + */ public final class AppleMapsCli { private static final int EXIT_USAGE = 2; @@ -55,6 +58,11 @@ public final class AppleMapsCli { private AppleMapsCli() {} + /** + * CLI entry point. + * + * @param args command line arguments + */ public static void main(String[] args) { try { run(args); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/AddressCategory.java b/src/main/java/com/williamcallahan/applemaps/domain/model/AddressCategory.java index e6fb3dd..6c7bcd3 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/AddressCategory.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/AddressCategory.java @@ -4,11 +4,29 @@ * Enumerated values that represent political geographical boundaries. */ public enum AddressCategory { + /** + * Country-level address component. + */ COUNTRY("Country"), + /** + * Top-level administrative area (for example, a state or province). + */ ADMINISTRATIVE_AREA("AdministrativeArea"), + /** + * Sub-level administrative area (for example, a county or district). + */ SUB_ADMINISTRATIVE_AREA("SubAdministrativeArea"), + /** + * Locality address component (for example, a city). + */ LOCALITY("Locality"), + /** + * Sub-locality address component (for example, a neighborhood). + */ SUB_LOCALITY("SubLocality"), + /** + * Postal code address component. + */ POSTAL_CODE("PostalCode"); private final String apiValue; @@ -17,6 +35,11 @@ public enum AddressCategory { this.apiValue = apiValue; } + /** + * Returns the Apple Maps Server API value for this category. + * + * @return the value used by the API + */ public String apiValue() { return apiValue; } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/AlternateIdsEntry.java b/src/main/java/com/williamcallahan/applemaps/domain/model/AlternateIdsEntry.java index 386d3a1..13332bf 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/AlternateIdsEntry.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/AlternateIdsEntry.java @@ -8,6 +8,12 @@ * Alternate Place identifiers associated with a primary Place ID. */ public record AlternateIdsEntry(Optional id, List alternateIds) { + /** + * Canonical constructor that normalizes potentially-null inputs to non-null values. + * + * @param id the primary place ID (optional) + * @param alternateIds alternate place IDs associated with {@code id} + */ public AlternateIdsEntry { id = normalizeOptional(id); alternateIds = normalizeList(alternateIds); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/AlternateIdsResponse.java b/src/main/java/com/williamcallahan/applemaps/domain/model/AlternateIdsResponse.java index fe6566a..00af8b4 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/AlternateIdsResponse.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/AlternateIdsResponse.java @@ -7,6 +7,12 @@ * A list of alternate Place ID results and lookup errors. */ public record AlternateIdsResponse(List results, List errors) { + /** + * Canonical constructor that normalizes potentially-null lists to immutable lists. + * + * @param results alternate ID results + * @param errors lookup errors + */ public AlternateIdsResponse { results = normalizeList(results); errors = normalizeList(errors); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/AutocompleteResult.java b/src/main/java/com/williamcallahan/applemaps/domain/model/AutocompleteResult.java index 810cf23..5286e44 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/AutocompleteResult.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/AutocompleteResult.java @@ -13,6 +13,14 @@ public record AutocompleteResult( Optional location, Optional structuredAddress ) { + /** + * Canonical constructor that normalizes potentially-null optionals and lists. + * + * @param completionUrl completion URL used to resolve this result + * @param displayLines display lines suitable for presentation + * @param location optional location associated with the completion + * @param structuredAddress optional structured address associated with the completion + */ public AutocompleteResult { completionUrl = Objects.requireNonNull(completionUrl, "completionUrl"); displayLines = normalizeList(displayLines); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsAvoid.java b/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsAvoid.java index c50f329..a2c892e 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsAvoid.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsAvoid.java @@ -4,6 +4,9 @@ * Route features that can be avoided during direction requests. */ public enum DirectionsAvoid { + /** + * Avoid toll roads where possible. + */ TOLLS("Tolls"); private final String apiValue; @@ -12,6 +15,11 @@ public enum DirectionsAvoid { this.apiValue = apiValue; } + /** + * Returns the Apple Maps Server API value for this avoid option. + * + * @return the value used by the API + */ public String apiValue() { return apiValue; } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsEndpoint.java b/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsEndpoint.java index 5c52a2b..e6fcac5 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsEndpoint.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsEndpoint.java @@ -8,18 +8,41 @@ public record DirectionsEndpoint(String formattedLocation) { private static final String COORDINATE_SEPARATOR = ","; + /** + * Canonical constructor that validates the formatted location is non-null. + * + * @param formattedLocation formatted endpoint string (address or coordinate pair) + */ public DirectionsEndpoint { formattedLocation = Objects.requireNonNull(formattedLocation, "formattedLocation"); } + /** + * Creates an endpoint from a free-form address string. + * + * @param address the address text + * @return a directions endpoint + */ public static DirectionsEndpoint fromAddress(String address) { return new DirectionsEndpoint(address); } + /** + * Creates an endpoint from latitude/longitude coordinates. + * + * @param latitude latitude in decimal degrees + * @param longitude longitude in decimal degrees + * @return a directions endpoint + */ public static DirectionsEndpoint fromLatitudeLongitude(double latitude, double longitude) { return new DirectionsEndpoint(formatCoordinatePair(latitude, longitude)); } + /** + * Converts this endpoint into the format used by query parameters. + * + * @return the query string value + */ public String toQueryString() { return formattedLocation; } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsResponse.java b/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsResponse.java index 7cb7485..936dcf4 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsResponse.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsResponse.java @@ -14,6 +14,15 @@ public record DirectionsResponse( List steps, List> stepPaths ) { + /** + * Canonical constructor that normalizes potentially-null optionals and lists. + * + * @param origin origin place, if available + * @param destination destination place, if available + * @param routes routes returned by the API + * @param steps steps returned by the API + * @param stepPaths step path coordinate arrays returned by the API + */ public DirectionsResponse { origin = normalizeOptional(origin); destination = normalizeOptional(destination); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsRoute.java b/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsRoute.java index 2eee6e2..a47ae5b 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsRoute.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsRoute.java @@ -15,6 +15,16 @@ public record DirectionsRoute( List stepIndexes, Optional transportType ) { + /** + * Canonical constructor that normalizes potentially-null optionals and lists. + * + * @param name route name, if available + * @param distanceMeters route distance in meters, if available + * @param durationSeconds route duration in seconds, if available + * @param hasTolls whether the route has tolls, if available + * @param stepIndexes indexes into the directions steps array + * @param transportType transport type, if available + */ public DirectionsRoute { name = normalizeOptional(name); distanceMeters = normalizeOptional(distanceMeters); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsStep.java b/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsStep.java index 038da06..3f0525c 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsStep.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/DirectionsStep.java @@ -13,6 +13,15 @@ public record DirectionsStep( Optional instructions, Optional transportType ) { + /** + * Canonical constructor that normalizes potentially-null optionals. + * + * @param stepPathIndex index into the step paths array, if available + * @param distanceMeters step distance in meters, if available + * @param durationSeconds step duration in seconds, if available + * @param instructions instructions text, if available + * @param transportType transport type, if available + */ public DirectionsStep { stepPathIndex = normalizeOptional(stepPathIndex); distanceMeters = normalizeOptional(distanceMeters); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/ErrorResponse.java b/src/main/java/com/williamcallahan/applemaps/domain/model/ErrorResponse.java index 9edd90f..88a2cd9 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/ErrorResponse.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/ErrorResponse.java @@ -7,6 +7,12 @@ * Information about an error that occurs while processing a request. */ public record ErrorResponse(String message, List details) { + /** + * Canonical constructor that normalizes potentially-null lists to immutable lists. + * + * @param message error message + * @param details error details + */ public ErrorResponse { message = Objects.requireNonNull(message, "message"); details = List.copyOf(Objects.requireNonNullElse(details, List.of())); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/EtaEstimate.java b/src/main/java/com/williamcallahan/applemaps/domain/model/EtaEstimate.java index cf20993..b88f78c 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/EtaEstimate.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/EtaEstimate.java @@ -13,6 +13,15 @@ public record EtaEstimate( Optional staticTravelTimeSeconds, Optional transportType ) { + /** + * Canonical constructor that normalizes potentially-null optionals. + * + * @param destination destination location, if available + * @param distanceMeters travel distance in meters, if available + * @param expectedTravelTimeSeconds expected travel time in seconds, if available + * @param staticTravelTimeSeconds static travel time in seconds, if available + * @param transportType transport type, if available + */ public EtaEstimate { destination = normalizeOptional(destination); distanceMeters = normalizeOptional(distanceMeters); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/EtaResponse.java b/src/main/java/com/williamcallahan/applemaps/domain/model/EtaResponse.java index 9357ad5..7312fd8 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/EtaResponse.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/EtaResponse.java @@ -7,6 +7,11 @@ * Estimated time of arrival results for destinations. */ public record EtaResponse(List etas) { + /** + * Canonical constructor that normalizes potentially-null lists to immutable lists. + * + * @param etas ETA estimates returned by the API + */ public EtaResponse { etas = normalizeList(etas); } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/PaginationInfo.java b/src/main/java/com/williamcallahan/applemaps/domain/model/PaginationInfo.java index 907ea87..7f9fb3c 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/PaginationInfo.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/PaginationInfo.java @@ -12,6 +12,14 @@ public record PaginationInfo( long totalPageCount, long totalResults ) { + /** + * Canonical constructor that normalizes potentially-null page tokens. + * + * @param nextPageToken next page token, if available + * @param prevPageToken previous page token, if available + * @param totalPageCount total number of pages + * @param totalResults total number of results + */ public PaginationInfo { nextPageToken = normalizeOptional(nextPageToken); prevPageToken = normalizeOptional(prevPageToken); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/Place.java b/src/main/java/com/williamcallahan/applemaps/domain/model/Place.java index 839a333..7402ef6 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/Place.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/Place.java @@ -18,6 +18,19 @@ public record Place( String country, String countryCode ) { + /** + * Canonical constructor that normalizes potentially-null optionals and lists. + * + * @param id place identifier, if available + * @param alternateIds alternate place identifiers + * @param name place name + * @param coordinate place coordinate + * @param displayMapRegion map region to display, if available + * @param formattedAddressLines formatted address lines + * @param structuredAddress structured address, if available + * @param country country name + * @param countryCode country code + */ public Place { id = normalizeOptional(id); alternateIds = normalizeList(alternateIds); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/PlaceLookupError.java b/src/main/java/com/williamcallahan/applemaps/domain/model/PlaceLookupError.java index 8c2f5b4..ad3ae28 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/PlaceLookupError.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/PlaceLookupError.java @@ -6,6 +6,12 @@ * An error associated with a place lookup request. */ public record PlaceLookupError(PlaceLookupErrorCode errorCode, String id) { + /** + * Canonical constructor that validates required fields. + * + * @param errorCode error code returned by the API + * @param id place identifier associated with the error + */ public PlaceLookupError { Objects.requireNonNull(errorCode, "errorCode"); Objects.requireNonNull(id, "id"); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/PlaceLookupErrorCode.java b/src/main/java/com/williamcallahan/applemaps/domain/model/PlaceLookupErrorCode.java index 6a66e1a..b33425d 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/PlaceLookupErrorCode.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/PlaceLookupErrorCode.java @@ -4,7 +4,16 @@ * Error codes returned when looking up places by identifier. */ public enum PlaceLookupErrorCode { + /** + * The request provided an invalid place identifier. + */ FAILED_INVALID_ID, + /** + * The requested place could not be found. + */ FAILED_NOT_FOUND, + /** + * The lookup failed due to an internal error. + */ FAILED_INTERNAL_ERROR } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/PlaceResults.java b/src/main/java/com/williamcallahan/applemaps/domain/model/PlaceResults.java index dac6a60..8a53dc6 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/PlaceResults.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/PlaceResults.java @@ -7,6 +7,11 @@ * An object that contains an array of places. */ public record PlaceResults(List results) { + /** + * Canonical constructor that normalizes potentially-null lists to immutable lists. + * + * @param results places returned by the API + */ public PlaceResults { results = normalizeList(results); } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/PlacesResponse.java b/src/main/java/com/williamcallahan/applemaps/domain/model/PlacesResponse.java index 9ce1d2a..d8ed36d 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/PlacesResponse.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/PlacesResponse.java @@ -7,6 +7,12 @@ * A list of place results and lookup errors. */ public record PlacesResponse(List results, List errors) { + /** + * Canonical constructor that normalizes potentially-null lists to immutable lists. + * + * @param results places returned by the API + * @param errors lookup errors + */ public PlacesResponse { results = normalizeList(results); errors = normalizeList(errors); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/PoiCategory.java b/src/main/java/com/williamcallahan/applemaps/domain/model/PoiCategory.java index 69102ca..59949fd 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/PoiCategory.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/PoiCategory.java @@ -4,51 +4,97 @@ * Defines Apple Maps point-of-interest categories. */ public enum PoiCategory { + /** Airport. */ AIRPORT("Airport"), + /** Airport gate. */ AIRPORT_GATE("AirportGate"), + /** Airport terminal. */ AIRPORT_TERMINAL("AirportTerminal"), + /** Amusement park. */ AMUSEMENT_PARK("AmusementPark"), + /** ATM. */ ATM("ATM"), + /** Aquarium. */ AQUARIUM("Aquarium"), + /** Bakery. */ BAKERY("Bakery"), + /** Bank. */ BANK("Bank"), + /** Beach. */ BEACH("Beach"), + /** Brewery. */ BREWERY("Brewery"), + /** Bowling alley. */ BOWLING("Bowling"), + /** Cafe. */ CAFE("Cafe"), + /** Campground. */ CAMPGROUND("Campground"), + /** Car rental. */ CAR_RENTAL("CarRental"), + /** EV charger. */ EV_CHARGER("EVCharger"), + /** Fire station. */ FIRE_STATION("FireStation"), + /** Fitness center. */ FITNESS_CENTER("FitnessCenter"), + /** Food market. */ FOOD_MARKET("FoodMarket"), + /** Gas station. */ GAS_STATION("GasStation"), + /** Hospital. */ HOSPITAL("Hospital"), + /** Hotel. */ HOTEL("Hotel"), + /** Laundry. */ LAUNDRY("Laundry"), + /** Library. */ LIBRARY("Library"), + /** Marina. */ MARINA("Marina"), + /** Movie theater. */ MOVIE_THEATER("MovieTheater"), + /** Museum. */ MUSEUM("Museum"), + /** National park. */ NATIONAL_PARK("NationalPark"), + /** Nightlife. */ NIGHTLIFE("Nightlife"), + /** Park. */ PARK("Park"), + /** Parking. */ PARKING("Parking"), + /** Pharmacy. */ PHARMACY("Pharmacy"), + /** Playground. */ PLAYGROUND("Playground"), + /** Police. */ POLICE("Police"), + /** Post office. */ POST_OFFICE("PostOffice"), + /** Public transport. */ PUBLIC_TRANSPORT("PublicTransport"), + /** Religious site. */ RELIGIOUS_SITE("ReligiousSite"), + /** Restaurant. */ RESTAURANT("Restaurant"), + /** Restroom. */ RESTROOM("Restroom"), + /** School. */ SCHOOL("School"), + /** Stadium. */ STADIUM("Stadium"), + /** Store. */ STORE("Store"), + /** Theater. */ THEATER("Theater"), + /** University. */ UNIVERSITY("University"), + /** Winery. */ WINERY("Winery"), + /** Zoo. */ ZOO("Zoo"), + /** Landmark. */ LANDMARK("Landmark"); private final String apiValue; @@ -57,6 +103,11 @@ public enum PoiCategory { this.apiValue = apiValue; } + /** + * Returns the Apple Maps Server API value for this category. + * + * @return the value used by the API + */ public String apiValue() { return apiValue; } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/RouteLocation.java b/src/main/java/com/williamcallahan/applemaps/domain/model/RouteLocation.java index 872c02a..04e6a80 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/RouteLocation.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/RouteLocation.java @@ -8,14 +8,31 @@ public record RouteLocation(String coordinatePair) { private static final String COORDINATE_SEPARATOR = ","; + /** + * Canonical constructor that validates the coordinate pair is non-null. + * + * @param coordinatePair formatted coordinate pair + */ public RouteLocation { coordinatePair = Objects.requireNonNull(coordinatePair, "coordinatePair"); } + /** + * Creates a route location from latitude/longitude coordinates. + * + * @param latitude latitude in decimal degrees + * @param longitude longitude in decimal degrees + * @return a route location + */ public static RouteLocation fromLatitudeLongitude(double latitude, double longitude) { return new RouteLocation(formatCoordinatePair(latitude, longitude)); } + /** + * Converts this location into the format used by query parameters. + * + * @return the query string value + */ public String toQueryString() { return coordinatePair; } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchACResultType.java b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchACResultType.java index 302939d..6f6f474 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchACResultType.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchACResultType.java @@ -4,10 +4,15 @@ * Enumerated values that indicate the result type for autocomplete requests. */ public enum SearchACResultType { + /** Point of interest result. */ POI("poi"), + /** Address result. */ ADDRESS("address"), + /** Physical feature result. */ PHYSICAL_FEATURE("physicalFeature"), + /** Point of interest result. */ POINT_OF_INTEREST("pointOfInterest"), + /** Query result. */ QUERY("query"); private final String apiValue; @@ -16,6 +21,11 @@ public enum SearchACResultType { this.apiValue = apiValue; } + /** + * Returns the Apple Maps Server API value for this result type. + * + * @return the value used by the API + */ public String apiValue() { return apiValue; } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchAutocompleteResponse.java b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchAutocompleteResponse.java index 61bb72d..43ec162 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchAutocompleteResponse.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchAutocompleteResponse.java @@ -7,6 +7,11 @@ * An object that contains autocomplete results. */ public record SearchAutocompleteResponse(List results) { + /** + * Canonical constructor that normalizes potentially-null lists to immutable lists. + * + * @param results autocomplete results returned by the API + */ public SearchAutocompleteResponse { results = normalizeList(results); } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchLocation.java b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchLocation.java index 600d2a5..8950caa 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchLocation.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchLocation.java @@ -8,10 +8,22 @@ public record SearchLocation(String coordinatePair) { private static final String COORDINATE_SEPARATOR = ","; + /** + * Canonical constructor that validates the coordinate pair is non-null. + * + * @param coordinatePair formatted coordinate pair + */ public SearchLocation { Objects.requireNonNull(coordinatePair, "coordinatePair"); } + /** + * Creates a search location from latitude/longitude coordinates. + * + * @param latitude latitude in decimal degrees + * @param longitude longitude in decimal degrees + * @return a search location + */ public static SearchLocation fromLatitudeLongitude( double latitude, double longitude @@ -19,6 +31,11 @@ public static SearchLocation fromLatitudeLongitude( return new SearchLocation(formatCoordinatePair(latitude, longitude)); } + /** + * Converts this location into the format used by query parameters. + * + * @return the query string value + */ public String toQueryString() { return coordinatePair; } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchRegion.java b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchRegion.java index d0cfb97..37ab954 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchRegion.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchRegion.java @@ -9,10 +9,24 @@ public record SearchRegion(String coordinateBounds) { private static final String COORDINATE_SEPARATOR = ","; + /** + * Canonical constructor that validates the coordinate bounds are non-null. + * + * @param coordinateBounds formatted coordinate bounds string + */ public SearchRegion { Objects.requireNonNull(coordinateBounds, "coordinateBounds"); } + /** + * Creates a search region from a bounding box. + * + * @param northLatitude northern latitude in decimal degrees + * @param eastLongitude eastern longitude in decimal degrees + * @param southLatitude southern latitude in decimal degrees + * @param westLongitude western longitude in decimal degrees + * @return a search region + */ public static SearchRegion fromBounds( double northLatitude, double eastLongitude, @@ -22,6 +36,12 @@ public static SearchRegion fromBounds( return new SearchRegion(formatBounds(northLatitude, eastLongitude, southLatitude, westLongitude)); } + /** + * Creates a search region from a {@link SearchMapRegion}. + * + * @param searchMapRegion the map region to convert + * @return a search region + */ public static SearchRegion fromSearchMapRegion(SearchMapRegion searchMapRegion) { Objects.requireNonNull(searchMapRegion, "searchMapRegion"); return fromBounds( @@ -32,6 +52,11 @@ public static SearchRegion fromSearchMapRegion(SearchMapRegion searchMapRegion) ); } + /** + * Converts this region into the format used by query parameters. + * + * @return the query string value + */ public String toQueryString() { return coordinateBounds; } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchRegionPriority.java b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchRegionPriority.java index 76462f7..6b05cd9 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchRegionPriority.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchRegionPriority.java @@ -4,7 +4,9 @@ * Indicates the importance of the configured search region. */ public enum SearchRegionPriority { + /** Default priority. */ DEFAULT("default"), + /** Required priority. */ REQUIRED("required"); private final String apiValue; @@ -13,6 +15,11 @@ public enum SearchRegionPriority { this.apiValue = apiValue; } + /** + * Returns the Apple Maps Server API value for this priority. + * + * @return the value used by the API + */ public String apiValue() { return apiValue; } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchResponse.java b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchResponse.java index 2a7b26d..0643ab8 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchResponse.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchResponse.java @@ -12,6 +12,13 @@ public record SearchResponse( Optional paginationInfo, List results ) { + /** + * Canonical constructor that normalizes potentially-null optionals and lists. + * + * @param displayMapRegion display map region returned by the API + * @param paginationInfo pagination information, if available + * @param results search results returned by the API + */ public SearchResponse { displayMapRegion = Objects.requireNonNull(displayMapRegion, "displayMapRegion"); paginationInfo = normalizeOptional(paginationInfo); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchResponsePlace.java b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchResponsePlace.java index f569da3..a8faed0 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchResponsePlace.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchResponsePlace.java @@ -19,6 +19,20 @@ public record SearchResponsePlace( String countryCode, Optional poiCategory ) { + /** + * Canonical constructor that normalizes potentially-null optionals and lists. + * + * @param id place identifier, if available + * @param alternateIds alternate place identifiers + * @param name place name + * @param coordinate place coordinate + * @param displayMapRegion map region to display, if available + * @param formattedAddressLines formatted address lines + * @param structuredAddress structured address, if available + * @param country country name + * @param countryCode country code + * @param poiCategory point-of-interest category, if available + */ public SearchResponsePlace { id = normalizeOptional(id); alternateIds = normalizeList(alternateIds); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchResultType.java b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchResultType.java index 4cf4b34..9ec370e 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/SearchResultType.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/SearchResultType.java @@ -4,9 +4,13 @@ * Enumerated values that indicate the result type for search requests. */ public enum SearchResultType { + /** Point of interest result. */ POI("poi"), + /** Address result. */ ADDRESS("address"), + /** Physical feature result. */ PHYSICAL_FEATURE("physicalFeature"), + /** Point of interest result. */ POINT_OF_INTEREST("pointOfInterest"); private final String apiValue; @@ -15,6 +19,11 @@ public enum SearchResultType { this.apiValue = apiValue; } + /** + * Returns the Apple Maps Server API value for this result type. + * + * @return the value used by the API + */ public String apiValue() { return apiValue; } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/StructuredAddress.java b/src/main/java/com/williamcallahan/applemaps/domain/model/StructuredAddress.java index c7e6450..3cda3c6 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/StructuredAddress.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/StructuredAddress.java @@ -20,6 +20,21 @@ public record StructuredAddress( Optional subThoroughfare, Optional thoroughfare ) { + /** + * Canonical constructor that normalizes potentially-null optionals and lists. + * + * @param administrativeArea administrative area, if available + * @param administrativeAreaCode administrative area code, if available + * @param subAdministrativeArea sub-administrative area, if available + * @param areasOfInterest areas of interest + * @param dependentLocalities dependent localities + * @param fullThoroughfare full thoroughfare, if available + * @param locality locality, if available + * @param postCode postal code, if available + * @param subLocality sub-locality, if available + * @param subThoroughfare sub-thoroughfare, if available + * @param thoroughfare thoroughfare, if available + */ public StructuredAddress { administrativeArea = normalizeOptional(administrativeArea); administrativeAreaCode = normalizeOptional(administrativeAreaCode); diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/TokenResponse.java b/src/main/java/com/williamcallahan/applemaps/domain/model/TokenResponse.java index 44b9fee..8f1b7d1 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/TokenResponse.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/TokenResponse.java @@ -6,6 +6,12 @@ * An object that contains an access token and its expiration time in seconds. */ public record TokenResponse(String accessToken, long expiresInSeconds) { + /** + * Canonical constructor that validates required fields. + * + * @param accessToken access token string + * @param expiresInSeconds expiration time in seconds + */ public TokenResponse { Objects.requireNonNull(accessToken, "accessToken"); } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/TransportType.java b/src/main/java/com/williamcallahan/applemaps/domain/model/TransportType.java index e856617..8ea2e40 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/TransportType.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/TransportType.java @@ -4,9 +4,13 @@ * Supported transportation modes for routing and ETA requests. */ public enum TransportType { + /** Automobile transport mode. */ AUTOMOBILE("Automobile"), + /** Transit transport mode. */ TRANSIT("Transit"), + /** Walking transport mode. */ WALKING("Walking"), + /** Cycling transport mode. */ CYCLING("Cycling"); private final String apiValue; @@ -15,6 +19,11 @@ public enum TransportType { this.apiValue = apiValue; } + /** + * Returns the Apple Maps Server API value for this transport type. + * + * @return the value used by the API + */ public String apiValue() { return apiValue; } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/model/UserLocation.java b/src/main/java/com/williamcallahan/applemaps/domain/model/UserLocation.java index 01d5fce..fa5acd3 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/model/UserLocation.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/model/UserLocation.java @@ -8,10 +8,22 @@ public record UserLocation(String coordinatePair) { private static final String COORDINATE_SEPARATOR = ","; + /** + * Canonical constructor that validates the coordinate pair is non-null. + * + * @param coordinatePair formatted coordinate pair + */ public UserLocation { Objects.requireNonNull(coordinatePair, "coordinatePair"); } + /** + * Creates a user location from latitude/longitude coordinates. + * + * @param latitude latitude in decimal degrees + * @param longitude longitude in decimal degrees + * @return a user location + */ public static UserLocation fromLatitudeLongitude( double latitude, double longitude @@ -19,6 +31,11 @@ public static UserLocation fromLatitudeLongitude( return new UserLocation(formatCoordinatePair(latitude, longitude)); } + /** + * Converts this location into the format used by query parameters. + * + * @return the query string value + */ public String toQueryString() { return coordinatePair; } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/port/AppleMapsGateway.java b/src/main/java/com/williamcallahan/applemaps/domain/port/AppleMapsGateway.java index 8a4be74..17985c1 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/port/AppleMapsGateway.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/port/AppleMapsGateway.java @@ -20,26 +20,92 @@ * Port for invoking Apple Maps Server API operations. */ public interface AppleMapsGateway { + /** + * Performs a geocode request. + * + * @param input request parameters + * @return geocode results + */ PlaceResults geocode(GeocodeInput input); + /** + * Performs a text search request. + * + * @param input request parameters + * @return search results + */ SearchResponse search(SearchInput input); + /** + * Performs an autocomplete request. + * + * @param input request parameters + * @return autocomplete results + */ SearchAutocompleteResponse autocomplete(SearchAutocompleteInput input); + /** + * Resolves a completion URL returned from an autocomplete response. + * + * @param completionUrl the URL to resolve + * @return search results for the completion URL + */ SearchResponse resolveCompletionUrl(String completionUrl); + /** + * Performs a reverse geocode request. + * + * @param latitude latitude in decimal degrees + * @param longitude longitude in decimal degrees + * @param language response language (BCP 47) + * @return reverse geocode results + */ PlaceResults reverseGeocode(double latitude, double longitude, String language); + /** + * Performs a directions request. + * + * @param input request parameters + * @return directions results + */ DirectionsResponse directions(DirectionsInput input); + /** + * Performs an ETA request. + * + * @param input request parameters + * @return ETA results + */ EtaResponse etas(EtaInput input); + /** + * Looks up a place by ID. + * + * @param placeId place identifier + * @param language response language (BCP 47) + * @return a place + */ Place lookupPlace(String placeId, String language); + /** + * Performs a place lookup request. + * + * @param input request parameters + * @return places results + */ PlacesResponse lookupPlaces(PlaceLookupInput input); + /** + * Performs an alternate IDs lookup request. + * + * @param input request parameters + * @return alternate IDs results + */ AlternateIdsResponse lookupAlternateIds(AlternateIdsInput input); + /** + * Releases any resources owned by the gateway. + */ default void close() { } } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/request/AlternateIdsInput.java b/src/main/java/com/williamcallahan/applemaps/domain/request/AlternateIdsInput.java index 6b812d2..4de1ce9 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/request/AlternateIdsInput.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/request/AlternateIdsInput.java @@ -15,17 +15,33 @@ public record AlternateIdsInput(List ids) { private static final String PARAMETER_IDS = "ids"; private static final String LIST_SEPARATOR = ","; + /** + * Canonical constructor that validates required fields and normalizes collections. + * + * @param ids place identifiers to resolve alternate IDs for + */ public AlternateIdsInput { ids = normalizeList(ids); validateIds(ids); } + /** + * Converts this input to a query string suitable for the Apple Maps Server API. + * + * @return a query string beginning with {@code ?} + */ public String toQueryString() { List parameters = new ArrayList<>(); parameters.add(formatParameter(PARAMETER_IDS, joinEncoded(ids))); return QUERY_PREFIX + String.join(PARAMETER_SEPARATOR, parameters); } + /** + * Creates a builder initialized with the required place IDs. + * + * @param ids the place IDs to resolve alternate IDs for + * @return a builder + */ public static Builder builder(List ids) { return new Builder(ids); } @@ -55,6 +71,9 @@ private static String encode(String rawText) { return URLEncoder.encode(rawText, StandardCharsets.UTF_8); } + /** + * Builder for {@link AlternateIdsInput}. + */ public static final class Builder { private final List ids; @@ -62,6 +81,11 @@ private Builder(List ids) { this.ids = normalizeList(ids); } + /** + * Builds a validated {@link AlternateIdsInput}. + * + * @return an input instance + */ public AlternateIdsInput build() { return new AlternateIdsInput(ids); } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/request/DirectionsInput.java b/src/main/java/com/williamcallahan/applemaps/domain/request/DirectionsInput.java index a442177..0e0ef0e 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/request/DirectionsInput.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/request/DirectionsInput.java @@ -44,6 +44,21 @@ public record DirectionsInput( private static final String PARAMETER_TRANSPORT_TYPE = "transportType"; private static final String PARAMETER_USER_LOCATION = "userLocation"; + /** + * Canonical constructor that validates required fields and normalizes optional values. + * + * @param origin the origin endpoint + * @param destination the destination endpoint + * @param arrivalDate optional arrival date/time (format as expected by the API) + * @param avoid route features to avoid + * @param departureDate optional departure date/time (format as expected by the API) + * @param language optional response language (BCP 47) + * @param requestsAlternateRoutes optional flag to request alternate routes + * @param searchLocation optional search location hint + * @param searchRegion optional search region hint + * @param transportType optional transport type + * @param userLocation optional user location hint + */ public DirectionsInput { origin = Objects.requireNonNull(origin, "origin"); destination = Objects.requireNonNull(destination, "destination"); @@ -59,6 +74,11 @@ public record DirectionsInput( validateDates(arrivalDate, departureDate); } + /** + * Converts this input to a query string suitable for the Apple Maps Server API. + * + * @return a query string beginning with {@code ?} + */ public String toQueryString() { List parameters = new ArrayList<>(); parameters.add(formatParameter(PARAMETER_ORIGIN, encode(origin.toQueryString()))); @@ -82,6 +102,13 @@ public String toQueryString() { return QUERY_PREFIX + String.join(PARAMETER_SEPARATOR, parameters); } + /** + * Creates a builder initialized with the required origin and destination. + * + * @param origin the origin endpoint + * @param destination the destination endpoint + * @return a builder + */ public static Builder builder(DirectionsEndpoint origin, DirectionsEndpoint destination) { return new Builder(origin, destination); } @@ -115,6 +142,9 @@ private static String encode(String rawText) { return URLEncoder.encode(rawText, StandardCharsets.UTF_8); } + /** + * Builder for {@link DirectionsInput}. + */ public static final class Builder { private final DirectionsEndpoint origin; private final DirectionsEndpoint destination; @@ -133,51 +163,110 @@ private Builder(DirectionsEndpoint origin, DirectionsEndpoint destination) { this.destination = Objects.requireNonNull(destination, "destination"); } + /** + * Sets the arrival date/time parameter (format as expected by the API). + * + * @param arrivalDate the arrival date/time, or {@code null} to clear + * @return this builder + */ public Builder arrivalDate(String arrivalDate) { this.arrivalDate = Optional.ofNullable(arrivalDate); return this; } + /** + * Sets route features to avoid. + * + * @param avoid route features to avoid (empty means no avoid filters) + * @return this builder + */ public Builder avoid(List avoid) { this.avoid = normalizeList(avoid); return this; } + /** + * Sets the departure date/time parameter (format as expected by the API). + * + * @param departureDate the departure date/time, or {@code null} to clear + * @return this builder + */ public Builder departureDate(String departureDate) { this.departureDate = Optional.ofNullable(departureDate); return this; } + /** + * Sets the response language (BCP 47). + * + * @param language the language tag, or {@code null} to clear + * @return this builder + */ public Builder language(String language) { this.language = Optional.ofNullable(language); return this; } + /** + * Sets whether alternate routes should be requested. + * + * @param requestsAlternateRoutes {@code true} to request alternate routes, or {@code null} to clear + * @return this builder + */ public Builder requestsAlternateRoutes(Boolean requestsAlternateRoutes) { this.requestsAlternateRoutes = Optional.ofNullable(requestsAlternateRoutes); return this; } + /** + * Sets the search location hint used by the API. + * + * @param searchLocation the search location, or {@code null} to clear + * @return this builder + */ public Builder searchLocation(RouteLocation searchLocation) { this.searchLocation = Optional.ofNullable(searchLocation); return this; } + /** + * Sets the search region hint used by the API. + * + * @param searchRegion the search region, or {@code null} to clear + * @return this builder + */ public Builder searchRegion(SearchRegion searchRegion) { this.searchRegion = Optional.ofNullable(searchRegion); return this; } + /** + * Sets the requested transport type. + * + * @param transportType the transport type, or {@code null} to clear + * @return this builder + */ public Builder transportType(TransportType transportType) { this.transportType = Optional.ofNullable(transportType); return this; } + /** + * Sets the user location hint used by the API. + * + * @param userLocation the user location, or {@code null} to clear + * @return this builder + */ public Builder userLocation(RouteLocation userLocation) { this.userLocation = Optional.ofNullable(userLocation); return this; } + /** + * Builds a validated {@link DirectionsInput}. + * + * @return an input instance + */ public DirectionsInput build() { return new DirectionsInput( origin, diff --git a/src/main/java/com/williamcallahan/applemaps/domain/request/EtaInput.java b/src/main/java/com/williamcallahan/applemaps/domain/request/EtaInput.java index 4738dd8..f941235 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/request/EtaInput.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/request/EtaInput.java @@ -29,6 +29,15 @@ public record EtaInput( private static final String PARAMETER_DEPARTURE_DATE = "departureDate"; private static final String PARAMETER_ARRIVAL_DATE = "arrivalDate"; + /** + * Canonical constructor that validates required fields and normalizes optional values. + * + * @param origin origin location + * @param destinations destination locations (maximum 10) + * @param transportType optional transport type + * @param departureDate optional departure date/time (format as expected by the API) + * @param arrivalDate optional arrival date/time (format as expected by the API) + */ public EtaInput { origin = Objects.requireNonNull(origin, "origin"); destinations = normalizeList(destinations); @@ -38,6 +47,11 @@ public record EtaInput( validateDestinations(destinations); } + /** + * Converts this input to a query string suitable for the Apple Maps Server API. + * + * @return a query string beginning with {@code ?} + */ public String toQueryString() { List parameters = new ArrayList<>(); parameters.add(formatParameter(PARAMETER_ORIGIN, origin.toQueryString())); @@ -48,6 +62,13 @@ public String toQueryString() { return QUERY_PREFIX + String.join(PARAMETER_SEPARATOR, parameters); } + /** + * Creates a builder initialized with the required origin and destinations. + * + * @param origin the route origin + * @param destinations the destinations (maximum 10) + * @return a builder + */ public static Builder builder(RouteLocation origin, List destinations) { return new Builder(origin, destinations); } @@ -84,6 +105,9 @@ private static String encode(String rawText) { return URLEncoder.encode(rawText, StandardCharsets.UTF_8); } + /** + * Builder for {@link EtaInput}. + */ public static final class Builder { private final RouteLocation origin; private final List destinations; @@ -96,21 +120,44 @@ private Builder(RouteLocation origin, List destinations) { this.destinations = normalizeList(destinations); } + /** + * Sets the requested transport type. + * + * @param transportType the transport type, or {@code null} to clear + * @return this builder + */ public Builder transportType(TransportType transportType) { this.transportType = Optional.ofNullable(transportType); return this; } + /** + * Sets the departure date/time parameter (format as expected by the API). + * + * @param departureDate the departure date/time, or {@code null} to clear + * @return this builder + */ public Builder departureDate(String departureDate) { this.departureDate = Optional.ofNullable(departureDate); return this; } + /** + * Sets the arrival date/time parameter (format as expected by the API). + * + * @param arrivalDate the arrival date/time, or {@code null} to clear + * @return this builder + */ public Builder arrivalDate(String arrivalDate) { this.arrivalDate = Optional.ofNullable(arrivalDate); return this; } + /** + * Builds a validated {@link EtaInput}. + * + * @return an input instance + */ public EtaInput build() { return new EtaInput(origin, destinations, transportType, departureDate, arrivalDate); } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/request/GeocodeInput.java b/src/main/java/com/williamcallahan/applemaps/domain/request/GeocodeInput.java index 9b92bed..d6da610 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/request/GeocodeInput.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/request/GeocodeInput.java @@ -31,6 +31,16 @@ public record GeocodeInput( private static final String PARAMETER_SEARCH_REGION = "searchRegion"; private static final String PARAMETER_USER_LOCATION = "userLocation"; + /** + * Canonical constructor that validates required fields and normalizes optional values. + * + * @param address address text to geocode + * @param limitToCountries country codes to restrict results to + * @param language optional response language (BCP 47) + * @param searchLocation optional search location hint + * @param searchRegion optional search region hint + * @param userLocation optional user location hint + */ public GeocodeInput { address = Objects.requireNonNull(address, "address"); limitToCountries = normalizeList(limitToCountries); @@ -40,6 +50,11 @@ public record GeocodeInput( userLocation = normalizeOptional(userLocation); } + /** + * Converts this input to a query string suitable for the Apple Maps Server API. + * + * @return a query string beginning with {@code ?} + */ public String toQueryString() { List parameters = new ArrayList<>(); parameters.add(formatParameter(PARAMETER_QUERY, encode(address))); @@ -53,6 +68,12 @@ public String toQueryString() { return QUERY_PREFIX + String.join(PARAMETER_SEPARATOR, parameters); } + /** + * Creates a builder initialized with the required address string. + * + * @param address the address text to geocode + * @return a builder + */ public static Builder builder(String address) { return new Builder(address); } @@ -80,6 +101,9 @@ private static String encode(String value) { return URLEncoder.encode(value, StandardCharsets.UTF_8); } + /** + * Builder for {@link GeocodeInput}. + */ public static final class Builder { private final String address; private List limitToCountries = List.of(); @@ -92,31 +116,66 @@ private Builder(String address) { this.address = Objects.requireNonNull(address, "address"); } + /** + * Limits results to specific country codes. + * + * @param countries country codes (for example, ISO 3166-1 alpha-2) + * @return this builder + */ public Builder limitToCountries(List countries) { this.limitToCountries = normalizeList(countries); return this; } + /** + * Sets the response language (BCP 47). + * + * @param language the language tag, or {@code null} to clear + * @return this builder + */ public Builder language(String language) { this.language = Optional.ofNullable(language); return this; } + /** + * Sets the search location hint used by the API. + * + * @param searchLocation the search location, or {@code null} to clear + * @return this builder + */ public Builder searchLocation(SearchLocation searchLocation) { this.searchLocation = Optional.ofNullable(searchLocation); return this; } + /** + * Sets the search region hint used by the API. + * + * @param searchRegion the search region, or {@code null} to clear + * @return this builder + */ public Builder searchRegion(SearchRegion searchRegion) { this.searchRegion = Optional.ofNullable(searchRegion); return this; } + /** + * Sets the user location hint used by the API. + * + * @param userLocation the user location, or {@code null} to clear + * @return this builder + */ public Builder userLocation(UserLocation userLocation) { this.userLocation = Optional.ofNullable(userLocation); return this; } + /** + * Builds a {@link GeocodeInput}. + * + * @return an input instance + */ public GeocodeInput build() { return new GeocodeInput(address, limitToCountries, language, searchLocation, searchRegion, userLocation); } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/request/PlaceLookupInput.java b/src/main/java/com/williamcallahan/applemaps/domain/request/PlaceLookupInput.java index d7b4e3d..0aa9fa7 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/request/PlaceLookupInput.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/request/PlaceLookupInput.java @@ -17,12 +17,23 @@ public record PlaceLookupInput(List ids, Optional language) { private static final String PARAMETER_IDS = "ids"; private static final String PARAMETER_LANGUAGE = "lang"; + /** + * Canonical constructor that validates required fields and normalizes optional values. + * + * @param ids place identifiers to look up + * @param language optional response language (BCP 47) + */ public PlaceLookupInput { ids = normalizeList(ids); language = normalizeOptional(language); validateIds(ids); } + /** + * Converts this input to a query string suitable for the Apple Maps Server API. + * + * @return a query string beginning with {@code ?} + */ public String toQueryString() { List parameters = new ArrayList<>(); parameters.add(formatParameter(PARAMETER_IDS, joinEncoded(ids))); @@ -30,6 +41,12 @@ public String toQueryString() { return QUERY_PREFIX + String.join(PARAMETER_SEPARATOR, parameters); } + /** + * Creates a builder initialized with the required place IDs. + * + * @param ids place identifiers to look up + * @return a builder + */ public static Builder builder(List ids) { return new Builder(ids); } @@ -63,6 +80,9 @@ private static String encode(String rawText) { return URLEncoder.encode(rawText, StandardCharsets.UTF_8); } + /** + * Builder for {@link PlaceLookupInput}. + */ public static final class Builder { private final List ids; private Optional language = Optional.empty(); @@ -71,11 +91,22 @@ private Builder(List ids) { this.ids = normalizeList(ids); } + /** + * Sets the response language (BCP 47). + * + * @param language the language tag, or {@code null} to clear + * @return this builder + */ public Builder language(String language) { this.language = Optional.ofNullable(language); return this; } + /** + * Builds a validated {@link PlaceLookupInput}. + * + * @return an input instance + */ public PlaceLookupInput build() { return new PlaceLookupInput(ids, language); } diff --git a/src/main/java/com/williamcallahan/applemaps/domain/request/SearchAutocompleteInput.java b/src/main/java/com/williamcallahan/applemaps/domain/request/SearchAutocompleteInput.java index ccc7dad..24a91ac 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/request/SearchAutocompleteInput.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/request/SearchAutocompleteInput.java @@ -48,6 +48,22 @@ public record SearchAutocompleteInput( private static final String PARAMETER_INCLUDE_ADDRESS_CATEGORIES = "includeAddressCategories"; private static final String PARAMETER_EXCLUDE_ADDRESS_CATEGORIES = "excludeAddressCategories"; + /** + * Canonical constructor that validates required fields and normalizes optional values. + * + * @param q query string + * @param excludePoiCategories POI categories to exclude + * @param includePoiCategories POI categories to include + * @param limitToCountries country codes to restrict results to + * @param resultTypeFilter autocomplete result type filter + * @param includeAddressCategories address categories to include + * @param excludeAddressCategories address categories to exclude + * @param language optional response language (BCP 47) + * @param searchLocation optional search location hint + * @param searchRegion optional search region hint + * @param userLocation optional user location hint + * @param searchRegionPriority optional search region priority + */ public SearchAutocompleteInput { q = Objects.requireNonNull(q, "q"); excludePoiCategories = normalizeList(excludePoiCategories); @@ -63,6 +79,11 @@ public record SearchAutocompleteInput( searchRegionPriority = normalizeOptional(searchRegionPriority); } + /** + * Converts this input to a query string suitable for the Apple Maps Server API. + * + * @return a query string beginning with {@code ?} + */ public String toQueryString() { List parameters = new ArrayList<>(); parameters.add(formatParameter(PARAMETER_QUERY, encode(q))); @@ -92,6 +113,12 @@ public String toQueryString() { return QUERY_PREFIX + String.join(PARAMETER_SEPARATOR, parameters); } + /** + * Creates a builder initialized with the required query string. + * + * @param q query string + * @return a builder + */ public static Builder builder(String q) { return new Builder(q); } @@ -126,6 +153,9 @@ private static String encode(String value) { return URLEncoder.encode(value, StandardCharsets.UTF_8); } + /** + * Builder for {@link SearchAutocompleteInput}. + */ public static final class Builder { private final String q; private List excludePoiCategories = List.of(); @@ -144,61 +174,132 @@ private Builder(String q) { this.q = Objects.requireNonNull(q, "q"); } + /** + * Sets POI categories to exclude. + * + * @param categories categories to exclude + * @return this builder + */ public Builder excludePoiCategories(List categories) { this.excludePoiCategories = normalizeList(categories); return this; } + /** + * Sets POI categories to include. + * + * @param categories categories to include + * @return this builder + */ public Builder includePoiCategories(List categories) { this.includePoiCategories = normalizeList(categories); return this; } + /** + * Limits results to specific country codes. + * + * @param countries country codes (for example, ISO 3166-1 alpha-2) + * @return this builder + */ public Builder limitToCountries(List countries) { this.limitToCountries = normalizeList(countries); return this; } + /** + * Sets the autocomplete result type filter. + * + * @param resultTypes result types to include + * @return this builder + */ public Builder resultTypeFilter(List resultTypes) { this.resultTypeFilter = normalizeList(resultTypes); return this; } + /** + * Sets address categories to include. + * + * @param categories address categories to include + * @return this builder + */ public Builder includeAddressCategories(List categories) { this.includeAddressCategories = normalizeList(categories); return this; } + /** + * Sets address categories to exclude. + * + * @param categories address categories to exclude + * @return this builder + */ public Builder excludeAddressCategories(List categories) { this.excludeAddressCategories = normalizeList(categories); return this; } + /** + * Sets the response language (BCP 47). + * + * @param language the language tag, or {@code null} to clear + * @return this builder + */ public Builder language(String language) { this.language = Optional.ofNullable(language); return this; } + /** + * Sets the search location hint used by the API. + * + * @param searchLocation the search location, or {@code null} to clear + * @return this builder + */ public Builder searchLocation(SearchLocation searchLocation) { this.searchLocation = Optional.ofNullable(searchLocation); return this; } + /** + * Sets the search region hint used by the API. + * + * @param searchRegion the search region, or {@code null} to clear + * @return this builder + */ public Builder searchRegion(SearchRegion searchRegion) { this.searchRegion = Optional.ofNullable(searchRegion); return this; } + /** + * Sets the user location hint used by the API. + * + * @param userLocation the user location, or {@code null} to clear + * @return this builder + */ public Builder userLocation(UserLocation userLocation) { this.userLocation = Optional.ofNullable(userLocation); return this; } + /** + * Sets the search region priority. + * + * @param searchRegionPriority the search region priority, or {@code null} to clear + * @return this builder + */ public Builder searchRegionPriority(SearchRegionPriority searchRegionPriority) { this.searchRegionPriority = Optional.ofNullable(searchRegionPriority); return this; } + /** + * Builds a {@link SearchAutocompleteInput}. + * + * @return an input instance + */ public SearchAutocompleteInput build() { return new SearchAutocompleteInput( q, diff --git a/src/main/java/com/williamcallahan/applemaps/domain/request/SearchInput.java b/src/main/java/com/williamcallahan/applemaps/domain/request/SearchInput.java index a166b08..f2bea05 100644 --- a/src/main/java/com/williamcallahan/applemaps/domain/request/SearchInput.java +++ b/src/main/java/com/williamcallahan/applemaps/domain/request/SearchInput.java @@ -52,6 +52,24 @@ public record SearchInput( private static final String PARAMETER_INCLUDE_ADDRESS_CATEGORIES = "includeAddressCategories"; private static final String PARAMETER_EXCLUDE_ADDRESS_CATEGORIES = "excludeAddressCategories"; + /** + * Canonical constructor that validates required fields and normalizes optional values. + * + * @param q query string + * @param excludePoiCategories POI categories to exclude + * @param includePoiCategories POI categories to include + * @param limitToCountries country codes to restrict results to + * @param resultTypeFilter search result type filter + * @param includeAddressCategories address categories to include + * @param excludeAddressCategories address categories to exclude + * @param language optional response language (BCP 47) + * @param searchLocation optional search location hint + * @param searchRegion optional search region hint + * @param userLocation optional user location hint + * @param searchRegionPriority optional search region priority + * @param enablePagination optional flag to enable pagination + * @param pageToken optional page token for pagination + */ public SearchInput { q = Objects.requireNonNull(q, "q"); excludePoiCategories = normalizeList(excludePoiCategories); @@ -69,6 +87,11 @@ public record SearchInput( pageToken = normalizeOptional(pageToken); } + /** + * Converts this input to a query string suitable for the Apple Maps Server API. + * + * @return a query string beginning with {@code ?} + */ public String toQueryString() { List parameters = new ArrayList<>(); parameters.add(formatParameter(PARAMETER_QUERY, encode(q))); @@ -100,6 +123,12 @@ public String toQueryString() { return QUERY_PREFIX + String.join(PARAMETER_SEPARATOR, parameters); } + /** + * Creates a builder initialized with the required query string. + * + * @param q query string + * @return a builder + */ public static Builder builder(String q) { return new Builder(q); } @@ -134,6 +163,9 @@ private static String encode(String value) { return URLEncoder.encode(value, StandardCharsets.UTF_8); } + /** + * Builder for {@link SearchInput}. + */ public static final class Builder { private final String q; private List excludePoiCategories = List.of(); @@ -154,71 +186,154 @@ private Builder(String q) { this.q = Objects.requireNonNull(q, "q"); } + /** + * Sets POI categories to exclude. + * + * @param categories categories to exclude + * @return this builder + */ public Builder excludePoiCategories(List categories) { this.excludePoiCategories = normalizeList(categories); return this; } + /** + * Sets POI categories to include. + * + * @param categories categories to include + * @return this builder + */ public Builder includePoiCategories(List categories) { this.includePoiCategories = normalizeList(categories); return this; } + /** + * Limits results to specific country codes. + * + * @param countries country codes (for example, ISO 3166-1 alpha-2) + * @return this builder + */ public Builder limitToCountries(List countries) { this.limitToCountries = normalizeList(countries); return this; } + /** + * Sets the search result type filter. + * + * @param resultTypes result types to include + * @return this builder + */ public Builder resultTypeFilter(List resultTypes) { this.resultTypeFilter = normalizeList(resultTypes); return this; } + /** + * Sets address categories to include. + * + * @param categories address categories to include + * @return this builder + */ public Builder includeAddressCategories(List categories) { this.includeAddressCategories = normalizeList(categories); return this; } + /** + * Sets address categories to exclude. + * + * @param categories address categories to exclude + * @return this builder + */ public Builder excludeAddressCategories(List categories) { this.excludeAddressCategories = normalizeList(categories); return this; } + /** + * Sets the response language (BCP 47). + * + * @param language the language tag, or {@code null} to clear + * @return this builder + */ public Builder language(String language) { this.language = Optional.ofNullable(language); return this; } + /** + * Sets the search location hint used by the API. + * + * @param searchLocation the search location, or {@code null} to clear + * @return this builder + */ public Builder searchLocation(SearchLocation searchLocation) { this.searchLocation = Optional.ofNullable(searchLocation); return this; } + /** + * Sets the search region hint used by the API. + * + * @param searchRegion the search region, or {@code null} to clear + * @return this builder + */ public Builder searchRegion(SearchRegion searchRegion) { this.searchRegion = Optional.ofNullable(searchRegion); return this; } + /** + * Sets the user location hint used by the API. + * + * @param userLocation the user location, or {@code null} to clear + * @return this builder + */ public Builder userLocation(UserLocation userLocation) { this.userLocation = Optional.ofNullable(userLocation); return this; } + /** + * Sets the search region priority. + * + * @param searchRegionPriority the search region priority, or {@code null} to clear + * @return this builder + */ public Builder searchRegionPriority(SearchRegionPriority searchRegionPriority) { this.searchRegionPriority = Optional.ofNullable(searchRegionPriority); return this; } + /** + * Sets whether pagination should be enabled. + * + * @param enablePagination {@code true} to enable pagination, or {@code null} to clear + * @return this builder + */ public Builder enablePagination(Boolean enablePagination) { this.enablePagination = Optional.ofNullable(enablePagination); return this; } + /** + * Sets the page token used for pagination. + * + * @param pageToken the page token, or {@code null} to clear + * @return this builder + */ public Builder pageToken(String pageToken) { this.pageToken = Optional.ofNullable(pageToken); return this; } + /** + * Builds a {@link SearchInput}. + * + * @return an input instance + */ public SearchInput build() { return new SearchInput( q, From 05d3f39b43bfaf146f5c0d494f6f43ac84c1bda2 Mon Sep 17 00:00:00 2001 From: William Callahan Date: Fri, 16 Jan 2026 21:41:43 -0800 Subject: [PATCH 5/7] feat: add Origin header support for JWT configurations Some Apple Maps JWT configurations require an Origin header that matches the origin claim in the token. This adds optional origin support throughout the SDK, allowing users to specify the origin via constructor parameter or environment variable. - Add origin parameter to AppleMaps constructors with overloads - Thread origin through HttpAppleMapsGateway to AppleMapsAuthorizationService - Set Origin header on token refresh and API requests when provided - Add getOrigin() accessor to AppleMapsAuthorizationService - Update CLI to read APPLE_MAPS_ORIGIN from env/system properties - Reorder imports to place java.* before project imports (style consistency) --- .../williamcallahan/applemaps/AppleMaps.java | 34 ++++++++++--- .../AppleMapsAuthorizationService.java | 37 +++++++++++--- .../mapsserver/HttpAppleMapsGateway.java | 50 ++++++++++++------- .../applemaps/cli/AppleMapsCli.java | 18 ++++--- .../AppleMapsAuthorizationServiceTest.java | 9 +++- 5 files changed, 107 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/williamcallahan/applemaps/AppleMaps.java b/src/main/java/com/williamcallahan/applemaps/AppleMaps.java index 3aaeed6..b0f5b14 100644 --- a/src/main/java/com/williamcallahan/applemaps/AppleMaps.java +++ b/src/main/java/com/williamcallahan/applemaps/AppleMaps.java @@ -1,5 +1,10 @@ package com.williamcallahan.applemaps; +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + import com.williamcallahan.applemaps.adapters.mapsserver.AppleMapsClientException; import com.williamcallahan.applemaps.adapters.mapsserver.HttpAppleMapsGateway; import com.williamcallahan.applemaps.domain.model.AlternateIdsResponse; @@ -19,10 +24,6 @@ import com.williamcallahan.applemaps.domain.request.PlaceLookupInput; import com.williamcallahan.applemaps.domain.request.SearchAutocompleteInput; import com.williamcallahan.applemaps.domain.request.SearchInput; -import java.time.Duration; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; /** * Entry point for Apple Maps Server API operations. @@ -39,7 +40,17 @@ public final class AppleMaps implements AutoCloseable { * @param authToken the Apple Maps Server API authorization token */ public AppleMaps(String authToken) { - this(authToken, DEFAULT_TIMEOUT); + this(authToken, DEFAULT_TIMEOUT, null); + } + + /** + * Creates an {@link AppleMaps} client using the provided authorization token and origin. + * + * @param authToken the Apple Maps Server API authorization token + * @param origin value for the Origin header (required for some JWT configurations) + */ + public AppleMaps(String authToken, String origin) { + this(authToken, DEFAULT_TIMEOUT, origin); } /** @@ -49,9 +60,20 @@ public AppleMaps(String authToken) { * @param timeout request timeout */ public AppleMaps(String authToken, Duration timeout) { + this(authToken, timeout, null); + } + + /** + * Creates an {@link AppleMaps} client using the provided authorization token, timeout, and origin. + * + * @param authToken the Apple Maps Server API authorization token + * @param timeout request timeout + * @param origin value for the Origin header (required for some JWT configurations) + */ + public AppleMaps(String authToken, Duration timeout, String origin) { Objects.requireNonNull(authToken, "authToken"); Objects.requireNonNull(timeout, "timeout"); - this.gateway = new HttpAppleMapsGateway(authToken, timeout); + this.gateway = new HttpAppleMapsGateway(authToken, timeout, origin); } /** diff --git a/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsAuthorizationService.java b/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsAuthorizationService.java index e6fbe19..deb4ece 100644 --- a/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsAuthorizationService.java +++ b/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsAuthorizationService.java @@ -1,7 +1,5 @@ package com.williamcallahan.applemaps.adapters.mapsserver; -import com.williamcallahan.applemaps.adapters.jackson.AppleMapsObjectMapperFactory; -import com.williamcallahan.applemaps.domain.model.TokenResponse; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -13,6 +11,10 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; + +import com.williamcallahan.applemaps.adapters.jackson.AppleMapsObjectMapperFactory; +import com.williamcallahan.applemaps.domain.model.TokenResponse; + import tools.jackson.databind.ObjectMapper; /** @@ -27,6 +29,7 @@ public final class AppleMapsAuthorizationService { private final URI tokenUri; private final Duration timeout; private final String authToken; + private final String origin; private final Clock clock; private final ReentrantLock refreshLock = new ReentrantLock(); private final AtomicReference accessToken = new AtomicReference<>(); @@ -37,8 +40,8 @@ public final class AppleMapsAuthorizationService { * @param authToken the Apple Maps Server API authorization token * @param timeout request timeout for token exchange */ - public AppleMapsAuthorizationService(String authToken, Duration timeout) { - this(new Dependencies(authToken, timeout)); + public AppleMapsAuthorizationService(String authToken, Duration timeout, String origin) { + this(new Dependencies(authToken, timeout, origin)); } AppleMapsAuthorizationService(Dependencies dependencies) { @@ -47,8 +50,13 @@ public AppleMapsAuthorizationService(String authToken, Duration timeout) { this.tokenUri = dependencies.tokenUri(); this.timeout = dependencies.timeout(); this.authToken = dependencies.authToken(); + this.origin = dependencies.origin(); this.clock = dependencies.clock(); } + + public String getOrigin() { + return origin; + } /** * Returns a cached access token, refreshing it when needed. @@ -74,12 +82,17 @@ public String getAccessToken() { } private AccessToken refreshAccessToken() { - HttpRequest httpRequest = HttpRequest.newBuilder() + HttpRequest.Builder builder = HttpRequest.newBuilder() .GET() .timeout(timeout) .uri(tokenUri) - .setHeader("Authorization", "Bearer " + authToken) - .build(); + .setHeader("Authorization", "Bearer " + authToken); + + if (origin != null) { + builder.setHeader("Origin", origin); + } + + HttpRequest httpRequest = builder.build(); try { HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); if (response.statusCode() != 200) { @@ -130,15 +143,17 @@ static final class Dependencies { private final URI tokenUri; private final Duration timeout; private final String authToken; + private final String origin; private final Clock clock; - Dependencies(String authToken, Duration timeout) { + Dependencies(String authToken, Duration timeout, String origin) { this(new DependenciesConfig( AppleMapsObjectMapperFactory.create(), HttpClient.newHttpClient(), URI.create("https://maps-api.apple.com" + TOKEN_PATH), timeout, authToken, + origin, Clock.systemUTC() )); } @@ -149,6 +164,7 @@ static final class Dependencies { this.tokenUri = Objects.requireNonNull(config.tokenUri(), "tokenUri"); this.timeout = Objects.requireNonNull(config.timeout(), "timeout"); this.authToken = Objects.requireNonNull(config.authToken(), "authToken"); + this.origin = config.origin(); this.clock = Objects.requireNonNull(config.clock(), "clock"); } @@ -158,6 +174,7 @@ record DependenciesConfig( URI tokenUri, Duration timeout, String authToken, + String origin, Clock clock ) { } @@ -182,6 +199,10 @@ String authToken() { return authToken; } + String origin() { + return origin; + } + Clock clock() { return clock; } diff --git a/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/HttpAppleMapsGateway.java b/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/HttpAppleMapsGateway.java index 95c2174..d421b85 100644 --- a/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/HttpAppleMapsGateway.java +++ b/src/main/java/com/williamcallahan/applemaps/adapters/mapsserver/HttpAppleMapsGateway.java @@ -1,5 +1,17 @@ package com.williamcallahan.applemaps.adapters.mapsserver; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + import com.williamcallahan.applemaps.adapters.jackson.AppleMapsObjectMapperFactory; import com.williamcallahan.applemaps.domain.model.AlternateIdsResponse; import com.williamcallahan.applemaps.domain.model.DirectionsResponse; @@ -17,17 +29,7 @@ import com.williamcallahan.applemaps.domain.request.PlaceLookupInput; import com.williamcallahan.applemaps.domain.request.SearchAutocompleteInput; import com.williamcallahan.applemaps.domain.request.SearchInput; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.Objects; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; + import tools.jackson.databind.ObjectMapper; /** @@ -63,7 +65,11 @@ public final class HttpAppleMapsGateway implements AppleMapsGateway { * @param timeout request timeout */ public HttpAppleMapsGateway(String authToken, Duration timeout) { - this(new Dependencies(authToken, timeout)); + this(new Dependencies(authToken, timeout, null)); + } + + public HttpAppleMapsGateway(String authToken, Duration timeout, String origin) { + this(new Dependencies(authToken, timeout, origin)); } HttpAppleMapsGateway(Dependencies dependencies) { @@ -164,12 +170,18 @@ private URI buildUri(String path, String queryString) { } private T invokeApi(String operation, URI uri, Class responseType) { - HttpRequest httpRequest = HttpRequest.newBuilder() + + HttpRequest.Builder builder = HttpRequest.newBuilder() .GET() .uri(uri) .timeout(timeout) - .setHeader("Authorization", "Bearer " + authorizationService.getAccessToken()) - .build(); + .setHeader("Authorization", "Bearer " + authorizationService.getAccessToken()); + + if (authorizationService.getOrigin() != null) { + builder.setHeader("Origin", authorizationService.getOrigin()); + } + + HttpRequest httpRequest = builder.build(); try { HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); if (response.statusCode() != 200) { @@ -190,8 +202,8 @@ static final class Dependencies { private final Duration timeout; private final ExecutorService executorService; - Dependencies(String authToken, Duration timeout) { - this(createDefaultDependenciesConfig(authToken, timeout)); + Dependencies(String authToken, Duration timeout, String origin) { + this(createDefaultDependenciesConfig(authToken, timeout, origin)); } Dependencies(DependenciesConfig config) { @@ -211,7 +223,7 @@ record DependenciesConfig( ) { } - private static DependenciesConfig createDefaultDependenciesConfig(String authToken, Duration timeout) { + private static DependenciesConfig createDefaultDependenciesConfig(String authToken, Duration timeout, String origin) { ThreadFactory httpClientThreadFactory = new ThreadFactory() { private final AtomicInteger httpClientThreadSequence = new AtomicInteger(1); @@ -227,7 +239,7 @@ public Thread newThread(Runnable runnable) { HttpClient httpClient = HttpClient.newBuilder().executor(httpClientExecutorService).build(); return new DependenciesConfig( - new AppleMapsAuthorizationService(authToken, timeout), + new AppleMapsAuthorizationService(authToken, timeout, origin), AppleMapsObjectMapperFactory.create(), httpClient, timeout, diff --git a/src/main/java/com/williamcallahan/applemaps/cli/AppleMapsCli.java b/src/main/java/com/williamcallahan/applemaps/cli/AppleMapsCli.java index 4144710..d45f955 100644 --- a/src/main/java/com/williamcallahan/applemaps/cli/AppleMapsCli.java +++ b/src/main/java/com/williamcallahan/applemaps/cli/AppleMapsCli.java @@ -1,5 +1,11 @@ package com.williamcallahan.applemaps.cli; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + import com.williamcallahan.applemaps.AppleMaps; import com.williamcallahan.applemaps.adapters.jackson.AppleMapsObjectMapperFactory; import com.williamcallahan.applemaps.adapters.mapsserver.AppleMapsApiException; @@ -13,11 +19,6 @@ import com.williamcallahan.applemaps.domain.request.GeocodeInput; import com.williamcallahan.applemaps.domain.request.SearchAutocompleteInput; import com.williamcallahan.applemaps.domain.request.SearchInput; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Objects; /** * Command line interface for exercising Apple Maps Server API operations. @@ -112,8 +113,9 @@ private static void run(String[] args) { Arrays.copyOfRange(args, 1, args.length) ); String token = resolveToken(); + String origin = resolveOrigin(); - try (AppleMaps api = new AppleMaps(token)) { + try (AppleMaps api = new AppleMaps(token, origin)) { ParsedOptions resolvedOptions = commandUsesLocationHints(command) ? options.resolveUserLocation(api) : options; @@ -255,6 +257,10 @@ private static String resolveToken() { ); } + private static String resolveOrigin() { + return readSettingText("APPLE_MAPS_ORIGIN").orElse(null); + } + private static java.util.Optional readSettingText( String settingName ) { diff --git a/src/test/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsAuthorizationServiceTest.java b/src/test/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsAuthorizationServiceTest.java index 198f8ea..40c00d8 100644 --- a/src/test/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsAuthorizationServiceTest.java +++ b/src/test/java/com/williamcallahan/applemaps/adapters/mapsserver/AppleMapsAuthorizationServiceTest.java @@ -1,6 +1,7 @@ package com.williamcallahan.applemaps.adapters.mapsserver; -import com.williamcallahan.applemaps.adapters.jackson.AppleMapsObjectMapperFactory; +import static org.junit.jupiter.api.Assertions.assertEquals; + import java.net.Authenticator; import java.net.CookieHandler; import java.net.ProxySelector; @@ -29,12 +30,14 @@ import java.util.concurrent.Executor; import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicInteger; + import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; + import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; +import com.williamcallahan.applemaps.adapters.jackson.AppleMapsObjectMapperFactory; class AppleMapsAuthorizationServiceTest { private static final URI TOKEN_URI = URI.create("https://maps-api.apple.com/v1/token"); @@ -61,6 +64,7 @@ void getAccessTokenCachesUntilExpiring() { TOKEN_URI, REQUEST_TIMEOUT, AUTH_TOKEN, + "origin", tokenClock ) ) @@ -87,6 +91,7 @@ void getAccessTokenRefreshesAfterExpiry() { TOKEN_URI, REQUEST_TIMEOUT, AUTH_TOKEN, + "origin", tokenClock ) ) From 39fdf9658db6b7e1654ba1d70cc4ba9259afbf4e Mon Sep 17 00:00:00 2001 From: William Callahan Date: Fri, 16 Jan 2026 21:41:50 -0800 Subject: [PATCH 6/7] docs: document Origin header configuration Update documentation to explain how to configure the optional Origin header for JWT configurations that require it. - Add APPLE_MAPS_ORIGIN to environment variable examples - Update code example showing origin parameter usage - Document origin option in CLI prerequisites --- docs/authorization.md | 9 +++++++-- docs/cli.md | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/authorization.md b/docs/authorization.md index bd5109f..c49f2e5 100644 --- a/docs/authorization.md +++ b/docs/authorization.md @@ -18,6 +18,7 @@ Option A (recommended): environment variable ```bash export APPLE_MAPS_TOKEN="your-token" +export APPLE_MAPS_ORIGIN="https://api.example.com" # Required if your JWT has a specific origin ``` Option B (local dev for this repo): `.env` fallback @@ -31,7 +32,7 @@ cp .env-example .env `.env` is ignored by git (so you don’t accidentally commit secrets). This project’s Gradle build loads `.env` into **system properties**, which is mainly convenient for running tests locally. -In CI, prefer setting `APPLE_MAPS_TOKEN` as an environment variable. +In CI, set `APPLE_MAPS_TOKEN` as an environment variable. Optionally set `APPLE_MAPS_ORIGIN` if your token requires it (e.g., `https://api.example.com` matching your JWT's `origin` claim). ## Supplying the token to the SDK @@ -40,9 +41,13 @@ For example: ```java String token = System.getenv("APPLE_MAPS_TOKEN"); +String origin = System.getenv("APPLE_MAPS_ORIGIN"); if (token == null || token.isBlank()) { token = System.getProperty("APPLE_MAPS_TOKEN"); } +if (origin == null || origin.isBlank()) { + origin = System.getProperty("APPLE_MAPS_ORIGIN"); +} -AppleMaps api = new AppleMaps(token); +AppleMaps api = new AppleMaps(token, origin); ``` diff --git a/docs/cli.md b/docs/cli.md index eb08b4b..57e4dd1 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -4,7 +4,7 @@ This repo includes a small CLI for running Apple Maps Server queries from your t ## Prerequisites -Set `APPLE_MAPS_TOKEN` (env var recommended). See `README.md` / `docs/authorization.md`. +Set `APPLE_MAPS_TOKEN` (env var recommended). Optionally set `APPLE_MAPS_ORIGIN` if your token requires it (e.g. `https://api.example.com` matching your JWT's `origin` claim). See `README.md` / `docs/authorization.md`. ## Run From b0f24eeb7810a4776e694d2ebd68ff96fbd299e8 Mon Sep 17 00:00:00 2001 From: William Callahan Date: Fri, 16 Jan 2026 21:41:55 -0800 Subject: [PATCH 7/7] chore: bump version to 0.1.4-SNAPSHOT --- build.gradle.kts | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 9fec39e..79dd5e6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,7 +47,7 @@ val javaTargetVersion = 17 group = providers.gradleProperty("GROUP").orNull ?: "com.williamcallahan" version = providers.gradleProperty("version").orNull ?: providers.gradleProperty("VERSION_NAME").orNull - ?: "0.1.3" + ?: "0.1.4-SNAPSHOT" repositories { mavenCentral() diff --git a/gradle.properties b/gradle.properties index c744b7d..5ffe9e4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ GROUP=com.williamcallahan POM_ARTIFACT_ID=apple-maps-java -VERSION_NAME=0.1.3-SNAPSHOT +VERSION_NAME=0.1.4-SNAPSHOT POM_NAME=Apple Maps Java POM_DESCRIPTION=Apple Maps Java implements the Apple Maps Server API for use in JVMs.