diff --git a/java/google/registry/model/registry/Registry.java b/java/google/registry/model/registry/Registry.java
index 7588d8049..8a2e78d2f 100644
--- a/java/google/registry/model/registry/Registry.java
+++ b/java/google/registry/model/registry/Registry.java
@@ -217,6 +217,26 @@ public class Registry extends ImmutableObject implements Buildable {
}
}
+ /** Returns the registry for a given TLD, throwing if none exists. */
+ public static Registry get(String tld) {
+ Registry registry = CACHE.getUnchecked(tld).orNull();
+ if (registry == null) {
+ throw new RegistryNotFoundException(tld);
+ }
+ return registry;
+ }
+
+ /**
+ * Invalidates the cache entry.
+ *
+ *
This is called automatically when the registry is saved. One should also call it when a
+ * registry is deleted.
+ */
+ @OnSave
+ public void invalidateInCache() {
+ CACHE.invalidate(tldStr);
+ }
+
/** A cache that loads the {@link Registry} for a given tld. */
private static final LoadingCache> CACHE =
CacheBuilder.newBuilder()
@@ -236,21 +256,6 @@ public class Registry extends ImmutableObject implements Buildable {
}}));
}});
- /** Returns the registry for a given TLD, throwing if none exists. */
- public static Registry get(String tld) {
- Registry registry = CACHE.getUnchecked(tld).orNull();
- if (registry == null) {
- throw new RegistryNotFoundException(tld);
- }
- return registry;
- }
-
- /** Whenever a registry is saved, invalidate the cache entry. */
- @OnSave
- void updateCache() {
- CACHE.invalidate(tldStr);
- }
-
/**
* The name of the pricing engine that this TLD uses.
*
diff --git a/java/google/registry/tools/DeleteTldCommand.java b/java/google/registry/tools/DeleteTldCommand.java
new file mode 100644
index 000000000..257afb71a
--- /dev/null
+++ b/java/google/registry/tools/DeleteTldCommand.java
@@ -0,0 +1,89 @@
+// Copyright 2017 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.tools;
+
+import static com.google.common.base.Preconditions.checkState;
+import static google.registry.model.ofy.ObjectifyService.ofy;
+
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import com.googlecode.objectify.VoidWork;
+import google.registry.model.domain.DomainResource;
+import google.registry.model.registrar.Registrar;
+import google.registry.model.registry.Registry;
+import google.registry.model.registry.Registry.TldType;
+import google.registry.tools.Command.RemoteApiCommand;
+
+/**
+ * Command to delete the {@link Registry} associated with the specified TLD in Datastore.
+ *
+ * This command will fail if any domains are currently registered on the TLD.
+ */
+@Parameters(separators = " =", commandDescription = "Delete a TLD from Datastore.")
+final class DeleteTldCommand extends ConfirmingCommand implements RemoteApiCommand {
+
+ private Registry registry;
+
+ @Parameter(
+ names = {"-t", "--tld"},
+ description = "The TLD to delete.",
+ required = true)
+ private String tld;
+
+ /**
+ * Perform the command by deleting the TLD.
+ *
+ *
Note that this uses an eventually consistent query, so theoretically, if you create a TLD,
+ * create domains on it, then delete the TLD quickly enough, the code won't notice the domains,
+ * and will let you delete the TLD. Since this command is only intended to be used in cleanup
+ * tasks, that should be ok, and the check should always provide the desired safety against
+ * accidental deletion of established TLDs with domains on them.
+ */
+ @Override
+ protected void init() throws Exception {
+ registry = Registry.get(tld);
+ checkState(registry.getTldType().equals(TldType.TEST), "Cannot delete a real TLD");
+
+ for (Registrar registrar : Registrar.loadAll()) {
+ checkState(
+ !registrar.getAllowedTlds().contains(tld),
+ "Cannot delete TLD because registrar %s lists it as an allowed TLD",
+ registrar.getClientId());
+ }
+
+ int count = ofy().load()
+ .type(DomainResource.class)
+ .filter("tld", tld)
+ .limit(1)
+ .count();
+ checkState(count == 0, "Cannot delete TLD because a domain is defined on it");
+ }
+
+ @Override
+ protected String prompt() {
+ return "You are about to delete TLD: " + tld;
+ }
+
+ @Override
+ protected String execute() throws Exception {
+ ofy().transactNew(new VoidWork() {
+ @Override
+ public void vrun() {
+ ofy().delete().entity(registry).now();
+ }});
+ registry.invalidateInCache();
+ return String.format("Deleted TLD '%s'.\n", tld);
+ }
+}
diff --git a/java/google/registry/tools/RegistryTool.java b/java/google/registry/tools/RegistryTool.java
index 1e9289de5..4b13d4e86 100644
--- a/java/google/registry/tools/RegistryTool.java
+++ b/java/google/registry/tools/RegistryTool.java
@@ -52,6 +52,7 @@ public final class RegistryTool {
.put("delete_entity", DeleteEntityCommand.class)
.put("delete_premium_list", DeletePremiumListCommand.class)
.put("delete_reserved_list", DeleteReservedListCommand.class)
+ .put("delete_tld", DeleteTldCommand.class)
.put("domain_application_info", DomainApplicationInfoCommand.class)
.put("domain_check", DomainCheckCommand.class)
.put("domain_check_claims", DomainCheckClaimsCommand.class)
diff --git a/javatests/google/registry/testing/DatastoreHelper.java b/javatests/google/registry/testing/DatastoreHelper.java
index 89e2ca988..37e5369ec 100644
--- a/javatests/google/registry/testing/DatastoreHelper.java
+++ b/javatests/google/registry/testing/DatastoreHelper.java
@@ -85,6 +85,7 @@ import google.registry.model.pricing.StaticPremiumListPricingEngine;
import google.registry.model.registrar.Registrar;
import google.registry.model.registry.Registry;
import google.registry.model.registry.Registry.TldState;
+import google.registry.model.registry.Registry.TldType;
import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.PremiumList.PremiumListEntry;
import google.registry.model.registry.label.PremiumList.PremiumListRevision;
@@ -237,7 +238,23 @@ public class DatastoreHelper {
public static Registry newRegistry(
String tld, String roidSuffix, ImmutableSortedMap tldStates) {
- return new Registry.Builder()
+ return setupRegistry(new Registry.Builder(), tld, roidSuffix, tldStates);
+ }
+
+ public static Registry newRegistry(
+ String tld,
+ String roidSuffix,
+ ImmutableSortedMap tldStates,
+ TldType tldType) {
+ return setupRegistry(new Registry.Builder().setTldType(tldType), tld, roidSuffix, tldStates);
+ }
+
+ private static Registry setupRegistry(
+ Registry.Builder registryBuilder,
+ String tld,
+ String roidSuffix,
+ ImmutableSortedMap tldStates) {
+ return registryBuilder
.setTldStr(tld)
.setRoidSuffix(roidSuffix)
.setTldStateTransitions(tldStates)
diff --git a/javatests/google/registry/tools/DeleteTldCommandTest.java b/javatests/google/registry/tools/DeleteTldCommandTest.java
new file mode 100644
index 000000000..51063050f
--- /dev/null
+++ b/javatests/google/registry/tools/DeleteTldCommandTest.java
@@ -0,0 +1,91 @@
+// Copyright 2017 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.tools;
+
+import static google.registry.testing.DatastoreHelper.allowRegistrarAccess;
+import static google.registry.testing.DatastoreHelper.newRegistry;
+import static google.registry.testing.DatastoreHelper.persistDeletedDomain;
+import static google.registry.testing.DatastoreHelper.persistResource;
+import static google.registry.util.DateTimeUtils.START_OF_TIME;
+
+import com.google.common.base.Ascii;
+import com.google.common.collect.ImmutableSortedMap;
+import google.registry.model.registry.Registry;
+import google.registry.model.registry.Registry.RegistryNotFoundException;
+import google.registry.model.registry.Registry.TldState;
+import google.registry.model.registry.Registry.TldType;
+import org.joda.time.DateTime;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Unit tests for {@link DeleteTldCommand}. */
+public class DeleteTldCommandTest extends CommandTestCase {
+
+ private static final String TLD_REAL = "tldreal";
+ private static final String TLD_TEST = "tldtest";
+
+ @Before
+ public void setUp() {
+ persistResource(
+ newRegistry(
+ TLD_REAL,
+ Ascii.toUpperCase(TLD_REAL),
+ ImmutableSortedMap.of(START_OF_TIME, TldState.GENERAL_AVAILABILITY),
+ TldType.REAL));
+ persistResource(
+ newRegistry(
+ TLD_TEST,
+ Ascii.toUpperCase(TLD_TEST),
+ ImmutableSortedMap.of(START_OF_TIME, TldState.GENERAL_AVAILABILITY),
+ TldType.TEST));
+ }
+
+ @Test
+ public void testSuccess_otherTldUnaffected() throws Exception {
+ runCommandForced("--tld=" + TLD_TEST);
+
+ Registry.get(TLD_REAL);
+ thrown.expect(RegistryNotFoundException.class);
+ Registry.get(TLD_TEST);
+ }
+
+ @Test
+ public void testFailure_whenTldDoesNotExist() throws Exception {
+ thrown.expect(RegistryNotFoundException.class);
+ runCommandForced("--tld=nonexistenttld");
+ }
+
+ @Test
+ public void testFailure_whenTldIsReal() throws Exception {
+ thrown.expect(IllegalStateException.class);
+ runCommandForced("--tld=" + TLD_REAL);
+ }
+
+ @Test
+ public void testFailure_whenDomainsArePresent() throws Exception {
+ persistDeletedDomain("domain." + TLD_TEST, DateTime.parse("2000-01-01TZ"));
+
+ thrown.expect(IllegalStateException.class);
+ runCommandForced("--tld=" + TLD_TEST);
+ }
+
+ @Test
+ public void testFailure_whenRegistrarLinksToTld() throws Exception {
+ allowRegistrarAccess("TheRegistrar", TLD_TEST);
+
+ thrown.expect(IllegalStateException.class);
+ runCommandForced("--tld=" + TLD_TEST);
+ }
+}