diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/BatchResultImpl.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/BatchResultImpl.java new file mode 100644 index 000000000000..ea5015d05eb8 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/BatchResultImpl.java @@ -0,0 +1,44 @@ +package com.google.gcloud.dns; + +import com.google.gcloud.dns.batch.BatchResult; + +/** + * This class holds a single result of a batch call to the Cloud DNS. + */ +public class BatchResultImpl implements BatchResult { + + private T result; + private boolean submitted = false; + private DnsException error; + + BatchResultImpl() { + } + + @Override + public boolean submitted() { + return submitted; + } + + @Override + public T get() throws DnsException { + if(!submitted()) { + throw new IllegalStateException("Batch has not been submitted yet"); + } + if(error != null) { + throw error; + } + return result; + } + + void result(T result) { + this.result = result; + } + + void error(DnsException error) { + this.error = error; + } + + void submit() { + this.submitted = true; + } +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/Dns.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/Dns.java index 6ce6b4c19994..7b2912b0d42d 100644 --- a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/Dns.java +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/Dns.java @@ -533,4 +533,12 @@ ChangeRequest getChangeRequest(String zoneName, String changeRequestId, * @see Cloud DNS Chages: list */ Page listChangeRequests(String zoneName, ChangeRequestListOption... options); + + /** + * Initiates a new empty batch ready to be populated with service calls, which will use this + * {@code Dns} instance when submitted for processing to Google Cloud DNS. + */ + DnsBatch batch(); + + void submitBatch(DnsBatch batch); } diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsBatch.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsBatch.java new file mode 100644 index 000000000000..c6d7ecbd5d82 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsBatch.java @@ -0,0 +1,205 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import com.google.gcloud.Page; +import com.google.gcloud.dns.batch.BatchResult; + +import java.util.LinkedList; +import java.util.List; + +/** + * A batch of operations to be submitted to Google Cloud DNS using a single HTTP request. + */ +public class DnsBatch { + + private List requests = new LinkedList<>(); + private Dns dns; + + /** + * An operation to be submitted to Google Cloud DNS within this batch. Only an subset of the class + * attributes appropriate for the represented operation is initialized. Refer to the class method + * and attribute documentation for the specific fields. + */ + public static class Request { + + private final String zoneName; + private final String changeId; + private final ChangeRequest changeRequest; + private final ZoneInfo zoneInfo; + private final Operation operation; + private final AbstractOption[] options; + private final BatchResultImpl result; + + private Request(RequestBuilder builder) { + this.zoneName = builder.zoneName; + this.changeId = builder.changeId; + this.changeRequest = builder.changeRequest; + this.zoneInfo = builder.zoneInfo; + this.operation = builder.operation; + this.options = builder.options; + this.result = builder.result; + } + + private static RequestBuilder builder(Operation operation, BatchResultImpl result, AbstractOption... options) { + return new RequestBuilder(operation, result, options); + } + + /** + * Returns the name of the zone to which the operation is applied. This field is initialized for + * zone create, get and delete operation, and listing DNS records and changes within a zone. + * Returns {@code null} in other cases. + */ + public String zoneName() { + return zoneName; + } + + /** + * Returns the id of the change request which is being retrieved. Getting a change request is + * the only operation when this attribute is initialized. The method returns {@code null} in the + * remaining cases. + */ + public String changeId() { + return changeId; + } + + /** + * Returns the change request which is being created. Creating a change request is the only + * operation when this attribute is initialized. The method returns {@code null} in the + * remaining cases. + */ + public ChangeRequest changeRequest() { + return changeRequest; + } + + /** + * Returns the zone which is being created. Creating a zone is the only operation when this + * attribute is initialized. The method returns {@code null} in the remaining cases. + */ + public ZoneInfo zoneInfo() { + return zoneInfo; + } + + /** + * Returns the type of the operation represented by this {@link DnsBatch.Request}. This field is + * always initialized. + */ + public Operation operation() { + return operation; + } + + /** + * Returns options provided to the operation. Returns an empty array if no options were + * provided. + */ + public AbstractOption[] options() { + return options == null ? new AbstractOption[0] : options; + } + + BatchResultImpl result() { + return result; + } + } + + static class RequestBuilder { + private final AbstractOption[] options; + private String zoneName; + private String changeId; + private ChangeRequest changeRequest; + private ZoneInfo zoneInfo; + private final Operation operation; + private final BatchResultImpl result; + + RequestBuilder(Operation operation, BatchResultImpl result, AbstractOption... options) { + this.operation = operation; + this.options = options; + this.result = result; + } + + RequestBuilder zoneName(String zoneName) { + this.zoneName = zoneName; + return this; + } + + RequestBuilder changeId(String changeId) { + this.changeId = changeId; + return this; + } + + RequestBuilder changeRequest(ChangeRequest changeRequest) { + this.changeRequest = changeRequest; + return this; + } + + RequestBuilder zoneInfo(ZoneInfo zoneInfo) { + this.zoneInfo = zoneInfo; + return this; + } + + Request build() { + return new Request(this); + } + } + + /** + * Represents the type of the batch operation. + */ + public enum Operation { + CREATE_ZONE, + DELETE_ZONE, + GET_ZONE, + LIST_ZONES, + APPLY_CHANGE_REQUEST, + GET_CHANGE_REQUEST, + LIST_CHANGES_REQUESTS, + LIST_DNS_RECORDS + } + + DnsBatch(Dns dns) { + this.dns = dns; + } + + public Dns service() { + return dns; + } + + List requests() { + return requests; + } + + /** + * Adds a {@code DnsBatch.Request} representing the list zones operation to this batch. The + * request will not have initialized any fields except for the operation type and options (if + * provided). The {@code options} can be used to restrict the fields returned or provide page size + * limits in the same way as for {@link Dns#listZones(Dns.ZoneListOption...)}. + */ + public BatchResult, DnsException> listZones(Dns.ZoneListOption... options) { + BatchResultImpl> result = new BatchResultImpl<>(); + Request request = Request.builder(Operation.LIST_ZONES, result, options).build(); + requests.add(request); + return result; + } + + // todo(mderka) add the rest of the operations + + /** + * Submits this batch for processing using a single HTTP request. + */ + public void submit() { + dns.submitBatch(this); + } +} \ No newline at end of file diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsException.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsException.java index 1ecb98a3fdc6..bdb7f14feeca 100644 --- a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsException.java +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsException.java @@ -16,6 +16,7 @@ package com.google.gcloud.dns; +import com.google.api.client.googleapis.json.GoogleJsonError; import com.google.common.collect.ImmutableSet; import com.google.gcloud.BaseServiceException; import com.google.gcloud.RetryHelper.RetryHelperException; @@ -39,6 +40,10 @@ public class DnsException extends BaseServiceException { new Error(null, "rateLimitExceeded")); private static final long serialVersionUID = 490302380416260252L; + public DnsException(GoogleJsonError error) { + super(error, true); + } + public DnsException(IOException exception) { super(exception, true); } diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsImpl.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsImpl.java index a60cfd9151da..4a798c6805c7 100644 --- a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsImpl.java +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsImpl.java @@ -21,8 +21,13 @@ import static com.google.gcloud.RetryHelper.runWithRetries; import static com.google.gcloud.dns.ChangeRequest.fromPb; +import com.google.api.client.googleapis.batch.json.JsonBatchCallback; +import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.util.Lists; import com.google.api.services.dns.model.Change; import com.google.api.services.dns.model.ManagedZone; +import com.google.api.services.dns.model.ManagedZonesListResponse; import com.google.api.services.dns.model.ResourceRecordSet; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; @@ -34,7 +39,10 @@ import com.google.gcloud.PageImpl; import com.google.gcloud.RetryHelper; import com.google.gcloud.dns.spi.DnsRpc; +import com.google.gcloud.dns.spi.RpcBatchCall; +import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.concurrent.Callable; @@ -111,6 +119,16 @@ public Page nextPage() { dnsRpc = options.rpc(); } + private static Function pbToZoneFunction(final DnsOptions options) { + return new Function() { + @Override + public Zone apply( + com.google.api.services.dns.model.ManagedZone zonePb) { + return Zone.fromPb(options.service(), zonePb); + } + }; + } + @Override public Page listZones(ZoneListOption... options) { return listZones(options(), optionMap(options)); @@ -118,15 +136,6 @@ public Page listZones(ZoneListOption... options) { private static Page listZones(final DnsOptions serviceOptions, final Map optionsMap) { - // define transformation function - // this differs from the other list operations since zone is functional and requires dns service - Function pbToZoneFunction = new Function() { - @Override - public Zone apply( - com.google.api.services.dns.model.ManagedZone zonePb) { - return Zone.fromPb(serviceOptions.service(), zonePb); - } - }; try { // get a list of managed zones final DnsRpc rpc = serviceOptions.rpc(); @@ -139,8 +148,8 @@ public DnsRpc.ListResult call() { }, serviceOptions.retryParams(), EXCEPTION_HANDLER); String cursor = result.pageToken(); // transform that list into zone objects - Iterable zones = result.results() == null - ? ImmutableList.of() : Iterables.transform(result.results(), pbToZoneFunction); + Iterable zones = result.results() == null ? ImmutableList.of() + : Iterables.transform(result.results(), pbToZoneFunction(serviceOptions)); return new PageImpl<>(new ZonePageFetcher(serviceOptions, cursor, optionsMap), cursor, zones); } catch (RetryHelperException e) { @@ -309,6 +318,52 @@ public com.google.api.services.dns.model.Change call() { } } + @Override + public DnsBatch batch() { + return new DnsBatch(this); + } + + @Override + public void submitBatch(DnsBatch batch) { + List calls = Lists.newArrayListWithCapacity(batch.requests().size()); + for (final DnsBatch.Request request : batch.requests()) { + final Map optionMap = optionMap(request.options()); + JsonBatchCallback callback = prepareCallback(this.options(), request, optionMap); + RpcBatchCall call = new RpcBatchCall(request, callback, optionMap); + calls.add(call); + } + dnsRpc.submitBatch(calls); + } + + // todo(mderka) make methods to prepare other callbacks + private JsonBatchCallback prepareCallback(final DnsOptions serviceOptions, + final DnsBatch.Request request, + final Map optionMap) { + JsonBatchCallback callback = new JsonBatchCallback() { + @Override + public void onSuccess(ManagedZonesListResponse response, HttpHeaders httpHeaders) + throws IOException { + BatchResultImpl result = request.result(); + List zones = response.getManagedZones(); + Page zonePage = new PageImpl<>( + new ZonePageFetcher(options(), response.getNextPageToken(), optionMap), + response.getNextPageToken(), zones == null ? ImmutableList.of() + : Iterables.transform(zones, pbToZoneFunction(serviceOptions))); + result.result(zonePage); + result.submit(); + } + + @Override + public void onFailure(GoogleJsonError googleJsonError, HttpHeaders httpHeaders) + throws IOException { + BatchResultImpl result = request.result(); + result.error(new DnsException(googleJsonError)); + result.submit(); + } + }; + return callback; + } + private Map optionMap(AbstractOption... options) { Map temp = Maps.newEnumMap(DnsRpc.Option.class); for (AbstractOption option : options) { diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/batch/BatchResult.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/batch/BatchResult.java new file mode 100644 index 000000000000..b7efb98f1852 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/batch/BatchResult.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns.batch; + +/** + * The class that holds a single result of a batch call. + */ +public interface BatchResult { + + /** + * Returns {@code true} if the batch has been submitted and the result is available, and {@code + * false} otherwise + */ + boolean submitted(); + + /** + * Returns result of this call. + * + * @throws IllegalArgumentException if the batch has not been submitted yet + * @throws E if an error occured when processing this request + */ + T get() throws E; +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DefaultDnsRpc.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DefaultDnsRpc.java index f8b8adb87ada..e6dd534e14e9 100644 --- a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DefaultDnsRpc.java +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DefaultDnsRpc.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.gcloud.dns.spi; import static com.google.gcloud.dns.spi.DnsRpc.ListResult.of; @@ -10,6 +26,7 @@ import static com.google.gcloud.dns.spi.DnsRpc.Option.SORTING_ORDER; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import com.google.api.client.googleapis.batch.BatchRequest; import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpTransport; import com.google.api.client.json.jackson.JacksonFactory; @@ -21,10 +38,13 @@ import com.google.api.services.dns.model.Project; import com.google.api.services.dns.model.ResourceRecordSet; import com.google.api.services.dns.model.ResourceRecordSetsListResponse; +import com.google.common.annotations.VisibleForTesting; +import com.google.gcloud.dns.DnsBatch; import com.google.gcloud.dns.DnsException; import com.google.gcloud.dns.DnsOptions; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -193,4 +213,43 @@ public ListResult listChangeRequests(String zoneName, Map opt throw translate(ex); } } + + @Override + public void submitBatch(List calls) { + try { + BatchRequest batchRequest = prepareBatch(calls); + batchRequest.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Since {@code BatchRequest} is a final class, it cannot be mocked with easy mock and the call of + * {@code execute()} cannot be tested. Thus, most of the functionality of {@link + * #submitBatch(List)} is extracted to this method which does not make the call so it does not + * communicate with the service. + */ + @VisibleForTesting + BatchRequest prepareBatch(List calls) throws IOException { + BatchRequest batch = dns.batch(); + for (RpcBatchCall call : calls) { + DnsBatch.Request request = call.request(); + Map options = call.options(); + switch (request.operation()) { + case LIST_ZONES: + // todo(mderka) extract this to a separate method call by this and listZones() + Dns.ManagedZones.List list = dns.managedZones().list(this.options.projectId()) + .setFields(FIELDS.getString(options)) + .setMaxResults(PAGE_SIZE.getInt(options)) + .setDnsName(DNS_NAME.getString(options)) + .setPageToken(PAGE_TOKEN.getString(options)); + list.queue(batch, call.callback()); + break; + default: + // todo(mderka) implement other operations + } + } + return batch; + } } diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DnsRpc.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DnsRpc.java index bde93b99bfdd..0c23735b26f5 100644 --- a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DnsRpc.java +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DnsRpc.java @@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableList; import com.google.gcloud.dns.DnsException; +import java.util.List; import java.util.Map; public interface DnsRpc { @@ -171,4 +172,9 @@ Change getChangeRequest(String zoneName, String changeRequestId, Map */ ListResult listChangeRequests(String zoneName, Map options) throws DnsException; + + /** + * Submits a batch of requests for processing using a single HTTP request to Cloud DNS. + */ + void submitBatch(List requests); } diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/RpcBatchCall.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/RpcBatchCall.java new file mode 100644 index 000000000000..7a88c8487477 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/RpcBatchCall.java @@ -0,0 +1,35 @@ +package com.google.gcloud.dns.spi; + +import com.google.api.client.googleapis.batch.json.JsonBatchCallback; +import com.google.gcloud.dns.DnsBatch; + +import java.util.Map; + +/** + * A container for the Cloud DNS calls to be made within a batch. + */ +public class RpcBatchCall { + + private DnsBatch.Request request; + private JsonBatchCallback callback; + private Map options; + + public RpcBatchCall(DnsBatch.Request request, JsonBatchCallback callback, + Map options) { + this.request = request; + this.callback = callback; + this.options = options; + } + + DnsBatch.Request request() { + return request; + } + + JsonBatchCallback callback() { + return callback; + } + + Map options() { + return options; + } +} diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/it/ITDnsTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/it/ITDnsTest.java index bfea46dfe039..06c217b2c9bb 100644 --- a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/it/ITDnsTest.java +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/it/ITDnsTest.java @@ -25,8 +25,10 @@ import com.google.common.collect.ImmutableList; import com.google.gcloud.Page; +import com.google.gcloud.dns.batch.BatchResult; import com.google.gcloud.dns.ChangeRequest; import com.google.gcloud.dns.Dns; +import com.google.gcloud.dns.DnsBatch; import com.google.gcloud.dns.DnsException; import com.google.gcloud.dns.DnsOptions; import com.google.gcloud.dns.DnsRecord; @@ -952,4 +954,24 @@ public void testListDnsRecords() { clear(); } } + + @Test + public void testListZoneBatch() { + DNS.create(ZONE1); + DNS.create(ZONE_EMPTY_DESCRIPTION); + try { + DnsBatch batch = DNS.batch(); + BatchResult, DnsException> batchResult = batch.listZones(); + batch.submit(); + assertTrue(batchResult.submitted()); + Iterator iteratorBatch = batchResult.get().iterateAll(); + Iterator iteratorList = DNS.listZones().iterateAll(); + while(iteratorBatch.hasNext()) { + assertEquals(iteratorList.next(), iteratorBatch.next()); + } + } finally { + DNS.delete(ZONE1.name()); + DNS.delete(ZONE_EMPTY_DESCRIPTION.name()); + } + } }