/*
* ModeShape (http://www.modeshape.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.modeshape.jcr.value.binary;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.jcr.Binary;
import javax.jcr.RepositoryException;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.modeshape.common.FixFor;
import org.modeshape.common.junit.SkipTestRule;
import org.modeshape.common.util.IoUtil;
import org.modeshape.jcr.TextExtractors;
import org.modeshape.jcr.api.text.TextExtractor;
import org.modeshape.jcr.mimetype.DefaultMimeTypeDetector;
import org.modeshape.jcr.mimetype.MimeTypeDetector;
import org.modeshape.jcr.value.BinaryKey;
import org.modeshape.jcr.value.BinaryValue;
/**
* Use this abstract class to realize test cases which can easily executed on different BinaryStores
*/
public abstract class AbstractBinaryStoreTest {
@Rule
public TestRule skipTestRule = new SkipTestRule();
/**
* We need to generate the test byte arrays based on the minimum binary size, because that controls the distinction between
* Stored/In Memory binary values.
*/
public static final int LARGE_BINARY_SIZE = (int) AbstractBinaryStore.DEFAULT_MINIMUM_BINARY_SIZE_IN_BYTES * 4;
public static final byte[] STORED_LARGE_BINARY = new byte[LARGE_BINARY_SIZE];
public static final BinaryKey STORED_LARGE_KEY;
public static final byte[] IN_MEMORY_BINARY = new byte[(int)(AbstractBinaryStore.DEFAULT_MINIMUM_BINARY_SIZE_IN_BYTES / 2)];
public static final BinaryKey IN_MEMORY_KEY;
public static final byte[] STORED_MEDIUM_BINARY = new byte[(int)(AbstractBinaryStore.DEFAULT_MINIMUM_BINARY_SIZE_IN_BYTES * 2)];
public static final BinaryKey STORED_MEDIUM_KEY;
public static final byte[] EMPTY_BINARY = new byte[0];
public static final BinaryKey EMPTY_BINARY_KEY;
public static final String TEXT_DATA;
protected static final MimeTypeDetector DEFAULT_DETECTOR = new DefaultMimeTypeDetector();
private static final Random RANDOM = new Random();
static {
RANDOM.nextBytes(STORED_LARGE_BINARY);
STORED_LARGE_KEY = BinaryKey.keyFor(STORED_LARGE_BINARY);
RANDOM.nextBytes(IN_MEMORY_BINARY);
IN_MEMORY_KEY = BinaryKey.keyFor(IN_MEMORY_BINARY);
RANDOM.nextBytes(STORED_MEDIUM_BINARY);
STORED_MEDIUM_KEY = BinaryKey.keyFor(STORED_MEDIUM_BINARY);
EMPTY_BINARY_KEY = BinaryKey.keyFor(EMPTY_BINARY);
TEXT_DATA = "Flash Gordon said: Ich bin Bärliner." + UUID.randomUUID().toString();
}
protected abstract BinaryStore getBinaryStore();
@Test
public void shouldAllowChangingTheMinimumBinarySize() throws Exception {
BinaryStore binaryStore = getBinaryStore();
long originalSize = binaryStore.getMinimumBinarySizeInBytes();
assertTrue(originalSize > 0);
long newSize = 12l;
binaryStore.setMinimumBinarySizeInBytes(newSize);
assertEquals(newSize, binaryStore.getMinimumBinarySizeInBytes());
binaryStore.setMinimumBinarySizeInBytes(originalSize);
}
@Test(expected = BinaryStoreException.class)
public void shouldFailWhenGettingInvalidBinary() throws BinaryStoreException {
getBinaryStore().getInputStream(invalidBinaryKey());
}
@Test
public void shouldStoreLargeBinary() throws BinaryStoreException, IOException {
storeAndValidate(STORED_LARGE_KEY, STORED_LARGE_BINARY);
}
@Test
public void shouldStoreMediumBinary() throws BinaryStoreException, IOException {
storeAndValidate(STORED_MEDIUM_KEY, STORED_MEDIUM_BINARY);
}
@Test
public void shouldStoreSmallBinary() throws BinaryStoreException, IOException {
storeAndValidate(IN_MEMORY_KEY, IN_MEMORY_BINARY);
}
@Test
public void shouldStoreZeroLengthBinary() throws BinaryStoreException, IOException {
storeAndValidate(EMPTY_BINARY_KEY, EMPTY_BINARY);
}
@Test
public void shouldHaveKey() throws BinaryStoreException, IOException {
storeAndValidate(STORED_MEDIUM_KEY, STORED_MEDIUM_BINARY);
assertTrue("Expected BinaryStore to contain the key", getBinaryStore().hasBinary(STORED_MEDIUM_KEY));
}
@Test
public void shouldNotHaveKey() {
assertTrue("Did not expect BinaryStore to contain the key", !getBinaryStore().hasBinary(invalidBinaryKey()));
}
private BinaryValue storeAndValidate( BinaryKey key,
byte[] data ) throws BinaryStoreException, IOException {
BinaryValue res = getBinaryStore().storeValue(new ByteArrayInputStream(data), false);
assertNotNull(res);
assertEquals(key, res.getKey());
assertEquals(data.length, res.getSize());
InputStream inputStream = getBinaryStore().getInputStream(key);
byte[] content = IoUtil.readBytes(inputStream);
assertArrayEquals(data, content);
BinaryKey currentKey = BinaryKey.keyFor(content);
assertEquals(key, currentKey);
return res;
}
@Test
public void shouldCleanupUnunsedValues() throws Exception {
getBinaryStore().storeValue(new ByteArrayInputStream(IN_MEMORY_BINARY), false);
List<BinaryKey> keys = new ArrayList<BinaryKey>();
keys.add(IN_MEMORY_KEY);
getBinaryStore().markAsUnused(keys);
Thread.sleep(100);
// now remove and test if still there
getBinaryStore().removeValuesUnusedLongerThan(1, TimeUnit.MILLISECONDS);
try {
// no annotation used here to differ from other BinaryStoreException
getBinaryStore().getInputStream(IN_MEMORY_KEY);
fail("Key was not removed");
} catch (BinaryStoreException ex) {
}
}
@Test
@FixFor( "MODE-2302" )
public void shouldMarkBinariesAsUsed() throws Exception {
BinaryStore binaryStore = getBinaryStore();
binaryStore.storeValue(new ByteArrayInputStream(IN_MEMORY_BINARY), false);
binaryStore.markAsUnused(Arrays.asList(IN_MEMORY_KEY));
Thread.sleep(100);
binaryStore.markAsUsed(Arrays.asList(IN_MEMORY_KEY));
Thread.sleep(2);
binaryStore.removeValuesUnusedLongerThan(1, TimeUnit.MILLISECONDS);
InputStream is = binaryStore.getInputStream(IN_MEMORY_KEY);
assertNotNull(is);
}
@Test
@FixFor( "MODE-2302" )
public void shouldStoreBinariesAsUnused() throws Exception {
byte[] randomBinary = new byte[(int)(AbstractBinaryStore.DEFAULT_MINIMUM_BINARY_SIZE_IN_BYTES * 2)];
RANDOM.nextBytes(randomBinary);
BinaryKey binaryKey = BinaryKey.keyFor(randomBinary);
BinaryStore binaryStore = getBinaryStore();
binaryStore.storeValue(new ByteArrayInputStream(randomBinary), true);
assertTrue("Binary not stored", binaryStore.hasBinary(binaryKey));
for (BinaryKey storedKey : binaryStore.getAllBinaryKeys()) {
if (storedKey.equals(binaryKey)) {
fail("Binary key found as used even though it was added as unused");
}
}
Thread.sleep(100);
binaryStore.removeValuesUnusedLongerThan(1, TimeUnit.MILLISECONDS);
assertFalse(binaryStore.hasBinary(binaryKey));
}
@Test
public void shouldAcceptStrategyHintsForStoringValues() throws Exception {
BinaryValue res = getBinaryStore().storeValue(new ByteArrayInputStream(STORED_MEDIUM_BINARY), null, false);
assertTrue(getBinaryStore().hasBinary(res.getKey()));
}
@Test(expected = BinaryStoreException.class)
public void shouldFailWhenGettingTheMimeTypeOfBinaryWhichIsntStored() throws IOException, RepositoryException {
getBinaryStore().getMimeType(new StoredBinaryValue(getBinaryStore(), invalidBinaryKey(), 0), "foobar.txt");
}
@Test(expected = BinaryStoreException.class)
public void shouldFailWhenGettingTheTextOfBinaryWhichIsntStored() throws RepositoryException {
getBinaryStore().getText(new StoredBinaryValue(getBinaryStore(), invalidBinaryKey(), 0));
}
private BinaryKey invalidBinaryKey() {
return new BinaryKey(UUID.randomUUID().toString());
}
@Test
public void shouldReturnAllStoredKeys() throws Exception {
storeAndValidate(STORED_MEDIUM_KEY, STORED_MEDIUM_BINARY);
storeAndValidate(IN_MEMORY_KEY, IN_MEMORY_BINARY);
List<String> keys = new ArrayList<String>(Arrays.asList(STORED_MEDIUM_KEY.toString(), IN_MEMORY_KEY.toString()));
for (BinaryKey key : getBinaryStore().getAllBinaryKeys()) {
keys.remove(key.toString());
}
assertEquals(0, keys.size());
}
@Test
public void shouldExtractAndStoreMimeTypeWhenDetectorConfigured() throws RepositoryException, IOException {
getBinaryStore().setMimeTypeDetector(new DummyMimeTypeDetector());
BinaryValue binaryValue = getBinaryStore().storeValue(new ByteArrayInputStream(IN_MEMORY_BINARY), false);
// unclean stuff... a getter modifies silently data
assertEquals(DummyMimeTypeDetector.DEFAULT_TYPE, getBinaryStore().getMimeType(binaryValue, "foobar.txt"));
}
@Test
public void shouldExtractAndStoreTextWhenExtractorConfigured() throws Exception {
TextExtractors extractors = new TextExtractors(Executors.newSingleThreadExecutor(),
new ArrayList<>(Arrays.asList(new DummyTextExtractor())));
try {
BinaryStore binaryStore = getBinaryStore();
binaryStore.setTextExtractors(extractors);
BinaryValue binaryValue = getBinaryStore().storeValue(new ByteArrayInputStream(STORED_LARGE_BINARY), false);
String extractedText = binaryStore.getText(binaryValue);
if (extractedText == null) {
// if nothing is found the first time, sleep and try again - Mongo on Windows seems to exhibit this problem for some
// reason
Thread.sleep(TimeUnit.SECONDS.toMillis(2));
extractedText = binaryStore.getText(binaryValue);
}
assertEquals(DummyTextExtractor.EXTRACTED_TEXT, extractedText);
} finally {
extractors.shutdown();
}
}
@Test
@FixFor("MODE-2547")
public void shouldStoreBinariesConcurrently() throws Exception {
int binariesCount = 11; //to cover MongoDB, make sure this is >10 which is the default Mongo pool size
List<byte[]> bytesToStore = new ArrayList<>(binariesCount);
for (int i = 0; i < binariesCount; i++) {
byte[] data = new byte[LARGE_BINARY_SIZE];
RANDOM.nextBytes(data);
bytesToStore.add(data);
}
ExecutorService executorService = Executors.newFixedThreadPool(binariesCount);
try {
final CyclicBarrier barrier = new CyclicBarrier(binariesCount + 1);
List<Future<BinaryValue>> results = new ArrayList<>(binariesCount);
for (final byte[] data : bytesToStore) {
Future<BinaryValue> result = executorService.submit(() -> {
barrier.await();
return getBinaryStore().storeValue(new ByteArrayInputStream(data), false);
});
results.add(result);
}
barrier.await();
List<BinaryValue> storedBinaries = new ArrayList<>(binariesCount);
for (Future<BinaryValue> result : results) {
storedBinaries.add(result.get(2, TimeUnit.SECONDS));
}
for (int i = 0; i < binariesCount; i++) {
byte[] expectedBytes = bytesToStore.get(i);
byte[] actualBytes = IoUtil.readBytes(storedBinaries.get(i).getStream());
Assert.assertArrayEquals("Invalid binary found", expectedBytes, actualBytes);
}
} finally {
executorService.shutdownNow();
}
}
protected static final class DummyMimeTypeDetector implements MimeTypeDetector {
public static final String DEFAULT_TYPE = "application/foobar";
@Override
public String mimeTypeOf( String name,
Binary binaryValue ) {
return DEFAULT_TYPE;
}
}
protected static final class DummyTextExtractor extends TextExtractor {
private static final String EXTRACTED_TEXT = "some text";
@Override
public void extractFrom( org.modeshape.jcr.api.Binary binary,
Output output,
Context context ) throws Exception {
output.recordText(EXTRACTED_TEXT);
}
@Override
public boolean supportsMimeType( String mimeType ) {
return true;
}
}
}