Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/ConsistencyCheckTest.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/ConsistencyCheckTest.java (revision 0) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/ConsistencyCheckTest.java (revision 0) @@ -0,0 +1,445 @@ +/* + * 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.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Properties; + +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.version.Version; + +import junit.framework.TestCase; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.test.JUnitTest; +import org.apache.jackrabbit.test.config.PersistenceManagerConf; +import org.apache.jackrabbit.test.config.RepositoryConf; +import org.apache.jackrabbit.uuid.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ConsistencyCheckTest tests the PersistenceManager's consistencyCheck feature. + * + * For analysis, run this test with log4j enabled (see below). + * + */ +public class ConsistencyCheckTest extends TestCase { + + private static Logger log = LoggerFactory.getLogger(JUnitTest.class); + + //-------------------------------------------------------------------< generic utility methods > + + public static File createTempDir(String prefix, String suffix) { + try { + File dir = File.createTempFile(prefix, suffix); + dir.delete(); + dir.mkdir(); + + return dir; + } catch (IOException e) { + fail("Cannot create temp directory for test: " + e); + return null; + } + } + + public static void delete(File file) { + File[] files = file.listFiles(); + for (int i = 0; files != null && i < files.length; i++) { + delete(files[i]); + } + file.delete(); + } + + private static String getUUID(Node node) { + return ((NodeImpl) node).getNodeId().toString(); + } + + private static int indentCount = 0; + + private static void displayTree(Node node) throws RepositoryException { + StringBuffer indent = new StringBuffer(); + for (int i=0; i < indentCount; i++) { + indent.append(" "); + } + log.debug(indent + node.getName() + " [" + getUUID(node) + "]"); + NodeIterator nodes = node.getNodes(); + indentCount++; + while (nodes.hasNext()) { + displayTree(nodes.nextNode()); + } + indentCount--; + } + + //-------------------------------------------------------------------< repository setup > + + private final static File DIRECTORY = createTempDir("jackrabbit", "test"); + + private final static String PROTOCOL = "jdbc:derby:"; + + private final static String DB_PATH = "/db/itemState"; + + private final static String VERSIONING_DB_PATH = "/version/db/itemState"; + + private static AdminRepositoryImpl repository = createRepository(); + + private static AdminRepositoryImpl createRepository() { + log.debug("Start repo at '" + DIRECTORY.getPath() + "'..."); + + RepositoryConf conf = new RepositoryConf(); + // ensure correct derby config + PersistenceManagerConf pmc = conf.getWorkspaceConfTemplate().getPersistenceManagerConf(); + pmc.setParameter("url", PROTOCOL + "${wsp.home}" + DB_PATH + ";create=true"); + + pmc = conf.getVersioningConf().getPersistenceManagerConf(); + pmc.setParameter("url", PROTOCOL + "${rep.home}" + VERSIONING_DB_PATH + ";create=true"); + pmc.setParameter("schemaObjectPrefix", "VERSION_"); + try { + RepositoryConfig config = conf.createConfig(DIRECTORY.getPath()); + config.init(); + return new AdminRepositoryImpl(config); + } catch (Exception e) { + fail("Could not create repository for test: " + e); + return null; + } + } + + private Session login(String workspace) throws RepositoryException { + return repository.login(new SimpleCredentials("admin", "admin".toCharArray()), workspace); + } + + private static void deleteRepository() { + log.debug("Shutdown repo..."); + repository.shutdown(); + delete(DIRECTORY); + } + + /** + * AdminRepositoryImpl makes checkConsistency() method + * in RepositoryImpl accessible. + */ + private static class AdminRepositoryImpl extends RepositoryImpl { + + public AdminRepositoryImpl(RepositoryConfig config) throws RepositoryException { + super(config); + } + + // make protected method in RepositoryImpl public + public void checkConsistency(String workspaceName, String[] uuids, + boolean recursive, boolean fix) throws IllegalStateException, + NoSuchWorkspaceException, RepositoryException { + super.checkConsistency(workspaceName, uuids, recursive, fix); + } + } + + //-------------------------------------------------------------------< helper methods > + + private Connection getDerbyConnection(String workspace) throws SQLException { + Connection conn = null; + Properties props = new Properties(); + props.put("user", ""); + props.put("password", ""); + + if (JCR_SYSTEM.equals(workspace)) { + conn = DriverManager.getConnection(PROTOCOL + DIRECTORY.getPath() + VERSIONING_DB_PATH, props); + log.debug(conn.getMetaData().getURL()); + } else { + String basePath = DIRECTORY.getPath() + File.separatorChar + "workspaces" + File.separatorChar; + conn = DriverManager.getConnection(PROTOCOL + basePath + workspace + DB_PATH, props); + } + + conn.setAutoCommit(false); + return conn; + } + + private void setUUIDParams(PreparedStatement s, int startIndex, UUID uuid) throws SQLException { + s.setLong(startIndex, uuid.getMostSignificantBits()); + s.setLong(startIndex+1, uuid.getLeastSignificantBits()); + } + + private void deleteNodeBundle(Connection conn, String workspace, String uuid) throws SQLException { + PreparedStatement s = conn.prepareStatement( + "delete from " + workspace.toUpperCase() + "_BUNDLE where NODE_ID_HI = ? and NODE_ID_LO = ?"); + setUUIDParams(s, 1, new UUID(uuid)); + s.execute(); + } + + private byte[] readNodeBundleBlob(Connection conn, String workspace, String uuid) throws SQLException { + PreparedStatement s = conn.prepareStatement( + "select BUNDLE_DATA from " + workspace.toUpperCase() + "_BUNDLE where NODE_ID_HI = ? and NODE_ID_LO = ?"); + setUUIDParams(s, 1, new UUID(uuid)); + ResultSet rs = s.executeQuery(); + if (rs.next()) { + return rs.getBytes(1); + } + return null; + } + + private void writeNodeBundleBlob(Connection conn, String workspace, String uuid, byte[] bytes) throws SQLException { + PreparedStatement s = conn.prepareStatement( + "update " + workspace.toUpperCase() + "_BUNDLE set BUNDLE_DATA = ? where NODE_ID_HI = ? and NODE_ID_LO = ?"); + s.setBytes(1, bytes); + setUUIDParams(s, 2, new UUID(uuid)); + s.execute(); + } + + //-------------------------------------------------------------------< test data setup > + + private final static String WORKSPACE = "default"; + + private static final String JCR_SYSTEM = "jcr:system"; + + private static String testNodeUUID; + + private static String missingChildUUID; + + private static String missingGrandChildUUID; + + private static String missingParentUUID; + + private static String clearedNodeUUID; + + private static String brokenNodeUUID; + + private void setupWorkspaceTestData() throws Exception { + Session session = login(WORKSPACE); + + // prepare some parent-child relationships + + // nested tree where children will get lost + Node fruits = session.getRootNode().addNode("fruits"); + Node apple = fruits.addNode("apple"); + Node bananas = fruits.addNode("bananas"); + Node melons = fruits.addNode("melons"); + melons.addNode("bittermelon"); + Node watermelon = melons.addNode("watermelon"); + Node honeydew = melons.addNode("honeydew"); + + // separate tree where the parent will get lost + Node vegetables = session.getRootNode().addNode("vegetables"); + vegetables.addNode("cucumber"); + + testNodeUUID = getUUID(fruits); + missingChildUUID = getUUID(bananas); + missingGrandChildUUID = getUUID(watermelon); + missingParentUUID = getUUID(vegetables); + clearedNodeUUID = getUUID(honeydew); + brokenNodeUUID = getUUID(apple); + + session.save(); + + Connection conn = getDerbyConnection(WORKSPACE); + + // now delete/modify some bundles directly in the database + log.debug("delete parent " + missingParentUUID); + deleteNodeBundle(conn, WORKSPACE, missingParentUUID); + + log.debug("delete child " + missingChildUUID); + deleteNodeBundle(conn, WORKSPACE, missingChildUUID); + log.debug("delete grand child " + missingGrandChildUUID); + deleteNodeBundle(conn, WORKSPACE, missingGrandChildUUID); + + log.debug("clear node " + clearedNodeUUID); + // makes the bundle invalid, ie. BundleBinding.checkBundle() will throw an exception + writeNodeBundleBlob(conn, WORKSPACE, clearedNodeUUID, new byte[10]); + + log.debug("break node " + brokenNodeUUID); + byte[] bytes = readNodeBundleBlob(conn, WORKSPACE, brokenNodeUUID); + // makes the bundle invalid, ie. BundleBinding.checkBundle() will return false (but won't throw an exception) + bytes[25] = 1; + writeNodeBundleBlob(conn, WORKSPACE, brokenNodeUUID, bytes); + + conn.commit(); + conn.close(); + } + + + private static String brokenVersion; + + private void setupVersioningTestData() throws Exception { + Session session = login(WORKSPACE); + // create a new node + first version + Node node = session.getRootNode().addNode("food"); + node.addMixin("mix:versionable"); + node.addNode("values").setProperty("kcal", 300); + session.save(); + node.checkin(); + + // create a second version + node.checkout(); + node.getNode("values").setProperty("kcal", 1234); + session.save(); + node.checkin(); + + // create a third version + node.checkout(); + node.getNode("values").setProperty("kcal", 5000); + session.save(); + node.checkin(); + + displayTree(session.getRootNode().getNode("jcr:system/jcr:versionStorage")); + + brokenVersion = "1.1"; + Version v = node.getVersionHistory().getVersion(brokenVersion); + String missingNodeUUID = getUUID(v.getNode("jcr:frozenNode").getNode("values")); + + Connection conn = getDerbyConnection(JCR_SYSTEM); + + log.debug("delete node " + missingNodeUUID); + deleteNodeBundle(conn, "version", missingNodeUUID); + + conn.commit(); + conn.close(); + } + + private void assertRepositoryException(Node rootNode, String path) { + try { + rootNode.getNode(path); + } catch (PathNotFoundException e) { + fail("JCR-1428: no need for a consistencyFix then..."); + } catch (RepositoryException e) { + // expected + } + } + + private void assertPathNotFoundException(Node rootNode, String path) { + try { + rootNode.getNode(path); + } catch (PathNotFoundException e) { + // expewcted + } catch (RepositoryException e) { + log.error("JCR-1428: fix in consistencyCheck did not work", e); + fail("JCR-1428: fix in consistencyCheck did not work: " + e); + } + } + + //-------------------------------------------------------------------< tests > + + /** + * To verify the check, one has to manually look at the error log + * therefore run this test with these log4j settings: + * + * for looking at bundle errors only: + * log4j.rootLogger=ERROR, file + * log4j.logger.org.apache.jackrabbit.core=ERROR, stdout + * log4j.logger.org.apache.jackrabbit.test=DEBUG + * + * for full details on checked bundles, add this: + * log4j.logger.org.apache.jackrabbit.core.persistence.bundle.util.BundleBinding=INFO + * + * for details on fixing, add this: + * log4j.logger.org.apache.jackrabbit.core.persistence.bundle.BundleDbPersistenceManager=INFO + * + */ + public void testConsistencyCheck() throws Exception { + // 1. test, init data + setupWorkspaceTestData(); + + log.debug("================================================ Checking workspace " + WORKSPACE + "..."); + repository.checkConsistency(WORKSPACE, null, false, false); + + log.debug("================================================ Checking workspace " + WORKSPACE + ", single node..."); + repository.checkConsistency(WORKSPACE, new String[] { testNodeUUID, missingParentUUID }, false, false); + + log.debug("================================================ Checking workspace " + WORKSPACE + ", single node, recursively..."); + repository.checkConsistency(WORKSPACE, new String[] { testNodeUUID }, true, false); + } + + public void testConsistencyFix() throws Exception { + // ensure all item state caches are empty + repository.shutdown(); + repository = createRepository(); + + Session session; + + // tests only the erroneous behaviour when bundles are missing + session = login(WORKSPACE); + assertRepositoryException(session.getRootNode(), "fruits/bananas"); + assertRepositoryException(session.getRootNode(), "fruits/melons/watermelon"); + + log.debug("================================================ Checking and fixing workspace " + WORKSPACE + "..."); + repository.checkConsistency(WORKSPACE, null, false, true); + + // check the log here, it should no longer include any "Fixing bundle..." messages + log.debug("================================================ Rechecking workspace " + WORKSPACE + "..."); + repository.checkConsistency(WORKSPACE, null, false, false); + + repository.shutdown(); + repository = createRepository(); + + session = login(WORKSPACE); + assertPathNotFoundException(session.getRootNode(), "fruits/bananas"); + assertPathNotFoundException(session.getRootNode(), "fruits/melons/watermelon"); + } + + public void testConsistencyCheckVersioning() throws Exception { + log.debug(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> versioning storage..."); + // 1. versioning test, setup data + setupVersioningTestData(); + + log.debug("================================================ Checking " + JCR_SYSTEM + "..."); + repository.checkConsistency(JCR_SYSTEM, null, false, false); + } + + public void testConsistencyFixVersioning() throws Exception { + // ensure all item state caches are empty + repository.shutdown(); + repository = createRepository(); + + Session session = login(WORKSPACE); + + // for debugging + //displayTree(session.getRootNode().getNode("jcr:system/jcr:versionStorage")); + + Node node = session.getRootNode().getNode("food"); + assertRepositoryException(node.getVersionHistory().getVersion(brokenVersion), "jcr:frozenNode/values"); + + log.debug("================================================ Checking and fixing " + JCR_SYSTEM + "..."); + repository.checkConsistency(JCR_SYSTEM, null, false, true); + + // check the log here, it should no longer include any "Fixing bundle..." messages + log.debug("================================================ Rechecking " + JCR_SYSTEM + "..."); + repository.checkConsistency(JCR_SYSTEM, null, false, false); + + repository.shutdown(); + repository = createRepository(); + + session = login(WORKSPACE); + node = session.getRootNode().getNode("food"); + assertPathNotFoundException(node.getVersionHistory().getVersion(brokenVersion), "jcr:frozenNode/values"); + } + + public void testRepositoryShutdown() { + // last test, stop (not necessarily needed, but cleans up file system) + //deleteRepository(); + } +} Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/TestAll.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/TestAll.java (revision 631607) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/TestAll.java (working copy) @@ -36,6 +36,7 @@ public static Test suite() { TestSuite suite = new TestSuite("Persistence tests"); + suite.addTestSuite(ConsistencyCheckTest.class); suite.addTestSuite(DatabaseConnectionFailureTest.class); return suite;