Mercurial > hg > ConflictEditor
changeset 0:6f11757c4811
first drop of the conflict editor - mostly model work
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" + ] + } + ] +}