diff --git a/core/src/main/java/google/registry/schema/cursor/Cursor.java b/core/src/main/java/google/registry/schema/cursor/Cursor.java new file mode 100644 index 000000000..ad9f5e2c2 --- /dev/null +++ b/core/src/main/java/google/registry/schema/cursor/Cursor.java @@ -0,0 +1,116 @@ +// Copyright 2019 The Nomulus Authors. 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 google.registry.schema.cursor; + +import static com.google.appengine.api.search.checkers.Preconditions.checkNotNull; + +import google.registry.model.ImmutableObject; +import google.registry.model.UpdateAutoTimestamp; +import google.registry.model.common.Cursor.CursorType; +import google.registry.schema.cursor.Cursor.CursorId; +import google.registry.util.DateTimeUtils; +import java.io.Serializable; +import java.time.ZonedDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.Table; +import org.joda.time.DateTime; + +/** + * Shared entity for date cursors. This uses a compound primary key as defined in {@link CursorId}. + */ +@Entity +@Table +@IdClass(CursorId.class) +public class Cursor { + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Id + private CursorType type; + + @Column @Id private String scope; + + @Column(nullable = false) + private ZonedDateTime cursorTime; + + @Column(nullable = false) + private UpdateAutoTimestamp lastUpdateTime = UpdateAutoTimestamp.create(null); + + /** The scope of a global cursor. A global cursor is a cursor that is not specific to one tld. */ + public static final String GLOBAL = "GLOBAL"; + + private Cursor(CursorType type, String scope, DateTime cursorTime) { + this.type = type; + this.scope = scope; + this.cursorTime = DateTimeUtils.toZonedDateTime(cursorTime); + } + + // Hibernate requires a default constructor. + private Cursor() {} + + /** Constructs a {@link Cursor} object. */ + public static Cursor create(CursorType type, String scope, DateTime cursorTime) { + checkNotNull( + scope, "Scope cannot be null. To create a global cursor, use the createGlobal method"); + return new Cursor(type, scope, cursorTime); + } + + /** Constructs a {@link Cursor} object with a {@link GLOBAL} scope. */ + public static Cursor createGlobal(CursorType type, DateTime cursorTime) { + return new Cursor(type, GLOBAL, cursorTime); + } + + /** Returns the type of the cursor. */ + public CursorType getType() { + return type; + } + + /** + * Returns the scope of the cursor. The scope will typically be the tld the cursor is referring + * to. If the cursor is a global cursor, the scope will be {@link GLOBAL}. + */ + public String getScope() { + return scope; + } + + /** Returns the time the cursor is set to. */ + public DateTime getCursorTime() { + return DateTimeUtils.toJodaDateTime(cursorTime); + } + + /** Returns the last time the cursor was updated. */ + public DateTime getLastUpdateTime() { + return lastUpdateTime.getTimestamp(); + } + + static class CursorId extends ImmutableObject implements Serializable { + + public CursorType type; + + public String scope; + + private CursorId() {} + + public CursorId(CursorType type, String scope) { + this.type = type; + this.scope = scope; + } + } +} diff --git a/core/src/main/java/google/registry/schema/cursor/CursorDao.java b/core/src/main/java/google/registry/schema/cursor/CursorDao.java new file mode 100644 index 000000000..d901daae5 --- /dev/null +++ b/core/src/main/java/google/registry/schema/cursor/CursorDao.java @@ -0,0 +1,70 @@ +// Copyright 2019 The Nomulus Authors. 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 google.registry.schema.cursor; + +import static com.google.appengine.api.search.checkers.Preconditions.checkNotNull; +import static google.registry.model.transaction.TransactionManagerFactory.jpaTm; + +import google.registry.model.common.Cursor.CursorType; +import google.registry.schema.cursor.Cursor.CursorId; +import java.util.List; + +/** Data access object class for {@link Cursor}. */ +public class CursorDao { + + public static void save(Cursor cursor) { + jpaTm() + .transact( + () -> { + jpaTm().getEntityManager().merge(cursor); + }); + } + + public static Cursor load(CursorType type, String scope) { + checkNotNull(scope, "The scope of the cursor to load cannot be null"); + checkNotNull(type, "The type of the cursor to load must be specified"); + return jpaTm() + .transact(() -> jpaTm().getEntityManager().find(Cursor.class, new CursorId(type, scope))); + } + + /** If no scope is given, use {@link Cursor.GLOBAL} as the scope. */ + public static Cursor load(CursorType type) { + checkNotNull(type, "The type of the cursor to load must be specified"); + return load(type, Cursor.GLOBAL); + } + + public static List loadAll() { + return jpaTm() + .transact( + () -> + jpaTm() + .getEntityManager() + .createQuery("SELECT cursor FROM Cursor cursor", Cursor.class) + .getResultList()); + } + + public static List loadByType(CursorType type) { + checkNotNull(type, "The type of the cursors to load must be specified"); + return jpaTm() + .transact( + () -> + jpaTm() + .getEntityManager() + .createQuery( + "SELECT cursor FROM Cursor cursor WHERE cursor.type = :type", Cursor.class) + .setParameter("type", type) + .getResultList()); + } +} diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml index 47e866d8a..c6a1721e4 100644 --- a/core/src/main/resources/META-INF/persistence.xml +++ b/core/src/main/resources/META-INF/persistence.xml @@ -22,6 +22,7 @@ google.registry.model.domain.DomainBase google.registry.schema.domain.RegistryLock google.registry.schema.tmch.ClaimsList + google.registry.schema.cursor.Cursor google.registry.model.transfer.BaseTransferObject google.registry.schema.tld.PremiumList google.registry.schema.tld.ReservedList diff --git a/core/src/test/java/google/registry/schema/cursor/CursorDaoTest.java b/core/src/test/java/google/registry/schema/cursor/CursorDaoTest.java new file mode 100644 index 000000000..d76174ca2 --- /dev/null +++ b/core/src/test/java/google/registry/schema/cursor/CursorDaoTest.java @@ -0,0 +1,132 @@ +// Copyright 2019 The Nomulus Authors. 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 google.registry.schema.cursor; + +import static com.google.common.truth.Truth.assertThat; + +import google.registry.model.common.Cursor.CursorType; +import google.registry.model.transaction.JpaTransactionManagerRule; +import google.registry.testing.FakeClock; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link Cursor}. */ +@RunWith(JUnit4.class) +public class CursorDaoTest { + + private FakeClock fakeClock = new FakeClock(); + + @Rule + public final JpaTransactionManagerRule jpaTmRule = + new JpaTransactionManagerRule.Builder().build(); + + @Test + public void save_worksSuccessfullyOnNewCursor() { + Cursor cursor = Cursor.create(CursorType.BRDA, "tld", fakeClock.nowUtc()); + CursorDao.save(cursor); + Cursor returnedCursor = CursorDao.load(CursorType.BRDA, "tld"); + assertThat(returnedCursor.getCursorTime()).isEqualTo(cursor.getCursorTime()); + } + + @Test + public void save_worksSuccessfullyOnExistingCursor() { + Cursor cursor = Cursor.create(CursorType.RDE_REPORT, "tld", fakeClock.nowUtc()); + CursorDao.save(cursor); + Cursor cursor2 = Cursor.create(CursorType.RDE_REPORT, "tld", fakeClock.nowUtc().plusDays(3)); + CursorDao.save(cursor2); + Cursor returnedCursor = CursorDao.load(CursorType.RDE_REPORT, "tld"); + assertThat(returnedCursor.getCursorTime()).isEqualTo(cursor2.getCursorTime()); + } + + @Test + public void save_worksSuccessfullyOnNewGlobalCursor() { + Cursor cursor = Cursor.createGlobal(CursorType.RECURRING_BILLING, fakeClock.nowUtc()); + CursorDao.save(cursor); + Cursor returnedCursor = CursorDao.load(CursorType.RECURRING_BILLING); + assertThat(returnedCursor.getCursorTime()).isEqualTo(cursor.getCursorTime()); + } + + @Test + public void save_worksSuccessfullyOnExistingGlobalCursor() { + Cursor cursor = Cursor.createGlobal(CursorType.RECURRING_BILLING, fakeClock.nowUtc()); + CursorDao.save(cursor); + Cursor cursor2 = + Cursor.createGlobal(CursorType.RECURRING_BILLING, fakeClock.nowUtc().plusDays(3)); + CursorDao.save(cursor2); + Cursor returnedCursor = CursorDao.load(CursorType.RECURRING_BILLING); + assertThat(returnedCursor.getCursorTime()).isEqualTo(cursor2.getCursorTime()); + } + + @Test + public void load_worksSuccessfully() { + Cursor cursor = Cursor.createGlobal(CursorType.RECURRING_BILLING, fakeClock.nowUtc()); + Cursor cursor2 = Cursor.create(CursorType.RDE_REPORT, "tld", fakeClock.nowUtc()); + Cursor cursor3 = Cursor.create(CursorType.RDE_REPORT, "foo", fakeClock.nowUtc()); + Cursor cursor4 = Cursor.create(CursorType.BRDA, "foo", fakeClock.nowUtc()); + CursorDao.save(cursor); + CursorDao.save(cursor2); + CursorDao.save(cursor3); + CursorDao.save(cursor4); + Cursor returnedCursor = CursorDao.load(CursorType.RDE_REPORT, "tld"); + assertThat(returnedCursor.getCursorTime()).isEqualTo(cursor2.getCursorTime()); + returnedCursor = CursorDao.load(CursorType.BRDA, "foo"); + assertThat(returnedCursor.getCursorTime()).isEqualTo(cursor4.getCursorTime()); + returnedCursor = CursorDao.load(CursorType.RECURRING_BILLING); + assertThat(returnedCursor.getCursorTime()).isEqualTo(cursor.getCursorTime()); + } + + @Test + public void loadAll_worksSuccessfully() { + Cursor cursor = Cursor.createGlobal(CursorType.RECURRING_BILLING, fakeClock.nowUtc()); + Cursor cursor2 = Cursor.create(CursorType.RDE_REPORT, "tld", fakeClock.nowUtc()); + Cursor cursor3 = Cursor.create(CursorType.RDE_REPORT, "foo", fakeClock.nowUtc()); + Cursor cursor4 = Cursor.create(CursorType.BRDA, "foo", fakeClock.nowUtc()); + CursorDao.save(cursor); + CursorDao.save(cursor2); + CursorDao.save(cursor3); + CursorDao.save(cursor4); + List returnedCursors = CursorDao.loadAll(); + assertThat(returnedCursors.size()).isEqualTo(4); + } + + @Test + public void loadAll_worksSuccessfullyEmptyTable() { + List returnedCursors = CursorDao.loadAll(); + assertThat(returnedCursors.size()).isEqualTo(0); + } + + @Test + public void loadByType_worksSuccessfully() { + Cursor cursor = Cursor.createGlobal(CursorType.RECURRING_BILLING, fakeClock.nowUtc()); + Cursor cursor2 = Cursor.create(CursorType.RDE_REPORT, "tld", fakeClock.nowUtc()); + Cursor cursor3 = Cursor.create(CursorType.RDE_REPORT, "foo", fakeClock.nowUtc()); + Cursor cursor4 = Cursor.create(CursorType.BRDA, "foo", fakeClock.nowUtc()); + CursorDao.save(cursor); + CursorDao.save(cursor2); + CursorDao.save(cursor3); + CursorDao.save(cursor4); + List returnedCursors = CursorDao.loadByType(CursorType.RDE_REPORT); + assertThat(returnedCursors.size()).isEqualTo(2); + } + + @Test + public void loadByType_worksSuccessfullyNoneOfType() { + List returnedCursors = CursorDao.loadByType(CursorType.RDE_REPORT); + assertThat(returnedCursors.size()).isEqualTo(0); + } +} diff --git a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java index 9fdc87fa4..aab095c09 100644 --- a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java +++ b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java @@ -25,6 +25,7 @@ import google.registry.persistence.DateTimeConverterTest; import google.registry.persistence.JodaMoneyConverterTest; import google.registry.persistence.UpdateAutoTimestampConverterTest; import google.registry.persistence.ZonedDateTimeConverterTest; +import google.registry.schema.cursor.CursorDaoTest; import google.registry.schema.tld.PremiumListDaoTest; import google.registry.ui.server.registrar.RegistryLockGetActionTest; import org.junit.runner.RunWith; @@ -47,6 +48,7 @@ import org.junit.runners.Suite.SuiteClasses; ClaimsListDaoTest.class, CreateAutoTimestampConverterTest.class, CurrencyUnitConverterTest.class, + CursorDaoTest.class, DateTimeConverterTest.class, JodaMoneyConverterTest.class, JpaTransactionManagerImplTest.class, diff --git a/db/src/main/resources/sql/flyway/V11__create_cursor.sql b/db/src/main/resources/sql/flyway/V11__create_cursor.sql new file mode 100644 index 000000000..e495f3177 --- /dev/null +++ b/db/src/main/resources/sql/flyway/V11__create_cursor.sql @@ -0,0 +1,21 @@ +-- Copyright 2019 The Nomulus Authors. 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. + + create table "Cursor" ( + scope text not null, + type text not null, + cursor_time timestamptz not null, + last_update_time timestamptz not null, + primary key (scope, type) + ); diff --git a/db/src/main/resources/sql/schema/db-schema.sql.generated b/db/src/main/resources/sql/schema/db-schema.sql.generated index d2bb8ddf3..819ef6778 100644 --- a/db/src/main/resources/sql/schema/db-schema.sql.generated +++ b/db/src/main/resources/sql/schema/db-schema.sql.generated @@ -26,6 +26,14 @@ primary key (revision_id) ); + create table "Cursor" ( + scope text not null, + type text not null, + cursor_time timestamptz not null, + last_update_time timestamptz not null, + primary key (scope, type) + ); + create table "DelegationSignerData" ( key_tag int4 not null, algorithm int4 not null, diff --git a/db/src/main/resources/sql/schema/nomulus.golden.sql b/db/src/main/resources/sql/schema/nomulus.golden.sql index 5a21eb19b..e0341b6f7 100644 --- a/db/src/main/resources/sql/schema/nomulus.golden.sql +++ b/db/src/main/resources/sql/schema/nomulus.golden.sql @@ -61,6 +61,18 @@ CREATE SEQUENCE public."ClaimsList_revision_id_seq" ALTER SEQUENCE public."ClaimsList_revision_id_seq" OWNED BY public."ClaimsList".revision_id; +-- +-- Name: Cursor; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public."Cursor" ( + scope text NOT NULL, + type text NOT NULL, + cursor_time timestamp with time zone NOT NULL, + last_update_time timestamp with time zone NOT NULL +); + + -- -- Name: PremiumEntry; Type: TABLE; Schema: public; Owner: - -- @@ -228,6 +240,14 @@ ALTER TABLE ONLY public."ClaimsList" ADD CONSTRAINT "ClaimsList_pkey" PRIMARY KEY (revision_id); +-- +-- Name: Cursor Cursor_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Cursor" + ADD CONSTRAINT "Cursor_pkey" PRIMARY KEY (scope, type); + + -- -- Name: PremiumEntry PremiumEntry_pkey; Type: CONSTRAINT; Schema: public; Owner: - --