/** * Copyright (C) 2010-2017 Structr GmbH * * This file is part of Structr <http://structr.org>. * * Structr is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Structr is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Structr. If not, see <http://www.gnu.org/licenses/>. */ package org.structr.web.advanced; import java.util.LinkedList; import java.util.List; import java.util.function.Function; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.structr.api.config.Settings; import org.structr.common.error.FrameworkException; import org.structr.core.graph.Tx; import org.structr.web.StructrUiTest; import org.structr.web.common.RenderContext; import org.structr.web.diff.InvertibleModificationOperation; import org.structr.web.entity.dom.DOMNode; import org.structr.web.entity.dom.Page; import org.structr.web.entity.html.Div; import org.structr.web.importer.Importer; import org.w3c.dom.NodeList; /** * * */ public class DiffTest extends StructrUiTest { private static final Logger logger = LoggerFactory.getLogger(DiffTest.class.getName()); @Test public void testReplaceContent() { final String result1 = testDiff("<html><head><title>Title</title></head><body>Test</body></html>", (String from) -> from.replace("Test", "Wurst")); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>Wurst</body>\n" + "</html>", result1 ); } @Test public void testInsertHeading() { final String result1 = testDiff("<html><head><title>Title</title></head><body>Test</body></html>", (String from) -> from.replace("Test", "<h1>Title text</h1>")); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>\n" + " <h1>Title text</h1>\n" + " </body>\n" + "</html>", result1 ); } @Test public void testInsertDivBranch() { final String result1 = testDiff("<html><head><title>Title</title></head><body>Test</body></html>", (String from) -> from.replace("Test", "<div><h1>Title text</h1></div>")); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>\n" + " <div>\n" + " <h1>Title text</h1>\n" + " </div>\n" + " </body>\n" + "</html>", result1 ); } @Test public void testInsertDivBranch2() { final String result1 = testDiff("<html><head><title>Title</title></head><body>Test</body></html>", (String from) -> from.replace("Test", "<div><div><h1>Title text</h1><p>paragraph</p></div></div>")); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>\n" + " <div>\n" + " <div>\n" + " <h1>Title text</h1>\n" + " <p>paragraph</p>\n" + " </div>\n" + " </div>\n" + " </body>\n" + "</html>", result1 ); } @Test public void testInsertMultipleTextNodes() { final String result1 = testDiff("<html><head><title>Title</title></head><body>Test</body></html>", (String from) -> from.replace("Test", "Test<b>bold</b>between<i>italic</i>Text")); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>Test<b>bold</b>between<i>italic</i>Text</body>\n" + "</html>", result1 ); } @Test public void testModifyMultipleTextNodes2() { final String result1 = testDiff("<html><head><title>Title</title></head><body>Test<b>bold</b>between<i>italic</i>Text</body></html>", (String from) -> { String mod = from; mod = mod.replace("bold", "BOLD"); mod = mod.replace("between", "BETWEEN"); mod = mod.replace("italic", "ITALIC"); mod = mod.replace("Text", "abcdef"); return mod; }); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>Test<b>BOLD</b>BETWEEN<i>ITALIC</i>abcdef</body>\n" + "</html>", result1 ); } @Test public void testReparentOneLevel() { final String result1 = testDiff("<html><head><title>Title</title></head><body><h1>Title text</h1></body></html>", (String from) -> { final StringBuilder buf = new StringBuilder(from); int startPos = buf.indexOf("<h1"); int endPos = buf.indexOf("</h1>") + 5; // insert from back to front, otherwise insert position changes buf.insert(endPos, "</div>"); buf.insert(startPos, "<div>"); return buf.toString(); }); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>\n" + " <div>\n" + " <h1>Title text</h1>\n" + " </div>\n" + " </body>\n" + "</html>", result1 ); } @Test public void testReparentTwoLevels() { final String result1 = testDiff("<html><head><title>Title</title></head><body><h1>Title text</h1></body></html>", (String from) -> { final StringBuilder buf = new StringBuilder(from); int startPos = buf.indexOf("<h1"); int endPos = buf.indexOf("</h1>") + 5; // insert from back to front, otherwise insert position changes buf.insert(endPos, "</div></div>"); buf.insert(startPos, "<div><div>"); return buf.toString(); }); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>\n" + " <div>\n" + " <div>\n" + " <h1>Title text</h1>\n" + " </div>\n" + " </div>\n" + " </body>\n" + "</html>", result1 ); } @Test public void testReparentThreeLevels() { final String result1 = testDiff("<html><head><title>Title</title></head><body><h1>Title text</h1></body></html>", (String from) -> { final StringBuilder buf = new StringBuilder(from); int startPos = buf.indexOf("<h1"); int endPos = buf.indexOf("</h1>") + 5; // insert from back to front, otherwise insert position changes buf.insert(endPos, "</div></div></div>"); buf.insert(startPos, "<div><div><div>"); return buf.toString(); }); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>\n" + " <div>\n" + " <div>\n" + " <div>\n" + " <h1>Title text</h1>\n" + " </div>\n" + " </div>\n" + " </div>\n" + " </body>\n" + "</html>", result1 ); } @Test public void testMove() { final String result1 = testDiff("<html><head><title>Title</title></head><body><h1>Title text</h1><div><h2>subtitle</h2></div></body></html>", (String from) -> { final StringBuilder buf = new StringBuilder(from); int startPos = buf.indexOf("<h1"); int endPos = buf.indexOf("</h1>") + 5; // cut out <h1> block final String toMove = buf.substring(startPos, endPos); buf.replace(startPos, endPos, ""); // insert after <h2> int insertPos = buf.indexOf("</h2>") + 5; // insert from back to front, otherwise insert position changes buf.insert(insertPos, toMove); return buf.toString(); }); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>\n" + " <div>\n" + " <h2>subtitle</h2>\n" + " <h1>Title text</h1>\n" + " </div>\n" + " </body>\n" + "</html>", result1 ); } @Test public void testSwap() { final StringBuilder clipboard = new StringBuilder(); final String result1 = testDiff("<html><head><title>Title</title></head><body><div><h2>one</h2></div><div><h2>two</h2></div><div><h2>three</h2></div><div><h2>four</h2></div></body></html>", (String from) -> { final StringBuilder buf = new StringBuilder(from); // cut out <div> block int cutStart = buf.indexOf("<h2") - (DOMNode.dataHashProperty.jsonName().length() + 46); int cutEnd = buf.indexOf("</h2>") + 16; clipboard.append(buf.substring(cutStart, cutEnd)); buf.replace(cutStart, cutEnd, ""); int insert = buf.indexOf("</h2>") + 16; buf.insert(insert, clipboard.toString()); return buf.toString(); }); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>\n" + " <div>\n" + " <h2>two</h2>\n" + " </div>\n" + " <div>\n" + " <h2>one</h2>\n" + " </div>\n" + " <div>\n" + " <h2>three</h2>\n" + " </div>\n" + " <div>\n" + " <h2>four</h2>\n" + " </div>\n" + " </body>\n" + "</html>", result1 ); } @Test public void testAddAttributes() { final String result1 = testDiff("<html><head><title>Title</title></head><body><div><h2>one</h2></div><div><h2>two</h2></div><div><h2>three</h2></div><div><h2>four</h2></div></body></html>", (String from) -> { final StringBuilder buf = new StringBuilder(from); int insert = buf.indexOf("<div ") + 5; buf.insert(insert, " class='test' id='one' "); return buf.toString(); }); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>\n" + " <div class=\"test\" id=\"one\">\n" + " <h2>one</h2>\n" + " </div>\n" + " <div>\n" + " <h2>two</h2>\n" + " </div>\n" + " <div>\n" + " <h2>three</h2>\n" + " </div>\n" + " <div>\n" + " <h2>four</h2>\n" + " </div>\n" + " </body>\n" + "</html>", result1 ); } @Test public void testModifyRemoveAttributes() { final String result1 = testDiff("<html><head><title>Title</title></head><body><div><h2 class=\"test\" id=\"one\">one</h2></div><div><h2>two</h2></div><div><h2 id=\"three\">three</h2></div><div><h2>four</h2></div></body></html>", (String from) -> { String modified = from; modified = modified.replace(" class=\"test\"", " class=\"foo\""); modified = modified.replace(" id=\"one\"", " id=\"two\""); modified = modified.replace(" id=\"three\"", " class=\"test\""); return modified; }); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>\n" + " <div>\n" + " <h2 class=\"foo\" id=\"two\">one</h2>\n" + " </div>\n" + " <div>\n" + " <h2>two</h2>\n" + " </div>\n" + " <div>\n" + " <h2 class=\"test\">three</h2>\n" + " </div>\n" + " <div>\n" + " <h2>four</h2>\n" + " </div>\n" + " </body>\n" + "</html>", result1 ); } @Test public void testBlockMoveUp() { final String result1 = testDiff("<html><head><title>Title</title></head><body><div>Text<h2>one</h2><p>two</p></div></body></html>", (String from) -> { final StringBuilder clipboard = new StringBuilder(); final StringBuilder buf = new StringBuilder(from); int cutStart = buf.indexOf("<h2"); int cutEnd = buf.indexOf("</p>") + 7; // cut out <h1> block clipboard.append(buf.substring(cutStart, cutEnd)); buf.replace(cutStart, cutEnd, ""); int insert = buf.indexOf("<div"); buf.insert(insert, clipboard.toString()); return buf.toString(); }); System.out.println(result1); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>\n" + " <h2>one</h2>\n" + " <p>two</p>\n" + " <div>Text </div>\n" + " </body>\n" + "</html>", result1 ); } @Test public void testBlockMoveDown() { final String result1 = testDiff("<html><head><title>Title</title></head><body><div>Text<h2>one</h2><div><p>two</p></div></div></body></html>", (String from) -> { final StringBuilder clipboard = new StringBuilder(); final StringBuilder buf = new StringBuilder(from); int cutStart = buf.indexOf("<h2"); int cutEnd = buf.indexOf("</h2>") + 5; // cut out <h1> block clipboard.append(buf.substring(cutStart, cutEnd)); buf.replace(cutStart, cutEnd, ""); int insert = buf.indexOf("<p "); buf.insert(insert, clipboard.toString()); System.out.println(buf.toString()); return buf.toString(); }); System.out.println(result1); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>\n" + " <div>Text \n" + " <div>\n" + " <h2>one</h2>\n" + " <p>two</p>\n" + " </div>\n" + " </div>\n" + " </body>\n" + "</html>", result1 ); } @Test public void testSurroundBlock() { final String result1 = testDiff("<html><head><title>Title</title></head><body><h1>title</h1><p>text</p></body></html>", (String from) -> { final StringBuilder buf = new StringBuilder(from); int insertStart = buf.indexOf("<h1"); int insertEnd = buf.indexOf("</p>") + 4; buf.insert(insertEnd, "</div>"); buf.insert(insertStart, "<div>"); return buf.toString(); }); System.out.println(result1); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>\n" + " <div>\n" + " <h1>title</h1>\n" + " <p>text</p>\n" + " </div>\n" + " </body>\n" + "</html>", result1 ); } @Test public void testModifyTag() { final String result1 = testDiff("<html><head><title>Title</title></head><body><h1>title</h1><p>text</p></body></html>", (String from) -> { String modified = from; modified = modified.replace("<h1 ", "<h2 "); modified = modified.replace("</h1>", "</h2>"); modified = modified.replace("<p ", "<a "); modified = modified.replace("</p>", "</a>"); return modified; }); System.out.println(result1); assertEquals( "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <title>Title</title>\n" + " </head>\n" + " <body>\n" + " <h2>title</h2>\n" + " <a>text</a>\n" + " </body>\n" + "</html>", result1 ); } @Test public void testTreeRemovalFix() { final String comment = "<!-- comment --->"; testDiff("<html><head><title>Title</title></head><body><div>" + comment + "<div>" + comment + "<div>test</div>" + comment + "<div></div></div></body></html>", (String from) -> { String modified = from; modified = modified.replace(comment, ""); return modified; }); // test result on the node level try (final Tx tx = app.tx()) { final Page page = app.nodeQuery(Page.class).andName("test").getFirst(); assertNotNull(page); final NodeList nodes = page.getElementsByTagName("div"); final List<Div> divs = collectNodes(nodes, Div.class); assertEquals("Wrong number of divs returned from node query", 4, divs.size()); // check first div, should have no siblings and one child final Div firstDiv = divs.get(0); assertEquals("Wrong number of children", 1, firstDiv.getChildRelationships().size()); assertNull("Node should not have siblings", firstDiv.getNextSibling()); // check second div, should have no siblings and two children final Div secondDiv = divs.get(1); assertEquals("Wrong number of children", 2, secondDiv.getChildRelationships().size()); assertNull("Node should not have siblings", secondDiv.getNextSibling()); // check third div, should have one sibling and one #text child final Div thirdDiv = divs.get(2); assertEquals("Wrong number of children", 1, thirdDiv.getChildRelationships().size()); assertNotNull("Node should have one sibling", thirdDiv.getNextSibling()); // check fourth div, should have no siblings and no children final Div fourthDiv = divs.get(3); assertEquals("Wrong number of children", 0, fourthDiv.getChildRelationships().size()); assertNull("Node should not have siblings", fourthDiv.getNextSibling()); } catch (FrameworkException fex) { fail("Unexpected exception"); } } private String testDiff(final String source, final Function<String, String> modifier) { Settings.JsonIndentation.setValue(true); Settings.HtmlIndentation.setValue(true); final StringBuilder buf = new StringBuilder(); String sourceHtml = null; try { // create page from source final Page sourcePage = Importer.parsePageFromSource(securityContext, source, "test"); // render page into HTML string try (final Tx tx = app.tx()) { sourceHtml = sourcePage.getContent(RenderContext.EditMode.RAW); tx.success(); } // modify HTML string with transformation function final String modifiedHtml = modifier.apply(sourceHtml); // parse page from modified source final Page modifiedPage = Importer.parsePageFromSource(securityContext, modifiedHtml, "Test"); // create and apply diff operations try (final Tx tx = app.tx()) { final List<InvertibleModificationOperation> changeSet = Importer.diffNodes(sourcePage, modifiedPage); for (final InvertibleModificationOperation op : changeSet) { System.out.println(op); // execute operation op.apply(app, sourcePage, modifiedPage); System.out.println("############################################################################################"); // System.out.println(sourcePage.getContent(RenderContext.EditMode.NONE)); } tx.success(); } // render modified page into buffer try (final Tx tx = app.tx()) { buf.append(sourcePage.getContent(RenderContext.EditMode.NONE)); tx.success(); } } catch (Throwable t) { logger.warn("", t); } return buf.toString(); } private <T> List<T> collectNodes(final NodeList source, final Class<T> type) { final List<T> list = new LinkedList<>(); final int len = source.getLength(); for (int i=0; i<len; i++) { list.add((T)source.item(i)); } return list; } }