google.registry.model.registry.label.PremiumList.java Source code

Java tutorial

Introduction

Here is the source code for google.registry.model.registry.label.PremiumList.java

Source

// Copyright 2016 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.appengine.api.datastore.DatastoreServiceFactory.getDatastoreService;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.partition;
import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.allocateId;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.ofy.Ofy.RECOMMENDED_MEMCACHE_EXPIRATION;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

import com.google.appengine.api.datastore.EntityNotFoundException;
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.ImmutableMap;
import com.google.common.collect.Maps;
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;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.OnLoad;
import com.googlecode.objectify.annotation.Parent;
import com.googlecode.objectify.cmd.Query;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.annotations.VirtualEntity;
import google.registry.model.registry.Registry;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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.
 */
@ReportedOn
@Entity
@Cache(expirationSeconds = RECOMMENDED_MEMCACHE_EXPIRATION)
public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.PremiumListEntry> {

    /** 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<PremiumListRevision> revisionKey;

    @Ignore
    Map<String, PremiumListEntry> premiumListMap;

    /** Virtual parent entity for premium list entry entities associated with a single revision. */
    @Entity
    @VirtualEntity
    public static class PremiumListRevision extends ImmutableObject {
        @Parent
        Key<PremiumList> parent;

        @Id
        long revisionId;

        static Key<PremiumListRevision> createKey(PremiumList parent) {
            PremiumListRevision revision = new PremiumListRevision();
            revision.parent = Key.create(parent);
            revision.revisionId = allocateId();
            return Key.create(revision);
        }
    }

    private static LoadingCache<String, PremiumList> cache = CacheBuilder.newBuilder()
            .expireAfterWrite(getDomainLabelListCacheDuration().getMillis(), MILLISECONDS)
            .build(new CacheLoader<String, PremiumList>() {
                @Override
                public PremiumList load(final String listName) {
                    return ofy().doTransactionless(new Work<PremiumList>() {
                        @Override
                        public PremiumList run() {
                            return ofy().load().type(PremiumList.class).parent(getCrossTldKey()).id(listName).now();
                        }
                    });
                }
            });

    /**
     * Gets the premium price for the specified label on the specified tld, or returns Optional.absent
     * if there is no premium price.
     */
    public static Optional<Money> getPremiumPrice(String label, String tld) {
        Registry registry = Registry.get(checkNotNull(tld, "tld"));
        if (registry.getPremiumList() == null) {
            return Optional.<Money>absent();
        }
        String listName = registry.getPremiumList().getName();
        Optional<PremiumList> premiumList = get(listName);
        if (!premiumList.isPresent()) {
            throw new IllegalStateException("Could not load premium list named " + listName);
        }
        return premiumList.get().getPremiumPrice(label);
    }

    @OnLoad
    private void loadPremiumListMap() {
        try {
            ImmutableMap.Builder<String, PremiumListEntry> entriesMap = new ImmutableMap.Builder<>();
            if (revisionKey != null) {
                for (PremiumListEntry entry : loadEntriesForCurrentRevision()) {
                    entriesMap.put(entry.getLabel(), entry);
                }
            }
            premiumListMap = entriesMap.build();
        } catch (Exception e) {
            throw new RuntimeException("Could not retrieve entries for premium list " + name, e);
        }
    }

    /**
     * Gets the premium price for the specified label in the current PremiumList, or returns
     * Optional.absent if there is no premium price.
     */
    public Optional<Money> getPremiumPrice(String label) {
        return Optional
                .fromNullable(premiumListMap.containsKey(label) ? premiumListMap.get(label).getValue() : null);
    }

    public Map<String, PremiumListEntry> getPremiumListEntries() {
        return nullToEmptyImmutableCopy(premiumListMap);
    }

    public Key<PremiumListRevision> getRevisionKey() {
        return revisionKey;
    }

    /** Returns the PremiumList with the specified name. */
    public static Optional<PremiumList> get(String name) {
        try {
            return Optional.of(cache.get(name));
        } catch (InvalidCacheLoadException e) {
            return Optional.<PremiumList>absent();
        } catch (ExecutionException e) {
            throw new UncheckedExecutionException("Could not retrieve premium list named " + name, e);
        }
    }

    /**
     * Returns whether a PremiumList of the given name exists, without going through the overhead
     * of loading up all of the premium list entities. Also does not hit the cache.
     */
    public static boolean exists(String name) {
        try {
            // Use DatastoreService to bypass the @OnLoad method that loads the premium list entries.
            getDatastoreService().get(Key.create(getCrossTldKey(), PremiumList.class, name).getRaw());
            return true;
        } catch (EntityNotFoundException e) {
            return false;
        }
    }

    /**
     * A premium list entry entity, persisted to Datastore.  Each instance represents the price of a
     * single label on a given TLD.
     */
    @ReportedOn
    @Entity
    @Cache(expirationSeconds = RECOMMENDED_MEMCACHE_EXPIRATION)
    public static class PremiumListEntry extends DomainLabelEntry<Money, PremiumListEntry> implements Buildable {

        @Parent
        Key<PremiumListRevision> parent;

        Money price;

        @Override
        public Money getValue() {
            return price;
        }

        @Override
        public Builder asBuilder() {
            return new Builder(clone(this));
        }

        /**
         * A builder for constructing {@link PremiumList} objects, since they are immutable.
         */
        public static class Builder extends DomainLabelEntry.Builder<PremiumListEntry, Builder> {
            public Builder() {
            }

            private Builder(PremiumListEntry instance) {
                super(instance);
            }

            public Builder setParent(Key<PremiumListRevision> parentKey) {
                getInstance().parent = parentKey;
                return this;
            }

            public Builder setPrice(Money price) {
                getInstance().price = price;
                return this;
            }
        }
    }

    @Override
    @Nullable
    PremiumListEntry createFromLine(String originalLine) {
        List<String> lineAndComment = splitOnComment(originalLine);
        if (lineAndComment.isEmpty()) {
            return null;
        }
        String line = lineAndComment.get(0);
        String comment = lineAndComment.get(1);
        List<String> parts = Splitter.on(',').trimResults().splitToList(line);
        checkArgument(parts.size() == 2, "Could not parse line in premium list: %s", originalLine);
        return new PremiumListEntry.Builder().setLabel(parts.get(0)).setPrice(Money.parse(parts.get(1)))
                .setComment(comment).build();
    }

    /**
     * Persists a PremiumList object to Datastore.
     *
     * <p> 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.
     */
    public PremiumList saveAndUpdateEntries() {
        final Optional<PremiumList> oldPremiumList = get(name);
        // Only update entries if there's actually a new revision of the list to save (which there will
        // be if the list content changes, vs just the description/metadata).
        boolean entriesToUpdate = !oldPremiumList.isPresent()
                || !Objects.equals(oldPremiumList.get().revisionKey, this.revisionKey);
        // If needed, save the new child entities in a series of transactions.
        if (entriesToUpdate) {
            for (final List<PremiumListEntry> batch : partition(premiumListMap.values(), TRANSACTION_BATCH_SIZE)) {
                ofy().transactNew(new VoidWork() {
                    @Override
                    public void vrun() {
                        ofy().save().entities(batch);
                    }
                });
            }
        }
        // Save the new PremiumList itself.
        PremiumList updated = ofy().transactNew(new Work<PremiumList>() {
            @Override
            public PremiumList run() {
                DateTime now = ofy().getTransactionTime();
                // Assert that the premium list hasn't been changed since we started this process.
                checkState(
                        Objects.equals(ofy().load().type(PremiumList.class).parent(getCrossTldKey()).id(name).now(),
                                oldPremiumList.orNull()),
                        "PremiumList was concurrently edited");
                PremiumList newList = PremiumList.this.asBuilder().setLastUpdateTime(now)
                        .setCreationTime(oldPremiumList.isPresent() ? oldPremiumList.get().creationTime : now)
                        .build();
                ofy().save().entity(newList);
                return newList;
            }
        });
        // Update the cache.
        PremiumList.cache.put(name, updated);
        // If needed and there are any, delete the entities under the old PremiumList.
        if (entriesToUpdate && oldPremiumList.isPresent()) {
            oldPremiumList.get().deleteEntries();
        }
        return updated;
    }

    @Override
    public boolean refersToKey(Registry registry, Key<? extends BaseDomainLabelList<?, ?>> 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);
            }
        });
        deleteEntries();
        cache.invalidate(name);
    }

    private void deleteEntries() {
        if (revisionKey == null) {
            return;
        }
        for (final List<Key<PremiumListEntry>> batch : partition(loadEntriesForCurrentRevision().keys(),
                TRANSACTION_BATCH_SIZE)) {
            ofy().transactNew(new VoidWork() {
                @Override
                public void vrun() {
                    ofy().delete().keys(batch);
                }
            });
        }
    }

    private Query<PremiumListEntry> loadEntriesForCurrentRevision() {
        return ofy().load().type(PremiumListEntry.class).ancestor(revisionKey);
    }

    @Override
    public Builder asBuilder() {
        return new Builder(clone(this));
    }

    /** A builder for constructing {@link PremiumList} objects, since they are immutable.  */
    public static class Builder extends BaseDomainLabelList.Builder<PremiumList, Builder> {

        public Builder() {
        }

        private Builder(PremiumList instance) {
            super(instance);
        }

        private boolean entriesWereUpdated;

        public Builder setPremiumListMap(ImmutableMap<String, PremiumListEntry> premiumListMap) {
            entriesWereUpdated = true;
            getInstance().premiumListMap = premiumListMap;
            return this;
        }

        /** Updates the premiumListMap from input lines. */
        public Builder setPremiumListMapFromLines(Iterable<String> lines) {
            return setPremiumListMap(getInstance().parse(lines));
        }

        @Override
        public PremiumList build() {
            final PremiumList instance = getInstance();
            if (getInstance().revisionKey == null || entriesWereUpdated) {
                getInstance().revisionKey = PremiumListRevision.createKey(instance);
            }
            // When we build an instance, make sure all entries are parented on its revisionKey.
            instance.premiumListMap = Maps.transformValues(nullToEmpty(instance.premiumListMap),
                    new Function<PremiumListEntry, PremiumListEntry>() {
                        @Override
                        public PremiumListEntry apply(PremiumListEntry entry) {
                            return entry.asBuilder().setParent(instance.revisionKey).build();
                        }
                    });
            return super.build();
        }
    }
}