/* * (C) Copyright 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 */ package org.nuxeo.ecm.core; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.IOException; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; import javax.inject.Inject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.nuxeo.ecm.core.api.AbstractSession; 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.PropertyException; import org.nuxeo.ecm.core.api.model.DeltaLong; import org.nuxeo.ecm.core.api.model.DocumentPart; import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; import org.nuxeo.ecm.core.api.model.impl.DocumentPartImpl; import org.nuxeo.ecm.core.model.Document; import org.nuxeo.ecm.core.model.Document.WriteContext; import org.nuxeo.ecm.core.model.Session; import org.nuxeo.ecm.core.schema.SchemaManager; import org.nuxeo.ecm.core.schema.types.CompositeType; import org.nuxeo.ecm.core.schema.types.Schema; 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.Features; import org.nuxeo.runtime.test.runner.FeaturesRunner; import org.nuxeo.runtime.test.runner.LocalDeploy; /** * Tests using the low-level Session / Document API. * <p> * It's important to test them as they may be used by user code for security policies or versioning service * contributions, or blob providers. * * @since 7.3 */ @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") public class TestDocument { @Inject protected CoreFeature coreFeature; @Inject protected CoreSession coreSession; @Inject protected SchemaManager schemaManager; protected Session session; @Before public void setUp() { session = ((AbstractSession) coreSession).getSession(); } protected void reopenSession() { coreSession = coreFeature.reopenCoreSession(); setUp(); } @FunctionalInterface private interface TriConsumer<T, U, V> { void accept(T t, U u, V v); } protected final BiFunction<Document, String, Object> DocumentGetValue = Document::getValue; protected final TriConsumer<Document, String, Object> DocumentSetValue = Document::setValue; @Test public void testGetValueErrors() throws Exception { Document root = session.getRootDocument(); Document doc = root.addChild("doc", "TestDocument"); tryUnknownProperty(xpath -> DocumentGetValue.apply(doc, xpath)); } @Test public void testSetValueErrors() throws Exception { Document root = session.getRootDocument(); Document doc = root.addChild("doc", "TestDocument"); tryUnknownProperty(xpath -> DocumentSetValue.accept(doc, xpath, null)); } protected void tryUnknownProperty(Consumer<String> c) { check(c, "nosuchprop", null); check(c, "tp:nosuchprop", null); check(c, "nosuchschema:nosuchprop", null); check(c, "tp:complexChain/nosuchprop", "Unknown segment: nosuchprop"); check(c, "tp:complexChain/complex/nosuchprop", "Unknown segment: nosuchprop"); check(c, "tp:complexChain/0", "Cannot use index after segment: tp:complexChain"); check(c, "tp:complexList/notaninteger/foo", "Missing list index after segment: tp:complexList"); check(c, "tp:complexList/0/foo", "Index out of bounds: 0"); check(c, "tp:stringArray/foo", "Segment must be last: tp:stringArray"); } protected void check(Consumer<String> c, String xpath, String detail) { try { c.accept(xpath); fail(); } catch (PropertyNotFoundException e) { assertEquals(xpath, e.getPath()); assertEquals(detail, e.getDetail()); } } @Test public void testSetValueErrors2() throws Exception { Document root = session.getRootDocument(); Document doc1 = root.addChild("doc", "TestDocument"); Document doc2 = root.addChild("doc", "File"); doc1.setValue("tp:complexList", Collections.singletonList(Collections.emptyMap())); BiConsumer<String, Object> c1 = (xpath, value) -> DocumentSetValue.accept(doc1, xpath, value); checkSet(c1, "tp:complexList", Long.valueOf(0), "Expected List value for: tp:complexList, got java.lang.Long instead"); checkSet(c1, "tp:complexList/0", Long.valueOf(0), "Expected Map value for: tp:complexList/0, got java.lang.Long instead"); checkSet(c1, "tp:complexList/0", Collections.singletonMap("foo", null), "Unknown key: foo for tp:complexList/0"); BiConsumer<String, Object> c2 = (xpath, value) -> DocumentSetValue.accept(doc2, xpath, value); checkSet(c2, "content", Long.valueOf(0), "Expected Blob value for: content, got java.lang.Long instead"); } protected void checkSet(BiConsumer<String, Object> c, String xpath, Object value, String message) { try { c.accept(xpath, value); fail(); } catch (PropertyException e) { assertEquals(message, e.getMessage()); } } @Test public void testSimple() throws Exception { Document root = session.getRootDocument(); Document doc = root.addChild("doc", "File"); // basic property doc.setValue("dc:title", "title"); assertEquals("title", doc.getValue("dc:title")); // array doc.setValue("dc:subjects", new Object[] { "a", "b" }); assertEquals(Arrays.asList("a", "b"), Arrays.asList((Object[]) doc.getValue("dc:subjects"))); doc.setValue("dc:subjects", Arrays.asList("c", "d")); assertEquals(Arrays.asList("c", "d"), Arrays.asList((Object[]) doc.getValue("dc:subjects"))); // blob Blob blob = Blobs.createBlob("hello world!"); doc.setValue("content", blob); blob = (Blob) doc.getValue("content"); assertEquals("hello world!", blob.getString()); } @Test public void testComplexUnset() throws Exception { Document root = session.getRootDocument(); Document doc = root.addChild("doc", "TestDocument"); assertNull(doc.getValue("tp:complexChain/string")); } @Test public void testComplex() throws Exception { Document root = session.getRootDocument(); Document doc = root.addChild("doc", "ComplexDoc"); String content2 = "My content 2"; Blob blob = Blobs.createBlob("My content"); Blob blob2 = Blobs.createBlob(content2); Long size1 = Long.valueOf(123); Long size2 = Long.valueOf(456); Long size3 = Long.valueOf(789); Map<String, Object> attachedFile = new HashMap<>(); List<Map<String, Object>> vignettes = new ArrayList<>(); attachedFile.put("vignettes", vignettes); Map<String, Object> vignette = new HashMap<>(); vignette.put("width", size1); vignette.put("content", blob); vignettes.add(vignette); // 0 vignettes.add(Collections.singletonMap("height", size3)); // 1 vignettes.add(Collections.singletonMap("height", size3)); // 2 vignettes.add(Collections.singletonMap("height", size3)); // 3 // set recursive doc.setValue("cmpf:attachedFile", attachedFile); // set deep doc.setValue("cmpf:attachedFile/vignettes/0/content", blob2); doc.setValue("cmpf:attachedFile/vignettes/0/content/mime-type", "text/foo"); doc.setValue("cmpf:attachedFile/vignettes/1/width", size2); doc.setValue("cmpf:attachedFile/vignettes/vignette[1]/width", size2); // non-canonical xpath doc.setValue("cmpf:attachedFile/vignettes/2", new HashMap<>()); // overwrite doc.setValue("cmpf:attachedFile/vignettes/3", null); // overwrite // get deep assertEquals("text/foo", doc.getValue("cmpf:attachedFile/vignettes/0/content/mime-type")); assertEquals(size1, doc.getValue("cmpf:attachedFile/vignettes/0/width")); assertEquals(size2, doc.getValue("cmpf:attachedFile/vignettes/1/width")); assertEquals(size3, doc.getValue("cmpf:attachedFile/vignettes/1/height")); assertNull(doc.getValue("cmpf:attachedFile/vignettes/2/height")); // was overwritten assertNull(doc.getValue("cmpf:attachedFile/vignettes/3/height")); // was overwritten // get recursive blob Object b = doc.getValue("cmpf:attachedFile/vignettes/0/content"); assertTrue(b instanceof Blob); assertEquals(content2, ((Blob) b).getString()); // get recursive list item @SuppressWarnings("unchecked") Map<String, Object> v0 = (Map<String, Object>) doc.getValue("cmpf:attachedFile/vignettes/0"); assertEquals(size1, v0.get("width")); b = v0.get("content"); assertEquals(content2, ((Blob) b).getString()); Object v1 = doc.getValue("cmpf:attachedFile/vignettes/1"); Map<String, Object> ev1 = new HashMap<>(); ev1.put("width", size2); ev1.put("height", size3); ev1.put("label", null); ev1.put("content", null); assertEquals(ev1, v1); Object v2 = doc.getValue("cmpf:attachedFile/vignettes/2"); Map<String, Object> ev2or3 = new HashMap<>(); ev2or3.put("width", null); ev2or3.put("height", null); ev2or3.put("label", null); ev2or3.put("content", null); assertEquals(ev2or3, v2); Object v3 = doc.getValue("cmpf:attachedFile/vignettes/3"); assertEquals(ev2or3, v3); // get recursive list Object list = doc.getValue("cmpf:attachedFile/vignettes"); assertTrue(list instanceof List); assertEquals(4, ((List<?>) list).size()); assertEquals(Arrays.asList(v0, ev1, ev2or3, ev2or3), list); // get recursive map Map<String, Object> atf = new HashMap<>(); atf.put("name", null); atf.put("vignettes", Arrays.asList(v0, ev1, ev2or3, ev2or3)); assertEquals(atf, doc.getValue("cmpf:attachedFile")); } @Test public void testComplexFiles() throws Exception { Document root = session.getRootDocument(); Document doc = root.addChild("doc", "File"); Blob blob = Blobs.createBlob("My content"); doc.setValue("files", Collections.singletonList(Collections.singletonMap("file", blob))); assertEquals(blob, doc.getValue("files/0/file")); } @Test public void testBlobList() throws Exception { Document root = session.getRootDocument(); Document doc = root.addChild("doc", "TestDocument"); Object list = doc.getValue("tp:fileList"); assertTrue(list instanceof List); assertEquals(0, ((List<?>) list).size()); doc.setValue("tp:fileList", Collections.singletonList(Blobs.createBlob("My content"))); list = doc.getValue("tp:fileList"); assertTrue(list instanceof List); @SuppressWarnings("unchecked") List<Blob> blobs = (List<Blob>) list; assertEquals(1, blobs.size()); assertEquals("My content", blobs.get(0).getString()); } @Test public void testFacet() throws Exception { Document root = session.getRootDocument(); Document doc = root.addChild("doc", "File"); try { doc.getValue("age:age"); fail(); } catch (PropertyNotFoundException e) { assertEquals("age:age", e.getPath()); assertNull(e.getDetail()); } try { doc.setValue("age:age", "123"); fail(); } catch (PropertyNotFoundException e) { assertEquals("age:age", e.getPath()); assertNull(e.getDetail()); } doc.addFacet("Aged"); doc.setValue("age:age", "123"); assertEquals("123", doc.getValue("age:age")); } @Test public void testProxySchema() throws Exception { Document root = session.getRootDocument(); Document doc = root.addChild("doc", "File"); Document proxy = session.createProxy(doc, root); session.save(); try { doc.getValue("info:info"); } catch (PropertyNotFoundException e) { assertEquals("info:info", e.getPath()); assertNull(e.getDetail()); } try { doc.setValue("info:info", "docinfo"); } catch (PropertyNotFoundException e) { assertEquals("info:info", e.getPath()); assertNull(e.getDetail()); } assertNull(proxy.getValue("info:info")); proxy.setValue("info:info", "proxyinfo"); session.save(); assertEquals("proxyinfo", proxy.getValue("info:info")); } @Test public void testBlobsVisitor() throws Exception { Document root = session.getRootDocument(); Document doc = root.addChild("doc", "ComplexDoc"); Blob blob1 = Blobs.createBlob("content1", "text/plain"); Blob blob2 = Blobs.createBlob("content2", "text/html"); List<Map<String, Object>> vignettes = new ArrayList<>(); vignettes.add(Collections.singletonMap("content", blob1)); vignettes.add(Collections.singletonMap("content", blob2)); Map<String, Object> attachedFile = new HashMap<>(); attachedFile.put("vignettes", vignettes); doc.setValue("cmpf:attachedFile", attachedFile); // list the paths List<String> paths = new ArrayList<>(); doc.visitBlobs(accessor -> paths.add(accessor.getXPath())); assertEquals(Arrays.asList("cmpf:attachedFile/vignettes/0/content", "cmpf:attachedFile/vignettes/1/content"), paths); // get the MIME types List<String> mimeTypes = new ArrayList<>(); doc.visitBlobs(accessor -> mimeTypes.add(accessor.getBlob().getMimeType())); assertEquals(Arrays.asList("text/plain", "text/html"), mimeTypes); // set the file names doc.visitBlobs(accessor -> { Blob blob = accessor.getBlob(); blob.setFilename("myfile-" + blob.getMimeType()); accessor.setBlob(blob); }); assertEquals("myfile-text/plain", doc.getValue("cmpf:attachedFile/vignettes/0/content/name")); assertEquals("myfile-text/html", doc.getValue("cmpf:attachedFile/vignettes/1/content/name")); // upload new blobs doc.visitBlobs(accessor -> { try { String c = accessor.getBlob().getString(); accessor.setBlob(Blobs.createBlob(c + "-updated")); } catch (IOException e) { throw new RuntimeException(e); } }); Blob b1 = (Blob) doc.getValue("cmpf:attachedFile/vignettes/0/content"); assertEquals("content1-updated", b1.getString()); Blob b2 = (Blob) doc.getValue("cmpf:attachedFile/vignettes/1/content"); assertEquals("content2-updated", b2.getString()); } @Test public void testBlobsVisitorWithOldFacet() throws Exception { Document root = session.getRootDocument(); Document doc = root.addChild("doc", "ComplexDoc"); doc.addFacet("Aged"); Blob blob = Blobs.createBlob("content1", "text/plain"); doc.setValue("cmpf:attachedFile", Collections.singletonMap("vignettes", Collections.singletonList(Collections.singletonMap("content", blob)))); // simulate an obsolete Aged facet present on the document but not in the schema manager Map<String, CompositeType> facets = getSchemaManagerFacets(); CompositeType agedFacet = facets.remove("Aged"); try { // list the paths List<String> paths = new ArrayList<>(); doc.visitBlobs(accessor -> paths.add(accessor.getXPath())); assertEquals(Arrays.asList("cmpf:attachedFile/vignettes/0/content"), paths); } finally { facets.put("Aged", agedFacet); } } /** Gets the facets internal datastructure from the schema manager. */ protected Map<String, CompositeType> getSchemaManagerFacets() throws Exception { Field field = schemaManager.getClass().getDeclaredField("facets"); field.setAccessible(true); return (Map<String, CompositeType>) field.get(schemaManager); } @Test public void testGetChanges() throws Exception { Document root = session.getRootDocument(); Document doc = root.addChild("doc", "ComplexDoc"); Blob blob1 = Blobs.createBlob("My content"); Blob blob2 = Blobs.createBlob("My content 2"); Long size1 = Long.valueOf(123); Long size2 = Long.valueOf(456); Map<String, Object> attachedFile = new HashMap<>(); List<Map<String, Object>> vignettes = new ArrayList<>(); attachedFile.put("vignettes", vignettes); Map<String, Object> vignette = new HashMap<>(); vignette.put("width", size1); vignette.put("content", blob1); vignettes.add(vignette); // 0 vignette = new HashMap<>(); vignette.put("width", size2); vignette.put("content", blob2); vignettes.add(vignette); // 1 doc.setValue("cmpf:attachedFile", attachedFile); // write changes through a Property // change to dc:title Schema schema = doc.getType().getSchema("dublincore"); DocumentPart dp = new DocumentPartImpl(schema); dp.setValue("dc:title", "foo"); WriteContext writeContext = doc.getWriteContext(); boolean changed = doc.writeDocumentPart(dp, writeContext); assertTrue(changed); Set<String> changes = writeContext.getChanges(); assertEquals(Collections.singleton("dc:title"), changes); // change to complex prop schema = doc.getType().getSchema("complexschema"); dp = new DocumentPartImpl(schema); doc.readDocumentPart(dp); // read whole state to get existing values, needed for list dp.setValue("cmpf:attachedFile/vignettes/item[1]/width", Long.valueOf(789)); writeContext = doc.getWriteContext(); changed = doc.writeDocumentPart(dp, writeContext); assertTrue(changed); changes = writeContext.getChanges(); // check that we don't have cmpf:attachedFile/vignettes/0 in the list assertEquals(new HashSet<>(Arrays.asList("cmpf:attachedFile", "cmpf:attachedFile/vignettes", "cmpf:attachedFile/vignettes/1", "cmpf:attachedFile/vignettes/1/width")), changes); // change to blob dp = new DocumentPartImpl(schema); doc.readDocumentPart(dp); // read whole state to get existing values, needed for list dp.setValue("cmpf:attachedFile/vignettes/item[1]/content", blob1); writeContext = doc.getWriteContext(); changed = doc.writeDocumentPart(dp, writeContext); assertTrue(changed); changes = writeContext.getChanges(); // check that we don't have cmpf:attachedFile/vignettes/0 in the list assertEquals(new HashSet<>(Arrays.asList("cmpf:attachedFile", "cmpf:attachedFile/vignettes", "cmpf:attachedFile/vignettes/1", "cmpf:attachedFile/vignettes/1/content")), changes); } @Test public void testDeltaAfterPhantomNull() throws Exception { Document root = session.getRootDocument(); Document doc = root.addChild("doc", "MyDocType"); // change to dc:title Schema schema = doc.getType().getSchema("myschema"); DocumentPart dp = new DocumentPartImpl(schema); doc.readDocumentPart(dp); // change unrelated prop, it should initialize all phantom properties to non-null as well dp.setValue("my:string", "foo"); assertTrue(dp.get("my:testDefaultLong").isPhantom()); WriteContext writeContext = doc.getWriteContext(); doc.writeDocumentPart(dp, writeContext); session.save(); // then write a delta, the database-level increment must work on 0 and not null dp.setValue("my:testDefaultLong", DeltaLong.valueOf(Long.valueOf(0), 10)); writeContext = doc.getWriteContext(); doc.writeDocumentPart(dp, writeContext); reopenSession(); root = session.getRootDocument(); doc = root.getChild("doc"); assertEquals(Long.valueOf(10), doc.getValue("my:testDefaultLong")); } }