/**
* Copyright 2009 Google Inc.
*
* 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 org.waveprotocol.wave.model.document.operation.util;
import org.waveprotocol.wave.model.document.operation.util.ImmutableUpdateMap.AttributeUpdate;
import org.waveprotocol.wave.model.util.Preconditions;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
public abstract class ImmutableStateMap<T extends ImmutableStateMap<T, U>, U extends UpdateMap>
extends AbstractMap<String, String> {
/**
* A name-value pair representing an attribute.
*/
public static final class Attribute implements Map.Entry<String,String> {
// TODO: This class can be simplified greatly if
// AbstractMap.SimpleImmutableEntry from Java 6 can be used.
private final String name;
private final String value;
/**
* Creates an attribute with a map entry representing an attribute
* name-value pair.
*
* @param entry The attribute's name-value pair.
*/
public Attribute(Map.Entry<String,String> entry) {
this(entry.getKey(), entry.getValue());
}
/**
* Creates an attribute given a name-value pair.
*
* @param name The name of the attribute.
* @param value The value of the attribute.
*/
public Attribute(String name, String value) {
Preconditions.checkNotNull(name, "Null attribute name");
Preconditions.checkNotNull(value, "Null attribute value");
this.name = name;
this.value = value;
}
@Override
public String getKey() {
return name;
}
@Override
public String getValue() {
return value;
}
@Override
public String setValue(String value) {
throw new UnsupportedOperationException("Attempt to modify an immutable map entry.");
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Map.Entry)) {
return false;
}
Map.Entry<?,?> entry = (Map.Entry<?,?>) o;
return ((name == null) ? entry.getKey() == null : name.equals(entry.getKey())) &&
((value == null) ? entry.getValue() == null : value.equals(entry.getValue()));
}
@Override
public int hashCode() {
return ((name == null) ? 0 : name.hashCode()) ^
((value == null) ? 0 : value.hashCode());
}
@Override
public String toString() {
return "Attribute(" + name + "=" + value + ")";
}
}
private final List<Attribute> attributes;
private final Set<Map.Entry<String,String>> entrySet =
new AbstractSet<Map.Entry<String,String>>() {
@Override
public Iterator<Map.Entry<String,String>> iterator() {
return new Iterator<Map.Entry<String,String>>() {
private final Iterator<Attribute> iterator = attributes.iterator();
public boolean hasNext() {
return iterator.hasNext();
}
public Attribute next() {
return iterator.next();
}
public void remove() {
throw new UnsupportedOperationException("Attempt to modify an immutable set.");
}
};
}
@Override
public int size() {
return attributes.size();
}
};
/**
* Creates a new T object containing no T.
*/
public ImmutableStateMap() {
attributes = Collections.emptyList();
}
protected static final Comparator<Attribute> comparator = new Comparator<Attribute>() {
@Override
public int compare(Attribute a, Attribute b) {
return a.name.compareTo(b.name);
}
};
/**
* Constructs a new <code>T</code> object with the T
* specified by the given mapping.
*
* @param map The mapping of attribute names to attribute values.
*/
public ImmutableStateMap(Map<String,String> map) {
this.attributes = attributeListFromMap(map);
}
public ImmutableStateMap(String ... pairs) {
Preconditions.checkArgument(pairs.length % 2 == 0, "Pairs must come in groups of two");
Map<String, String> map = new HashMap<String, String>();
for (int i = 0; i < pairs.length; i += 2) {
Preconditions.checkNotNull(pairs[i], "Null key");
Preconditions.checkNotNull(pairs[i + 1], "Null value");
if (map.containsKey(pairs[i])) {
Preconditions.illegalArgument("Duplicate key: " + pairs[i]);
}
map.put(pairs[i], pairs[i + 1]);
}
this.attributes = attributeListFromMap(map);
}
private List<Attribute> attributeListFromMap(Map<String, String> map) {
ArrayList<Attribute> attributeList = new ArrayList<Attribute>(map.size());
for (Map.Entry<String, String> entry : map.entrySet()) {
if (entry.getKey() == null || entry.getValue() == null) {
Preconditions.nullPointer("This map does not allow null keys or values");
}
attributeList.add(new Attribute(entry));
}
Collections.sort(attributeList, comparator);
return attributeList;
}
protected ImmutableStateMap(List<Attribute> attributes) {
this.attributes = attributes;
}
@Override
public Set<Map.Entry<String,String>> entrySet() {
return entrySet;
}
/**
* Returns a <code>T</code> object obtained by applying the update
* specified by the <code>U</code> object into this
* <code>T</code> object.
*
* @param attributeUpdate The update to apply.
* @return A <code>T</code> object obtained by applying the given
* update onto this object.
*/
public T updateWith(U attributeUpdate) {
return updateWith(attributeUpdate, true);
}
public T updateWithNoCompatibilityCheck(U attributeUpdate) {
return updateWith(attributeUpdate, false);
}
private T updateWith(U attributeUpdate, boolean checkCompatibility) {
List<Attribute> newImmutableStateMap = new ArrayList<Attribute>();
Iterator<Attribute> iterator = attributes.iterator();
Attribute nextAttribute = iterator.hasNext() ? iterator.next() : null;
// TODO: Have a slow path when the cast would fail.
List<AttributeUpdate> updates = ((ImmutableUpdateMap<?,?>) attributeUpdate).updates;
for (AttributeUpdate update : updates) {
while (nextAttribute != null) {
int comparison = update.name.compareTo(nextAttribute.name);
if (comparison > 0) {
newImmutableStateMap.add(nextAttribute);
nextAttribute = iterator.hasNext() ? iterator.next() : null;
} else if (comparison < 0) {
if (checkCompatibility && update.oldValue != null) {
Preconditions.illegalArgument(
"Mismatched old value: attempt to update unset attribute with " + update);
}
break;
} else if (comparison == 0) {
if (checkCompatibility && !nextAttribute.value.equals(update.oldValue)) {
Preconditions.illegalArgument(
"Mismatched old value: attempt to update " + nextAttribute + " with " + update);
}
nextAttribute = iterator.hasNext() ? iterator.next() : null;
break;
}
}
if (update.newValue != null) {
newImmutableStateMap.add(new Attribute(update.name, update.newValue));
}
}
if (nextAttribute != null) {
newImmutableStateMap.add(nextAttribute);
while (iterator.hasNext()) {
newImmutableStateMap.add(iterator.next());
}
}
return createFromList(newImmutableStateMap);
}
protected abstract T createFromList(List<Attribute> attributes);
public static void checkAttributesSorted(List<Attribute> attributes) {
Attribute previous = null;
for (Attribute a : attributes) {
Preconditions.checkNotNull(a, "Null attribute");
assert a.name != null;
assert a.value != null;
if (previous != null && previous.name.compareTo(a.name) >= 0) {
Preconditions.illegalArgument(
"Attribute keys not strictly monotonic: " + previous.name + ", " + a.name);
}
previous = a;
}
}
public static <T extends ImmutableStateMap<T, U>, U extends ImmutableUpdateMap<U, ?>> T
updateWithoutCompatibilityCheck(T state, U update) {
// the cast below is required to work with javac from OpenJDK 7
return ((ImmutableStateMap<T, U>) state).updateWith(update, false);
}
}