You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shindig.apache.org by jo...@apache.org on 2010/02/22 17:31:59 UTC

svn commit: r912644 - in /shindig/trunk/java/gadgets/src: main/java/org/apache/shindig/gadgets/rewrite/DomWalker.java test/java/org/apache/shindig/gadgets/rewrite/DomWalkerTest.java

Author: johnh
Date: Mon Feb 22 16:31:58 2010
New Revision: 912644

URL: http://svn.apache.org/viewvc?rev=912644&view=rev
Log:
DomWalker is a helper class/framework for writing rewriters that take
the very typical form of walking a DOM tree and manipulating nodes as
you go.

It supports multiple Visitors at the same time, reducing the need for
multiple DOM walks depending on the semantics.

As of this CL the framework isn't used -- it sets up several follow-up
CLs to come.


Added:
    shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/DomWalker.java
    shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/DomWalkerTest.java

Added: shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/DomWalker.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/DomWalker.java?rev=912644&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/DomWalker.java (added)
+++ shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/DomWalker.java Mon Feb 22 16:31:58 2010
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.shindig.gadgets.rewrite;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import org.w3c.dom.Node;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Framework-in-a-framework facilitating the common Visitor case
+ * in which a DOM tree is walked in order to manipulate it.
+ * 
+ * See subclass doc for additional detail.
+ */
+public class DomWalker {
+  /**
+   * Implemented by classes that do actual manipulation of the DOM
+   * while {@code DomWalker.ContentVisitor} walks it. {@code Visitor}
+   * instances are called for each {@code Node} in the DOM in the order
+   * they are registered with the {@code DomVisitor.ContentVisitor}
+   */
+  public interface Visitor {
+    /**
+     * Returned by the {@code visit(Gadget, Node)} method, signaling:
+     * 
+     * BYPASS = Visitor doesn't care about the node.
+     * MODIFY = Visitor has modified the node.
+     * RESERVE_NODE = Visitor reserves exactly the node passed. No other
+     *   Visitor will visit the node.
+     * RESERVE_TREE = Visitor reserves the node passed and all its descendants
+     *   No other Visitor will visit them.
+     *   
+     * Visitors are expected to be well-behaved in that they do not
+     * modify unreserved nodes: that is, in revisit(...) they do not access
+     * adjacent, parent, etc. nodes and modify them. visit(...) may return
+     * MODIFY to indicate a modification of the given node.
+     * 
+     * Other append and delete operations are acceptable
+     * but only in revisit(). Reservations are supported in order to support
+     * "batched" lookups relating to a similar set of data retrieved from a
+     * backend.
+     */
+    public enum VisitStatus {
+      BYPASS,
+      MODIFY,
+      RESERVE_NODE,
+      RESERVE_TREE
+    }
+    
+    /**
+     * Visit a particular Node in the DOM.
+     * 
+     * @param gadget Context for the request.
+     * @param node Node being visited.
+     * @return Status, see {@code VisitStatus}
+     */
+    VisitStatus visit(Gadget gadget, Node node) throws RewritingException;
+    
+    /**
+     * Revisit a node in the DOM that was marked by the
+     * {@code visit(Gadget, Node)} as reserved during DOM traversal.
+     * 
+     * @param gadget Context for the request.
+     * @param nodes Nodes being revisited, previously marked as reserved.
+     * @return True if any node modified, false otherwise.
+     */
+    boolean revisit(Gadget gadget, List<Node> nodes) throws RewritingException;
+  }
+  
+  /**
+   * Rewriter that traverses the DOM, passing each node to its
+   * list of {@code Visitor} instances in order. Each visitor
+   * may bypass, modify, or reserve the node. Reserved nodes
+   * will be revisited after the entire DOM tree is walked.
+   * The DOM tree is walked in depth-first order.
+   */
+  public static class Rewriter implements GadgetRewriter, RequestRewriter {
+    private final List<Visitor> visitors;
+    
+    public Rewriter(List<Visitor> visitors) {
+      this.visitors = visitors;
+    }
+    
+    public Rewriter(Visitor... visitors) {
+      this.visitors = Arrays.asList(visitors);
+    }
+    
+    public Rewriter() {
+      this.visitors = null;
+    }
+
+    // Override this to supply a list of Visitors generated using request context
+    // rather than supplied at construction time.
+    protected List<Visitor> makeVisitors(Gadget context, Uri gadgetUri) {
+      return visitors;
+    }
+
+    /**
+     * Performs the DomWalker rewrite operation described in class javadoc.
+     */
+    public void rewrite(Gadget gadget, MutableContent content)
+        throws RewritingException {
+      rewrite(makeVisitors(gadget, gadget.getSpec().getUrl()), gadget, content);
+    }
+
+    public boolean rewrite(HttpRequest request, HttpResponse original,
+        MutableContent content) throws RewritingException {
+      if (RewriterUtils.isHtml(request, original)) {
+        Gadget context = makeGadget(request);
+        return rewrite(makeVisitors(context, request.getGadget()), context, content);
+      }
+      return false;
+    }
+    
+    private boolean rewrite(List<Visitor> visitors, Gadget gadget, MutableContent content) 
+        throws RewritingException {
+      Map<Visitor, List<Node>> reservations = Maps.newHashMap();
+        
+      LinkedList<Node> toVisit = Lists.newLinkedList();
+      toVisit.add(content.getDocument().getDocumentElement());
+      boolean mutated = false;
+      while (!toVisit.isEmpty()) {
+        Node visiting = toVisit.removeFirst();
+          
+        // Iterate through all visitors evaluating their visitation status.
+        boolean treeReserved = false;
+        boolean nodeReserved = false;
+        for (Visitor visitor : visitors) {
+          switch(visitor.visit(gadget, visiting)) {
+          case MODIFY:
+            content.documentChanged();
+            mutated = true;
+            break;
+          case RESERVE_NODE:
+            nodeReserved = true;
+            break;
+          case RESERVE_TREE:
+            treeReserved = true;
+            break;
+          default:
+            // Aka BYPASS - do nothing.
+            break;
+          }
+            
+          if (nodeReserved || treeReserved) {
+            // Reservation was made.
+            if (!reservations.containsKey(visitor)) {
+              reservations.put(visitor, Lists.<Node>newLinkedList());
+            }
+            reservations.get(visitor).add(visiting);
+            break;
+          }
+        }
+          
+        if (!treeReserved && visiting.hasChildNodes()) {
+          // Tree wasn't reserved - walk children.
+          // In order to preserve DFS order, walk children in reverse.
+          for (Node child = visiting.getLastChild(); child != null;
+               child = child.getPreviousSibling()) {
+            toVisit.addFirst(child);
+          }
+        }
+      }
+        
+      // Run through all reservations, revisiting as needed.
+      for (Visitor visitor : visitors) {
+        List<Node> nodesReserved = reservations.get(visitor);
+        if (nodesReserved != null && visitor.revisit(gadget, nodesReserved)) {
+          content.documentChanged();
+          mutated = true;
+        }
+      }
+      
+      return mutated;
+    }
+  }
+  
+  // TODO: Remove these lame hacks by changing Gadget to a proper general Context object.
+  public static Gadget makeGadget(final Uri context) {
+    final GadgetSpec spec = null;
+    try {
+      new GadgetSpec(context,
+        "<Module><ModulePrefs author=\"a\" title=\"t\"></ModulePrefs>" +
+        "<Content></Content></Module>");
+    } catch (Exception e) {
+      throw new RuntimeException("Unexpected boilerplate parse failure");
+    }
+    return new Gadget() {
+      @Override
+      public GadgetSpec getSpec() {
+        return spec;
+      }
+    }.setContext(new GadgetContext() {
+      @Override
+      public Uri getUrl() {
+        return context;
+      }
+    });
+  }
+  
+  public static Gadget makeGadget(final HttpRequest request) {
+    Gadget gadget = makeGadget(request.getUri());
+    gadget.setContext(new GadgetContext(gadget.getContext()) {
+      @Override
+      public String getParameter(String key) {
+        return request.getParam(key);
+      }
+      
+      @Override
+      public boolean getIgnoreCache() {
+        return request.getIgnoreCache();
+      }
+      
+      @Override
+      public String getContainer() {
+        return request.getContainer();
+      }
+      
+      @Override
+      public boolean getDebug() {
+        return "1".equalsIgnoreCase(getParameter(Param.DEBUG.getKey()));
+      }
+    });
+    return gadget;
+  }
+}

Added: shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/DomWalkerTest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/DomWalkerTest.java?rev=912644&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/DomWalkerTest.java (added)
+++ shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/DomWalkerTest.java Mon Feb 22 16:31:58 2010
@@ -0,0 +1,292 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.shindig.gadgets.rewrite;
+
+import static org.easymock.classextension.EasyMock.createMock;
+import static org.easymock.classextension.EasyMock.expect;
+import static org.easymock.classextension.EasyMock.expectLastCall;
+import static org.easymock.classextension.EasyMock.replay;
+import static org.easymock.classextension.EasyMock.verify;
+
+import com.google.common.collect.Lists;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.name.Names;
+import com.google.inject.util.Modules;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Node;
+
+import java.util.List;
+
+public class DomWalkerTest {
+  private Document doc;
+  private Node root;
+  private Node child1;
+  private Node child2;
+  private Node subchild1;
+  private Node text1;
+  private Node text2;
+  
+  @Before
+  public void setUp() {
+    // Create a base document with structure:
+    // <root>
+    //   <child1>text1</child1>
+    //   <child2>
+    //     <subchild1>text2</subchild1>
+    //   </child2>
+    // </root>
+    // ...which should allow all relevant test cases to be exercised.
+    Injector injector = Guice.createInjector(Modules.override(new ParseModule())
+        .with(new AbstractModule() {
+          @Override
+          protected void configure() {
+            bind(Integer.class).annotatedWith(
+                Names.named("shindig.cache.lru.default.capacity"))
+                  .toInstance(0);
+          }
+        }));
+    DOMImplementation domImpl = injector.getInstance(DOMImplementation.class);
+    doc = domImpl.createDocument(null, null, null);
+    root = doc.createElement("root");
+    child1 = doc.createElement("child1");
+    text1 = doc.createTextNode("text1");
+    child1.appendChild(text1);
+    root.appendChild(child1);
+    child2 = doc.createElement("child2");
+    subchild1 = doc.createElement("subchild1");
+    text2 = doc.createTextNode("text2");
+    subchild1.appendChild(text2);
+    child2.appendChild(subchild1);
+    root.appendChild(child2);
+    doc.appendChild(root);
+  }
+  
+  @Test
+  public void allBypassDoesNothing() throws Exception {
+    Gadget gadget = gadget();
+    
+    // Visitor always bypasses nodes, never gets called with revisit(),
+    // but visits every node in the document.
+    DomWalker.Visitor visitor = createMock(DomWalker.Visitor.class);
+    expect(visitor.visit(gadget, root))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor.visit(gadget, child1))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor.visit(gadget, child2))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor.visit(gadget, subchild1))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor.visit(gadget, text1))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor.visit(gadget, text2))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    replay(visitor);
+    
+    MutableContent mc = getContent(0);
+    
+    DomWalker.Rewriter rewriter = getRewriter(visitor);
+    rewriter.rewrite(gadget, mc);
+    
+    // Verifying mutations on MutableContent completes the test.
+    verify(mc);
+  }
+  
+  @Test
+  public void allMutateMutatesEveryTime() throws Exception {
+    Gadget gadget = gadget();
+    
+    // Visitor mutates every node it sees immediately and inline.
+    DomWalker.Visitor visitor = createMock(DomWalker.Visitor.class);
+    expect(visitor.visit(gadget, root))
+        .andReturn(DomWalker.Visitor.VisitStatus.MODIFY).once();
+    expect(visitor.visit(gadget, child1))
+        .andReturn(DomWalker.Visitor.VisitStatus.MODIFY).once();
+    expect(visitor.visit(gadget, child2))
+        .andReturn(DomWalker.Visitor.VisitStatus.MODIFY).once();
+    expect(visitor.visit(gadget, subchild1))
+        .andReturn(DomWalker.Visitor.VisitStatus.MODIFY).once();
+    expect(visitor.visit(gadget, text1))
+        .andReturn(DomWalker.Visitor.VisitStatus.MODIFY).once();
+    expect(visitor.visit(gadget, text2))
+        .andReturn(DomWalker.Visitor.VisitStatus.MODIFY).once();
+    replay(visitor);
+    
+    MutableContent mc = getContent(6);
+    
+    DomWalker.Rewriter rewriter = getRewriter(visitor);
+    rewriter.rewrite(gadget, mc);
+    
+    // Verifying mutations on MutableContent completes the test.
+    verify(mc);
+  }
+  
+  @Test
+  public void allReserveNodeReservesAll() throws Exception {
+    Gadget gadget = gadget();
+    
+    // Visitor mutates every node it sees immediately and inline.
+    DomWalker.Visitor visitor = createMock(DomWalker.Visitor.class);
+    expect(visitor.visit(gadget, root))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_NODE).once();
+    expect(visitor.visit(gadget, child1))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_NODE).once();
+    expect(visitor.visit(gadget, child2))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_NODE).once();
+    expect(visitor.visit(gadget, subchild1))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_NODE).once();
+    expect(visitor.visit(gadget, text1))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_NODE).once();
+    expect(visitor.visit(gadget, text2))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_NODE).once();
+    
+    // All nodes are revisited in DFS order.
+    List<Node> allReserved =
+        Lists.newArrayList(root, child1, text1, child2, subchild1, text2);
+    expect(visitor.revisit(gadget, allReserved))
+        .andReturn(true).once();
+    replay(visitor);
+    
+    MutableContent mc = getContent(1);  // Mutated each revisit.
+    
+    DomWalker.Rewriter rewriter = getRewriter(visitor);
+    rewriter.rewrite(gadget, mc);
+    
+    // Verifying mutations on MutableContent completes the test.
+    verify(mc);
+  }
+  
+  @Test
+  public void reserveRootPrecludesAllElse() throws Exception {
+    Gadget gadget = gadget();
+    
+    // Visitor1 reserves root, visitor2 never gets anything.
+    DomWalker.Visitor visitor1 = createMock(DomWalker.Visitor.class);
+    expect(visitor1.visit(gadget, root))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_TREE).once();
+    List<Node> allReserved = Lists.newArrayList(root);
+    expect(visitor1.revisit(gadget, allReserved))
+        .andReturn(true).once();
+    DomWalker.Visitor visitor2 = createMock(DomWalker.Visitor.class);
+    replay(visitor1, visitor2);
+    
+    MutableContent mc = getContent(1);  // Mutated once by revisit.
+    
+    DomWalker.Rewriter rewriter = getRewriter(visitor1, visitor2);
+    rewriter.rewrite(gadget, mc);
+    
+    // Verifying mutations on MutableContent completes the test.
+    verify(mc);
+  }
+  
+  @Test
+  public void allMixedModes() throws Exception {
+    Gadget gadget = gadget();
+    
+    // Visitor1 reserves single text node 1
+    DomWalker.Visitor visitor1 = createMock(DomWalker.Visitor.class);
+    expect(visitor1.visit(gadget, root))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor1.visit(gadget, child1))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_NODE).once();
+    expect(visitor1.visit(gadget, text1))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor1.visit(gadget, child2))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    // No visitation of text2 for visitor1 since visitor2 reserves the tree.
+    expect(visitor1.visit(gadget, subchild1))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    // No modification the second time around.
+    List<Node> reserved1 = Lists.newArrayList(child1);
+    expect(visitor1.revisit(gadget, reserved1))
+        .andReturn(false).once();
+    
+    // Visitor2 reserves tree of subchild 1
+    DomWalker.Visitor visitor2 = createMock(DomWalker.Visitor.class);
+    expect(visitor2.visit(gadget, root))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    // No visitation of v1-reserved child 1
+    expect(visitor2.visit(gadget, text1))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor2.visit(gadget, child2))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor2.visit(gadget, subchild1))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_TREE).once();
+    List<Node> reserved2 = Lists.newArrayList(subchild1);
+    expect(visitor2.revisit(gadget, reserved2))
+        .andReturn(true).once();
+    
+    // Visitor3 modifies child 2
+    DomWalker.Visitor visitor3 = createMock(DomWalker.Visitor.class);
+    expect(visitor3.visit(gadget, root))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    // No visitation of v1-reserved child 1
+    expect(visitor3.visit(gadget, text1))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor3.visit(gadget, child2))
+        .andReturn(DomWalker.Visitor.VisitStatus.MODIFY).once();
+    // No visitation of tree of subchild 1
+    
+    replay(visitor1, visitor2, visitor3);
+    
+    MutableContent mc = getContent(2);  // Once v2.revisit(), once v3.visit()
+    
+    DomWalker.Rewriter rewriter = getRewriter(visitor1, visitor2, visitor3);
+    rewriter.rewrite(gadget, mc);
+    
+    // As before, MutableContent verification is the test.
+    verify(mc);
+  }
+  
+  private DomWalker.Rewriter getRewriter(DomWalker.Visitor... visitors) {
+    return new DomWalker.Rewriter(Lists.newArrayList(visitors));
+  }
+  
+  private MutableContent getContent(int docChangedTimes) {
+    MutableContent mc = createMock(MutableContent.class);
+    expect(mc.getDocument()).andReturn(doc).once();
+    if (docChangedTimes > 0) {
+      mc.documentChanged();
+      expectLastCall().times(docChangedTimes);
+    }
+    replay(mc);
+    return mc;
+  }
+  
+  private Gadget gadget() {
+    GadgetSpec spec = createMock(GadgetSpec.class);
+    expect(spec.getUrl()).andReturn(Uri.parse("http://example.com")).anyTimes();
+    Gadget gadget = createMock(Gadget.class);
+    expect(gadget.getSpec()).andReturn(spec).anyTimes();
+    replay(spec, gadget);
+    return gadget;
+  }
+}