/*
* 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.core.persistence;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.UUID;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.nodetype.ConstraintViolationException;
import org.apache.commons.io.FileUtils;
import org.apache.jackrabbit.core.TestHelper;
import org.apache.jackrabbit.core.TransientRepository;
import org.apache.jackrabbit.core.persistence.check.ConsistencyReport;
import junit.framework.TestCase;
/**
* Tests that a corrupt node is automatically fixed.
*/
public class AutoFixCorruptNode extends TestCase {
private final String TEST_DIR = "target/temp/" + getClass().getSimpleName();
public void setUp() throws Exception {
FileUtils.deleteDirectory(new File(TEST_DIR));
}
public void tearDown() throws Exception {
setUp();
}
/**
* Unit test for <a
* href="https://issues.apache.org/jira/browse/JCR-3069">JCR-3069</a>
*/
public void testAutoFixWithConsistencyCheck() throws Exception {
// new repository
TransientRepository rep = new TransientRepository(new File(TEST_DIR));
Session s = openSession(rep, false);
Node root = s.getRootNode();
// add nodes /test and /test/missing
Node test = root.addNode("test");
Node missing = test.addNode("missing");
missing.addMixin("mix:referenceable");
UUID id = UUID.fromString(missing.getIdentifier());
s.save();
s.logout();
destroyBundle(id, "workspaces/default");
s = openSession(rep, false);
try {
ConsistencyReport r = TestHelper.checkConsistency(s, false, null);
assertNotNull(r);
assertNotNull(r.getItems());
assertEquals(1, r.getItems().size());
assertEquals(test.getIdentifier(), r.getItems().iterator().next()
.getNodeId());
} finally {
s.logout();
rep.shutdown();
FileUtils.deleteDirectory(new File("repository"));
}
}
public void testOrphan() throws Exception {
// new repository
TransientRepository rep = new TransientRepository(new File(TEST_DIR));
Session s = openSession(rep, false);
try {
Node root = s.getRootNode();
Node parent = root.addNode("parent");
Node test = parent.addNode("test");
test.addMixin("mix:referenceable");
String lost = test.getIdentifier();
Node lnf = root.addNode("lost+found");
lnf.addMixin("mix:referenceable");
String lnfid = lnf.getIdentifier();
s.save();
Node brokenNode = parent;
UUID destroy = UUID.fromString(brokenNode.getIdentifier());
s.logout();
destroyBundle(destroy, "workspaces/default");
s = openSession(rep, false);
ConsistencyReport report = TestHelper.checkConsistency(s, false, null);
assertTrue("Report should have reported broken nodes", !report.getItems().isEmpty());
// now retry with lost+found functionality
ConsistencyReport report2 = TestHelper.checkConsistency(s, true, lnfid);
assertTrue("Report should have reported broken nodes", !report2.getItems().isEmpty());
s.logout();
s = openSession(rep, false);
Node q = s.getNodeByIdentifier(lost);
// check the node was moved
assertEquals(lnfid, q.getParent().getIdentifier());
} finally {
s.logout();
}
}
public void testMissingVHR() throws Exception {
// new repository
TransientRepository rep = new TransientRepository(new File(TEST_DIR));
Session s = openSession(rep, false);
String oldVersionRecoveryProp = System
.getProperty("org.apache.jackrabbit.version.recovery");
try {
Node root = s.getRootNode();
Node test = root.addNode("test");
test.addMixin("mix:versionable");
s.save();
Node vhr = s.getWorkspace().getVersionManager()
.getVersionHistory(test.getPath());
assertNotNull(vhr);
Node brokenNode = vhr;
String vhrRootVersionId = vhr.getNode("jcr:rootVersion").getIdentifier();
UUID destroy = UUID.fromString(brokenNode.getIdentifier());
s.logout();
destroyBundle(destroy, "version");
s = openSession(rep, false);
ConsistencyReport report = TestHelper.checkVersionStoreConsistency(s, false, null);
assertTrue("Report should have reported broken nodes", !report.getItems().isEmpty());
try {
test = s.getRootNode().getNode("test");
vhr = s.getWorkspace().getVersionManager()
.getVersionHistory(test.getPath());
fail("should not get here");
} catch (Exception ex) {
// expected
}
s.logout();
System.setProperty("org.apache.jackrabbit.version.recovery", "true");
s = openSession(rep, false);
test = s.getRootNode().getNode("test");
// versioning should be disabled now
assertFalse(test.isNodeType("mix:versionable"));
try {
// try to enable versioning again
test.addMixin("mix:versionable");
s.save();
fail("enabling versioning succeeded unexpectedly");
}
catch (Exception e) {
// we expect this to fail
}
s.logout();
// now redo after running fixup on versioning storage
s = openSession(rep, false);
report = TestHelper.checkVersionStoreConsistency(s, true, null);
assertTrue("Report should have reported broken nodes", !report.getItems().isEmpty());
int reportitems = report.getItems().size();
// problems should now be fixed
report = TestHelper.checkVersionStoreConsistency(s, false, null);
assertTrue("Some problems should have been fixed but are not: " + report, report.getItems().size() < reportitems);
// get a fresh session
s.logout();
s = openSession(rep, false);
test = s.getRootNode().getNode("test");
// versioning should be disabled now
assertFalse(test.isNodeType("mix:versionable"));
// try to enable versioning again
test.addMixin("mix:versionable");
s.save();
Node oldRootVersion = s.getNodeByIdentifier(vhrRootVersionId);
try {
String path = oldRootVersion.getPath();
fail("got path " + path + " for a node believed to be orphaned");
}
catch (ItemNotFoundException ex) {
// orphaned
}
Node newRootVersion = s.getWorkspace().getVersionManager()
.getVersionHistory(test.getPath()).getRootVersion();
assertFalse(
"new root version should be a different node than the one destroyed by the test case",
newRootVersion.getIdentifier().equals(vhrRootVersionId));
assertNotNull("new root version should have a intact path",
newRootVersion.getPath());
} finally {
s.logout();
System.setProperty("org.apache.jackrabbit.version.recovery",
oldVersionRecoveryProp == null ? ""
: oldVersionRecoveryProp);
}
}
public void testMissingRootVersion() throws Exception {
// new repository
TransientRepository rep = new TransientRepository(new File(TEST_DIR));
Session s = openSession(rep, false);
String oldVersionRecoveryProp = System
.getProperty("org.apache.jackrabbit.version.recovery");
try {
Node root = s.getRootNode();
// add nodes /test and /test/missing
Node test = root.addNode("test", "nt:file");
test.addNode("jcr:content", "nt:unstructured");
test.addMixin("mix:versionable");
s.save();
s.getWorkspace().getVersionManager().checkout(test.getPath());
s.getWorkspace().getVersionManager().checkin(test.getPath());
Node vhr = s.getWorkspace().getVersionManager()
.getVersionHistory(test.getPath());
assertNotNull(vhr);
Node brokenNode = vhr.getNode("jcr:rootVersion");
String vhrId = vhr.getIdentifier();
UUID destroy = UUID.fromString(brokenNode.getIdentifier());
s.logout();
destroyBundle(destroy, "version");
s = openSession(rep, false);
ConsistencyReport report = TestHelper.checkVersionStoreConsistency(s, false, null);
assertTrue("Report should have reported broken nodes", !report.getItems().isEmpty());
try {
test = s.getRootNode().getNode("test");
vhr = s.getWorkspace().getVersionManager()
.getVersionHistory(test.getPath());
fail("should not get here");
} catch (Exception ex) {
// expected
}
s.logout();
System.setProperty("org.apache.jackrabbit.version.recovery", "true");
s = openSession(rep, false);
test = s.getRootNode().getNode("test");
// versioning should be disabled now
assertFalse(test.isNodeType("mix:versionable"));
try {
// try to enable versioning again
test.addMixin("mix:versionable");
s.save();
fail("enabling versioning succeeded unexpectedly");
}
catch (Exception e) {
// we expect this to fail
}
s.logout();
// now redo after running fixup on versioning storage
s = openSession(rep, false);
report = TestHelper.checkVersionStoreConsistency(s, true, null);
assertTrue("Report should have reported broken nodes", !report.getItems().isEmpty());
int reportitems = report.getItems().size();
// problems should now be fixed
report = TestHelper.checkVersionStoreConsistency(s, false, null);
assertTrue("Some problems should have been fixed but are not: " + report, report.getItems().size() < reportitems);
test = s.getRootNode().getNode("test");
// versioning should be disabled now
assertFalse(test.isNodeType("mix:versionable"));
// jcr:uuid property should still be present
assertTrue(test.hasProperty("jcr:uuid"));
// ...and have a proper definition
assertNotNull(test.getProperty("jcr:uuid").getDefinition().getName());
// try to enable versioning again
test.addMixin("mix:versionable");
s.save();
Node oldVHR = s.getNodeByIdentifier(vhrId);
Node newVHR = s.getWorkspace().getVersionManager().getVersionHistory(test.getPath());
assertTrue("old and new version history path should be different: "
+ oldVHR.getPath() + " vs " + newVHR.getPath(), !oldVHR
.getPath().equals(newVHR.getPath()));
// name should be same plus suffix
assertTrue(oldVHR.getName().startsWith(newVHR.getName()));
// try a checkout / checkin
s.getWorkspace().getVersionManager().checkout(test.getPath());
s.getWorkspace().getVersionManager().checkin(test.getPath());
validateDisconnectedVHR(oldVHR);
} finally {
s.logout();
System.setProperty("org.apache.jackrabbit.version.recovery",
oldVersionRecoveryProp == null ? ""
: oldVersionRecoveryProp);
}
}
// similar to above, but disconnects version history before damaging the repository
public void testMissingRootVersion2() throws Exception {
// new repository
TransientRepository rep = new TransientRepository(new File(TEST_DIR));
Session s = openSession(rep, false);
String oldVersionRecoveryProp = System
.getProperty("org.apache.jackrabbit.version.recovery");
try {
Node root = s.getRootNode();
// add nodes /test and /test/missing
Node test = root.addNode("test");
test.addMixin("mix:versionable");
s.save();
Node vhr = s.getWorkspace().getVersionManager()
.getVersionHistory(test.getPath());
assertNotNull(vhr);
Node brokenNode = vhr.getNode("jcr:rootVersion");
String vhrId = vhr.getIdentifier();
UUID destroy = UUID.fromString(brokenNode.getIdentifier());
// disable versioning
test.removeMixin("mix:versionable");
s.save();
s.logout();
destroyBundle(destroy, "version");
s = openSession(rep, false);
ConsistencyReport report = TestHelper.checkVersionStoreConsistency(s, false, null);
assertTrue("Report should have reported broken nodes", !report.getItems().isEmpty());
s.logout();
System.setProperty("org.apache.jackrabbit.version.recovery", "true");
s = openSession(rep, false);
s.logout();
s = openSession(rep, false);
test = s.getRootNode().getNode("test");
// versioning should still be disabled
assertFalse(test.isNodeType("mix:versionable"));
// try to enable versioning again
test.addMixin("mix:versionable");
s.save();
Node oldVHR = s.getNodeByIdentifier(vhrId);
Node newVHR = s.getWorkspace().getVersionManager().getVersionHistory(test.getPath());
assertTrue("old and new version history path should be different: "
+ oldVHR.getPath() + " vs " + newVHR.getPath(), !oldVHR
.getPath().equals(newVHR.getPath()));
// name should be same plus suffix
assertTrue(oldVHR.getName().startsWith(newVHR.getName()));
// try a checkout / checkin
s.getWorkspace().getVersionManager().checkout(test.getPath());
s.getWorkspace().getVersionManager().checkin(test.getPath());
validateDisconnectedVHR(oldVHR);
} finally {
s.logout();
System.setProperty("org.apache.jackrabbit.version.recovery",
oldVersionRecoveryProp == null ? ""
: oldVersionRecoveryProp);
}
}
// tests recovery from a broken hierarchy in the version store
public void testBrokenVhrParent() throws Exception {
// new repository
TransientRepository rep = new TransientRepository(new File(TEST_DIR));
Session s = openSession(rep, false);
try {
Node root = s.getRootNode();
// add node /test
Node test = root.addNode("test");
test.addMixin("mix:versionable");
s.save();
Node vhr = s.getWorkspace().getVersionManager().getVersionHistory(test.getPath());
assertNotNull(vhr);
Node brokenNode = vhr.getParent().getParent();
UUID destroy = UUID.fromString(brokenNode.getIdentifier());
// disable versioning
test.removeMixin("mix:versionable");
s.save();
s.logout();
destroyBundle(destroy, "version");
s = openSession(rep, false);
ConsistencyReport report = TestHelper.checkVersionStoreConsistency(s, true, null);
assertTrue("Report should have reported broken nodes", !report.getItems().isEmpty());
s.logout();
s = openSession(rep, false);
test = s.getRootNode().getNode("test");
// versioning should still be disabled
assertFalse(test.isNodeType("mix:versionable"));
// try to enable versioning again
test.addMixin("mix:versionable");
s.save();
// try a checkout / checkin
s.getWorkspace().getVersionManager().checkout(test.getPath());
s.getWorkspace().getVersionManager().checkin(test.getPath());
} finally {
s.logout();
}
}
public void testAutoFix() throws Exception {
// new repository
TransientRepository rep = new TransientRepository(new File(TEST_DIR));
Session s = openSession(rep, false);
Node root = s.getRootNode();
// add nodes /test and /test/missing
Node test = root.addNode("test");
Node missing = test.addNode("missing");
missing.addMixin("mix:referenceable");
UUID id = UUID.fromString(missing.getIdentifier());
s.save();
s.logout();
destroyBundle(id, "workspaces/default");
// login and try the operation
s = openSession(rep, false);
test = s.getRootNode().getNode("test");
// try to add a node with the same name
try {
test.addNode("missing");
s.save();
} catch (RepositoryException e) {
// expected
}
s.logout();
s = openSession(rep, true);
test = s.getRootNode().getNode("test");
// iterate over all child nodes fixes the corruption
NodeIterator it = test.getNodes();
while (it.hasNext()) {
it.nextNode();
}
// try to add a node with the same name
test.addNode("missing");
s.save();
// try to delete the parent node
test.remove();
s.save();
s.logout();
rep.shutdown();
FileUtils.deleteDirectory(new File("repository"));
}
private void destroyBundle(UUID id, String where) throws SQLException {
Connection conn = DriverManager.getConnection("jdbc:derby:" + TEST_DIR
+ "/" + where + "/db");
String table = where.equals("version") ? "VERSION_BUNDLE" : "DEFAULT_BUNDLE";
PreparedStatement prep = conn.prepareStatement("delete from " + table
+ " where NODE_ID_HI=? and NODE_ID_LO=?");
prep.setLong(1, id.getMostSignificantBits());
prep.setLong(2, id.getLeastSignificantBits());
prep.executeUpdate();
conn.close();
}
private Session openSession(Repository rep, boolean autoFix)
throws RepositoryException {
SimpleCredentials cred = new SimpleCredentials("admin",
"admin".toCharArray());
if (autoFix) {
cred.setAttribute("org.apache.jackrabbit.autoFixCorruptions",
"true");
}
return rep.login(cred);
}
// JCR-4118: check that the old VHR can be retrieved
private void validateDisconnectedVHR(Node oldVHR) throws RepositoryException {
Session s = oldVHR.getSession();
Node old = s.getNode(oldVHR.getPath());
assertNotNull("disconnected VHR should be accessible", old);
assertEquals("nt:versionHistory", old.getPrimaryNodeType().getName());
NodeIterator ni = old.getNodes();
while (ni.hasNext()) {
Node n = ni.nextNode();
String type = n.getPrimaryNodeType().getName();
assertTrue("node type of VHR child nodes should be nt:version or nt:versionLabels",
"nt:version".equals(type) || "nt:versionLabels".equals(type));
}
try {
old.remove();
s.save();
fail("removal of node using remove() should throw because it's in the versioning workspace");
} catch (ConstraintViolationException expected) {
}
}
}