/** * 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.client.editor.content; import org.waveprotocol.wave.client.editor.EditorStaticDeps; import org.waveprotocol.wave.model.document.indexed.LocationMapper; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.document.operation.DocOpCursor; import org.waveprotocol.wave.model.document.operation.ModifiableDocument; import org.waveprotocol.wave.model.document.operation.impl.DocOpBuffer; import org.waveprotocol.wave.model.document.util.DocHelper; import org.waveprotocol.wave.model.document.util.DomOperationUtil; import org.waveprotocol.wave.model.document.util.LocalDocument; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.document.util.ReadableDocumentView; import org.waveprotocol.wave.model.document.util.ReadableTreeWalker; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.operation.SilentOperationSink; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.IdentityMap; import org.waveprotocol.wave.model.util.Preconditions; import java.util.Map; /** * Bundle of documents to manage the lazy promotion of local nodes into the persistent view. * * To add this feature, the manager adapts the following objects: * - a LocalDocument containing the entire local document DOM tree * - a LocationMapper to assist in finding locations in the the persisted tree. * - a ReadableDocumentView containing a view over the local document of just persisted nodes. * - a Sink to send correction ops to. * * It allows nodes in the LocalDocument to be marked as lazily persistable, then when needed * (defined as a point inside being needed to be persisted) it creates an op that will persist * the required subtree DOM. * * Additionally, when the op is sent, a delegate is created that redirects node creation methods * to reuse the nodes that already exist in the local doc for the persisted version. This avoids * the need for the lazy persistence to affect the local document at all. * * @author patcoleman@google.com (Pat Coleman) */ public class LazyPersistenceManager { private final SilentOperationSink<DocOp> sink; private final LocalDocument<ContentNode, ContentElement, ContentTextNode> localDoc; private final LocationMapper<ContentNode> persistedLocations; private final ReadableDocumentView<ContentNode, ContentElement, ContentTextNode> persistedView; /** Delegate out for createElement / createTextNode calls that may reuse old nodes. */ private ReadableTreeWalker<ContentNode, ContentElement, ContentTextNode> nodeCreationDelegate = null; /** * Contains all nodes that are lazily persisted. * NOTE(patcoleman): Use IdentitySet when possible. */ private final IdentityMap<ContentNode, ContentNode> lazilyPersistedNodes = CollectionUtils.createIdentityMap(); /** * Creates by attaching helper members for manipulating the persistent filteredView. * @param outgoingSink Outgoing Sink for the fix-up persistence ops. * @param localDoc Document with all local and persisted nodes. * @param persistedLocations Indexed version of the persisted tree. * @param persistedView View over the persisted document, for filtering. * @param localCorrectionSink Internal sink for the fix-up persistence ops. */ public LazyPersistenceManager( final SilentOperationSink<DocOp> outgoingSink, LocalDocument<ContentNode, ContentElement, ContentTextNode> localDoc, LocationMapper<ContentNode> persistedLocations, ReadableDocumentView<ContentNode, ContentElement, ContentTextNode> persistedView, final ModifiableDocument localCorrectionSink) { // combine both sinks into one: this.sink = new SilentOperationSink<DocOp>() { @Override public void consume(DocOp op) { try { localCorrectionSink.consume(op); outgoingSink.consume(op); } catch (OperationException e) { EditorStaticDeps.logger.fatal().log(e); } } }; this.localDoc = localDoc; this.persistedLocations = persistedLocations; this.persistedView = persistedView; } /** Mark a node to be persisted when it is next needed. */ public void markAsLazyPersisted(ContentNode node) { // NOTE(patcoleman): not checked due to speed reasons, but this node should not have // a descendant or ancestor also lazily persisted, or a persisted descendant. // i.e. it should be true that every lazy node is the root of a local subtree containing // no other lazy nodes. lazilyPersistedNodes.put(node, node); } /** @return Whether this should be used to 'create' nodes by retrieving the local instances. */ public boolean isCreationDelegate() { return nodeCreationDelegate != null; } /** Creates an element by instead sourcing already placed local nodes. */ public ContentElement createElement(String tagName, Map<String, String> attributes, ContentElement parent, ContentNode nodeAfter) { Preconditions.checkState(isCreationDelegate(), "Lazy persistence delegated an element creation when not a delegate."); return nodeCreationDelegate.checkElement(tagName, attributes); } /** Creates a text node by instead sourcing already placed local nodes. */ public ContentTextNode createTextNode(String data, ContentElement parent, ContentNode nodeAfter) { Preconditions.checkState(isCreationDelegate(), "Lazy persistence delegated a text node creationgwhen not a delegate."); return nodeCreationDelegate.checkTextNode(data); } /** Manage the lazy persistence of nodes, given a position that is required to be persisted. */ public void updateLazyNodes(ContentNode at) { // Walk up transparent local nodes to find the lowest lazy node... for (; localDoc.isTransparent(at); at = localDoc.getParentElement(at)) { if (lazilyPersistedNodes.has(at)) { // ... and if found, persist. lazilyPersistedNodes.remove(at); this.actuallyPersist(at); break; } if (localDoc.isSameNode(at, localDoc.getDocumentElement())) { break; } } } /** Utility for the logic that goes into actually persisting a given node and its subtree. */ private void actuallyPersist(ContentNode localNode) { // NOTE(patcoleman): assumes entire subtree is local - do not call with persisted children! if (persistedView.isSameNode(localNode, persistedView.getVisibleNode(localNode))) { throw new IllegalArgumentException("opaquePersist: element must not be persistent"); } // when promoting to opaque, we find where it is and mark it as no longer transparent, // persisting that operation to the server // find length to the insertion point, and length after: int position = DocHelper.getFilteredLocation(persistedLocations, persistedView, Point.end(localNode)); int remainder = persistedLocations.size() - position; // create op, wrapping the tree operations within retains. DocOpBuffer opBuffer = new DocOpBuffer(); safeRetain(opBuffer, position); DomOperationUtil.buildDomInitializationFromSubtree(localDoc, localNode, opBuffer); safeRetain(opBuffer, remainder); DocOp op = opBuffer.finish(); // fake out the node creation, and consume: nodeCreationDelegate = new ReadableTreeWalker<ContentNode, ContentElement, ContentTextNode>(localDoc, localNode); sink.consume(op); Preconditions.checkState(nodeCreationDelegate.checkComplete(), "Walk of tree did not match up..."); nodeCreationDelegate = null; } // Utility - TODO(patcoleman): move somewhere common, if not already exists? private static void safeRetain(DocOpCursor cursor, int retain) { if (retain > 0) { cursor.retain(retain); } } }