/**
* Copyright 2010 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.supplement;
import org.waveprotocol.wave.model.supplement.ObservablePrimitiveSupplement.Listener;
import org.waveprotocol.wave.model.document.ObservableMutableDocument;
import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl;
import org.waveprotocol.wave.model.document.util.DocHelper;
import org.waveprotocol.wave.model.document.util.DocumentEventRouter;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.ElementListener;
import org.waveprotocol.wave.model.util.Preconditions;
import org.waveprotocol.wave.model.util.ReadableStringMap;
import org.waveprotocol.wave.model.util.ReadableStringMap.ProcV;
import org.waveprotocol.wave.model.util.ReadableStringSet.Proc;
import org.waveprotocol.wave.model.util.StringMap;
import org.waveprotocol.wave.model.util.StringSet;
/**
* Collection of per-gadget state maps, implemented by embedding them in
* elements of a document.
*
*/
class GadgetStateCollection<E> implements ElementListener<E> {
private final DocumentEventRouter<? super E, E, ?> router;
private final E container;
/** Gadget state, expressed as a per-wavelet structure. */
private final StringMap<GadgetState> gadgetSupplements = CollectionUtils.createStringMap();
/** Listener to inject into each read-state. */
private final Listener listener;
private GadgetStateCollection(DocumentEventRouter<? super E, E, ?> router, E container,
Listener listener) {
this.router = router;
this.container = container;
this.listener = listener;
}
/**
* Creates a gadget state collection in a given document.
*
* @param <E> Element and container type.
* @param doc Document that will hold the gadget state collection.
* @param container Collection container.
* @param listener Event listener for the collection.
* @return Gadget state collection.
*/
public static <E> GadgetStateCollection<E> create(
DocumentEventRouter<? super E, E, ?> router, E container, Listener listener) {
GadgetStateCollection<E> col = new GadgetStateCollection<E>(router, container, listener);
router.addChildListener(container, col);
col.load();
return col;
}
private ObservableMutableDocument<? super E, E, ?> getDocument() {
return router.getDocument();
}
private void load() {
ObservableMutableDocument<? super E, E, ?> doc = getDocument();
E child = DocHelper.getFirstChildElement(doc, doc.getDocumentElement());
while (child != null) {
onElementAdded(child);
child = DocHelper.getNextSiblingElement(doc, child);
}
}
private String valueOf(E element) {
return getDocument().getAttribute(element, WaveletBasedSupplement.ID_ATTR);
}
@Override
public void onElementAdded(E element) {
ObservableMutableDocument<? super E, E, ?> doc = getDocument();
assert container.equals(doc.getParentElement(element));
if (!WaveletBasedSupplement.GADGET_TAG.equals(doc.getTagName(element))) {
return;
}
String gadgetId = valueOf(element);
if (gadgetId != null) {
GadgetState existing = gadgetSupplements.get(gadgetId);
if (existing == null) {
GadgetState state = DocumentBasedGadgetState.create(router, element, gadgetId, listener);
gadgetSupplements.put(gadgetId, state);
// TODO(user): Follow the changes in WaveletReadStateCollection and update this class.
//
// NOTE(user): it is important that these events get fired after the new read-state
// object is added to the map above, in order that the interface presented by this
// collection object is consistent with the events being broadcast to the listener.
//
listener.onGadgetStateChanged(gadgetId, null, null, null);
} else {
// TODO(user): Follow the changes in WaveletReadStateCollection and update this class.
}
} else {
// XML error: someone added a WAVELET element without an id. Ignore.
// TODO(user): log this at error level, once loggers are injected into
// these classes.
// TODO(user): Follow the changes in WaveletReadStateCollection and update this class.
}
}
@Override
public void onElementRemoved(E element) {
if (WaveletBasedSupplement.GADGET_TAG.equals(getDocument().getTagName(element))) {
String gadgetId = valueOf(element);
if (gadgetId != null) {
gadgetSupplements.remove(gadgetId);
}
}
}
private void createEntry(String gadgetId) {
getDocument().createChildElement(getDocument().getDocumentElement(),
WaveletBasedSupplement.GADGET_TAG,
new AttributesImpl(WaveletBasedSupplement.ID_ATTR, gadgetId));
}
GadgetState getSupplement(String gadgetId) {
Preconditions.checkNotNull(gadgetId, "Gadget ID must not be null");
GadgetState state = gadgetSupplements.get(gadgetId);
if (state == null) {
// Create a new container element for tracking state for the gadget.
createEntry(gadgetId);
state = gadgetSupplements.get(gadgetId);
assert state != null;
}
return state;
}
/**
* Saves the gadget state in the underlying implementation.
*
* @param gadgetId ID of the gadget that owns the state.
* @param key The key.
* @param value The value for the key. If null, the key will be removed.
*/
void setGadgetState(String gadgetId, String key, String value) {
getSupplement(gadgetId).setState(key, value);
}
/**
* Removes entire saved object.
*/
void clear() {
final StringSet keys = CollectionUtils.createStringSet();
gadgetSupplements.each(new ProcV<GadgetState>() {
@Override
public void apply(String key, GadgetState value) {
keys.add(key);
}
});
keys.each(new Proc() {
@Override
public void apply(String key) {
gadgetSupplements.get(key).remove();
}
});
}
ReadableStringMap<String> getGadgetState(String gadgetId) {
GadgetState state = gadgetSupplements.get(gadgetId);
return state != null ? state.getStateMap() : CollectionUtils.<String> emptyMap();
}
}