// 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.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static google.registry.model.common.EntityGroupRoot.getCrossTldKey; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.ofy.Ofy.RECOMMENDED_MEMCACHE_EXPIRATION; import static google.registry.model.registry.label.ReservationType.FULLY_BLOCKED; import static google.registry.model.registry.label.ReservationType.RESERVED_FOR_ANCHOR_TENANT; import static google.registry.model.registry.label.ReservationType.UNRESERVED; import static google.registry.util.CollectionUtils.nullToEmpty; import static java.util.concurrent.TimeUnit.MILLISECONDS; 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.LoadingCache; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.net.InternetDomainName; import com.google.common.util.concurrent.UncheckedExecutionException; import com.googlecode.objectify.Key; import com.googlecode.objectify.VoidWork; import com.googlecode.objectify.annotation.Cache; import com.googlecode.objectify.annotation.Embed; import com.googlecode.objectify.annotation.Entity; import com.googlecode.objectify.annotation.Mapify; import com.googlecode.objectify.mapper.Mapper; import google.registry.config.RegistryEnvironment; 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; /** * A reserved list entity, persisted to Datastore, that is used to check domain label reservations. */ @Entity @Cache(expirationSeconds = RECOMMENDED_MEMCACHE_EXPIRATION) public final class ReservedList extends BaseDomainLabelList { @Mapify(ReservedListEntry.LabelMapper.class) Map reservedListMap; boolean shouldPublish = true; /** * A reserved list entry entity, persisted to Datastore, that represents a single label and its * reservation type. */ @Embed public static class ReservedListEntry extends DomainLabelEntry { ReservationType reservationType; /** * Contains the auth code necessary to register a domain with this label. * Note that this field will only ever be populated for entries with type * RESERVED_FOR_ANCHOR_TENANT. */ String authCode; /** Mapper for use with @Mapify */ static class LabelMapper implements Mapper { @Override public String getKey(ReservedListEntry entry) { return entry.getLabel(); } } public static ReservedListEntry create( String label, ReservationType reservationType, @Nullable String authCode, String comment) { if (authCode != null) { checkArgument(reservationType == RESERVED_FOR_ANCHOR_TENANT, "Only anchor tenant reservations should have an auth code configured"); } else { checkArgument(reservationType != RESERVED_FOR_ANCHOR_TENANT, "Anchor tenant reservations must have an auth code configured"); } ReservedListEntry entry = new ReservedListEntry(); entry.label = label; entry.reservationType = reservationType; entry.authCode = authCode; entry.comment = comment; return entry; } @Override public ReservationType getValue() { return reservationType; } public String getAuthCode() { return authCode; } } @Override protected boolean refersToKey(Registry registry, Key> key) { return registry.getReservedLists().contains(key); } /** Determines whether the ReservedList is in use on any Registry */ public boolean isInUse() { return !getReferencingTlds().isEmpty(); } /** * Returns whether this reserved list is included in the concatenated list of reserved terms * published to Google Drive for viewing by registrars. */ public boolean getShouldPublish() { return shouldPublish; } public ImmutableMap getReservedListEntries() { return ImmutableMap.copyOf(nullToEmpty(reservedListMap)); } /** * Gets a ReservedList by name using the caching layer. * * @return An Optional that has a value if a reserved list exists by the given * name, or absent if not. * @throws UncheckedExecutionException if some other error occurs while trying to load the * ReservedList from the cache or Datastore. */ public static Optional get(String listName) { return getFromCache(listName, cache); } /** Loads a ReservedList from its Objectify key. */ public static Optional load(Key key) { return get(key.getName()); } /** * Queries the set of all reserved lists associated with the specified tld and returns the * reservation type of the first occurrence of label seen. If the label is in none of the lists, * it returns UNRESERVED. */ public static ReservationType getReservation(String label, String tld) { checkNotNull(label, "label"); if (label.length() == 0) { return FULLY_BLOCKED; } ReservedListEntry entry = getReservedListEntry(label, tld); return (entry != null) ? entry.reservationType : UNRESERVED; } /** * Returns true if the given label and TLD is reserved for an anchor tenant, and the given * auth code matches the one set on the reservation. */ public static boolean matchesAnchorTenantReservation( InternetDomainName domainName, String authCode) { ReservedListEntry entry = getReservedListEntry(domainName.parts().get(0), domainName.parent().toString()); return entry != null && entry.reservationType == RESERVED_FOR_ANCHOR_TENANT && Objects.equals(entry.getAuthCode(), authCode); } /** * Helper function to retrieve the entry associated with this label and TLD, or null if no such * entry exists. */ @Nullable private static ReservedListEntry getReservedListEntry(String label, String tld) { Registry registry = Registry.get(checkNotNull(tld, "tld")); ImmutableSet> reservedLists = registry.getReservedLists(); ImmutableSet lists = loadReservedLists(reservedLists); ReservedListEntry entry = null; // Loop through all reservation lists and check each one for the inputted label, and return // the most severe ReservationType found. for (ReservedList rl : lists) { Map entries = rl.getReservedListEntries(); ReservedListEntry nextEntry = entries.get(label); if (nextEntry != null && (entry == null || nextEntry.reservationType.compareTo(entry.reservationType) > 0)) { entry = nextEntry; } } return entry; } private static ImmutableSet loadReservedLists( ImmutableSet> reservedListKeys) { ImmutableSet.Builder builder = new ImmutableSet.Builder<>(); for (Key listKey : reservedListKeys) { try { builder.add(cache.get(listKey.getName())); } catch (ExecutionException e) { throw new UncheckedExecutionException(String.format( "Could not load the reserved list '%s' from the cache", listKey.getName()), e); } } return builder.build(); } private static LoadingCache cache = CacheBuilder .newBuilder() .expireAfterWrite( RegistryEnvironment.get().config().getDomainLabelListCacheDuration().getMillis(), MILLISECONDS) .build(new CacheLoader() { @Override public ReservedList load(String listName) { return ofy().load().type(ReservedList.class).parent(getCrossTldKey()).id(listName).now(); }}); /** Deletes the ReservedList with the given name. */ public static void delete(final String listName) { final ReservedList reservedList = ReservedList.get(listName).orNull(); checkState( reservedList != null, "Attempted to delete reserved list %s which doesn't exist", listName); ofy().transactNew(new VoidWork() { @Override public void vrun() { ofy().delete().entity(reservedList).now(); } }); cache.invalidate(listName); } /** * Gets the {@link ReservationType} of a label in a single ReservedList, or returns an absent * Optional if none exists in the list. * *

Note that this logic is significantly less complicated than the getReservation() methods, * which are applicable to an entire Registry, and need to check across multiple reserved lists. */ public Optional getReservationInList(String label) { ReservedListEntry entry = getReservedListEntries().get(label); return Optional.fromNullable(entry == null ? null : entry.reservationType); } @Override @Nullable ReservedListEntry createFromLine(String originalLine) { List lineAndComment = splitOnComment(originalLine); if (lineAndComment.isEmpty()) { return null; } String line = lineAndComment.get(0); String comment = lineAndComment.get(1); List parts = Splitter.on(',').trimResults().splitToList(line); checkArgument(parts.size() == 2 || parts.size() == 3, "Could not parse line in reserved list: %s", originalLine); String label = parts.get(0); ReservationType reservationType = ReservationType.valueOf(parts.get(1)); String authCode = (parts.size() > 2) ? parts.get(2) : null; return ReservedListEntry.create(label, reservationType, authCode, comment); } @Override public Builder asBuilder() { return new Builder(clone(this)); } /** * A builder for constructing {@link ReservedList} objects, since they are immutable. */ public static class Builder extends BaseDomainLabelList.Builder { public Builder() {} private Builder(ReservedList instance) { super(instance); } public Builder setReservedListMap(ImmutableMap reservedListMap) { getInstance().reservedListMap = reservedListMap; return this; } public Builder setShouldPublish(boolean shouldPublish) { getInstance().shouldPublish = shouldPublish; return this; } /** * Updates the reservedListMap from input lines. * * @throws IllegalArgumentException if the lines cannot be parsed correctly. */ public Builder setReservedListMapFromLines(Iterable lines) { return setReservedListMap(getInstance().parse(lines)); } } }