/*
* (C) Copyright 2006-2015 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, jcarsique
*/
package org.nuxeo.ecm.core;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import javax.inject.Inject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.nuxeo.common.Environment;
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.impl.DocumentModelImpl;
import org.nuxeo.ecm.core.blob.binary.Binary;
import org.nuxeo.ecm.core.blob.binary.BinaryBlob;
import org.nuxeo.ecm.core.blob.binary.BinaryManager;
import org.nuxeo.ecm.core.blob.binary.DefaultBinaryManager;
import org.nuxeo.ecm.core.test.CoreFeature;
import org.nuxeo.ecm.core.test.annotations.Granularity;
import org.nuxeo.ecm.core.test.annotations.RepositoryConfig;
import org.nuxeo.runtime.test.runner.Deploy;
import org.nuxeo.runtime.test.runner.Features;
import org.nuxeo.runtime.test.runner.FeaturesRunner;
import org.nuxeo.runtime.test.runner.LocalDeploy;
/**
* Sample test showing how to use a direct access to the binaries storage.
*/
@RunWith(FeaturesRunner.class)
@Features(CoreFeature.class)
@RepositoryConfig(cleanup = Granularity.METHOD)
@LocalDeploy("org.nuxeo.ecm.core.test.tests:OSGI-INF/test-repo-core-types-contrib.xml")
@Deploy("org.nuxeo.ecm.core.api.tests:OSGI-INF/test-default-blob-provider.xml")
public class TestSQLRepositoryDirectBlob {
@Inject
protected CoreSession session;
// ----- Third-party application -----
/** The application that creates a file. */
public String createFile() throws Exception {
FileManager fileMaker = new FileManager();
// get the tmp dir where to create files
File tmpDir = fileMaker.getTmpDir();
// third-party application creates a file there
File file = File.createTempFile("myapp", null, tmpDir);
FileOutputStream out = new FileOutputStream(file);
out.write("this is a file".getBytes("UTF-8"));
out.close();
// then it moves the tmp file to the binaries storage, and gets the
// digest
String digest = fileMaker.moveTmpFileToBinaries(file);
return digest;
}
// ----- Nuxeo application -----
@Test
public void testDirectBlob() throws Exception {
DocumentModel folder = session.getRootDocument();
DocumentModel file = new DocumentModelImpl(folder.getPathAsString(), "filea", "File");
file = session.createDocument(file);
session.save();
/*
* 1. A third-party application returns a digest for a created file.
*/
String digest = createFile();
/*
* 2. Later, create and use the blob for this digest.
*/
BinaryManager binaryManager = new DefaultBinaryManager();
binaryManager.initialize("repo", Collections.emptyMap());
Binary binary = binaryManager.getBinary(digest);
assertNotNull("Missing file for digest: " + digest, binary);
String filename = "doc.txt";
long length = binary.getFile().length();
Blob blob = new BinaryBlob(binary, digest, filename, "text/plain", "utf-8", digest, length);
blob.setFilename(filename);
assertEquals("MD5", blob.getDigestAlgorithm());
assertEquals(digest, blob.getDigest());
file.setProperty("file", "content", blob);
session.saveDocument(file);
session.save();
/*
* 3. Check the retrieved doc.
*/
String expected = "this is a file";
file = session.getDocument(file.getRef());
blob = (Blob) file.getProperty("file", "content");
assertEquals("doc.txt", blob.getFilename());
assertEquals(expected.length(), blob.getLength());
assertEquals("utf-8", blob.getEncoding());
assertEquals("text/plain", blob.getMimeType());
assertEquals(expected, blob.getString());
/*
* remove attached file
*/
file.setProperty("file", "content", null);
file = session.saveDocument(file);
session.save();
assertNull(file.getProperty("file", "content"));
binaryManager.close();
}
@Test
public void testBinarySerialization() throws Exception {
DocumentModel folder = session.getRootDocument();
DocumentModel file = new DocumentModelImpl(folder.getPathAsString(), "filea", "File");
file = session.createDocument(file);
session.save();
// create a binary instance pointing to some content stored on the
// filesystem
String digest = createFile();
BinaryManager binaryManager = new DefaultBinaryManager();
binaryManager.initialize(session.getRepositoryName(), Collections.emptyMap());
Binary binary = binaryManager.getBinary(digest);
assertNotNull("Missing file for digest: " + digest, binary);
String expected = "this is a file";
byte[] observedContent = new byte[expected.length()];
assertEquals(digest, binary.getDigest());
assertEquals(expected.length(), binary.getStream().read(observedContent));
assertEquals(expected, new String(observedContent));
// serialize and deserialize the binary instance
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(binary);
out.flush();
out.close();
// Make an input stream from the byte array and read
// a copy of the object back in.
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Binary binaryCopy = (Binary) in.readObject();
observedContent = new byte[expected.length()];
assertEquals(digest, binaryCopy.getDigest());
assertEquals(expected.length(), binaryCopy.getStream().read(observedContent));
assertEquals(expected, new String(observedContent));
binaryManager.close();
}
@Test
public void testBinaryManagerTmpFileMoveNotCopy() throws Exception {
// tmp file
Blob blob = Blobs.createBlob(new ByteArrayInputStream("abcd\b".getBytes("UTF-8")));
File originaFile = blob.getFile();
// set in doc
DocumentModel doc = new DocumentModelImpl("/", "myfile", "File");
doc.setPropertyValue("file:content", (Serializable) blob);
doc = session.createDocument(doc);
session.save();
assertFalse(originaFile.exists());
assertTrue(blob.getFile().exists());
}
}
/**
* Class doing a simplified version of what the binaries storage does.
* <p>
* In a real application, change the constructor to pass the rootDir as a parameter or use configuration.
*
* @author Florent Guillaume
*/
class FileManager {
/*
* These parameters have to be the same as the one from the binaries storage.
*/
public static final String DIGEST_ALGORITHM = "MD5";
public static final int DEPTH = 2;
protected final File tmpDir;
protected final File dataDir;
public FileManager() {
// from inside Nuxeo components, this can be used
// otherwise use a hardcoded string or parameter to that directory
File rootDir = new File(Environment.getDefault().getData(), "binaries");
tmpDir = new File(rootDir, "tmp");
dataDir = new File(rootDir, "data");
tmpDir.mkdirs();
dataDir.mkdirs();
}
public File getTmpDir() {
return tmpDir;
}
public String moveTmpFileToBinaries(File file) throws IOException {
// digest the file
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw (IOException) new IOException().initCause(e);
}
FileInputStream in = new FileInputStream(file);
try {
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) != -1) {
messageDigest.update(buf, 0, n);
}
} finally {
in.close();
}
String digest = toHexString(messageDigest.digest());
// move the file to its final location
File dest = getFileForDigest(digest, dataDir);
file.renameTo(dest); // atomic move, fails if already there
file.delete(); // fails if the move was successful
if (!dest.exists()) {
throw new IOException("Could not create file: " + dest);
}
return digest;
}
protected static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
protected static String toHexString(byte[] data) {
StringBuilder buf = new StringBuilder(2 * data.length);
for (byte b : data) {
buf.append(HEX_DIGITS[(0xF0 & b) >> 4]);
buf.append(HEX_DIGITS[0x0F & b]);
}
return buf.toString();
}
protected static File getFileForDigest(String digest, File dataDir) {
StringBuilder buf = new StringBuilder(3 * DEPTH - 1);
for (int i = 0; i < DEPTH; i++) {
if (i != 0) {
buf.append(File.separatorChar);
}
buf.append(digest.substring(2 * i, 2 * i + 2));
}
File dir = new File(dataDir, buf.toString());
dir.mkdirs();
return new File(dir, digest);
}
}