changeset 0:6f11757c4811

first drop of the conflict editor - mostly model work
author Dirk Olmes <dirk@xanthippe.ping.de>
date Mon, 12 Sep 2011 11:47:48 +0200
parents
children 9c42f25cd944
files conflict-editor/pom.xml conflict-editor/src/main/java/de/codedo/conflicteditor/Conflict.java conflict-editor/src/main/java/de/codedo/conflicteditor/ConflictsView.java conflict-editor/src/main/java/de/codedo/conflicteditor/CouchDb.java conflict-editor/src/main/java/de/codedo/conflicteditor/DocumentMatcher.java conflict-editor/src/main/java/de/codedo/conflicteditor/HttpAccess.java conflict-editor/src/main/java/de/codedo/conflicteditor/UrlConnectionHttpAccess.java conflict-editor/src/test/java/de/codedo/conflicteditor/ConflictTestCase.java conflict-editor/src/test/java/de/codedo/conflicteditor/ConflictsViewTestCase.java conflict-editor/src/test/java/de/codedo/conflicteditor/CouchDbTestCase.java conflict-editor/src/test/java/de/codedo/conflicteditor/Playground.java conflict-editor/src/test/resources/log4j.properties conflict-editor/src/test/resources/no-conflicts.json conflict-editor/src/test/resources/single-conflict.json
diffstat 14 files changed, 651 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conflict-editor/pom.xml	Mon Sep 12 11:47:48 2011 +0200
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>de.codedo</groupId>
+    <artifactId>conflict-editor</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+    <name>new project</name>
+    <url>http://maven.apache.org</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <vmtype>org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType</vmtype>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-mapper-asl</artifactId>
+            <version>1.8.5</version>
+        </dependency>
+
+        <!-- test dependencies -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.9</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>1.8.5</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>2.3.2</version>
+                <configuration>
+                    <source>1.6</source>
+                    <target>1.6</target>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-eclipse-plugin</artifactId>
+                <version>2.8</version>
+                <configuration>
+                    <downloadSources>true</downloadSources>
+                    <classpathContainers>
+                        <classpathContainer>org.eclipse.jdt.launching.JRE_CONTAINER/${vmtype}/JavaSE-1.6</classpathContainer>
+                    </classpathContainers>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conflict-editor/src/main/java/de/codedo/conflicteditor/Conflict.java	Mon Sep 12 11:47:48 2011 +0200
@@ -0,0 +1,47 @@
+
+package de.codedo.conflicteditor;
+
+import org.codehaus.jackson.JsonNode;
+
+public class Conflict extends Object
+{
+    private JsonNode _currentVersionNode;
+
+    public Conflict(JsonNode node)
+    {
+        super();
+        _currentVersionNode = node.findValue("key");
+    }
+
+    public JsonNode getCurrentDocument()
+    {
+        return _currentVersionNode;
+    }
+
+    public String getId()
+    {
+        return _currentVersionNode.findValue("_id").getTextValue();
+    }
+
+    /**
+     * @return the currently active revision of the document
+     */
+    public String getRevision()
+    {
+        return _currentVersionNode.findValue("_rev").getTextValue();
+    }
+
+    /**
+     * @return the conflicting revision
+     */
+    public String getConflictRevision()
+    {
+        JsonNode conflictsNode = _currentVersionNode.findValue("_conflicts");
+        if (conflictsNode.isArray())
+        {
+            return conflictsNode.get(0).getTextValue();
+        }
+
+        throw new IllegalStateException("this node does not have conflicts");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conflict-editor/src/main/java/de/codedo/conflicteditor/ConflictsView.java	Mon Sep 12 11:47:48 2011 +0200
@@ -0,0 +1,75 @@
+
+package de.codedo.conflicteditor;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.codehaus.jackson.JsonNode;
+import org.codehaus.jackson.map.ObjectMapper;
+
+public class ConflictsView extends Object
+{
+    // @formatter:off
+    private static final String VIEW =
+        "function(doc) {" +
+        "    if(doc._conflicts) {" +
+        "        emit(doc, null);" +
+        "    }" +
+        "}";
+    // @formatter:on
+
+    public static final String JSON = String.format("{ \"map\": \"%s\" }", VIEW);
+
+    private static final String TEMP_VIEW_URL_PATH = "_temp_view";
+
+    private HttpAccess _httpAccess = null;
+
+    private URL _viewUrl;
+
+    public ConflictsView(CouchDb database) throws MalformedURLException
+    {
+        super();
+        _viewUrl = database.urlByAddingPath(TEMP_VIEW_URL_PATH);
+    }
+
+    public ConflictsView(HttpAccess httpAccess)
+    {
+        super();
+        _httpAccess = httpAccess;
+    }
+
+    public JsonNode getConflicts() throws IOException
+    {
+        Reader reader = null;
+        try
+        {
+            reader = getHttpAccess().post(JSON);
+            return rowsNodeFromJson(reader);
+        }
+        finally
+        {
+            if (reader != null)
+            {
+                reader.close();
+            }
+        }
+    }
+
+    private HttpAccess getHttpAccess()
+    {
+        if (_httpAccess == null)
+        {
+            _httpAccess = new UrlConnectionHttpAccess(_viewUrl);
+        }
+        return _httpAccess;
+    }
+
+    private JsonNode rowsNodeFromJson(Reader reader) throws IOException
+    {
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode rootNode = mapper.readTree(reader);
+        return rootNode.findValue("rows");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conflict-editor/src/main/java/de/codedo/conflicteditor/CouchDb.java	Mon Sep 12 11:47:48 2011 +0200
@@ -0,0 +1,59 @@
+
+package de.codedo.conflicteditor;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class CouchDb
+{
+    private String _baseUrl;
+
+    public CouchDb(String baseUrl) throws MalformedURLException
+    {
+        super();
+        check(baseUrl);
+        initBaseUrl(baseUrl);
+    }
+
+    private void check(String baseUrl) throws MalformedURLException
+    {
+        if (baseUrl == null)
+        {
+            throw new IllegalArgumentException("base url may not be null");
+        }
+        if (baseUrl.length() == 0)
+        {
+            throw new MalformedURLException("base url may not be empty");
+        }
+    }
+
+    private void initBaseUrl(String baseUrl)
+    {
+        if (baseUrl.endsWith("/") == false)
+        {
+            baseUrl += "/";
+        }
+        _baseUrl = baseUrl;
+    }
+
+    public URL urlByAddingPath(String path) throws MalformedURLException
+    {
+        String extendedPath = appendPathToBaseUrl(path);
+        return new URL(extendedPath);
+    }
+
+    public URL urlForDocumentAndRevision(String id, String revision) throws MalformedURLException
+    {
+        String path = String.format("%s?rev=%s", appendPathToBaseUrl(id), revision);
+        return new URL(path);
+    }
+
+    private String appendPathToBaseUrl(String path)
+    {
+        if (path.startsWith("/"))
+        {
+            path = path.substring(1);
+        }
+        return _baseUrl + path;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conflict-editor/src/main/java/de/codedo/conflicteditor/DocumentMatcher.java	Mon Sep 12 11:47:48 2011 +0200
@@ -0,0 +1,58 @@
+
+package de.codedo.conflicteditor;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.codehaus.jackson.JsonNode;
+
+public class DocumentMatcher
+{
+    // @formatter:off
+    private static final Collection<String> IGNORED_FIELD_NAMES = Arrays.asList(
+        "_id", "_rev","_conflicts"
+    );
+    // @formatter:on
+
+    private JsonNode _original;
+    private JsonNode _other;
+
+    public DocumentMatcher(JsonNode original, JsonNode other)
+    {
+        super();
+        _original = original;
+        _other = other;
+    }
+
+    public void compare()
+    {
+        Set<String> originalFieldNames = getFieldNames(_original);
+        for (String name : originalFieldNames)
+        {
+            Object originalValue = _original.findValue(name).getValueAsText();
+            Object otherValue = _other.findValue(name).getValueAsText();
+            if (originalValue.equals(otherValue) == false)
+            {
+                System.out.printf("%s : \"%s\" - \"%s\"\n", name, originalValue, otherValue);
+            }
+        }
+    }
+
+    private Set<String> getFieldNames(JsonNode node)
+    {
+        Set<String> fieldNames = new HashSet<String>();
+        Iterator<String> fieldNameIter = node.getFieldNames();
+        while (fieldNameIter.hasNext())
+        {
+            String name = fieldNameIter.next();
+            if (IGNORED_FIELD_NAMES.contains(name) == false)
+            {
+                fieldNames.add(name);
+            }
+        }
+        return fieldNames;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conflict-editor/src/main/java/de/codedo/conflicteditor/HttpAccess.java	Mon Sep 12 11:47:48 2011 +0200
@@ -0,0 +1,12 @@
+
+package de.codedo.conflicteditor;
+
+import java.io.IOException;
+import java.io.Reader;
+
+public interface HttpAccess
+{
+    Reader post(String content) throws IOException;
+
+    Reader get() throws IOException;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conflict-editor/src/main/java/de/codedo/conflicteditor/UrlConnectionHttpAccess.java	Mon Sep 12 11:47:48 2011 +0200
@@ -0,0 +1,70 @@
+
+package de.codedo.conflicteditor;
+
+import java.io.BufferedReader;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.HttpURLConnection;
+import java.net.ProtocolException;
+import java.net.URL;
+
+public class UrlConnectionHttpAccess extends Object implements HttpAccess
+{
+    private static final String HEADER_CONTENT_LENGTH = "Content-Length";
+
+    private static final String HEADER_CONTENT_TYPE = "Content-Type";
+
+    private static final String HTTP_METHOD_POST = "POST";
+
+    private URL _url;
+
+    public UrlConnectionHttpAccess(URL url)
+    {
+        super();
+        _url = url;
+    }
+
+    @Override
+    public Reader post(String content) throws IOException
+    {
+        HttpURLConnection connection = createUrlConnection(content);
+        send(connection, content);
+        return createReader(connection);
+    }
+
+    private HttpURLConnection createUrlConnection(String content) throws IOException, ProtocolException
+    {
+        HttpURLConnection connection = (HttpURLConnection)_url.openConnection();
+        connection.setRequestMethod(HTTP_METHOD_POST);
+        connection.setRequestProperty(HEADER_CONTENT_TYPE, "application/json");
+        connection.setRequestProperty(HEADER_CONTENT_LENGTH, Integer.toString(content.length()));
+        connection.setDoInput(true);
+        connection.setDoOutput(true);
+        return connection;
+    }
+
+    private void send(HttpURLConnection connection, String content) throws IOException
+    {
+        DataOutputStream output = new DataOutputStream(connection.getOutputStream());
+        output.writeBytes(content);
+        output.flush();
+        output.close();
+    }
+
+    private Reader createReader(HttpURLConnection connection) throws IOException
+    {
+        InputStream input = connection.getInputStream();
+        return new BufferedReader(new InputStreamReader(input));
+    }
+
+    @Override
+    public Reader get() throws IOException
+    {
+        HttpURLConnection connection = (HttpURLConnection)_url.openConnection();
+        InputStream input = connection.getInputStream();
+        return new BufferedReader(new InputStreamReader(input));
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conflict-editor/src/test/java/de/codedo/conflicteditor/ConflictTestCase.java	Mon Sep 12 11:47:48 2011 +0200
@@ -0,0 +1,74 @@
+
+package de.codedo.conflicteditor;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertThat;
+
+import org.codehaus.jackson.JsonNode;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.node.ArrayNode;
+import org.codehaus.jackson.node.ObjectNode;
+import org.junit.Test;
+
+public class ConflictTestCase extends Object
+{
+    private static final String BASE_REVISION = "24-223d00d8f77d78a2911cbb265f5025eb";
+    private static final String CONFLICT_REVISION = "16-7f27b886bf38e4f9e543bee0ddf85758";
+    private static final String ID = "54620eb9a46d495eac294a3bd52f7b00";
+
+    @Test
+    public void idFromConflict() throws Exception
+    {
+        JsonNode node = createNodeWithConflictRevisions(CONFLICT_REVISION);
+        Conflict conflict = new Conflict(node);
+        assertThat(conflict.getId(), equalTo(ID));
+    }
+
+    @Test
+    public void baseRevisionFromConflict()
+    {
+        JsonNode node = createNodeWithConflictRevisions(CONFLICT_REVISION);
+        Conflict conflict = new Conflict(node);
+        assertThat(conflict.getRevision(), equalTo(BASE_REVISION));
+    }
+
+    @Test
+    public void conflictingRevisionFromConflict()
+    {
+        JsonNode node = createNodeWithConflictRevisions(CONFLICT_REVISION);
+        Conflict conflict = new Conflict(node);
+        assertThat(conflict.getConflictRevision(), equalTo(CONFLICT_REVISION));
+    }
+
+    private ObjectNode createNodeWithConflictRevisions(String... revisions)
+    {
+        ObjectNode node = createBaseNode();
+        ObjectNode keyNode = (ObjectNode)node.findValue("key");
+
+        ArrayNode conflicts = node.arrayNode();
+        for (String revision : revisions)
+        {
+            conflicts.add(revision);
+        }
+        keyNode.put("_conflicts", conflicts);
+        return node;
+    }
+
+    private ObjectNode createBaseNode()
+    {
+        ObjectMapper mapper = new ObjectMapper();
+        ObjectNode node = mapper.createObjectNode();
+        node.put("id", ID);
+
+        ObjectNode keyNode = node.objectNode();
+        keyNode.put("_id", ID);
+        keyNode.put("_rev", BASE_REVISION);
+        node.put("key", keyNode);
+
+        ArrayNode valueNode = node.arrayNode();
+        valueNode.add(CONFLICT_REVISION);
+        node.put("value", valueNode);
+
+        return node;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conflict-editor/src/test/java/de/codedo/conflicteditor/ConflictsViewTestCase.java	Mon Sep 12 11:47:48 2011 +0200
@@ -0,0 +1,46 @@
+
+package de.codedo.conflicteditor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+import org.codehaus.jackson.JsonNode;
+import org.junit.Test;
+
+public class ConflictsViewTestCase extends Object
+{
+    @Test
+    public void noConflicts() throws Exception
+    {
+        JsonNode conflicts = loadConflicts("no-conflicts.json");
+        assertEquals(0, conflicts.size());
+    }
+
+    @Test
+    public void oneConflict() throws Exception
+    {
+        JsonNode conflicts = loadConflicts("single-conflict.json");
+        assertEquals(1, conflicts.size());
+    }
+
+    private JsonNode loadConflicts(String filename) throws IOException
+    {
+        InputStream input = getClass().getClassLoader().getResourceAsStream(filename);
+        assertNotNull(input);
+
+        HttpAccess httpAccess = mock(HttpAccess.class);
+        Reader reader = new InputStreamReader(input);
+        when(httpAccess.post(anyString())).thenReturn(reader);
+
+        ConflictsView conflictsView = new ConflictsView(httpAccess);
+        return conflictsView.getConflicts();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conflict-editor/src/test/java/de/codedo/conflicteditor/CouchDbTestCase.java	Mon Sep 12 11:47:48 2011 +0200
@@ -0,0 +1,43 @@
+
+package de.codedo.conflicteditor;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertThat;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.junit.Test;
+
+public class CouchDbTestCase extends Object
+{
+    @Test(expected = IllegalArgumentException.class)
+    public void nullUrl() throws Exception
+    {
+        new CouchDb(null);
+    }
+
+    @Test(expected = MalformedURLException.class)
+    public void emptyUrl() throws Exception
+    {
+        new CouchDb("");
+    }
+
+    @Test
+    public void extendBaseUrlEndingWithSlash() throws Exception
+    {
+        String baseUrl = "http://localhost:5984/test/";
+        CouchDb db = new CouchDb(baseUrl);
+        URL extended = db.urlByAddingPath("/foo");
+        assertThat(extended.toString(), equalTo("http://localhost:5984/test/foo"));
+    }
+
+    @Test
+    public void urlForDocumentAndRevision() throws Exception
+    {
+        String baseUrl = "http://localhost:5984/test/";
+        CouchDb db = new CouchDb(baseUrl);
+        URL docUrl = db.urlForDocumentAndRevision("doc", "revision");
+        assertThat(docUrl.toString(), equalTo("http://localhost:5984/test/doc?rev=revision"));
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conflict-editor/src/test/java/de/codedo/conflicteditor/Playground.java	Mon Sep 12 11:47:48 2011 +0200
@@ -0,0 +1,71 @@
+
+package de.codedo.conflicteditor;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.net.URL;
+
+import org.codehaus.jackson.JsonNode;
+import org.codehaus.jackson.map.ObjectMapper;
+
+public class Playground extends Object
+{
+    private static final String BASE_URL = "http://localhost:5984/conflicts";
+
+    public static void main(String[] args) throws Exception
+    {
+        CouchDb database = new CouchDb(BASE_URL);
+
+        ConflictsView conflictsView = new ConflictsView(database);
+        JsonNode conflicts = conflictsView.getConflicts();
+
+        int size = conflicts.size();
+        for (int i = 0; i < size; i++)
+        {
+            Conflict conflict = new Conflict(conflicts.get(i));
+            JsonNode currentDocument = conflict.getCurrentDocument();
+            System.out.println(currentDocument.findValue("rss_url").getTextValue());
+            // System.out.println("current:");
+            // prettyPrint(currentDocument);
+
+            URL documentUrl = database.urlForDocumentAndRevision(conflict.getId(),
+                conflict.getConflictRevision());
+            System.out.printf("curl -X DELETE \"%s\"\n", documentUrl);
+            Reader reader = new UrlConnectionHttpAccess(documentUrl).get();
+
+            JsonNode conflictDocument = new ObjectMapper().readTree(reader);
+            // System.out.println("\nconflict:");
+            // prettyPrint(conflictDocument);
+
+            compare(currentDocument, conflictDocument);
+            System.out.println("\n");
+        }
+    }
+
+    private static void compare(JsonNode currentDocument, JsonNode conflictDocument)
+    {
+        new DocumentMatcher(currentDocument, conflictDocument).compare();
+    }
+
+    private static void prettyPrint(JsonNode node) throws IOException
+    {
+        PrintStream out = new IgnoreClosePrintStream(System.out);
+        new ObjectMapper().defaultPrettyPrintingWriter().writeValue(out, node);
+    }
+
+    private static class IgnoreClosePrintStream extends PrintStream
+    {
+        public IgnoreClosePrintStream(OutputStream outputStream)
+        {
+            super(outputStream);
+        }
+
+        @Override
+        public void close()
+        {
+            // do not close
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conflict-editor/src/test/resources/log4j.properties	Mon Sep 12 11:47:48 2011 +0200
@@ -0,0 +1,7 @@
+
+# The usual stuff. Note that A1 is configured in root, not separately
+log4j.rootCategory = DEBUG, A1
+log4j.appender.A1 = org.apache.log4j.ConsoleAppender
+log4j.appender.A1.layout = org.apache.log4j.PatternLayout
+#log4j.appender.A1.layout.ConversionPattern = %d{MMM dd HH:mm:ss} [%t] %p %c - %m%n
+log4j.appender.A1.layout.ConversionPattern = %c{1} - %m%n
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conflict-editor/src/test/resources/no-conflicts.json	Mon Sep 12 11:47:48 2011 +0200
@@ -0,0 +1,1 @@
+{ "total_rows":0, "offset":0, "rows":[]}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conflict-editor/src/test/resources/single-conflict.json	Mon Sep 12 11:47:48 2011 +0200
@@ -0,0 +1,26 @@
+{
+  "total_rows": 24,
+  "offset": 0,
+  "rows": [
+    {
+      "id": "54620eb9a46d495eac294a3bd508db39",
+      "key": {
+        "_id": "54620eb9a46d495eac294a3bd508db39",
+        "_rev": "24-223d00d8f77d78a2911cbb265f5025eb",
+        "rss_url": "http://feeds.feedburner.com/muleblog\n",
+        "always_open_in_browser": false,
+        "auto_load_entry_link": false,
+        "title": "From the Mule\u2019s Mouth",
+        "doctype": "feed",
+        "update_interval": 60,
+        "next_update": "2011-09-12T03:20:06Z",
+        "_conflicts": [
+          "16-7f27b886bf38e4f9e543bee0ddf85758"
+        ]
+      },
+      "value": [
+        "16-7f27b886bf38e4f9e543bee0ddf85758"
+      ]
+    }
+  ]
+}