diff --git a/dna-common/src/main/java/org/jboss/dna/common/util/Base64.java b/dna-common/src/main/java/org/jboss/dna/common/util/Base64.java index f804900..fd58beb 100644 --- a/dna-common/src/main/java/org/jboss/dna/common/util/Base64.java +++ b/dna-common/src/main/java/org/jboss/dna/common/util/Base64.java @@ -671,6 +671,75 @@ public class Base64 { } + /** + * 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. + *

+ * Valid options: + * + *

+     *   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>
+     * 
+ *

+ * Example: encodeBytes( myData, Base64.GZIP ) or + *

+ * Example: encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES ) + * + * @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 ******** */ /** diff --git a/dna-common/src/test/java/org/jboss/dna/common/util/Base64Test.java b/dna-common/src/test/java/org/jboss/dna/common/util/Base64Test.java index 097c680..d345b05 100644 --- a/dna-common/src/test/java/org/jboss/dna/common/util/Base64Test.java +++ b/dna-common/src/test/java/org/jboss/dna/common/util/Base64Test.java @@ -26,6 +26,9 @@ package org.jboss.dna.common.util; 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 @@ public class Base64Test { 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); diff --git a/extensions/dna-web-jcr-rest-war/src/test/java/org/jboss/dna/web/jcr/rest/JcrResourcesTest.java b/extensions/dna-web-jcr-rest-war/src/test/java/org/jboss/dna/web/jcr/rest/JcrResourcesTest.java index 89961fc..d4bf4d1 100644 --- a/extensions/dna-web-jcr-rest-war/src/test/java/org/jboss/dna/web/jcr/rest/JcrResourcesTest.java +++ b/extensions/dna-web-jcr-rest-war/src/test/java/org/jboss/dna/web/jcr/rest/JcrResourcesTest.java @@ -41,12 +41,11 @@ import org.codehaus.jettison.json.JSONArray; 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 class JcrResourcesTest { 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 @@ public class JcrResourcesTest { @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 class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK)); connection.disconnect(); - + } @Test @@ -738,7 +737,7 @@ public class JcrResourcesTest { String payload = "{ \"firstProperty\": \"someValue\" }"; connection.getOutputStream().write(payload.getBytes()); - + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NOT_FOUND)); connection.disconnect(); } @@ -754,7 +753,7 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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,14 +775,82 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { 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 @@ public class JcrResourcesTest { payload = "{\"jcr:mixinTypes\": []}"; connection.getOutputStream().write(payload.getBytes()); - + body = new JSONObject(getResponseFor(connection)); assertThat(body.length(), is(1)); diff --git a/extensions/dna-web-jcr-rest/src/main/java/org/jboss/dna/web/jcr/rest/JcrResources.java b/extensions/dna-web-jcr-rest/src/main/java/org/jboss/dna/web/jcr/rest/JcrResources.java index e446589..f9c08a9 100644 --- a/extensions/dna-web-jcr-rest/src/main/java/org/jboss/dna/web/jcr/rest/JcrResources.java +++ b/extensions/dna-web-jcr-rest/src/main/java/org/jboss/dna/web/jcr/rest/JcrResources.java @@ -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.NodeIterator; 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.JSONArray; 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 @@ import org.jboss.resteasy.spi.UnauthorizedException; * ALL * * + *

Binary data

+ *

+ * 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. + *

+ *

+ * For example, if the "jcr:data" property contains a single binary value of "propertyValue", then the JSON object representing + * that property will be: + * + *

+ *   "jcr:data/base64/" : "cHJvcGVydHlWYWx1ZQ=="
+ * 
+ * + * Likewise, if the "jcr:data" property contains two binary values each being "propertyValue", then the JSON object representing + * that property will be: + * + *
+ *   "jcr:data/base64/" : [ "cHJvcGVydHlWYWx1ZQ==", "cHJvcGVydHlWYWx1ZQ==" ]
+ * 
+ * + * 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. + *

+ *

+ * Here are the details: + *

+ *

*/ @Immutable @Path( "/" ) @@ -251,30 +304,84 @@ public class JcrResources { 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. + *

+ * Note that if any of the values are binary, then all 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. + *

* * @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 list = new ArrayList(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()); + } + } + valueObject = new JSONArray(list); + } else { + Value value = property.getValue(); + encoded = value.getType() == PropertyType.BINARY; + valueObject = encoded ? jsonEncodedStringFor(value) : value.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); + } } - return new JSONArray(list).toString(); } - return JSONObject.quote(property.getValue().getString()); } /** @@ -296,16 +403,29 @@ public class JcrResources { 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 @@ public class JcrResources { 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 @@ public class JcrResources { 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 toBeMixins = new HashSet(Arrays.asList(values)); + Set toBeMixins = new HashSet(); + for (Value theValue : values) { + toBeMixins.add(theValue.getString()); + } Set asIsMixins = new HashSet(); for (NodeType nodeType : node.getMixinNodeTypes()) { @@ -564,6 +721,7 @@ public class JcrResources { * @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 @@ public class JcrResources { @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 @@ public class JcrResources { } 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();