Author: rhauch
Date: 2009-09-09 20:02:26 -0400 (Wed, 09 Sep 2009)
New Revision: 1198
Modified:
trunk/dna-common/src/main/java/org/jboss/dna/common/util/Base64.java
trunk/dna-common/src/test/java/org/jboss/dna/common/util/Base64Test.java
trunk/extensions/dna-web-jcr-rest-war/src/test/java/org/jboss/dna/web/jcr/rest/JcrResourcesTest.java
trunk/extensions/dna-web-jcr-rest/src/main/java/org/jboss/dna/web/jcr/rest/JcrResources.java
Log:
DNA-513 REST server does not correctly encode binary values
The new approach is used, where the JSON representation of a property that has binary
values has the property name annotated/suffixed with '/base64/'. Also, the PUT/GET
property request and response representations were modified to include the name and the
value(s). Previously, the PUT request and GET response only included the value (or
values), but this didn't allow the sender to say whether the property values were
Base64 encoded. I considered using 'Content-Transfer-Encoding' header (with value
of 'base64'), but that pretty much means the entire body is encoded. Works great
for PUT/GET with a single property value, but breaks down with an array (since the
'[', ',' and ']' characters aren't encoded.
A few test methods were added, and the changes appear to work. However, before committing,
I'd like some verification that this patch goes in the right direction.
Modified: trunk/dna-common/src/main/java/org/jboss/dna/common/util/Base64.java
===================================================================
--- trunk/dna-common/src/main/java/org/jboss/dna/common/util/Base64.java 2009-09-09
21:27:02 UTC (rev 1197)
+++ trunk/dna-common/src/main/java/org/jboss/dna/common/util/Base64.java 2009-09-10
00:02:26 UTC (rev 1198)
@@ -671,6 +671,75 @@
}
+ /**
+ * Encodes content of the supplied InputStream into Base64 notation. Does not
GZip-compress data.
+ *
+ * @param source The data to convert
+ * @return the encoded bytes
+ */
+ public static String encode( java.io.InputStream source ) {
+ return encode(source, NO_OPTIONS);
+ }
+
+ /**
+ * Encodes the content of the supplied InputStream into Base64 notation.
+ * <p>
+ * Valid options:
+ *
+ * <pre>
+ * GZIP: gzip-compresses object before encoding it.
+ * DONT_BREAK_LINES: don't break lines at 76 characters
+ * <i>Note: Technically, this makes your encoding
non-compliant.</i>
+ * </pre>
+ * <p>
+ * Example: <code>encodeBytes( myData, Base64.GZIP )</code> or
+ * <p>
+ * Example: <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES
)</code>
+ *
+ * @param source The data to convert
+ * @param options Specified options- the alphabet type is pulled from this (standard,
url-safe, ordered)
+ * @return the encoded bytes
+ * @see Base64#GZIP
+ * @see Base64#DONT_BREAK_LINES
+ */
+ public static String encode( java.io.InputStream source,
+ int options ) {
+ CheckArg.isNotNull(source, "source");
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ Base64.OutputStream b64os = new Base64.OutputStream(baos, ENCODE | options);
+ BufferedInputStream input = new BufferedInputStream(source);
+ java.io.OutputStream output = b64os;
+
+ boolean error = false;
+ try {
+ if ((options & GZIP) == GZIP) {
+ output = new java.util.zip.GZIPOutputStream(output);
+ }
+ int numRead = 0;
+ byte[] buffer = new byte[1024];
+ while ((numRead = input.read(buffer)) > -1) {
+ output.write(buffer, 0, numRead);
+ }
+ output.close();
+ } catch (IOException e) {
+ error = true;
+ throw new SystemFailureException(e); // error using reading from byte array!
+ } finally {
+ try {
+ input.close();
+ } catch (IOException e) {
+ if (!error) new SystemFailureException(e); // error closing input stream
+ }
+ }
+
+ // Return value according to relevant encoding.
+ try {
+ return new String(baos.toByteArray(), PREFERRED_ENCODING);
+ } catch (java.io.UnsupportedEncodingException uue) {
+ return new String(baos.toByteArray());
+ }
+ }
+
/* ******** D E C O D I N G M E T H O D S ******** */
/**
Modified: trunk/dna-common/src/test/java/org/jboss/dna/common/util/Base64Test.java
===================================================================
--- trunk/dna-common/src/test/java/org/jboss/dna/common/util/Base64Test.java 2009-09-09
21:27:02 UTC (rev 1197)
+++ trunk/dna-common/src/test/java/org/jboss/dna/common/util/Base64Test.java 2009-09-10
00:02:26 UTC (rev 1198)
@@ -26,6 +26,9 @@
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.junit.Assert.assertThat;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
import org.junit.Test;
/**
@@ -68,6 +71,28 @@
System.out.println();
}
+ @Test
+ public void shouldEncodeStringValue() throws UnsupportedEncodingException {
+ String actualValue = "propertyValue";
+ String encoded = Base64.encodeBytes(actualValue.getBytes("UTF-8"));
+ byte[] decoded = Base64.decode(encoded);
+ String decodedValue = new String(decoded, "UTF-8");
+ assertThat(decodedValue, is(actualValue));
+ }
+
+ @Test
+ public void shouldEncodeStreamableValue() {
+ String actualValue = "propertyValue";
+ byte[] actualBytes = actualValue.getBytes();
+ InputStream actualStream = new ByteArrayInputStream(actualBytes);
+ String encoded = Base64.encode(actualStream);
+ String encoded2 = Base64.encodeBytes(actualBytes);
+ assertThat(encoded, is(encoded2));
+ byte[] decoded = Base64.decode(encoded);
+ String decodedValue = new String(decoded);
+ assertThat(decodedValue, is(actualValue));
+ }
+
@Test( expected = NullPointerException.class )
public void testEncodeNullByteArray() {
Base64.encodeBytes(null);
Modified:
trunk/extensions/dna-web-jcr-rest/src/main/java/org/jboss/dna/web/jcr/rest/JcrResources.java
===================================================================
---
trunk/extensions/dna-web-jcr-rest/src/main/java/org/jboss/dna/web/jcr/rest/JcrResources.java 2009-09-09
21:27:02 UTC (rev 1197)
+++
trunk/extensions/dna-web-jcr-rest/src/main/java/org/jboss/dna/web/jcr/rest/JcrResources.java 2009-09-10
00:02:26 UTC (rev 1198)
@@ -23,9 +23,10 @@
*/
package org.jboss.dna.web.jcr.rest;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -38,9 +39,11 @@
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
+import javax.jcr.ValueFactory;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.PropertyDefinition;
import javax.servlet.http.HttpServletRequest;
@@ -64,6 +67,7 @@
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.jboss.dna.common.text.UrlEncoder;
+import org.jboss.dna.common.util.Base64;
import org.jboss.dna.web.jcr.rest.model.RepositoryEntry;
import org.jboss.dna.web.jcr.rest.model.WorkspaceEntry;
import org.jboss.resteasy.spi.NotFoundException;
@@ -99,6 +103,55 @@
* <td>ALL</td>
* </tr>
* </table>
+ * <h3>Binary data</h3>
+ * <p>
+ * There are several ways to transfer binary property values, but all involve encoding
the binary value into ASCII characters
+ * using a {@link Base64} notation and denoting this by adding annotating the property
name with a suffix defining the type of
+ * encoding. Currently, only "base64" encoding is supported.
+ * </p>
+ * <p>
+ * For example, if the "jcr:data" property contains a single binary value of
"propertyValue", then the JSON object representing
+ * that property will be:
+ *
+ * <pre>
+ * "jcr:data/base64/" : "cHJvcGVydHlWYWx1ZQ=="
+ * </pre>
+ *
+ * Likewise, if the "jcr:data" property contains two binary values each being
"propertyValue", then the JSON object representing
+ * that property will be:
+ *
+ * <pre>
+ * "jcr:data/base64/" : [ "cHJvcGVydHlWYWx1ZQ==",
"cHJvcGVydHlWYWx1ZQ==" ]
+ * </pre>
+ *
+ * Note that JCR 1.0.1 does not allow property names to and with a '/' character
(among others), while JCR 2.0 does not allow
+ * property names to contain an unescaped or unencoded '/' character. Therefore,
the "/{encoding}/" suffix can never appear in a
+ * valid JCR property name, and will always identify an encoded property.
+ * </p>
+ * <p>
+ * Here are the details:
+ * <ul>
+ * <li>Getting a node with <code>GET
/resources/{repositoryName}/item/{pathToNode}</code> obtains the JSON object
representing the
+ * node, and each property is represented as a nested JSON object where the name is the
property name and the value(s) are
+ * represented as either a single string value or an array of string values. If the
property has a binary value, then the property
+ * name is appended with "/base64/" and the string representation of each value
is encoded in Base64.</li>
+ * <li>Getting a property with <code>GET
/resources/{repositoryName}/item/{pathToProperty}</code> allows only the value(s)
for the
+ * one property to be included in the response. If any of the values is a binary value,
then <i>all</i> of the values will be
+ * encoded in Base64.</li>
+ * <li>Setting a property with <code>PUT
/resources/{repositoryName}/item/{pathToProperty}</code> allows setting the property
to a
+ * single value, and only that value needs to be included in the body of the request. If
the value is binary, the value
+ * <i>must</i> be {@link Base64 encoded} by the client and the
"Content-Transfer-Encoding" header must be set to "base64" (case
+ * does not matter). When the request is received, the value is decoded before the
property value is updated on the node.</li>
+ * <li>Creating a node with <code>POST
/resources/{repositoryName}/item/{pathToNode}</code> requires a request that is
structured
+ * in the same way as the response from getting a node: the resulting JSON object
represents the node, with nested JSON objects
+ * for the properties and children. If any property of the new node has a binary value,
then the name of the property <i>must</i>
+ * be appended with "/base64/" and the string representation of each value are
to be encoded in Base64.</li>
+ * <li>Updating a node with <code>PUT
/resources/{repositoryName}/item/{pathToNode}</code> requires a request that is
structured
+ * in the same way as the response from getting or posting a node: the resulting JSON
object represents the node, with nested JSON
+ * objects for the properties and children. If any property of the new node has a binary
value, then the name of the property
+ * <i>must</i> be appended with "/base64/" and the string
representation of each value are to be encoded in Base64.</li>
+ * </ul>
+ * </p>
*/
@Immutable
@Path( "/" )
@@ -251,33 +304,87 @@
if (item instanceof Node) {
return jsonFor((Node)item, depth).toString();
}
- return jsonFor((Property)item);
+ return jsonFor((Property)item).toString();
}
/**
- * Returns the JSON-encoded version of the given property. If the property is
single-valued, the returned string is {@code
- * property.getValue().getString()} encoded as a JSON string. If the property is
multi-valued with {@code N} values, this
- * method returns a JSON array containing {@code property.getValues()[N].getString()}
for all values of {@code N}.
+ * Returns the JSON-encoded version of the given property. If the property is
single-valued, the returned string is the value
+ * of the property encoded as a JSON string, including the name. If the property is
multi-valued with {@code N} values, this
+ * method returns a JSON array containing the JSON string for each value.
+ * <p>
+ * Note that if any of the values are binary, then <i>all</i> values will
be first encoded as {@link Base64} string values.
+ * However, if no values are binary, then all values will simply be the {@link
Value#getString() string} representation of the
+ * value.
+ * </p>
*
* @param property the property to be encoded
* @return the JSON-encoded version of the property
+ * @throws JSONException if there is an error encoding the node
* @throws RepositoryException if an error occurs accessing the property, its values,
or its definition.
* @see Property#getDefinition()
* @see PropertyDefinition#isMultiple()
*/
- private String jsonFor( Property property ) throws RepositoryException {
+ private JSONObject jsonFor( Property property ) throws JSONException,
RepositoryException {
+ boolean encoded = false;
+ Object valueObject = null;
if (property.getDefinition().isMultiple()) {
Value[] values = property.getValues();
+ for (Value value : values) {
+ if (value.getType() == PropertyType.BINARY) {
+ encoded = true;
+ break;
+ }
+ }
List<String> list = new ArrayList<String>(values.length);
- for (int i = 0; i < values.length; i++) {
- list.add(values[i].getString());
+ if (encoded) {
+ for (Value value : values) {
+ list.add(jsonEncodedStringFor(value));
+ }
+ } else {
+ for (Value value : values) {
+ list.add(value.getString());
+ }
}
- return new JSONArray(list).toString();
+ valueObject = new JSONArray(list);
+ } else {
+ Value value = property.getValue();
+ encoded = value.getType() == PropertyType.BINARY;
+ valueObject = encoded ? jsonEncodedStringFor(value) : value.getString();
}
- return JSONObject.quote(property.getValue().getString());
+ String propertyName = property.getName();
+ if (encoded) propertyName = propertyName + "/base64/";
+ JSONObject jsonProperty = new JSONObject();
+ jsonProperty.put(propertyName, valueObject);
+ return jsonProperty;
}
/**
+ * Return the JSON-compatible string representation of the given property value. If
the value is a {@link PropertyType#BINARY
+ * binary} value, then this method returns the Base-64 encoding of that value.
Otherwise, it just returns the string
+ * representation of the value.
+ *
+ * @param value the property value; may not be null
+ * @return the string representation of the value
+ * @throws RepositoryException if there is a problem accessing the value
+ */
+ private String jsonEncodedStringFor( Value value ) throws RepositoryException {
+ // Encode the binary value in Base64 ...
+ InputStream stream = value.getStream();
+ try {
+ return Base64.encode(stream);
+ } finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ // Error accessing the value, so throw this ...
+ throw new RepositoryException(e);
+ }
+ }
+ }
+ }
+
+ /**
* Recursively returns the JSON-encoding of a node and its children to depth {@code
toDepth}.
*
* @param node the node to be encoded
@@ -296,16 +403,29 @@
Property prop = iter.nextProperty();
String propName = prop.getName();
+ boolean encoded = false;
+
if (prop.getDefinition().isMultiple()) {
Value[] values = prop.getValues();
+ // Do any of the property values need to be encoded ?
+ for (Value value : values) {
+ if (value.getType() == PropertyType.BINARY) {
+ encoded = true;
+ break;
+ }
+ }
+ if (encoded) propName = propName + "/base64/";
JSONArray array = new JSONArray();
for (int i = 0; i < values.length; i++) {
- array.put(values[i].getString());
+ array.put(encoded ? jsonEncodedStringFor(values[i]) :
values[i].getString());
}
properties.put(propName, array);
} else {
- properties.put(propName, prop.getValue().getString());
+ Value value = prop.getValue();
+ encoded = value.getType() == PropertyType.BINARY;
+ if (encoded) propName = propName + "/base64/";
+ properties.put(propName, encoded ? jsonEncodedStringFor(value) :
value.getString());
}
}
@@ -454,6 +574,22 @@
return newNode;
}
+ private Value decodeValue( String encodedValue,
+ ValueFactory valueFactory ) throws RepositoryException {
+ byte[] binaryValue = Base64.decode(encodedValue);
+ InputStream stream = new ByteArrayInputStream(binaryValue);
+ try {
+ return valueFactory.createValue(stream);
+ } finally {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ // Error accessing the value, so throw this ...
+ throw new RepositoryException(e);
+ }
+ }
+ }
+
/**
* Sets the named property on the given node. This method expects {@code value} to be
either a JSON string or a JSON array of
* JSON strings. If {@code value} is a JSON array, {@code Node#setProperty(String,
String[]) the multi-valued property setter}
@@ -468,20 +604,41 @@
private void setPropertyOnNode( Node node,
String propName,
Object value ) throws RepositoryException,
JSONException {
- String[] values;
+ // Are the property values encoded ?
+ boolean encoded = propName.endsWith("/base64/");
+ if (encoded) {
+ int newLength = propName.length() - "/base64/".length();
+ propName = newLength > 0 ? propName.substring(0, newLength) :
"";
+ }
+
+ Value[] values;
+ ValueFactory valueFactory = node.getSession().getValueFactory();
if (value instanceof JSONArray) {
JSONArray jsonValues = (JSONArray)value;
- values = new String[jsonValues.length()];
+ values = new Value[jsonValues.length()];
for (int i = 0; i < values.length; i++) {
- values[i] = jsonValues.getString(i);
+ String strValue = jsonValues.getString(i);
+ if (encoded) {
+ values[i] = decodeValue(strValue, valueFactory);
+ } else {
+ values[i] = valueFactory.createValue(strValue);
+ }
}
} else {
- values = new String[] {(String)value};
+ String strValue = (String)value;
+ if (encoded) {
+ values = new Value[] {decodeValue(strValue, valueFactory)};
+ } else {
+ values = new Value[] {valueFactory.createValue(strValue)};
+ }
}
if (propName.equals(JcrResources.MIXIN_TYPES_PROPERTY)) {
- Set<String> toBeMixins = new
HashSet<String>(Arrays.asList(values));
+ Set<String> toBeMixins = new HashSet<String>();
+ for (Value theValue : values) {
+ toBeMixins.add(theValue.getString());
+ }
Set<String> asIsMixins = new HashSet<String>();
for (NodeType nodeType : node.getMixinNodeTypes()) {
@@ -564,6 +721,7 @@
* @throws UnauthorizedException if the user does not have the access required to
create the node at this path
* @throws JSONException if there is an error encoding the node
* @throws RepositoryException if any other error occurs
+ * @throws IOException if there is a problem reading the value
*/
@PUT
@Path( "/{repositoryName}/{workspaceName}/items{path:.*}" )
@@ -572,7 +730,7 @@
@PathParam( "repositoryName" ) String
rawRepositoryName,
@PathParam( "workspaceName" ) String
rawWorkspaceName,
@PathParam( "path" ) String path,
- String requestContent ) throws UnauthorizedException,
JSONException, RepositoryException {
+ String requestContent ) throws UnauthorizedException,
JSONException, RepositoryException, IOException {
assert path != null;
assert rawRepositoryName != null;
@@ -603,14 +761,15 @@
} else {
/*
- * The incoming content should be a JSON string or a JSON array. Wrap it into
an object so it can be parsed more easily
+ * The incoming content should be a JSON object containing the property name
and a value that is either a JSON
+ * string or a JSON array.
*/
-
- JSONObject properties = new JSONObject("{ \"value\": " +
requestContent + "}");
Property property = (Property)item;
+ String propertyName = property.getName();
+ JSONObject jsonProperty = new JSONObject(requestContent);
+ String jsonPropertyName = jsonProperty.has(propertyName) ? propertyName :
propertyName + "/base64/";
node = property.getParent();
-
- setPropertyOnNode(node, property.getName(),
properties.get("value"));
+ setPropertyOnNode(node, jsonPropertyName,
jsonProperty.get(jsonPropertyName));
}
node.save();
return jsonFor(node, 0).toString();
Modified:
trunk/extensions/dna-web-jcr-rest-war/src/test/java/org/jboss/dna/web/jcr/rest/JcrResourcesTest.java
===================================================================
---
trunk/extensions/dna-web-jcr-rest-war/src/test/java/org/jboss/dna/web/jcr/rest/JcrResourcesTest.java 2009-09-09
21:27:02 UTC (rev 1197)
+++
trunk/extensions/dna-web-jcr-rest-war/src/test/java/org/jboss/dna/web/jcr/rest/JcrResourcesTest.java 2009-09-10
00:02:26 UTC (rev 1198)
@@ -41,12 +41,11 @@
import org.codehaus.jettison.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
+import com.sun.org.apache.xml.internal.security.utils.Base64;
/**
- * Test of the DNA JCR REST resource. Note that this test case uses a very low-level API
to construct
- * requests and deconstruct the responses. Users are encouraged to use a higher-level
library to communicate
- * with the REST server (e.g., Apache HTTP Commons).
- *
+ * Test of the DNA JCR REST resource. Note that this test case uses a very low-level API
to construct requests and deconstruct the
+ * responses. Users are encouraged to use a higher-level library to communicate with the
REST server (e.g., Apache HTTP Commons).
*/
public class JcrResourcesTest {
@@ -57,17 +56,17 @@
public void beforeEach() {
// Configured in pom
- final String login ="dnauser";
- final String password ="password";
+ final String login = "dnauser";
+ final String password = "password";
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
- return new PasswordAuthentication (login, password.toCharArray());
+ return new PasswordAuthentication(login, password.toCharArray());
}
});
}
-
+
private String getResponseFor( HttpURLConnection connection ) throws IOException {
StringBuffer buff = new StringBuffer();
@@ -84,13 +83,13 @@
@Test
public void shouldNotServeContentToUnauthorizedUser() throws Exception {
- final String login ="dnauser";
- final String password ="invalidpassword";
+ final String login = "dnauser";
+ final String password = "invalidpassword";
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
- return new PasswordAuthentication (login, password.toCharArray());
+ return new PasswordAuthentication(login, password.toCharArray());
}
});
@@ -110,13 +109,13 @@
public void shouldNotServeContentToUserWithoutConnectRole() throws Exception {
// Configured in pom
- final String login ="unauthorizeduser";
- final String password ="password";
+ final String login = "unauthorizeduser";
+ final String password = "password";
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
- return new PasswordAuthentication (login, password.toCharArray());
+ return new PasswordAuthentication(login, password.toCharArray());
}
});
@@ -323,7 +322,7 @@
connection.setRequestProperty("Content-Type",
MediaType.APPLICATION_JSON);
String body = getResponseFor(connection);
- assertThat(body, is("\"dna:system\""));
+ assertThat(body,
is("{\"jcr:primaryType\":\"dna:system\"}"));
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK));
connection.disconnect();
}
@@ -339,7 +338,7 @@
String payload = "{ \"properties\":
{\"jcr:primaryType\": \"nt:unstructured\", \"testProperty\":
\"testValue\", \"multiValuedProperty\": [\"value1\",
\"value2\"]}}";
connection.getOutputStream().write(payload.getBytes());
-
+
JSONObject body = new JSONObject(getResponseFor(connection));
assertThat(body.length(), is(1));
@@ -355,7 +354,7 @@
assertThat(values.length(), is(2));
assertThat(values.getString(0), is("value1"));
assertThat(values.getString(1), is("value2"));
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED));
connection.disconnect();
}
@@ -371,15 +370,15 @@
String payload = "{}";
connection.getOutputStream().write(payload.getBytes());
-
+
JSONObject body = new JSONObject(getResponseFor(connection));
assertThat(body.length(), is(1));
-
+
JSONObject properties = body.getJSONObject("properties");
assertThat(properties, is(notNullValue()));
assertThat(properties.length(), is(1));
assertThat(properties.getString("jcr:primaryType"),
is("nt:unstructured"));
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED));
connection.disconnect();
}
@@ -395,10 +394,10 @@
String payload = "{ \"properties\": {\"jcr:mixinTypes\":
\"mix:referenceable\"}}";
connection.getOutputStream().write(payload.getBytes());
-
+
JSONObject body = new JSONObject(getResponseFor(connection));
assertThat(body.length(), is(1));
-
+
JSONObject properties = body.getJSONObject("properties");
assertThat(properties, is(notNullValue()));
assertThat(properties.length(), is(3));
@@ -409,7 +408,7 @@
assertThat(values, is(notNullValue()));
assertThat(values.length(), is(1));
assertThat(values.getString(0), is("mix:referenceable"));
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED));
connection.disconnect();
@@ -423,7 +422,7 @@
body = new JSONObject(getResponseFor(connection));
assertThat(body.length(), is(1));
-
+
properties = body.getJSONObject("properties");
assertThat(properties, is(notNullValue()));
assertThat(properties.length(), is(3));
@@ -434,10 +433,10 @@
assertThat(values, is(notNullValue()));
assertThat(values.length(), is(1));
assertThat(values.getString(0), is("mix:referenceable"));
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK));
connection.disconnect();
-
+
}
@Test
@@ -448,7 +447,7 @@
connection.setDoOutput(true);
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type",
MediaType.APPLICATION_JSON);
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NOT_FOUND));
connection.disconnect();
@@ -465,7 +464,7 @@
String payload = "{ \"properties\":
{\"jcr:primaryType\": \"invalidType\", \"testProperty\":
\"testValue\", \"multiValuedProperty\": [\"value1\",
\"value2\"]}}";
connection.getOutputStream().write(payload.getBytes());
-
+
assertThat(connection.getResponseCode(),
is(HttpURLConnection.HTTP_BAD_REQUEST));
connection.disconnect();
@@ -491,7 +490,7 @@
connection.setRequestProperty("Content-Type",
MediaType.APPLICATION_JSON);
String payload = "{ \"properties\":
{\"jcr:primaryType\": \"nt:unstructured\", \"testProperty\":
\"testValue\", \"multiValuedProperty\": [\"value1\",
\"value2\"]},"
- + " \"children\": { \"childNode\" : {
\"properties\": {\"nestedProperty\":
\"nestedValue\"}}}}";
+ + " \"children\": { \"childNode\" : {
\"properties\": {\"nestedProperty\":
\"nestedValue\"}}}}";
connection.getOutputStream().write(payload.getBytes());
@@ -508,7 +507,7 @@
JSONObject body = new JSONObject(getResponseFor(connection));
assertThat(body.length(), is(2));
-
+
JSONObject properties = body.getJSONObject("properties");
assertThat(properties, is(notNullValue()));
assertThat(properties.length(), is(3));
@@ -521,22 +520,22 @@
assertThat(values.length(), is(2));
assertThat(values.getString(0), is("value1"));
assertThat(values.getString(1), is("value2"));
-
+
JSONObject children = body.getJSONObject("children");
assertThat(children, is(notNullValue()));
assertThat(children.length(), is(1));
-
+
JSONObject child = children.getJSONObject("childNode");
assertThat(child, is(notNullValue()));
assertThat(child.length(), is(1));
-
+
properties = child.getJSONObject("properties");
assertThat(properties, is(notNullValue()));
assertThat(properties.length(), is(2));
// Parent primary type is nt:unstructured, so this should default to
nt:unstructured primary type
assertThat(properties.getString("jcr:primaryType"),
is("nt:unstructured"));
assertThat(properties.getString("nestedProperty"),
is("nestedValue"));
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK));
connection.disconnect();
@@ -552,9 +551,9 @@
connection.setRequestProperty("Content-Type",
MediaType.APPLICATION_JSON);
String payload = "{ \"properties\":
{\"jcr:primaryType\": \"nt:unstructured\", \"testProperty\":
\"testValue\", \"multiValuedProperty\": [\"value1\",
\"value2\"]},"
- + " \"children\": { \"childNode\" : {
\"properties\": {\"jcr:primaryType\":
\"invalidType\"}}}}";
+ + " \"children\": { \"childNode\" : {
\"properties\": {\"jcr:primaryType\":
\"invalidType\"}}}}";
connection.getOutputStream().write(payload.getBytes());
-
+
assertThat(connection.getResponseCode(),
is(HttpURLConnection.HTTP_BAD_REQUEST));
connection.disconnect();
@@ -565,13 +564,13 @@
connection.setDoOutput(true);
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type",
MediaType.APPLICATION_JSON);
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NOT_FOUND));
connection.disconnect();
-
+
}
- @Test
+ @Test
public void shouldNotDeleteNonExistentItem() throws Exception {
URL postUrl = new URL(SERVER_URL +
"/dna%3arepository/default/items/invalidItemForDelete");
HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection();
@@ -579,12 +578,12 @@
connection.setDoOutput(true);
connection.setRequestMethod("DELETE");
connection.setRequestProperty("Content-Type",
MediaType.APPLICATION_JSON);
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NOT_FOUND));
connection.disconnect();
}
- @Test
+ @Test
public void shouldDeleteExtantNode() throws Exception {
// Create the node
@@ -597,7 +596,7 @@
String payload = "{ \"properties\":
{\"jcr:primaryType\": \"nt:unstructured\", \"testProperty\":
\"testValue\", \"multiValuedProperty\": [\"value1\",
\"value2\"]}}";
connection.getOutputStream().write(payload.getBytes());
-
+
JSONObject body = new JSONObject(getResponseFor(connection));
assertThat(body.length(), is(1));
@@ -613,7 +612,7 @@
assertThat(values.length(), is(2));
assertThat(values.getString(0), is("value1"));
assertThat(values.getString(1), is("value2"));
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED));
connection.disconnect();
@@ -651,7 +650,7 @@
connection.disconnect();
}
- @Test
+ @Test
public void shouldDeleteExtantProperty() throws Exception {
URL postUrl = new URL(SERVER_URL +
"/dna%3arepository/default/items/propertyForDeletion");
HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection();
@@ -662,10 +661,10 @@
String payload = "{ \"properties\":
{\"jcr:primaryType\": \"nt:unstructured\", \"testProperty\":
\"testValue\", \"multiValuedProperty\": [\"value1\",
\"value2\"]}}";
connection.getOutputStream().write(payload.getBytes());
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED));
connection.disconnect();
-
+
// Confirm that it exists
postUrl = new URL(SERVER_URL +
"/dna%3arepository/default/items/propertyForDeletion");
connection = (HttpURLConnection)postUrl.openConnection();
@@ -723,7 +722,7 @@
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK));
connection.disconnect();
-
+
}
@Test
@@ -738,7 +737,7 @@
String payload = "{ \"firstProperty\": \"someValue\"
}";
connection.getOutputStream().write(payload.getBytes());
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NOT_FOUND));
connection.disconnect();
}
@@ -754,7 +753,7 @@
String payload = "{ \"properties\":
{\"jcr:primaryType\": \"nt:unstructured\", \"testProperty\":
\"testValue\" }}";
connection.getOutputStream().write(payload.getBytes());
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED));
connection.disconnect();
@@ -765,9 +764,9 @@
connection.setRequestMethod("PUT");
connection.setRequestProperty("Content-Type",
MediaType.APPLICATION_JSON);
- payload = "\"someOtherValue\"";
+ payload = "{\"testProperty\":\"someOtherValue\"}";
connection.getOutputStream().write(payload.getBytes());
-
+
JSONObject body = new JSONObject(getResponseFor(connection));
assertThat(body.length(), is(1));
@@ -776,15 +775,83 @@
assertThat(properties.length(), is(2));
assertThat(properties.getString("jcr:primaryType"),
is("nt:unstructured"));
assertThat(properties.getString("testProperty"),
is("someOtherValue"));
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK));
connection.disconnect();
-
+
}
@Test
- public void shouldNotBeAbleToPutPropertiesToNode() throws Exception {
+ public void shouldBeAbleToPutBinaryValueToProperty() throws Exception {
+ URL postUrl = new URL(SERVER_URL +
"/dna%3arepository/default/items/nodeForPutBinaryProperty");
+ HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection();
+ connection.setDoOutput(true);
+ connection.setRequestMethod("POST");
+ connection.setRequestProperty("Content-Type",
MediaType.APPLICATION_JSON);
+
+ // Base64-encode a value ...
+ String encodedValue =
Base64.encode("propertyValue".getBytes("UTF-8"));
+
+ String payload = "{ \"properties\":
{\"jcr:primaryType\": \"nt:unstructured\",
\"testProperty/base64/\": \""
+ + encodedValue + "\" }}";
+ connection.getOutputStream().write(payload.getBytes());
+
+ assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED));
+ connection.disconnect();
+
+ URL putUrl = new URL(SERVER_URL +
"/dna%3arepository/default/items/nodeForPutBinaryProperty/testProperty");
+ connection = (HttpURLConnection)putUrl.openConnection();
+
+ connection.setDoOutput(true);
+ connection.setRequestMethod("PUT");
+ connection.setRequestProperty("Content-Type",
MediaType.APPLICATION_JSON);
+
+ String otherValue = "someOtherValue";
+ payload = "{\"testProperty/base64/\":\"" +
Base64.encode(otherValue.getBytes("UTF-8")) + "\"}";
+ connection.getOutputStream().write(payload.getBytes());
+
+ JSONObject body = new JSONObject(getResponseFor(connection));
+ assertThat(body.length(), is(1));
+
+ JSONObject properties = body.getJSONObject("properties");
+ assertThat(properties, is(notNullValue()));
+ assertThat(properties.length(), is(2));
+ assertThat(properties.getString("jcr:primaryType"),
is("nt:unstructured"));
+ String responseEncodedValue =
properties.getString("testProperty/base64/");
+ String decodedValue = new String(Base64.decode(responseEncodedValue),
"UTF-8");
+ assertThat(decodedValue, is(otherValue));
+
+ assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK));
+ connection.disconnect();
+
+ // Try putting a non-binary value ...
+ connection = (HttpURLConnection)putUrl.openConnection();
+
+ connection.setDoOutput(true);
+ connection.setRequestMethod("PUT");
+ connection.setRequestProperty("Content-Type",
MediaType.APPLICATION_JSON);
+
+ String anotherValue = "yetAnotherValue";
+ payload = "{\"testProperty\":\"" + anotherValue +
"\"}";
+ connection.getOutputStream().write(payload.getBytes());
+
+ body = new JSONObject(getResponseFor(connection));
+ assertThat(body.length(), is(1));
+
+ properties = body.getJSONObject("properties");
+ assertThat(properties, is(notNullValue()));
+ assertThat(properties.length(), is(2));
+ assertThat(properties.getString("jcr:primaryType"),
is("nt:unstructured"));
+ assertThat(properties.getString("testProperty"), is(anotherValue));
+
+ assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK));
+ connection.disconnect();
+ }
+
+ @Test
+ public void shouldBeAbleToPutPropertiesToNode() throws Exception {
+
URL postUrl = new URL(SERVER_URL +
"/dna%3arepository/default/items/nodeForPutProperties");
HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection();
@@ -794,7 +861,7 @@
String payload = "{ \"properties\":
{\"jcr:primaryType\": \"nt:unstructured\" }}";
connection.getOutputStream().write(payload.getBytes());
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED));
connection.disconnect();
@@ -807,7 +874,7 @@
payload = "{\"testProperty\": \"testValue\",
\"multiValuedProperty\": [\"value1\", \"value2\"]}";
connection.getOutputStream().write(payload.getBytes());
-
+
JSONObject body = new JSONObject(getResponseFor(connection));
assertThat(body.length(), is(1));
@@ -823,10 +890,10 @@
assertThat(values.length(), is(2));
assertThat(values.getString(0), is("value1"));
assertThat(values.getString(1), is("value2"));
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK));
connection.disconnect();
-
+
}
@Test
@@ -841,7 +908,7 @@
String payload = "{ \"properties\":
{\"jcr:primaryType\": \"nt:unstructured\" }}";
connection.getOutputStream().write(payload.getBytes());
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED));
connection.disconnect();
@@ -853,7 +920,7 @@
payload = "{\"jcr:mixinTypes\":
\"mix:referenceable\"}";
connection.getOutputStream().write(payload.getBytes());
-
+
JSONObject body = new JSONObject(getResponseFor(connection));
assertThat(body.length(), is(1));
@@ -867,7 +934,7 @@
assertThat(mixinTypes.length(), is(1));
assertThat(mixinTypes.getString(0), is("mix:referenceable"));
assertThat(properties.getString("jcr:uuid"), is(notNullValue()));
-
+
assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK));
connection.disconnect();
@@ -879,7 +946,7 @@
payload = "{\"jcr:mixinTypes\": []}";
connection.getOutputStream().write(payload.getBytes());
-
+
body = new JSONObject(getResponseFor(connection));
assertThat(body.length(), is(1));