baselineNames = new ArrayList<>();
+
+ try {
+ paginator.stream()
+ .flatMap(response -> response.baselines().stream())
+ .forEach(baseline -> baselineNames.add(baseline.name()));
+
+ System.out.println(baselineNames.size() + " baseline(s) retrieved.");
+ for (String baselineName : baselineNames) {
+ System.out.println("\t" + baselineName);
+ }
+
+ } catch (ControlTowerException e) {
+ if ("AccessDeniedException".equals(e.awsErrorDetails().errorCode())) {
+ System.out.println("Access denied. Please ensure you have the necessary permissions.");
+ } else {
+ System.out.println("An error occurred: " + e.getMessage());
+ }
+ }
+ }
+}
+// snippet-end:[controltower.java2.hello.main]
diff --git a/javav2/example_code/controltower/src/main/java/com/example/controltower/scenario/ControlTowerActions.java b/javav2/example_code/controltower/src/main/java/com/example/controltower/scenario/ControlTowerActions.java
new file mode 100644
index 00000000000..676f81bfdf5
--- /dev/null
+++ b/javav2/example_code/controltower/src/main/java/com/example/controltower/scenario/ControlTowerActions.java
@@ -0,0 +1,980 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package com.example.controltower.scenario;
+
+import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
+import software.amazon.awssdk.core.retry.RetryMode;
+import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
+import software.amazon.awssdk.http.crt.AwsCrtAsyncHttpClient;
+import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
+import software.amazon.awssdk.services.controlcatalog.ControlCatalogAsyncClient;
+import software.amazon.awssdk.services.controltower.ControlTowerAsyncClient;
+import software.amazon.awssdk.services.controltower.model.*;
+import software.amazon.awssdk.services.controltower.paginators.ListBaselinesPublisher;
+import software.amazon.awssdk.services.controltower.paginators.ListEnabledBaselinesPublisher;
+import software.amazon.awssdk.services.controltower.paginators.ListEnabledControlsPublisher;
+import software.amazon.awssdk.core.exception.SdkException;
+import software.amazon.awssdk.services.controlcatalog.model.ControlSummary;
+import software.amazon.awssdk.services.controlcatalog.model.ListControlsRequest;
+import software.amazon.awssdk.services.controlcatalog.paginators.ListControlsPublisher;
+import software.amazon.awssdk.services.controltower.paginators.ListLandingZonesPublisher;
+import software.amazon.awssdk.services.organizations.OrganizationsAsyncClient;
+import software.amazon.awssdk.services.organizations.model.CreateOrganizationRequest;
+import software.amazon.awssdk.services.organizations.model.ListOrganizationalUnitsForParentRequest;
+import software.amazon.awssdk.services.organizations.model.Organization;
+import software.amazon.awssdk.services.organizations.model.OrganizationFeatureSet;
+import software.amazon.awssdk.services.organizations.model.OrganizationalUnit;
+import software.amazon.awssdk.services.organizations.paginators.ListOrganizationalUnitsForParentPublisher;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Before running this Java V2 code example, set up your development
+ * environment, including your credentials.
+ *
+ * For more information, see the following documentation topic:
+ *
+ * https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/get-started.html
+ *
+ * This Java code example shows how to perform AWS Control Tower operations.
+ */
+
+// snippet-start:[controltower.java2.controltower_actions.main]
+public class ControlTowerActions {
+ private static ControlCatalogAsyncClient controlCatalogAsyncClient;
+ private static ControlTowerAsyncClient controlTowerAsyncClient;
+ private static OrganizationsAsyncClient orgAsyncClient;
+
+
+ private static OrganizationsAsyncClient getAsyncOrgClient() {
+ if (orgAsyncClient == null) {
+ SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder()
+ .maxConcurrency(50)
+ .connectionTimeout(Duration.ofSeconds(60))
+ .readTimeout(Duration.ofSeconds(60))
+ .writeTimeout(Duration.ofSeconds(60))
+ .build();
+
+ ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder()
+ .apiCallTimeout(Duration.ofMinutes(2))
+ .apiCallAttemptTimeout(Duration.ofSeconds(90))
+ .build();
+
+ orgAsyncClient = OrganizationsAsyncClient.builder()
+ .httpClient(httpClient)
+ .overrideConfiguration(overrideConfig)
+ .build();
+ }
+ return orgAsyncClient;
+ }
+
+ private static ControlCatalogAsyncClient getAsyncCatClient() {
+ if (controlCatalogAsyncClient == null) {
+ SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder()
+ .maxConcurrency(100)
+ .connectionTimeout(Duration.ofSeconds(60))
+ .readTimeout(Duration.ofSeconds(60))
+ .writeTimeout(Duration.ofSeconds(60))
+ .build();
+
+ ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder()
+ .apiCallTimeout(Duration.ofMinutes(2))
+ .apiCallAttemptTimeout(Duration.ofSeconds(90))
+ .retryStrategy(RetryMode.STANDARD)
+ .build();
+
+ controlCatalogAsyncClient = ControlCatalogAsyncClient.builder()
+ .httpClient(httpClient)
+ .overrideConfiguration(overrideConfig)
+ .build();
+ }
+ return controlCatalogAsyncClient;
+ }
+
+ private static ControlTowerAsyncClient getAsyncClient() {
+ if (controlTowerAsyncClient == null) {
+
+ SdkAsyncHttpClient httpClient =
+ AwsCrtAsyncHttpClient.builder()
+ .maxConcurrency(100)
+ .connectionTimeout(Duration.ofSeconds(60))
+ .build();
+
+ ClientOverrideConfiguration overrideConfig =
+ ClientOverrideConfiguration.builder()
+ .apiCallTimeout(Duration.ofMinutes(2))
+ .apiCallAttemptTimeout(Duration.ofSeconds(90))
+ .retryStrategy(RetryMode.STANDARD)
+ .build();
+
+ controlTowerAsyncClient =
+ ControlTowerAsyncClient.builder()
+ .httpClient(httpClient)
+ .overrideConfiguration(overrideConfig)
+ .build();
+ }
+
+ return controlTowerAsyncClient;
+ }
+
+ public record OrgSetupResult(String orgId, String sandboxOuArn) {}
+
+ public CompletableFuture setupOrganizationAsync() {
+ System.out.println("Starting organization setup…");
+
+ OrganizationsAsyncClient client = getAsyncOrgClient();
+
+ // Step 1: Describe or create organization
+ CompletableFuture orgFuture = client.describeOrganization()
+ .thenApply(desc -> {
+ System.out.println("Organization exists: "+ desc.organization().id());
+ return desc.organization();
+ })
+ .exceptionallyCompose(ex -> {
+ Throwable cause = ex.getCause() != null ? ex.getCause() : ex;
+ if (cause instanceof AwsServiceException awsEx &&
+ "AWSOrganizationsNotInUseException".equals(awsEx.awsErrorDetails().errorCode())) {
+ System.out.println("No organization found. Creating one…");
+ return client.createOrganization(CreateOrganizationRequest.builder()
+ .featureSet(OrganizationFeatureSet.ALL)
+ .build())
+ .thenApply(createResp -> {
+ System.out.println("Created organization: {}" + createResp.organization().id());
+ return createResp.organization();
+ });
+ }
+ return CompletableFuture.failedFuture(
+ new CompletionException("Failed to describe or create organization", cause)
+ );
+ });
+
+ // Step 2: Locate Sandbox OU
+ return orgFuture.thenCompose(org -> {
+ String orgId = org.id();
+ System.out.println("Organization ID: {}"+ orgId);
+
+ return client.listRoots()
+ .thenCompose(rootsResp -> {
+ if (rootsResp.roots().isEmpty()) {
+ return CompletableFuture.failedFuture(
+ new RuntimeException("No root found in organization")
+ );
+ }
+ String rootId = rootsResp.roots().get(0).id();
+
+ ListOrganizationalUnitsForParentRequest ouRequest =
+ ListOrganizationalUnitsForParentRequest.builder()
+ .parentId(rootId)
+ .build();
+
+ ListOrganizationalUnitsForParentPublisher paginator =
+ client.listOrganizationalUnitsForParentPaginator(ouRequest);
+
+ AtomicReference sandboxOuArnRef = new AtomicReference<>();
+
+ return paginator.subscribe(page -> {
+ for (OrganizationalUnit ou : page.organizationalUnits()) {
+ if ("Sandbox".equals(ou.name())) {
+ sandboxOuArnRef.set(ou.arn());
+ System.out.println("Found Sandbox OU: " + ou.id());
+ break;
+ }
+ }
+ })
+ .thenApply(v -> {
+ String sandboxArn = sandboxOuArnRef.get();
+ if (sandboxArn == null) {
+ System.out.println("Sandbox OU not found.");
+ }
+ return new OrgSetupResult(orgId, sandboxArn);
+ });
+ });
+ }).exceptionally(ex -> {
+ Throwable cause = ex.getCause() != null ? ex.getCause() : ex;
+ System.out.println("Failed to setup organization: {}" + cause.getMessage());
+ throw new CompletionException(cause);
+ });
+ }
+
+
+
+ // snippet-start:[controltower.java2.list_landing_zones.main]
+ /**
+ * Lists all landing zones using pagination to retrieve complete results.
+ *
+ * @return a list of all landing zones
+ * @throws ControlTowerException if a service-specific error occurs
+ * @throws SdkException if an SDK error occurs
+ */
+ public CompletableFuture> listLandingZonesAsync() {
+ System.out.format("Starting list landing zones paginator…");
+
+ ListLandingZonesRequest request = ListLandingZonesRequest.builder().build();
+ ListLandingZonesPublisher paginator = getAsyncClient().listLandingZonesPaginator(request);
+ List landingZones = new ArrayList<>();
+
+ return paginator.subscribe(response -> {
+ if (response.landingZones() != null && !response.landingZones().isEmpty()) {
+ response.landingZones().forEach(lz -> {
+ System.out.format("Landing zone ARN: {}", lz.arn());
+ landingZones.add(lz);
+ });
+ } else {
+ System.out.println("Page contained no landing zones.");
+ }
+ })
+ .thenRun(() -> System.out.println("Successfully retrieved {} landing zones."+ landingZones.size()))
+ .thenApply(v -> landingZones)
+ .exceptionally(ex -> {
+ Throwable cause = ex.getCause() != null ? ex.getCause() : ex;
+
+ if (cause instanceof ControlTowerException e) {
+ String errorCode = e.awsErrorDetails().errorCode();
+ switch (errorCode) {
+ case "AccessDeniedException":
+ throw new CompletionException(
+ "Access denied when listing landing zones: " + e.getMessage(), e);
+ default:
+ throw new CompletionException(
+ "Error listing landing zones: " + e.getMessage(), e);
+ }
+ }
+
+ if (cause instanceof SdkException) {
+ throw new CompletionException(
+ "SDK error listing landing zones: " + cause.getMessage(), cause);
+ }
+
+ throw new CompletionException("Failed to list landing zones", cause);
+ });
+ }
+ // snippet-end:[controltower.java2.list_landing_zones.main]
+
+ // snippet-start:[controltower.java2.list_baselines.main]
+ /**
+ * Lists all available baselines using pagination to retrieve complete results.
+ *
+ * @return a list of all baselines
+ * @throws ControlTowerException if a service-specific error occurs
+ * @throws SdkException if an SDK error occurs
+ */
+ public CompletableFuture> listBaselinesAsync() {
+ System.out.format("Starting list baselines paginator…");
+ ListBaselinesRequest request = ListBaselinesRequest.builder().build();
+ ListBaselinesPublisher paginator =
+ getAsyncClient().listBaselinesPaginator(request);
+
+ List baselines = new ArrayList<>();
+ return paginator.subscribe(response -> {
+ if (response.baselines() != null && !response.baselines().isEmpty()) {
+ response.baselines().forEach(baseline -> {
+ System.out.format("Baseline: {}", baseline.name());
+ baselines.add(baseline);
+ });
+ } else {
+ System.out.format("Page contained no baselines.");
+ }
+ })
+ .thenRun(() ->
+ System.out.format("Successfully listed baselines. Total: {}", baselines.size())
+ )
+ .thenApply(v -> baselines)
+ .exceptionally(ex -> {
+ Throwable cause = ex.getCause() != null ? ex.getCause() : ex;
+
+ if (cause instanceof ControlTowerException e) {
+ String errorCode = e.awsErrorDetails().errorCode();
+
+ if ("AccessDeniedException".equals(errorCode)) {
+ throw new CompletionException(
+ "Access denied when listing baselines: %s".formatted(e.getMessage()),
+ e
+ );
+ }
+
+ throw new CompletionException(
+ "Error listing baselines: %s".formatted(e.getMessage()),
+ e
+ );
+ }
+
+ if (cause instanceof SdkException) {
+ throw new CompletionException(
+ "SDK error listing baselines: %s".formatted(cause.getMessage()),
+ cause
+ );
+ }
+
+ throw new CompletionException("Failed to list baselines", cause);
+ });
+ }
+ // snippet-end:[controltower.java2.list_baselines.main]
+
+ // snippet-start:[controltower.java2.list_enabled_baselines.main]
+ /**
+ * Lists all enabled baselines using pagination to retrieve complete results.
+ *
+ * @return a list of all enabled baselines
+ * @throws ControlTowerException if a service-specific error occurs
+ * @throws SdkException if an SDK error occurs
+ */
+ public CompletableFuture> listEnabledBaselinesAsync() {
+ System.out.format("Starting list enabled baselines paginator…");
+
+ ListEnabledBaselinesRequest request =
+ ListEnabledBaselinesRequest.builder().build();
+
+ ListEnabledBaselinesPublisher paginator =
+ getAsyncClient().listEnabledBaselinesPaginator(request);
+
+ List enabledBaselines = new ArrayList<>();
+ return paginator.subscribe(response -> {
+ if (response.enabledBaselines() != null
+ && !response.enabledBaselines().isEmpty()) {
+
+ response.enabledBaselines().forEach(baseline -> {
+ System.out.format("Enabled baseline: {}", baseline.baselineIdentifier());
+ enabledBaselines.add(baseline);
+ });
+ } else {
+ System.out.format("Page contained no enabled baselines.");
+ }
+ })
+ .thenRun(() ->
+ System.out.println(
+ "Successfully listed enabled baselines. Total: {}"+
+ enabledBaselines.size()
+ )
+ )
+ .thenApply(v -> enabledBaselines)
+ .exceptionally(ex -> {
+ Throwable cause = ex.getCause() != null ? ex.getCause() : ex;
+
+ if (cause instanceof ControlTowerException e) {
+ String errorCode = e.awsErrorDetails().errorCode();
+
+ if ("ResourceNotFoundException".equals(errorCode)) {
+ throw new CompletionException(
+ "Target not found when listing enabled baselines: %s"
+ .formatted(e.getMessage()),
+ e
+ );
+ }
+
+ throw new CompletionException(
+ "Error listing enabled baselines: %s"
+ .formatted(e.getMessage()),
+ e
+ );
+ }
+
+ if (cause instanceof SdkException) {
+ throw new CompletionException(
+ "SDK error listing enabled baselines: %s"
+ .formatted(cause.getMessage()),
+ cause
+ );
+ }
+
+ throw new CompletionException(
+ "Failed to list enabled baselines",
+ cause
+ );
+ });
+ }
+ // snippet-end:[controltower.java2.list_enabled_baselines.main]
+
+ // snippet-start:[controltower.java2.enable_baseline.main]
+ /**
+ * Enables a baseline for a specified target.
+ *
+ * @param baselineIdentifier the identifier of the baseline to enable
+ * @param baselineVersion the version of the baseline to enable
+ * @param targetIdentifier the identifier of the target (e.g., OU ARN)
+ * @return the operation identifier
+ * @throws ControlTowerException if a service-specific error occurs
+ * @throws SdkException if an SDK error occurs
+ */
+ public CompletableFuture enableBaselineAsync(
+ String baselineIdentifier,
+ String baselineVersion,
+ String targetIdentifier) {
+
+ EnableBaselineRequest request = EnableBaselineRequest.builder()
+ .baselineIdentifier(baselineIdentifier)
+ .baselineVersion(baselineVersion)
+ .targetIdentifier(targetIdentifier)
+ .build();
+
+ return getAsyncClient().enableBaseline(request)
+ .whenComplete((response, exception) -> {
+ if (exception != null) {
+ Throwable cause = exception.getCause() != null
+ ? exception.getCause()
+ : exception;
+
+ if (cause instanceof ControlTowerException e) {
+ String errorCode = e.awsErrorDetails().errorCode();
+
+ switch (errorCode) {
+ case "ValidationException":
+ if (e.getMessage() != null
+ && e.getMessage().contains("already enabled")) {
+ throw new CompletionException(
+ "Baseline is already enabled for this target",
+ e
+ );
+ }
+ throw new CompletionException(
+ "Validation error enabling baseline: " + e.getMessage(),
+ e
+ );
+
+ case "ConflictException":
+ throw new CompletionException(
+ "Conflict enabling baseline: " + e.getMessage(),
+ e
+ );
+
+ default:
+ throw new CompletionException(
+ "Error enabling baseline: " + e.getMessage(),
+ e
+ );
+ }
+ }
+
+ if (cause instanceof SdkException) {
+ throw new CompletionException(
+ "SDK error enabling baseline: " + cause.getMessage(),
+ cause
+ );
+ }
+
+ throw new CompletionException(
+ "Unexpected error enabling baseline: " + exception.getMessage(),
+ exception
+ );
+ }
+ })
+ .thenApply(response -> {
+ String operationId = response.operationIdentifier();
+ return "Enabled baseline with operation ID: " + operationId;
+ });
+ }
+ // snippet-end:[controltower.java2.enable_baseline.main]
+
+ // snippet-start:[controltower.java2.disable_baseline.main]
+ /**
+ * Disables a baseline for a specified target.
+ *
+ * @param enabledBaselineIdentifier the identifier of the enabled baseline to disable
+ * @return the operation identifier
+ * @throws ControlTowerException if a service-specific error occurs
+ * @throws SdkException if an SDK error occurs
+ */
+ public CompletableFuture disableBaselineAsync(
+ String enabledBaselineIdentifier) {
+
+ DisableBaselineRequest request = DisableBaselineRequest.builder()
+ .enabledBaselineIdentifier(enabledBaselineIdentifier)
+ .build();
+
+ return getAsyncClient().disableBaseline(request)
+ .whenComplete((response, exception) -> {
+ if (exception != null) {
+ Throwable cause = exception.getCause() != null
+ ? exception.getCause()
+ : exception;
+
+ if (cause instanceof ControlTowerException e) {
+ String errorCode = e.awsErrorDetails().errorCode();
+
+ switch (errorCode) {
+ case "ConflictException":
+ throw new CompletionException(
+ "Conflict disabling baseline: %s"
+ .formatted(e.getMessage()),
+ e
+ );
+
+ case "ResourceNotFoundException":
+ throw new CompletionException(
+ "Baseline not found for disabling: %s"
+ .formatted(e.getMessage()),
+ e
+ );
+
+ default:
+ throw new CompletionException(
+ "Error disabling baseline: %s"
+ .formatted(e.getMessage()),
+ e
+ );
+ }
+ }
+
+ if (cause instanceof SdkException) {
+ throw new CompletionException(
+ "SDK error disabling baseline: %s"
+ .formatted(cause.getMessage()),
+ cause
+ );
+ }
+
+ throw new CompletionException(
+ "Failed to disable baseline",
+ cause
+ );
+ }
+ })
+ .thenApply(response -> {
+ String operationId = response.operationIdentifier();
+ return "Disabled baseline with operation ID: " + operationId;
+ });
+ }
+ // snippet-end:[controltower.java2.disable_baseline.main]
+
+ // snippet-start:[controltower.java2.get_baseline_operation.main]
+ /**
+ * Gets the status of a baseline operation.
+ *
+ * @param operationIdentifier the identifier of the operation
+ * @return the operation status
+ * @throws ControlTowerException if a service-specific error occurs
+ * @throws SdkException if an SDK error occurs
+ */
+ public CompletableFuture getBaselineOperationAsync(
+ String operationIdentifier) {
+
+ GetBaselineOperationRequest request = GetBaselineOperationRequest.builder()
+ .operationIdentifier(operationIdentifier)
+ .build();
+
+ return getAsyncClient().getBaselineOperation(request)
+ .whenComplete((response, exception) -> {
+ if (exception != null) {
+ Throwable cause = exception.getCause() != null
+ ? exception.getCause()
+ : exception;
+
+ if (cause instanceof ControlTowerException e) {
+ String errorCode = e.awsErrorDetails().errorCode();
+
+ if ("ResourceNotFoundException".equals(errorCode)) {
+ throw new CompletionException(
+ "Baseline operation not found: %s"
+ .formatted(e.getMessage()),
+ e
+ );
+ }
+
+ throw new CompletionException(
+ "Error getting baseline operation status: %s"
+ .formatted(e.getMessage()),
+ e
+ );
+ }
+
+ if (cause instanceof SdkException) {
+ throw new CompletionException(
+ "SDK error getting baseline operation status: %s"
+ .formatted(cause.getMessage()),
+ cause
+ );
+ }
+
+ throw new CompletionException(
+ "Failed to get baseline operation status",
+ cause
+ );
+ }
+ })
+ .thenApply(response -> {
+ BaselineOperationStatus status =
+ response.baselineOperation().status();
+ return status;
+ });
+ }
+ // snippet-end:[controltower.java2.get_baseline_operation.main]
+
+ // snippet-start:[controltower.java2.list_enabled_controls.main]
+ /**
+ * Lists all enabled controls for a specific target using pagination.
+ *
+ * @param targetIdentifier the identifier of the target (e.g., OU ARN)
+ * @return a list of enabled controls
+ * @throws ControlTowerException if a service-specific error occurs
+ * @throws SdkException if an SDK error occurs
+ */
+ public CompletableFuture> listEnabledControlsAsync(String targetIdentifier) {
+ System.out.format("Starting list enabled controls paginator for target {}…", targetIdentifier);
+ ListEnabledControlsRequest request = ListEnabledControlsRequest.builder()
+ .targetIdentifier(targetIdentifier)
+ .build();
+
+ ListEnabledControlsPublisher paginator = getAsyncClient().listEnabledControlsPaginator(request);
+ List enabledControls = new ArrayList<>();
+
+ // Subscribe to the paginator asynchronously
+ return paginator.subscribe(response -> {
+ if (response.enabledControls() != null && !response.enabledControls().isEmpty()) {
+ response.enabledControls().forEach(control -> {
+ System.out.println("Enabled control: {}"+ control.controlIdentifier());
+ enabledControls.add(control);
+ });
+ } else {
+ System.out.println("Page contained no enabled controls.");
+ }
+ })
+ .thenRun(() -> System.out.format(
+ "Successfully retrieved {} enabled controls for target {}",
+ enabledControls.size(),
+ targetIdentifier
+ ))
+ .thenApply(v -> enabledControls)
+ .exceptionally(ex -> {
+ Throwable cause = ex.getCause() != null ? ex.getCause() : ex;
+
+ if (cause instanceof ControlTowerException e) {
+ String errorCode = e.awsErrorDetails().errorCode();
+
+ switch (errorCode) {
+ case "AccessDeniedException":
+ throw new CompletionException(
+ "Access denied when listing enabled controls: %s".formatted(e.getMessage()), e);
+
+ case "ResourceNotFoundException":
+ if (e.getMessage() != null && e.getMessage().contains("not registered with AWS Control Tower")) {
+ throw new CompletionException(
+ "Control Tower must be enabled to work with controls", e);
+ }
+ throw new CompletionException(
+ "Target not found when listing enabled controls: %s".formatted(e.getMessage()), e);
+
+ default:
+ throw new CompletionException(
+ "Error listing enabled controls: %s".formatted(e.getMessage()), e);
+ }
+ }
+
+ if (cause instanceof SdkException) {
+ throw new CompletionException(
+ "SDK error listing enabled controls: %s".formatted(cause.getMessage()), cause);
+ }
+
+ throw new CompletionException("Failed to list enabled controls", cause);
+ });
+ }
+ // snippet-end:[controltower.java2.list_enabled_controls.main]
+
+ // snippet-start:[controltower.java2.enable_control.main]
+ /**
+ * Enables a control for a specified target.
+ *
+ * @param controlIdentifier the identifier of the control to enable
+ * @param targetIdentifier the identifier of the target (e.g., OU ARN)
+ * @return the operation identifier
+ * @throws ControlTowerException if a service-specific error occurs
+ * @throws SdkException if an SDK error occurs
+ */
+ public CompletableFuture enableControlAsync(
+ String controlIdentifier,
+ String targetIdentifier) {
+
+ EnableControlRequest request = EnableControlRequest.builder()
+ .controlIdentifier(controlIdentifier)
+ .targetIdentifier(targetIdentifier)
+ .build();
+
+ return getAsyncClient().enableControl(request)
+ .whenComplete((response, exception) -> {
+ if (exception != null) {
+ Throwable cause = exception.getCause() != null
+ ? exception.getCause()
+ : exception;
+
+ if (cause instanceof ControlTowerException e) {
+ String errorCode = e.awsErrorDetails().errorCode();
+
+ switch (errorCode) {
+ case "ValidationException":
+ if (e.getMessage() != null
+ && e.getMessage().contains("already enabled")) {
+ // Preserve sync behavior: treat as no-op
+ return;
+ }
+
+ throw new CompletionException(
+ "Validation error enabling control: %s"
+ .formatted(e.getMessage()),
+ e
+ );
+
+ case "ResourceNotFoundException":
+ if (e.getMessage() != null
+ && e.getMessage().contains(
+ "not registered with AWS Control Tower")) {
+ throw new CompletionException(
+ "Control Tower must be enabled to work with controls",
+ e
+ );
+ }
+
+ throw new CompletionException(
+ "Control not found: %s"
+ .formatted(e.getMessage()),
+ e
+ );
+
+ default:
+ throw new CompletionException(
+ "Error enabling control: %s"
+ .formatted(e.getMessage()),
+ e
+ );
+ }
+ }
+
+ if (cause instanceof SdkException) {
+ throw new CompletionException(
+ "SDK error enabling control: %s"
+ .formatted(cause.getMessage()),
+ cause
+ );
+ }
+
+ throw new CompletionException(
+ "Failed to enable control",
+ cause
+ );
+ }
+ })
+ .thenApply(response ->
+ response != null ? response.operationIdentifier() : null
+ );
+ }
+
+ // snippet-end:[controltower.java2.enable_control.main]
+
+ // snippet-start:[controltower.java2.disable_control.main]
+
+ /**
+ * Disables a control for a specified target.
+ *
+ * @param controlIdentifier the identifier of the control to disable
+ * @param targetIdentifier the identifier of the target (e.g., OU ARN)
+ * @return the operation identifier
+ * @throws ControlTowerException if a service-specific error occurs
+ * @throws SdkException if an SDK error occurs
+ */
+ public CompletableFuture disableControlAsync(
+ String controlIdentifier,
+ String targetIdentifier) {
+
+ DisableControlRequest request = DisableControlRequest.builder()
+ .controlIdentifier(controlIdentifier)
+ .targetIdentifier(targetIdentifier)
+ .build();
+
+ return getAsyncClient().disableControl(request)
+ .whenComplete((response, exception) -> {
+ if (exception != null) {
+ Throwable cause = exception.getCause() != null
+ ? exception.getCause()
+ : exception;
+
+ if (cause instanceof ControlTowerException e) {
+ String errorCode = e.awsErrorDetails().errorCode();
+
+ if ("ResourceNotFoundException".equals(errorCode)) {
+ throw new CompletionException(
+ "Control not found for disabling: %s"
+ .formatted(e.getMessage()),
+ e
+ );
+ }
+
+ throw new CompletionException(
+ "Error disabling control: %s"
+ .formatted(e.getMessage()),
+ e
+ );
+ }
+
+ if (cause instanceof SdkException) {
+ throw new CompletionException(
+ "SDK error disabling control: %s"
+ .formatted(cause.getMessage()),
+ cause
+ );
+ }
+
+ throw new CompletionException(
+ "Failed to disable control",
+ cause
+ );
+ }
+ })
+ .thenApply(response -> response.operationIdentifier());
+ }
+ // snippet-end:[controltower.java2.disable_control.main]
+
+ // snippet-start:[controltower.java2.get_control_operation.main]
+
+ /**
+ * Gets the status of a control operation.
+ *
+ * @param operationIdentifier the identifier of the operation
+ * @return the operation status
+ * @throws ControlTowerException if a service-specific error occurs
+ * @throws SdkException if an SDK error occurs
+ */
+ public CompletableFuture getControlOperationAsync(
+ String operationIdentifier) {
+
+ GetControlOperationRequest request = GetControlOperationRequest.builder()
+ .operationIdentifier(operationIdentifier)
+ .build();
+
+ return getAsyncClient().getControlOperation(request)
+ .whenComplete((response, exception) -> {
+ if (exception != null) {
+ Throwable cause = exception.getCause() != null ? exception.getCause() : exception;
+
+ if (cause instanceof ControlTowerException e) {
+ String errorCode = e.awsErrorDetails().errorCode();
+
+ if ("ResourceNotFoundException".equals(errorCode)) {
+ throw new CompletionException(
+ "Control operation not found: %s".formatted(e.getMessage()),
+ e
+ );
+ }
+
+ throw new CompletionException(
+ "Error getting control operation status: %s".formatted(e.getMessage()),
+ e
+ );
+ }
+
+ if (cause instanceof SdkException) {
+ throw new CompletionException(
+ "SDK error getting control operation status: %s".formatted(cause.getMessage()),
+ cause
+ );
+ }
+
+ throw new CompletionException("Failed to get control operation status", cause);
+ }
+ })
+ .thenApply(response -> response.controlOperation().status());
+ }
+ // snippet-end:[controltower.java2.get_control_operation.main]
+
+ // snippet-start:[controltower.java2.list_controls.main]
+
+ /**
+ * Lists all controls in the Control Tower control catalog.
+ *
+ * @return a list of controls
+ * @throws SdkException if a service-specific error occurs
+ */
+ public CompletableFuture> listControlsAsync() {
+ System.out.println("Starting list controls paginator…");
+
+ ListControlsRequest request = ListControlsRequest.builder().build();
+ ListControlsPublisher paginator = getAsyncCatClient().listControlsPaginator(request);
+ List controls = new ArrayList<>();
+
+ return paginator.subscribe(response -> {
+ if (response.controls() != null && !response.controls().isEmpty()) {
+ response.controls().forEach(control -> {
+ controls.add(control);
+ });
+ } else {
+ System.out.println("Page contained no controls.");
+ }
+ })
+ .thenRun(() -> System.out.println("Successfully retrieved {} controls."+ controls.size()))
+ .thenApply(v -> controls)
+ .exceptionally(ex -> {
+ Throwable cause = ex.getCause() != null ? ex.getCause() : ex;
+
+ if (cause instanceof SdkException sdkEx) {
+ if (sdkEx.getMessage() != null && sdkEx.getMessage().contains("AccessDeniedException")) {
+ throw new CompletionException(
+ "Access denied when listing controls. Please ensure you have the necessary permissions.",
+ sdkEx
+ );
+ } else {
+ throw new CompletionException(
+ "SDK error listing controls: %s".formatted(sdkEx.getMessage()),
+ sdkEx
+ );
+ }
+ }
+
+ throw new CompletionException("Failed to list controls", cause);
+ });
+ }
+ // snippet-end:[controltower.java2.list_controls.main]
+
+ // snippet-start:[controltower.java2.reset_enabled_baseline.main]
+
+ /**
+ * Resets an enabled baseline for a specific target.
+ *
+ * @param enabledBaselineIdentifier the identifier of the enabled baseline to reset
+ * @return the operation identifier
+ * @throws ControlTowerException if a service-specific error occurs
+ * @throws SdkException if an SDK error occurs
+ */
+ public CompletableFuture resetEnabledBaselineAsync(
+ String enabledBaselineIdentifier) {
+
+ System.out.println("Starting reset of enabled baseline…");
+ ResetEnabledBaselineRequest request = ResetEnabledBaselineRequest.builder()
+ .enabledBaselineIdentifier(enabledBaselineIdentifier)
+ .build();
+
+ return getAsyncClient().resetEnabledBaseline(request)
+ .thenApply(response -> {
+ String operationId = response.operationIdentifier();
+ System.out.println("Reset enabled baseline with operation ID: {}" + operationId);
+ return operationId;
+ })
+ .exceptionally(ex -> {
+ Throwable cause = ex.getCause() != null ? ex.getCause() : ex;
+
+ if (cause instanceof ControlTowerException e) {
+ String errorCode = e.awsErrorDetails().errorCode();
+ switch (errorCode) {
+ case "ResourceNotFoundException":
+ System.out.println("Target not found: {}"+ e.getMessage());
+ break;
+ default:
+ System.out.println("Couldn't reset enabled baseline. Here's why: {}" + e.getMessage());
+ }
+ throw new CompletionException(e);
+ }
+
+ if (cause instanceof SdkException sdkEx) {
+ System.out.println("SDK error resetting enabled baseline: {}"+ sdkEx.getMessage());
+ throw new CompletionException(sdkEx);
+ }
+
+ throw new CompletionException("Failed to reset enabled baseline", cause);
+ });
+ }
+ // snippet-end:[controltower.java2.reset_enabled_baseline.main]
+}
+// snippet-end:[controltower.java2.controltower_actions.main]
\ No newline at end of file
diff --git a/javav2/example_code/controltower/src/main/java/com/example/controltower/scenario/ControlTowerScenario.java b/javav2/example_code/controltower/src/main/java/com/example/controltower/scenario/ControlTowerScenario.java
new file mode 100644
index 00000000000..ad7aaa041ca
--- /dev/null
+++ b/javav2/example_code/controltower/src/main/java/com/example/controltower/scenario/ControlTowerScenario.java
@@ -0,0 +1,278 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package com.example.controltower.scenario;
+
+import software.amazon.awssdk.services.controlcatalog.ControlCatalogClient;
+import software.amazon.awssdk.services.controlcatalog.model.ControlSummary;
+import software.amazon.awssdk.services.controltower.model.*;
+import software.amazon.awssdk.services.organizations.OrganizationsClient;
+
+import java.util.List;
+import java.util.Scanner;
+
+import static java.lang.System.in;
+import static java.lang.System.out;
+
+
+/**
+ * Before running this Java V2 code example, set up your development
+ * environment, including your credentials.
+ *
+ * For more information, see the following documentation topic:
+ *
+ * https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/get-started.html
+ *
+ * Use the AWS SDK for Java (v2) to create an AWS Control Tower client
+ * and list all available baselines.
+ * This example uses the default settings specified in your shared credentials
+ * and config files.
+ */
+
+// snippet-start:[controltower.java2.controltower_scenario.main]
+public class ControlTowerScenario {
+
+ public static final String DASHES = new String(new char[80]).replace("\0", "-");
+ private static final Scanner scanner = new Scanner(in);
+
+ private static OrganizationsClient orgClient;
+ private static ControlCatalogClient catClient;
+
+ private static String ouId = null;
+ private static String ouArn = null;
+ private static String landingZoneArn = null;
+ private static boolean useLandingZone = false;
+
+ private String stack = null;
+ private String accountId = null;
+
+ public static void main(String[] args) {
+
+ out.println(DASHES);
+ out.println("Welcome to the AWS Control Tower basics scenario!");
+ out.println(DASHES);
+
+ try {
+ runScenarioAsync();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ // -----------------------------
+ // Utilities
+ // -----------------------------
+ private static boolean askYesNo(String msg) {
+ out.print(msg);
+ return scanner.nextLine().trim().toLowerCase().startsWith("y");
+ }
+
+ private static void runScenarioAsync() {
+ ControlTowerActions actions = new ControlTowerActions();
+
+ // -----------------------------
+ // Step 1: Landing Zones
+ // -----------------------------
+ out.println(DASHES);
+ out.println("""
+ Some demo operations require the use of a landing zone.
+ You can use an existing landing zone or opt out of these operations in the demo.
+ For instructions on how to set up a landing zone,
+ see https://docs.aws.amazon.com/controltower/latest/userguide/getting-started-from-console.html
+ """);
+
+ out.println("Step 1: Listing landing zones...");
+ waitForInputToContinue(scanner);
+
+ List landingZones =
+ actions.listLandingZonesAsync().join();
+
+ if (landingZones.isEmpty()) {
+ out.println("No landing zones found. Landing-zone-dependent steps will be skipped.");
+ useLandingZone = false;
+ waitForInputToContinue(scanner);
+ } else {
+ out.println("\nAvailable Landing Zones:");
+ for (int i = 0; i < landingZones.size(); i++) {
+ out.printf("%d) %s%n", i + 1, landingZones.get(i).arn());
+ }
+
+ if (askYesNo("Do you want to use the first landing zone in the list (" +
+ landingZones.get(0).arn() + ")? (y/n): ")) {
+ useLandingZone = true;
+ landingZoneArn = landingZones.get(0).arn();
+ } else if (askYesNo("Do you want to use a different existing Landing Zone for this demo? (y/n): ")) {
+ useLandingZone = true;
+ out.print("Enter landing zone ARN: ");
+ landingZoneArn = scanner.nextLine().trim();
+ } else {
+ out.println("Proceeding without a landing zone.");
+ useLandingZone = false;
+ waitForInputToContinue(scanner);
+ }
+ }
+
+ // -----------------------------
+ // Setup Organization + Sandbox OU
+ // -----------------------------
+ if (useLandingZone) {
+ out.println("Using landing zone ARN: " + landingZoneArn);
+
+ ControlTowerActions.OrgSetupResult result =
+ actions.setupOrganizationAsync().join();
+
+ ouArn = result.sandboxOuArn();
+ ouId = result.sandboxOuArn();
+
+ out.println("Organization ID: " + result.orgId());
+ out.println("Using Sandbox OU ARN: " + ouArn);
+ }
+
+ // -----------------------------
+ // Step 2: Baselines
+ // -----------------------------
+ out.println(DASHES);
+ out.println("Step 2: Listing available baselines...");
+ waitForInputToContinue(scanner);
+
+ List baselines =
+ actions.listBaselinesAsync().join();
+
+ BaselineSummary controlTowerBaseline = null;
+ for (BaselineSummary b : baselines) {
+ out.println("Baseline: " + b.name());
+ out.println(" ARN: " + b.arn());
+ if ("AWSControlTowerBaseline".equals(b.name())) {
+ controlTowerBaseline = b;
+ }
+ }
+
+ waitForInputToContinue(scanner);
+
+ if (useLandingZone && controlTowerBaseline != null) {
+
+ out.println("\nListing enabled baselines:");
+ List enabledBaselines =
+ actions.listEnabledBaselinesAsync().join();
+
+ EnabledBaselineSummary identityCenterBaseline = null;
+ for (EnabledBaselineSummary eb : enabledBaselines) {
+ out.println(eb.baselineIdentifier());
+ if (eb.baselineIdentifier().contains("baseline/LN25R72TTG6IGPTQ")) {
+ identityCenterBaseline = eb;
+ }
+ }
+
+ if (askYesNo("Do you want to enable the Control Tower Baseline? (y/n): ")) {
+ out.println("\nEnabling Control Tower Baseline...");
+
+ String enabledBaselineId =
+ actions.enableBaselineAsync(
+ ouArn,
+ controlTowerBaseline.arn(),
+ "4.0"
+ ).join();
+
+ out.println("Enabled baseline operation ID: " + enabledBaselineId);
+
+ if (askYesNo("Do you want to reset the Control Tower Baseline? (y/n): ")) {
+ String operationId =
+ actions.resetEnabledBaselineAsync(enabledBaselineId).join();
+ out.println("Reset baseline operation ID: " + operationId);
+ }
+
+ if (askYesNo("Do you want to disable the Control Tower Baseline? (y/n): ")) {
+ String operationId =
+ actions.disableBaselineAsync(enabledBaselineId).join();
+ out.println("Disabled baseline operation ID: " + operationId);
+
+ // Re-enable for next steps
+ actions.enableBaselineAsync(
+ ouArn,
+ controlTowerBaseline.arn(),
+ "4.0"
+ ).join();
+ }
+ }
+ }
+
+ // -----------------------------
+ // Step 3: Controls
+ // -----------------------------
+ out.println(DASHES);
+ out.println("Step 3: Managing Controls:");
+ waitForInputToContinue(scanner);
+
+ List controls =
+ actions.listControlsAsync().join();
+
+ out.println("\nListing first 5 available Controls:");
+ for (int i = 0; i < Math.min(5, controls.size()); i++) {
+ ControlSummary c = controls.get(i);
+ System.out.format("%d. %s - %s".formatted(i + 1, c.name(), c.arn()));
+ }
+
+ if (useLandingZone) {
+ waitForInputToContinue(scanner);
+
+ List enabledControls =
+ actions.listEnabledControlsAsync(ouArn).join();
+
+ out.println("\nListing enabled controls:");
+ for (int i = 0; i < enabledControls.size(); i++) {
+ out.println("%d. %s".formatted(i + 1, enabledControls.get(i).controlIdentifier()));
+ }
+
+ String controlArnToEnable = null;
+ for (ControlSummary control : controls) {
+ boolean enabled = enabledControls.stream()
+ .anyMatch(ec -> ec.controlIdentifier().equals(control.arn()));
+ if (!enabled) {
+ controlArnToEnable = control.arn();
+ break;
+ }
+ }
+
+ waitForInputToContinue(scanner);
+
+ if (controlArnToEnable != null &&
+ askYesNo("Do you want to enable the control " + controlArnToEnable + "? (y/n): ")) {
+
+ String operationId =
+ actions.enableControlAsync(controlArnToEnable, ouArn).join();
+
+ out.println("Enabled control with operation ID: " + operationId);
+ }
+
+ waitForInputToContinue(scanner);
+
+ if (controlArnToEnable != null &&
+ askYesNo("Do you want to disable the control? (y/n): ")) {
+
+ String operationId =
+ actions.disableControlAsync(controlArnToEnable, ouArn).join();
+
+ out.println("Disable operation ID: " + operationId);
+ }
+ }
+
+ out.println("\nThis concludes the example scenario.");
+ out.println("Thanks for watching!");
+ out.println(DASHES);
+ }
+
+
+
+
+ private static void waitForInputToContinue(Scanner sc) {
+ out.println("\nEnter 'c' then to continue:");
+ while (true) {
+ String input = sc.nextLine();
+ if ("c".equalsIgnoreCase(input.trim())) {
+ out.println("Continuing...");
+ break;
+ }
+ }
+ }
+}
+// snippet-end:[controltower.java2.controltower_scenario.main]
\ No newline at end of file
diff --git a/javav2/example_code/controltower/src/test/java/ControlTowerTest.java b/javav2/example_code/controltower/src/test/java/ControlTowerTest.java
new file mode 100644
index 00000000000..1e8aa3cadd4
--- /dev/null
+++ b/javav2/example_code/controltower/src/test/java/ControlTowerTest.java
@@ -0,0 +1,111 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import com.example.controltower.HelloControlTower;
+import com.example.controltower.scenario.ControlTowerActions;
+import com.example.controltower.scenario.ControlTowerScenario;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestMethodOrder;
+import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.controlcatalog.model.ControlSummary;
+import software.amazon.awssdk.services.controltower.ControlTowerClient;
+import software.amazon.awssdk.services.controltower.model.BaselineSummary;
+import software.amazon.awssdk.services.controltower.model.LandingZoneSummary;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@TestInstance(TestInstance.Lifecycle.PER_METHOD)
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class ControlTowerTest {
+ private static ControlTowerClient controlTowerClient;
+
+ @BeforeAll
+ public static void setUp() {
+ controlTowerClient = ControlTowerClient.builder()
+ .region(Region.US_EAST_1)
+ .credentialsProvider(ProfileCredentialsProvider.create("default"))
+ .build();
+ }
+
+ @Test
+ @Order(1)
+ public void testHelloService() {
+ assertDoesNotThrow(() -> {
+ HelloControlTower.helloControlTower(controlTowerClient);
+ });
+ System.out.println("Test 1 passed");
+ }
+
+ @Test
+ @Order(2)
+ public void testControlTowerActionsAsync() {
+ assertDoesNotThrow(() -> {
+ // Create an instance of the async actions class
+ ControlTowerActions actions = new ControlTowerActions();
+
+ // SAFE: read-only, no admin role required
+ List landingZones = actions.listLandingZonesAsync().join();
+ List baselines = actions.listBaselinesAsync().join();
+ List controls = actions.listControlsAsync().join();
+
+ // Simple sanity checks
+ assertNotNull(landingZones, "Landing zones list should not be null");
+ assertNotNull(baselines, "Baselines list should not be null");
+ assertNotNull(controls, "Controls list should not be null");
+
+ System.out.println("Landing Zones: " + landingZones.size());
+ System.out.println("Baselines: " + baselines.size());
+ System.out.println("Controls: " + controls.size());
+ });
+
+ System.out.println("Test 2 passed");
+ }
+
+
+ @Test
+ @Tag("IntegrationTest")
+ @Order(3)
+ public void testControlTowerScenarioEndToEnd() {
+ assertDoesNotThrow(() -> {
+ String simulatedInput = String.join("\n",
+ Arrays.asList(
+ "c", "y", "c", "c", "y", "n", "n", "c", "y", "n", "c"
+ )) + "\n";
+
+ InputStream originalIn = System.in;
+ PrintStream originalOut = System.out;
+
+ try {
+ // Simulate user input
+ ByteArrayInputStream testIn = new ByteArrayInputStream(simulatedInput.getBytes());
+ System.setIn(testIn);
+
+ // Capture output
+ System.setOut(new PrintStream(new ByteArrayOutputStream()));
+
+ // Run the scenario
+ ControlTowerScenario.main(new String[]{});
+
+ } finally {
+ // Restore original I/O
+ System.setIn(originalIn);
+ System.setOut(originalOut);
+ }
+ });
+
+ System.out.println("Test 3 (Control Tower scenario end-to-end) passed");
+ }
+}
\ No newline at end of file