/**
* Copyright 2008 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 com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.Text;
import org.waveprotocol.wave.client.common.util.DomHelper;
import org.waveprotocol.wave.client.common.util.LogicalPanel;
import org.waveprotocol.wave.client.editor.Editor;
import org.waveprotocol.wave.client.editor.EditorAnnotationTree;
import org.waveprotocol.wave.client.editor.EditorContext;
import org.waveprotocol.wave.client.editor.EditorImpl.MiniBundle;
import org.waveprotocol.wave.client.editor.EditorStaticDeps;
import org.waveprotocol.wave.client.editor.ElementHandlerRegistry;
import org.waveprotocol.wave.client.editor.NodeMutationHandler;
import org.waveprotocol.wave.client.editor.content.ClientDocumentContext.RenderingConcerns;
import org.waveprotocol.wave.client.editor.content.DiffHighlightingFilter.DiffHighlightTarget;
import org.waveprotocol.wave.client.editor.content.ExtendedClientDocumentContext.LowLevelEditingConcerns;
import org.waveprotocol.wave.client.editor.content.SelectionMaintainer.TextNodeChangeType;
import org.waveprotocol.wave.client.editor.content.misc.DisplayEditModeHandler;
import org.waveprotocol.wave.client.editor.content.paragraph.DefaultParagraphHtmlRenderer;
import org.waveprotocol.wave.client.editor.content.paragraph.LineContainerParagraphiser;
import org.waveprotocol.wave.client.editor.content.paragraph.LineRendering;
import org.waveprotocol.wave.client.editor.extract.InconsistencyException;
import org.waveprotocol.wave.client.editor.extract.RepairListener;
import org.waveprotocol.wave.client.editor.extract.Repairer;
import org.waveprotocol.wave.client.editor.extract.TypingExtractor;
import org.waveprotocol.wave.client.editor.gwt.HasGwtWidget;
import org.waveprotocol.wave.client.editor.impl.HtmlView;
import org.waveprotocol.wave.client.editor.impl.HtmlViewImpl;
import org.waveprotocol.wave.client.editor.impl.NodeManager;
import org.waveprotocol.wave.client.editor.impl.StrippingHtmlView;
import org.waveprotocol.wave.client.editor.impl.TransparencyUtil;
import org.waveprotocol.wave.client.editor.operation.EditorOperationSequencer;
import org.waveprotocol.wave.client.editor.selection.content.SelectionHelper;
import org.waveprotocol.wave.client.editor.selection.content.ValidSelectionStrategy;
import org.waveprotocol.wave.client.editor.sugg.SuggestionsManager;
import org.waveprotocol.wave.client.scheduler.FinalTaskRunner;
import org.waveprotocol.wave.client.scheduler.FinalTaskRunnerImpl;
import org.waveprotocol.wave.client.scheduler.Scheduler.Task;
import org.waveprotocol.wave.model.document.AnnotationMutationHandler;
import org.waveprotocol.wave.model.document.MutableAnnotationSet;
import org.waveprotocol.wave.model.document.ReadableDocument;
import org.waveprotocol.wave.model.document.indexed.AnnotationSetListener;
import org.waveprotocol.wave.model.document.indexed.IndexedDocumentImpl;
import org.waveprotocol.wave.model.document.indexed.LocationMapper;
import org.waveprotocol.wave.model.document.indexed.RawAnnotationSet;
import org.waveprotocol.wave.model.document.indexed.SimpleAnnotationSet;
import org.waveprotocol.wave.model.document.indexed.StubModifiableAnnotations;
import org.waveprotocol.wave.model.document.indexed.Validator;
import org.waveprotocol.wave.model.document.operation.Attributes;
import org.waveprotocol.wave.model.document.operation.DocInitialization;
import org.waveprotocol.wave.model.document.operation.DocOp;
import org.waveprotocol.wave.model.document.operation.Nindo;
import org.waveprotocol.wave.model.document.operation.NindoSink;
import org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.ViolationCollector;
import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema;
import org.waveprotocol.wave.model.document.raw.TextNodeOrganiser;
import org.waveprotocol.wave.model.document.util.DocHelper;
import org.waveprotocol.wave.model.document.util.DocIterate;
import org.waveprotocol.wave.model.document.util.ElementManager;
import org.waveprotocol.wave.model.document.util.FilteredView;
import org.waveprotocol.wave.model.document.util.IdentityView;
import org.waveprotocol.wave.model.document.util.LineContainers;
import org.waveprotocol.wave.model.document.util.LocalAnnotationSetImpl;
import org.waveprotocol.wave.model.document.util.LocalDocument;
import org.waveprotocol.wave.model.document.util.PersistentContent;
import org.waveprotocol.wave.model.document.util.Point;
import org.waveprotocol.wave.model.document.util.Point.El;
import org.waveprotocol.wave.model.document.util.Property;
import org.waveprotocol.wave.model.document.util.ReadableDocumentView;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.operation.OperationRuntimeException;
import org.waveprotocol.wave.model.operation.OperationSequencer;
import org.waveprotocol.wave.model.operation.SilentOperationSink;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.Preconditions;
import org.waveprotocol.wave.model.util.StringMap;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Editor specific extensions of {@link IndexedDocumentImpl} supporting
* the management of the content-vs-html duality.
*
* TODO(danilatos): Document thoroughly.
*
* @author danilatos@google.com (Daniel Danilatos)
* @author lars@google.com (Lars Rasmussen)
*/
public class ContentDocument {
/**
* Tag interface to denote mutation handlers that should not be disabled when
* rendering is turned off.
*/
public interface PermanentMutationHandler { }
/**
* If true, local ops will be validated before application. Set to false to
* avoid this overhead, but at the rist of permitting invalid ops to be sent
* out.
*/
public static boolean validateLocalOps = true;
/**
* If true, expensive health checks will be performed if assertions are also
* on.
*/
public static boolean performExpensiveChecks = true;
/**
* Thrown before a locally generated operation is applied to the document.
* This prevents its corruption and means that it is still usable and
* consistent with the server, provided the invalid operation is not then
* sent out into the op stream.
*/
public abstract static class LocalOperationException extends RuntimeException {
private final ViolationCollector violation;
LocalOperationException(ViolationCollector violation) {
super("" + violation);
Preconditions.checkNotNull(violation, "Missing violation information");
this.violation = violation;
}
public ViolationCollector getViolations() {
return violation;
}
@Override
public String toString() {
return "LocalOperationException: " + violation;
}
}
public static class SchemaViolatingLocalOperationException extends LocalOperationException {
SchemaViolatingLocalOperationException(ViolationCollector violation) {
super(violation);
Preconditions.checkArgument(
violation.getValidationResult().isInvalidSchema(),
"Not a schema violation");
}
}
public static class BadOpLocalOperationException extends LocalOperationException {
BadOpLocalOperationException(ViolationCollector violation) {
super(violation);
Preconditions.checkArgument(
!violation.getValidationResult().isInvalidSchema(),
"Not a low level violation");
}
}
/**
* For easy switching of annotation set for debugging
*/
public enum AnnotationImplFactory {
/***/
SIMPLE {
@Override
RawAnnotationSet<Object> create(AnnotationSetListener<Object> annotationListener) {
return new SimpleAnnotationSet(annotationListener);
}
},
/***/
TREE {
@Override
RawAnnotationSet<Object> create(AnnotationSetListener<Object> annotationListener) {
return new EditorAnnotationTree(annotationListener);
}
},
/***/
STUB {
@Override
RawAnnotationSet<Object> create(AnnotationSetListener<Object> annotationListener) {
return new StubModifiableAnnotations<Object>();
}
};
abstract RawAnnotationSet<Object> create(AnnotationSetListener<Object> annotationListener);
}
/**
* For easy switching of annotation set for debugging
*/
public static AnnotationImplFactory annotationFactory = AnnotationImplFactory.TREE;
/**
* Singleton listener
*/
private SilentOperationSink<? super DocOp> outgoingOperationSink =
SilentOperationSink.Void.get();
/**
* Wraps up the listener also with a call to repaint nodes.
*/
private final SilentOperationSink<DocOp> outgoingRepaintSink =
new SilentOperationSink<DocOp>() {
@Override
public void consume(DocOp op) {
outgoingOperationSink.consume(op);
flushNodeRepaint();
}};
/**
* Sink for locally sourced operations, applying the operation to the document
* and sending it out.
*/
private class SourceNindoSink implements NindoSink.Silent {
private final boolean saveSelection;
private SourceNindoSink(boolean saveSelection) {
this.saveSelection = saveSelection;
}
@Override
public DocOp consumeAndReturnInvertible(Nindo op) {
DocOp docOp = consumeLocal(op, saveSelection);
outgoingOperationSink.consume(docOp);
return docOp;
}
}
/**
* This sink applies the operations locally and to an outgoing sink. When
* applying locally, it uses SelectionMaintainer to update the selection if
* neccessary.
*/
private final NindoSink.Silent nindoSink = new SourceNindoSink(true);
/**
* This version of the nindo sink does not save selection. It is intended for
* use when the DOM is already modified.
*/
private final NindoSink.Silent dontSaveSelectionNindoSink = new SourceNindoSink(false);
// NOTE(danilatos): This will become nullable in the future
private final RenderingConcerns renderingConcerns = new RenderingConcerns() {
@Override
public NodeManager getNodeManager() {
return nodeManager;
}
@Override
public ContentView getRenderedContentView() {
return ContentDocument.this.getRenderedView();
}
@Override
public FilteredHtml getFilteredHtmlView() {
return ContentDocument.this.getFilteredHtmlView();
}
@Override
public HtmlView getFullHtmlView() {
return ContentDocument.this.getRawHtmlView();
}
@Override
public Repairer getRepairer() {
return repairer;
}
};
private LowLevelEditingConcerns editingConcerns = LowLevelEditingConcerns.STUB;
/** Used for getting a unique id per document */
private static int counter = 1;
/** Document's unique id */
private final String documentUniqueString = "ed" + counter++ + "_";
/**
* Map from name attribute to content element
*/
private final StringMap<ContentElement> nameMap = CollectionUtils.createStringMap();
/** List of nodes that we need to repaint. */
private final List<ContentNode> nodesToRepaint = new ArrayList<ContentNode>();
private final ContentRawDocument.Factory factory = new ContentRawDocument.Factory() {
@Override
public ContentElement createElement(String tagName, Map<String, String> attributes) {
AgentAdapter el = new AgentAdapter(tagName, attributes,
context, registries.getElementHandlerRegistry());
postCreation(el);
return el;
}
@Override
public void setupBehaviour(ContentElement element) {
ContentDocument.this.setupBehaviour(element, null);
}
@Override
public ContentTextNode createText(String text) {
return new ContentTextNode(text, context);
}
};
@SuppressWarnings({"unchecked", "fallthrough"}) // NodeMutationHandler is generic
private void setupBehaviour(ContentElement element, Level oldLevel) {
AgentAdapter e = (AgentAdapter) element;
ElementHandlerRegistry elementRegistry = registries.getElementHandlerRegistry();
// bootstrapping for new nodes
if (oldLevel == null) {
NodeMutationHandler mutationHandler = elementRegistry.getMutationHandler(e);
if (mutationHandler instanceof PermanentMutationHandler) {
e.setNodeMutationHandler(mutationHandler);
}
oldLevel = Level.SHELVED;
}
boolean notRendered = (e.getRenderer() == AgentAdapter.noRenderer);
assert notRendered == (oldLevel == Level.SHELVED) : "oldLevel: " + oldLevel + " notRendered:"+notRendered;
boolean shouldBeRendered = level.isAtLeast(Level.RENDERED);
// Increasing level
switch (oldLevel) {
case SHELVED: // -> RENDERED
if (level.isAtMost(Level.SHELVED)) break;
setupRenderer(e, true);
if (e == fullRawSubstrate.getDocumentElement()) {
initRootElementRendering(true);
}
e.setNodeMutationHandler(elementRegistry.getMutationHandler(e));
case RENDERED: // -> INTERACTIVE
if (level.isAtMost(Level.RENDERED)) break;
e.setNodeEventHandler(elementRegistry.getEventHandler(e));
maybeSetupGwtWidget(e);
case INTERACTIVE: // -> EDITING
if (level.isAtMost(Level.INTERACTIVE)) break;
maybeSetupModeNotifications(e);
break;
}
// Decreasing level
switch (oldLevel) {
case EDITING: // -> INTERACTIVE
if (level.isAtLeast(Level.EDITING)) break;
// No need to cleanup mode notifications
case INTERACTIVE: // -> RENDERED
if (level.isAtLeast(Level.INTERACTIVE)) break;
maybeSetupGwtWidget(e);
e.setNodeEventHandler(null);
case RENDERED: // -> SHELVED
if (level.isAtLeast(Level.RENDERED)) break;
NodeMutationHandler mutationHandler = elementRegistry.getMutationHandler(e);
if (mutationHandler instanceof PermanentMutationHandler) {
e.setNodeMutationHandler(mutationHandler);
} else {
e.setNodeMutationHandler(null);
}
setupRenderer(e, false);
if (e == fullRawSubstrate.getDocumentElement()) {
initRootElementRendering(false);
}
}
for (ContentNode n = e.getFirstChild(); n != null; n = n.getNextSibling()) {
if (n instanceof ContentElement) {
setupBehaviour((AgentAdapter) n, oldLevel);
} else {
n.asText().setRendering(shouldBeRendered);
}
}
if (notRendered && shouldBeRendered) {
e.reInsertImpl();
}
// Another increasing-level piece of logic, similar to above.
// if oldLevel < RENDERED && RENDERED <= level
if (!oldLevel.isAtLeast(Level.RENDERED) && level.isAtLeast(Level.RENDERED)) {
e.triggerChildrenReady();
}
assert checkHealthy(e, false);
}
// TODO(danilatos): Kill the postCreation methods
@SuppressWarnings("unchecked")
private void postCreation(ContentElement element) {
maybeAddToNameMap(element);
}
/**
* Adds element to nameMap if it has a name attribute
* TODO(user): consider a friendly warning if detecting duplicate name here.
* Would be useful for agent developers...
*
* @param element
*/
private void maybeAddToNameMap(ContentElement element) {
if (element.hasName()) {
nameMap.put(element.getName(), element);
}
}
/**
* Sanity checks for an element
* @param e
* @return dummy boolean so the code can be run inside an assert statement
*/
private boolean checkHealthy(ContentElement e, boolean recursive) {
if (level.isAtLeast(Level.RENDERED)) {
if (LineRendering.isLocalParagraph(e)) {
if (e.getImplNodelet() == null) {
throw new AssertionError("Local paragraphs have no impl nodelet?");
}
if (!e.getImplNodelet().getTagName().equalsIgnoreCase(
DefaultParagraphHtmlRenderer.PARAGRAPH_IMPL_TAGNAME)
&& !e.getImplNodelet().getTagName().equalsIgnoreCase(
DefaultParagraphHtmlRenderer.LIST_IMPL_TAGNAME)) {
throw new AssertionError("Local paragraph impl nodelet is " +
e.getImplNodelet().getTagName());
}
if (e.getContainerNodelet() == null) {
throw new AssertionError("Local paragraphs have no container nodelet?");
}
}
if (e.getContainerNodelet() != null) {
for (ContentNode n = e.getFirstChild(); n != null; n = n.getNextSibling()) {
if (n.getImplNodelet() != null && !n.getImplNodelet().hasParentElement()) {
throw new AssertionError("Unattached impl nodelet for " + n + " in "+ e);
}
}
}
}
if (recursive) {
for (ContentNode n = e.getFirstChild(); n != null; n = n.getNextSibling()) {
if (n.getImplNodelet() != null && !n.getImplNodelet().hasParentElement()) {
if (n.isElement()) {
checkHealthy(n.asElement(), true);
}
}
}
}
return true;
}
/**
* Provides a parent for the GWT Widget if so needed by the doodad
* @param element
*/
private void maybeSetupGwtWidget(ContentElement element) {
if (element instanceof HasGwtWidget) {
((HasGwtWidget) element).setLogicalParent(logicalPanel);
}
}
/**
* Registers element for mode notifications if the it
* implements HasDisplayEditModes
*
* @param element
*/
private void maybeSetupModeNotifications(ContentElement element) {
if (editorPackage != null && DisplayEditModeHandler.hasListener(element)) {
DisplayEditModeHandler.onEditModeChange(element, editorPackage.inEditMode());
editorPackage.getElementsWithDisplayModes().add(element);
}
}
private void setupRenderer(AgentAdapter e, boolean isRendering) {
if (isRendering) {
Renderer renderer = registries.getElementHandlerRegistry().getRenderer(e);
e.setRenderer(renderer != null ? renderer : AgentAdapter.defaultRenderer);
} else {
e.clearRenderer();
}
}
// TODO(danilatos): Reconcile this with the old bundle, and have only one,
// once the interfaces have stabilised.
private final ExtendedClientDocumentContext context =
new ExtendedClientDocumentContext() {
FinalTaskRunner finalTaskRunner = new FinalTaskRunnerImpl() {
@Override protected void begin() {
beginDeferredMutation();
}
@Override protected void end() {
endDeferredMutation();
}
};
@Override
public LocalDocument<ContentNode, ContentElement, ContentTextNode> annotatableContent() {
return ContentDocument.this.getAnnotatableContent();
}
@Override
public CMutableDocument document() {
return ContentDocument.this.getMutableDoc();
}
@Override
public MutableAnnotationSet.Local localAnnotations() {
return ContentDocument.this.getLocalAnnotations();
}
@Override
public LocationMapper<ContentNode> locationMapper() {
return ContentDocument.this.getLocationMapper();
}
@Override
public ContentView persistentView() {
return ContentDocument.this.getPersistentView();
}
@Override
public ReadableDocumentView<ContentNode, ContentElement, ContentTextNode> hardView() {
return persistentContentView.hardView();
}
@Override
public TextNodeOrganiser<ContentTextNode> textNodeOrganiser() {
return ContentDocument.this.indexedDoc;
}
@Override
public ElementManager<ContentElement> elementManager() {
return ContentElement.ELEMENT_MANAGER;
}
@Override
public LowLevelEditingConcerns editing() {
return editingConcerns;
}
@Override
public String getDocumentId() {
return documentUniqueString;
}
@Override
public ContentElement getElementByName(String name) {
return nameMap.get(name);
}
@Override
public boolean isEditing() {
return editorPackage.inEditMode();
}
@Override
public RenderingConcerns rendering() {
return renderingConcerns;
}
@Override
public void scheduleFinally(Task task) {
finalTaskRunner.scheduleFinally(task);
}
@Override
public void beginDeferredMutation() {
EditorStaticDeps.startIgnoreMutations();
selectionMaintainer.saveSelection();
}
@Override
public void endDeferredMutation() {
selectionMaintainer.restoreSelection();
EditorStaticDeps.endIgnoreMutations();
}
};
/**
* Presents a view that skips over any ContentNode that does not have a ImpleNodelet.
*/
public static class RenderedContent
extends FilteredView<ContentNode, ContentElement, ContentTextNode>
implements ContentView {
/***/
public RenderedContent(ContentView rawView) {
super(rawView);
}
@Override
protected Skip getSkipLevel(ContentNode node) {
return node.isRendered() ? Skip.NONE : Skip.SHALLOW;
}
}
/**
* Full view of the content
*
* Wrapper to guard the local mutation methods with selection preservation,
* and for protection against potential naughty casting to RawDoc.
*/
public class FullContent
extends IdentityView<ContentNode, ContentElement, ContentTextNode>
implements ContentView, LocalDocument<ContentNode, ContentElement, ContentTextNode> {
FullContent(ReadableDocument<ContentNode, ContentElement, ContentTextNode> inner) {
super(inner);
}
@Override
public void transparentSetAttribute(ContentElement element, String name, String value) {
persistentContentView.transparentSetAttribute(element, name, value);
}
@Override
public ContentElement transparentCreate(String tagName,
Map<String, String> attributes, ContentElement parent,
ContentNode nodeAfter) {
selectionMaintainer.saveSelection();
try {
return persistentContentView.transparentCreate(tagName, attributes, parent, nodeAfter);
} finally {
selectionMaintainer.restoreSelection();
}
}
@Override
public ContentTextNode transparentCreate(String text, ContentElement parent,
ContentNode nodeAfter) {
selectionMaintainer.saveSelection();
try {
return persistentContentView.transparentCreate(text, parent, nodeAfter);
} finally {
selectionMaintainer.restoreSelection();
}
}
@Override
public void transparentMove(ContentElement newParent, ContentNode fromIncl,
ContentNode toExcl, ContentNode refChild) {
selectionMaintainer.saveSelection();
try {
persistentContentView.transparentMove(newParent, fromIncl, toExcl, refChild);
} finally {
selectionMaintainer.restoreSelection();
}
}
@Override
public void transparentUnwrap(ContentElement element) {
selectionMaintainer.saveSelection();
try {
persistentContentView.transparentUnwrap(element);
} finally {
selectionMaintainer.restoreSelection();
}
}
@Override
public void transparentDeepRemove(ContentNode node) {
selectionMaintainer.saveSelection();
try {
persistentContentView.transparentDeepRemove(node);
} finally {
selectionMaintainer.restoreSelection();
}
}
@Override
public <T> T getProperty(Property<T> property, ContentElement element) {
return persistentContentView.getProperty(property, element);
}
@Override
public boolean isDestroyed(ContentElement element) {
return persistentContentView.isDestroyed(element);
}
@Override
public <T> void setProperty(Property<T> property, ContentElement element, T value) {
persistentContentView.setProperty(property, element, value);
}
@Override
public ContentNode transparentSlice(ContentNode splitAt) {
return persistentContentView.transparentSlice(splitAt);
}
@Override
public void onBeforeFilter(Point<ContentNode> at) {
persistentContentView.onBeforeFilter(at);
}
@Override
public String toString() {
return "FullContentView: " + getDocumentElement().toString();
}
@Override
public void markNodeForPersistence(ContentNode localNode, boolean lazy) {
selectionMaintainer.saveSelection();
try {
persistentContentView.markNodeForPersistence(localNode, lazy);
} finally {
selectionMaintainer.restoreSelection();
}
}
@Override
public boolean isTransparent(ContentNode node) {
return persistentContentView.isTransparent(node);
}
}
/**
* Factory for creating our substrate, by creating our full raw document and boxing it
* in our persistent raw doc wrapper.
*/
public class PersistentContentDoc extends PersistentContentView {
/** NOTE(patcoleman): Not final for circular construction issues. Only set once. */
private LazyPersistenceManager persistenceManager;
private boolean isInsideFilter; /* Flag to avoid callbacks within the filter. */
/** Constructor */
public PersistentContentDoc(ContentRawDocument fullRawSubstrate) {
super(fullRawSubstrate);
}
/** Set the persistence manager after construction - only can be called once. */
void setPersistenceManager(LazyPersistenceManager persistenceManager) {
Preconditions.checkArgument(persistenceManager != null,
"Can't use a null persistence manager.");
Preconditions.checkState(this.persistenceManager == null,
"Can't set persistence manager twice.");
this.persistenceManager = persistenceManager;
}
@Override
protected void schedulePaint(final ContentNode node) {
nodesToRepaint.add(node);
}
@Override
public void markNodeForPersistence(ContentNode localNode, boolean lazy) {
persistenceManager.markAsLazyPersisted(localNode);
if (!lazy) {
persistenceManager.updateLazyNodes(localNode);
}
}
@Override
public void onBeforeFilter(Point<ContentNode> at) {
if (isConsistent && !isInsideFilter) {
isInsideFilter = true;
persistenceManager.updateLazyNodes(at.getContainer());
isInsideFilter = false;
}
}
@Override
public ContentElement createElement(String tagName, Map<String, String> attributes,
ContentElement parent, ContentNode nodeAfter) {
// delegate when appropriate:
if (persistenceManager.isCreationDelegate()) {
ContentElement node = persistenceManager.createElement(
tagName, attributes, parent, nodeAfter);
schedulePaint(node);
return node;
}
return super.createElement(tagName, attributes, parent, nodeAfter);
}
@Override
public ContentTextNode createTextNode(String data, ContentElement parent,
ContentNode nodeAfter) {
// delegate when appropriate:
if (persistenceManager.isCreationDelegate()) {
ContentTextNode node = persistenceManager.createTextNode(data, parent, nodeAfter);
schedulePaint(node);
return node;
}
return super.createTextNode(data, parent, nodeAfter);
}
}
/**
* Presents a view that skips over any HTML node that does not have a back reference to a
* ContentNode.
*/
public class FilteredHtml extends FilteredView<Node, Element, Text>
implements HtmlView, TransparentManager<Element> {
private final List<Element> invadingElements = new ArrayList<Element>();
FilteredHtml(ReadableDocument<Node, Element, Text> rawView) {
super(rawView);
}
@Override
protected Skip getSkipLevel(Node node) {
// TODO(danilatos): Detect and repair new elements. Currently we just ignore them.
if (DomHelper.isTextNode(node) || NodeManager.hasBackReference(node.<Element>cast())) {
return Skip.NONE;
} else {
Element element = node.<Element>cast();
Skip level = NodeManager.getTransparency(element);
if (level == null) {
if (!getDocumentElement().isOrHasChild(element)) {
return Skip.INVALID;
}
register(element);
}
// For now, we treat unknown nodes as shallow as well.
// TODO(danilatos): Either strip them or extract them
return level == null ? Skip.SHALLOW : level;
}
}
@Override
protected Node getNextOrPrevNodeDepthFirst(ReadableDocument<Node, Element, Text> doc,
Node start, Node stopAt, boolean enter, boolean rightwards) {
if (!DomHelper.isTextNode(start) && start.<Element>cast().getPropertyBoolean(
ContentElement.COMPLEX_IMPLEMENTATION_MARKER)) {
// If the nodelet is marked as part of a complex implementation structure
// (e.g. a part of an image thumbnail's implementation), then we find the
// next html node by instead popping up into the filtered content view,
// getting the next node from there, and popping back down into html land.
// This should be both faster and more accurate than doing a depth first
// search through the impl dom of an arbitrarily complex doodad.
// Go upwards to find the backreference to the doodad wrapper
Element e = start.cast();
while (!NodeManager.hasBackReference(e)) {
e = e.getParentElement();
}
// This must be true, otherwise we could get into an infinite loop
assert (start == stopAt || !start.isOrHasChild(stopAt));
// Try to get a wrapper for stopAt as well.
// TODO(danilatos): How robust is this?
// What are the chances that it would ever fail in practice anyway?
ContentNode stopAtWrapper = null;
for (int tries = 1; tries <= 3; tries++) {
try {
stopAtWrapper = nodeManager.findNodeWrapper(stopAt);
break;
} catch (InconsistencyException ex) {
stopAt = stopAt.getParentElement();
}
}
// Do a depth first next-node-find in the content view
ContentNode next = DocHelper.getNextOrPrevNodeDepthFirst(renderedContentView,
NodeManager.getBackReference(e),
stopAtWrapper, enter, rightwards);
// return the impl nodelet
return next != null ? next.getImplNodelet() : null;
} else {
// In other cases, just do the default.
return super.getNextOrPrevNodeDepthFirst(doc, start, stopAt, enter, rightwards);
}
}
/**
* {@inheritDoc}
*/
public Element needToSplit(Element transparentNode) {
Element e = transparentNode.cloneNode(false).cast();
register(e);
return e;
}
/**
* Clear out any dodgy browser-inserted elements
*/
public void clearInvadingElements() {
TransparencyUtil.clear(invadingElements);
}
/**
* Forget the existence of dodgy browser-inserted elements
*/
public void forgetInvadingElements() {
invadingElements.clear();
}
/**
* @return list of dodgy browser-inserted elements
*/
public List<Element> getInvadingElements() {
return invadingElements;
}
/**
* Register this element as an invading element (i.e. the browser put it there)
* We'll note it and look after it as a transparent node, rather than just
* removing it. Then some other smarter code can come and decide what to do with it.
* @param element
*/
private void register(Element element) {
NodeManager.setTransparency(element, Skip.SHALLOW);
NodeManager.setTransparentBackref(element, this);
invadingElements.add(element);
}
}
private boolean applyingToDocument = false;
/**
* Local annotation set, changes to which are not sent out, but it is implemented
* in the same data structure as the persistent annotation set.
*/
private class LocalAnnotationSet extends LocalAnnotationSetImpl
implements DiffHighlightTarget {
LocalAnnotationSet(RawAnnotationSet<Object> fullAnnotationSet) {
super(fullAnnotationSet);
}
@Override
public void startLocalAnnotation(String key, Object value) {
checkLocalKey(key);
fullAnnotationSet.startAnnotation(key, value);
}
@Override
public void endLocalAnnotation(String key) {
checkLocalKey(key);
fullAnnotationSet.endAnnotation(key);
}
@Override
public ContentNode getCurrentNode() {
return indexedDoc.getCurrentNode();
}
@Override
public void consume(DocOp op) throws OperationException {
try {
ContentDocument.this.consume(op, false, true);
} catch (OperationRuntimeException e) {
throw e.get();
}
}
@Override
public boolean isApplyingToDocument() {
return applyingToDocument;
}
}
private class ContentIndexedDoc
extends IndexedDocumentImpl<ContentNode, ContentElement, ContentTextNode, Void> {
ContentIndexedDoc(PersistentContentDoc doc, RawAnnotationSet<Object> annotations,
DocumentSchema schema) {
super(doc, annotations, schema);
}
@Override
public ContentNode getCurrentNode() {
// NOTE(danilatos): reimplemented to promote visibility to public.
return super.getCurrentNode();
}
/**
* This method currently also handle nodes not in the persistent view, in which
* case it adjusts in a forwards direction.
*
* {@inheritDoc}
*/
// TODO(danilatos): A nicer way than munging it here... somehow ensure
// that we never get invalid points in the first place
@Override
public int getLocation(ContentNode node) {
node = persistentContentView.getVisibleNodeFirst(node);
return node == null ? size() : super.getLocation(node);
}
// TODO(mtsui/danilatos): clean this up later
private final LocationMapper<ContentNode> indexedDocLocationMapper =
new LocationMapper<ContentNode>() {
@Override
public int getLocation(ContentNode node) {
return ContentIndexedDoc.super.getLocation(node);
}
@Override
public int getLocation(Point<ContentNode> point) {
return ContentIndexedDoc.super.getLocation(point);
}
@Override
public Point<ContentNode> locate(int location) {
return ContentIndexedDoc.super.locate(location);
}
@Override
public int size() {
return ContentIndexedDoc.super.size();
}
};
/**
* This method currently also handle nodes not in the persistent view, in which
* case it adjusts in a forwards direction.
*
* {@inheritDoc}
*/
@Override
public int getLocation(Point<ContentNode> point) {
return DocHelper.getFilteredLocation(
indexedDocLocationMapper, persistentContentView, point);
}
@Override
protected Void evaluate() {
return super.evaluate();
}
@Override
public ContentTextNode splitText(ContentTextNode textNode, int offset) {
selectionMaintainer.saveSelection();
try {
return super.splitText(textNode, offset);
} finally {
selectionMaintainer.restoreSelection();
}
}
@Override
public ContentTextNode mergeText(ContentTextNode secondSibling) {
selectionMaintainer.saveSelection();
try {
return super.mergeText(secondSibling);
} finally {
selectionMaintainer.restoreSelection();
}
}
}
private Element rootElement;
private final RenderedContent renderedContentView;
private final PersistentContentDoc persistentContentView;
private final FullContent fullContentView;
private final LazyPersistenceManager lazyPersistenceManager;
private HtmlView rawHtmlView, strippingHtmlView;
private FilteredHtml filteredHtmlView;
private final LocalAnnotationSet localAnnotations;
// Temporarily here during this stage of reshuffling
private Repairer repairer;
private NodeManager nodeManager;
private final ContentIndexedDoc indexedDoc;
/**
* This object allows operations to be grouped together and handles selections
* and suboperations. Operations it sequences are applied to the document and
* also passed on to the given sink.
*/
private final EditorOperationSequencer sequencer;
private final CMutableDocument mutableContent;
private MiniBundle editorPackage;
private LogicalPanel logicalPanel;
private final RawAnnotationSet<Object> fullAnnotationSet;
private final SelectionMaintainer selectionMaintainer;
private final FilteredView<ContentNode, ContentElement, ContentTextNode> selectionContent;
private Registries registries;
private final ContentRawDocument fullRawSubstrate;
private final RepairListener repairListener = new RepairListener() {
@Override
public void onFullDocumentRevert(
ReadableDocument<ContentNode, ContentElement, ContentTextNode> doc) {
if (editorPackage != null) {
editorPackage.getRepairListener().onFullDocumentRevert(doc);
}
}
@Override
public void onRangeRevert(El<ContentNode> start, El<ContentNode> end) {
if (editorPackage != null) {
editorPackage.getRepairListener().onRangeRevert(start, end);
}
}
};
/**
* Constructs a content document with initial registry information, content and a schema.
*/
public ContentDocument(Registries initialRegistries, DocInitialization initialState,
DocumentSchema schema) {
this(schema);
setRegistries(initialRegistries);
consume(initialState);
}
/**
* Sets up all documents and handlers needed for the content document.
*/
public ContentDocument(DocumentSchema documentSchema) {
AnnotationSetListener<Object> annotationListener = new AnnotationSetListener<Object>() {
public void onAnnotationChange(int start, int end, String key, Object newValue) {
EditorStaticDeps.startIgnoreMutations();
try {
Iterator<AnnotationMutationHandler> handlers =
registries.getAnnotationHandlerRegistry().getHandlers(key);
while (handlers.hasNext()) {
try {
handlers.next().handleAnnotationChange(
getContext(), start, end, key, newValue);
} catch (Exception e) {
// Swallow exceptions from mutation handlers - we don't want to
// corrupt the document by allowing an exception to escape here.
EditorStaticDeps.logger.error().log("Exception from annotation change handler", e);
}
}
} finally {
EditorStaticDeps.endIgnoreMutations();
}
}
};
// {
// NOTE(user): This check prevents paste/typing extraction from working
// TODO(danilatos): Fix this and add it back.
// @Override
// public void begin() {
// if (!isConsistent()) {
// throw new IllegalStateException("Editor is in a transient state, you " +
// "may not use the mutable document at this point (try deferring)");
// }
// super.begin();
// }
// };
// TODO(danilatos): Parametrise this element, probably in a renderer for
// the doc root element.
ContentElement contentRoot = new AgentAdapter(
ContentDocElement.DEFAULT_TAGNAME, Attributes.EMPTY_MAP,
// HACK(danilatos): Circular dependency, use root handler registry for now.
context, Editor.ROOT_HANDLER_REGISTRY);
sequencer = new EditorOperationSequencer(nindoSink);
fullAnnotationSet = annotationFactory.create(annotationListener);
this.localAnnotations = new LocalAnnotationSet(fullAnnotationSet);
fullRawSubstrate = new ContentRawDocument(contentRoot, factory);
fullContentView = new FullContent(fullRawSubstrate);
renderedContentView = new RenderedContent(fullRawSubstrate);
// NOTE(patcoleman): these rely on eachother, hence the fourth step not being in construction.
persistentContentView = new PersistentContentDoc(fullRawSubstrate);
indexedDoc = new ContentIndexedDoc(persistentContentView, fullAnnotationSet, documentSchema);
lazyPersistenceManager = new LazyPersistenceManager(
outgoingRepaintSink, fullContentView, indexedDoc, persistentContentView, indexedDoc);
persistentContentView.setPersistenceManager(lazyPersistenceManager);
selectionMaintainer = new SelectionMaintainer(indexedDoc);
mutableContent = new CMutableDocument(sequencer, indexedDoc);
selectionContent = ValidSelectionStrategy.buildSelectionFilter(
persistentContentView, renderedContentView);
}
public void setOutgoingSink(SilentOperationSink<? super DocOp> outgoingOperationSink) {
Preconditions.checkNotNull(outgoingOperationSink, "Outgoing operation sink cannot be null");
Preconditions.checkState(this.outgoingOperationSink == SilentOperationSink.Void.get(),
"Already has a sink");
this.outgoingOperationSink = outgoingOperationSink;
}
/**
* Replaces the existing outgoing sink with a new one.
*
* @param newSink new sink to use
* @return previous sink
*/
public SilentOperationSink<? super DocOp> replaceOutgoingSink(
SilentOperationSink<? super DocOp> newSink) {
Preconditions.checkState(outgoingOperationSink != null, "");
SilentOperationSink<? super DocOp> oldSink = outgoingOperationSink;
outgoingOperationSink = newSink;
return oldSink;
}
/**
* Synchronously flushes any annotation painting that may have been deferred.
*/
public void flushAnnotationPainting() {
AnnotationPainter.flush(context);
}
/**
* Brings the editor into a consistent state, possibly asynchronously.
*
* @param resume command to run later once the editor has reached consistency,
* only if it is not consistent right now
* @return true if the editor is already consistent, false otherwise. If this
* method returns true, {@code resume} will not be executed. If this
* method returns {@code false}, {@code resume} will be executed once
* the editor is consistent.
*/
public boolean flush(Runnable resume) {
return editorPackage != null ? editorPackage.flush(resume) : true;
}
public void setShelved() {
Level oldLevel = adjustLevel(Level.SHELVED);
setupBehaviour(fullRawSubstrate.getDocumentElement(), oldLevel);
}
/**
* Transitions this document to/from a rendering state.
*/
public void setRendering() {
// setupBehaviour deactivates and re-activates rendering, does re-creation
// of GWT widgets, clobbers event handlers etc. Skip it if possible.
if (level == Level.RENDERED) {
return;
}
Level oldLevel = adjustLevel(Level.RENDERED);
setupBehaviour(fullRawSubstrate.getDocumentElement(), oldLevel);
}
/**
* Downgrades the current level to interactive.
*
* @throws IllegalStateException if the current level is not at or above
* interactive.
*/
public void setInteractive() {
Preconditions.checkState(logicalPanel != null, "Don't have a logicalPanel");
assert level.isAtLeast(Level.INTERACTIVE);
adjustLevel(Level.INTERACTIVE);
// No need to setupBehaviour, nothing to do if already interactive, or just
// leaving edit mode.
}
public void setInteractive(LogicalPanel logicalPanel) {
Preconditions.checkNotNull(logicalPanel, "Null logicalPanel");
if (this.logicalPanel == logicalPanel) {
this.setInteractive();
return;
}
this.logicalPanel = logicalPanel;
Level oldLevel = adjustLevel(Level.INTERACTIVE);
setupBehaviour(fullRawSubstrate.getDocumentElement(), oldLevel);
}
/**
* Puts the document into the new level. Does not traverse the document
* to re-render elements and so forth.
*
* @return the old level for convenience
*/
private Level adjustLevel(Level newLevel) {
Level old = level;
for (int i = level.ordinal() + 1; i <= newLevel.ordinal(); i++) {
Level currentLevel = Level.values()[i];
switch (currentLevel) {
case RENDERED:
if (!fullRawSubstrate.getAffectHtml()) {
fullRawSubstrate.setAffectHtml();
}
break;
case INTERACTIVE:
assert logicalPanel != null;
Preconditions.checkState(fullRawSubstrate.getAffectHtml(),
"rendered or higher state should imply affectHtml");
break;
case EDITING:
// Most is done in attachEditor()
break;
default:
throw new AssertionError("Unknown level " + currentLevel);
}
}
for (int i = level.ordinal() - 1; i >= newLevel.ordinal(); i--) {
Level currentLevel = Level.values()[i];
switch (currentLevel) {
case INTERACTIVE:
assert editorPackage != null;
editingConcerns = LowLevelEditingConcerns.STUB;
editorPackage = null;
selectionMaintainer.detachEditor();
break;
case RENDERED:
logicalPanel = null;
break;
case SHELVED:
if (fullRawSubstrate.getAffectHtml()) {
fullRawSubstrate.clearAffectHtml();
}
AnnotationPainter.clearDocPainter(context);
break;
default:
throw new AssertionError("Unknown level " + currentLevel);
}
}
level = newLevel;
return old;
}
public void initRootElementRendering(boolean isRendering) {
if ((rootElement != null) == isRendering) {
return;
}
ContentElement root = fullRawSubstrate.getDocumentElement();
if (isRendering) {
this.rootElement = root.getImplNodelet();
rawHtmlView = new HtmlViewImpl(rootElement);
filteredHtmlView = new FilteredHtml(rawHtmlView);
strippingHtmlView = new StrippingHtmlView(rootElement);
repairer = new Repairer(persistentContentView, renderedContentView, strippingHtmlView,
repairListener);
nodeManager = new NodeManager(filteredHtmlView, renderedContentView, repairer);
assert rootElement == fullRawSubstrate.getDocumentElement().getImplNodelet();
} else {
this.rootElement = null;
rawHtmlView = null;
filteredHtmlView = null;
strippingHtmlView = null;
repairer = null;
nodeManager = null;
}
}
public enum Level {
/** Completely unrendered, no HTML manipulations (faster and smaller memory footprint) */
SHELVED,
/** Rendered */
RENDERED,
/** Event handlers attached */
INTERACTIVE,
/** Editor attached */
EDITING;
public boolean isAtLeast(Level other) {
return this.compareTo(other) >= 0;
}
public boolean isAtMost(Level other) {
return this.compareTo(other) <= 0;
}
}
private Level level = Level.SHELVED;
public Level getLevel() {
return level;
}
/**
* Attaches an editor to this document.
*
* Note that this method does not yet support attaching an editor if the
* document already has a DOM built up.
*
* @param editorBundle
*/
// TODO(danilatos): Ultimately, remove the document's explicit knowledge
// of editors altogether.
public void attachEditor(MiniBundle editorBundle, LogicalPanel panel) {
Preconditions.checkNotNull(editorBundle, "editorBundle must not be null");
Preconditions.checkState(level != Level.EDITING,
"Cannot attach editor to a document already with an editor");
if (panel == null) {
Preconditions.checkState(this.logicalPanel != null,
"Must either already have a logical panel, or one must be provided");
} else {
this.logicalPanel = panel;
}
Level oldLevel = adjustLevel(Level.EDITING);
this.editorPackage = editorBundle;
editingConcerns = new LowLevelEditingConcerns() {
@Override
public SelectionHelper getSelectionHelper() {
// WARNING(danilatos): THIS SHOULD ALWAYS BE THE PASSIVE SELECTION HELPER
// (As opposed to the aggressive one). Otherwise, lots of subtle bug-inducing
// side effects could occur.
return editorPackage.getPassiveSelectionHelper();
}
@Override
public TypingExtractor getTypingExtractor() {
return editorPackage.getTypingExtractor();
}
@Override
public void textNodeletAffected(Text nodelet, int affectedAfterOffset, int insertionAmount,
TextNodeChangeType changeType) {
selectionMaintainer.textNodeletAffected(nodelet, affectedAfterOffset, insertionAmount,
changeType);
}
@Override
public SuggestionsManager getSuggestionsManager() {
return editorPackage.getSuggestionsManager();
}
/** Returns true */
@Override
public boolean hasEditor() {
return true;
}
@Override
public EditorContext editorContext() {
return editorPackage.getEditorContext();
}
};
selectionMaintainer.attachEditor(editingConcerns);
setupBehaviour(fullRawSubstrate.getDocumentElement(), oldLevel);
}
public void setRegistries(Registries registriesBundle) {
registries = registriesBundle;
if (registries != null) {
AnnotationPainter.createAndSetDocPainter(getContext(), registries.getPaintRegistry());
} else {
AnnotationPainter.clearDocPainter(getContext());
}
}
public Registries getRegistries() {
return registries;
}
/**
* @return The operation sequencer used by the mutable document
*/
public EditorOperationSequencer getOpSequencer() {
return sequencer;
}
/**
* NOTE(danilatos): This method temporary.
*
* @return repairer
*/
public Repairer getRepairer() {
return repairer;
}
/**
* @return node manager
*/
public NodeManager getNodeManager() {
return nodeManager;
}
/**
* @return the persistent view of the document expressed as an operation
*/
public DocInitialization asOperation() {
return indexedDoc.asOperation();
}
/**
* @return the document schema
*/
public DocumentSchema getSchema() {
return indexedDoc.getSchema();
}
/**
* @return location mapper for the persistent view
*/
public LocationMapper<ContentNode> getLocationMapper() {
return indexedDoc;
}
/**
* @return the document context bundle for this document
*/
public ClientDocumentContext getContext() {
return context;
}
/**
* Mutable document of this content doc
*/
public CMutableDocument getMutableDoc() {
return mutableContent;
}
/**
* @see RenderedContent
*/
public ContentView getRenderedView() {
return renderedContentView;
}
/**
* @see PersistentContent
*/
public ContentView getPersistentView() {
return persistentContentView;
}
/**
* @see FullContent
*/
public ContentView getFullContentView() {
return fullContentView;
}
/**
* @see LocalDocument
*/
public LocalDocument<ContentNode, ContentElement, ContentTextNode>
getAnnotatableContent() {
return fullContentView;
}
/**
* @see HtmlViewImpl
*/
public HtmlView getRawHtmlView() {
return rawHtmlView;
}
/**
* @see FilteredHtml
*/
public FilteredHtml getFilteredHtmlView() {
return filteredHtmlView;
}
/**
* @see StrippingHtmlView
*/
public HtmlView getStrippingHtmlView() {
return strippingHtmlView;
}
/**
* Get the view for valid selection placement.
*/
public ReadableDocumentView<ContentNode, ContentElement, ContentTextNode> getSelectionFilter() {
return selectionContent;
}
/**
* Get local annotations. Hack?
*/
public MutableAnnotationSet.Local getLocalAnnotations() {
return localAnnotations;
}
/**
* Gets a validator to check schema.
*/
public Validator getValidator() {
return indexedDoc;
}
/**
* "Sources" an operation, which means applying it locally and sending it out
* on the wire.
*
* This variant does not affect the html, or move the selection.
*/
public void sourceNindoWithoutModifyingHtml(Nindo nindo) {
clearAffectHtml();
try {
// NOTE(user): SelectionMaintainer does not know how to save selection
// when the DOM is already modified, so don't try to save and restore the
// selection here.
// Typing extractor already places the selection
// correctly.
dontSaveSelectionNindoSink.consumeAndReturnInvertible(nindo);
} finally {
setAffectHtml();
}
}
/**
* Applies the op locally and also sends it out on the wire.
* @param nindo
*/
public void sourceNindo(Nindo nindo) {
nindoSink.consumeAndReturnInvertible(nindo);
}
//// Begin DocumentOperationSink impl ////
/**
* Applies an operation while disabling DOM mutation events
*
* Callers should ensure that if the editor is attached, it should be in a
* consistent state, before calling this method.
*/
public void consume(DocOp operation) {
consume(operation, false, true);
notifyListener(operation);
}
private boolean isConsistent = true;
private DocOp consumeLocal(Nindo nindo, boolean saveSelection) {
if (!isConsistent) {
throw new IllegalStateException("Document is not in a consistent state - " +
"must have died during a previous bad op");
}
// First, validate ops. This is especially important for ops we have sourced,
// which could be wrong. This should catch most errors before corrupting the
// document and the CC state (as the server should reject the invalid op as well).
if (validateLocalOps) {
try {
indexedDoc.maybeThrowOperationExceptionFor(nindo);
} catch (OperationException e1) {
dealWithBadOp(true, nindo, e1, false);
}
}
isConsistent = false;
beginConsume(saveSelection);
DocOp op = null;
try {
op = indexedDoc.consumeAndReturnInvertible(nindo, false);
} catch (OperationException e) {
dealWithBadOp(true, nindo, e, true);
} catch (RuntimeException e) {
dealWithBadOp(true, nindo, e, true);
}
endConsume(saveSelection, op);
return op;
}
private void consume(DocOp operation, boolean isLocal, boolean saveSelection) {
if (!isConsistent) {
throw new IllegalStateException("Document is not in a consistent state - " +
"must have died during a previous bad op");
}
// First, validate ops. This is especially important for ops we have sourced,
// which could be wrong. This should catch most errors before corrupting the
// document and the CC state (as the server should reject the invalid op as well).
try {
if (isLocal) {
indexedDoc.maybeThrowOperationExceptionFor(operation);
}
} catch (OperationException e1) {
dealWithBadOp(isLocal, operation, e1, false);
}
isConsistent = false;
beginConsume(saveSelection);
try {
applyingToDocument = true;
try {
indexedDoc.consume(operation, false);
} finally {
applyingToDocument = false;
}
} catch (OperationException e) {
dealWithBadOp(isLocal, operation, e, true);
} catch (RuntimeException e) {
dealWithBadOp(isLocal, operation, e, true);
}
endConsume(saveSelection, operation);
}
/**
* Deals with a bad operation and throws the appropriate exception.
*
* Does not return.
*
* @param isLocal
* @param operation either an OperationException or a RuntimeException.
* @param e
* @param probableCorruption
*/
private void dealWithBadOp(
boolean isLocal, Object operation, Exception e, boolean probableCorruption) {
// Project the exception onto one of its two possible states.
OperationException oe;
RuntimeException re;
if (e instanceof OperationException) {
oe = (OperationException) e;
re = null;
} else {
oe = null;
re = (RuntimeException) e;
}
String msg = (probableCorruption ? "DEATH: " : "")
+ "Invalid " + (isLocal ? "LOCAL" : "REMOTE") + " operation: " + operation
+ (oe != null && oe.hasViolationsInformation()
? " Violation: " + oe.getViolations().firstDescription()
: " <No violation information!> ")
+ "Exception: " + e
+ " IndexedDoc: " + indexedDoc.toString();
EditorStaticDeps.logger.error().logPlainText(msg);
RuntimeException death;
if (oe == null) {
death = re;
} else if (isLocal && !probableCorruption) {
assert oe.hasViolationsInformation();
if (oe.getViolations().getValidationResult().isInvalidSchema()) {
death = new SchemaViolatingLocalOperationException(oe.getViolations());
} else {
death = new BadOpLocalOperationException(oe.getViolations());
}
} else {
death = new OperationRuntimeException("Invalid for current document", oe);
}
// If the op failed, the document may be arbitrarily broken state, so there
// is no reason to believe that a repair will work, let alone execute
// without throwing its own exceptions. Nevertheless, since we're about to
// kill the universe by throwing an unchecked exception anyway, it can't
// hurt to try.
if (repairer != null) {
try {
repairer.revert(fullContentView, fullContentView.getDocumentElement());
} catch (RuntimeException ex) {
// Ignore, since a system-killing exception is about to be thrown anyway.
}
}
throw death;
}
private void beginConsume(boolean saveSelection) {
if (selectionMaintainer.isNested()) {
EditorStaticDeps.logger.error().log("Selection save/restore imbalance!");
// Recover...
selectionMaintainer.hackForceClearDepth();
}
if (saveSelection) {
selectionMaintainer.saveSelection();
} else {
selectionMaintainer.startDontSaveSelection();
}
assert nodesToRepaint.isEmpty();
}
private void endConsume(boolean saveSelection, DocOp opApplied) {
flushNodeRepaint();
assert debugCheckHealthy();
if (saveSelection) {
selectionMaintainer.restoreSelection(opApplied);
} else {
selectionMaintainer.endDontSaveSelection();
}
isConsistent = true;
}
/**
* Notifies the listener of an incoming op.
* @param opApplied
*/
private void notifyListener(DocOp opApplied) {
if (editorPackage != null) {
editorPackage.onIncomingOp(opApplied);
}
}
private void flushNodeRepaint() {
for (ContentNode n : nodesToRepaint) {
repaintNode(n);
}
nodesToRepaint.clear();
}
public DiffHighlightTarget getDiffTarget() {
return localAnnotations;
}
@Override
public String toString() {
return indexedDoc.toString();
}
/**
* @see ContentRawDocument#clearAffectHtml()
*/
private void clearAffectHtml() {
persistentContentView.clearAffectHtml();
}
/**s
* @see ContentRawDocument#clearAffectHtml()
*/
private void setAffectHtml() {
persistentContentView.setAffectHtml();
}
/** Check that the document is fine. For now, just check the line container is fine. */
public boolean debugCheckHealthy() {
if (performExpensiveChecks) {
for (ContentElement element : DocIterate.deepElements(
indexedDoc, indexedDoc.getDocumentElement(), null)) {
if (LineContainers.isLineContainer(indexedDoc, element)) {
return LineContainerParagraphiser.containerIsHealthyStrong(element);
}
}
}
return true;
}
public boolean debugCheckHealthy2() {
checkHealthy(fullRawSubstrate.getDocumentElement(), true);
return debugCheckHealthy();
}
private void repaintNode(ContentNode node) {
if (node.isTextNode()) {
ContentTextNode textNode = node.asText();
if (textNode.getParentElement() != null) {
int start = indexedDoc.getLocation(textNode);
AnnotationPainter.maybeScheduleRepaint(context,
start, start + textNode.getLength());
}
} else {
ContentElement element = node.asElement();
int start = indexedDoc.getLocation(Point.start(indexedDoc, element));
int end = indexedDoc.getLocation(Point.<ContentNode>end(element));
AnnotationPainter.maybeScheduleRepaint(context,
start, end);
}
}
public CMutableDocument createSequencedDocumentWrapper(OperationSequencer<Nindo> sequencer) {
return new CMutableDocument(sequencer, indexedDoc);
}
// Try to get rid of these debug methods eventually
/**
* DO NOT USE EXCEPT FOR TESTING!!!
*/
public ContentRawDocument debugGetRawDocument() {
return fullRawSubstrate;
}
/**
* DO NOT USE EXCEPT FOR TESTING!!!
*/
public ExtendedClientDocumentContext debugGetContext() {
return context;
}
}