// Copyright 2016 The Domain Registry 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 com.google.domain.registry.model.common; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.domain.registry.util.DateTimeUtils.START_OF_TIME; import static com.google.domain.registry.util.DateTimeUtils.latestOf; import com.google.common.base.Function; import com.google.common.collect.ForwardingMap; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.google.domain.registry.model.ImmutableObject; import com.google.domain.registry.util.TypeUtils; import com.googlecode.objectify.mapper.Mapper; import org.joda.time.DateTime; import java.util.NavigableMap; import java.util.TreeMap; /** * 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. *

* 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. *

* The type parameter {@code T} specifies a user-defined subclass of {@code TimedTransition} 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> extends ForwardingMap { /** * 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. *

* 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 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 for use with @Mapify; extracts the time from a TimedTransition to use it as a key. */ public static class TimeMapper implements Mapper> { @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 parameter could be eliminated by getting the class via reflection, but then // the callsite cannot infer T, so unless you explicitly call this as .fromValueMap() it // will default to using just TimedTransition, which fails at runtime. private static > NavigableMap makeTransitionMap( ImmutableSortedMap valueMap, final Class 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() { // 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) 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. *

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

* 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 > TimedTransitionProperty forMapify( ImmutableSortedMap valueMap, Class timedTransitionSubclass) { return new TimedTransitionProperty<>(new TreeMap<>( makeTransitionMap(valueMap, timedTransitionSubclass))); } /** The backing map of DateTime to TimedTransition subclass used to store the transitions. */ private final NavigableMap backingMap; /** Returns a new {@code TimedTransitionProperty} backed by the provided map instance. */ private TimedTransitionProperty(NavigableMap 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 delegate() { return backingMap; } /** Returns the map of DateTime to value that is the "natural" representation of this property. */ public ImmutableSortedMap toValueMap() { return ImmutableSortedMap.copyOfSorted(Maps.transformValues( backingMap, new Function() { @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(); } }