// Copyright 2018 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; import static com.google.common.base.Predicates.equalTo; import static com.google.common.collect.ImmutableList.toImmutableList; import static google.registry.model.eppcommon.EppXmlTransformer.unmarshal; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.util.CollectionUtils.isNullOrEmpty; import static google.registry.util.DomainNameUtils.ACE_PREFIX; import com.google.common.base.Ascii; import com.google.common.base.Predicates; import com.google.common.collect.HashMultiset; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.Multiset; import com.googlecode.objectify.Key; import com.googlecode.objectify.cmd.Query; import google.registry.model.domain.DomainCommand; import google.registry.model.domain.fee.FeeCreateCommandExtension; import google.registry.model.domain.launch.LaunchCreateExtension; import google.registry.model.domain.secdns.SecDnsCreateExtension; import google.registry.model.domain.secdns.SecDnsUpdateExtension; import google.registry.model.eppinput.EppInput; import google.registry.model.eppinput.EppInput.ResourceCommandWrapper; import google.registry.model.host.HostCommand; import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry.Type; import google.registry.xml.XmlException; import java.util.Arrays; import java.util.EnumSet; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; /** Represents stats derived from HistoryEntry objects on actions taken by registrars. */ public class OteStats { /** * Returns the statistics about the OT&E actions that have been taken by a particular registrar. */ public static OteStats getFromRegistrar(String registrarName) { return new OteStats().recordRegistrarHistory(registrarName); } private OteStats() {} private static final Predicate HAS_CLAIMS_NOTICE = eppInput -> { Optional launchCreate = eppInput.getSingleExtension(LaunchCreateExtension.class); return launchCreate.isPresent() && launchCreate.get().getNotice() != null; }; private static final Predicate HAS_SEC_DNS = eppInput -> eppInput.getSingleExtension(SecDnsCreateExtension.class).isPresent() || eppInput.getSingleExtension(SecDnsUpdateExtension.class).isPresent(); private static final Predicate IS_SUNRISE = eppInput -> { Optional launchCreate = eppInput.getSingleExtension(LaunchCreateExtension.class); return launchCreate.isPresent() && !isNullOrEmpty(launchCreate.get().getSignedMarks()); }; private static final Predicate IS_IDN = eppInput -> ((DomainCommand.Create) ((ResourceCommandWrapper) eppInput.getCommandWrapper().getCommand()) .getResourceCommand()) .getFullyQualifiedDomainName() .startsWith(ACE_PREFIX); private static final Predicate IS_SUBORDINATE = eppInput -> !isNullOrEmpty( ((HostCommand.Create) ((ResourceCommandWrapper) eppInput.getCommandWrapper().getCommand()) .getResourceCommand()) .getInetAddresses()); /** Enum defining the distinct statistics (types of registrar actions) to record. */ public enum StatType { CONTACT_CREATES(0, equalTo(Type.CONTACT_CREATE)), CONTACT_DELETES(0, equalTo(Type.CONTACT_DELETE)), CONTACT_TRANSFER_APPROVES(0, equalTo(Type.CONTACT_TRANSFER_APPROVE)), CONTACT_TRANSFER_CANCELS(0, equalTo(Type.CONTACT_TRANSFER_CANCEL)), CONTACT_TRANSFER_REJECTS(0, equalTo(Type.CONTACT_TRANSFER_REJECT)), CONTACT_TRANSFER_REQUESTS(0, equalTo(Type.CONTACT_TRANSFER_REQUEST)), CONTACT_UPDATES(0, equalTo(Type.CONTACT_UPDATE)), DOMAIN_AUTORENEWS(0, equalTo(Type.DOMAIN_AUTORENEW)), DOMAIN_CREATES(0, equalTo(Type.DOMAIN_CREATE)), DOMAIN_CREATES_ASCII(1, equalTo(Type.DOMAIN_CREATE), IS_IDN.negate()), DOMAIN_CREATES_IDN(1, equalTo(Type.DOMAIN_CREATE), IS_IDN), DOMAIN_CREATES_START_DATE_SUNRISE(1, equalTo(Type.DOMAIN_CREATE), IS_SUNRISE), DOMAIN_CREATES_WITH_CLAIMS_NOTICE(1, equalTo(Type.DOMAIN_CREATE), HAS_CLAIMS_NOTICE), DOMAIN_CREATES_WITH_FEE( 1, equalTo(Type.DOMAIN_CREATE), eppInput -> eppInput.getSingleExtension(FeeCreateCommandExtension.class).isPresent()), DOMAIN_CREATES_WITH_SEC_DNS(1, equalTo(Type.DOMAIN_CREATE), HAS_SEC_DNS), DOMAIN_CREATES_WITHOUT_SEC_DNS(0, equalTo(Type.DOMAIN_CREATE), HAS_SEC_DNS.negate()), DOMAIN_DELETES(1, equalTo(Type.DOMAIN_DELETE)), DOMAIN_RENEWS(0, equalTo(Type.DOMAIN_RENEW)), DOMAIN_RESTORES(1, equalTo(Type.DOMAIN_RESTORE)), DOMAIN_TRANSFER_APPROVES(1, equalTo(Type.DOMAIN_TRANSFER_APPROVE)), DOMAIN_TRANSFER_CANCELS(1, equalTo(Type.DOMAIN_TRANSFER_CANCEL)), DOMAIN_TRANSFER_REJECTS(1, equalTo(Type.DOMAIN_TRANSFER_REJECT)), DOMAIN_TRANSFER_REQUESTS(1, equalTo(Type.DOMAIN_TRANSFER_REQUEST)), DOMAIN_UPDATES(0, equalTo(Type.DOMAIN_UPDATE)), DOMAIN_UPDATES_WITH_SEC_DNS(1, equalTo(Type.DOMAIN_UPDATE), HAS_SEC_DNS), DOMAIN_UPDATES_WITHOUT_SEC_DNS(0, equalTo(Type.DOMAIN_UPDATE), HAS_SEC_DNS.negate()), HOST_CREATES(0, equalTo(Type.HOST_CREATE)), HOST_CREATES_EXTERNAL(0, equalTo(Type.HOST_CREATE), IS_SUBORDINATE.negate()), HOST_CREATES_SUBORDINATE(1, equalTo(Type.HOST_CREATE), IS_SUBORDINATE), HOST_DELETES(1, equalTo(Type.HOST_DELETE)), HOST_UPDATES(1, equalTo(Type.HOST_UPDATE)), UNCLASSIFIED_FLOWS(0, Predicates.alwaysFalse()); /** StatTypes with a non-zero requirement */ public static final ImmutableList REQUIRED_STAT_TYPES = Arrays.stream(values()) .filter(statType -> statType.requirement > 0) .collect(toImmutableList()); /** Required number of times registrars must complete this action. */ private final int requirement; /** Filter to check the HistoryEntry Type */ @SuppressWarnings("ImmutableEnumChecker") // Predicates are immutable. private final Predicate typeFilter; /** Optional filter on the EppInput. */ @SuppressWarnings("ImmutableEnumChecker") // Predicates are immutable. private final Optional> eppInputFilter; StatType(int requirement, Predicate typeFilter) { this(requirement, typeFilter, null); } StatType( int requirement, Predicate typeFilter, Predicate eppInputFilter) { this.requirement = requirement; this.typeFilter = typeFilter; if (eppInputFilter == null) { this.eppInputFilter = Optional.empty(); } else { this.eppInputFilter = Optional.of(eppInputFilter); } } /** Returns the number of times this StatType must be performed. */ public int getRequirement() { return requirement; } /** Returns a more human-readable translation of the enum constant. */ public String getDescription() { return Ascii.toLowerCase(this.name().replace('_', ' ')); } /** * Check if the {@link HistoryEntry} type matches as well as the {@link EppInput} if supplied. */ private boolean matches(HistoryEntry.Type historyType, Optional eppInput) { if (eppInputFilter.isPresent() && eppInput.isPresent()) { return typeFilter.test(historyType) && eppInputFilter.get().test(eppInput.get()); } else { return typeFilter.test(historyType); } } } /** Stores counts of how many times each action type was performed. */ private final Multiset statCounts = HashMultiset.create(); /** * Records data on what actions have been performed by the four numbered OT&E variants of the * registrar name. * *

Stops when it notices that all tests have passed. */ private OteStats recordRegistrarHistory(String registrarName) { ImmutableCollection clientIds = OteAccountBuilder.createClientIdToTldMap(registrarName).keySet(); Query query = ofy() .load() .type(HistoryEntry.class) .filter("clientId in", clientIds) .order("modificationTime"); for (HistoryEntry historyEntry : query) { try { record(historyEntry); } catch (XmlException e) { throw new RuntimeException("Couldn't parse history entry " + Key.create(historyEntry), e); } // Break out early if all tests were passed. if (wereAllTestsPassed()) { break; } } return this; } /** Interprets the data in the provided HistoryEntry and increments counters. */ private void record(final HistoryEntry historyEntry) throws XmlException { byte[] xmlBytes = historyEntry.getXmlBytes(); // xmlBytes can be null on contact create and update for safe-harbor compliance. final Optional eppInput = (xmlBytes == null) ? Optional.empty() : Optional.of(unmarshal(EppInput.class, xmlBytes)); if (!statCounts.addAll( EnumSet.allOf(StatType.class).stream() .filter(statType -> statType.matches(historyEntry.getType(), eppInput)) .collect(toImmutableList()))) { statCounts.add(StatType.UNCLASSIFIED_FLOWS); } } private boolean wereAllTestsPassed() { return Arrays.stream(StatType.values()).allMatch(s -> statCounts.count(s) >= s.requirement); } /** Returns the total number of actions taken */ public int getSize() { return statCounts.size(); } /** Returns the number of times that a particular StatType was seen */ public int getCount(StatType statType) { return statCounts.count(statType); } /** * Returns a list of failures, any cases where the passed stats fail to meet the required * thresholds, or the empty list if all requirements are met. */ public ImmutableList getFailures() { return StatType.REQUIRED_STAT_TYPES.stream() .filter(statType -> statCounts.count(statType) < statType.requirement) .collect(toImmutableList()); } /** Returns a string showing all possible actions and how many times each was performed. */ @Override public String toString() { return String.format( "%s\nTOTAL: %d", EnumSet.allOf(StatType.class).stream() .map(stat -> String.format("%s: %d", stat.getDescription(), statCounts.count(stat))) .collect(Collectors.joining("\n")), statCounts.size()); } }