diff --git a/java/google/registry/flows/domain/DomainInfoFlow.java b/java/google/registry/flows/domain/DomainInfoFlow.java index 8af0b758f..907e3d01c 100644 --- a/java/google/registry/flows/domain/DomainInfoFlow.java +++ b/java/google/registry/flows/domain/DomainInfoFlow.java @@ -107,7 +107,7 @@ public class DomainInfoFlow extends BaseDomainInfoFlow Optional extraLogicManager = RegistryExtraFlowLogicProxy.newInstanceForTld(existingResource.getTld()); if (extraLogicManager.isPresent()) { - List flags = extraLogicManager.get().getFlags( + List flags = extraLogicManager.get().getExtensionFlags( existingResource, this.getClientId(), now); // As-of date is always now for info commands. if (!flags.isEmpty()) { extensions.add(FlagsInfoResponseExtension.create(flags)); diff --git a/java/google/registry/flows/domain/RegistryExtraFlowLogic.java b/java/google/registry/flows/domain/RegistryExtraFlowLogic.java index 707d50fb5..a3899b5ec 100644 --- a/java/google/registry/flows/domain/RegistryExtraFlowLogic.java +++ b/java/google/registry/flows/domain/RegistryExtraFlowLogic.java @@ -26,8 +26,8 @@ import org.joda.time.DateTime; */ public interface RegistryExtraFlowLogic { - /** Get the flags to be passed to the client in the EPP flags extension. */ - public List getFlags( + /** Get the flags to be used in the EPP flags extension. This is used for EPP info commands. */ + public List getExtensionFlags( DomainResource domainResource, String clientIdentifier, DateTime asOfDate); /** diff --git a/java/google/registry/model/common/TimedTransitionProperty.java b/java/google/registry/model/common/TimedTransitionProperty.java index d98f17614..0f8243f97 100644 --- a/java/google/registry/model/common/TimedTransitionProperty.java +++ b/java/google/registry/model/common/TimedTransitionProperty.java @@ -16,17 +16,22 @@ package google.registry.model.common; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; +import static google.registry.util.CollectionUtils.nullToEmpty; import static google.registry.util.DateTimeUtils.START_OF_TIME; import static google.registry.util.DateTimeUtils.latestOf; import com.google.common.base.Function; import com.google.common.collect.ForwardingMap; +import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.googlecode.objectify.mapper.Mapper; import google.registry.model.ImmutableObject; import google.registry.util.TypeUtils; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; import javax.annotation.Nullable; @@ -136,6 +141,99 @@ public class TimedTransitionProperty(backingMap.headMap(asOfDate, false)); } + /** + * Returns a new immutable {@code TimedTransitionProperty} containing the same transitions as the + * current object, plus the additional specified transition. + * + * @param transitionTime the time of the new transition + * @param transitionValue the value of the new transition + * @param transitionClass the class of transitions in this map + * @param allowedTransitions map of all possible state-to-state transitions + * @param allowedTransitionMapName transition map description string for error messages + */ + public TimedTransitionProperty copyWithAddedTransition( + DateTime transitionTime, + V transitionValue, + Class transitionClass, + ImmutableMultimap allowedTransitions, + String allowedTransitionMapName) { + ImmutableSortedMap currentMap = toValueMap(); + checkArgument( + transitionTime.isAfter(currentMap.lastKey()), + "New transitions can only be appended after the last previous transition."); + Map newInnerMap = new HashMap<>(currentMap); + newInnerMap.put(transitionTime, transitionValue); + ImmutableSortedMap newMap = + ImmutableSortedMap.copyOf(newInnerMap); + validateTimedTransitionMap(newMap, allowedTransitions, allowedTransitionMapName); + return fromValueMap(newMap, transitionClass); + } + + /** + * Validates a new set of transitions and returns the resulting TimedTransitionProperty map. + * + * @param newTransitions map from date time to transition value + * @param transitionClass the class of transitions in this map + * @param allowedTransitions optional map of all possible state-to-state transitions + * @param allowedTransitionMapName optional transition map description string for error messages + * @param initialValue optional initial value; if present, the first transition must have this + * value + * @param badInitialValueErrorMessage option error message string if the initial value is wrong + */ + public static > + TimedTransitionProperty make( + ImmutableSortedMap newTransitions, + Class transitionClass, + ImmutableMultimap allowedTransitions, + String allowedTransitionMapName, + V initialValue, + String badInitialValueErrorMessage) { + validateTimedTransitionMap( + newTransitions, + allowedTransitions, + allowedTransitionMapName); + checkArgument( + newTransitions.firstEntry().getValue() == initialValue, + badInitialValueErrorMessage); + return fromValueMap(newTransitions, transitionClass); + } + + /** + * Validates that a transition map is not null or empty, starts at START_OF_TIME, and has + * transitions which move from one value to another in allowed ways. + */ + public static > + void validateTimedTransitionMap( + @Nullable NavigableMap transitionMap, + ImmutableMultimap allowedTransitions, + String mapName) { + checkArgument( + !nullToEmpty(transitionMap).isEmpty(), "%s map cannot be null or empty.", mapName); + checkArgument( + transitionMap.firstKey().equals(START_OF_TIME), + "%s map must start at START_OF_TIME.", + mapName); + + // Check that all transitions between states are allowed. + Iterator it = transitionMap.values().iterator(); + V currentState = it.next(); + while (it.hasNext()) { + checkArgument( + allowedTransitions.containsKey(currentState), + "%s map cannot transition from %s.", + mapName, + currentState); + V nextState = it.next(); + checkArgument( + allowedTransitions.containsEntry(currentState, nextState), + "%s map cannot transition from %s to %s.", + mapName, + currentState, + nextState); + currentState = nextState; + } + } + /** * Returns a new mutable {@code TimedTransitionProperty} representing the given map of DateTime * to value, with transitions constructed using the given {@code TimedTransition} subclass. diff --git a/javatests/google/registry/flows/BUILD b/javatests/google/registry/flows/BUILD index f0b2de9f0..b1db6414c 100644 --- a/javatests/google/registry/flows/BUILD +++ b/javatests/google/registry/flows/BUILD @@ -25,6 +25,7 @@ java_library( ]), resources = glob(["**/testdata/*.xml"]), deps = [ + "//java/com/google/common/annotations", "//java/com/google/common/base", "//java/com/google/common/collect", "//java/com/google/common/io", diff --git a/javatests/google/registry/model/domain/TestExtraLogicManager.java b/javatests/google/registry/model/domain/TestExtraLogicManager.java index 0a5621c2e..146162364 100644 --- a/javatests/google/registry/model/domain/TestExtraLogicManager.java +++ b/javatests/google/registry/model/domain/TestExtraLogicManager.java @@ -30,7 +30,7 @@ import org.joda.time.DateTime; public class TestExtraLogicManager implements RegistryExtraFlowLogic { @Override - public List getFlags( + public List getExtensionFlags( DomainResource domainResource, String clientIdentifier, DateTime asOfDate) { // Take the part before the period, split by dashes, and treat each part after the first as // a flag.