google.registry.model.common.TimedTransitionProperty.java Source code

Java tutorial

Introduction

Here is the source code for google.registry.model.common.TimedTransitionProperty.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.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;
import org.joda.time.DateTime;

/**
 * An entity property whose value transitions over time.  Each value it takes on becomes active
 * at a corresponding instant, and remains active until the next transition occurs.  At least one
 * "start of time" value (corresponding to START_OF_TIME, i.e. the Unix epoch) must be provided
 * so that the property will have a value for all possible times.
 *
 * <p>This concept is naturally represented by a sorted map of {@code DateTime} to {@code V}, but
 * the AppEngine datastore cannot natively represent a map keyed on non-strings.  Instead, we store
 * an ordered list of transitions and use Objectify's @Mapify annotation to automatically recreate
 * the sorted map on load from the datastore, which is used as a backing map for this property; the
 * property itself also implements Map by way of extending ForwardingMap, so that this property can
 * stored directly as the @Mapify field in the entity.
 *
 * <p>The type parameter {@code T} specifies a user-defined subclass of {@code TimedTransition<V>}
 * to use for storing the list of transitions.  The user is given this choice of subclass so that
 * the field of the value type stored in the transition can be given a customized name.
 */
public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedTransition<V>>
        extends ForwardingMap<DateTime, T> {

    /**
     * A transition to a value of type {@code V} at a certain time.  This superclass only has a field
     * for the {@code DateTime}, which means that subclasses should supply the field of type {@code V}
     * and implementations of the abstract getter and setter methods to access that field. This design
     * is so that subclasses tagged with @Embed can define a custom field name for their value, for
     * the purpose of backwards compatibility and better readability of the datastore representation.
     *
     * <p>The public visibility of this class exists only so that it can be subclassed; clients should
     * never call any methods on this class or attempt to access its members, but should instead treat
     * it as a customizable implementation detail of {@code TimedTransitionProperty}.  However, note
     * that subclasses must also have public visibility so that they can be instantiated via
     * reflection in a call to {@code fromValueMap}.
     */
    public abstract static class TimedTransition<V> extends ImmutableObject {
        /** The time at which this value becomes the active value. */
        private DateTime transitionTime;

        /** Returns the value that this transition will activate. */
        protected abstract V getValue();

        /** Sets the value that will be activated at this transition's time. */
        protected abstract void setValue(V value);
    }

    /** Mapper used with @Mapify extracting time from TimedTransition to use as key. */
    public static class TimeMapper implements Mapper<DateTime, TimedTransition<?>> {
        @Override
        public DateTime getKey(TimedTransition<?> transition) {
            return transition.transitionTime;
        }
    }

    /**
     * Converts the provided value map into the equivalent transition map, using transition objects
     * of the given TimedTransition subclass.  The value map must be sorted according to the natural
     * ordering of its DateTime keys, and keys cannot be earlier than START_OF_TIME.
     */
    // NB: The Class<T> parameter could be eliminated by getting the class via reflection, but then
    // the callsite cannot infer T, so unless you explicitly call this as .<V, T>fromValueMap() it
    // will default to using just TimedTransition<V>, which fails at runtime.
    private static <V, T extends TimedTransition<V>> NavigableMap<DateTime, T> makeTransitionMap(
            ImmutableSortedMap<DateTime, V> valueMap, final Class<T> timedTransitionSubclass) {
        checkArgument(Ordering.natural().equals(valueMap.comparator()),
                "Timed transition value map must have transition time keys in chronological order");
        return Maps.transformEntries(valueMap, new Maps.EntryTransformer<DateTime, V, T>() {
            // For each entry in the input value map, make the output map have an entry at the
            // corresponding time that points to a transition containing that time and that value.
            @Override
            public T transformEntry(DateTime transitionTime, V value) {
                checkArgument(!transitionTime.isBefore(START_OF_TIME),
                        "Timed transition times cannot be earlier than START_OF_TIME / Unix Epoch");
                T subclass = TypeUtils.instantiate(timedTransitionSubclass);
                ((TimedTransition<V>) subclass).transitionTime = transitionTime;
                subclass.setValue(value);
                return subclass;
            }
        });
    }

    /**
     * Returns a new immutable {@code TimedTransitionProperty} representing the given map of DateTime
     * to value, with transitions constructed using the given {@code TimedTransition} subclass.
     *
     * <p>This method should be the normal method for constructing a {@link TimedTransitionProperty}.
     */
    public static <V, T extends TimedTransition<V>> TimedTransitionProperty<V, T> fromValueMap(
            ImmutableSortedMap<DateTime, V> valueMap, final Class<T> timedTransitionSubclass) {
        return new TimedTransitionProperty<>(
                ImmutableSortedMap.copyOf(makeTransitionMap(valueMap, timedTransitionSubclass)));
    }

    /**
     * Returns a new immutable {@code TimedTransitionProperty} containing the same transitions as the
     * current object, up to but not including the desired date. All transitions on or after that date
     * will be deleted.
     *
     * @param asOfDate the date before which transitions should be retained
     */
    public TimedTransitionProperty<V, T> copyUntilJustBefore(DateTime asOfDate) {
        return new 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<V, T> copyWithAddedTransition(DateTime transitionTime, V transitionValue,
            Class<T> transitionClass, ImmutableMultimap<V, V> allowedTransitions, String allowedTransitionMapName) {
        ImmutableSortedMap<DateTime, V> currentMap = toValueMap();
        checkArgument(transitionTime.isAfter(currentMap.lastKey()),
                "New transitions can only be appended after the last previous transition.");
        Map<DateTime, V> newInnerMap = new HashMap<>(currentMap);
        newInnerMap.put(transitionTime, transitionValue);
        ImmutableSortedMap<DateTime, V> newMap = ImmutableSortedMap.<DateTime, V>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 <V, T extends TimedTransitionProperty.TimedTransition<V>> TimedTransitionProperty<V, T> make(
            ImmutableSortedMap<DateTime, V> newTransitions, Class<T> transitionClass,
            ImmutableMultimap<V, V> 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 <V, T extends TimedTransitionProperty.TimedTransition<V>> void validateTimedTransitionMap(
            @Nullable NavigableMap<DateTime, V> transitionMap, ImmutableMultimap<V, V> 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<V> 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.
     *
     * <p>This method should only be used for initializing fields that are declared with the
     * @Mapify annotation. The map for those fields must be mutable so that Objectify can load values
     * from the datastore into the map, but clients should still never mutate the field's map
     * directly.
     */
    public static <V, T extends TimedTransition<V>> TimedTransitionProperty<V, T> forMapify(
            ImmutableSortedMap<DateTime, V> valueMap, Class<T> timedTransitionSubclass) {
        return new TimedTransitionProperty<>(new TreeMap<>(makeTransitionMap(valueMap, timedTransitionSubclass)));
    }

    /**
     * Returns a new mutable {@code TimedTransitionProperty} representing the given value being set at
     * start of time, constructed using the given {@code TimedTransition} subclass.
     *
     * <p>This method should only be used for initializing fields that are declared with the
     * @Mapify annotation. The map for those fields must be mutable so that Objectify can load values
     * from the datastore into the map, but clients should still never mutate the field's map
     * directly.
     */
    public static <V, T extends TimedTransition<V>> TimedTransitionProperty<V, T> forMapify(V valueAtStartOfTime,
            Class<T> timedTransitionSubclass) {
        return forMapify(ImmutableSortedMap.of(START_OF_TIME, valueAtStartOfTime), timedTransitionSubclass);
    }

    /** The backing map of DateTime to TimedTransition subclass used to store the transitions. */
    private final NavigableMap<DateTime, T> backingMap;

    /** Returns a new {@code TimedTransitionProperty} backed by the provided map instance. */
    private TimedTransitionProperty(NavigableMap<DateTime, T> backingMap) {
        checkArgument(backingMap.get(START_OF_TIME) != null,
                "Must provide transition entry for the start of time (Unix Epoch)");
        this.backingMap = backingMap;
    }

    /**
     * Checks whether this TimedTransitionProperty is in a valid state, i.e. whether it has a
     * transition entry for START_OF_TIME, and throws IllegalStateException if not.
     */
    public void checkValidity() {
        checkState(backingMap.get(START_OF_TIME) != null,
                "Timed transition values missing required entry for the start of time (Unix Epoch)");
    }

    @Override
    protected NavigableMap<DateTime, T> delegate() {
        return backingMap;
    }

    /** Returns the map of DateTime to value that is the "natural" representation of this property. */
    public ImmutableSortedMap<DateTime, V> toValueMap() {
        return ImmutableSortedMap.copyOfSorted(Maps.transformValues(backingMap, new Function<T, V>() {
            @Override
            public V apply(T timedTransition) {
                return timedTransition.getValue();
            }
        }));
    }

    /**
     * Returns the value of the property that is active at the specified time.  The active value for
     * a time before START_OF_TIME is extrapolated to be the value that is active at START_OF_TIME.
     */
    public V getValueAtTime(DateTime time) {
        // Retrieve the current value by finding the latest transition before or at the given time,
        // where any given time earlier than START_OF_TIME is replaced by START_OF_TIME.
        return backingMap.floorEntry(latestOf(START_OF_TIME, time)).getValue().getValue();
    }

    /**
     * Returns the time of the next transition.  Returns null if there is no subsequent transition.
     */
    @Nullable
    public DateTime getNextTransitionAfter(DateTime time) {
        return backingMap.higherKey(latestOf(START_OF_TIME, time));
    }
}