Correction, bad description: Gadget subclasses MutableHtmlContent, not the other way around.
On Thu, Sep 4, 2008 at 5:35 PM, <[EMAIL PROTECTED]> wrote: > Author: johnh > Date: Thu Sep 4 17:35:36 2008 > New Revision: 692314 > > URL: http://svn.apache.org/viewvc?rev=692314&view=rev > Log: > Introducing class MutableHtmlContent and subclassing it with Gadget. This > accommodates part 1 of SHINDIG-571. > > MutableHtmlContent manages a String and its equivalent parse tree > representation, ensuring with certain guarantees, as documented, that the > two remain in sync. In particular, a parse tree derived from String in > MutableHtmlContent cannot me modified once the source String from which it > was created is changed. > > This change precedes the use of MutableHtmlContent in HttpResponse, > enabling consolidation of rewriting logic for both ContentRewriter APIs: > Gadget-based and HttpResponse-based. > > > Added: > > > incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/MutableHtmlContent.java > > > incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/MutableHtmlContentTest.java > Modified: > > > incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/Gadget.java > > > incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTest.java > > Modified: > incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/Gadget.java > URL: > http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/Gadget.java?rev=692314&r1=692313&r2=692314&view=diff > > ============================================================================== > --- > incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/Gadget.java > (original) > +++ > incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/Gadget.java > Thu Sep 4 17:35:36 2008 > @@ -19,9 +19,7 @@ > > import org.apache.shindig.common.ContainerConfig; > import org.apache.shindig.gadgets.http.HttpResponse; > -import org.apache.shindig.gadgets.parse.GadgetHtmlNode; > import org.apache.shindig.gadgets.parse.GadgetHtmlParser; > -import org.apache.shindig.gadgets.parse.ParsedHtmlNode; > import org.apache.shindig.gadgets.spec.GadgetSpec; > import org.apache.shindig.gadgets.spec.LocaleSpec; > import org.apache.shindig.gadgets.spec.Preload; > @@ -30,11 +28,8 @@ > import org.json.JSONArray; > import org.json.JSONException; > > -import java.io.IOException; > -import java.io.StringWriter; > import java.util.Collection; > import java.util.HashMap; > -import java.util.List; > import java.util.Map; > import java.util.concurrent.Future; > > @@ -42,7 +37,7 @@ > * Intermediary representation of all state associated with processing > * of a single gadget request. > */ > -public class Gadget { > +public class Gadget extends MutableHtmlContent { > private final GadgetContext context; > > /** > @@ -97,7 +92,7 @@ > * @return The (immutable) View applicable for the current request (part > of GadgetSpec). > */ > public View getCurrentView() { > - return currentView; > + return currentView; > } > > /** > @@ -132,146 +127,20 @@ > } > return view; > } > - > - private String content; > - private int contentParseId; > - > - /** > - * Retrieves the current content for this gadget in String form. > - * If gadget content has been retrieved in parse tree form and has > - * been edited, the String form is computed from the parse tree by > - * rendering it. It is <b>strongly</b> encouraged to avoid switching > - * between retrieval of parse tree (through [EMAIL PROTECTED] > getParseTree}), > - * with subsequent edits and retrieval of String contents to avoid > - * repeated serialization and deserialization. > - * @return Renderable/active content for the gadget. > - */ > - public String getContent() { > - if (parseEditId > contentParseId) { > - // Regenerate content from parse tree node, since the parse tree > - // was modified relative to the last time content was generated > from it. > - // This is an expensive operation that should happen only once > - // per rendering cycle: all rewriters (or other manipulators) > - // operating on the parse tree should happen together. > - contentParseId = parseEditId; > - StringWriter sw = new StringWriter(); > - for (GadgetHtmlNode node : parseTree.getChildren()) { > - try { > - node.render(sw); > - } catch (IOException e) { > - // Never happens. > - } > - } > - content = sw.toString(); > - } > - return content; > - } > - > - /** > - * Sets the content for the gadget as a raw String. Note, this operation > - * may be done at any time, even after a parse tree node has been > retrieved > - * and modified (though a warning will be emitted in this case). Once > - * new content has been set, all subsequent edits to parse trees > generated > - * from the <i>previous</i> content will be invalid, throwing an > - * [EMAIL PROTECTED] IllegalStateException}. > - * @param newContent New content for the gadget. > - */ > - public void setContent(String newContent) { > - if (!content.equals(newContent)) { > - content = newContent; > - if (editListener != null) { > - editListener.stringEdited(); > - } > - } > - } > - > - private GadgetHtmlNode parseTree; > - public static final String ROOT_NODE_TAG_NAME = "gadget-root"; > - > - /** > - * Retrieves the contents of the gadget in parse tree form, if a > - * [EMAIL PROTECTED] GadgetHtmlParser} is configured and is able to parse > the > string > - * contents appropriately. The resultant parse tree has a special, > - * single top-level node that wraps all subsequent content, with > - * tag name [EMAIL PROTECTED] ROOT_NODE_TAG_NAME}. While it may be edited > just > - * as any other node may, doing so is pointless since the root node > - * is stripped out during rendering. Any edits to the returned parse > - * tree performed after the source [EMAIL PROTECTED] Gadget} has new > content > - * set via [EMAIL PROTECTED] setContent} will throw an [EMAIL PROTECTED] > IllegalStateException} > - * to maintain content consistency in the gadget. To modify a gadget's > - * contents by parse tree after setting new String contents, > - * this method must be called again. However, this practice is highly > - * discouraged, as parsing a tree from String is a costly operation. > - * @return Top-level node whose children represent the gadget's > contents, or > - * null if no parser is configured or if String contents are > null. > - * @throws GadgetException Throw by the GadgetHtmlParser generating the > tree from String. > - */ > - public GadgetHtmlNode getParseTree() throws GadgetException { > - if (parseTree != null && !editListener.stringWasEdited()) { > - return parseTree; > - } > - > - if (content == null || contentParser == null) { > - return null; > - } > - > - // One ContentEditListener per parse tree. > - editListener = new ContentEditListener(); > - parseTree = new GadgetHtmlNode(ROOT_NODE_TAG_NAME, null); > - List<ParsedHtmlNode> parsed = contentParser.parse(content); > - for (ParsedHtmlNode parsedNode : parsed) { > - parseTree.appendChild(new GadgetHtmlNode(parsedNode, > editListener)); > - } > - > - // Parse tree created from content: edit IDs are the same > - contentParseId = parseEditId; > - return parseTree; > - } > - > - private ContainerConfig containerConfig; > - private GadgetHtmlParser contentParser; > - private int parseEditId; > - private ContentEditListener editListener; > - > public Gadget(GadgetContext context, GadgetSpec spec, > Collection<JsLibrary> jsLibraries, ContainerConfig containerConfig, > GadgetHtmlParser contentParser) { > + super(contentParser); > + > this.context = context; > this.spec = spec; > this.jsLibraries = jsLibraries; > - this.containerConfig = containerConfig; > - this.contentParser = contentParser; > - this.currentView = getView(this.containerConfig); > + this.currentView = getView(containerConfig); > if (this.currentView != null) { > // View might be invalid or associated with no content (type=URL) > - this.content = this.currentView.getContent(); > + setContent(this.currentView.getContent()); > } else { > - this.content = null; > + setContent(null); > } > - contentParseId = 0; > - } > - > - // Intermediary object tracking edit behavior for the Gadget to help > maintain > - // state consistency. GadgetHtmlNode calls nodeEdited whenever a > modification > - // is made to its original source. > - private class ContentEditListener implements GadgetHtmlNode.EditListener > { > - private boolean stringEdited = false; > - > - public void nodeEdited() { > - ++parseEditId; > - if (stringEdited) { > - // Parse tree is invalid: a new String representation was > set > - // as tree source in the meantime. > - throw new IllegalStateException("Edited parse node after > setting String content"); > - } > - } > - > - private void stringEdited() { > - stringEdited = true; > - } > - > - private boolean stringWasEdited() { > - return stringEdited; > - } > } > } > \ No newline at end of file > > Added: > incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/MutableHtmlContent.java > URL: > http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/MutableHtmlContent.java?rev=692314&view=auto > > ============================================================================== > --- > incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/MutableHtmlContent.java > (added) > +++ > incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/MutableHtmlContent.java > Thu Sep 4 17:35:36 2008 > @@ -0,0 +1,170 @@ > +/* > + * 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; > + > +import java.io.IOException; > +import java.io.StringWriter; > +import java.util.List; > + > +import org.apache.shindig.gadgets.parse.GadgetHtmlNode; > +import org.apache.shindig.gadgets.parse.GadgetHtmlParser; > +import org.apache.shindig.gadgets.parse.ParsedHtmlNode; > + > +/** > + * Object that maintains a String representation of arbitrary contents > + * and a consistent view of those contents as an HTML parse tree. > + */ > +public class MutableHtmlContent { > + private String content; > + private GadgetHtmlNode parseTree; > + private ContentEditListener editListener; > + private int parseEditId; > + private int contentParseId; > + private final GadgetHtmlParser contentParser; > + > + public MutableHtmlContent(GadgetHtmlParser contentParser) { > + this.contentParser = contentParser; > + this.contentParseId = parseEditId = 0; > + } > + > + public static final String ROOT_NODE_TAG_NAME = "gadget-root"; > + > + /** > + * Retrieves the current content for this object in String form. > + * If content has been retrieved in parse tree form and has > + * been edited, the String form is computed from the parse tree by > + * rendering it. It is <b>strongly</b> encouraged to avoid switching > + * between retrieval of parse tree (through [EMAIL PROTECTED] > getParseTree}), > + * with subsequent edits and retrieval of String contents to avoid > + * repeated serialization and deserialization. > + * @return Renderable/active content. > + */ > + public String getContent() { > + if (parseEditId > contentParseId) { > + // Regenerate content from parse tree node, since the parse tree > + // was modified relative to the last time content was generated from > it. > + // This is an expensive operation that should happen only once > + // per rendering cycle: all rewriters (or other manipulators) > + // operating on the parse tree should happen together. > + contentParseId = parseEditId; > + StringWriter sw = new StringWriter(); > + for (GadgetHtmlNode node : parseTree.getChildren()) { > + try { > + node.render(sw); > + } catch (IOException e) { > + // Never happens. > + } > + } > + content = sw.toString(); > + } > + return content; > + } > + > + /** > + * Sets the object's content as a raw String. Note, this operation > + * may be done at any time, even after a parse tree node has been > retrieved > + * and modified (though a warning will be emitted in this case). Once > + * new content has been set, all subsequent edits to parse trees > generated > + * from the <i>previous</i> content will be invalid, throwing an > + * [EMAIL PROTECTED] IllegalStateException}. > + * @param newContent New content. > + */ > + public void setContent(String newContent) { > + if (content == null || > + !content.equals(newContent)) { > + content = newContent; > + if (editListener != null) { > + editListener.stringEdited(); > + } > + } > + } > + > + /** > + * Retrieves the object contents in parse tree form, if a > + * [EMAIL PROTECTED] GadgetHtmlParser} is configured and is able to parse > the > string > + * contents appropriately. The resultant parse tree has a special, > + * single top-level node that wraps all subsequent content, with > + * tag name [EMAIL PROTECTED] ROOT_NODE_TAG_NAME}. While it may be edited > just > + * as any other node may, doing so is pointless since the root node > + * is stripped out during rendering. Any edits to the returned parse > + * tree performed after the source [EMAIL PROTECTED] MutableHtmlContent} > has new > content > + * set via [EMAIL PROTECTED] setContent} will throw an [EMAIL PROTECTED] > IllegalStateException} > + * to maintain content consistency in the object. To modify the object's > + * contents by parse tree after setting new String contents, > + * this method must be called again. However, this practice is highly > + * discouraged, as parsing a tree from String is a costly operation. > + * @return Top-level node whose children represent the gadget's > contents, or > + * null if no parser is configured, String contents are null, or > contents unparseable. > + */ > + public GadgetHtmlNode getParseTree() { > + if (parseTree != null && !editListener.stringWasEdited()) { > + return parseTree; > + } > + > + if (content == null || contentParser == null) { > + return null; > + } > + > + // One ContentEditListener per parse tree. > + editListener = new ContentEditListener(); > + parseTree = new GadgetHtmlNode(ROOT_NODE_TAG_NAME, null); > + List<ParsedHtmlNode> parsed = null; > + try { > + parsed = contentParser.parse(content); > + } catch (GadgetException e) { > + // TODO: emit info message > + return null; > + } > + > + if (parsed == null) { > + return null; > + } > + > + for (ParsedHtmlNode parsedNode : parsed) { > + parseTree.appendChild(new GadgetHtmlNode(parsedNode, editListener)); > + } > + > + // Parse tree created from content: edit IDs are the same > + contentParseId = parseEditId; > + return parseTree; > + } > + > + // Intermediary object tracking edit behavior for the MutableHtmlContent > to help maintain > + // state consistency. GadgetHtmlNode calls nodeEdited whenever a > modification > + // is made to its original source. > + private class ContentEditListener implements GadgetHtmlNode.EditListener > { > + private boolean stringEdited = false; > + > + public void nodeEdited() { > + ++parseEditId; > + if (stringEdited) { > + // Parse tree is invalid: a new String representation was set > + // as tree source in the meantime. > + throw new IllegalStateException("Edited parse node after setting > String content"); > + } > + } > + > + private void stringEdited() { > + stringEdited = true; > + } > + > + private boolean stringWasEdited() { > + return stringEdited; > + } > + } > +} > > Modified: > incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTest.java > URL: > http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTest.java?rev=692314&r1=692313&r2=692314&view=diff > > ============================================================================== > --- > incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTest.java > (original) > +++ > incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTest.java > Thu Sep 4 17:35:36 2008 > @@ -21,14 +21,9 @@ > import static org.easymock.EasyMock.expect; > import static org.easymock.classextension.EasyMock.replay; > import static org.junit.Assert.assertEquals; > -import static org.junit.Assert.assertNotSame; > -import static org.junit.Assert.assertSame; > -import static org.junit.Assert.assertTrue; > -import static org.junit.Assert.fail; > > import org.apache.shindig.common.ContainerConfig; > import org.apache.shindig.gadgets.http.HttpResponse; > -import org.apache.shindig.gadgets.parse.GadgetHtmlNode; > import org.apache.shindig.gadgets.parse.caja.CajaHtmlParser; > import org.apache.shindig.gadgets.spec.GadgetSpec; > import org.apache.shindig.gadgets.spec.LocaleSpec; > @@ -101,80 +96,6 @@ > > assertEquals("DEFAULT VIEW", view.getContent()); > } > - > - @Test > - public void getContentAndParseTreeNoSets() throws Exception { > - replay(config); > - > - String content = gadget.getContent(); > - assertEquals("DEFAULT VIEW", content); > - > - GadgetHtmlNode root = gadget.getParseTree(); > - assertEquals(1, root.getChildren().size()); > - assertTrue(root.getChildren().get(0).isText()); > - assertEquals(content, root.getChildren().get(0).getText()); > - > - assertSame(content, gadget.getContent()); > - assertSame(root, gadget.getParseTree()); > - } > - > - @Test > - public void modifyContentReflectedInTree() throws Exception { > - replay(config); > - > - gadget.setContent("NEW CONTENT"); > - GadgetHtmlNode root = gadget.getParseTree(); > - assertEquals(1, root.getChildren().size()); > - assertEquals("NEW CONTENT", root.getChildren().get(0).getText()); > - } > - > - @Test > - public void modifyTreeReflectedInContent() throws Exception { > - replay(config); > - > - GadgetHtmlNode root = gadget.getParseTree(); > - > - // First child should be text node per other tests. Modify it. > - root.getChildren().get(0).setText("FOO CONTENT"); > - assertEquals("FOO CONTENT", gadget.getContent()); > - > - // Do it again > - root.getChildren().get(0).setText("BAR CONTENT"); > - assertEquals("BAR CONTENT", gadget.getContent()); > - > - // GadgetHtmlNode hasn't changed because string hasn't changed > - assertSame(root, gadget.getParseTree()); > - } > - > - @Test > - public void staleTreeEditsInvalidatedAfterContentSet() throws Exception > { > - replay(config); > - > - GadgetHtmlNode firstRoot = gadget.getParseTree(); > - > - // Re-set content > - gadget.setContent("INVALIDATING CONTENT"); > - > - // Should still be able to obtain this. > - GadgetHtmlNode secondRoot = gadget.getParseTree(); > - assertNotSame(firstRoot, secondRoot); > - > - // Should be able to *obtain* first child node... > - GadgetHtmlNode firstTextNode = firstRoot.getChildren().get(0); > - try { > - // ...but not edit it. > - firstTextNode.setText("STALE-SET CONTENT"); > - fail("Should not be able to modify stale parse tree"); > - } catch (IllegalStateException e) { > - // Expected condition. > - } > - > - assertEquals("INVALIDATING CONTENT", > secondRoot.getChildren().get(0).getText()); > - > - // For good measure, modify secondRoot and get content > - secondRoot.getChildren().get(0).setText("NEW CONTENT"); > - assertEquals("NEW CONTENT", gadget.getContent()); > - } > > @Test > public void getAliasedView() { > > Added: > incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/MutableHtmlContentTest.java > URL: > http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/MutableHtmlContentTest.java?rev=692314&view=auto > > ============================================================================== > --- > incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/MutableHtmlContentTest.java > (added) > +++ > incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/MutableHtmlContentTest.java > Thu Sep 4 17:35:36 2008 > @@ -0,0 +1,110 @@ > +/* > + * 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; > + > +import static org.junit.Assert.assertEquals; > +import static org.junit.Assert.assertNotSame; > +import static org.junit.Assert.assertSame; > +import static org.junit.Assert.assertTrue; > +import static org.junit.Assert.fail; > + > +import org.apache.shindig.gadgets.parse.GadgetHtmlNode; > +import org.apache.shindig.gadgets.parse.caja.CajaHtmlParser; > + > +import org.junit.Before; > +import org.junit.Test; > + > +public class MutableHtmlContentTest { > + private MutableHtmlContent mhc; > + > + @Before > + public void setUp() throws Exception { > + // Note dependency on CajaHtmlParser - this isn't particularly ideal > but is > + // sufficient given that this test doesn't exercise the parser > extensively at all, > + // instead focusing on the additional utility provided by > MutableHtmlContent > + mhc = new MutableHtmlContent(new CajaHtmlParser()); > + mhc.setContent("DEFAULT VIEW"); > + } > + > + @Test > + public void getContentAndParseTreeNoSets() throws Exception { > + String content = mhc.getContent(); > + assertEquals("DEFAULT VIEW", content); > + > + GadgetHtmlNode root = mhc.getParseTree(); > + assertEquals(1, root.getChildren().size()); > + assertTrue(root.getChildren().get(0).isText()); > + assertEquals(content, root.getChildren().get(0).getText()); > + > + assertSame(content, mhc.getContent()); > + assertSame(root, mhc.getParseTree()); > + } > + > + @Test > + public void modifyContentReflectedInTree() throws Exception { > + mhc.setContent("NEW CONTENT"); > + GadgetHtmlNode root = mhc.getParseTree(); > + assertEquals(1, root.getChildren().size()); > + assertEquals("NEW CONTENT", root.getChildren().get(0).getText()); > + } > + > + @Test > + public void modifyTreeReflectedInContent() throws Exception { > + GadgetHtmlNode root = mhc.getParseTree(); > + > + // First child should be text node per other tests. Modify it. > + root.getChildren().get(0).setText("FOO CONTENT"); > + assertEquals("FOO CONTENT", mhc.getContent()); > + > + // Do it again > + root.getChildren().get(0).setText("BAR CONTENT"); > + assertEquals("BAR CONTENT", mhc.getContent()); > + > + // GadgetHtmlNode hasn't changed because string hasn't changed > + assertSame(root, mhc.getParseTree()); > + } > + > + @Test > + public void staleTreeEditsInvalidatedAfterContentSet() throws Exception > { > + GadgetHtmlNode firstRoot = mhc.getParseTree(); > + > + // Re-set content > + mhc.setContent("INVALIDATING CONTENT"); > + > + // Should still be able to obtain this. > + GadgetHtmlNode secondRoot = mhc.getParseTree(); > + assertNotSame(firstRoot, secondRoot); > + > + // Should be able to *obtain* first child node... > + GadgetHtmlNode firstTextNode = firstRoot.getChildren().get(0); > + try { > + // ...but not edit it. > + firstTextNode.setText("STALE-SET CONTENT"); > + fail("Should not be able to modify stale parse tree"); > + } catch (IllegalStateException e) { > + // Expected condition. > + } > + > + assertEquals("INVALIDATING CONTENT", > secondRoot.getChildren().get(0).getText()); > + > + // For good measure, modify secondRoot and get content > + secondRoot.getChildren().get(0).setText("NEW CONTENT"); > + assertEquals("NEW CONTENT", mhc.getContent()); > + } > +} > > >

