package org.swellrt.model.generic;
import org.swellrt.model.ReadableBoolean;
import org.swellrt.model.ReadableMap;
import org.swellrt.model.ReadableNumber;
import org.swellrt.model.ReadableTypeVisitor;
import org.swellrt.model.adt.DocumentBasedBasicRMap;
import org.waveprotocol.wave.model.adt.ObservableBasicMap;
import org.waveprotocol.wave.model.document.Doc;
import org.waveprotocol.wave.model.document.ObservableDocument;
import org.waveprotocol.wave.model.document.util.DefaultDocEventRouter;
import org.waveprotocol.wave.model.document.util.DocHelper;
import org.waveprotocol.wave.model.util.CopyOnWriteSet;
import org.waveprotocol.wave.model.util.DocumentEventGroupListener;
import org.waveprotocol.wave.model.util.Preconditions;
import org.waveprotocol.wave.model.util.Serializer;
import org.waveprotocol.wave.model.wave.SourcesEvents;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class MapType extends Type implements ReadableMap, SourcesEvents<MapType.Listener>,
DocumentEventGroupListener {
public class Event {
protected Event(String key, Type oldValue, Type newValue) {
super();
this.key = key;
this.oldValue = oldValue;
this.newValue = newValue;
}
public String key;
public Type oldValue;
public Type newValue;
}
/**
* Listener to map events.
*/
public interface Listener {
void onValueChanged(String key, Type oldValue, Type newValue);
void onValueRemoved(String key, Type value);
}
/**
* Get an instance of MapType within the model backed by a document. This
* method is used for deserialization.
*
* @param model
* @param substrateDocumentId
* @return
*/
protected static MapType deserialize(Type parent, String substrateDocumentId) {
Preconditions.checkArgument(substrateDocumentId.startsWith(PREFIX),
"Not a document id for MapType");
MapType map = new MapType(parent.getModel());
map.attach(parent, substrateDocumentId);
return map;
}
protected static MapType deserialize(Model model, String substrateDocumentId) {
Preconditions.checkArgument(substrateDocumentId.startsWith(PREFIX),
"Not a document id for MapType");
MapType map = new MapType(model);
map.attach(null, substrateDocumentId);
return map;
}
public final static String TYPE_NAME = "MapType";
public final static String PREFIX = "map";
public final static String TAG_MAP = "map";
public final static String TAG_ENTRY = "entry";
public final static String KEY_ATTR_NAME = "k";
public final static String VALUE_ATTR_NAME = "v";
private ObservableBasicMap<String, Type> observableMap;
private ObservableBasicMap.Listener<String, Type> observableMapListener;
private Model model;
private ObservableDocument backendDocument;
private String backendDocumentId;
private MetadataContainer metadata;
private ValuesContainer values;
private Doc.E backendMapElement;
private boolean isAttached;
private final CopyOnWriteSet<Listener> listeners = CopyOnWriteSet.create();
private DefaultDocEventRouter router;
private final Map<String, Type> cachedMap = new HashMap<String, Type>();
private final List<Event> pendingEvents = new ArrayList<Event>();
/**
* Constructor for MapType instances.
*
* @param model The model metadata object where this instance belongs to.
*/
protected MapType(Model model) {
this.model = model;
this.isAttached = false;
observableMapListener = new ObservableBasicMap.Listener<String, Type>() {
@Override
public void onEntrySet(final String key, final Type oldValue, final Type newValue) {
// Events are not delivered yet. Wait until all events are delivered.
pendingEvents.add(new Event(key, oldValue, newValue));
}
};
}
private void deliverPendingEvents() {
for (Event e: pendingEvents) {
// Invalidate cache
cachedMap.remove(e.key);
if (e.newValue == null) {
for (Listener l : listeners)
l.onValueRemoved(e.key, e.oldValue);
} else {
for (Listener l : listeners)
l.onValueChanged(e.key, e.oldValue, e.newValue);
}
}
pendingEvents.clear();
}
//
// Type interface
//
@Override
protected void attach(Type parent) {
Preconditions.checkArgument(!isAttached, "Already attached map type");
String substrateDocumentId = model.generateDocId(getPrefix());
attach(parent, substrateDocumentId);
}
@Override
protected void attach(Type parent, int slotIndex) {
throw new IllegalStateException("This method is not allowed for a MapType");
}
@Override
protected void attach(Type parent, String substrateDocumentId) {
Preconditions.checkArgument(!isAttached, "Already attached map type");
Preconditions.checkNotNull(substrateDocumentId, "Document id is null");
backendDocumentId = substrateDocumentId;
boolean isNew = false;
// Be careful with order of following steps!
// Get or create substrate document
if (!model.getModelDocuments().contains(backendDocumentId)) {
backendDocument = model.createDocument(backendDocumentId);
isNew = true;
} else {
backendDocument = model.getDocument(backendDocumentId);
}
router = DefaultDocEventRouter.create(backendDocument);
// Metadata section
metadata = MetadataContainer.get(backendDocument);
if (isNew) {
metadata.setCreator(model.getCurrentParticipantId());
}
// Map section
backendMapElement = DocHelper.getElementWithTagName(backendDocument, TAG_MAP);
if (backendMapElement == null) {
backendMapElement =
backendDocument.createChildElement(backendDocument.getDocumentElement(), TAG_MAP,
Collections.<String, String> emptyMap());
}
// Initialize values section. Always before loading the observable map
this.values = ValuesContainer.get(backendDocument, router, this);
// Attached! Before the observable list initialization to allow access
// during initialization
this.isAttached = true;
// Initialize observable map
this.observableMap =
DocumentBasedBasicRMap.create(router, backendMapElement, Serializer.STRING,
new MapSerializer(this), TAG_ENTRY, KEY_ATTR_NAME, VALUE_ATTR_NAME);
this.observableMap.addListener(observableMapListener);
router.setEventGroupListener(this);
}
protected void deattach() {
Preconditions.checkArgument(isAttached, "Unable to detach an unattached Map");
metadata.setDetachedPath();
isAttached = false;
}
@Override
protected String getPrefix() {
return PREFIX;
}
@Override
protected boolean isAttached() {
return isAttached;
}
@Override
protected String serialize() {
Preconditions.checkArgument(isAttached, "Unable to serialize an unattached Map");
return backendDocumentId;
}
@Override
protected ListElementInitializer getListElementInitializer() {
return new ListElementInitializer() {
@Override
public String getType() {
return PREFIX;
}
@Override
public String getBackendId() {
return serialize();
}
};
}
//
// Listeners
//
@Override
public void addListener(Listener listener) {
listeners.add(listener);
}
@Override
public void removeListener(Listener listener) {
listeners.remove(listener);
}
//
// Map operations
//
@Override
public Type get(String key) {
Preconditions.checkArgument(isAttached, "Unable to get values from an unattached Map");
if (observableMap.keySet().contains(key)) {
if (!cachedMap.containsKey(key)) {
Type value = observableMap.get(key);
// For no container types, set the path in runtime
if (!value.getType().equals(MapType.TYPE_NAME) &&
!value.getType().equals(ListType.TYPE_NAME)) {
value.setPath(getPath()+"."+key);
}
cachedMap.put(key, value);
}
return cachedMap.get(key);
}
return null;
}
public Type put(String key, Type value) {
Preconditions.checkArgument(isAttached, "Unable to put values into an unattached Map");
Preconditions.checkArgument(!value.isAttached(),
"Already attached Type instances can't be put into a Map");
Type oldValue = observableMap.get(key);
value.attach(this);
// This should be always a new put, otherwise the !value.isAttached
// precondition would be false
observableMap.put(key, value);
value = observableMap.get(key);
if (value == null) return null;
value.setPath(getPath() + "." + key);
cachedMap.put(key, value);
if (oldValue != null) {
oldValue.deattach();
}
return value;
}
public StringType put(String key, String value) {
Preconditions.checkArgument(isAttached, "Unable to put values into an unattached Map");
StringType strValue = new StringType(value);
put(key, strValue);
return strValue;
}
@Override
public Set<String> keySet() {
return observableMap.keySet();
}
@Override
public boolean hasKey(String key) {
return observableMap.keySet().contains(key);
}
public void remove(String key) {
Preconditions.checkArgument(isAttached, "Unable to remove values from an unattached Map");
Type removedValue = observableMap.get(key);
if (removedValue != null) {
cachedMap.remove(key);
observableMap.remove(key);
removedValue.deattach();
}
}
@Override
public String getDocumentId() {
return backendDocumentId;
}
@Override
public String getType() {
return TYPE_NAME;
}
@Override
public String getPath() {
return metadata.getPath();
}
@Override
protected void setPath(String path) {
metadata.setPath(path);
}
@Override
protected boolean hasValuesContainer() {
return true;
}
@Override
protected ValuesContainer getValuesContainer() {
return values;
}
protected String getValueReference(Type value) {
if (observableMap == null) return null;
for (String key : observableMap.keySet()) {
Type thisValue = observableMap.get(key);
if (thisValue != null && thisValue.equals(value)) return key;
}
return null;
}
@Override
public Model getModel() {
return model;
}
@Override
public void accept(ReadableTypeVisitor visitor) {
visitor.visit(this);
}
@Override
public MapType asMap() {
return this;
}
@Override
public StringType asString() {
return null;
}
@Override
public ListType asList() {
return null;
}
@Override
public TextType asText() {
return null;
}
@Override
public FileType asFile() {
return null;
}
@Override
public ReadableNumber asNumber() {
return null;
}
@Override
public ReadableBoolean asBoolean() {
return null;
}
@Override
protected void markValueUpdate(Type value) {
// Force a redundant put to trigger a convinient DocOp
// for a primitive value update.
String key = getValueReference(value);
if (key != null) observableMap.put(key, value);
}
@Override
public void onBeginEventGroup(String groupId) {
// TODO Auto-generated method stub
}
@Override
public void onEndEventGroup(String groupId) {
deliverPendingEvents();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((backendDocumentId == null) ? 0 : backendDocumentId.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MapType other = (MapType) obj;
if (backendDocumentId == null) {
if (other.backendDocumentId != null)
return false;
} else if (!backendDocumentId.equals(other.backendDocumentId))
return false;
return true;
}
}