| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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.apache.jackrabbit.oak.plugins.document; |
| |
| 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 static org.junit.Assert.fail; |
| import static org.junit.Assume.assumeNotNull; |
| import static org.junit.Assume.assumeTrue; |
| |
| import java.io.UnsupportedEncodingException; |
| import java.nio.charset.Charset; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.UUID; |
| |
| import org.apache.jackrabbit.oak.plugins.document.util.Utils; |
| import org.junit.Test; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import com.google.common.collect.ContiguousSet; |
| import com.google.common.collect.DiscreteDomain; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Range; |
| |
| public class BasicDocumentStoreTest extends AbstractDocumentStoreTest { |
| |
| private static final Logger LOG = LoggerFactory.getLogger(BasicDocumentStoreTest.class); |
| |
| public BasicDocumentStoreTest(DocumentStoreFixture dsf) { |
| super(dsf); |
| } |
| |
| @Test |
| public void testAddAndRemove() { |
| String id = this.getClass().getName() + ".testAddAndRemove"; |
| |
| // remove if present |
| NodeDocument nd = super.ds.find(Collection.NODES, id); |
| if (nd != null) { |
| super.ds.remove(Collection.NODES, id); |
| } |
| |
| // add |
| UpdateOp up = new UpdateOp(id, true); |
| assertTrue(super.ds.create(Collection.NODES, Collections.singletonList(up))); |
| super.ds.invalidateCache(); |
| assertNotNull(super.ds.find(Collection.NODES, id)); |
| removeMe.add(id); |
| } |
| |
| @Test |
| public void testAddAndRemoveWithoutIdInUpdateOp() { |
| String id = this.getClass().getName() + ".testAddAndRemoveWithoutIdInUpdateOp"; |
| |
| // remove if present |
| NodeDocument nd = super.ds.find(Collection.NODES, id); |
| if (nd != null) { |
| super.ds.remove(Collection.NODES, id); |
| } |
| |
| // add |
| UpdateOp up = new UpdateOp(id, true); |
| assertTrue(super.ds.create(Collection.NODES, Collections.singletonList(up))); |
| super.ds.invalidateCache(); |
| assertNotNull(super.ds.find(Collection.NODES, id)); |
| removeMe.add(id); |
| } |
| |
| @Test |
| public void testValuesForSystemProps() { |
| String id = this.getClass().getName() + ".testValuesForSystemProps"; |
| |
| // remove if present |
| NodeDocument nd = super.ds.find(Collection.NODES, id); |
| if (nd != null) { |
| super.ds.remove(Collection.NODES, id); |
| } |
| removeMe.add(id); |
| |
| // add |
| UpdateOp up = new UpdateOp(id, true); |
| assertTrue(super.ds.create(Collection.NODES, Collections.singletonList(up))); |
| |
| super.ds.invalidateCache(); |
| nd = super.ds.find(Collection.NODES, id, 0); |
| assertNull(nd.get(NodeDocument.DELETED_ONCE)); |
| assertNull(nd.get(NodeDocument.HAS_BINARY_FLAG)); |
| assertFalse(nd.wasDeletedOnce()); |
| assertFalse(nd.hasBinary()); |
| |
| up = new UpdateOp(id, false); |
| up.set(NodeDocument.DELETED_ONCE, true); |
| super.ds.findAndUpdate(Collection.NODES, up); |
| |
| super.ds.invalidateCache(); |
| nd = super.ds.find(Collection.NODES, id, 0); |
| assertEquals(true, nd.get(NodeDocument.DELETED_ONCE)); |
| assertNull(nd.get(NodeDocument.HAS_BINARY_FLAG)); |
| assertTrue(nd.wasDeletedOnce()); |
| assertFalse(nd.hasBinary()); |
| |
| up = new UpdateOp(id, false); |
| up.set(NodeDocument.DELETED_ONCE, false); |
| up.set(NodeDocument.HAS_BINARY_FLAG, NodeDocument.HAS_BINARY_VAL); |
| super.ds.findAndUpdate(Collection.NODES, up); |
| |
| super.ds.invalidateCache(); |
| nd = super.ds.find(Collection.NODES, id, 0); |
| assertEquals(false, nd.get(NodeDocument.DELETED_ONCE)); |
| assertEquals(NodeDocument.HAS_BINARY_VAL, nd.get(NodeDocument.HAS_BINARY_FLAG)); |
| assertFalse(nd.wasDeletedOnce()); |
| assertTrue(nd.hasBinary()); |
| |
| // remove |
| up = new UpdateOp(id, false); |
| up.remove(NodeDocument.DELETED_ONCE); |
| up.remove(NodeDocument.HAS_BINARY_FLAG); |
| super.ds.findAndUpdate(Collection.NODES, up); |
| |
| super.ds.invalidateCache(); |
| nd = super.ds.find(Collection.NODES, id, 0); |
| assertNull(nd.get(NodeDocument.DELETED_ONCE)); |
| assertNull(nd.get(NodeDocument.HAS_BINARY_FLAG)); |
| } |
| |
| @Test |
| public void testSetId() { |
| String id = this.getClass().getName() + ".testSetId"; |
| |
| UpdateOp up = new UpdateOp(id, true); |
| try { |
| up.set("_id", id); |
| } catch (IllegalArgumentException expected) { |
| } |
| } |
| |
| @Test |
| public void testCreateOrUpdate() { |
| String id = this.getClass().getName() + ".testCreateOrUpdate"; |
| |
| super.ds.remove(Collection.NODES, id); |
| |
| // create |
| UpdateOp up = new UpdateOp(id, true); |
| assertNull(super.ds.createOrUpdate(Collection.NODES, up)); |
| |
| // update |
| up = new UpdateOp(id, true); |
| assertNotNull(super.ds.createOrUpdate(Collection.NODES, up)); |
| removeMe.add(id); |
| } |
| |
| @Test |
| public void testCreateOrUpdateIsNewFalse() { |
| String id = this.getClass().getName() + ".testCreateOrUpdateIsNewFalse"; |
| |
| super.ds.remove(Collection.NODES, id); |
| |
| // create with isNew==false |
| UpdateOp up = new UpdateOp(id, false); |
| assertNull(super.ds.createOrUpdate(Collection.NODES, up)); |
| |
| // has the document been created? |
| NodeDocument nd = super.ds.find(Collection.NODES, id); |
| assertNull(nd); |
| } |
| |
| @Test |
| public void testCreateOrUpdateWithoutIdInUpdateOp() { |
| String id = this.getClass().getName() + ".testCreateOrUpdateWithoutIdInUpdateOp"; |
| |
| // remove if present |
| NodeDocument nd = super.ds.find(Collection.NODES, id); |
| if (nd != null) { |
| super.ds.remove(Collection.NODES, id); |
| } |
| |
| // create |
| UpdateOp up = new UpdateOp(id, true); |
| assertNull(super.ds.createOrUpdate(Collection.NODES, up)); |
| |
| // update |
| up = new UpdateOp(id, true); |
| assertNotNull(super.ds.createOrUpdate(Collection.NODES, up)); |
| removeMe.add(id); |
| } |
| |
| @Test |
| public void testAddAndRemoveJournalEntry() { |
| // OAK-4021 |
| String id = this.getClass().getName() + ".testAddAndRemoveJournalEntry"; |
| |
| // remove if present |
| Document d = super.ds.find(Collection.JOURNAL, id); |
| if (d != null) { |
| super.ds.remove(Collection.JOURNAL, id); |
| } |
| |
| // add |
| UpdateOp up = new UpdateOp(id, true); |
| assertTrue(super.ds.create(Collection.JOURNAL, Collections.singletonList(up))); |
| } |
| |
| @Test |
| public void testConditionalUpdate() { |
| String id = this.getClass().getName() + ".testConditionalUpdate"; |
| |
| // remove if present |
| NodeDocument nd = super.ds.find(Collection.NODES, id); |
| if (nd != null) { |
| super.ds.remove(Collection.NODES, id); |
| } |
| |
| String existingProp = "_recoverylock"; |
| String existingRevisionProp = "recoverylock"; |
| String nonExistingProp = "_qux"; |
| String nonExistingRevisionProp = "qux"; |
| Revision r = new Revision(1, 1, 1); |
| |
| // add |
| UpdateOp up = new UpdateOp(id, true); |
| up.set(existingProp, "lock"); |
| up.setMapEntry(existingRevisionProp, r, "lock"); |
| assertTrue(super.ds.create(Collection.NODES, Collections.singletonList(up))); |
| |
| // updates |
| up = new UpdateOp(id, false); |
| up.notEquals(nonExistingProp, "none"); |
| NodeDocument result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNotNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.equals(nonExistingProp, null); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNotNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.notEquals(nonExistingRevisionProp, r, "none"); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNotNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.equals(nonExistingRevisionProp, r, null); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNotNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.equals(existingProp, "none"); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.equals(existingProp, null); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.equals(existingRevisionProp, r, "none"); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.equals(existingRevisionProp, r, null); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.notEquals(existingProp, "lock"); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.equals(existingProp, null); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.notEquals(existingRevisionProp, r, "lock"); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.equals(existingRevisionProp, r, null); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.equals(existingProp, "lock"); |
| up.set(existingProp, "none"); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNotNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.equals(existingRevisionProp, r, "lock"); |
| up.setMapEntry(existingRevisionProp, r, "none"); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNotNull(result); |
| |
| // OAK-6812 |
| up = new UpdateOp(id, false); |
| up.contains(existingProp, true); |
| up.set(existingProp, "none"); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNotNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.containsMapEntry(existingRevisionProp, r, true); |
| up.setMapEntry(existingRevisionProp, r, "none"); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNotNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.contains(nonExistingProp, false); |
| up.set(existingProp, "none"); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNotNull(result); |
| |
| up = new UpdateOp(id, false); |
| up.containsMapEntry(nonExistingRevisionProp, r, false); |
| up.setMapEntry(existingRevisionProp, r, "none"); |
| result = super.ds.findAndUpdate(Collection.NODES, up); |
| assertNotNull(result); |
| |
| removeMe.add(id); |
| } |
| |
| @Test |
| public void testConditionalUpdateForbidden() { |
| String id = this.getClass().getName() + ".testConditionalupdateForbidden"; |
| |
| // remove if present |
| NodeDocument nd = super.ds.find(Collection.NODES, id); |
| if (nd != null) { |
| super.ds.remove(Collection.NODES, id); |
| } |
| |
| try { |
| UpdateOp up = new UpdateOp(id, true); |
| up.equals("foo", "bar"); |
| super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| fail("conditional create should fail"); |
| } |
| catch (IllegalStateException expected) { |
| // reported by UpdateOp |
| } |
| |
| UpdateOp cup = new UpdateOp(id, true); |
| assertTrue(super.ds.create(Collection.NODES, Collections.singletonList(cup))); |
| removeMe.add(id); |
| |
| try { |
| UpdateOp up = new UpdateOp(id, false); |
| up.equals("foo", "bar"); |
| super.ds.createOrUpdate(Collection.NODES, up); |
| fail("conditional createOrUpdate should fail"); |
| } |
| catch (IllegalArgumentException expected) { |
| // reported by DocumentStore |
| } |
| |
| try { |
| UpdateOp up = new UpdateOp(id, false); |
| up.equals("foo", "bar"); |
| super.ds.createOrUpdate(Collection.NODES, Collections.singletonList(up)); |
| fail("conditional createOrUpdate should fail"); |
| } |
| catch (IllegalArgumentException expected) { |
| // reported by DocumentStore |
| } |
| } |
| |
| @Test |
| public void testMaxIdAscii() { |
| int result = testMaxId(true); |
| assertTrue("needs to support keys of 512 bytes length, but only supports " + result, result >= 512); |
| } |
| |
| @Test |
| public void testMaxIdNonAscii() { |
| testMaxId(false); |
| } |
| |
| @Test |
| public void testLongId() { |
| String id = "0:/" + generateId(2048, true); |
| assertNull("find() with ultra-long id needs to return 'null'", super.ds.find(Collection.NODES, id)); |
| |
| if (! super.dsname.contains("Memory")) { |
| UpdateOp up = new UpdateOp(id, true); |
| assertFalse("create() with ultra-long id needs to fail", super.ds.create(Collection.NODES, Collections.singletonList(up))); |
| } |
| } |
| |
| //OAK-3001 |
| @Test |
| public void testRangeRemove() { |
| String idPrefix = this.getClass().getName() + ".testRangeRemove"; |
| |
| com.google.common.collect.Range<Long> modTimes = Range.closed(1L, 30L); |
| for (Long modTime : ContiguousSet.create(modTimes, DiscreteDomain.longs())) { |
| String id = idPrefix + modTime; |
| // remove if present |
| Document d = super.ds.find(Collection.JOURNAL, id); |
| if (d != null) { |
| super.ds.remove(Collection.JOURNAL, id); |
| } |
| |
| // add |
| UpdateOp up = new UpdateOp(id, true); |
| up.set("_modified", modTime); |
| super.ds.create(Collection.JOURNAL, Collections.singletonList(up)); |
| removeMeJournal.add(id); |
| } |
| |
| assertEquals("Number of entries removed didn't match", 3, |
| ds.remove(Collection.JOURNAL, "_modified", 20, 24)); |
| |
| assertEquals("Number of entries removed didn't match", 0, |
| ds.remove(Collection.JOURNAL, "_modified", 20, 24)); |
| |
| assertEquals("Number of entries removed didn't match", 4, |
| ds.remove(Collection.JOURNAL, "_modified", -1, 5)); |
| |
| assertEquals("Number of entries removed didn't match", 5, |
| ds.remove(Collection.JOURNAL, "_modified", 0, 10)); |
| |
| // interesting cases |
| assertEquals("Number of entries removed didn't match", 0, |
| ds.remove(Collection.JOURNAL, "_modified", 20, 19)); |
| |
| assertEquals("Number of entries removed didn't match", 0, |
| ds.remove(Collection.JOURNAL, "_modified", 31, 40)); |
| |
| assertEquals("Number of entries removed didn't match", 2, |
| ds.remove(Collection.JOURNAL, "_modified", 28, 40)); |
| } |
| |
| private int testMaxId(boolean ascii) { |
| int min = 0; |
| int max = 32768; |
| int test = 0; |
| int last = 0; |
| |
| while (max - min >= 2) { |
| test = (max + min) / 2; |
| String id = generateId(test, ascii); |
| // make sure it's gone before trying to create it |
| try { |
| super.ds.remove(Collection.NODES, id); |
| } catch (DocumentStoreException ignored) { |
| } |
| UpdateOp up = new UpdateOp(id, true); |
| boolean success = super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| if (success) { |
| // check that we really can read it |
| NodeDocument findme = super.ds.find(Collection.NODES, id, 0); |
| assertNotNull("failed to retrieve previously stored document", findme); |
| assertEquals(id, findme.getId()); |
| super.ds.remove(Collection.NODES, id); |
| min = test; |
| last = test; |
| } else { |
| max = test; |
| } |
| } |
| |
| LOG.info("max " + (ascii ? "ASCII ('0')" : "non-ASCII (U+1F4A9)") + " id length for " + super.dsname + " was " + last); |
| return last; |
| } |
| |
| @Test |
| public void testMaxProperty() { |
| int min = 0; |
| int max = 1024 * 1024 * 8; |
| int test = 0; |
| int last = 0; |
| |
| while (max - min >= 256) { |
| if (test == 0) { |
| test = max; // try largest first |
| } else { |
| test = (max + min) / 2; |
| } |
| String id = this.getClass().getName() + ".testMaxProperty-" + test; |
| String pval = generateString(test, true); |
| UpdateOp up = new UpdateOp(id, true); |
| up.set("foo", pval); |
| boolean success = super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| if (success) { |
| // check that we really can read it |
| NodeDocument findme = super.ds.find(Collection.NODES, id, 0); |
| assertNotNull("failed to retrieve previously stored document", findme); |
| super.ds.remove(Collection.NODES, id); |
| min = test; |
| last = test; |
| } else { |
| max = test; |
| } |
| } |
| |
| LOG.info("max prop length for " + super.dsname + " was " + last); |
| } |
| |
| @Test |
| public void testInterestingPropLengths() throws UnsupportedEncodingException { |
| int lengths[] = { 1, 10, 100, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 11000, 12000, 13000, 14000, |
| 15000, 16000, 20000 }; |
| |
| for (int test : lengths) { |
| String id = this.getClass().getName() + ".testInterestingPropLengths-" + test; |
| String pval = generateString(test, true); |
| UpdateOp up = new UpdateOp(id, true); |
| up.set("foo", pval); |
| super.ds.remove(Collection.NODES, id); |
| boolean success = super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| assertTrue("failed to insert a document with property of length " + test + "(ASCII) in " + super.dsname, success); |
| super.ds.remove(Collection.NODES, id); |
| } |
| |
| for (int test : lengths) { |
| String id = this.getClass().getName() + ".testInterestingPropLengths-" + test; |
| String pval = generateString(test, false); |
| UpdateOp up = new UpdateOp(id, true); |
| up.set("foo", pval); |
| super.ds.remove(Collection.NODES, id); |
| boolean success = super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| assertTrue("failed to insert a document with property of length " + test |
| + " (potentially non-ASCII, actual octet length with UTF-8 encoding: " + pval.getBytes("UTF-8").length + ") in " |
| + super.dsname, success); |
| // check that update works as well |
| if (success) { |
| try { |
| super.ds.findAndUpdate(Collection.NODES, up); |
| } catch (Exception ex) { |
| ex.printStackTrace(System.err); |
| fail("failed to update a document with property of length " + test |
| + " (potentially non-ASCII, actual octet length with UTF-8 encoding: " + pval.getBytes("UTF-8").length + ") in " |
| + super.dsname); |
| } |
| } |
| super.ds.remove(Collection.NODES, id); |
| } |
| } |
| |
| @Test |
| public void testModifiedMaxUpdateQuery() { |
| String id = this.getClass().getName() + ".testModifiedMaxUpdate"; |
| // create a test node |
| UpdateOp up = new UpdateOp(id, true); |
| up.set("_modified", 1000L); |
| boolean success = super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| assertTrue(success); |
| removeMe.add(id); |
| |
| // update with smaller _modified |
| UpdateOp up2 = new UpdateOp(id, true); |
| up2.max("_modified", 100L); |
| super.ds.findAndUpdate(Collection.NODES, up2); |
| |
| super.ds.invalidateCache(); |
| |
| // this should find the document; will fail if the MAX operation wasn't applied to the indexed property |
| String startId = this.getClass().getName() + ".testModifiedMaxUpdatd"; |
| String endId = this.getClass().getName() + ".testModifiedMaxUpdatf"; |
| List<NodeDocument> results = super.ds.query(Collection.NODES, startId, endId, "_modified", 1000, 1); |
| assertEquals("document not found, maybe indexed _modified property not properly updated", 1, results.size()); |
| } |
| |
| @Test |
| public void testModifiedMaxUpdateQuery2() { |
| // test for https://issues.apache.org/jira/browse/OAK-4388 |
| String id = this.getClass().getName() + ".testModifiedMaxUpdate2"; |
| // create a test node |
| UpdateOp up = new UpdateOp(id, true); |
| up.set("_modified", 1000L); |
| boolean success = super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| assertTrue(success); |
| removeMe.add(id); |
| |
| for (int i = 0; i < 25; i++) { |
| // update with smaller _modified |
| UpdateOp up2 = new UpdateOp(id, true); |
| up2.max("_modified", 100L); |
| super.ds.findAndUpdate(Collection.NODES, up2); |
| super.ds.invalidateCache(); |
| NodeDocument doc = super.ds.find(Collection.NODES, id, 0); |
| assertEquals("modified should not have been set back (test iteration " + i + ")", 1000, (long)doc.getModified()); |
| } |
| } |
| |
| @Test |
| public void testModifyDeletedOnce() { |
| // https://issues.apache.org/jira/browse/OAK-3852 |
| String id = this.getClass().getName() + ".testModifyDeletedOnce"; |
| // create a test node |
| UpdateOp up = new UpdateOp(id, true); |
| up.set(NodeDocument.DELETED_ONCE, Boolean.FALSE); |
| boolean success = super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| assertTrue(success); |
| removeMe.add(id); |
| NodeDocument nd = super.ds.find(Collection.NODES, id, 0); |
| assertNotNull(nd); |
| Boolean dovalue = (Boolean)nd.get(NodeDocument.DELETED_ONCE); |
| if (dovalue != null) { |
| // RDB persistence does not distinguish null and false |
| assertEquals(dovalue.booleanValue(), Boolean.FALSE); |
| } |
| } |
| |
| @Test |
| public void testInterestingStrings() { |
| // https://jira.mongodb.org/browse/JAVA-1305 |
| boolean repoUsesBadUnicodeAPI = dsf instanceof DocumentStoreFixture.MongoFixture; |
| |
| String[] tests = new String[] { "simple:foo", "cr:a\n\b", "dquote:a\"b", "bs:a\\b", "euro:a\u201c", "gclef:\uD834\uDD1E", |
| "tab:a\tb", "nul:a\u0000b", "brokensurrogate:\ud800" }; |
| |
| for (String t : tests) { |
| boolean roundTrips = roundtripsThroughJavaUTF8(t); |
| if (!roundTrips && repoUsesBadUnicodeAPI) { |
| // skip the test because it will fail, see OAK-3683 |
| break; |
| } |
| |
| int pos = t.indexOf(":"); |
| String testname = t.substring(0, pos); |
| String test = t.substring(pos + 1); |
| String id = this.getClass().getName() + ".testInterestingStrings-" + testname; |
| super.ds.remove(Collection.NODES, id); |
| UpdateOp up = new UpdateOp(id, true); |
| up.set("foo", test); |
| boolean success = super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| assertTrue("failed to insert a document with property value of " + test + " (" + testname + ") in " + super.dsname |
| + " (JDK roundtripping: " + roundTrips + ")", success); |
| // re-read from persistence |
| super.ds.invalidateCache(); |
| NodeDocument nd = super.ds.find(Collection.NODES, id); |
| assertEquals( |
| "failure to round-trip " + testname + " through " + super.dsname + " (JDK roundtripping: " + roundTrips + ")", |
| test, nd.get("foo")); |
| super.ds.remove(Collection.NODES, id); |
| } |
| } |
| |
| @Test |
| public void testInterestingValidIds() { |
| // OAK-7261 |
| String[] tests = new String[] { "simple:foo", "cr:a\n\b", "dquote:a\"b", "bs:a\\b", "euro:a\u201c", "gclef:\uD834\uDD1E", |
| "tab:a\tb" }; |
| |
| for (String t : tests) { |
| int pos = t.indexOf(":"); |
| String test = t.substring(pos + 1); |
| String id = Utils.getIdFromPath("/" + this.getClass().getName() + ".testInterestingValidIds-" + test); |
| super.ds.remove(Collection.NODES, id); |
| UpdateOp up = new UpdateOp(id, true); |
| boolean success = super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| assertTrue("failed to insert a document with id '" + id + "' in " + super.dsname, success); |
| // re-read from persistence |
| super.ds.invalidateCache(); |
| NodeDocument nd = super.ds.find(Collection.NODES, id); |
| assertEquals("failure to round-trip " + t + " through " + super.dsname, id, nd.getId()); |
| super.ds.remove(Collection.NODES, id); |
| } |
| } |
| |
| @Test |
| public void testInterestingInvalidIds() { |
| // see OAK-7261 |
| assumeTrue("fails on MongoDocumentStore, see OAK-7271", !(dsf instanceof DocumentStoreFixture.MongoFixture)); |
| |
| String[] tests = new String[] { "nul:a\u0000b", "brokensurrogate:\ud800" }; |
| |
| for (String t : tests) { |
| int pos = t.indexOf(":"); |
| String test = t.substring(pos + 1); |
| String id = Utils.getIdFromPath("/" + this.getClass().getName() + ".testInterestingInvalidIds-" + test); |
| |
| try { |
| super.ds.remove(Collection.NODES, id); |
| } catch (DocumentStoreException acceptable) { |
| // it would be acceptable to reject the delete request due to |
| // malformed string (for instance, PostgreSQL behaves like that) |
| } |
| |
| UpdateOp up = new UpdateOp(id, true); |
| boolean success = super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| |
| // failing to persist is ok - otherwise proceed with consistency |
| // tests |
| if (success) { |
| // re-read from persistence |
| super.ds.invalidateCache(); |
| NodeDocument nd = super.ds.find(Collection.NODES, id); |
| assertEquals("failure to round-trip " + t + " through " + super.dsname, id, nd.getId()); |
| |
| // if the character does not round-trip through UTF-8, try to |
| // delete the remapped one and do another lookup |
| if (!roundtripsThroughJavaUTF8(id)) { |
| Charset utf8 = Charset.forName("UTF-8"); |
| String mapped = new String(id.getBytes(utf8), utf8); |
| super.ds.remove(Collection.NODES, mapped); |
| super.ds.invalidateCache(); |
| NodeDocument nd2 = super.ds.find(Collection.NODES, id); |
| assertNotNull("deleting '" + mapped + "' has caused '" + id + "' to disappear", nd2); |
| } |
| |
| super.ds.remove(Collection.NODES, id); |
| } |
| } |
| } |
| |
| private static boolean roundtripsThroughJavaUTF8(String test) { |
| Charset utf8 = Charset.forName("UTF-8"); |
| byte bytes[] = test.getBytes(utf8); |
| return test.equals(new String(bytes, utf8)); |
| } |
| |
| @Test |
| public void testCreatePartialFailure() { |
| String bid = this.getClass().getName() + ".testCreatePartialFailure-"; |
| int cnt = 10; |
| assertTrue(cnt > 8); |
| |
| // clear repo |
| for (int i = 0; i < cnt; i++) { |
| super.ds.remove(Collection.NODES, bid + i); |
| removeMe.add(bid + i); |
| } |
| |
| // create one of the test nodes |
| int pre = cnt / 2; |
| UpdateOp up = new UpdateOp(bid + pre, true); |
| up.set("foo", "bar"); |
| assertTrue(super.ds.create(Collection.NODES, Collections.singletonList(up))); |
| |
| // batch create |
| Set<String> toCreate = new HashSet<String>(); |
| Set<String> toCreateFailEarly = new HashSet<String>(); |
| List<UpdateOp> ups = new ArrayList<UpdateOp>(); |
| for (int i = 0; i < cnt; i++) { |
| UpdateOp op = new UpdateOp(bid + i, true); |
| op.set("foo", "qux"); |
| ups.add(op); |
| if (i != pre) { |
| toCreate.add(bid + i); |
| } |
| if (i < pre) { |
| toCreateFailEarly.add(bid + i); |
| } |
| } |
| assertFalse(super.ds.create(Collection.NODES, ups)); |
| |
| // check how many nodes are there |
| Set<String> created = new HashSet<String>(); |
| for (int i = 0; i < cnt; i++) { |
| boolean present = null != super.ds.find(Collection.NODES, bid + i, 0); |
| if (i == pre && !present) { |
| fail(super.dsname + ": batch update removed previously existing node " + (bid + i)); |
| } else if (present && i != pre) { |
| created.add(bid + i); |
| } |
| } |
| |
| // diagnostics |
| toCreate.removeAll(created); |
| if (created.isEmpty()) { |
| LOG.info(super.dsname + ": create() apparently is atomic"); |
| } else if (created.size() == toCreate.size()) { |
| LOG.info(super.dsname + ": create() apparently is best-effort"); |
| } else if (created.equals(toCreateFailEarly)) { |
| LOG.info(super.dsname + ": create() stops at first failure"); |
| } else { |
| LOG.info(super.dsname + ": create() created: " + created + ", missing: " + toCreate); |
| } |
| } |
| |
| @Test |
| public void testDeleteNonExisting() { |
| String id = this.getClass().getName() + ".testDeleteNonExisting-" + UUID.randomUUID(); |
| // delete is best effort |
| ds.remove(Collection.NODES, id); |
| } |
| |
| @Test |
| public void testDeleteNonExistingMultiple() { |
| String id = this.getClass().getName() + ".testDeleteNonExistingMultiple-" + UUID.randomUUID(); |
| // create a test node |
| UpdateOp up = new UpdateOp(id + "-2", true); |
| boolean success = super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| assertTrue(success); |
| List<String> todelete = new ArrayList<String>(); |
| todelete.add(id + "-2"); |
| todelete.add(id); |
| ds.remove(Collection.NODES, todelete); |
| // id-2 should be removed |
| Document d = ds.find(Collection.NODES, id + "-2"); |
| assertTrue(d == null); |
| } |
| |
| @Test |
| public void testUpdateModified() { |
| String id = this.getClass().getName() + ".testUpdateModified"; |
| // create a test node |
| super.ds.remove(Collection.SETTINGS, id); |
| UpdateOp up = new UpdateOp(id, true); |
| boolean success = super.ds.create(Collection.SETTINGS, Collections.singletonList(up)); |
| assertTrue(success); |
| removeMeSettings.add(id); |
| |
| Document d = super.ds.find(Collection.SETTINGS, id); |
| Object m = d.get("_modified"); |
| assertNull("_modified should be null until set", m); |
| |
| up = new UpdateOp(id, true); |
| up.set("_modified", 123L); |
| super.ds.findAndUpdate(Collection.SETTINGS, up); |
| |
| d = super.ds.find(Collection.SETTINGS, id); |
| m = d.get("_modified"); |
| assertNotNull("_modified should now be != null", m); |
| assertEquals("123", m.toString()); |
| |
| up = new UpdateOp(id, true); |
| up.max("_modified", 122L); |
| super.ds.findAndUpdate(Collection.SETTINGS, up); |
| |
| d = super.ds.find(Collection.SETTINGS, id); |
| m = d.get("_modified"); |
| assertNotNull("_modified should now be != null", m); |
| assertEquals("123", m.toString()); |
| |
| up = new UpdateOp(id, true); |
| up.max("_modified", 124L); |
| super.ds.findAndUpdate(Collection.SETTINGS, up); |
| |
| ds.invalidateCache(); |
| d = super.ds.find(Collection.SETTINGS, id); |
| m = d.get("_modified"); |
| assertNotNull("_modified should now be != null", m); |
| assertEquals("124", m.toString()); |
| } |
| |
| @Test |
| public void testQuery() { |
| // create ten documents |
| String base = this.getClass().getName() + ".testQuery-"; |
| for (int i = 0; i < 10; i++) { |
| String id = base + i; |
| UpdateOp up = new UpdateOp(id, true); |
| boolean success = super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| assertTrue("document with " + id + " not created", success); |
| removeMe.add(id); |
| } |
| |
| List<String> result = getKeys(ds.query(Collection.NODES, base, base + "A", 5)); |
| assertEquals(5, result.size()); |
| assertTrue(result.contains(base + "4")); |
| assertFalse(result.contains(base + "5")); |
| |
| result = getKeys(ds.query(Collection.NODES, base, base + "A", 20)); |
| assertEquals(10, result.size()); |
| assertTrue(result.contains(base + "0")); |
| assertTrue(result.contains(base + "9")); |
| } |
| |
| @Test |
| public void testQueryBinary() { |
| // create ten documents |
| String base = this.getClass().getName() + ".testQueryBinary-"; |
| for (int i = 0; i < 10; i++) { |
| String id = base + i; |
| UpdateOp up = new UpdateOp(id, true); |
| up.set(NodeDocument.HAS_BINARY_FLAG, i % 2L); |
| boolean success = super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| assertTrue("document with " + id + " not created", success); |
| removeMe.add(id); |
| } |
| |
| List<String> result = getKeys(ds.query(Collection.NODES, base, base + "Z", NodeDocument.HAS_BINARY_FLAG, |
| NodeDocument.HAS_BINARY_VAL, 1000)); |
| assertEquals(5, result.size()); |
| assertTrue(result.contains(base + "1")); |
| assertFalse(result.contains(base + "0")); |
| } |
| |
| @Test |
| public void testQueryDeletedOnce() { |
| // create ten documents |
| String base = this.getClass().getName() + ".testQueryDeletedOnce-"; |
| for (int i = 0; i < 10; i++) { |
| String id = base + i; |
| UpdateOp up = new UpdateOp(id, true); |
| up.set(NodeDocument.DELETED_ONCE, Boolean.valueOf(i % 2 == 0)); |
| boolean success = super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| assertTrue("document with " + id + " not created", success); |
| removeMe.add(id); |
| } |
| |
| List<String> result = getKeys(ds.query(Collection.NODES, base, base + "Z", NodeDocument.DELETED_ONCE, |
| 1L, 1000)); |
| assertEquals(5, result.size()); |
| assertTrue(result.contains(base + "0")); |
| assertFalse(result.contains(base + "1")); |
| } |
| |
| @Test |
| public void testQueryCollation() { |
| // create ten documents |
| String base = "2:/" + this.getClass().getName() + ".testQueryCollation"; |
| List<UpdateOp> creates = new ArrayList<UpdateOp>(); |
| |
| List<String> expected = new ArrayList<String>(); |
| // test US-ASCII except control characters |
| for (char c : "!\"#$%&'()*+,-./0123456789:;<=>?@AZ[\\]^_`az{|}~".toCharArray()) { |
| String id = base + c; |
| UpdateOp up = new UpdateOp(id, true); |
| creates.add(up); |
| removeMe.add(id); |
| id = base + "/" + c; |
| up = new UpdateOp(id, true); |
| creates.add(up); |
| expected.add(id); |
| removeMe.add(id); |
| } |
| boolean success = super.ds.create(Collection.NODES, creates); |
| assertTrue("documents not created", success); |
| |
| List<String> result = getKeys(ds.query(Collection.NODES, base + "/", base + "0", 1000)); |
| |
| List<String> diff = new ArrayList<String>(); |
| diff.addAll(result); |
| diff.removeAll(expected); |
| if (!diff.isEmpty()) { |
| fail("unexpected query results (broken collation handling in persistence?): " + diff); |
| } |
| |
| diff = new ArrayList<String>(); |
| diff.addAll(expected); |
| diff.removeAll(result); |
| if (!diff.isEmpty()) { |
| fail("missing query results (broken collation handling in persistence?): " + diff); |
| } |
| assertEquals("incorrect result ordering in query result (broken collation handling in persistence?)", expected, result); |
| } |
| |
| @Test |
| public void modCountCondition() { |
| String id = this.getClass().getName() + ".modCountCondition"; |
| removeMe.add(id); |
| UpdateOp op = new UpdateOp(id, true); |
| op.set("p", "a"); |
| assertTrue(ds.create(Collection.NODES, Collections.singletonList(op))); |
| NodeDocument doc = ds.find(Collection.NODES, id); |
| assertNotNull(doc); |
| Long modCount = doc.getModCount(); |
| // can only proceed if store maintains modCount |
| assumeNotNull(modCount); |
| // check equals (non-matching) |
| op = new UpdateOp(id, false); |
| op.set("p", "b"); |
| op.equals(Document.MOD_COUNT, modCount + 1); |
| assertNull(ds.findAndUpdate(Collection.NODES, op)); |
| // check equals (matching) |
| op = new UpdateOp(id, false); |
| op.set("p", "b"); |
| op.equals(Document.MOD_COUNT, modCount); |
| assertNotNull(ds.findAndUpdate(Collection.NODES, op)); |
| // validate |
| doc = ds.find(Collection.NODES, id); |
| assertNotNull(doc); |
| assertEquals("b", doc.get("p")); |
| modCount = doc.getModCount(); |
| assertNotNull(modCount); |
| // check not equals (non-matching) |
| op = new UpdateOp(id, false); |
| op.set("p", "c"); |
| op.notEquals(Document.MOD_COUNT, modCount); |
| assertNull(ds.findAndUpdate(Collection.NODES, op)); |
| // check not equals (matching) |
| op = new UpdateOp(id, false); |
| op.set("p", "c"); |
| op.notEquals(Document.MOD_COUNT, modCount + 1); |
| assertNotNull(ds.findAndUpdate(Collection.NODES, op)); |
| // validate |
| doc = ds.find(Collection.NODES, id); |
| assertNotNull(doc); |
| assertEquals("c", doc.get("p")); |
| } |
| |
| private List<String> getKeys(List<NodeDocument> docs) { |
| List<String> result = new ArrayList<String>(); |
| for (NodeDocument doc : docs) { |
| result.add(doc.getId()); |
| } |
| return result; |
| } |
| |
| private static String generateId(int length, boolean ascii) { |
| StringBuffer sb = new StringBuffer(); |
| for (int i = 0; i < length; i++) { |
| if (ascii) { |
| sb.append("0"); |
| } |
| else { |
| sb.append(Character.toChars(0x1F4A9)); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| // make sure _collisionsModCount property is maintained properly when it exists |
| @Test |
| public void testCollisionsModCount() { |
| String id = this.getClass().getName() + ".testCollisionsModCount"; |
| |
| // remove if present |
| NodeDocument nd = super.ds.find(Collection.NODES, id); |
| if (nd != null) { |
| super.ds.remove(Collection.NODES, id); |
| } |
| |
| // add |
| Revision revision = Revision.fromString("r0-0-1"); |
| UpdateOp up = new UpdateOp(id, true); |
| up.setMapEntry("_collisions", revision, "foo"); |
| assertTrue(super.ds.create(Collection.NODES, Collections.singletonList(up))); |
| removeMe.add(id); |
| |
| // get it |
| nd = super.ds.find(Collection.NODES, id); |
| assertNotNull(nd); |
| Number cmc = (Number)nd.get("_collisionsModCount"); |
| if (cmc == null) { |
| // not supported |
| } |
| else { |
| // update |
| Revision revision2 = Revision.fromString("r0-0-2"); |
| UpdateOp up2 = new UpdateOp(id, false); |
| up2.setMapEntry("_collisions", revision2, "foobar"); |
| NodeDocument old = super.ds.findAndUpdate(Collection.NODES, up2); |
| assertNotNull(old); |
| |
| nd = super.ds.find(Collection.NODES, id, 0); |
| assertNotNull(nd); |
| Number cmc2 = (Number)nd.get("_collisionsModCount"); |
| assertNotNull(cmc2); |
| assertTrue(cmc2.longValue() > cmc.longValue()); |
| |
| // update |
| UpdateOp up3 = new UpdateOp(id, false); |
| up3.set("foo", "bar"); |
| old = super.ds.findAndUpdate(Collection.NODES, up3); |
| assertNotNull(old); |
| |
| nd = super.ds.find(Collection.NODES, id, 0); |
| assertNotNull(nd); |
| Number cmc3 = (Number)nd.get("_collisionsModCount"); |
| assertNotNull(cmc3); |
| assertTrue(cmc2.longValue() == cmc3.longValue()); |
| } |
| } |
| |
| @Test |
| public void description() throws Exception{ |
| Map<String, String> desc = ds.getMetadata(); |
| assertNotNull(desc.get("type")); |
| } |
| |
| @Test |
| public void testServerTimeDiff() throws Exception { |
| UpdateOp up = new UpdateOp("0:/", true); |
| super.ds.create(Collection.NODES, Collections.singletonList(up)); |
| removeMe.add("0:/"); |
| long td = super.ds.determineServerTimeDifferenceMillis(); |
| LOG.info("Server time difference on " + super.dsname + ": " + td + "ms"); |
| } |
| |
| @Test |
| public void removeWithCondition() throws Exception { |
| |
| Set<Path> existingDocs = new HashSet<>(); |
| for (NodeDocument doc : Utils.getAllDocuments(ds)) { |
| existingDocs.add(doc.getPath()); |
| } |
| |
| List<UpdateOp> docs = Lists.newArrayList(); |
| docs.add(newDocument("/foo", 100)); |
| removeMe.add(Utils.getIdFromPath("/foo")); |
| docs.add(newDocument("/bar", 200)); |
| removeMe.add(Utils.getIdFromPath("/bar")); |
| docs.add(newDocument("/baz", 300)); |
| removeMe.add(Utils.getIdFromPath("/baz")); |
| ds.create(Collection.NODES, docs); |
| |
| for (UpdateOp op : docs) { |
| assertNotNull(ds.find(Collection.NODES, op.getId())); |
| } |
| |
| Map<String, Long> toRemove = Maps.newHashMap(); |
| toRemove.put(Utils.getIdFromPath("/foo"), 100L); // matches |
| toRemove.put(Utils.getIdFromPath("/bar"), 300L); // modified differs |
| toRemove.put(Utils.getIdFromPath("/qux"), 100L); // does not exist |
| toRemove.put(Utils.getIdFromPath("/baz"), 300L); // matches |
| |
| int removed = ds.remove(Collection.NODES, toRemove); |
| |
| assertEquals(2, removed); |
| assertNotNull(ds.find(Collection.NODES, Utils.getIdFromPath("/bar"))); |
| Path bar = Path.fromString("/bar"); |
| for (NodeDocument doc : Utils.getAllDocuments(ds)) { |
| if (!doc.getPath().equals(bar) && !existingDocs.contains(doc.getPath())) { |
| fail("document must not exist: " + doc.getId()); |
| } |
| } |
| } |
| |
| @Test |
| public void removeInvalidatesCache() throws Exception { |
| String path = "/foo"; |
| String id = Utils.getIdFromPath(path); |
| long modified = 1; |
| removeMe.add(id); |
| ds.create(Collection.NODES, Collections.singletonList(newDocument(path, modified))); |
| int removed = ds.remove(Collection.NODES, Collections.singletonMap(id, modified)); |
| assertEquals(1, removed); |
| assertNull(ds.getIfCached(Collection.NODES, id)); |
| } |
| |
| // OAK-3932 |
| @Test |
| public void getIfCachedNonExistingDocument() throws Exception { |
| String id = Utils.getIdFromPath("/foo"); |
| assertNull(ds.find(Collection.NODES, id)); |
| assertNull(ds.getIfCached(Collection.NODES, id)); |
| } |
| |
| private UpdateOp newDocument(String path, long modified) { |
| String id = Utils.getIdFromPath(path); |
| UpdateOp op = new UpdateOp(id, true); |
| op.set(NodeDocument.MODIFIED_IN_SECS, modified); |
| return op; |
| } |
| } |