/*
* (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.core.opencmis.impl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static org.nuxeo.ecm.core.opencmis.tests.Helper.FILE1_CONTENT;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
import java.io.Writer;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import javax.inject.Inject;
import javax.servlet.http.HttpServletResponse;
import org.apache.chemistry.opencmis.client.api.CmisObject;
import org.apache.chemistry.opencmis.client.api.Session;
import org.apache.chemistry.opencmis.commons.data.RepositoryInfo;
import org.apache.chemistry.opencmis.commons.impl.Base64;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamHashImpl;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.ProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.FormBodyPart;
import org.apache.http.entity.mime.FormBodyPartBuilder;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.Blobs;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentRef;
import org.nuxeo.ecm.core.api.IdRef;
import org.nuxeo.ecm.core.api.VersioningOption;
import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
import org.nuxeo.ecm.core.blob.BlobManager;
import org.nuxeo.ecm.core.blob.BlobManagerComponent;
import org.nuxeo.ecm.core.blob.BlobProviderDescriptor;
import org.nuxeo.ecm.core.opencmis.impl.server.NuxeoPropertyData;
import org.nuxeo.ecm.core.opencmis.tests.Helper;
import org.nuxeo.ecm.core.test.CoreFeature;
import org.nuxeo.ecm.core.test.TransactionalFeature;
import org.nuxeo.ecm.core.test.annotations.Granularity;
import org.nuxeo.ecm.core.test.annotations.RepositoryConfig;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.test.runner.Features;
import org.nuxeo.runtime.test.runner.FeaturesRunner;
import org.nuxeo.runtime.test.runner.RuntimeHarness;
/**
* Suite of CMIS tests with minimal setup, checking HTTP headers.
*/
@RunWith(FeaturesRunner.class)
@Features(CmisFeature.class)
@RepositoryConfig(cleanup = Granularity.METHOD)
public class CmisSuiteSession2 {
protected static final String USERNAME = "Administrator";
protected static final String PASSWORD = "test";
protected static final String BASIC_AUTH = "Basic " + Base64.encodeBytes((USERNAME + ":" + PASSWORD).getBytes());
@Inject
protected RuntimeHarness harness;
@Inject
protected CoreFeature coreFeature;
@Inject
protected TransactionalFeature txFeature;
@Inject
protected CmisFeatureSession cmisFeatureSession;
@Inject
protected CoreSession coreSession;
@Inject
protected BlobManager blobManager;
@Inject
protected Session session;
protected boolean isAtomPub;
protected boolean isBrowser;
protected static class NeverRedirectStrategy extends DefaultRedirectStrategy {
public static final NeverRedirectStrategy INSTANCE = new NeverRedirectStrategy();
@Override
public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context)
throws ProtocolException {
return false;
}
}
@Before
public void setUp() throws Exception {
isAtomPub = cmisFeatureSession.isAtomPub;
isBrowser = cmisFeatureSession.isBrowser;
}
/**
* Use a BlobProvider that can redirect to a different URI for download.
*/
protected void useDummyCmisBlobProvider() {
BlobManagerComponent blobManagerComponent = (BlobManagerComponent) blobManager;
BlobProviderDescriptor descr = new BlobProviderDescriptor();
descr.name = coreSession.getRepositoryName();
descr.klass = DummyCmisBlobProvider.class;
blobManagerComponent.registerBlobProvider(descr);
}
protected void setUpData() throws Exception {
Helper.makeNuxeoRepository(coreSession);
DocumentModel file7 = new DocumentModelImpl("/testfolder1", "testfile7", "File");
file7.setPropertyValue("dc:title", "title7");
String content = FILE1_CONTENT;
String filename = "testfile.txt";
Blob blob7 = Blobs.createBlob(content);
blob7.setDigest(DigestUtils.md5Hex(content));
blob7.setFilename(filename);
file7.setPropertyValue("content", (Serializable) blob7);
file7 = Helper.createDocument(coreSession, file7);
Helper.sleepForAuditGranularity();
file7.putContextData("disableDublinCoreListener", Boolean.TRUE);
DocumentRef file7verref = file7.checkIn(VersioningOption.MINOR, null);
txFeature.nextTransaction();
coreFeature.getStorageConfiguration().sleepForFulltext();
}
protected String getURI(String path) {
CmisObject file = session.getObjectByPath(path);
RepositoryInfo ri = session.getRepositoryInfo();
String uri = ri.getThinClientUri() + ri.getId() + "/";
uri += isAtomPub ? "content?id=" : "root?objectId=";
uri += file.getId();
return uri;
}
protected HttpEntity getCreateDocumentHttpEntity(File file) {
FormBodyPart cmisactionPart = FormBodyPartBuilder.create("cmisaction",
new StringBody("createDocument", ContentType.TEXT_PLAIN)).build();
FormBodyPart contentPart = FormBodyPartBuilder.create("content",
new FileBody(file, ContentType.TEXT_PLAIN, "testfile.txt")).build();
HttpEntity entity = MultipartEntityBuilder.create()
.addPart(cmisactionPart)
.addTextBody("propertyId[0]", "cmis:name")
.addTextBody("propertyValue[0]", "testfile01")
.addTextBody("propertyId[1]", "cmis:objectTypeId")
.addTextBody("propertyValue[1]", "File")
.addPart(contentPart).build();
return entity;
}
protected HttpEntity getCheckInHttpEntity(File file) {
FormBodyPart cmisactionPart = FormBodyPartBuilder.create("cmisaction",
new StringBody("checkIn", ContentType.TEXT_PLAIN)).build();
FormBodyPart contentPart = FormBodyPartBuilder.create("content",
new FileBody(file, ContentType.TEXT_PLAIN, "testfile.txt")).build();
HttpEntity entity = MultipartEntityBuilder.create().addPart(cmisactionPart).addPart(contentPart).build();
return entity;
}
protected HttpEntity getSetContentStreamHttpEntity(File file, String changeToken) {
FormBodyPart cmisactionPart = FormBodyPartBuilder.create("cmisaction",
new StringBody("setContent", ContentType.TEXT_PLAIN)).build();
FormBodyPart contentPart = FormBodyPartBuilder.create("content",
new FileBody(file, ContentType.TEXT_PLAIN, "testfile.txt")).build();
HttpEntity entity = MultipartEntityBuilder.create()
.addPart(cmisactionPart)
.addTextBody("changeToken", changeToken)
.addPart(contentPart).build();
return entity;
}
@Test
public void testCreateDocumentWithContentStreamAndDigestHeader() throws Exception {
setUpData();
session.clear(); // clear cache
assumeTrue(isBrowser);
String content = FILE1_CONTENT;
String contentMD5Hex = DigestUtils.md5Hex(content);
String contentMD5Base64 = NuxeoPropertyData.transcodeHexToBase64(contentMD5Hex);
File[] files = createFiles(content);
ObjectMapper mapper = new ObjectMapper();
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
String uri = getURI("/testfolder1") + "&succinct=true";
HttpPost request = new HttpPost(uri);
request.setHeader("Authorization", BASIC_AUTH);
for (int i = 0; i < 2; i++) {
boolean okRequest = i == 0;
request.setHeader("Digest", "md5=" + (String) (okRequest ? contentMD5Base64 : "bogusMD5Sum"));
HttpEntity reqEntity = getCreateDocumentHttpEntity(files[i]);
request.setEntity(reqEntity);
try (CloseableHttpResponse response = httpClient.execute(request)) {
if (okRequest) {
JsonNode root = checkOkContentStreamResponse(contentMD5Hex, mapper, response);
String objectId = root.path("succinctProperties").path("cmis:objectId").getTextValue();
assertNotNull(objectId);
coreSession.removeDocument(new IdRef(objectId));
coreSession.save();
} else {
checkBadContentStreamResponse(mapper, response);
}
}
}
}
deleteFiles(files);
}
@Test
public void testCheckInWithDigestHeader() throws Exception {
setUpData();
session.clear(); // clear cache
assumeTrue(isBrowser);
String content = FILE1_CONTENT + " Updated";
String contentMD5Hex = DigestUtils.md5Hex(content);
String contentMD5Base64 = NuxeoPropertyData.transcodeHexToBase64(contentMD5Hex);
File[] files = createFiles(content);
ObjectMapper mapper = new ObjectMapper();
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
String uri = getURI("/testfolder1/testfile7") + "&succinct=true&filter=cmis:contentStreamHash";
HttpPost request = new HttpPost(uri);
request.setHeader("Authorization", BASIC_AUTH);
for (int i = 0; i < 2; i++) {
boolean okRequest = i == 0;
List<NameValuePair> paramList = Arrays.asList(new BasicNameValuePair("cmisaction", "checkOut"));
HttpEntity reqEntity = new UrlEncodedFormEntity(paramList);
request.setEntity(reqEntity);
try (CloseableHttpResponse response = httpClient.execute(request)) {
assertEquals(HttpServletResponse.SC_CREATED, response.getStatusLine().getStatusCode());
InputStream is = response.getEntity().getContent();
JsonNode root = mapper.readTree(is);
String objectId = root.path("succinctProperties").path("cmis:objectId").getTextValue();
assertNotNull(objectId);
}
request.setHeader("Digest", "md5=" + (String) (okRequest ? contentMD5Base64 : "bogusMD5Sum"));
reqEntity = getCheckInHttpEntity(files[i]);
request.setEntity(reqEntity);
try (CloseableHttpResponse response = httpClient.execute(request)) {
if (okRequest) {
checkOkContentStreamResponse(contentMD5Hex, mapper, response);
} else {
checkBadContentStreamResponse(mapper, response);
}
}
}
}
deleteFiles(files);
}
@Test
public void testSetContentStreamWithDigestHeader() throws Exception {
setUpData();
session.clear(); // clear cache
assumeTrue(isBrowser);
String content = FILE1_CONTENT + " Updated";
String contentMD5Hex = DigestUtils.md5Hex(content);
String contentMD5Base64 = NuxeoPropertyData.transcodeHexToBase64(contentMD5Hex);
File[] files = createFiles(content);
ObjectMapper mapper = new ObjectMapper();
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
String uri = getURI("/testfolder1/testfile1") + "&succinct=true&filter=cmis:contentStreamHash";
HttpPost request = new HttpPost(uri);
request.setHeader("Authorization", BASIC_AUTH);
for (int i = 0; i < 2; i++) {
boolean okRequest = i == 0;
request.setHeader("Digest", "md5=" + (String) (okRequest ? contentMD5Base64 : "bogusMD5Sum"));
session.clear();
String changeToken = session.getObjectByPath("/testfolder1/testfile1").getChangeToken();
HttpEntity reqEntity = getSetContentStreamHttpEntity(files[i], changeToken);
request.setEntity(reqEntity);
try (CloseableHttpResponse response = httpClient.execute(request)) {
if (okRequest) {
checkOkContentStreamResponse(contentMD5Hex, mapper, response);
} else {
checkBadContentStreamResponse(mapper, response);
}
}
}
}
deleteFiles(files);
}
protected JsonNode checkOkContentStreamResponse(String contentMD5Hex, ObjectMapper mapper,
CloseableHttpResponse response) throws IOException {
String content;
try (InputStream is = response.getEntity().getContent()) {
content = IOUtils.toString(is);
}
assertEquals(content, HttpServletResponse.SC_CREATED, response.getStatusLine().getStatusCode());
JsonNode root = mapper.readTree(content);
String expectedContentStreamHash = new ContentStreamHashImpl(
ContentStreamHashImpl.ALGORITHM_MD5, contentMD5Hex).toString();
Iterator iter = root.path("succinctProperties").path("cmis:contentStreamHash").getElements();
boolean found = false;
while (iter.hasNext()) {
String hash = ((JsonNode) iter.next()).getTextValue();
if (expectedContentStreamHash.equals(hash)) {
found = true;
break;
}
}
assertTrue("cmis:contentStreamHash does not contain " + expectedContentStreamHash, found);
return root;
}
protected JsonNode checkBadContentStreamResponse(ObjectMapper mapper, CloseableHttpResponse response)
throws IOException {
String content;
try (InputStream is = response.getEntity().getContent()) {
content = IOUtils.toString(is);
}
assertEquals(content, HttpServletResponse.SC_BAD_REQUEST, response.getStatusLine().getStatusCode());
JsonNode root = mapper.readTree(content);
String exception = root.path("exception").getTextValue();
assertEquals("invalidArgument", exception);
return root;
}
protected File[] createFiles(String content) throws IOException {
File[] files = new File[2];
for (int i = 0; i < 2; i++) {
File file = files[i] = Framework.createTempFile("NuxeoCMIS-", null);
try (Writer writer = new FileWriter(file); Reader reader = new StringReader(content)) {
IOUtils.copy(reader, writer);
}
}
return files;
}
protected void deleteFiles(File[] files) throws IOException {
for (File file : files) {
file.delete();
}
}
@Test
public void testContentStreamRedirect() throws Exception {
useDummyCmisBlobProvider();
setUpData();
session.clear(); // clear cache
assumeTrue(isAtomPub || isBrowser);
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
httpClientBuilder.setRedirectStrategy(NeverRedirectStrategy.INSTANCE); // to check Location header manually
try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
String uri = getURI("/testfolder1/testfile1") + "&testredirect=true"; // to provoke a redirect in our dummy blob provider
HttpGet request = new HttpGet(uri);
request.setHeader("Authorization", BASIC_AUTH);
try (CloseableHttpResponse response = httpClient.execute(request)) {
assertEquals(HttpServletResponse.SC_MOVED_TEMPORARILY, response.getStatusLine().getStatusCode());
Header locationHeader = response.getFirstHeader("Location");
assertNotNull(locationHeader);
assertEquals("http://example.com/dummyedirect", locationHeader.getValue());
}
}
}
@Test
public void testContentStreamUsingGetMethod() throws Exception {
setUpData();
session.clear(); // clear cache
doTestContentStream(new HttpGet(getURI("/testfolder1/testfile1")));
}
@Test
public void testContentStreamUsingHeadMethod() throws Exception {
setUpData();
session.clear(); // clear cache
doTestContentStream(new HttpHead(getURI("/testfolder1/testfile1")));
}
private void doTestContentStream(HttpUriRequest request) throws Exception {
assumeTrue(isAtomPub || isBrowser);
String contentMD5Hex = DigestUtils.md5Hex(FILE1_CONTENT);
String contentMD5Base64 = NuxeoPropertyData.transcodeHexToBase64(contentMD5Hex);
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
request.setHeader("Authorization", BASIC_AUTH);
boolean isHeadRequest = request instanceof HttpHead;
request.setHeader("Want-Digest", isHeadRequest ? "contentMD5" : "md5");
harness.deployContrib("org.nuxeo.ecm.core.opencmis.tests.tests", "OSGI-INF/download-listener-contrib.xml");
DownloadListener.clearMessages();
try (CloseableHttpResponse response = httpClient.execute(request)) {
assertEquals(HttpServletResponse.SC_OK, response.getStatusLine().getStatusCode());
Header lengthHeader = response.getFirstHeader("Content-Length");
assertNotNull(lengthHeader);
byte[] expectedBytes = FILE1_CONTENT.getBytes("UTF-8");
int expectedLength = expectedBytes.length;
assertEquals(String.valueOf(expectedLength), lengthHeader.getValue());
List<String> downloadMessages = DownloadListener.getMessages();
if (isHeadRequest) {
Header contentMD5Header = response.getFirstHeader("Content-MD5");
assertEquals(contentMD5Base64, contentMD5Header.getValue());
assertNull(response.getEntity());
assertEquals(0, downloadMessages.size());
} else {
Header digestHeader = response.getFirstHeader("Digest");
assertEquals("MD5=" + contentMD5Base64, digestHeader.getValue());
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (InputStream in = response.getEntity().getContent()) {
IOUtils.copy(in, out);
}
assertEquals(expectedLength, out.size());
assertTrue(Arrays.equals(expectedBytes, out.toByteArray()));
assertEquals(Arrays.asList("download:comment=testfile.txt,downloadReason=cmis"), downloadMessages);
}
}
} finally {
harness.undeployContrib("org.nuxeo.ecm.core.opencmis.tests.tests", "OSGI-INF/download-listener-contrib.xml");
}
}
}