Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Doc update handlers #5

Merged
merged 2 commits into from

2 participants

@bytefoundry

Hello,

As discussed I have added code to implement the creation and execution of document update handlers in couchdb. Document update handlers are useful because they allow you to define functions on the server which can allow you to do partial document updates, updating individual fields etc. This saves downloading the entire document in to your application, making a couple of changes and then sending the whole document back etc...

I have also created a page on my fork wiki which shows how to use them:

https://github.com/bytefoundry/couchdb4j/wiki/Usage:-Document-Update-Handlers

Thanks and Regards

Raymond Wilson

@mbreese mbreese merged commit cc852f7 into mbreese:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
1  .gitignore
@@ -1,6 +1,7 @@
.classpath
.project
.svn
+.settings
build/
dist/
test.log
View
76 src/java/com/fourspaces/couchdb/Database.java
@@ -17,7 +17,6 @@
package com.fourspaces.couchdb;
import java.io.IOException;
-
import com.fourspaces.couchdb.util.JSONUtils;
import static com.fourspaces.couchdb.util.JSONUtils.urlEncodePath;
import net.sf.json.*;
@@ -45,6 +44,7 @@
private static final String VIEW = "/_view/";
private static final String DESIGN = "_design/";
+ private static final String UPDATE = "/_update/";
/**
@@ -414,10 +414,82 @@ public String getAttachment(String id, String attachment) throws IOException {
* @param fname attachment name
* @param ctype content type
* @param attachment attachment body
- * @return was the delete successful?
+ * @return was the PUT successful?
*/
public String putAttachment(String id, String fname, String ctype, String attachment) throws IOException {
CouchResponse resp = session.put(name + "/" + urlEncodePath(id) + "/" + fname, ctype, attachment);
return resp.getBody();
}
+
+ /**
+ * Update an existing document using a document update handler. Returns false if there is a failure
+ * making the PUT/POST or there is a problem with the CouchResponse.
+ * @author rwilson
+ * @param update
+ * @return
+ */
+ public boolean updateDocument(Update update) {
+ if ((update == null) || (update.getDocId() == null) || (update.getDocId().equals(""))) {
+ return false;
+ }
+
+ String url = null;
+
+ String[] elements = update.getName().split("/");
+ url = this.name + "/" + ((elements.length < 2) ? elements[0] : DESIGN + elements[0] + UPDATE + elements[1]) + "/" + update.getDocId();
+
+ if (update.getMethodPOST()) {
+ try {
+ // Invoke the POST method passing the parameters in the body
+ CouchResponse resp = session.post(url, "application/x-www-form-urlencoded", update.getURLFormEncodedString(), null);
+ return resp.isOk();
+ } catch (Exception e) {
+ return false;
+ }
+ } else {
+ try {
+ // Invoke the PUT method passing the parameters as a query string
+ CouchResponse resp = session.put(url, null, null, update.getQueryString());
+ return resp.isOk();
+ } catch (Exception e) {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Update an existing document using a document update handler and return the message body.
+ * Returns null if the is problem with the PUT/POST or CouchResponse.
+ * @author rwilson
+ * @param update
+ * @return
+ */
+ public String updateDocumentWithResponse(Update update) {
+ if ((update == null) || (update.getDocId() == null) || (update.getDocId().equals(""))) {
+ return "";
+ }
+
+ String url = null;
+
+ String[] elements = update.getName().split("/");
+ url = this.name + "/" + ((elements.length < 2) ? elements[0] : DESIGN + elements[0] + UPDATE + elements[1]) + "/" + update.getDocId();
+
+ if (update.getMethodPOST()) {
+ try {
+ // Invoke the POST method passing the parameters in the body
+ CouchResponse resp = session.post(url, "application/x-www-form-urlencoded", update.getURLFormEncodedString(), null);
+ return resp.getBody();
+ } catch (Exception e) {
+ return null;
+ }
+ } else {
+ try {
+ // Invoke the PUT method passing the parameters as a query string
+ CouchResponse resp = session.put(url, null, null, update.getQueryString());
+ return resp.getBody();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+ }
}
View
42 src/java/com/fourspaces/couchdb/Document.java
@@ -209,6 +209,48 @@ public View addView(String designDoc, String viewName, String function) {
}
/**
+ * Adds an update handler to the document and sets the document ID to that of a design
+ * document. If the function name key already exists within the "updates" element it will
+ * be overwritten. The document must be saved for the change to persist.
+ * @author rwilson
+ * @param designDoc
+ * @param functionName
+ * @param function
+ */
+ public void addUpdateHandler(String designDoc, String functionName, String function) {
+ object.put("_id", "_design/"+ designDoc);
+
+ if (object.has("updates")) {
+ JSONObject updates = object.getJSONObject("updates");
+ updates.put(functionName, JSONUtils.stringSerializedFunction(function));
+ } else {
+ JSONObject func = new JSONObject();
+ func.put(functionName, JSONUtils.stringSerializedFunction(function));
+ object.put("updates", func);
+ }
+ }
+
+ /**
+ * Adds an update handler to the document. The ID of the document is not modified. If the function
+ * name key already exists within the "updates" element it will be overwritten. The document
+ * must be saved for the change to persist.
+ * @author rwilson
+ * @param functionName
+ * @param function
+ * @return
+ */
+ public void addUpdateHandler(String functionName, String function) {
+ if (object.has("updates")) {
+ JSONObject updates = object.getJSONObject("updates");
+ updates.put(functionName, JSONUtils.stringSerializedFunction(function));
+ } else {
+ JSONObject func = new JSONObject();
+ func.put(functionName, JSONUtils.stringSerializedFunction(function));
+ object.put("updates", func);
+ }
+ }
+
+ /**
* Removes a view from this document.
* <p>
* This isn't persisted until the document is saved.
View
54 src/java/com/fourspaces/couchdb/Session.java
@@ -324,6 +324,33 @@ CouchResponse post(String url, String content, String queryString) {
}
/**
+ * Send a POST with a body, query string and specified content type
+ * @author rwilson
+ * @param url
+ * @param ctype
+ * @param content
+ * @param queryString
+ * @return
+ */
+ CouchResponse post(String url, String ctype, String content, String queryString) {
+ HttpPost post = new HttpPost(buildUrl(url, queryString));
+ if (content!=null) {
+ HttpEntity entity;
+ try {
+ entity = new StringEntity(content, DEFAULT_CHARSET);
+ post.setEntity(entity);
+ if (ctype != null) {
+ post.setHeader(new BasicHeader("Content-Type", ctype));
+ }
+ } catch (UnsupportedEncodingException e) {
+ log.error(ExceptionUtils.getStackTrace(e));
+ }
+ }
+
+ return http(post);
+ }
+
+ /**
* Send a PUT (for creating databases)
* @param url
* @return
@@ -369,6 +396,33 @@ CouchResponse put(String url, String ctype, String content) {
}
return http(put);
}
+
+ /**
+ * Overloaded Put using by attachments and query string
+ * @author rwilson
+ * @param url
+ * @param ctype
+ * @param content
+ * @param queryString
+ * @return
+ */
+ CouchResponse put(String url, String ctype, String content, String queryString) {
+ HttpPut put = new HttpPut(buildUrl(url, queryString));
+ if (content!=null) {
+ HttpEntity entity;
+ try {
+ entity = new StringEntity(content, DEFAULT_CHARSET);
+ put.setEntity(entity);
+ if (ctype!=null) {
+ put.setHeader(new BasicHeader("Content-Type", ctype));
+ }
+ } catch (UnsupportedEncodingException e) {
+ log.error(ExceptionUtils.getStackTrace(e));
+ }
+ }
+ return http(put);
+ }
+
/**
* Send a GET request
* @param url
View
199 src/java/com/fourspaces/couchdb/Update.java
@@ -0,0 +1,199 @@
+/**
+ * Copyright (c) 2012 Raymond Wilson (http://www.bytefoundry.co.uk)
+ *
+ * Licensed 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 com.fourspaces.couchdb;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.message.BasicNameValuePair;
+
+/**
+ * The 'Update' is a mechanism for executing document update handlers against existing documents. These
+ * handlers are server-side functions that exist in your database's design documents that allow you to do things
+ * such as providing a server-side last modified timestamp, updating individual fields in a document without
+ * first getting the latest revision, etc. See the following web page for more information:
+ * <p/>
+ * http://wiki.apache.org/couchdb/Document_Update_Handlers
+ *
+ * @author rwilson
+ *
+ */
+public class Update {
+ protected String name;
+ protected String docId;
+ protected boolean usePOST;
+ protected List<NameValuePair> params;
+
+ /**
+ * Standard c-tor
+ * @param name The name should be in the format "[designDoc]/[update]" e.g "accounts/personal" (corresponding
+ * to "_design/accounts/_update/personal/").
+ */
+ public Update(String name) {
+ this.name = name;
+ docId = null;
+ usePOST = false;
+ params = new ArrayList<NameValuePair>();
+ }
+
+ /**
+ * Overloaded C-tor with ID of document to update specified.
+ * @param name
+ * @param docId
+ */
+ public Update(String name, String docId) {
+ this.name = name;
+ this.docId = docId;
+ usePOST = false;
+ params = new ArrayList<NameValuePair>();
+ }
+
+ /**
+ * Overloaded c-tor with doc ID and whether or not to use POST method specified. Note the default
+ * update method is PUT with the data passed via a query string.
+ * @param name
+ * @param docId
+ * @param usePOST
+ */
+ public Update(String name, String docId, boolean usePOST) {
+ this.name = name;
+ this.docId = docId;
+ this.usePOST = usePOST;
+ params = new ArrayList<NameValuePair>();
+ }
+
+ /**
+ * The ID of the document to update. This is a requirement and the update will fail
+ * without it.
+ * @param docId
+ */
+ public void setDocId(String docId) {
+ this.docId = docId;
+ }
+
+ /**
+ * Get the ID of the document to update.
+ * @return
+ */
+ public String getDocId() {
+ return docId;
+ }
+
+ /**
+ * The name of design document and the update function in the format "[designDoc]/[updateFunc]" e.g.
+ * "accounts/personal" (corresponding to "_design/accounts/_update/personal").
+ * @param name
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Get the name of the update handler.
+ * @return
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Set whether or not to use POST rather than PUT. Note that your document update handler must be
+ * written to use POST parameters (e.g. 'req.form.field' as opposed to 'req.query.field').
+ * @param usePOST
+ */
+ public void setMethodPOST(boolean usePOST) {
+ this.usePOST = usePOST;
+ }
+
+ /**
+ * Get whether or not to use the POST method.
+ * @return
+ */
+ public boolean getMethodPOST() {
+ return usePOST;
+ }
+
+ /**
+ * Add a key/value parameter to be passed to the document update handler function. Note that if
+ * the key already exists, all instances of it will be removed prior to insertion.
+ * @param key
+ * @param value
+ */
+ public void addParameter(String key, String value) {
+ if ((key == null) || (key.equals(""))) {
+ return;
+ }
+
+ removeParameter(key);
+
+ params.add(new BasicNameValuePair(key, value));
+ }
+
+ /**
+ * Remove a key/value parameter from being passed to the document update handler function. If
+ * multiple instances of the key exists they will all be removed.
+ * @param key
+ */
+ public void removeParameter(String key) {
+ if ((key == null) || (key.equals(""))) {
+ return;
+ }
+
+ List<NameValuePair> toRemove = new ArrayList<NameValuePair>();
+
+ for (NameValuePair p : params) {
+ if (p.getName().equals(key)) {
+ toRemove.add(p);
+ }
+ }
+
+ for (NameValuePair p : toRemove) {
+ params.remove(p);
+ }
+ }
+
+ /**
+ * Return the parameters as a query string. You're unlikely to call this yourself as it will be called
+ * by the Database::updateDocment() method depending on whether or not it is using PUT or POST.
+ * @return
+ */
+ public String getQueryString() {
+ String qs = "";
+
+ for (NameValuePair p : params) {
+ qs += p.getName() + "=" + p.getValue() + "&";
+ }
+
+ // Strip the trailing ampersand
+ if (qs.endsWith("&")) {
+ qs = qs.substring(0, (qs.length() - 1));
+ }
+
+ return qs;
+ }
+
+ /**
+ * Return the parameters in a format compatible with the 'x-www-form-urlencoded' content type. This
+ * is called by the Database::updateDocument() method to perform a POST update
+ * @return
+ */
+ public String getURLFormEncodedString() {
+ return URLEncodedUtils.format(params, "UTF-8");
+ }
+}
View
130 src/test/com/fourspaces/couchdb/test/UpdateTest.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright (c) 2012 Raymond Wilson (http://www.bytefoundry.co.uk)
+ *
+ * Licensed 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 com.fourspaces.couchdb.test;
+
+import static org.junit.Assert.*;
+
+import net.sf.json.JSONObject;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.fourspaces.couchdb.Database;
+import com.fourspaces.couchdb.Document;
+import com.fourspaces.couchdb.Session;
+import com.fourspaces.couchdb.Update;
+import com.fourspaces.couchdb.util.JSONUtils;
+
+public class UpdateTest {
+ Log log = LogFactory.getLog(getClass());
+
+ Session sess = TestSession.getTestSession();
+ Database foo;
+
+ @Before
+ public void createTestDB()
+ throws Exception {
+ foo = sess.createDatabase("update_test");
+
+ // Create test design document
+ Document design = new Document();
+
+ design.put("_id", "_design/junit");
+
+ JSONObject funcs = new JSONObject();
+ funcs.put("put", JSONUtils.stringSerializedFunction("function(doc,req){doc.Field1=req.query.field1; return [doc, '{\\\"ok\\\":\\\"true\\\"}'];}"));
+ funcs.put("post", JSONUtils.stringSerializedFunction("function(doc,req){doc.Field2=req.form.field2; return [doc, '{\\\"ok\\\":\\\"true\\\"}'];}"));
+
+ design.accumulate("updates", funcs);
+
+ // System.err.println("UDFUNCS: " + design.toString());
+
+ foo.saveDocument(design);
+
+ // Create a document containing test data to process
+ Document testDoc = new Document();
+
+ testDoc.put("_id", "test_data");
+ testDoc.put("Field1", "Default");
+ testDoc.put("Field2", "Default");
+
+ foo.saveDocument(testDoc);
+ }
+
+ @Test
+ public void testPUTUpdate()
+ throws Exception {
+ Update putUpdate = new Update("junit/put", "test_data");
+ putUpdate.addParameter("field1", "UpdatedByPUT");
+
+ boolean result = foo.updateDocument(putUpdate);
+ assertTrue(result);
+
+ // Retrieve the field and make sure the value is correct
+ Document testDoc = foo.getDocument("test_data");
+ assertNotNull(testDoc);
+ assertEquals("UpdatedByPUT", testDoc.getString("Field1"));
+ }
+
+ @Test
+ public void testPOSTUpdate()
+ throws Exception {
+ Update postUpdate = new Update("junit/post", "test_data");
+ postUpdate.addParameter("field2", "UpdatedByPOST");
+ postUpdate.setMethodPOST(true);
+
+ boolean result = foo.updateDocument(postUpdate);
+ assertTrue(result);
+
+ // Retrieve the field and make sure the value is correct
+ Document testDoc = foo.getDocument("test_data");
+ assertNotNull(testDoc);
+ assertEquals("UpdatedByPOST", testDoc.getString("Field2"));
+ }
+
+ @Test
+ public void testAddUpdateHandler()
+ throws Exception {
+ // Retrieve the test design document
+ Document designDoc = foo.getDocument("_design/junit");
+ assertNotNull(designDoc);
+
+ // Add the new update handler
+ designDoc.addUpdateHandler("test", "function(doc,req){doc.Field1='HANDLERTEST'; return [doc, '{\\\"ok\\\":\\\"true\\\"}'];}");
+ foo.saveDocument(designDoc);
+
+ // Request a new copy of the design document (NOTE: not calling refresh() as it doesn't overwrite
+ // unsaved data
+ Document designDocNew = foo.getDocument("_design/junit");
+ assertNotNull(designDocNew);
+
+ // Ensure the three update handlers exist
+ JSONObject handlers = designDocNew.getJSONObject("updates");
+ assertNotNull(handlers);
+ assertTrue(handlers.has("put"));
+ assertTrue(handlers.has("post"));
+ assertTrue(handlers.has("test"));
+ }
+
+ @After
+ public void deleteAll() {
+ sess.deleteDatabase("update_test");
+ }
+}
Something went wrong with that request. Please try again.