/**
* 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.extract;
import com.google.common.annotations.VisibleForTesting;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.StyleInjector;
import org.waveprotocol.wave.client.debug.logger.LogLevel;
import org.waveprotocol.wave.client.editor.EditorStaticDeps;
import org.waveprotocol.wave.client.editor.RestrictedRange;
import org.waveprotocol.wave.client.editor.content.ContentElement;
import org.waveprotocol.wave.client.editor.content.ContentNode;
import org.waveprotocol.wave.client.editor.content.ContentTextNode;
import org.waveprotocol.wave.client.editor.content.ContentView;
import org.waveprotocol.wave.client.editor.extract.InconsistencyException.HtmlInserted;
import org.waveprotocol.wave.client.editor.extract.InconsistencyException.HtmlMissing;
import org.waveprotocol.wave.client.editor.impl.HtmlView;
import org.waveprotocol.wave.client.scheduler.ScheduleTimer;
import org.waveprotocol.wave.model.document.ReadableDocument;
import org.waveprotocol.wave.model.document.util.Point;
/**
* Repairs inconsistencies between content and html, in both directions.
*
* TODO(danilatos): This class is just a loosely related collection of methods,
* and does not encapsulate any state other than a reference to its bundle. Perhaps
* break it up, or convert to static methods?
*
* @author danilatos@google.com (Daniel Danilatos)
*/
public class Repairer {
/** For testing/debug purposes, treat repairs as fatal errors */
@VisibleForTesting public static boolean debugRepairIsFatal = getAssertionsOn();
private static boolean getAssertionsOn() {
try {
assert false;
} catch (AssertionError e) {
return true;
}
return false;
}
/**
* Resources for repairing & problem finding
*/
public static final ExtractResources resources;
static {
if (GWT.isClient()) {
resources = GWT.create(ExtractResources.class);
StyleInjector.inject(resources.css().getText(), true);
} else {
resources = null;
}
}
private final ContentView persistentView, renderedView;
private final HtmlView strippingView;
private final RepairListener listener;
/**
* @param persistentView
* @param renderedView
* @param strippingView
*/
public Repairer(ContentView persistentView, ContentView renderedView, HtmlView strippingView,
RepairListener listener) {
this.persistentView = persistentView;
this.renderedView = renderedView;
this.strippingView = strippingView;
this.listener = listener;
}
/**
* Generic handler for any inconsistency exception
* @param error
*/
public void handle(InconsistencyException error) {
// Do something nicer (visitor?) if we end up with more
// inconsistency exception types
if (error instanceof HtmlInserted) {
handleInserted((HtmlInserted) error);
} else {
handleMissing((HtmlMissing) error);
}
}
/**
* Specific handler for {@link HtmlInserted}
* Currently just reverts the HTML
* @param error
*/
public void handleInserted(HtmlInserted error) {
EditorStaticDeps.logger.error().log("handleInserted: ", error);
assert false : "Repairer triggered, handleInserted";
revert(error.getContentPoint(), null);
}
/**
* Specific handler for {@link HtmlMissing}
* Currently just reverts the HTML
* @param error
*/
public void handleMissing(HtmlMissing error) {
EditorStaticDeps.logger.error().log("handleMissing: ", error);
assert false : "Repairer triggered, handleMissing";
revert(Point.before(renderedView, error.getBrokenNode()), null);
}
/**
* Revert the HTML implementation between two node-bounded points.
* The points must be in the container element.
* TODO(danilatos): Change interface to use RestrictedRange, or improve
* implementation to not require restriction.
*
* @param start
* @param end if null, keep going until consistency is found
*/
@SuppressWarnings("deprecation")
private void revertWithoutNotification(Point.El<ContentNode> start, Point.El<ContentNode> end) {
if (debugRepairIsFatal) {
throw new RuntimeException("Repair is fatal");
}
if (start.getContainer() == null) {
revert(renderedView, renderedView.getDocumentElement());
return;
}
EditorStaticDeps.startIgnoreMutations();
try {
// TODO(danilatos): Log all calls to here, & find causes.
// In the ideal universe, there would be none!
EditorStaticDeps.logger.error().logPlainText("REPAIRING!! " + start.getContainer());
flashShowRepair((ContentElement) start.getContainer());
try {
revertInner(start, end);
return;
} catch (RuntimeException t1) {
EditorStaticDeps.logger.error().logPlainText("exception while revertInner: " + t1);
for (ContentNode attempt = start.getContainer(); attempt != null; attempt =
attempt.getParentElement()) {
try {
attempt.revertImplementation();
return;
} catch (RuntimeException t2) {
EditorStaticDeps.logger.error().logPlainText(
"Exception while revertImplementation: " + t2);
// iterate
if (attempt == persistentView.getDocumentElement()) {
// We cannot reconstruct the document. Re-throw the original exception from
// the first repair attempt so we get as meaningful an error message and
// stacktrace as possible.
throw t1;
}
}
}
}
} finally {
EditorStaticDeps.endIgnoreMutations();
}
EditorStaticDeps.logger.trace().logPlainText("Revert successful");
}
/**
* Same as {@link #revertWithoutNotification(Point.El, Point.El)}, but first notifies listener.
*/
public void revert(Point.El<ContentNode> start, Point.El<ContentNode> end) {
// notify and fix
listener.onRangeRevert(start, end);
revertWithoutNotification(start, end);
}
/**
* Reverts the entire content of a particular element
* @param doc The document containing the element to revert
* @param element The element to revert
*/
public void revert(ReadableDocument<ContentNode, ContentElement, ContentTextNode> doc,
ContentElement element) {
// find start and end as points
Point.El<ContentNode> start = Point.start(doc, element);
Point.El<ContentNode> end = Point.end((ContentNode) element);
// notify and fix.
if (element == doc.getDocumentElement()) {
listener.onFullDocumentRevert(doc);
} else {
listener.onRangeRevert(start, end);
}
revertWithoutNotification(start, end);
}
/**
* Briefly give a visual cue that the given element has been repaired
* @param regionNode
*/
public void flashShowRepair(final ContentElement regionNode) {
EditorStaticDeps.logger.error().log("repairing region: " + regionNode);
if (!LogLevel.showErrors()) {
return;
}
if (regionNode.getImplNodelet() == null) {
return;
}
regionNode.getImplNodelet().addClassName(resources.css().repaired());
ScheduleTimer t = new ScheduleTimer() {
@Override
public void run() {
regionNode.getImplNodelet().removeClassName(resources.css().repaired());
}
};
t.schedule(800);
}
/**
* Mark a region (probably everything) as not recoverable
* @param regionNode
*/
public void showDeath(ContentElement regionNode) {
if (!LogLevel.showErrors()) {
return;
}
if (regionNode.getImplNodelet() != null) {
regionNode.getImplNodelet().addClassName(resources.css().dead());
}
}
/**
* Clear death marking. Probably because we are calling setContent and
* totally refreshing everything.
*
* @param regionNode
*/
public void hideDeath(ContentElement regionNode) {
if (!LogLevel.showErrors()) {
return;
}
if (regionNode.getImplNodelet() != null) {
regionNode.getImplNodelet().removeClassName(resources.css().dead());
}
}
/**
* Same as {@link #revertWithoutNotification(Point.El, Point.El)}, but without runtime exception
* handling
*/
void revertInner(Point.El<ContentNode> start, Point.El<ContentNode> end) {
// TODO(danilatos): This naive implementation has three main problems
// 1. It does more work than it needs to, by throwing everything away and
// redoing it
// 2. It blatantly disregards transparent nodes. It should "work" with them
// around, but it might mess them up.
// 3. The code is too complicated
ContentView renderedContent = renderedView;
assert end == null || end.getContainer() == start.getContainer() :
"No reverting across elements";
ContentNode before = Point.nodeBefore(renderedContent, start);
Node nodeletBefore = before == null ? null : before.getImplNodelet();
Element parentNodelet;
if (nodeletBefore == null) {
parentNodelet = renderedContent.getVisibleNode(start.getContainer()).getImplNodelet().cast();
} else {
parentNodelet = nodeletBefore.getParentElement();
}
ContentNode first = start.getNodeAfter();
ContentNode last = end == null ? null : end.getNodeAfter();
assert renderedContent.getVisibleNode(first) == first;
assert renderedContent.getVisibleNode(last) == last;
assert last == null
|| renderedContent.getParentElement(first) == renderedContent.getParentElement(last)
: "First and last are expected to have same parent";
{
ContentNode node;
for (node = first; node != last; node = renderedContent.getNextSibling(node)) {
// If node is consistent, we assume the ones after are ok.
if (end == null && node.isConsistent()) {
break;
}
node.revertImplementation();
}
last = node;
}
// TODO(danilatos): Actually use some view that strips unknown nodes out. or do something
// better.
Node nodeletAfter = last == null ? null : last.getImplNodelet();
reattachImplChildren(parentNodelet, nodeletBefore, nodeletAfter, first, last);
}
private void reattachImplChildren(Node parentNodelet, Node nodeletBefore, Node nodeletAfter,
ContentNode first, ContentNode last) {
// TODO(danilatos): Replace this hairy code with pre-order traversal
// getters, once they exist
while (true) {
Node nodelet = nodeletBefore == null
? strippingView.getFirstChild(parentNodelet)
: strippingView.getNextSibling(nodeletBefore);
if (nodelet == null || nodelet == nodeletAfter) {
break;
}
if (nodelet.getParentElement() != null) {
nodelet.removeFromParent();
}
}
for (ContentNode node = first; node != last; node = renderedView.getNextSibling(node)) {
parentNodelet.insertBefore(node.getImplNodelet(), nodeletAfter);
}
}
/**
* Zip between the given range
* @see ContentElement#zipChildren(ContentNode, ContentNode, Node)
* @param range
* @param userSelection Node within which the user has their selection. If
* zipping ends up splitting it, we will return true, so we know if we
* need to restore the selection.
* HACK(danilatos): This is a bit nasty. Better way? (issue can probably
* be avoided altogether if we can have some control on which text nodes
* operations affect, when there is a choice).
* @return true if html in the user's selection was affected
* (e.g. splitting a text node)
*/
public boolean zipRange(RestrictedRange<ContentNode> range, Node userSelection) {
try {
// nodeAfter is 1 further than we need, but meh
return ((ContentElement)range.getContainer()).zipChildrenExcludingFrom(
range.getNodeBefore(), range.getNodeAfter(), userSelection);
} catch (RuntimeException t) {
// Do the more expensive, but robust action
// TODO(danilatos): Should this be content or renderedContent?
revert(range.getPointBefore(persistentView), range.getPointAfter());
return true;
}
}
}