diff --git a/java/google/registry/env/common/backend/WEB-INF/web.xml b/java/google/registry/env/common/backend/WEB-INF/web.xml index 136a1d629..a2cf88fa8 100644 --- a/java/google/registry/env/common/backend/WEB-INF/web.xml +++ b/java/google/registry/env/common/backend/WEB-INF/web.xml @@ -265,6 +265,12 @@ /_dr/task/dnsRefreshForHostRename + + + backend-servlet + /_dr/task/expandRecurringBillingEvents + + diff --git a/java/google/registry/model/common/Cursor.java b/java/google/registry/model/common/Cursor.java index a57ec8919..92a104374 100644 --- a/java/google/registry/model/common/Cursor.java +++ b/java/google/registry/model/common/Cursor.java @@ -20,8 +20,6 @@ import static google.registry.model.common.EntityGroupRoot.getCrossTldKey; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.util.DateTimeUtils.START_OF_TIME; -import com.google.common.annotations.VisibleForTesting; - import com.googlecode.objectify.Key; import com.googlecode.objectify.annotation.Entity; import com.googlecode.objectify.annotation.Id; @@ -69,7 +67,12 @@ public class Cursor extends ImmutableObject { */ RDE_UPLOAD_SFTP(Registry.class), - /** Cursor for ensuring rolling transactional isolation of recurring billing expansion. */ + /** + * Cursor for ensuring rolling transactional isolation of recurring billing expansion. The + * value of this cursor represents the exclusive upper bound on the range of billing times + * for which Recurring billing events have been expanded (i.e. the inclusive first billing time + * for the next expansion job). + */ RECURRING_BILLING(EntityGroupRoot.class); /** See the definition of scope on {@link #getScopeClass}. */ @@ -120,16 +123,14 @@ public class Cursor extends ImmutableObject { } /** Creates a unique key for a given scope and cursor type. */ - @VisibleForTesting - static Key createKey(CursorType cursorType, ImmutableObject scope) { + public static Key createKey(CursorType cursorType, ImmutableObject scope) { Key scopeKey = Key.create(scope); checkValidCursorTypeForScope(cursorType, scopeKey); return Key.create(getCrossTldKey(), Cursor.class, generateId(cursorType, scopeKey)); } /** Creates a unique key for a given global cursor type. */ - @VisibleForTesting - static Key createGlobalKey(CursorType cursorType) { + public static Key createGlobalKey(CursorType cursorType) { checkArgument( cursorType.getScopeClass().equals(EntityGroupRoot.class), "Cursor type is not a global cursor."); diff --git a/java/google/registry/module/backend/BUILD b/java/google/registry/module/backend/BUILD index 24b5ce0d3..66390f65a 100644 --- a/java/google/registry/module/backend/BUILD +++ b/java/google/registry/module/backend/BUILD @@ -14,11 +14,11 @@ java_library( "//java/com/google/common/net", "//java/google/registry/backup", "//java/google/registry/bigquery", + "//java/google/registry/billing", "//java/google/registry/config", "//java/google/registry/cron", "//java/google/registry/dns", "//java/google/registry/dns/writer/api", - "//java/google/registry/dns/writer/dnsupdate", "//java/google/registry/export", "//java/google/registry/export/sheet", "//java/google/registry/flows", @@ -35,6 +35,7 @@ java_library( "//java/google/registry/util", "//third_party/java/bouncycastle", "//third_party/java/dagger", + "//third_party/java/joda_time", "//third_party/java/jsr305_annotations", "//third_party/java/jsr330_inject", "//third_party/java/servlet/servlet_api", diff --git a/java/google/registry/module/backend/BackendModule.java b/java/google/registry/module/backend/BackendModule.java index cfa970d58..162eb0463 100644 --- a/java/google/registry/module/backend/BackendModule.java +++ b/java/google/registry/module/backend/BackendModule.java @@ -15,14 +15,20 @@ package google.registry.module.backend; import static google.registry.model.registry.Registries.assertTldExists; +import static google.registry.request.RequestParameters.extractOptionalDatetimeParameter; import static google.registry.request.RequestParameters.extractRequiredParameter; +import com.google.common.base.Optional; + import dagger.Module; import dagger.Provides; +import google.registry.billing.ExpandRecurringBillingEventsAction; import google.registry.request.Parameter; import google.registry.request.RequestParameters; +import org.joda.time.DateTime; + import javax.servlet.http.HttpServletRequest; /** @@ -36,4 +42,11 @@ public class BackendModule { static String provideTld(HttpServletRequest req) { return assertTldExists(extractRequiredParameter(req, RequestParameters.PARAM_TLD)); } + + @Provides + @Parameter("cursorTime") + static Optional provideCursorTime(HttpServletRequest req) { + return extractOptionalDatetimeParameter( + req, ExpandRecurringBillingEventsAction.PARAM_CURSOR_TIME); + } } diff --git a/java/google/registry/request/RequestParameters.java b/java/google/registry/request/RequestParameters.java index c9320e3ca..1402c9e56 100644 --- a/java/google/registry/request/RequestParameters.java +++ b/java/google/registry/request/RequestParameters.java @@ -160,6 +160,25 @@ public final class RequestParameters { } } + /** + * Returns first request parameter associated with {@code name} parsed as an + * ISO 8601 timestamp, e.g. {@code 1984-12-18TZ}, + * {@code 2000-01-01T16:20:00Z}. + * + * @throws BadRequestException if request parameter is present but not a valid {@link DateTime}. + */ + public static Optional extractOptionalDatetimeParameter( + HttpServletRequest req, String name) { + String stringParam = req.getParameter(name); + try { + return isNullOrEmpty(stringParam) + ? Optional.absent() + : Optional.of(DateTime.parse(stringParam)); + } catch (IllegalArgumentException e) { + throw new BadRequestException("Bad ISO 8601 timestamp: " + name); + } + } + /** * Returns first request parameter associated with {@code name} parsed as an optional * {@link InetAddress} (which might be IPv6). diff --git a/javatests/google/registry/request/RequestParametersTest.java b/javatests/google/registry/request/RequestParametersTest.java index 65862ea1c..84a5d278e 100644 --- a/javatests/google/registry/request/RequestParametersTest.java +++ b/javatests/google/registry/request/RequestParametersTest.java @@ -17,6 +17,7 @@ package google.registry.request; import static com.google.common.truth.Truth.assertThat; import static google.registry.request.RequestParameters.extractBooleanParameter; import static google.registry.request.RequestParameters.extractEnumParameter; +import static google.registry.request.RequestParameters.extractOptionalDatetimeParameter; import static google.registry.request.RequestParameters.extractOptionalParameter; import static google.registry.request.RequestParameters.extractRequiredDatetimeParameter; import static google.registry.request.RequestParameters.extractRequiredMaybeEmptyParameter; @@ -184,6 +185,26 @@ public class RequestParametersTest { extractRequiredDatetimeParameter(req, "timeParam"); } + @Test + public void testExtractOptionalDatetimeParameter_correctValue_works() throws Exception { + when(req.getParameter("timeParam")).thenReturn("2015-08-27T13:25:34.123Z"); + assertThat(extractOptionalDatetimeParameter(req, "timeParam")) + .hasValue(DateTime.parse("2015-08-27T13:25:34.123Z")); + } + + @Test + public void testExtractOptionalDatetimeParameter_badValue_throwsBadRequest() throws Exception { + when(req.getParameter("timeParam")).thenReturn("Tuesday at three o'clock"); + thrown.expect(BadRequestException.class, "timeParam"); + extractOptionalDatetimeParameter(req, "timeParam"); + } + + @Test + public void testExtractOptionalDatetimeParameter_empty_returnsAbsent() throws Exception { + when(req.getParameter("timeParam")).thenReturn(""); + assertThat(extractOptionalDatetimeParameter(req, "timeParam")).isAbsent(); + } + @Test public void testExtractRequiredDatetimeParameter_noValue_throwsBadRequest() throws Exception { thrown.expect(BadRequestException.class, "timeParam");