diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 87d7796..74d2b4e 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -28,21 +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 }} - ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }} - ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} - run: ./gradlew publishAllPublicationsToMavenCentralRepository -x test + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + 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 c5896a1..64165ad 100644 --- a/.github/workflows/Release.yaml +++ b/.github/workflows/Release.yaml @@ -11,11 +11,10 @@ jobs: release: runs-on: ubuntu-latest 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 }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} steps: - uses: actions/checkout@v6 @@ -46,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/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 7a3214c..79dd5e6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,8 +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 @@ -45,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.4-SNAPSHOT" repositories { mavenCentral() @@ -160,12 +162,69 @@ tasks.withType().configureEach { (options as StandardJavadocDocletOptions).addStringOption("-release", javaTargetVersion.toString()) } -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - signAllPublications() +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 + } + } + } +} + +signing { + val signingKey = providers.environmentVariable("GPG_PRIVATE_KEY").orNull + val signingPassword = providers.environmentVariable("GPG_PASSPHRASE").orNull + + 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") + } } 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 diff --git a/gradle.properties b/gradle.properties index b8dc14d..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.2-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. diff --git a/src/main/java/com/williamcallahan/applemaps/AppleMaps.java b/src/main/java/com/williamcallahan/applemaps/AppleMaps.java index 30c18a7..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. @@ -33,41 +34,114 @@ 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); + 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); } + /** + * 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) { + 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); } + /** + * 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 +166,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..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,12 +29,19 @@ 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<>(); - public AppleMapsAuthorizationService(String authToken, Duration timeout) { - this(new Dependencies(authToken, timeout)); + /** + * 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, String origin) { + this(new Dependencies(authToken, timeout, origin)); } AppleMapsAuthorizationService(Dependencies dependencies) { @@ -41,9 +50,19 @@ 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. + * + * @return the access token string + */ public String getAccessToken() { AccessToken cachedToken = accessToken.get(); if (cachedToken != null && !isExpiring(cachedToken)) { @@ -63,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) { @@ -119,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() )); } @@ -138,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"); } @@ -147,6 +174,7 @@ record DependenciesConfig( URI tokenUri, Duration timeout, String authToken, + String origin, Clock clock ) { } @@ -171,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/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..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; /** @@ -56,8 +58,18 @@ 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)); + this(new Dependencies(authToken, timeout, null)); + } + + public HttpAppleMapsGateway(String authToken, Duration timeout, String origin) { + this(new Dependencies(authToken, timeout, origin)); } HttpAppleMapsGateway(Dependencies dependencies) { @@ -158,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) { @@ -184,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) { @@ -205,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); @@ -221,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 cd3adb9..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,12 +19,10 @@ 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. + */ public final class AppleMapsCli { private static final int EXIT_USAGE = 2; @@ -55,6 +59,11 @@ public final class AppleMapsCli { private AppleMapsCli() {} + /** + * CLI entry point. + * + * @param args command line arguments + */ public static void main(String[] args) { try { run(args); @@ -104,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; @@ -247,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/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, 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 ) )