# HG changeset patch # User Dirk Olmes # Date 1315820868 -7200 # Node ID 6f11757c48115ddf89eca2a2f67b8aa208911d4f first drop of the conflict editor - mostly model work diff -r 000000000000 -r 6f11757c4811 conflict-editor/pom.xml --- /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 @@ + + + 4.0.0 + de.codedo + conflict-editor + 1.0-SNAPSHOT + jar + new project + http://maven.apache.org + + + UTF-8 + org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType + + + + + org.codehaus.jackson + jackson-mapper-asl + 1.8.5 + + + + + junit + junit + 4.9 + test + + + org.mockito + mockito-all + 1.8.5 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.6 + 1.6 + UTF-8 + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.8 + + true + + org.eclipse.jdt.launching.JRE_CONTAINER/${vmtype}/JavaSE-1.6 + + + + + + diff -r 000000000000 -r 6f11757c4811 conflict-editor/src/main/java/de/codedo/conflicteditor/Conflict.java --- /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"); + } +} diff -r 000000000000 -r 6f11757c4811 conflict-editor/src/main/java/de/codedo/conflicteditor/ConflictsView.java --- /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"); + } +} diff -r 000000000000 -r 6f11757c4811 conflict-editor/src/main/java/de/codedo/conflicteditor/CouchDb.java --- /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; + } +} diff -r 000000000000 -r 6f11757c4811 conflict-editor/src/main/java/de/codedo/conflicteditor/DocumentMatcher.java --- /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 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 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 getFieldNames(JsonNode node) + { + Set fieldNames = new HashSet(); + Iterator fieldNameIter = node.getFieldNames(); + while (fieldNameIter.hasNext()) + { + String name = fieldNameIter.next(); + if (IGNORED_FIELD_NAMES.contains(name) == false) + { + fieldNames.add(name); + } + } + return fieldNames; + } +} diff -r 000000000000 -r 6f11757c4811 conflict-editor/src/main/java/de/codedo/conflicteditor/HttpAccess.java --- /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; +} diff -r 000000000000 -r 6f11757c4811 conflict-editor/src/main/java/de/codedo/conflicteditor/UrlConnectionHttpAccess.java --- /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)); + } +} diff -r 000000000000 -r 6f11757c4811 conflict-editor/src/test/java/de/codedo/conflicteditor/ConflictTestCase.java --- /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; + } +} diff -r 000000000000 -r 6f11757c4811 conflict-editor/src/test/java/de/codedo/conflicteditor/ConflictsViewTestCase.java --- /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(); + } +} diff -r 000000000000 -r 6f11757c4811 conflict-editor/src/test/java/de/codedo/conflicteditor/CouchDbTestCase.java --- /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")); + } +} diff -r 000000000000 -r 6f11757c4811 conflict-editor/src/test/java/de/codedo/conflicteditor/Playground.java --- /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 + } + } +} diff -r 000000000000 -r 6f11757c4811 conflict-editor/src/test/resources/log4j.properties --- /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 diff -r 000000000000 -r 6f11757c4811 conflict-editor/src/test/resources/no-conflicts.json --- /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":[]} diff -r 000000000000 -r 6f11757c4811 conflict-editor/src/test/resources/single-conflict.json --- /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" + ] + } + ] +}