/* * (C) Copyright 2010-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: * Nuxeo - initial API and implementation */ package org.nuxeo.ecm.platform.rendition.service; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.nuxeo.ecm.platform.rendition.Constants.FILES_FILES_PROPERTY; import static org.nuxeo.ecm.platform.rendition.Constants.RENDITION_FACET; import static org.nuxeo.ecm.platform.rendition.Constants.RENDITION_SOURCE_ID_PROPERTY; import static org.nuxeo.ecm.platform.rendition.Constants.RENDITION_SOURCE_VERSIONABLE_ID_PROPERTY; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CyclicBarrier; import java.util.stream.Collectors; import java.util.zip.ZipInputStream; import javax.inject.Inject; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; 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.CoreInstance; 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.NuxeoException; import org.nuxeo.ecm.core.api.NuxeoPrincipal; import org.nuxeo.ecm.core.api.PathRef; import org.nuxeo.ecm.core.api.VersioningOption; import org.nuxeo.ecm.core.api.blobholder.BlobHolder; import org.nuxeo.ecm.core.api.security.ACE; import org.nuxeo.ecm.core.api.security.ACL; import org.nuxeo.ecm.core.api.security.ACP; import org.nuxeo.ecm.core.api.security.SecurityConstants; import org.nuxeo.ecm.core.api.security.impl.ACLImpl; import org.nuxeo.ecm.core.api.security.impl.ACPImpl; import org.nuxeo.ecm.core.event.EventService; import org.nuxeo.ecm.core.test.CoreFeature; import org.nuxeo.ecm.core.test.StorageConfiguration; import org.nuxeo.ecm.core.test.TransactionalFeature; import org.nuxeo.ecm.core.versioning.VersioningService; import org.nuxeo.ecm.core.work.api.Work; import org.nuxeo.ecm.core.work.api.WorkManager; import org.nuxeo.ecm.platform.rendition.Rendition; import org.nuxeo.ecm.platform.rendition.impl.LazyRendition; import org.nuxeo.ecm.platform.rendition.lazy.AbstractRenditionBuilderWork; import org.nuxeo.ecm.platform.usermanager.UserManager; 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.LocalDeploy; import org.nuxeo.runtime.test.runner.RuntimeHarness; import org.nuxeo.runtime.transaction.TransactionHelper; /** * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a> */ @RunWith(FeaturesRunner.class) @Features(RenditionFeature.class) @LocalDeploy({ "org.nuxeo.ecm.platform.rendition.core:test-rendition-contrib.xml", "org.nuxeo.ecm.platform.rendition.core:test-lazy-rendition-contrib.xml" }) public class TestRenditionService { public static final String RENDITION_CORE = "org.nuxeo.ecm.platform.rendition.core"; private static final String RENDITION_FILTERS_COMPONENT_LOCATION = "test-rendition-filters-contrib.xml"; private static final String RENDITION_DEFINITION_PROVIDERS_COMPONENT_LOCATION = "test-rendition-definition-providers-contrib.xml"; private static final String RENDITION_WORKMANAGER_COMPONENT_LOCATION = "test-rendition-multithreads-workmanager-contrib.xml"; public static final String PDF_RENDITION_DEFINITION = "pdf"; public static final String ZIP_TREE_EXPORT_RENDITION_DEFINITION = "zipTreeExport"; public static CyclicBarrier[] CYCLIC_BARRIERS = new CyclicBarrier[] { new CyclicBarrier(2), new CyclicBarrier(2), new CyclicBarrier(2)}; public static final String CYCLIC_BARRIER_DESCRIPTION = "cyclicBarrierDesc"; public static final Log log = LogFactory.getLog(TestRenditionService.class); @Inject protected RuntimeHarness runtimeHarness; @Inject protected CoreFeature coreFeature; @Inject protected CoreSession session; @Inject protected EventService eventService; @Inject protected WorkManager works; @Inject protected RenditionService renditionService; @Test public void serviceRegistration() { assertNotNull(renditionService); } @Test public void testDeclaredRenditionDefinitions() { List<RenditionDefinition> renditionDefinitions = renditionService.getDeclaredRenditionDefinitions(); assertRenditionDefinitions(renditionDefinitions, PDF_RENDITION_DEFINITION, "renditionDefinitionWithUnknownOperationChain", "zipExport", "zipTreeExport", "zipTreeExportLazily"); RenditionDefinition rd = renditionDefinitions.stream() .filter(renditionDefinition -> PDF_RENDITION_DEFINITION.equals( renditionDefinition.getName())) .findFirst() .get(); assertNotNull(rd); assertEquals(PDF_RENDITION_DEFINITION, rd.getName()); assertEquals("blobToPDF", rd.getOperationChain()); assertEquals("label.rendition.pdf", rd.getLabel()); assertTrue(rd.isEnabled()); rd = renditionDefinitions.stream() .filter(renditionDefinition -> "renditionDefinitionWithCustomOperationChain".equals( renditionDefinition.getName())) .findFirst() .get(); assertNotNull(rd); assertEquals("renditionDefinitionWithCustomOperationChain", rd.getName()); assertEquals("Dummy", rd.getOperationChain()); } @Test public void testAvailableRenditionDefinitions() throws Exception { DocumentModel file = session.createDocumentModel("/", "file", "File"); file.setPropertyValue("dc:title", "TestFile"); file = session.createDocument(file); List<RenditionDefinition> renditionDefinitions = renditionService.getAvailableRenditionDefinitions(file); int availableRenditionDefinitionCount = renditionDefinitions.size(); assertTrue(availableRenditionDefinitionCount > 0); // add a blob Blob blob = Blobs.createBlob("I am a Blob"); file.setPropertyValue("file:content", (Serializable) blob); file = session.saveDocument(file); // rendition should be available now renditionDefinitions = renditionService.getAvailableRenditionDefinitions(file); assertEquals(availableRenditionDefinitionCount + 1, renditionDefinitions.size()); } @Test public void doPDFRendition() { DocumentModel file = createBlobFile(); DocumentRef renditionDocumentRef = renditionService.storeRendition(file, PDF_RENDITION_DEFINITION); DocumentModel renditionDocument = session.getDocument(renditionDocumentRef); assertNotNull(renditionDocument); assertTrue(renditionDocument.hasFacet(RENDITION_FACET)); assertEquals(file.getId(), renditionDocument.getPropertyValue(RENDITION_SOURCE_VERSIONABLE_ID_PROPERTY)); DocumentModel lastVersion = session.getLastDocumentVersion(file.getRef()); assertEquals(lastVersion.getId(), renditionDocument.getPropertyValue(RENDITION_SOURCE_ID_PROPERTY)); BlobHolder bh = renditionDocument.getAdapter(BlobHolder.class); Blob renditionBlob = bh.getBlob(); assertNotNull(renditionBlob); assertEquals("application/pdf", renditionBlob.getMimeType()); assertEquals("dummy.pdf", renditionBlob.getFilename()); // now refetch the rendition Rendition rendition = renditionService.getRendition(file, PDF_RENDITION_DEFINITION); assertNotNull(rendition); assertTrue(rendition.isStored()); assertEquals(renditionDocument.getRef(), rendition.getHostDocument().getRef()); assertEquals("/icons/pdf.png", renditionDocument.getPropertyValue("common:icon")); // now update the document file.setPropertyValue("dc:description", "I have been updated"); file = session.saveDocument(file); rendition = renditionService.getRendition(file, PDF_RENDITION_DEFINITION); assertNotNull(rendition); assertFalse(rendition.isStored()); } @Test public void doRenditionVersioning() { DocumentModel file = createBlobFile(); assertEquals("project", file.getCurrentLifeCycleState()); file.followTransition("approve"); assertEquals("approved", file.getCurrentLifeCycleState()); // create a version of the document file.putContextData(VersioningService.VERSIONING_OPTION, VersioningOption.MINOR); file = session.saveDocument(file); session.save(); eventService.waitForAsyncCompletion(); assertEquals("0.1", file.getVersionLabel()); // make a rendition on the document DocumentRef renditionDocumentRef = renditionService.storeRendition(file, PDF_RENDITION_DEFINITION); DocumentModel renditionDocument = session.getDocument(renditionDocumentRef); assertNotNull(renditionDocument); assertEquals(file.getId(), renditionDocument.getPropertyValue(RENDITION_SOURCE_VERSIONABLE_ID_PROPERTY)); DocumentModel lastVersion = session.getLastDocumentVersion(file.getRef()); assertEquals(lastVersion.getId(), renditionDocument.getPropertyValue(RENDITION_SOURCE_ID_PROPERTY)); // check that the redition is a version assertTrue(renditionDocument.isVersion()); // check same life-cycle state assertEquals(file.getCurrentLifeCycleState(), renditionDocument.getCurrentLifeCycleState()); // check that version label of the rendition is the same as the source assertEquals(file.getVersionLabel(), renditionDocument.getVersionLabel()); // fetch the rendition to check we have the same DocumentModel Rendition rendition = renditionService.getRendition(file, PDF_RENDITION_DEFINITION); assertNotNull(rendition); assertTrue(rendition.isStored()); assertEquals(renditionDocument.getRef(), rendition.getHostDocument().getRef()); // update the source Document file.setPropertyValue("dc:description", "I have been updated"); file = session.saveDocument(file); assertEquals("0.1+", file.getVersionLabel()); // get the rendition from checkedout doc rendition = renditionService.getRendition(file, PDF_RENDITION_DEFINITION); assertNotNull(rendition); // rendition should be live assertFalse(rendition.isStored()); // Live Rendition should point to the live doc assertTrue(rendition.getHostDocument().getRef().equals(file.getRef())); // needed for MySQL otherwise version order could be random coreFeature.getStorageConfiguration().maybeSleepToNextSecond(); // now store rendition for version 0.2 rendition = renditionService.getRendition(file, PDF_RENDITION_DEFINITION, true); assertEquals("0.2", rendition.getHostDocument().getVersionLabel()); assertTrue(rendition.isStored()); assertTrue(rendition.getHostDocument().isVersion()); System.out.println(rendition.getHostDocument().getACP()); // check that version 0.2 of file was created List<DocumentModel> versions = session.getVersions(file.getRef()); assertEquals(2, versions.size()); // check retrieval Rendition rendition2 = renditionService.getRendition(file, PDF_RENDITION_DEFINITION, false); assertTrue(rendition2.isStored()); assertEquals(rendition.getHostDocument().getRef(), rendition2.getHostDocument().getRef()); // update the source Document file.setPropertyValue("dc:description", "I have been updated again"); file = session.saveDocument(file); assertEquals("0.2+", file.getVersionLabel()); // needed for MySQL otherwise version order could be random coreFeature.getStorageConfiguration().maybeSleepToNextSecond(); // now store rendition for version 0.3 rendition = renditionService.getRendition(file, PDF_RENDITION_DEFINITION, true); assertEquals("0.3", rendition.getHostDocument().getVersionLabel()); assertTrue(rendition.isStored()); assertTrue(rendition.getHostDocument().isVersion()); } @Test public void doErrorRendition() { DocumentModel file = createBlobFile(); session.save(); nextTransaction(); String renditionName = "delayedErrorAutomationRendition"; Rendition rendition = renditionService.getRendition(file, renditionName); assertNotNull(rendition); try { rendition.getBlob(); fail(); } catch (NuxeoException e) { assertTrue(e.getMessage(), e.getMessage().contains("DelayedError")); } } @Test public void doErrorLazyRendition() throws Exception { DocumentModel file = createBlobFile(); Calendar issued = new GregorianCalendar(2010, Calendar.OCTOBER, 10, 10, 10, 10); file.setPropertyValue("dc:issued", (Serializable) issued.clone()); session.saveDocument(file); session.save(); nextTransaction(); String renditionName = "lazyDelayedErrorAutomationRendition"; for (int i = 0; i < 2; i++) { boolean store = i == 1; if (store) { issued.add(Calendar.SECOND, 10); file.setPropertyValue("dc:issued", (Serializable) issued.clone()); session.saveDocument(file); session.save(); nextTransaction(); } for (int j = 0; j < 3; j++) { boolean empty = j != 1; Rendition rendition = renditionService.getRendition(file, renditionName, store); assertNotNull(rendition); Blob blob = rendition.getBlob(); assertEquals(0, blob.getLength()); String mimeType = blob.getMimeType(); String marker = (empty ? LazyRendition.EMPTY_MARKER : LazyRendition.ERROR_MARKER); String falseMarker = (!empty ? LazyRendition.EMPTY_MARKER : LazyRendition.ERROR_MARKER); String markerMsg = String.format("mimeType: %s should contain %s (i=%s,j=%s)", mimeType, marker, i, j); String falseMarkerMsg = String.format( "mimeType: %s should not contain %s (i=$s,j=%s)", mimeType, falseMarker, i, j); assertTrue(markerMsg, mimeType.contains(marker)); assertFalse(falseMarkerMsg, mimeType.contains(falseMarker)); nextTransaction(); } } } @Test public void doZipTreeExportRendition() throws Exception { doZipTreeExportRendition(false); } @Test public void doZipTreeExportLazyRendition() throws Exception { doZipTreeExportRendition(true); } protected void doZipTreeExportRendition(boolean isLazy) throws Exception { DocumentModel folder = createFolderWithChildren(); String renditionName = ZIP_TREE_EXPORT_RENDITION_DEFINITION; if (isLazy) { renditionName += "Lazily"; } Rendition rendition = getRendition(folder, renditionName, true, isLazy); assertTrue(rendition.isStored()); assertTrue(rendition.isCompleted()); assertEquals(rendition.getHostDocument().getPropertyValue("dc:modified"), rendition.getModificationDate()); DocumentModel renditionDocument = session.getDocument(rendition.getHostDocument().getRef()); assertTrue(renditionDocument.hasFacet(RENDITION_FACET)); assertNull(renditionDocument.getPropertyValue(RENDITION_SOURCE_VERSIONABLE_ID_PROPERTY)); assertEquals(folder.getId(), renditionDocument.getPropertyValue(RENDITION_SOURCE_ID_PROPERTY)); BlobHolder bh = renditionDocument.getAdapter(BlobHolder.class); Blob renditionBlob = bh.getBlob(); assertNotNull(renditionBlob); assertEquals("application/zip", renditionBlob.getMimeType()); assertEquals("export.zip", renditionBlob.getFilename()); // now refetch the rendition rendition = renditionService.getRendition(folder, renditionName); assertNotNull(rendition); assertTrue(rendition.isStored()); assertEquals(renditionDocument.getRef(), rendition.getHostDocument().getRef()); assertEquals("/icons/zip.png", renditionDocument.getPropertyValue("common:icon")); // now get a different rendition as a different user NuxeoPrincipal totoPrincipal = Framework.getService(UserManager.class).getPrincipal("toto"); try (CoreSession userSession = coreFeature.openCoreSession(totoPrincipal)) { folder = userSession.getDocument(folder.getRef()); Rendition totoRendition = getRendition(folder, renditionName, true, isLazy); assertTrue(totoRendition.isStored()); assertNotEquals(renditionDocument.getRef(), totoRendition.getHostDocument().getRef()); // verify Administrator's rendition is larger than user's rendition assertNotEquals(rendition.getHostDocument().getRef(), totoRendition.getHostDocument().getRef()); long adminZipEntryCount = countZipEntries(new ZipInputStream(rendition.getBlob().getStream())); long totoZipEntryCount = countZipEntries(new ZipInputStream(totoRendition.getBlob().getStream())); assertTrue( String.format("Admin rendition entry count %s should be greater than user rendition entry count %s", adminZipEntryCount, totoZipEntryCount), adminZipEntryCount > totoZipEntryCount); } coreFeature.getStorageConfiguration().maybeSleepToNextSecond(); // now "update" the folder folder = session.getDocument(folder.getRef()); folder.setPropertyValue("dc:description", "I have been updated"); folder = session.saveDocument(folder); session.save(); nextTransaction(); folder = session.getDocument(folder.getRef()); rendition = getRendition(folder, renditionName, false, isLazy); assertFalse(rendition.isStored()); assertTrue(rendition.isCompleted()); Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(rendition.getBlob().getFile().lastModified()); assertEquals(cal, rendition.getModificationDate()); if (isLazy) { rendition = renditionService.getRendition(folder, renditionName, false); assertEquals(cal, rendition.getModificationDate()); } } protected Rendition getRendition(DocumentModel doc, String renditionName, boolean store, boolean isLazy) { Rendition rendition = renditionService.getRendition(doc, renditionName, store); assertNotNull(rendition); if (isLazy) { assertFalse(rendition.isStored()); assertFalse(rendition.isCompleted()); assertNull(rendition.getModificationDate()); Blob blob = rendition.getBlob(); assertEquals(0, blob.getLength()); assertTrue(blob.getMimeType().contains("empty=true")); assertTrue(blob.getFilename().equals("inprogress")); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("interrupted", e); } eventService.waitForAsyncCompletion(5000); rendition = renditionService.getRendition(doc, renditionName, store); } return rendition; } protected DocumentModel createBlobFile() { Blob blob = createTextBlob("Dummy text", "dummy.txt"); DocumentModel file = createDocumentWithBlob("/", blob, "dummy-file", "File"); assertNotNull(file); return file; } protected DocumentModel createDocumentWithBlob(String parentPath, Blob blob, String name, String typeName) { DocumentModel doc = session.createDocumentModel(parentPath, name, typeName); BlobHolder bh = doc.getAdapter(BlobHolder.class); bh.setBlob(blob); doc = session.createDocument(doc); return doc; } protected Blob createTextBlob(String content, String filename) { Blob blob = Blobs.createBlob(content); blob.setFilename(filename); return blob; } protected DocumentModel createFolderWithChildren() { DocumentModel root = session.getRootDocument(); ACP acp = session.getACP(root.getRef()); ACL existingACL = acp.getOrCreateACL(); existingACL.clear(); existingACL.add(new ACE("Administrator", SecurityConstants.EVERYTHING, true)); existingACL.add(new ACE("group_1", SecurityConstants.READ, true)); acp.addACL(existingACL); session.setACP(root.getRef(), acp, true); DocumentModel folder = session.createDocumentModel(root.getPathAsString(), "dummy", "Folder"); folder = session.createDocument(folder); session.save(); for (int i = 1; i <= 2; i++) { String childFolderName = "childFolder" + i; DocumentModel childFolder = session.createDocumentModel(folder.getPathAsString(), childFolderName, "Folder"); childFolder = session.createDocument(childFolder); if (i == 1) { acp = new ACPImpl(); ACL acl = new ACLImpl(); acl.add(new ACE("Administrator", SecurityConstants.EVERYTHING, true)); acl.add(ACE.BLOCK); acp.addACL(acl); childFolder.setACP(acp, true); session.save(); } DocumentModel doc1 = createDocumentWithBlob(childFolder.getPathAsString(), createTextBlob("Dummy1 text", "dummy1.txt"), "dummy1-file", "File"); DocumentModel doc2 = createDocumentWithBlob(childFolder.getPathAsString(), createTextBlob("Dummy2 text", "dummy2.txt"), "dummy2-file", "File"); } session.save(); TransactionHelper.commitOrRollbackTransaction(); eventService.waitForAsyncCompletion(); TransactionHelper.startTransaction(); folder = session.getDocument(folder.getRef()); return folder; } @Test public void shouldPreventAdminFromReusingOthersNonVersionedStoredRendition() throws Exception { DocumentModel folder = createFolderWithChildren(); String renditionName = ZIP_TREE_EXPORT_RENDITION_DEFINITION; Rendition totoRendition; // get rendition as non-admin user 'toto' NuxeoPrincipal totoPrincipal = Framework.getService(UserManager.class).getPrincipal("toto"); try (CoreSession userSession = coreFeature.openCoreSession(totoPrincipal)) { folder = userSession.getDocument(folder.getRef()); totoRendition = renditionService.getRendition(folder, renditionName, true); assertTrue(totoRendition.isStored()); } nextTransaction(); eventService.waitForAsyncCompletion(); coreFeature.getStorageConfiguration().maybeSleepToNextSecond(); // now get rendition as admin user 'Administrator' folder = session.getDocument(folder.getRef()); Rendition rendition = renditionService.getRendition(folder, renditionName, true); assertTrue(rendition.isStored()); assertTrue(rendition.isCompleted()); // verify Administrator's rendition is different from user's rendition, is larger than user's rendition, // and was created later assertNotEquals(rendition.getHostDocument().getRef(), totoRendition.getHostDocument().getRef()); long adminZipEntryCount = countZipEntries(new ZipInputStream(rendition.getBlob().getStream())); long totoZipEntryCount = countZipEntries(new ZipInputStream(totoRendition.getBlob().getStream())); assertTrue(String.format("Admin rendition entry count %s should be greater than user rendition entry count %s", adminZipEntryCount, totoZipEntryCount), adminZipEntryCount > totoZipEntryCount); Calendar adminModificationDate = rendition.getModificationDate(); Calendar totoModificationDate = totoRendition.getModificationDate(); assertTrue( String.format("Admin rendition modif date %s should be after user rendition modif date %s", adminModificationDate.toInstant(), totoModificationDate.toInstant()), adminModificationDate.after(totoModificationDate)); } private long countZipEntries(ZipInputStream zis) throws IOException { int entryCount = 0; while (zis.getNextEntry() != null) { entryCount++; } return entryCount; } @Test public void testRenderAProxyDocument() { DocumentModel file = createBlobFile(); DocumentModel proxy = session.createProxy(file.getRef(), new PathRef("/")); DocumentRef renditionRef = renditionService.storeRendition(proxy, PDF_RENDITION_DEFINITION); DocumentModel rendition = session.getDocument(renditionRef); assertTrue(rendition.isVersion()); assertEquals(null, rendition.getParentRef()); // placeless } @Test public void shouldNotCreateANewVersionForACheckedInDocument() { DocumentModel file = createBlobFile(); DocumentRef versionRef = file.checkIn(VersioningOption.MINOR, null); file.refresh(DocumentModel.REFRESH_STATE, null); DocumentModel version = session.getDocument(versionRef); DocumentRef renditionDocumentRef = renditionService.storeRendition(version, "pdf"); DocumentModel renditionDocument = session.getDocument(renditionDocumentRef); assertEquals(version.getId(), renditionDocument.getPropertyValue(RENDITION_SOURCE_ID_PROPERTY)); List<DocumentModel> versions = session.getVersions(file.getRef()); assertFalse(versions.isEmpty()); assertEquals(1, versions.size()); DocumentModel lastVersion = session.getLastDocumentVersion(file.getRef()); assertEquals(version.getRef(), lastVersion.getRef()); } @Test public void shouldNotRenderAnEmptyDocument() { DocumentModel file = session.createDocumentModel("/", "dummy", "File"); file = session.createDocument(file); try { renditionService.storeRendition(file, PDF_RENDITION_DEFINITION); fail(); } catch (NuxeoException e) { assertTrue(e.getMessage(), e.getMessage().startsWith("Rendition pdf not available")); } } @Test public void shouldNotRenderWithAnUndefinedRenditionDefinition() { DocumentModel file = session.createDocumentModel("/", "dummy", "File"); file = session.createDocument(file); try { renditionService.storeRendition(file, "undefinedRenditionDefinition"); fail(); } catch (NuxeoException e) { assertEquals(e.getMessage(), "The rendition definition 'undefinedRenditionDefinition' is not registered"); } } @Test public void shouldNotRenderWithAnUndefinedOperationChain() { DocumentModel file = session.createDocumentModel("/", "dummy", "File"); file = session.createDocument(file); try { renditionService.storeRendition(file, "renditionDefinitionWithUnknownOperationChain"); fail(); } catch (NuxeoException e) { assertTrue(e.getMessage(), e.getMessage().startsWith("Rendition renditionDefinitionWithUnknownOperationChain not available")); } } @Test public void shouldRenderOnFolder() throws Exception { DocumentModel folder = session.createDocumentModel("/", "dummy", "Folder"); folder = session.createDocument(folder); Rendition rendition = renditionService.getRendition(folder, "renditionDefinitionWithCustomOperationChain"); assertNotNull(rendition); assertNotNull(rendition.getBlob()); assertEquals(rendition.getBlob().getString(), "dummy"); } @Test @SuppressWarnings("unchecked") public void shouldRemoveFilesBlobsOnARendition() { DocumentModel fileDocument = createBlobFile(); Blob firstAttachedBlob = createTextBlob("first attached blob", "first"); Blob secondAttachedBlob = createTextBlob("second attached blob", "second"); List<Map<String, Serializable>> files = new ArrayList<>(); Map<String, Serializable> file = new HashMap<>(); file.put("file", (Serializable) firstAttachedBlob); files.add(file); file = new HashMap<>(); file.put("file", (Serializable) secondAttachedBlob); files.add(file); fileDocument.setPropertyValue(FILES_FILES_PROPERTY, (Serializable) files); DocumentRef renditionDocumentRef = renditionService.storeRendition(fileDocument, PDF_RENDITION_DEFINITION); DocumentModel renditionDocument = session.getDocument(renditionDocumentRef); BlobHolder bh = renditionDocument.getAdapter(BlobHolder.class); Blob renditionBlob = bh.getBlob(); assertNotNull(renditionBlob); assertEquals("application/pdf", renditionBlob.getMimeType()); List<Map<String, Serializable>> renditionFiles = (List<Map<String, Serializable>>) renditionDocument.getPropertyValue( FILES_FILES_PROPERTY); assertTrue(renditionFiles.isEmpty()); } @Test public void shouldNotRenderADocumentWithoutBlobHolder() { DocumentModel folder = session.createDocumentModel("/", "dummy-folder", "Folder"); folder = session.createDocument(folder); try { renditionService.storeRendition(folder, PDF_RENDITION_DEFINITION); fail(); } catch (NuxeoException e) { assertTrue(e.getMessage(), e.getMessage().startsWith("Rendition pdf not available")); } } @Test public void shouldNotStoreRenditionByDefault() { DocumentModel folder = createFolderWithChildren(); String renditionName = ZIP_TREE_EXPORT_RENDITION_DEFINITION; Rendition rendition = renditionService.getRendition(folder, renditionName); assertNotNull(rendition); assertFalse(rendition.isStored()); } @Test public void shouldStoreRenditionByDefault() { DocumentModel folder = createFolderWithChildren(); String renditionName = ZIP_TREE_EXPORT_RENDITION_DEFINITION + "Lazily"; Rendition rendition = renditionService.getRendition(folder, renditionName); assertNotNull(rendition); assertFalse(rendition.isStored()); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("interrupted", e); } eventService.waitForAsyncCompletion(5000); rendition = renditionService.getRendition(folder, renditionName); assertNotNull(rendition); assertTrue(rendition.isStored()); } @Test public void shouldStoreLatestNonVersionedRendition() throws Exception { runtimeHarness.deployContrib(RENDITION_CORE, RENDITION_WORKMANAGER_COMPONENT_LOCATION); final StorageConfiguration storageConfiguration = coreFeature.getStorageConfiguration(); final String repositoryName = session.getRepositoryName(); final String username = session.getPrincipal().getName(); final String renditionName = "renditionDefinitionWithCustomOperationChain"; final String sourceDocumentModificationDatePropertyName = "dc:issued"; DocumentModel folder = session.createDocumentModel("/", "dummy", "Folder"); folder.setPropertyValue(sourceDocumentModificationDatePropertyName, Calendar.getInstance()); folder = session.createDocument(folder); session.save(); nextTransaction(); eventService.waitForAsyncCompletion(); folder = session.getDocument(folder.getRef()); final String folderId = folder.getId(); RenditionThread t1 = new RenditionThread(storageConfiguration, repositoryName, username, folderId, renditionName, true); RenditionThread t2 = new RenditionThread(storageConfiguration, repositoryName, username, folderId, renditionName, false); t1.start(); t2.start(); // Sync #1 RenditionThread.cyclicBarrier.await(); // now "update" the folder description Calendar modificationDate = Calendar.getInstance(); String desc = "I have been updated"; folder = session.getDocument(folder.getRef()); folder.setPropertyValue("dc:description", desc); folder.setPropertyValue(sourceDocumentModificationDatePropertyName, modificationDate); folder = session.saveDocument(folder); session.save(); nextTransaction(); eventService.waitForAsyncCompletion(); // Sync #2 RenditionThread.cyclicBarrier.await(); // Sync #3 RenditionThread.cyclicBarrier.await(); t1.join(); t2.join(); nextTransaction(); eventService.waitForAsyncCompletion(); // get the "updated" folder rendition Rendition rendition = renditionService.getRendition(folder, renditionName, true); assertNotNull(rendition); assertTrue(rendition.isStored()); Calendar cal = rendition.getModificationDate(); assertTrue(!cal.before(modificationDate)); assertNotNull(rendition.getBlob()); assertTrue(rendition.getBlob().getString().contains(desc)); // verify the thread renditions StorageConfiguration storageConfig = coreFeature.getStorageConfiguration(); List<Rendition> renditions = Arrays.asList(t1.getDetachedRendition(), t2.getDetachedRendition()); for (Rendition rend : renditions) { assertNotNull(rend); assertTrue(rend.isStored()); assertFalse(cal.before(rend.getModificationDate())); assertNotNull(rend.getBlob()); assertTrue(rendition.getBlob().getString().contains(desc)); } runtimeHarness.undeployContrib(RENDITION_CORE, RENDITION_WORKMANAGER_COMPONENT_LOCATION); } protected static class RenditionThread extends Thread { public static final CyclicBarrier cyclicBarrier = new CyclicBarrier(3); private final StorageConfiguration storageConfiguration; private final String repositoryName; private final String username; private final String docId; private final String renditionName; private final boolean delayed; private Rendition detachedRendition; public RenditionThread(StorageConfiguration storageConfiguration, String repositoryName, String username, String docId, String renditionName, boolean delayed) { super(); this.storageConfiguration = storageConfiguration; this.repositoryName = repositoryName; this.username = username; this.docId = docId; this.renditionName = renditionName; this.delayed = delayed; } @Override public void run() { TransactionHelper.startTransaction(); try { try (CoreSession session = CoreInstance.openCoreSession(repositoryName, username)) { DocumentModel doc = session.getDocument(new IdRef(docId)); doc.putContextData("delayed", Boolean.valueOf(delayed)); RenditionService renditionService = Framework.getService(RenditionService.class); detachedRendition = renditionService.getRendition(doc, renditionName, true); } } catch (Exception e) { throw new RuntimeException(e); } finally { TransactionHelper.commitOrRollbackTransaction(); storageConfiguration.maybeSleepToNextSecond(); } if (!delayed) { try { // Not-Delayed Sync #3 RenditionThread.cyclicBarrier.await(); } catch (Exception e) { throw new RuntimeException(e); } } } public Rendition getDetachedRendition() { return detachedRendition; } } @Inject TransactionalFeature txFeature; protected void nextTransaction() { txFeature.nextTransaction(); } @Test public void shouldNotScheduleRedundantLazyRenditionBuilderWorks() throws Exception { final String renditionName = "lazyAutomation"; final String sourceDocumentModificationDatePropertyName = "dc:issued"; Calendar issued = new GregorianCalendar(2010, Calendar.OCTOBER, 10, 10, 10, 10); String desc = CYCLIC_BARRIER_DESCRIPTION; DocumentModel folder = session.createDocumentModel("/", "dummy", "Folder"); folder.setPropertyValue("dc:title", folder.getName()); folder.setPropertyValue("dc:description", desc); folder.setPropertyValue(sourceDocumentModificationDatePropertyName, (Serializable) issued.clone()); folder = session.createDocument(folder); session.save(); nextTransaction(); eventService.waitForAsyncCompletion(); for (int i = 0; i < 3; i++) { folder = session.getDocument(folder.getRef()); Rendition rendition = renditionService.getRendition(folder, renditionName, false); assertNotNull(rendition); assertTrue(rendition.getBlob().getMimeType().contains(LazyRendition.EMPTY_MARKER)); if (i == 0) { if (log.isDebugEnabled()) { log.debug(DummyDocToTxt.formatLogEntry(folder.getRef(), null, desc, issued) + " before barrier 0"); } CYCLIC_BARRIERS[0].await(); } assertEquals(issued, folder.getPropertyValue(sourceDocumentModificationDatePropertyName)); issued.add(Calendar.SECOND, 10); folder.setPropertyValue(sourceDocumentModificationDatePropertyName, (Serializable) issued.clone()); desc = "description" + Integer.toString(i); folder.setPropertyValue("dc:description", desc); session.saveDocument(folder); session.save(); if (TransactionHelper.isTransactionActiveOrMarkedRollback()) { TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(); } if (i == 0) { if (log.isDebugEnabled()) { log.debug(DummyDocToTxt.formatLogEntry(folder.getRef(), null, desc, issued) + " before barrier 1"); } CYCLIC_BARRIERS[1].await(); } } String queueId = works.getCategoryQueueId(AbstractRenditionBuilderWork.CATEGORY); assertEquals(1, works.listWorkIds(queueId, Work.State.RUNNING).size()); assertEquals(1, works.listWorkIds(queueId, Work.State.SCHEDULED).size()); if (log.isDebugEnabled()) { log.debug(DummyDocToTxt.formatLogEntry(folder.getRef(), null, desc, issued) + " before barrier 2"); } CYCLIC_BARRIERS[2].await(); eventService.waitForAsyncCompletion(5000); folder = session.getDocument(folder.getRef()); assertEquals(issued, folder.getPropertyValue(sourceDocumentModificationDatePropertyName)); for (int i = 0; i < 5; i++) { Rendition rendition = renditionService.getRendition(folder, renditionName, false); assertNotNull(rendition); assertNotNull(rendition.getBlob()); String mimeType = rendition.getBlob().getMimeType(); if (mimeType != null) { if (mimeType.contains(LazyRendition.EMPTY_MARKER)) { Thread.sleep(1000); eventService.waitForAsyncCompletion(5000); continue; } else if (mimeType.contains(LazyRendition.ERROR_MARKER)) { fail("Error generating rendition for folder"); } } String content = rendition.getBlob().getString(); assertNotNull(content); assertTrue(content.contains("dummy")); assertNotNull(desc); assertTrue(content.contains(desc)); return; } fail("Could not retrieve rendition for folder"); } @Test public void shouldFilterRenditionDefinitions() throws Exception { runtimeHarness.deployContrib(RENDITION_CORE, RENDITION_FILTERS_COMPONENT_LOCATION); List<RenditionDefinition> availableRenditionDefinitions; Rendition rendition; // ----- Note DocumentModel doc = session.createDocumentModel("/", "note", "Note"); doc = session.createDocument(doc); availableRenditionDefinitions = renditionService.getAvailableRenditionDefinitions(doc); assertRenditionDefinitions(availableRenditionDefinitions, "renditionOnlyForNote", "zipExport"); rendition = renditionService.getRendition(doc, "renditionOnlyForNote", false); assertNotNull(rendition); // others are filtered out try { rendition = renditionService.getRendition(doc, "renditionOnlyForFile", false); fail(); } catch (NuxeoException e) { assertTrue(e.getMessage(), e.getMessage().contains("Rendition renditionOnlyForFile cannot be used")); } // ----- File doc = session.createDocumentModel("/", "file", "File"); doc = session.createDocument(doc); availableRenditionDefinitions = renditionService.getAvailableRenditionDefinitions(doc); assertRenditionDefinitions(availableRenditionDefinitions, "renditionOnlyForFile", "zipExport"); doc.setPropertyValue("dc:rights", "Unauthorized"); session.saveDocument(doc); availableRenditionDefinitions = renditionService.getAvailableRenditionDefinitions(doc); // renditionOnlyForFile filtered out, unauthorized assertRenditionDefinitions(availableRenditionDefinitions, "zipExport"); // ----- Folder doc = session.createDocumentModel("/", "folder", "Folder"); doc = session.createDocument(doc); availableRenditionDefinitions = renditionService.getAvailableRenditionDefinitions(doc); assertRenditionDefinitions(availableRenditionDefinitions, "renditionOnlyForFolder", "zipTreeExport", "zipTreeExportLazily"); runtimeHarness.undeployContrib(RENDITION_CORE, RENDITION_FILTERS_COMPONENT_LOCATION); } @Test public void shouldFilterRenditionDefinitionProviders() throws Exception { runtimeHarness.deployContrib(RENDITION_CORE, RENDITION_DEFINITION_PROVIDERS_COMPONENT_LOCATION); DocumentModel doc = session.createDocumentModel("/", "note", "Note"); doc = session.createDocument(doc); List<RenditionDefinition> availableRenditionDefinitions = renditionService.getAvailableRenditionDefinitions( doc); assertRenditionDefinitions(availableRenditionDefinitions, "dummyRendition1", "dummyRendition2", "zipExport"); doc = session.createDocumentModel("/", "file", "File"); doc = session.createDocument(doc); availableRenditionDefinitions = renditionService.getAvailableRenditionDefinitions(doc); assertRenditionDefinitions(availableRenditionDefinitions, "dummyRendition1", "dummyRendition2", "zipExport"); doc.setPropertyValue("dc:rights", "Unauthorized"); session.saveDocument(doc); availableRenditionDefinitions = renditionService.getAvailableRenditionDefinitions(doc); assertRenditionDefinitions(availableRenditionDefinitions, "zipExport"); doc = session.createDocumentModel("/", "folder", "Folder"); doc = session.createDocument(doc); availableRenditionDefinitions = renditionService.getAvailableRenditionDefinitions(doc); assertRenditionDefinitions(availableRenditionDefinitions, "dummyRendition1", "dummyRendition2", "zipTreeExport", "zipTreeExportLazily"); runtimeHarness.undeployContrib(RENDITION_CORE, RENDITION_DEFINITION_PROVIDERS_COMPONENT_LOCATION); } protected static void assertRenditionDefinitions(List<RenditionDefinition> actual, String... otherExpected) { List<String> expected = new ArrayList<>(Arrays.asList( // "delayedErrorAutomationRendition", // "iamlazy", // "lazyAutomation", // "lazyDelayedErrorAutomationRendition", // "renditionDefinitionWithCustomOperationChain", // "xmlExport")); if (otherExpected != null) { expected.addAll(Arrays.asList(otherExpected)); Collections.sort(expected); } assertEquals(expected, renditionNames(actual)); } protected static List<String> renditionNames(List<RenditionDefinition> list) { return list.stream().map(RenditionDefinition::getName).sorted().collect(Collectors.toList()); } }