From ea4e471c04ccb134c2fbfff349db7dcd30497fdd Mon Sep 17 00:00:00 2001 From: mcilwain Date: Fri, 24 Feb 2017 13:38:59 -0800 Subject: [PATCH] Move premium list static helper methods into their own class It was kind of messy having all of that logic living alongside the entities themselves. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148498024 --- .../StaticPremiumListPricingEngine.java | 2 +- .../model/registry/label/PremiumList.java | 197 +------------- .../registry/label/PremiumListUtils.java | 234 +++++++++++++++++ .../tools/DeletePremiumListCommand.java | 6 +- .../tools/server/CreatePremiumListAction.java | 8 +- .../tools/server/UpdatePremiumListAction.java | 5 +- .../model/registry/label/PremiumListTest.java | 210 +-------------- .../registry/label/PremiumListUtilsTest.java | 248 ++++++++++++++++++ .../registry/testing/DatastoreHelper.java | 4 +- .../tools/DeletePremiumListCommandTest.java | 6 +- .../server/CreatePremiumListActionTest.java | 19 +- .../server/UpdatePremiumListActionTest.java | 5 +- 12 files changed, 523 insertions(+), 421 deletions(-) create mode 100644 java/google/registry/model/registry/label/PremiumListUtils.java create mode 100644 javatests/google/registry/model/registry/label/PremiumListUtilsTest.java diff --git a/java/google/registry/model/pricing/StaticPremiumListPricingEngine.java b/java/google/registry/model/pricing/StaticPremiumListPricingEngine.java index 158633845..fbc2adc18 100644 --- a/java/google/registry/model/pricing/StaticPremiumListPricingEngine.java +++ b/java/google/registry/model/pricing/StaticPremiumListPricingEngine.java @@ -17,7 +17,7 @@ package google.registry.model.pricing; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.emptyToNull; import static google.registry.model.registry.Registry.TldState.SUNRISE; -import static google.registry.model.registry.label.PremiumList.getPremiumPrice; +import static google.registry.model.registry.label.PremiumListUtils.getPremiumPrice; import static google.registry.model.registry.label.ReservationType.NAME_COLLISION; import static google.registry.model.registry.label.ReservedList.getReservation; import static google.registry.util.DomainNameUtils.getTldFromDomainName; diff --git a/java/google/registry/model/registry/label/PremiumList.java b/java/google/registry/model/registry/label/PremiumList.java index e87e443a3..2efccfe8f 100644 --- a/java/google/registry/model/registry/label/PremiumList.java +++ b/java/google/registry/model/registry/label/PremiumList.java @@ -13,10 +13,7 @@ // limitations under the License. package google.registry.model.registry.label; -import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.collect.Iterables.partition; import static com.google.common.hash.Funnels.unencodedCharsFunnel; import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration; import static google.registry.config.RegistryConfig.getSingletonCachePersistDuration; @@ -28,20 +25,15 @@ import static google.registry.model.ofy.Ofy.RECOMMENDED_MEMCACHE_EXPIRATION; import static java.util.concurrent.TimeUnit.MILLISECONDS; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Splitter; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.CacheLoader.InvalidCacheLoadException; import com.google.common.cache.LoadingCache; -import com.google.common.collect.FluentIterable; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.common.hash.BloomFilter; import com.google.common.util.concurrent.UncheckedExecutionException; import com.googlecode.objectify.Key; -import com.googlecode.objectify.VoidWork; import com.googlecode.objectify.Work; import com.googlecode.objectify.annotation.Cache; import com.googlecode.objectify.annotation.Entity; @@ -55,13 +47,11 @@ import google.registry.model.registry.Registry; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; import org.joda.money.Money; -import org.joda.time.DateTime; /** * A premium list entity, persisted to Datastore, that is used to check domain label prices. @@ -71,9 +61,6 @@ import org.joda.time.DateTime; @Cache(expirationSeconds = RECOMMENDED_MEMCACHE_EXPIRATION) public final class PremiumList extends BaseDomainLabelList { - /** The number of premium list entry entities that are created and deleted per batch. */ - private static final int TRANSACTION_BATCH_SIZE = 200; - /** Stores the revision key for the set of currently used premium list entry entities. */ Key revisionKey; @@ -139,7 +126,7 @@ public final class PremiumList extends BaseDomainLabelListThis is cached for a shorter duration because we need to periodically reload this entity to * check if a new revision has been published, and if so, then use that. */ - private static final LoadingCache cachePremiumLists = + static final LoadingCache cachePremiumLists = CacheBuilder.newBuilder() .expireAfterWrite(getDomainLabelListCacheDuration().getMillis(), MILLISECONDS) .build(new CacheLoader() { @@ -163,7 +150,7 @@ public final class PremiumList extends BaseDomainLabelList, PremiumListRevision> + static final LoadingCache, PremiumListRevision> cachePremiumListRevisions = CacheBuilder.newBuilder() .expireAfterWrite(getSingletonCachePersistDuration().getMillis(), MILLISECONDS) @@ -214,67 +201,6 @@ public final class PremiumList extends BaseDomainLabelList getPremiumPrice(String label, Registry registry) { - // If the registry has no configured premium list, then no labels are premium. - if (registry.getPremiumList() == null) { - return Optional. absent(); - } - String listName = registry.getPremiumList().getName(); - Optional optionalPremiumList = get(listName); - checkState(optionalPremiumList.isPresent(), "Could not load premium list '%s'", listName); - PremiumList premiumList = optionalPremiumList.get(); - PremiumListRevision revision; - try { - revision = cachePremiumListRevisions.get(premiumList.getRevisionKey()); - } catch (InvalidCacheLoadException | ExecutionException e) { - throw new RuntimeException( - "Could not load premium list revision " + premiumList.getRevisionKey(), e); - } - checkState( - revision.probablePremiumLabels != null, - "Probable premium labels bloom filter is null on revision '%s'", - premiumList.getRevisionKey()); - - if (revision.probablePremiumLabels.mightContain(label)) { - Key entryKey = - Key.create(premiumList.getRevisionKey(), PremiumListEntry.class, label); - try { - Optional entry = cachePremiumListEntries.get(entryKey); - return (entry.isPresent()) ? Optional.of(entry.get().getValue()) : Optional.absent(); - } catch (InvalidCacheLoadException | ExecutionException e) { - throw new RuntimeException("Could not load premium list entry " + entryKey, e); - } - } else { - return Optional.absent(); - } - } - - /** - * Loads and returns the entire premium list map. - * - *

This load operation is quite expensive for large premium lists because each premium list - * entry is a separate Datastore entity, and loading them this way bypasses the in-memory caches. - * Do not use this method if all you need to do is check the price of a small number of labels! - */ - @VisibleForTesting - public Map loadPremiumListEntries() { - try { - ImmutableMap.Builder entriesMap = new ImmutableMap.Builder<>(); - if (revisionKey != null) { - for (PremiumListEntry entry : queryEntriesForCurrentRevision()) { - entriesMap.put(entry.getLabel(), entry); - } - } - return entriesMap.build(); - } catch (Exception e) { - throw new RuntimeException("Could not retrieve entries for premium list " + name, e); - } - } - @VisibleForTesting public Key getRevisionKey() { return revisionKey; @@ -291,11 +217,6 @@ public final class PremiumList extends BaseDomainLabelList premiumListLines) { - return saveWithEntries(premiumList, premiumList.parse(premiumListLines)); - } - - /** Re-parents the given {@link PremiumListEntry}s on the given {@link PremiumListRevision}. */ - public static ImmutableSet parentEntriesOnRevision( - Iterable entries, final Key revisionKey) { - return FluentIterable.from(firstNonNull(entries, ImmutableSet.of())) - .transform( - new Function() { - @Override - public PremiumListEntry apply(PremiumListEntry entry) { - return entry.asBuilder().setParent(revisionKey).build(); - } - }) - .toSet(); - } - - /** - * Persists a new or updated PremiumList object and its descendant entities to Datastore. - * - *

The flow here is: save the new premium list entries parented on that revision entity, - * save/update the PremiumList, and then delete the old premium list entries associated with the - * old revision. - * - *

This is the only valid way to save these kinds of entities! - */ - public static PremiumList saveWithEntries( - final PremiumList premiumList, ImmutableMap premiumListEntries) { - final Optional oldPremiumList = get(premiumList.getName()); - - // Create the new revision (with its bloom filter) and parent the entries on it. - final PremiumListRevision newRevision = - PremiumListRevision.create(premiumList, premiumListEntries.keySet()); - final Key newRevisionKey = Key.create(newRevision); - ImmutableSet parentedEntries = - parentEntriesOnRevision( - firstNonNull(premiumListEntries.values(), ImmutableSet.of()), newRevisionKey); - - // Save the new child entities in a series of transactions. - for (final List batch : partition(parentedEntries, TRANSACTION_BATCH_SIZE)) { - ofy().transactNew(new VoidWork() { - @Override - public void vrun() { - ofy().save().entities(batch); - }}); - } - - // Save the new PremiumList and revision itself. - PremiumList updated = ofy().transactNew(new Work() { - @Override - public PremiumList run() { - DateTime now = ofy().getTransactionTime(); - // Assert that the premium list hasn't been changed since we started this process. - PremiumList existing = ofy().load() - .type(PremiumList.class) - .parent(getCrossTldKey()) - .id(premiumList.getName()) - .now(); - checkState( - Objects.equals(existing, oldPremiumList.orNull()), - "PremiumList was concurrently edited"); - PremiumList newList = premiumList.asBuilder() - .setLastUpdateTime(now) - .setCreationTime( - oldPremiumList.isPresent() ? oldPremiumList.get().creationTime : now) - .setRevision(newRevisionKey) - .build(); - ofy().save().entities(newList, newRevision); - return newList; - }}); - // Update the cache. - cachePremiumLists.put(premiumList.getName(), updated); - // Delete the entities under the old PremiumList. - if (oldPremiumList.isPresent()) { - oldPremiumList.get().deleteRevisionAndEntries(); - } - return updated; - } - @Override public boolean refersToKey(Registry registry, Key> key) { return Objects.equals(registry.getPremiumList(), key); } - /** Deletes the PremiumList and all of its child entities. */ - public void delete() { - ofy().transactNew(new VoidWork() { - @Override - public void vrun() { - ofy().delete().entity(PremiumList.this); - }}); - deleteRevisionAndEntries(); - cachePremiumLists.invalidate(name); - } - - private void deleteRevisionAndEntries() { - if (revisionKey == null) { - return; - } - for (final List> batch : partition( - queryEntriesForCurrentRevision().keys(), - TRANSACTION_BATCH_SIZE)) { - ofy().transactNew(new VoidWork() { - @Override - public void vrun() { - ofy().delete().keys(batch); - }}); - } - ofy().transactNew(new VoidWork() { - @Override - public void vrun() { - ofy().delete().key(revisionKey); - }}); - } - - private Query queryEntriesForCurrentRevision() { + Query queryEntriesForCurrentRevision() { return ofy().load().type(PremiumListEntry.class).ancestor(revisionKey); } diff --git a/java/google/registry/model/registry/label/PremiumListUtils.java b/java/google/registry/model/registry/label/PremiumListUtils.java new file mode 100644 index 000000000..3ebea3a71 --- /dev/null +++ b/java/google/registry/model/registry/label/PremiumListUtils.java @@ -0,0 +1,234 @@ +// 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.model.registry.label; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.partition; +import static google.registry.model.common.EntityGroupRoot.getCrossTldKey; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.model.registry.label.PremiumList.cachePremiumListEntries; +import static google.registry.model.registry.label.PremiumList.cachePremiumListRevisions; +import static google.registry.model.registry.label.PremiumList.cachePremiumLists; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.cache.CacheLoader.InvalidCacheLoadException; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.googlecode.objectify.Key; +import com.googlecode.objectify.VoidWork; +import com.googlecode.objectify.Work; +import google.registry.model.registry.Registry; +import google.registry.model.registry.label.PremiumList.PremiumListEntry; +import google.registry.model.registry.label.PremiumList.PremiumListRevision; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import org.joda.money.Money; +import org.joda.time.DateTime; + +/** Static helper methods for working with {@link PremiumList}s. */ +public final class PremiumListUtils { + + /** The number of premium list entry entities that are created and deleted per batch. */ + static final int TRANSACTION_BATCH_SIZE = 200; + + /** + * Returns the premium price for the specified label and registry, or absent if the label is not + * premium. + */ + public static Optional getPremiumPrice(String label, Registry registry) { + // If the registry has no configured premium list, then no labels are premium. + if (registry.getPremiumList() == null) { + return Optional. absent(); + } + String listName = registry.getPremiumList().getName(); + Optional optionalPremiumList = PremiumList.get(listName); + checkState(optionalPremiumList.isPresent(), "Could not load premium list '%s'", listName); + PremiumList premiumList = optionalPremiumList.get(); + PremiumListRevision revision; + try { + revision = cachePremiumListRevisions.get(premiumList.getRevisionKey()); + } catch (InvalidCacheLoadException | ExecutionException e) { + throw new RuntimeException( + "Could not load premium list revision " + premiumList.getRevisionKey(), e); + } + checkState( + revision.probablePremiumLabels != null, + "Probable premium labels bloom filter is null on revision '%s'", + premiumList.getRevisionKey()); + + if (revision.probablePremiumLabels.mightContain(label)) { + Key entryKey = + Key.create(premiumList.getRevisionKey(), PremiumListEntry.class, label); + try { + Optional entry = cachePremiumListEntries.get(entryKey); + return (entry.isPresent()) ? Optional.of(entry.get().getValue()) : Optional.absent(); + } catch (InvalidCacheLoadException | ExecutionException e) { + throw new RuntimeException("Could not load premium list entry " + entryKey, e); + } + } else { + return Optional.absent(); + } + } + + /** + * Persists a new or updated PremiumList object and its descendant entities to Datastore. + * + *

The flow here is: save the new premium list entries parented on that revision entity, + * save/update the PremiumList, and then delete the old premium list entries associated with the + * old revision. + * + *

This is the only valid way to save these kinds of entities! + */ + public static PremiumList savePremiumListAndEntries( + final PremiumList premiumList, + ImmutableMap premiumListEntries) { + final Optional oldPremiumList = PremiumList.get(premiumList.getName()); + + // Create the new revision (with its bloom filter) and parent the entries on it. + final PremiumListRevision newRevision = + PremiumListRevision.create(premiumList, premiumListEntries.keySet()); + final Key newRevisionKey = Key.create(newRevision); + ImmutableSet parentedEntries = + parentPremiumListEntriesOnRevision( + firstNonNull(premiumListEntries.values(), ImmutableSet.of()), newRevisionKey); + + // Save the new child entities in a series of transactions. + for (final List batch : + partition(parentedEntries, TRANSACTION_BATCH_SIZE)) { + ofy().transactNew(new VoidWork() { + @Override + public void vrun() { + ofy().save().entities(batch); + }}); + } + + // Save the new PremiumList and revision itself. + PremiumList updated = ofy().transactNew(new Work() { + @Override + public PremiumList run() { + DateTime now = ofy().getTransactionTime(); + // Assert that the premium list hasn't been changed since we started this process. + PremiumList existing = ofy().load() + .type(PremiumList.class) + .parent(getCrossTldKey()) + .id(premiumList.getName()) + .now(); + checkState( + Objects.equals(existing, oldPremiumList.orNull()), + "PremiumList was concurrently edited"); + PremiumList newList = premiumList.asBuilder() + .setLastUpdateTime(now) + .setCreationTime( + oldPremiumList.isPresent() ? oldPremiumList.get().creationTime : now) + .setRevision(newRevisionKey) + .build(); + ofy().save().entities(newList, newRevision); + return newList; + }}); + // Update the cache. + cachePremiumLists.put(premiumList.getName(), updated); + // Delete the entities under the old PremiumList. + if (oldPremiumList.isPresent()) { + deleteRevisionAndEntriesOfPremiumList(oldPremiumList.get()); + } + return updated; + } + + public static PremiumList savePremiumListAndEntries( + PremiumList premiumList, Iterable premiumListLines) { + return savePremiumListAndEntries(premiumList, premiumList.parse(premiumListLines)); + } + + /** Re-parents the given {@link PremiumListEntry}s on the given {@link PremiumListRevision}. */ + public static ImmutableSet parentPremiumListEntriesOnRevision( + Iterable entries, final Key revisionKey) { + return FluentIterable.from(firstNonNull(entries, ImmutableSet.of())) + .transform( + new Function() { + @Override + public PremiumListEntry apply(PremiumListEntry entry) { + return entry.asBuilder().setParent(revisionKey).build(); + } + }) + .toSet(); + } + + /** Deletes the PremiumList and all of its child entities. */ + public static void deletePremiumList(final PremiumList premiumList) { + ofy().transactNew(new VoidWork() { + @Override + public void vrun() { + ofy().delete().entity(premiumList); + }}); + deleteRevisionAndEntriesOfPremiumList(premiumList); + cachePremiumLists.invalidate(premiumList.getName()); + } + + static void deleteRevisionAndEntriesOfPremiumList(final PremiumList premiumList) { + if (premiumList.getRevisionKey() == null) { + return; + } + for (final List> batch : partition( + premiumList.queryEntriesForCurrentRevision().keys(), + TRANSACTION_BATCH_SIZE)) { + ofy().transactNew(new VoidWork() { + @Override + public void vrun() { + ofy().delete().keys(batch); + }}); + } + ofy().transactNew(new VoidWork() { + @Override + public void vrun() { + ofy().delete().key(premiumList.getRevisionKey()); + }}); + } + + /** Returns whether a PremiumList of the given name exists, bypassing the cache. */ + public static boolean doesPremiumListExist(String name) { + return ofy().load().key(Key.create(getCrossTldKey(), PremiumList.class, name)).now() != null; + } + + /** + * Loads and returns the entire premium list map. + * + *

This load operation is quite expensive for large premium lists because each premium list + * entry is a separate Datastore entity, and loading them this way bypasses the in-memory caches. + * Do not use this method if all you need to do is check the price of a small number of labels! + */ + @VisibleForTesting + public static Map loadPremiumListEntries(PremiumList premiumList) { + try { + ImmutableMap.Builder entriesMap = new ImmutableMap.Builder<>(); + if (premiumList.getRevisionKey() != null) { + for (PremiumListEntry entry : premiumList.queryEntriesForCurrentRevision()) { + entriesMap.put(entry.getLabel(), entry); + } + } + return entriesMap.build(); + } catch (Exception e) { + throw new RuntimeException( + "Could not retrieve entries for premium list " + premiumList.getName(), e); + } + } + + private PremiumListUtils() {} +} diff --git a/java/google/registry/tools/DeletePremiumListCommand.java b/java/google/registry/tools/DeletePremiumListCommand.java index 93a33a8d8..a5d956b7b 100644 --- a/java/google/registry/tools/DeletePremiumListCommand.java +++ b/java/google/registry/tools/DeletePremiumListCommand.java @@ -15,6 +15,8 @@ package google.registry.tools; import static com.google.common.base.Preconditions.checkArgument; +import static google.registry.model.registry.label.PremiumListUtils.deletePremiumList; +import static google.registry.model.registry.label.PremiumListUtils.doesPremiumListExist; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; @@ -43,7 +45,7 @@ final class DeletePremiumListCommand extends ConfirmingCommand implements Remote @Override protected void init() throws Exception { checkArgument( - PremiumList.exists(name), + doesPremiumListExist(name), "Cannot delete the premium list %s because it doesn't exist.", name); premiumList = PremiumList.get(name).get(); @@ -61,7 +63,7 @@ final class DeletePremiumListCommand extends ConfirmingCommand implements Remote @Override protected String execute() throws Exception { - premiumList.delete(); + deletePremiumList(premiumList); return String.format("Deleted premium list '%s'.\n", premiumList.getName()); } } diff --git a/java/google/registry/tools/server/CreatePremiumListAction.java b/java/google/registry/tools/server/CreatePremiumListAction.java index cb2a4aa13..dccee5355 100644 --- a/java/google/registry/tools/server/CreatePremiumListAction.java +++ b/java/google/registry/tools/server/CreatePremiumListAction.java @@ -16,7 +16,8 @@ package google.registry.tools.server; import static com.google.common.base.Preconditions.checkArgument; import static google.registry.model.registry.Registries.assertTldExists; -import static google.registry.model.registry.label.PremiumList.saveWithEntries; +import static google.registry.model.registry.label.PremiumListUtils.doesPremiumListExist; +import static google.registry.model.registry.label.PremiumListUtils.savePremiumListAndEntries; import static google.registry.request.Action.Method.POST; import com.google.common.base.Splitter; @@ -43,8 +44,7 @@ public class CreatePremiumListAction extends CreateOrUpdatePremiumListAction { @Override protected void savePremiumList() { checkArgument( - !PremiumList.exists(name), - "A premium list of this name already exists: %s.", name); + !doesPremiumListExist(name), "A premium list of this name already exists: %s.", name); if (!override) { assertTldExists(name); } @@ -54,7 +54,7 @@ public class CreatePremiumListAction extends CreateOrUpdatePremiumListAction { List inputDataPreProcessed = Splitter.on('\n').omitEmptyStrings().splitToList(inputData); PremiumList premiumList = new PremiumList.Builder().setName(name).build(); - saveWithEntries(premiumList, inputDataPreProcessed); + savePremiumListAndEntries(premiumList, inputDataPreProcessed); String message = String.format( diff --git a/java/google/registry/tools/server/UpdatePremiumListAction.java b/java/google/registry/tools/server/UpdatePremiumListAction.java index f3830ff02..ba390231d 100644 --- a/java/google/registry/tools/server/UpdatePremiumListAction.java +++ b/java/google/registry/tools/server/UpdatePremiumListAction.java @@ -15,7 +15,7 @@ package google.registry.tools.server; import static com.google.common.base.Preconditions.checkArgument; -import static google.registry.model.registry.label.PremiumList.saveWithEntries; +import static google.registry.model.registry.label.PremiumListUtils.savePremiumListAndEntries; import static google.registry.request.Action.Method.POST; import com.google.common.base.Optional; @@ -49,7 +49,8 @@ public class UpdatePremiumListAction extends CreateOrUpdatePremiumListAction { logger.infofmt("Got the following input data: %s", inputData); List inputDataPreProcessed = Splitter.on('\n').omitEmptyStrings().splitToList(inputData); - PremiumList newPremiumList = saveWithEntries(existingPremiumList.get(), inputDataPreProcessed); + PremiumList newPremiumList = + savePremiumListAndEntries(existingPremiumList.get(), inputDataPreProcessed); String message = String.format( diff --git a/javatests/google/registry/model/registry/label/PremiumListTest.java b/javatests/google/registry/model/registry/label/PremiumListTest.java index f3f90fb4d..63eb5bf04 100644 --- a/javatests/google/registry/model/registry/label/PremiumListTest.java +++ b/javatests/google/registry/model/registry/label/PremiumListTest.java @@ -17,9 +17,6 @@ package google.registry.model.registry.label; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static google.registry.model.ofy.ObjectifyService.ofy; -import static google.registry.model.registry.label.PremiumList.cachePremiumListEntries; -import static google.registry.model.registry.label.PremiumList.getPremiumPrice; -import static google.registry.model.registry.label.PremiumList.saveWithEntries; import static google.registry.testing.DatastoreHelper.createTld; import static google.registry.testing.DatastoreHelper.persistPremiumList; import static google.registry.testing.DatastoreHelper.persistReservedList; @@ -28,14 +25,11 @@ import static google.registry.testing.DatastoreHelper.persistResource; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.googlecode.objectify.Key; -import google.registry.model.pricing.StaticPremiumListPricingEngine; import google.registry.model.registry.Registry; import google.registry.model.registry.label.PremiumList.PremiumListEntry; import google.registry.model.registry.label.PremiumList.PremiumListRevision; import google.registry.testing.AppEngineRule; import google.registry.testing.ExceptionRule; -import java.util.Map; -import org.joda.money.Money; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -46,174 +40,23 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class PremiumListTest { - @Rule - public final ExceptionRule thrown = new ExceptionRule(); - - @Rule - public final AppEngineRule appEngine = AppEngineRule.builder() - .withDatastore() - .build(); + @Rule public final ExceptionRule thrown = new ExceptionRule(); + @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); @Before public void before() throws Exception { // createTld() overwrites the premium list, so call it first. createTld("tld"); - PremiumList pl = persistPremiumList( - "tld", - "lol,USD 999 # yup", - "rich,USD 1999 #tada", - "icann,JPY 100", - "johnny-be-goode,USD 20.50"); + PremiumList pl = + persistPremiumList( + "tld", + "lol,USD 999 # yup", + "rich,USD 1999 #tada", + "icann,JPY 100", + "johnny-be-goode,USD 20.50"); persistResource(Registry.get("tld").asBuilder().setPremiumList(pl).build()); } - @Test - public void testGetPremiumPrice_returnsNoPriceWhenNoPremiumListConfigured() throws Exception { - createTld("ghost"); - persistResource( - new Registry.Builder() - .setTldStr("ghost") - .setPremiumPricingEngine(StaticPremiumListPricingEngine.NAME) - .build()); - assertThat(Registry.get("ghost").getPremiumList()).isNull(); - assertThat(getPremiumPrice("blah", Registry.get("ghost"))).isAbsent(); - } - - @Test - public void testGetPremiumPrice_throwsExceptionWhenNonExistentPremiumListConfigured() - throws Exception { - PremiumList.get("tld").get().delete(); - thrown.expect(IllegalStateException.class, "Could not load premium list 'tld'"); - getPremiumPrice("blah", Registry.get("tld")); - } - - @Test - public void testSave_largeNumberOfEntries_succeeds() throws Exception { - PremiumList premiumList = persistHumongousPremiumList("tld", 2500); - assertThat(premiumList.loadPremiumListEntries()).hasSize(2500); - assertThat(getPremiumPrice("7", Registry.get("tld"))).hasValue(Money.parse("USD 100")); - } - - @Test - public void testSave_updateTime_isUpdatedOnEverySave() throws Exception { - PremiumList pl = - saveWithEntries( - new PremiumList.Builder().setName("tld3").build(), ImmutableList.of("slime,USD 10")); - PremiumList newPl = - saveWithEntries( - new PremiumList.Builder().setName(pl.getName()).build(), - ImmutableList.of("mutants,USD 20")); - assertThat(newPl.getLastUpdateTime()).isGreaterThan(pl.getLastUpdateTime()); - } - - @Test - public void testSave_creationTime_onlyUpdatedOnFirstCreation() throws Exception { - PremiumList pl = persistPremiumList("tld3", "sludge,JPY 1000"); - PremiumList newPl = saveWithEntries(pl, ImmutableList.of("sleighbells,CHF 2000")); - assertThat(newPl.creationTime).isEqualTo(pl.creationTime); - } - - @Test - public void testSave_removedPremiumListEntries_areNoLongerInDatastore() throws Exception { - Registry registry = Registry.get("tld"); - PremiumList pl = persistPremiumList("tld", "genius,USD 10", "dolt,JPY 1000"); - assertThat(getPremiumPrice("genius", registry)).hasValue(Money.parse("USD 10")); - assertThat(getPremiumPrice("dolt", registry)).hasValue(Money.parse("JPY 1000")); - assertThat(ofy() - .load() - .type(PremiumListEntry.class) - .parent(pl.getRevisionKey()) - .id("dolt") - .now() - .price) - .isEqualTo(Money.parse("JPY 1000")); - PremiumList pl2 = saveWithEntries(pl, ImmutableList.of("genius,USD 10", "savant,USD 90")); - assertThat(getPremiumPrice("genius", registry)).hasValue(Money.parse("USD 10")); - assertThat(getPremiumPrice("savant", registry)).hasValue(Money.parse("USD 90")); - assertThat(getPremiumPrice("dolt", registry)).isAbsent(); - assertThat(ofy() - .load() - .type(PremiumListEntry.class) - .parent(pl.getRevisionKey()) - .id("dolt") - .now()) - .isNull(); - assertThat(ofy() - .load() - .type(PremiumListEntry.class) - .parent(pl2.getRevisionKey()) - .id("dolt") - .now()) - .isNull(); - } - - @Test - public void testGetPremiumPrice_allLabelsAreNonPremium_whenNotInList() throws Exception { - assertThat(getPremiumPrice("blah", Registry.get("tld"))).isAbsent(); - assertThat(getPremiumPrice("slinge", Registry.get("tld"))).isAbsent(); - } - - @Test - public void testSave_simple() throws Exception { - PremiumList pl = - saveWithEntries( - new PremiumList.Builder().setName("tld2").build(), - ImmutableList.of("lol , USD 999 # yupper rooni ")); - createTld("tld"); - persistResource(Registry.get("tld").asBuilder().setPremiumList(pl).build()); - assertThat(getPremiumPrice("lol", Registry.get("tld"))).hasValue(Money.parse("USD 999")); - assertThat(getPremiumPrice("lol ", Registry.get("tld"))).isAbsent(); - Map entries = - PremiumList.get("tld2").get().loadPremiumListEntries(); - assertThat(entries.keySet()).containsExactly("lol"); - assertThat(entries).doesNotContainKey("lol "); - PremiumListEntry entry = entries.get("lol"); - assertThat(entry.comment).isEqualTo("yupper rooni"); - assertThat(entry.price).isEqualTo(Money.parse("USD 999")); - assertThat(entry.label).isEqualTo("lol"); - } - - @Test - public void test_saveAndUpdateEntriesTwice() throws Exception { - PremiumList pl = - saveWithEntries( - new PremiumList.Builder().setName("pl").build(), ImmutableList.of("test,USD 1")); - Map entries = pl.loadPremiumListEntries(); - assertThat(entries.keySet()).containsExactly("test"); - assertThat(PremiumList.get("pl").get().loadPremiumListEntries()).isEqualTo(entries); - // Save again with no changes, and clear the cache to force a re-load from Datastore. - PremiumList resaved = saveWithEntries(pl, ImmutableList.of("test,USD 1")); - ofy().clearSessionCache(); - Map entriesReloaded = - PremiumList.get("pl").get().loadPremiumListEntries(); - assertThat(entriesReloaded).hasSize(1); - assertThat(entriesReloaded).containsKey("test"); - assertThat(entriesReloaded.get("test").parent).isEqualTo(resaved.getRevisionKey()); - } - - @Test - public void testDelete() throws Exception { - persistPremiumList("gtld1", "trombone,USD 10"); - assertThat(PremiumList.get("gtld1")).isPresent(); - Key parent = PremiumList.get("gtld1").get().getRevisionKey(); - PremiumList.get("gtld1").get().delete(); - assertThat(PremiumList.get("gtld1")).isAbsent(); - assertThat(ofy().load().type(PremiumListEntry.class).ancestor(parent).list()).isEmpty(); - } - - @Test - public void testDelete_largeNumberOfEntries_succeeds() { - persistHumongousPremiumList("ginormous", 2500); - PremiumList.get("ginormous").get().delete(); - assertThat(PremiumList.get("ginormous")).isAbsent(); - } - - @Test - public void testDelete_failsWhenListDoesntExist() throws Exception { - thrown.expect(IllegalStateException.class); - PremiumList.get("tld-premium-blah").get().delete(); - } - @Test public void testSave_badSyntax() throws Exception { thrown.expect(IllegalArgumentException.class); @@ -226,32 +69,6 @@ public class PremiumListTest { persistReservedList("gtld1", "lol,XBTC 200"); } - @Test - public void testExists() throws Exception { - assertThat(PremiumList.exists("tld")).isTrue(); - assertThat(PremiumList.exists("nonExistentPremiumList")).isFalse(); - } - - @Test - public void testGetPremiumPrice_comesFromBloomFilter() throws Exception { - PremiumList pl = PremiumList.get("tld").get(); - PremiumListEntry entry = - persistResource( - new PremiumListEntry.Builder() - .setParent(pl.getRevisionKey()) - .setLabel("missingno") - .setPrice(Money.parse("USD 1000")) - .build()); - // "missingno" shouldn't be in the bloom filter, thus it should return not premium without - // attempting to load the entity that is actually present. - assertThat(getPremiumPrice("missingno", Registry.get("tld"))).isAbsent(); - // However, if we manually query the cache to force an entity load, it should be found. - assertThat( - cachePremiumListEntries.get( - Key.create(pl.getRevisionKey(), PremiumListEntry.class, "missingno"))) - .hasValue(entry); - } - @Test public void testProbablePremiumLabels() throws Exception { PremiumList pl = PremiumList.get("tld").get(); @@ -276,15 +93,6 @@ public class PremiumListTest { "lol,USD 100", "rofl,USD 90", "paper,USD 80", "wood,USD 70", "lol,USD 200")); } - /** Persists a premium list with a specified number of nonsense entries. */ - private PremiumList persistHumongousPremiumList(String name, int size) { - String[] entries = new String[size]; - for (int i = 0; i < size; i++) { - entries[i] = String.format("%d,USD 100 # blahz", i); - } - return persistPremiumList(name, entries); - } - /** Gets the label of a premium list entry. */ public static final Function, String> GET_ENTRY_NAME_FUNCTION = new Function, String>() { diff --git a/javatests/google/registry/model/registry/label/PremiumListUtilsTest.java b/javatests/google/registry/model/registry/label/PremiumListUtilsTest.java new file mode 100644 index 000000000..4b3728d12 --- /dev/null +++ b/javatests/google/registry/model/registry/label/PremiumListUtilsTest.java @@ -0,0 +1,248 @@ +// 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.model.registry.label; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.model.registry.label.PremiumList.cachePremiumListEntries; +import static google.registry.model.registry.label.PremiumListUtils.deletePremiumList; +import static google.registry.model.registry.label.PremiumListUtils.doesPremiumListExist; +import static google.registry.model.registry.label.PremiumListUtils.getPremiumPrice; +import static google.registry.model.registry.label.PremiumListUtils.loadPremiumListEntries; +import static google.registry.model.registry.label.PremiumListUtils.savePremiumListAndEntries; +import static google.registry.testing.DatastoreHelper.createTld; +import static google.registry.testing.DatastoreHelper.persistPremiumList; +import static google.registry.testing.DatastoreHelper.persistResource; + +import com.google.common.collect.ImmutableList; +import com.googlecode.objectify.Key; +import google.registry.model.pricing.StaticPremiumListPricingEngine; +import google.registry.model.registry.Registry; +import google.registry.model.registry.label.PremiumList.PremiumListEntry; +import google.registry.model.registry.label.PremiumList.PremiumListRevision; +import google.registry.testing.AppEngineRule; +import google.registry.testing.ExceptionRule; +import java.util.Map; +import org.joda.money.Money; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link PremiumListUtils}. */ +@RunWith(JUnit4.class) +public class PremiumListUtilsTest { + + @Rule public final ExceptionRule thrown = new ExceptionRule(); + @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); + + @Before + public void before() throws Exception { + // createTld() overwrites the premium list, so call it first. + createTld("tld"); + PremiumList pl = + persistPremiumList( + "tld", + "lol,USD 999 # yup", + "rich,USD 1999 #tada", + "icann,JPY 100", + "johnny-be-goode,USD 20.50"); + persistResource(Registry.get("tld").asBuilder().setPremiumList(pl).build()); + } + + @Test + public void testGetPremiumPrice_returnsNoPriceWhenNoPremiumListConfigured() throws Exception { + createTld("ghost"); + persistResource( + new Registry.Builder() + .setTldStr("ghost") + .setPremiumPricingEngine(StaticPremiumListPricingEngine.NAME) + .build()); + assertThat(Registry.get("ghost").getPremiumList()).isNull(); + assertThat(getPremiumPrice("blah", Registry.get("ghost"))).isAbsent(); + } + + @Test + public void testGetPremiumPrice_throwsExceptionWhenNonExistentPremiumListConfigured() + throws Exception { + deletePremiumList(PremiumList.get("tld").get()); + thrown.expect(IllegalStateException.class, "Could not load premium list 'tld'"); + getPremiumPrice("blah", Registry.get("tld")); + } + + @Test + public void testSave_largeNumberOfEntries_succeeds() throws Exception { + PremiumList premiumList = persistHumongousPremiumList("tld", 2500); + assertThat(loadPremiumListEntries(premiumList)).hasSize(2500); + assertThat(getPremiumPrice("7", Registry.get("tld"))).hasValue(Money.parse("USD 100")); + } + + @Test + public void testSave_updateTime_isUpdatedOnEverySave() throws Exception { + PremiumList pl = + savePremiumListAndEntries( + new PremiumList.Builder().setName("tld3").build(), ImmutableList.of("slime,USD 10")); + PremiumList newPl = + savePremiumListAndEntries( + new PremiumList.Builder().setName(pl.getName()).build(), + ImmutableList.of("mutants,USD 20")); + assertThat(newPl.getLastUpdateTime()).isGreaterThan(pl.getLastUpdateTime()); + } + + @Test + public void testSave_creationTime_onlyUpdatedOnFirstCreation() throws Exception { + PremiumList pl = persistPremiumList("tld3", "sludge,JPY 1000"); + PremiumList newPl = savePremiumListAndEntries(pl, ImmutableList.of("sleighbells,CHF 2000")); + assertThat(newPl.creationTime).isEqualTo(pl.creationTime); + } + + @Test + public void testExists() throws Exception { + assertThat(doesPremiumListExist("tld")).isTrue(); + assertThat(doesPremiumListExist("nonExistentPremiumList")).isFalse(); + } + + @Test + public void testGetPremiumPrice_comesFromBloomFilter() throws Exception { + PremiumList pl = PremiumList.get("tld").get(); + PremiumListEntry entry = + persistResource( + new PremiumListEntry.Builder() + .setParent(pl.getRevisionKey()) + .setLabel("missingno") + .setPrice(Money.parse("USD 1000")) + .build()); + // "missingno" shouldn't be in the bloom filter, thus it should return not premium without + // attempting to load the entity that is actually present. + assertThat(getPremiumPrice("missingno", Registry.get("tld"))).isAbsent(); + // However, if we manually query the cache to force an entity load, it should be found. + assertThat( + cachePremiumListEntries.get( + Key.create(pl.getRevisionKey(), PremiumListEntry.class, "missingno"))) + .hasValue(entry); + } + + + @Test + public void testSave_removedPremiumListEntries_areNoLongerInDatastore() throws Exception { + Registry registry = Registry.get("tld"); + PremiumList pl = persistPremiumList("tld", "genius,USD 10", "dolt,JPY 1000"); + assertThat(getPremiumPrice("genius", registry)).hasValue(Money.parse("USD 10")); + assertThat(getPremiumPrice("dolt", registry)).hasValue(Money.parse("JPY 1000")); + assertThat(ofy() + .load() + .type(PremiumListEntry.class) + .parent(pl.getRevisionKey()) + .id("dolt") + .now() + .price) + .isEqualTo(Money.parse("JPY 1000")); + PremiumList pl2 = + savePremiumListAndEntries(pl, ImmutableList.of("genius,USD 10", "savant,USD 90")); + assertThat(getPremiumPrice("genius", registry)).hasValue(Money.parse("USD 10")); + assertThat(getPremiumPrice("savant", registry)).hasValue(Money.parse("USD 90")); + assertThat(getPremiumPrice("dolt", registry)).isAbsent(); + assertThat(ofy() + .load() + .type(PremiumListEntry.class) + .parent(pl.getRevisionKey()) + .id("dolt") + .now()) + .isNull(); + assertThat(ofy() + .load() + .type(PremiumListEntry.class) + .parent(pl2.getRevisionKey()) + .id("dolt") + .now()) + .isNull(); + } + + @Test + public void testGetPremiumPrice_allLabelsAreNonPremium_whenNotInList() throws Exception { + assertThat(getPremiumPrice("blah", Registry.get("tld"))).isAbsent(); + assertThat(getPremiumPrice("slinge", Registry.get("tld"))).isAbsent(); + } + + @Test + public void testSave_simple() throws Exception { + PremiumList pl = + savePremiumListAndEntries( + new PremiumList.Builder().setName("tld2").build(), + ImmutableList.of("lol , USD 999 # yupper rooni ")); + createTld("tld"); + persistResource(Registry.get("tld").asBuilder().setPremiumList(pl).build()); + assertThat(getPremiumPrice("lol", Registry.get("tld"))).hasValue(Money.parse("USD 999")); + assertThat(getPremiumPrice("lol ", Registry.get("tld"))).isAbsent(); + Map entries = loadPremiumListEntries(PremiumList.get("tld2").get()); + assertThat(entries.keySet()).containsExactly("lol"); + assertThat(entries).doesNotContainKey("lol "); + PremiumListEntry entry = entries.get("lol"); + assertThat(entry.comment).isEqualTo("yupper rooni"); + assertThat(entry.price).isEqualTo(Money.parse("USD 999")); + assertThat(entry.label).isEqualTo("lol"); + } + + @Test + public void test_saveAndUpdateEntriesTwice() throws Exception { + PremiumList pl = + savePremiumListAndEntries( + new PremiumList.Builder().setName("pl").build(), ImmutableList.of("test,USD 1")); + Map entries = loadPremiumListEntries(pl); + assertThat(entries.keySet()).containsExactly("test"); + assertThat(loadPremiumListEntries(PremiumList.get("pl").get())).isEqualTo(entries); + // Save again with no changes, and clear the cache to force a re-load from Datastore. + PremiumList resaved = savePremiumListAndEntries(pl, ImmutableList.of("test,USD 1")); + ofy().clearSessionCache(); + Map entriesReloaded = + loadPremiumListEntries(PremiumList.get("pl").get()); + assertThat(entriesReloaded).hasSize(1); + assertThat(entriesReloaded).containsKey("test"); + assertThat(entriesReloaded.get("test").parent).isEqualTo(resaved.getRevisionKey()); + } + + @Test + public void testDelete() throws Exception { + persistPremiumList("gtld1", "trombone,USD 10"); + assertThat(PremiumList.get("gtld1")).isPresent(); + Key parent = PremiumList.get("gtld1").get().getRevisionKey(); + deletePremiumList(PremiumList.get("gtld1").get()); + assertThat(PremiumList.get("gtld1")).isAbsent(); + assertThat(ofy().load().type(PremiumListEntry.class).ancestor(parent).list()).isEmpty(); + } + + @Test + public void testDelete_largeNumberOfEntries_succeeds() { + persistHumongousPremiumList("ginormous", 2500); + deletePremiumList(PremiumList.get("ginormous").get()); + assertThat(PremiumList.get("ginormous")).isAbsent(); + } + + @Test + public void testDelete_failsWhenListDoesntExist() throws Exception { + thrown.expect(IllegalStateException.class); + deletePremiumList(PremiumList.get("tld-premium-blah").get()); + } + + /** Persists a premium list with a specified number of nonsense entries. */ + private PremiumList persistHumongousPremiumList(String name, int size) { + String[] entries = new String[size]; + for (int i = 0; i < size; i++) { + entries[i] = String.format("%d,USD 100 # blahz", i); + } + return persistPremiumList(name, entries); + } +} diff --git a/javatests/google/registry/testing/DatastoreHelper.java b/javatests/google/registry/testing/DatastoreHelper.java index f758b6924..6e8a0604f 100644 --- a/javatests/google/registry/testing/DatastoreHelper.java +++ b/javatests/google/registry/testing/DatastoreHelper.java @@ -27,7 +27,7 @@ import static google.registry.model.EppResourceUtils.createDomainRepoId; import static google.registry.model.EppResourceUtils.createRepoId; import static google.registry.model.domain.launch.ApplicationStatus.VALIDATED; import static google.registry.model.ofy.ObjectifyService.ofy; -import static google.registry.model.registry.label.PremiumList.parentEntriesOnRevision; +import static google.registry.model.registry.label.PremiumListUtils.parentPremiumListEntriesOnRevision; import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost; import static google.registry.util.CollectionUtils.difference; import static google.registry.util.CollectionUtils.union; @@ -365,7 +365,7 @@ public class DatastoreHelper { .now(); ofy() .saveWithoutBackup() - .entities(parentEntriesOnRevision(entries.values(), Key.create(revision))) + .entities(parentPremiumListEntriesOnRevision(entries.values(), Key.create(revision))) .now(); return ofy().load().entity(premiumList).now(); } diff --git a/javatests/google/registry/tools/DeletePremiumListCommandTest.java b/javatests/google/registry/tools/DeletePremiumListCommandTest.java index bd496d8ac..f4c191b47 100644 --- a/javatests/google/registry/tools/DeletePremiumListCommandTest.java +++ b/javatests/google/registry/tools/DeletePremiumListCommandTest.java @@ -17,6 +17,7 @@ package google.registry.tools; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.model.registry.label.PremiumListUtils.loadPremiumListEntries; import static google.registry.testing.DatastoreHelper.createTld; import static google.registry.testing.DatastoreHelper.persistPremiumList; import static google.registry.testing.DatastoreHelper.persistResource; @@ -32,7 +33,7 @@ public class DeletePremiumListCommandTest extends CommandTestCase