org.lockss.repository.TestRepositoryNodeImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.lockss.repository.TestRepositoryNodeImpl.java

Source

/*
 * $Id$
 */

/*
    
Copyright (c) 2000-2016 Board of Trustees of Leland Stanford Jr. University,
all rights reserved.
    
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
    
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
    
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
STANFORD UNIVERSITY BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    
Except as contained in this notice, the name of Stanford University shall not
be used in advertising or otherwise to promote the sale, use or other dealings
in this Software without prior written authorization from Stanford University.
    
*/

package org.lockss.repository;

import java.io.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.net.*;
import java.util.*;
import org.apache.commons.lang3.StringUtils;

import org.lockss.test.*;
import org.lockss.app.*;
import org.lockss.util.*;
import org.lockss.daemon.*;
import org.lockss.plugin.*;
import org.lockss.protocol.*;

/**
 * This is the test class for org.lockss.repository.RepositoryNodeImpl
 */
public class TestRepositoryNodeImpl extends LockssTestCase {
    static final String TREE_SIZE_PROPERTY = RepositoryNodeImpl.TREE_SIZE_PROPERTY;
    static final String CHILD_COUNT_PROPERTY = RepositoryNodeImpl.CHILD_COUNT_PROPERTY;

    private MockLockssDaemon theDaemon;
    private MyLockssRepositoryImpl repo;
    private String tempDirPath;
    MockArchivalUnit mau;

    private MockIdentityManager idmgr;

    Properties props;

    public void setUp() throws Exception {
        super.setUp();
        tempDirPath = getTempDir().getAbsolutePath() + File.separator;
        props = new Properties();
        props.setProperty(LockssRepositoryImpl.PARAM_CACHE_LOCATION, tempDirPath);
        ConfigurationUtil.setCurrentConfigFromProps(props);

        mau = new MockArchivalUnit();

        theDaemon = getMockLockssDaemon();

        // Create the identity manager...
        idmgr = new MockIdentityManager();
        theDaemon.setIdentityManager(idmgr);
        idmgr.initService(theDaemon);

        repo = (MyLockssRepositoryImpl) MyLockssRepositoryImpl.createNewLockssRepository(mau);
        theDaemon.setAuManager(LockssDaemon.LOCKSS_REPOSITORY, mau, repo);
        repo.initService(theDaemon);
        repo.startService();
    }

    public void tearDown() throws Exception {
        TimeBase.setReal();
        repo.stopService();
        theDaemon.stopDaemon();
        super.tearDown();
    }

    // RepositoryNodeImpl relies on nonexistent dir.listFiles() returning
    // null, not empty list.
    public void testFileAssumptions() throws Exception {
        // empty dir returns empty list
        File dir1 = getTempDir();
        assertNotNull(null, dir1.listFiles());
        assertEquals(new File[0], dir1.listFiles());
        // nonexistent dir returns null
        File dir2 = new File(dir1, "bacds");
        assertNull(null, dir2.listFiles());
        // dir list of non-dir returns null
        File file1 = File.createTempFile("xxx", ".tmp", dir1);
        assertTrue(file1.exists());
        assertNull(null, file1.listFiles());
    }

    // Ensure that our maximum filename component calculation agrees with
    // reality.

    File root;
    int maxDirname;
    int preflen;
    int sufflen;
    String pref;
    String suff;

    public void testUnicodeAssumptions() throws Exception {
        assertEquals("1234", trimTo("1234", 4));
        assertEquals("1234", trimTo("12345", 4));
        assertEquals("1234", trimTo("123454444444", 4));

        root = getTempDir();
        assertTrue(canMkdir(root, "xyz" + "\u00e9"));
        maxDirname = findMaxDirname(root);
        log.info("Max dirname on " + root + ": " + maxDirname);
        if (maxDirname < 30) {
            log.critical("Skipping test because filesystem is inadequate for LOCKSS");
            return;
        }
        preflen = (maxDirname - 10) / 2;
        sufflen = maxDirname - preflen;
        pref = mkstr(preflen);
        suff = mkstr(sufflen);

        String t1 = pref + suff;
        String t2 = pref + "e" + suff;
        assertTrue(byteLength(t1) == t1.length());
        assertTrue(canMkdir(root, t1));
        assertFalse(canMkdir(root, t2));

        String uniStrs[] = {

                "\u00e9", // Latin1 Supplement
                "\u0113", // Latin Extended-A
                "\u01a2", // Latin Extended-B
                "\u025a", // IPA Extensions
                "\u0393", // Greek
                "\u0409", // Cyrillic
                "\u05d2", // Hebrew
                "\u062c", // Arabic
                "\u0ab2", // Gujarati
                "\u0E28", // Thai
                "\u0EC0", // Lao
                "\u0F44", // Tibetan
                "\u305D", // Hiragana
                "\u30AE", // Katakana
                "\u2EA8", // CJK Radicals Supplement
                "\u3028", // CJK Symbols and Punctuation
                "\u4E05", // CJK Unified Ideographs
                "\uAC07", // Hangul Syllables
        };

        // One unicode char
        for (String s : uniStrs) {
            testUni(s);
        }

        // Two unicode chars
        for (String s1 : uniStrs) {
            for (String s2 : uniStrs) {
                testUni(s1 + s2);
            }
        }
    }

    void testUni(String unistr) {
        String str = pref + unistr + suff;

        int failat = -1;
        int probelen = str.length();
        while (probelen >= 1) {
            String probe = trimTo(str, probelen);

            int blen = byteLength(probe);
            boolean should = blen <= maxDirname;
            boolean does = canMkdir(root, probe);
            log.debug2("probelen: " + probe.length() + ", byte: " + byteLength(probe) + ": " + does);
            if (should) {
                if (does) {
                    return;
                }
                fail("foo");
            }
            probelen--;
        }
    }

    static String trimTo(String s, int len) {
        return s.substring(0, len);
    }

    int byteLength(String s) {
        return RepositoryNodeImpl.byteLength(s);
    }

    int findMaxDirname(File root) {
        for (int len = 1; len < 1000; len++) {
            if (!canMkdir(root, len)) {
                return len - 1;
            }
        }
        return -1;
    }

    void findMaxDirPath(File root) {
        int maxName = findMaxDirname(root) - 10;
        String one = mkstr("onedir", maxName) + "/";
        for (int rpt = 1; rpt < 1000; rpt++) {
            String path = StringUtils.repeat(one, rpt);
            File dir = new File(root, path);
            String dirstr = dir.getPath();
            boolean res = dir.mkdirs();
            if (!res) {
                log.info("mkdirs failed at " + dirstr.length() + " chars");
                break;
            }
            log.info("mkdirs ok: " + dirstr.length());
            File f = new File(dir, "foobbb");
            try {
                OutputStream os = new FileOutputStream(f);
                os.close();
                log.info("file ok at " + f.getPath().length() + " chars");
            } catch (FileNotFoundException fnfe) {
                log.info("FNF: " + f.getPath().length(), fnfe);
            } catch (IOException ioe) {
                log.error("IOE: " + f.getPath().length() + ", " + ioe.getMessage());
            }
        }
    }

    void findMaxDirPathNio(File root) {
        int maxName = findMaxDirname(root) - 10;
        String one = mkstr("onedir", maxName) + "/";
        for (int rpt = 1; rpt < 1000; rpt++) {
            String path = StringUtils.repeat(one, rpt);
            File dir = new File(root, path);
            String dirstr = dir.getPath();
            boolean res = dir.mkdirs();
            if (!res) {
                log.info("mkdirs failed at " + dirstr.length() + " chars");
                break;
            }
            log.info("mkdirs ok: " + dirstr.length());
            File f = new File(dir, "foobbb");
            try {
                Path npath = Paths.get(f.getPath());
                Files.createFile(npath);
                FileChannel ochan = FileChannel.open(npath, StandardOpenOption.WRITE);
                OutputStream os = Channels.newOutputStream(ochan);
                os.write((byte) 44);
                os.close();

                FileChannel ichan = FileChannel.open(npath, StandardOpenOption.READ);
                InputStream is = Channels.newInputStream(ichan);
                int bb = is.read();
                is.close();
                assertEquals(44, bb);
                log.info("file ok at " + npath.toString().length() + " chars");
            } catch (FileNotFoundException fnfe) {
                log.error("FNF: " + f.getPath().length(), fnfe);
            } catch (IOException ioe) {
                log.error("IOE: " + f.getPath().length() + ", " + ioe.getMessage());
            }
        }
    }

    boolean canMkdir(File root, int len) {
        return canMkdir(root, mkstr(len));
    }

    boolean canMkdir(String root, String name) {
        return canMkdir(new File(root), name);
    }

    boolean canMkdir(File root, String name) {
        File f = new File(root, name);
        boolean res = f.mkdirs();
        if (!res) {
            if (f.exists()) {
                throw new RuntimeException("mkdirs = false but dir exists: " + f);
            }
            log.debug2("canMkdir(" + f + "): false");
            return false;
        }
        if (f.exists()) {
            if (!f.delete()) {
                throw new RuntimeException("Couldn't delete newly created dir: " + f);
            }
            if (f.exists()) {
                throw new RuntimeException("Deleted newly created dir still exists: " + f);
            }
            log.debug2("canMkdir(" + f + "): true");
            return true;
        }
        throw new RuntimeException("mkdirs() == true but exists() == false: " + f);
    }

    public void testMkstr() {
        for (int ix = 0; ix < 1000; ix++) {
            assertEquals(ix, mkstr(ix).length());
        }
    }

    String mkstr(int len) {
        return mkstr("abcdefghijklmnopqrstuvwxyz0123456789", len);
    }

    String mkstr(String al, int len) {
        StringBuilder sb = new StringBuilder(len);
        for (int ix = 1; ix <= len / al.length(); ix++) {
            sb.append(al);
        }
        sb.append(al.substring(0, len % al.length()));
        if (sb.length() != len) {
            throw new RuntimeException("mkstr(" + len + ") made string w/ len: " + sb.length());
        }
        return sb.toString();
    }

    public void testGetNodeUrl() {
        RepositoryNode node = new RepositoryNodeImpl("testUrl", "testDir", null);
        assertEquals("testUrl", node.getNodeUrl());
        node = new RepositoryNodeImpl("testUrl/test.txt", "testUrl/test.txt", null);
        assertEquals("testUrl/test.txt", node.getNodeUrl());
    }

    public void testFileLocation() throws Exception {
        RepositoryNode leaf = createLeaf("http://www.example.com/testDir/branch1/leaf1", "test stream", null);
        tempDirPath = LockssRepositoryImpl.mapAuToFileLocation(tempDirPath, mau);
        tempDirPath = LockssRepositoryImpl.mapUrlToFileLocation(tempDirPath,
                "http://www.example.com/testDir/branch1/leaf1");
        File testFile = new File(tempDirPath);
        assertTrue(testFile.exists());
        testFile = new File(tempDirPath + "/#content/current");
        assertTrue(testFile.exists());
        testFile = new File(tempDirPath + "/#content/current.props");
        assertTrue(testFile.exists());
        testFile = new File(tempDirPath + "/#node_props");
        assertFalse(testFile.exists());
        testFile = new File(tempDirPath + "/#agreement");
        assertFalse(testFile.exists());
    }

    public void testUpdateAgreementCreatesFile() throws Exception {
        RepositoryNode leaf = createLeaf("http://www.example.com/testDir/branch1/leaf1", "test stream", null);
        tempDirPath = LockssRepositoryImpl.mapAuToFileLocation(tempDirPath, mau);
        tempDirPath = LockssRepositoryImpl.mapUrlToFileLocation(tempDirPath,
                "http://www.example.com/testDir/branch1/leaf1");
        File testFile = new File(tempDirPath, "#agreement");
        assertFalse(testFile.exists());

        // Agreeing IDs.
        PeerIdentity[] agreeingPeers = { new MockPeerIdentity("TCP:[192.168.0.1]:9723"),
                new MockPeerIdentity("TCP:[192.168.0.2]:9723") };

        leaf.signalAgreement(ListUtil.fromArray(agreeingPeers));
        assertTrue(testFile.exists());
    }

    public void testUpdateAndLoadAgreement() throws Exception {
        RepositoryNode leaf = createLeaf("http://www.example.com/testDir/branch1/leaf1", "test stream", null);
        tempDirPath = LockssRepositoryImpl.mapAuToFileLocation(tempDirPath, mau);
        tempDirPath = LockssRepositoryImpl.mapUrlToFileLocation(tempDirPath,
                "http://www.example.com/testDir/branch1/leaf1");
        PeerIdentity testid_1 = new MockPeerIdentity("TCP:[192.168.0.1]:9723");
        PeerIdentity testid_2 = new MockPeerIdentity("TCP:[192.168.0.2]:9723");
        PeerIdentity testid_3 = new MockPeerIdentity("TCP:[192.168.0.3]:9723");
        PeerIdentity testid_4 = new MockPeerIdentity("TCP:[192.168.0.4]:9723");

        idmgr.addPeerIdentity(testid_1.getIdString(), testid_1);
        idmgr.addPeerIdentity(testid_2.getIdString(), testid_2);
        idmgr.addPeerIdentity(testid_3.getIdString(), testid_3);
        idmgr.addPeerIdentity(testid_4.getIdString(), testid_4);

        leaf.signalAgreement(ListUtil.list(testid_1, testid_3));

        assertEquals(2, ((RepositoryNodeImpl) leaf).loadAgreementHistory().size());

        assertTrue(leaf.hasAgreement(testid_1));
        assertFalse(leaf.hasAgreement(testid_2));
        assertTrue(leaf.hasAgreement(testid_3));
        assertFalse(leaf.hasAgreement(testid_4));

        leaf.signalAgreement(ListUtil.list(testid_1, testid_2, testid_3, testid_4));

        assertEquals(4, ((RepositoryNodeImpl) leaf).loadAgreementHistory().size());

        assertTrue(leaf.hasAgreement(testid_1));
        assertTrue(leaf.hasAgreement(testid_2));
        assertTrue(leaf.hasAgreement(testid_3));
        assertTrue(leaf.hasAgreement(testid_4));
    }

    public void testVersionFileLocation() throws Exception {
        RepositoryNode leaf = createLeaf("http://www.example.com/testDir/branch1/leaf1", "test stream", null);
        tempDirPath = LockssRepositoryImpl.mapAuToFileLocation(tempDirPath, mau);
        tempDirPath = LockssRepositoryImpl.mapUrlToFileLocation(tempDirPath,
                "http://www.example.com/testDir/branch1/leaf1");
        File testFile = new File(tempDirPath + "/#content/1");
        assertFalse(testFile.exists());
        testFile = new File(tempDirPath + "/#content/1.props");
        assertFalse(testFile.exists());

        leaf.makeNewVersion();
        OutputStream os = leaf.getNewOutputStream();
        InputStream is = new StringInputStream("test stream 2");
        StreamUtil.copy(is, os);
        is.close();
        os.close();
        leaf.setNewProperties(new Properties());
        leaf.sealNewVersion();
        assertFalse(leaf.isIdenticalVersion());
        testFile = new File(tempDirPath + "/#content/1");
        assertTrue(testFile.exists());
        testFile = new File(tempDirPath + "/#content/1.props");
        assertTrue(testFile.exists());
    }

    public void testInactiveFileLocation() throws Exception {
        RepositoryNode leaf = createLeaf("http://www.example.com/testDir/branch1/leaf1", "test stream", null);
        tempDirPath = LockssRepositoryImpl.mapAuToFileLocation(tempDirPath, mau);
        tempDirPath = LockssRepositoryImpl.mapUrlToFileLocation(tempDirPath,
                "http://www.example.com/testDir/branch1/leaf1");
        File curFile = new File(tempDirPath + "/#content/current");
        File curPropsFile = new File(tempDirPath + "/#content/current.props");
        File inactFile = new File(tempDirPath + "/#content/inactive");
        File inactPropsFile = new File(tempDirPath + "/#content/inactive.props");
        assertTrue(curFile.exists());
        assertTrue(curPropsFile.exists());
        assertFalse(inactFile.exists());
        assertFalse(inactPropsFile.exists());

        leaf.deactivateContent();
        assertFalse(curFile.exists());
        assertFalse(curPropsFile.exists());
        assertTrue(inactFile.exists());
        assertTrue(inactPropsFile.exists());

        //reactivate
        leaf.restoreLastVersion();
        assertTrue(curFile.exists());
        assertTrue(curPropsFile.exists());
        assertFalse(inactFile.exists());
        assertFalse(inactPropsFile.exists());

        leaf.deactivateContent();
        assertFalse(curFile.exists());
        assertFalse(curPropsFile.exists());
        assertTrue(inactFile.exists());
        assertTrue(inactPropsFile.exists());

        // make new version
        leaf.makeNewVersion();
        OutputStream os = leaf.getNewOutputStream();
        InputStream is = new StringInputStream("test stream 2");
        StreamUtil.copy(is, os);
        is.close();
        os.close();
        leaf.setNewProperties(new Properties());
        leaf.sealNewVersion();
        assertTrue(curFile.exists());
        assertTrue(curPropsFile.exists());
        assertFalse(inactFile.exists());
        assertFalse(inactPropsFile.exists());
        assertFalse(leaf.isIdenticalVersion());
    }

    public void testDeleteFileLocation() throws Exception {
        RepositoryNode leaf = createLeaf("http://www.example.com/testDir/branch1/leaf1", "test stream", null);
        tempDirPath = LockssRepositoryImpl.mapAuToFileLocation(tempDirPath, mau);
        tempDirPath = LockssRepositoryImpl.mapUrlToFileLocation(tempDirPath,
                "http://www.example.com/testDir/branch1/leaf1");
        File curFile = new File(tempDirPath + "/#content/current");
        File curPropsFile = new File(tempDirPath + "/#content/current.props");
        File inactFile = new File(tempDirPath + "/#content/inactive");
        File inactPropsFile = new File(tempDirPath + "/#content/inactive.props");
        assertTrue(curFile.exists());
        assertTrue(curPropsFile.exists());
        assertFalse(inactFile.exists());
        assertFalse(inactPropsFile.exists());

        leaf.markAsDeleted();
        assertFalse(curFile.exists());
        assertFalse(curPropsFile.exists());
        assertTrue(inactFile.exists());
        assertTrue(inactPropsFile.exists());

        //reactivate
        leaf.restoreLastVersion();
        assertTrue(curFile.exists());
        assertTrue(curPropsFile.exists());
        assertFalse(inactFile.exists());
        assertFalse(inactPropsFile.exists());

        leaf.markAsDeleted();
        assertFalse(curFile.exists());
        assertFalse(curPropsFile.exists());
        assertTrue(inactFile.exists());
        assertTrue(inactPropsFile.exists());

        // make new version
        leaf.makeNewVersion();
        OutputStream os = leaf.getNewOutputStream();
        InputStream is = new StringInputStream("test stream 2");
        StreamUtil.copy(is, os);
        is.close();
        os.close();
        leaf.setNewProperties(new Properties());
        leaf.sealNewVersion();
        assertTrue(curFile.exists());
        assertTrue(curPropsFile.exists());
        assertFalse(inactFile.exists());
        assertFalse(inactPropsFile.exists());
        assertFalse(leaf.isIdenticalVersion());
    }

    public void testListEntriesNonexistentDir() throws Exception {
        RepositoryNode node = new RepositoryNodeImpl("foo-no-url", "foo-no-dir", null);
        try {
            node.listChildren(null, false);
            fail("listChildren() is nonexistent dir should throw");
        } catch (LockssRepository.RepositoryStateException e) {
        }
    }

    public void testListEntries() throws Exception {
        createLeaf("http://www.example.com/testDir/branch1/leaf1", "test stream", null);
        createLeaf("http://www.example.com/testDir/branch1/leaf2", "test stream", null);
        createLeaf("http://www.example.com/testDir/branch2/leaf3", "test stream", null);
        createLeaf("http://www.example.com/testDir/branch2", "test stream", null);
        createLeaf("http://www.example.com/testDir/leaf4", "test stream", null);

        // root branch
        RepositoryNode dirEntry = repo.getNode("http://www.example.com/testDir");
        Iterator childIt = dirEntry.listChildren(null, false);
        ArrayList childL = new ArrayList(3);
        while (childIt.hasNext()) {
            RepositoryNode node = (RepositoryNode) childIt.next();
            childL.add(node.getNodeUrl());
        }
        String[] expectedA = new String[] { "http://www.example.com/testDir/branch1",
                "http://www.example.com/testDir/branch2", "http://www.example.com/testDir/leaf4" };
        assertSameElements(expectedA, childL);

        // sub-branch
        dirEntry = repo.getNode("http://www.example.com/testDir/branch1");
        childL.clear();
        childIt = dirEntry.listChildren(null, false);
        while (childIt.hasNext()) {
            RepositoryNode node = (RepositoryNode) childIt.next();
            childL.add(node.getNodeUrl());
        }
        expectedA = new String[] { "http://www.example.com/testDir/branch1/leaf1",
                "http://www.example.com/testDir/branch1/leaf2", };
        assertSameElements(expectedA, childL);

        // sub-branch with content
        dirEntry = repo.getNode("http://www.example.com/testDir/branch2");
        childL.clear();
        childIt = dirEntry.listChildren(null, false);
        while (childIt.hasNext()) {
            RepositoryNode node = (RepositoryNode) childIt.next();
            childL.add(node.getNodeUrl());
        }
        expectedA = new String[] { "http://www.example.com/testDir/branch2/leaf3", };
        assertSameElements(expectedA, childL);

        // leaf node
        dirEntry = repo.getNode("http://www.example.com/testDir/branch1/leaf1");
        childL.clear();
        childIt = dirEntry.listChildren(null, false);
        while (childIt.hasNext()) {
            RepositoryNode node = (RepositoryNode) childIt.next();
            childL.add(node.getNodeUrl());
        }
        expectedA = new String[] {};
        assertSameElements(expectedA, childL);
    }

    String normalizeName(RepositoryNodeImpl node, String name) {
        return node.normalize(new File(name)).getPath();
    }

    public void testNormalizeUrlEncodingCase() throws Exception {
        if (!PlatformUtil.getInstance().isCaseSensitiveFileSystem()) {
            log.debug("Skipping testNormalizeUrlEncodingCase: file system is not case sensitive.");
            return;
        }
        RepositoryNodeImpl node = new RepositoryNodeImpl("foo", "bar", null);
        // nothing to normalize
        File file = new File("foo/bar/baz");
        assertSame(file, node.normalize(file));
        file = new File("foo/bar/ba%ABz");
        assertSame(file, node.normalize(file));
        // unnormalized in parent dir name is left alone
        file = new File("ba%abz/bar");
        assertSame(file, node.normalize(file));
        file = new File("foo/ba%abz/bar");
        assertSame(file, node.normalize(file));
        // should be normalized
        assertEquals("ba%ABz", normalizeName(node, "ba%aBz"));
        assertEquals("/ba%ABz", normalizeName(node, "/ba%aBz"));
        assertEquals("foo/bar/ba%ABz", normalizeName(node, "foo/bar/ba%aBz"));
        assertEquals("foo/bar/ba%ABz", normalizeName(node, "foo/bar/ba%Abz"));
        assertEquals("foo/bar/ba%ABz", normalizeName(node, "foo/bar/ba%abz"));
        assertEquals("foo/bar/ba%abz/ba%ABz", normalizeName(node, "foo/bar/ba%abz/ba%abz"));
    }

    public void testNormalizeTrailingQuestion() throws Exception {
        RepositoryNodeImpl node = new RepositoryNodeImpl("foo", "bar", null);
        // nothing to normalize
        File file = new File("foo/bar/baz");
        assertSame(file, node.normalize(file));
        file = new File("foo/bar/ba?z");
        assertSame(file, node.normalize(file));
        // unnormalized in parent dir name is left alone
        file = new File("ba?/bar");
        assertSame(file, node.normalize(file));
        // should be normalized
        assertEquals("baz", normalizeName(node, "baz?"));
        assertEquals(new File("/ba").getPath(), normalizeName(node, "/ba?"));
        assertEquals(new File("foo/bar/bar").getPath(), normalizeName(node, "foo/bar/bar?"));
        assertEquals(new File("foo/ba?r/bar").getPath(), normalizeName(node, "foo/ba?r/bar?"));
        assertEquals(new File("foo/bar?/bar").getPath(), normalizeName(node, "foo/bar?/bar?"));

        // disable trailing ? normalization
        ConfigurationUtil.addFromArgs(UrlUtil.PARAM_NORMALIZE_EMPTY_QUERY, "false");
        assertEquals("baz?", normalizeName(node, "baz?"));
    }

    List getChildNames(String nodeName) throws MalformedURLException {
        RepositoryNode dirEntry = repo.getNode(nodeName);
        ArrayList res = new ArrayList();
        for (Iterator childIt = dirEntry.listChildren(null, false); childIt.hasNext();) {
            RepositoryNode node = (RepositoryNode) childIt.next();
            log.debug2("node: " + node);
            res.add(node.getNodeUrl());
        }
        return res;
    }

    public void testFixUnnormalized_Rename() throws Exception {
        if (!PlatformUtil.getInstance().isCaseSensitiveFileSystem()) {
            log.debug("Skipping testFixUnnormalized_Rename: file system is not case sensitive.");
            return;
        }

        repo.setDontNormalize(true);
        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "No");
        createLeaf("http://www.example.com/testDir/branch%3c1/leaf%2C1", "test stream", null);
        createLeaf("http://www.example.com/testDir/branch%3c1/leaf%2c2", "test stream", null);
        createLeaf("http://www.example.com/testDir/branch2/leaf3", "test stream", null);
        createLeaf("http://www.example.com/testDir/branch2", "test stream", null);
        createLeaf("http://www.example.com/testDir/leaf4", "test stream", null);

        String[] expectedA = new String[] { "http://www.example.com/testDir/branch%3c1",
                "http://www.example.com/testDir/branch2", "http://www.example.com/testDir/leaf4" };
        assertSameElements(expectedA, getChildNames(("http://www.example.com/testDir")));

        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "Log");
        assertSameElements(expectedA, getChildNames(("http://www.example.com/testDir")));

        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "Fix");
        String[] expectedB = new String[] { "http://www.example.com/testDir/branch%3C1",
                "http://www.example.com/testDir/branch2", "http://www.example.com/testDir/leaf4" };
        assertSameElements(expectedB, getChildNames(("http://www.example.com/testDir")));

        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "No");
        assertSameElements(expectedB, getChildNames(("http://www.example.com/testDir")));

        String[] expectedC = new String[] { "http://www.example.com/testDir/branch%3C1/leaf%2C1",
                "http://www.example.com/testDir/branch%3C1/leaf%2c2", };
        assertSameElements(expectedC, getChildNames(("http://www.example.com/testDir/branch%3C1")));

        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "Log");
        assertSameElements(expectedB, getChildNames(("http://www.example.com/testDir")));

        assertSameElements(expectedC, getChildNames(("http://www.example.com/testDir/branch%3C1")));

        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "Fix");
        assertSameElements(expectedB, getChildNames(("http://www.example.com/testDir")));

        String[] expectedD = new String[] { "http://www.example.com/testDir/branch%3C1/leaf%2C1",
                "http://www.example.com/testDir/branch%3C1/leaf%2C2", };
        assertSameElements(expectedD, getChildNames(("http://www.example.com/testDir/branch%3C1")));
    }

    public void testFixUnnormalizedMultiple_Delete() throws Exception {
        if (!PlatformUtil.getInstance().isCaseSensitiveFileSystem()) {
            log.debug("Skipping testFixUnnormalizedMultiple_Delete: file system is not case sensitive.");
            return;
        }
        repo.setDontNormalize(true);
        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "No");
        createLeaf("http://www.example.com/testDir/leaf%2C1", "test stream", null);
        createLeaf("http://www.example.com/testDir/leaf%2c1", "test stream", null);
        createLeaf("http://www.example.com/testDir/leaf3", "test stream", null);

        String[] expectedA = new String[] { "http://www.example.com/testDir/leaf%2C1",
                "http://www.example.com/testDir/leaf%2c1", "http://www.example.com/testDir/leaf3", };
        assertSameElements(expectedA, getChildNames(("http://www.example.com/testDir")));

        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "Log");
        assertSameElements(expectedA, getChildNames(("http://www.example.com/testDir")));

        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "Fix");
        String[] expectedB = new String[] { "http://www.example.com/testDir/leaf%2C1",
                "http://www.example.com/testDir/leaf3", };
        assertSameElements(expectedB, getChildNames(("http://www.example.com/testDir")));

        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "No");
        assertSameElements(expectedB, getChildNames(("http://www.example.com/testDir")));
    }

    public void testFixUnnormalizedMultiple_DeleteMultiple() throws Exception {
        if (!PlatformUtil.getInstance().isCaseSensitiveFileSystem()) {
            log.debug("Skipping testFixUnnormalizedMultiple_DeleteMultiple: file system is not case sensitive.");
            return;
        }
        repo.setDontNormalize(true);
        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "No");
        createLeaf("http://www.example.com/testDir/leaf%CA%3E", "test stream", null);
        createLeaf("http://www.example.com/testDir/leaf%cA%3E", "test stream", null);
        createLeaf("http://www.example.com/testDir/leaf%ca%3E", "test stream", null);
        createLeaf("http://www.example.com/testDir/leaf%ca%3e", "test stream", null);
        createLeaf("http://www.example.com/testDir/leaf3", "test stream", null);

        String[] expectedA = new String[] { "http://www.example.com/testDir/leaf%CA%3E",
                "http://www.example.com/testDir/leaf%cA%3E", "http://www.example.com/testDir/leaf%ca%3E",
                "http://www.example.com/testDir/leaf%ca%3e", "http://www.example.com/testDir/leaf3", };
        assertSameElements(expectedA, getChildNames(("http://www.example.com/testDir")));

        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "Log");
        assertSameElements(expectedA, getChildNames(("http://www.example.com/testDir")));

        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "Fix");
        String[] expectedB = new String[] { "http://www.example.com/testDir/leaf%CA%3E",
                "http://www.example.com/testDir/leaf3", };
        assertSameElements(expectedB, getChildNames(("http://www.example.com/testDir")));

        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "No");
        assertSameElements(expectedB, getChildNames(("http://www.example.com/testDir")));
    }

    public void testFixUnnormalized_DontFixParent() throws Exception {
        if (!PlatformUtil.getInstance().isCaseSensitiveFileSystem()) {
            log.debug("Skipping testFixUnnormalized_DontFixParent: file system is not case sensitive.");
            return;
        }
        repo.setDontNormalize(true);
        createLeaf("http://www.example.com/testDir/branch%3c1/leaf%2C1", "test stream", null);
        createLeaf("http://www.example.com/testDir/branch%3c1/leaf%2c2", "test stream", null);

        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "Fix");
        String[] expectedA = new String[] { "http://www.example.com/testDir/branch%3c1/leaf%2C1",
                "http://www.example.com/testDir/branch%3c1/leaf%2C2", };
        assertSameElements(expectedA, getChildNames(("http://www.example.com/testDir/branch%3c1")));
    }

    public void testUnnormalizedIterate() throws Exception {
        if (!PlatformUtil.getInstance().isCaseSensitiveFileSystem()) {
            log.debug("Skipping testUnnormalizedIterate: file system is not case sensitive.");
            return;
        }
        repo.setDontNormalize(true);
        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "No");
        createLeaf("http://www.example.com/testDir/leaf1", "test stream", null);
        createLeaf("http://www.example.com/testDir/leaf%2c1", "test stream", null);
        createLeaf("http://www.example.com/testDir/leaf3", "test stream", null);
        repo.setDontNormalize(false);

        // Unnormalized name in result of listChildren not included in result
        // because name gets normalized when node created, thus not found
        String[] expectedA = new String[] { "http://www.example.com/testDir/leaf1",
                "http://www.example.com/testDir/leaf3", };
        assertSameElements(expectedA, getChildNames(("http://www.example.com/testDir")));

        // Same with checkUnnormalized = Log
        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "Log");
        assertSameElements(expectedA, getChildNames(("http://www.example.com/testDir")));

        // If checkUnnormalized = Fix, unnnormalized file in repo will be fixed
        // and inluded in result
        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_CHECK_UNNORMALIZED, "Fix");
        String[] expectedB = new String[] { "http://www.example.com/testDir/leaf1",
                "http://www.example.com/testDir/leaf%2C1", "http://www.example.com/testDir/leaf3", };
        assertSameElements(expectedB, getChildNames(("http://www.example.com/testDir")));
    }

    public void testEntrySort() throws Exception {
        createLeaf("http://www.example.com/testDir/branch2/leaf1", null, null);
        createLeaf("http://www.example.com/testDir/leaf4", null, null);
        createLeaf("http://www.example.com/testDir/branch1/leaf1", null, null);
        createLeaf("http://www.example.com/testDir/leaf3", null, null);

        RepositoryNode dirEntry = repo.getNode("http://www.example.com/testDir");
        Iterator childIt = dirEntry.listChildren(null, false);
        ArrayList childL = new ArrayList(4);
        while (childIt.hasNext()) {
            RepositoryNode node = (RepositoryNode) childIt.next();
            childL.add(node.getNodeUrl());
        }
        String[] expectedA = new String[] { "http://www.example.com/testDir/branch1",
                "http://www.example.com/testDir/branch2", "http://www.example.com/testDir/leaf3",
                "http://www.example.com/testDir/leaf4" };
        assertSameElements(expectedA, childL);
    }

    public void testIllegalOperations() throws Exception {
        RepositoryNode leaf = repo.createNewNode("http://www.example.com/testDir/test.cache");
        assertFalse(leaf.hasContent());
        try {
            leaf.getCurrentVersion();
            fail("Cannot get current version if no content.");
        } catch (UnsupportedOperationException uoe) {
        }
        try {
            leaf.getContentSize();
            fail("Cannot get content size if no content.");
        } catch (UnsupportedOperationException uoe) {
        }
        try {
            leaf.getNodeContents();
            fail("Cannot get RepositoryNodeContents if no content.");
        } catch (UnsupportedOperationException uoe) {
        }
        try {
            leaf.sealNewVersion();
            fail("Cannot seal version if not open.");
        } catch (UnsupportedOperationException uoe) {
        }
        leaf.makeNewVersion();
        try {
            leaf.sealNewVersion();
            fail("Cannot seal version if getNewOutputStream() uncalled.");
        } catch (UnsupportedOperationException uoe) {
        }
        leaf.makeNewVersion();
        try {
            leaf.deactivateContent();
            fail("Cannot deactivate if currently open for writing.");
        } catch (UnsupportedOperationException uoe) {
        }
        writeToLeaf(leaf, "test stream");
        try {
            leaf.sealNewVersion();
            fail("Cannot seal version if setNewProperties() uncalled.");
        } catch (UnsupportedOperationException uoe) {
        }
        leaf.makeNewVersion();
        writeToLeaf(leaf, "test stream");
        leaf.setNewProperties(new Properties());
        leaf.sealNewVersion();
        assertEquals(1, leaf.getCurrentVersion());
        assertTrue(leaf.hasContent());
        assertFalse(leaf.isIdenticalVersion());
    }

    public void testVersionTimeout() throws Exception {
        TimeBase.setSimulated();
        RepositoryNode leaf = repo.createNewNode("http://www.example.com/testDir/test.cache");
        RepositoryNode leaf2 = repo.getNode("http://www.example.com/testDir/test.cache");
        leaf.makeNewVersion();
        try {
            leaf2.makeNewVersion();
            fail("Can't make new version while version open.");
        } catch (UnsupportedOperationException e) {
        }
        TimeBase.step(RepositoryNodeImpl.DEFAULT_VERSION_TIMEOUT / 2);
        try {
            leaf2.makeNewVersion();
            fail("Can't make new version while version not timed out.");
        } catch (UnsupportedOperationException e) {
        }
        TimeBase.step(RepositoryNodeImpl.DEFAULT_VERSION_TIMEOUT / 2);
        leaf2.makeNewVersion();
    }

    public void testMakeNewCache() throws Exception {
        RepositoryNode leaf = repo.createNewNode("http://www.example.com/testDir/test.cache");
        assertFalse(leaf.hasContent());
        try {
            leaf.getCurrentVersion();
            fail("Cannot get current version if no content.");
        } catch (UnsupportedOperationException uoe) {
        }
        leaf.makeNewVersion();
        writeToLeaf(leaf, "test stream");
        leaf.setNewProperties(new Properties());
        leaf.sealNewVersion();
        assertTrue(leaf.hasContent());
        assertEquals(1, leaf.getCurrentVersion());
    }

    static String LONG_URL = "http://ijs.macroeconomicsresearch.org/articles/renderlist.action/fmt=ahah&items=http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY585,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY592,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY591,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY593,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY594,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY601,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY602,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY603,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY604,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY611,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY614,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY619,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY618,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY620,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY621,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY626,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY629,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY630,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY635,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY633,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY637,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY639,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY643,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY649,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY650,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY652,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY655,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY656,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY659,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY666,http:s/sgm.metawrite.magenta.com/content/journal/ijsem/42.867-5309/ijsem.0.XXXYYY673";

    public void testLongPath() throws Exception {
        //     findMaxDirPath(getTempDir());
        //     findMaxDirPathNio(getTempDir());

        String longUrl = trimUrlForOs(LONG_URL);

        RepositoryNode leaf = repo.createNewNode(longUrl);
        assertFalse(leaf.hasContent());
        try {
            leaf.getCurrentVersion();
            fail("Cannot get current version if no content.");
        } catch (UnsupportedOperationException uoe) {
        }
        leaf.makeNewVersion();
        writeToLeaf(leaf, "test stream");
        leaf.setNewProperties(new Properties());
        leaf.sealNewVersion();
        assertTrue(leaf.hasContent());
        assertEquals(1, leaf.getCurrentVersion());
        assertEquals(longUrl, leaf.getNodeUrl());
        RepositoryNode.RepositoryNodeContents rnc = leaf.getNodeContents();
        assertInputStreamMatchesString("test stream", rnc.getInputStream());
    }

    String trimUrlForOs(String url) {
        int pad = 10 + tempDirPath.length() + "/cache/xxx".length() + RepositoryNodeImpl.CONTENT_DIR.length()
                + Math.max(RepositoryNodeImpl.CURRENT_FILENAME.length(),
                        RepositoryNodeImpl.CURRENT_PROPS_FILENAME.length());

        PlatformUtil pi = PlatformUtil.getInstance();
        log.info("pi: " + pi);
        int max = pi.maxPathname() - pad;
        if (url.length() <= max) {
            return url;
        }
        url = trimTo(url, max);
        if (url.endsWith("/")) {
            url += "a";
        }
        log.info("Trimmed long URL to (" + url.length() + "): " + url);
        return url;
    }

    public void testMakeNodeLocation() throws Exception {
        RepositoryNodeImpl leaf = (RepositoryNodeImpl) repo.createNewNode("http://www.example.com/testDir");
        String nodeLoc = LockssRepositoryImpl.mapAuToFileLocation(tempDirPath, mau);
        nodeLoc = LockssRepositoryImpl.mapUrlToFileLocation(nodeLoc, "http://www.example.com/testDir");
        File testFile = new File(nodeLoc);
        assertFalse(testFile.exists());
        leaf.createNodeLocation();
        assertTrue(testFile.exists());
        assertTrue(testFile.isDirectory());
    }

    public void testMakeNewVersion() throws Exception {
        Properties props = new Properties();
        props.setProperty("test 1", "value 1");
        RepositoryNode leaf = createLeaf("http://www.example.com/testDir/test.cache", "test stream 1", props);
        assertEquals(1, leaf.getCurrentVersion());

        props = new Properties();
        props.setProperty("test 1", "value 2");
        leaf.makeNewVersion();
        leaf.setNewProperties(props);
        writeToLeaf(leaf, "test stream 2");
        leaf.sealNewVersion();
        assertEquals(2, leaf.getCurrentVersion());
        assertFalse(leaf.isIdenticalVersion());

        String resultStr = getLeafContent(leaf);
        assertEquals("test stream 2", resultStr);
        props = leaf.getNodeContents().getProperties();
        assertEquals("value 2", props.getProperty("test 1"));
    }

    static final int DEL_NODE_DIR = 1;
    static final int DEL_CONTENT_DIR = 2;
    static final int DEL_CONTENT_FILE = 3;
    static final int DEL_PROPS_FILE = 4;

    public void testDisappearingFile(int whichFile, boolean tryRead) throws Exception {
        String url = "http://www.example.com/foo.html";
        RepositoryNodeImpl leaf = (RepositoryNodeImpl) repo.createNewNode(url);
        String nodeLoc = LockssRepositoryImpl.mapAuToFileLocation(tempDirPath, mau);
        nodeLoc = LockssRepositoryImpl.mapUrlToFileLocation(nodeLoc, url);
        File testFile;
        switch (whichFile) {
        case DEL_NODE_DIR:
            testFile = new File(nodeLoc);
            break;
        case DEL_CONTENT_DIR:
            testFile = new File(nodeLoc, "#content");
            break;
        case DEL_CONTENT_FILE:
            testFile = new File(nodeLoc, "#content/current");
            break;
        case DEL_PROPS_FILE:
            testFile = new File(nodeLoc, "#content/current.props");
            break;
        default:
            throw new UnsupportedOperationException();
        }
        assertFalse(testFile.exists());

        Properties props1 = PropUtil.fromArgs("key1", "value 1");

        createContentVersion(leaf, "test content 11111", props1);
        assertEquals(1, leaf.getCurrentVersion());

        assertTrue(testFile.exists());
        switch (whichFile) {
        case DEL_NODE_DIR:
        case DEL_CONTENT_DIR:
            assertTrue(FileUtil.delTree(testFile));
            break;
        case DEL_CONTENT_FILE:
        case DEL_PROPS_FILE:
            assertTrue(testFile.delete());
            break;
        }
        assertFalse(testFile.exists());

        Properties props2 = PropUtil.fromArgs("key2", "value 2");
        RepositoryNode leaf2 = repo.createNewNode(url);
        assertSame(leaf, leaf2);
        assertTrue(leaf.hasContent());
        if (tryRead) {
            try {
                getLeafContent(leaf);
            } catch (LockssRepository.RepositoryStateException e) {
                // expected
            }
        }
        leaf2.makeNewVersion();

        writeToLeaf(leaf, "test content 22222");
        leaf.setNewProperties(props2);
        leaf.sealNewVersion();
        assertFalse(leaf.isIdenticalVersion());

        assertTrue(testFile.exists());
        int expver = 2;
        // if we tried to read while node or content dir was missing, version
        // number will have been reset.
        if (tryRead) {
            switch (whichFile) {
            case DEL_NODE_DIR:
            case DEL_CONTENT_DIR:
                expver = 1;
            }
        }
        assertEquals(expver, leaf.getCurrentVersion());

        assertEquals("test content 22222", getLeafContent(leaf));
        assertEquals("value 2", leaf.getNodeContents().getProperties().get("key2"));
    }

    public void testDisappearingNodeDir() throws Exception {
        testDisappearingFile(DEL_NODE_DIR, false);
    }

    public void testDisappearingContentDir() throws Exception {
        testDisappearingFile(DEL_CONTENT_DIR, false);
    }

    public void testDisappearingContentFile() throws Exception {
        testDisappearingFile(DEL_CONTENT_FILE, false);
    }

    public void testDisappearingPropsFile() throws Exception {
        testDisappearingFile(DEL_PROPS_FILE, false);
    }

    public void testDisappearingNodeDirWithRead() throws Exception {
        testDisappearingFile(DEL_NODE_DIR, true);
    }

    public void testDisappearingContentDirWithRead() throws Exception {
        testDisappearingFile(DEL_CONTENT_DIR, true);
    }

    public void testDisappearingContentFileWithRead() throws Exception {
        testDisappearingFile(DEL_CONTENT_FILE, true);
    }

    public void testDisappearingPropsFileWithRead() throws Exception {
        testDisappearingFile(DEL_PROPS_FILE, true);
    }

    public void testMakeNewVersionWithoutClosingStream() throws Exception {
        RepositoryNode leaf = createLeaf("http://www.example.com/testDir/test.cache", "test stream 1",
                new Properties());

        leaf.makeNewVersion();
        leaf.setNewProperties(new Properties());
        OutputStream os = leaf.getNewOutputStream();
        InputStream is = new StringInputStream("test stream 2");
        StreamUtil.copy(is, os);
        is.close();
        // don't close outputstream
        leaf.sealNewVersion();
        assertEquals(2, leaf.getCurrentVersion());
        String resultStr = getLeafContent(leaf);
        assertEquals("test stream 2", resultStr);
        assertFalse(leaf.isIdenticalVersion());
    }

    public void testMakeNewIdenticalVersionDefault() throws Exception {
        Properties props = new Properties();
        props.setProperty("test 1", "value 1");
        MyMockRepositoryNode leaf = new MyMockRepositoryNode(
                (RepositoryNodeImpl) createLeaf("http://www.example.com/testDir/test.cache", "test stream", props));
        assertEquals(1, leaf.getCurrentVersion());
        // set the file extension
        leaf.dateValue = 123321;

        props = new Properties();
        props.setProperty("test 1", "value 2");
        leaf.makeNewVersion();
        leaf.setNewProperties(props);
        writeToLeaf(leaf, "test stream");
        leaf.sealNewVersion();
        assertEquals(1, leaf.getCurrentVersion());
        assertTrue(leaf.isIdenticalVersion());

        String resultStr = getLeafContent(leaf);
        assertEquals("test stream", resultStr);
        props = leaf.getNodeContents().getProperties();
        assertEquals("value 2", props.getProperty("test 1"));

        // make sure proper files exist
        tempDirPath = LockssRepositoryImpl.mapAuToFileLocation(tempDirPath, mau);
        tempDirPath = LockssRepositoryImpl.mapUrlToFileLocation(tempDirPath,
                "http://www.example.com/testDir/test.cache");

        File testFileDir = new File(tempDirPath + "/#content");
        File[] files = testFileDir.listFiles();
        assertEquals(2, files.length);
        File testFile = new File(testFileDir, "current");
        assertTrue(testFile.exists());
        testFile = new File(testFileDir, "current.props");
        assertTrue(testFile.exists());
        //    testFile = new File(testFileDir, "1.props-123321");
        //    assertFalse(testFile.exists());

        // ensure non-identical version clears isIdenticalVersion()
        leaf.makeNewVersion();
        leaf.setNewProperties(props);
        writeToLeaf(leaf, "test stream not the same");
        leaf.sealNewVersion();
        assertEquals(2, leaf.getCurrentVersion());
        assertFalse(leaf.isIdenticalVersion());
    }

    public void testMakeNewIdenticalVersionOldWay() throws Exception {
        props.setProperty(RepositoryNodeImpl.PARAM_KEEP_ALL_PROPS_FOR_DUP_FILE, "true");
        ConfigurationUtil.setCurrentConfigFromProps(props);

        Properties props = new Properties();
        props.setProperty("test 1", "value 1");
        MyMockRepositoryNode leaf = new MyMockRepositoryNode(
                (RepositoryNodeImpl) createLeaf("http://www.example.com/testDir/test.cache", "test stream", props));
        assertEquals(1, leaf.getCurrentVersion());
        // set the file extension
        leaf.dateValue = 123321;

        props = new Properties();
        props.setProperty("test 1", "value 2");
        leaf.makeNewVersion();
        leaf.setNewProperties(props);
        writeToLeaf(leaf, "test stream");
        leaf.sealNewVersion();
        assertEquals(1, leaf.getCurrentVersion());
        assertTrue(leaf.isIdenticalVersion());

        String resultStr = getLeafContent(leaf);
        assertEquals("test stream", resultStr);
        props = leaf.getNodeContents().getProperties();
        assertEquals("value 2", props.getProperty("test 1"));

        // make sure proper files exist
        tempDirPath = LockssRepositoryImpl.mapAuToFileLocation(tempDirPath, mau);
        tempDirPath = LockssRepositoryImpl.mapUrlToFileLocation(tempDirPath,
                "http://www.example.com/testDir/test.cache");

        File testFileDir = new File(tempDirPath + "/#content");
        File[] files = testFileDir.listFiles();
        assertEquals(3, files.length);
        File testFile = new File(testFileDir, "current");
        assertTrue(testFile.exists());
        testFile = new File(testFileDir, "current.props");
        assertTrue(testFile.exists());
        testFile = new File(testFileDir, "1.props-123321");
        assertTrue(testFile.exists());
    }

    public void testMakeNewIdenticalVersionNewWay() throws Exception {
        props.setProperty(RepositoryNodeImpl.PARAM_KEEP_ALL_PROPS_FOR_DUP_FILE, "false");
        ConfigurationUtil.setCurrentConfigFromProps(props);

        Properties props = new Properties();
        props.setProperty("test 1", "value 1");
        MyMockRepositoryNode leaf = new MyMockRepositoryNode(
                (RepositoryNodeImpl) createLeaf("http://www.example.com/testDir/test.cache", "test stream", props));
        assertEquals(1, leaf.getCurrentVersion());
        // set the file extension
        leaf.dateValue = 123321;

        props = new Properties();
        props.setProperty("test 1", "value 2");
        leaf.makeNewVersion();
        leaf.setNewProperties(props);
        writeToLeaf(leaf, "test stream");
        leaf.sealNewVersion();
        assertEquals(1, leaf.getCurrentVersion());
        assertTrue(leaf.isIdenticalVersion());

        String resultStr = getLeafContent(leaf);
        assertEquals("test stream", resultStr);
        props = leaf.getNodeContents().getProperties();
        assertEquals("value 2", props.getProperty("test 1"));

        // make sure proper files exist
        tempDirPath = LockssRepositoryImpl.mapAuToFileLocation(tempDirPath, mau);
        tempDirPath = LockssRepositoryImpl.mapUrlToFileLocation(tempDirPath,
                "http://www.example.com/testDir/test.cache");

        File testFileDir = new File(tempDirPath + "/#content");
        File[] files = testFileDir.listFiles();
        assertEquals(2, files.length);
        File testFile = new File(testFileDir, "current");
        assertTrue(testFile.exists());
        testFile = new File(testFileDir, "current.props");
        assertTrue(testFile.exists());
        //    testFile = new File(testFileDir, "1.props-123321");
        //    assertFalse(testFile.exists());
    }

    public void testIdenticalVersionFixesVersionError() throws Exception {
        Properties props = new Properties();
        MyMockRepositoryNode leaf = new MyMockRepositoryNode(
                (RepositoryNodeImpl) createLeaf("http://www.example.com/testDir/test.cache", "test stream", props));
        assertEquals(1, leaf.getCurrentVersion());

        props = new Properties();
        leaf.makeNewVersion();
        leaf.setNewProperties(props);
        // set to error state
        leaf.currentVersion = 0;
        writeToLeaf(leaf, "test stream");
        assertEquals(0, leaf.currentVersion);
        leaf.sealNewVersion();
        // fixes error state, even though identical
        assertEquals(1, leaf.getCurrentVersion());
        assertTrue(leaf.isIdenticalVersion());
    }

    public void testMakeNewVersionFixesVersionError() throws Exception {
        Properties props = new Properties();
        MyMockRepositoryNode leaf = new MyMockRepositoryNode(
                (RepositoryNodeImpl) createLeaf("http://www.example.com/testDir/test.cache", "test stream", props));
        assertEquals(1, leaf.getCurrentVersion());

        props = new Properties();
        leaf.makeNewVersion();
        // set to error state
        leaf.currentVersion = -1;
        leaf.setNewProperties(props);
        writeToLeaf(leaf, "test stream2");
        leaf.sealNewVersion();
        // fixes error state
        assertEquals(1, leaf.getCurrentVersion());
        assertFalse(leaf.isIdenticalVersion());
    }

    public void testUnsealedRnc() throws Exception {
        String url = "http://www.example.com/foo.html";
        String content = "test test test";
        Properties props = new Properties();
        props.setProperty("test 1", "value 1");
        RepositoryNode leaf = repo.createNewNode(url);
        try {
            leaf.getUnsealedRnc();
            fail("Should throw");
        } catch (IllegalStateException e) {
        }
        leaf.makeNewVersion();
        writeToLeaf(leaf, content);
        RepositoryNode.RepositoryNodeContents rnc = leaf.getUnsealedRnc();
        assertInputStreamMatchesString(content, rnc.getInputStream());
        assertInputStreamMatchesString(content, rnc.getInputStream());

        try {
            rnc.getProperties();
            fail("Should throw");
        } catch (UnsupportedOperationException e) {
        }
        try {
            rnc.addProperty("foo", "bar");
            fail("Should throw");
        } catch (UnsupportedOperationException e) {
        }
        rnc.release();

        leaf.setNewProperties(props);
        leaf.sealNewVersion();
        assertFalse(leaf.isIdenticalVersion());
        try {
            rnc.getInputStream();
            fail("Should throw");
        } catch (IllegalStateException e) {
        }
        RepositoryNode.RepositoryNodeContents rncSealed = leaf.getNodeContents();
        assertInputStreamMatchesString(content, rncSealed.getInputStream());
    }

    public void testGetInputStream() throws Exception {
        RepositoryNode leaf = createLeaf("http://www.example.com/testDir/test.cache", "test stream", null);
        String resultStr = getLeafContent(leaf);
        assertEquals("test stream", resultStr);
    }

    public void testGetProperties() throws Exception {
        Properties props = new Properties();
        props.setProperty("test 1", "value 1");
        RepositoryNode leaf = createLeaf("http://www.example.com/testDir/test.cache", "test stream", props);

        RepositoryNode.RepositoryNodeContents contents = leaf.getNodeContents();
        props = contents.getProperties();
        // close stream to allow the file to be renamed later
        // XXX 'getProperties()' creates an input stream, and 'release()' just
        // sets it to null.  The rename still fails in Windows unless the stream
        // is closed first.
        contents.getInputStream().close();
        contents.release();

        assertEquals("value 1", props.getProperty("test 1"));

        leaf.makeNewVersion();
        props = new Properties();
        props.setProperty("test 1", "value 2");
        leaf.setNewProperties(props);
        writeToLeaf(leaf, "test stream 2");
        leaf.sealNewVersion();

        props = leaf.getNodeContents().getProperties();
        assertEquals("value 2", props.getProperty("test 1"));
    }

    // More addProperty tests below in testGetNodeVersions()
    public void testAddProperty() throws Exception {
        Properties props = new Properties();
        props.setProperty("test 1", "value 2");
        RepositoryNode leaf = createLeaf("http://www.example.com/testDir/test.cache", "test stream", props);

        RepositoryNode.RepositoryNodeContents rnc0 = leaf.getNodeContents();
        // get two more rncs on the same node
        RepositoryNode.RepositoryNodeContents rnc1 = leaf.getNodeContents();
        RepositoryNode.RepositoryNodeContents rnc2 = leaf.getNodeContents();
        // and have one of them load the props
        rnc2.getProperties();
        assertNotSame(rnc0, rnc1);

        // original props
        Properties rnc0p1 = rnc0.getProperties();
        assertEquals("value 2", rnc0p1.getProperty("test 1"));

        // add prop to node
        rnc0.addProperty(CachedUrl.PROPERTY_CHECKSUM, "checksum");

        // old Properties object isn't updated.
        assertFalse(rnc0p1.containsKey(CachedUrl.PROPERTY_CHECKSUM));

        // rnc properties should have new prop
        Properties rnc0p2 = rnc0.getProperties();
        // not essential, but this is the way it works
        assertNotSame(rnc0p1, rnc0p2);
        assertEquals("checksum", rnc0p2.getProperty(CachedUrl.PROPERTY_CHECKSUM));
        assertEquals("value 2", rnc0p2.getProperty("test 1"));

        // previously obtained rnc whose properties hadn't already been loaded
        // has new prop
        Properties rnc1p1 = rnc1.getProperties();
        assertEquals("checksum", rnc1p1.getProperty(CachedUrl.PROPERTY_CHECKSUM));
        assertEquals("value 2", rnc1p1.getProperty("test 1"));

        // previously obtained rnc whose properties *had* already been loaded
        // doesn't have new prop
        Properties rnc2p1 = rnc2.getProperties();
        assertFalse(rnc2p1.containsKey(CachedUrl.PROPERTY_CHECKSUM));
        assertEquals("value 2", rnc2p1.getProperty("test 1"));

        RepositoryNode.RepositoryNodeContents rnc = leaf.getNodeContents();
        props = rnc.getProperties();
        assertNotNull(props);
        assertTrue(props.containsKey(CachedUrl.PROPERTY_CHECKSUM));
        assertEquals("checksum", props.getProperty(CachedUrl.PROPERTY_CHECKSUM));

    }

    RepositoryNode createNodeWithCorruptProps(String url) throws Exception {
        Properties props = new Properties();
        props.setProperty("test 1", "value 1");
        RepositoryNode leaf = createLeaf(url, "test stream", props);

        RepositoryNodeImpl leafImpl = (RepositoryNodeImpl) leaf;
        File propsFile = new File(leafImpl.getContentDir(), RepositoryNodeImpl.CURRENT_PROPS_FILENAME);
        // Write a Malformed unicode escape that will cause Properties.load()
        // to throw
        OutputStream os = new BufferedOutputStream(new FileOutputStream(propsFile, true));
        os.write("\\uxxxxfoo=bar".getBytes());
        os.close();
        return leaf;
    }

    public void testCorruptProperties1() throws Exception {
        RepositoryNode leaf = createNodeWithCorruptProps("http://www.example.com/testDir/test.cache");

        assertFalse(leaf.hasContent());
        assertTrue(leaf.isDeleted());
        leaf.makeNewVersion();
        writeToLeaf(leaf, "test stream");
        leaf.setNewProperties(new Properties());
        leaf.sealNewVersion();
        assertTrue(leaf.hasContent());
        assertFalse(leaf.isDeleted());
    }

    public void testCorruptProperties2() throws Exception {
        String stem = "http://www.example.com/testDir";
        RepositoryNode leaf = createNodeWithCorruptProps(stem + "/test.cache");
        RepositoryNode leaf2 = createLeaf(stem + "/foo", "test stream", props);

        RepositoryNode dirEntry = repo.getNode("http://www.example.com/testDir");
        Iterator childIt = dirEntry.listChildren(null, false);
        assertEquals(ListUtil.list(leaf2), ListUtil.fromIterator(childIt));
    }

    static String PROP_VAL_STEM = "valstem_";
    static String PROP_KEY = "key";

    static String cntnt(int ix) {
        return "content " + ix + "ABCDEFGHIJKLMNOPQRSTUVWXYZ".substring(0, ix);
    }

    static int lngth(int ix) {
        return cntnt(ix).length();
    }

    void checkVersion(RepositoryNodeVersion nodeVer, int exp, String addedPropVal) throws IOException {
        RepositoryNode.RepositoryNodeContents rnc = nodeVer.getNodeContents();
        String verCont = getRNCContent(rnc);
        Properties verProps = rnc.getProperties();
        log.debug("ver: " + nodeVer.getVersion() + ", content: " + verCont);
        log.debug2(
                "ver: " + nodeVer.getVersion() + ", " + StringUtil.shortName(props.getClass()) + ": " + verProps);
        assertEquals(exp, nodeVer.getVersion());

        assertEquals(cntnt(exp), verCont);
        assertEquals(lngth(exp), nodeVer.getContentSize());
        assertEquals(PROP_VAL_STEM + exp, verProps.getProperty(PROP_KEY));

        assertEquals(addedPropVal, verProps.getProperty(CachedUrl.PROPERTY_CHECKSUM));

        // ensure can reread content from same rnc
        assertEquals(verCont, getRNCContent(rnc));
    }

    void addPropToVersion(RepositoryNodeVersion nodeVer, String propVal) {
        RepositoryNode.RepositoryNodeContents rnc = nodeVer.getNodeContents();
        rnc.addProperty(CachedUrl.PROPERTY_CHECKSUM, propVal);
    }

    public void testGetNodeVersion() throws Exception {
        int max = 5;
        String url = "http://www.example.com/versionedcontent.txt";
        Properties props = new Properties();

        RepositoryNode leaf = repo.createNewNode(url);
        // create several versions
        for (int ix = 1; ix <= max; ix++) {
            props.setProperty(PROP_KEY, PROP_VAL_STEM + ix);
            createContentVersion(leaf, cntnt(ix), props);
        }
        // getNodeVersion(current) should return the main node
        assertEquals(leaf, leaf.getNodeVersion(leaf.getCurrentVersion()));

        // loop through other versions checking version, content, props
        for (int ix = 1; ix < max; ix++) {
            RepositoryNodeVersion nodeVer = leaf.getNodeVersion(ix);
            checkVersion(nodeVer, ix, null);
        }
    }

    public void testGetNodeVersions() throws Exception {
        int max = 5;
        String url = "http://www.example.com/versionedcontent.txt";
        Properties props = new Properties();

        // No existing node
        RepositoryNode leaf0 = repo.createNewNode(url);
        assertFalse(leaf0.hasContent());
        RepositoryNodeVersion[] vers0 = leaf0.getNodeVersions();
        assertEquals(1, vers0.length);

        RepositoryNode leaf = repo.createNewNode(url);
        // create several versions
        for (int ix = 1; ix <= max; ix++) {
            props.setProperty(PROP_KEY, PROP_VAL_STEM + ix);
            createContentVersion(leaf, cntnt(ix), props);
        }
        // check expected current version number
        assertEquals(max, leaf.getCurrentVersion());
        assertEquals(max, leaf.getVersion());

        // check version, content, props of current version
        assertEquals(cntnt(max), getLeafContent(leaf));
        assertEquals(lngth(max), leaf.getContentSize());
        props = leaf.getNodeContents().getProperties();
        assertEquals(PROP_VAL_STEM + max, props.getProperty(PROP_KEY));

        // ask for all older versions
        RepositoryNodeVersion[] vers = leaf.getNodeVersions();
        assertEquals(max, vers.length);
        RepositoryNode.RepositoryNodeContents[] rncs = new RepositoryNode.RepositoryNodeContents[max];
        // loop through them checking version, content, props
        for (int ix = 0; ix < max; ix++) {
            int exp = max - ix;
            RepositoryNodeVersion nodeVer = vers[ix];

            rncs[ix] = nodeVer.getNodeContents();

            checkVersion(nodeVer, exp, null);
        }

        // now ask for and check a subset of the older versions
        assertTrue("max must be at least 4 for this test", max >= 4);
        int numver = max - 2;
        RepositoryNodeVersion[] subvers = leaf.getNodeVersions(numver);
        assertEquals(numver, subvers.length);
        for (int ix = 0; ix < numver; ix++) {
            int exp = max - ix;
            RepositoryNodeVersion nodeVer = subvers[ix];
            log.debug("ver: " + nodeVer.getVersion() + ", content: " + getLeafContent(nodeVer));
            assertEquals(exp, nodeVer.getVersion());

            assertEquals(cntnt(exp), getLeafContent(nodeVer));
            assertEquals(lngth(exp), nodeVer.getContentSize());
            props = nodeVer.getNodeContents().getProperties();
            assertEquals(PROP_VAL_STEM + exp, props.getProperty(PROP_KEY));
        }

        // Add a property to selected versions, ensure only the correct
        // versions change

        addPropToVersion(vers[0], "5new1");
        checkVersion(vers[0], 5, "5new1");
        for (int ix = 1; ix <= 4; ix++) {
            checkVersion(vers[ix], max - ix, null);
        }

        addPropToVersion(vers[2], "3new1");
        checkVersion(vers[2], 3, "3new1");
        checkVersion(vers[0], 5, "5new1");
        for (int ix : new int[] { 1, 3, 4 }) {
            checkVersion(vers[ix], max - ix, null);
        }

    }

    public void testIllegalVersionOperations() throws Exception {
        RepositoryNode.RepositoryNodeContents rnc;
        RepositoryNodeVersion nv;

        RepositoryNode leaf = repo.createNewNode("http://www.example.com/testDir/test.cache");
        try {
            nv = leaf.getNodeVersion(7);
            fail("No content, shouldn't be able to get versioned node: " + nv);
        } catch (UnsupportedOperationException e) {
        }
        // create first version
        Properties props = new Properties();
        props.setProperty("key", "val1");
        createContentVersion(leaf, cntnt(1), props);

        // We're allowed to get a RepositoryNodeVersion when the version
        // doesn't exist ...
        nv = leaf.getNodeVersion(7);
        // but all operations on it should throw
        try {
            nv.getContentSize();
            fail("No version; shouldn't get content size");
        } catch (UnsupportedOperationException e) {
        }
        try {
            rnc = nv.getNodeContents();
            fail("No version; shouldn't get RepositoryNodeContents");
        } catch (UnsupportedOperationException e) {
        }
    }

    public void testDirContent() throws Exception {
        RepositoryNode leaf = createLeaf("http://www.example.com/testDir/test.cache", "test stream", null);
        assertTrue(leaf.hasContent());

        RepositoryNode dir = repo.getNode("http://www.example.com/testDir");
        dir.makeNewVersion();
        writeToLeaf(dir, "test stream");
        dir.setNewProperties(new Properties());
        dir.sealNewVersion();
        assertTrue(dir.hasContent());

        dir = createLeaf("http://www.example.com/testDir/test.cache/new.test", "test stream", null);
        assertTrue(dir.hasContent());
    }

    public void testNodeSize() throws Exception {
        RepositoryNode leaf = createLeaf("http://www.example.com/testDir/test.cache", "test stream", null);
        assertTrue(leaf.hasContent());
        assertEquals(11, (int) leaf.getContentSize());
    }

    public void testTreeSize() throws Exception {
        createLeaf("http://www.example.com/testDir", "test", null);
        createLeaf("http://www.example.com/testDir/test1", "test1", null);
        createLeaf("http://www.example.com/testDir/test2", "test2", null);
        createLeaf("http://www.example.com/testDir/test3/branch1", "test33", null);
        createLeaf("http://www.example.com/testDir/test3/branch2", "test33", null);

        RepositoryNode leaf = repo.getNode("http://www.example.com/testDir");
        assertEquals(-1, leaf.getTreeContentSize(null, false));
        assertEquals(26, leaf.getTreeContentSize(null, true));
        assertEquals(26, leaf.getTreeContentSize(null, false));
        leaf = repo.getNode("http://www.example.com/testDir/test1");
        assertEquals(5, leaf.getTreeContentSize(null, true));
        leaf = repo.getNode("http://www.example.com/testDir/test3");
        assertEquals(12, leaf.getTreeContentSize(null, true));
        CachedUrlSetSpec cuss = new RangeCachedUrlSetSpec("http://www.example.com/testDir/test3", "/branch1",
                "/branch1");
        assertEquals(6, leaf.getTreeContentSize(cuss, true));
    }

    public void testDetermineParentNode() throws Exception {
        repo.createNewNode("http://www.example.com");
        repo.createNewNode("http://www.example.com/test");
        assertNotNull(repo.getNode("http://www.example.com/test"));
        RepositoryNodeImpl node = (RepositoryNodeImpl) repo.createNewNode("http://www.example.com/test/branch");
        assertEquals("http://www.example.com/test/branch", node.getNodeUrl());
        node = node.determineParentNode();
        assertEquals("http://www.example.com/test", node.getNodeUrl());
        node = node.determineParentNode();
        assertEquals("http://www.example.com", node.getNodeUrl());
        node = node.determineParentNode();
        assertEquals(AuUrl.PROTOCOL, node.getNodeUrl());
        node = node.determineParentNode();
        assertEquals(AuUrl.PROTOCOL, node.getNodeUrl());

        // query args containing slash formerly caused determineParentNode() to
        // return wrong result
        RepositoryNodeImpl node2 = (RepositoryNodeImpl) repo
                .createNewNode("http://www.example.com/test/branch/file?http://foo.bar/path/to/file");
        node = node2.determineParentNode();
        assertEquals("http://www.example.com/test/branch", node.getNodeUrl());
    }

    public void testCacheInvalidation() throws Exception {
        RepositoryNodeImpl root = (RepositoryNodeImpl) createLeaf("http://www.example.com", "test", null);
        RepositoryNodeImpl branch = (RepositoryNodeImpl) createLeaf("http://www.example.com/branch", "test", null);
        RepositoryNodeImpl branch2 = (RepositoryNodeImpl) createLeaf("http://www.example.com/branch/branch2",
                "test", null);
        // This one has directory level with no node prop file, to check that
        // cache invalidation traverses them correctly
        RepositoryNodeImpl leaf = (RepositoryNodeImpl) createLeaf(
                "http://www.example.com/branch/branch2/a/b/c/leaf", "test", null);
        assertNull(branch.nodeProps.getProperty(TREE_SIZE_PROPERTY));
        assertNull(leaf.nodeProps.getProperty(TREE_SIZE_PROPERTY));
        // force invalidation to happen
        branch.nodeProps.setProperty(TREE_SIZE_PROPERTY, "789");
        branch.invalidateCachedValues(true);
        // should now be explicitly marked invalid
        assertEquals(RepositoryNodeImpl.INVALID, branch.nodeProps.getProperty(TREE_SIZE_PROPERTY));
        assertEquals(RepositoryNodeImpl.INVALID, branch.nodeProps.getProperty(CHILD_COUNT_PROPERTY));
        // fake prop set at root to check invalidation stops properly
        root.nodeProps.setProperty(TREE_SIZE_PROPERTY, "789");
        root.nodeProps.setProperty(CHILD_COUNT_PROPERTY, "3");
        // don't set branch so the invalidate stops there
        branch2.nodeProps.setProperty(TREE_SIZE_PROPERTY, "456");
        branch2.nodeProps.setProperty(CHILD_COUNT_PROPERTY, "1");
        leaf.nodeProps.setProperty(TREE_SIZE_PROPERTY, "123");
        leaf.nodeProps.setProperty(CHILD_COUNT_PROPERTY, "0");

        leaf.invalidateCachedValues(true);
        // shoulddn't be set here anymore
        assertFalse(isPropValid(leaf.nodeProps.getProperty(TREE_SIZE_PROPERTY)));
        assertFalse(isPropValid(leaf.nodeProps.getProperty(CHILD_COUNT_PROPERTY)));
        // or here (requires recursing up through dirs that have no node props
        // file)
        assertFalse(isPropValid(branch2.nodeProps.getProperty(TREE_SIZE_PROPERTY)));
        assertFalse(isPropValid(branch2.nodeProps.getProperty(CHILD_COUNT_PROPERTY)));
        // still invalid, recursion should have stopped here
        assertFalse(isPropValid(branch.nodeProps.getProperty(TREE_SIZE_PROPERTY)));
        assertFalse(isPropValid(branch.nodeProps.getProperty(CHILD_COUNT_PROPERTY)));
        // so not cleared these
        assertTrue(isPropValid(root.nodeProps.getProperty(TREE_SIZE_PROPERTY)));
        assertTrue(isPropValid(root.nodeProps.getProperty(CHILD_COUNT_PROPERTY)));
        assertEquals("789", root.nodeProps.getProperty(TREE_SIZE_PROPERTY));
        assertEquals("3", root.nodeProps.getProperty(CHILD_COUNT_PROPERTY));
    }

    // Add a first child after #node_props have been created
    public void testCacheInvalidationIncremental() throws Exception {
        RepositoryNodeImpl root = (RepositoryNodeImpl) createLeaf("http://www.example.com", "test", null);
        RepositoryNodeImpl branch = (RepositoryNodeImpl) createLeaf("http://www.example.com/branch", "test", null);
        assertFalse(isPropValid(branch.nodeProps.getProperty(TREE_SIZE_PROPERTY)));
        assertTrue(branch.isLeaf());
        assertEquals(0, branch.getChildCount());
        assertEquals(4, branch.getTreeContentSize(null, true));
        assertTrue(isPropValid(branch.nodeProps.getProperty(TREE_SIZE_PROPERTY)));

        RepositoryNodeImpl leaf = (RepositoryNodeImpl) createLeaf(
                "http://www.example.com/branch/leaf?http://foo/bar/baz", "test", null);
        assertEquals(8, branch.getTreeContentSize(null, true));
        assertFalse(branch.isLeaf());
    }

    boolean isPropValid(String val) {
        return RepositoryNodeImpl.isPropValid(val);
    }

    boolean isPropInvalid(String val) {
        return RepositoryNodeImpl.isPropInvalid(val);
    }

    public void testTreeSizeCaching() throws Exception {
        createLeaf("http://www.example.com/testDir", "test", null);

        RepositoryNodeImpl leaf = (RepositoryNodeImpl) repo.getNode("http://www.example.com/testDir");
        assertNull(leaf.nodeProps.getProperty(TREE_SIZE_PROPERTY));
        assertEquals(4, leaf.getTreeContentSize(null, true));
        assertEquals("4", leaf.nodeProps.getProperty(TREE_SIZE_PROPERTY));
        leaf.markAsDeleted();
        assertTrue(isPropInvalid(leaf.nodeProps.getProperty(TREE_SIZE_PROPERTY)));
        assertEquals(0, leaf.getTreeContentSize(null, true));
        assertEquals("0", leaf.nodeProps.getProperty(TREE_SIZE_PROPERTY));
    }

    public void testChildCount() throws Exception {
        createLeaf("http://www.example.com/testDir", "test", null);

        RepositoryNodeImpl leaf = (RepositoryNodeImpl) repo.getNode("http://www.example.com/testDir");
        assertNull(leaf.nodeProps.getProperty(CHILD_COUNT_PROPERTY));
        assertEquals(0, leaf.getChildCount());
        assertEquals("0", leaf.nodeProps.getProperty(CHILD_COUNT_PROPERTY));

        createLeaf("http://www.example.com/testDir/test1", "test1", null);
        createLeaf("http://www.example.com/testDir/test2", "test2", null);
        assertEquals(2, leaf.getChildCount());
        assertEquals("2", leaf.nodeProps.getProperty(CHILD_COUNT_PROPERTY));
    }

    public void testDeactivate() throws Exception {
        RepositoryNodeImpl leaf = (RepositoryNodeImpl) createLeaf("http://www.example.com/test1", "test stream",
                null);
        assertTrue(leaf.hasContent());
        assertFalse(leaf.isContentInactive());
        assertEquals(1, leaf.getCurrentVersion());
        assertNull(leaf.nodeProps.getProperty(RepositoryNodeImpl.INACTIVE_CONTENT_PROPERTY));

        leaf.deactivateContent();
        assertFalse(leaf.hasContent());
        assertTrue(leaf.isContentInactive());
        assertEquals(RepositoryNodeImpl.INACTIVE_VERSION, leaf.getCurrentVersion());
        assertEquals("true", leaf.nodeProps.getProperty(RepositoryNodeImpl.INACTIVE_CONTENT_PROPERTY));
    }

    public void testDelete() throws Exception {
        RepositoryNodeImpl leaf = (RepositoryNodeImpl) createLeaf("http://www.example.com/test1", "test stream",
                null);
        assertTrue(leaf.hasContent());
        assertFalse(leaf.isDeleted());
        assertEquals(1, leaf.getCurrentVersion());
        assertNull(leaf.nodeProps.getProperty(RepositoryNodeImpl.DELETION_PROPERTY));

        leaf.markAsDeleted();
        assertFalse(leaf.hasContent());
        assertTrue(leaf.isDeleted());
        assertEquals(RepositoryNodeImpl.DELETED_VERSION, leaf.getCurrentVersion());
        assertEquals("true", leaf.nodeProps.getProperty(RepositoryNodeImpl.DELETION_PROPERTY));
    }

    public void testUnDelete() throws Exception {
        RepositoryNodeImpl leaf = (RepositoryNodeImpl) createLeaf("http://www.example.com/test1", "test stream",
                null);
        leaf.markAsDeleted();
        assertTrue(leaf.isDeleted());
        assertEquals(RepositoryNodeImpl.DELETED_VERSION, leaf.getCurrentVersion());

        leaf.markAsNotDeleted();
        assertFalse(leaf.isContentInactive());
        assertFalse(leaf.isDeleted());
        assertEquals(1, leaf.getCurrentVersion());
        // make to null, not 'false'
        assertNull(leaf.nodeProps.getProperty(RepositoryNodeImpl.DELETION_PROPERTY));
        String resultStr = getLeafContent(leaf);
        assertEquals("test stream", resultStr);
    }

    public void testRestoreLastVersion() throws Exception {
        Properties props = new Properties();
        props.setProperty("test 1", "value 1");
        RepositoryNode leaf = createLeaf("http://www.example.com/test1", "test stream 1", props);
        assertEquals(1, leaf.getCurrentVersion());

        props = new Properties();
        props.setProperty("test 1", "value 2");
        leaf.makeNewVersion();
        leaf.setNewProperties(props);
        writeToLeaf(leaf, "test stream 2");
        leaf.sealNewVersion();
        assertEquals(2, leaf.getCurrentVersion());

        leaf.restoreLastVersion();
        assertEquals(1, leaf.getCurrentVersion());

        String resultStr = getLeafContent(leaf);
        assertEquals("test stream 1", resultStr);
        props = leaf.getNodeContents().getProperties();
        assertEquals("value 1", props.getProperty("test 1"));
    }

    public void testReactivateViaRestore() throws Exception {
        RepositoryNodeImpl leaf = (RepositoryNodeImpl) createLeaf("http://www.example.com/test1", "test stream",
                null);
        leaf.deactivateContent();
        assertTrue(leaf.isContentInactive());
        assertEquals(RepositoryNodeImpl.INACTIVE_VERSION, leaf.getCurrentVersion());

        leaf.restoreLastVersion();
        assertFalse(leaf.isContentInactive());
        assertEquals(1, leaf.getCurrentVersion());
        // back to null, not 'false'
        assertNull(leaf.nodeProps.getProperty(RepositoryNodeImpl.INACTIVE_CONTENT_PROPERTY));
        String resultStr = getLeafContent(leaf);
        assertEquals("test stream", resultStr);
    }

    public void testReactivateViaNewVersion() throws Exception {
        RepositoryNodeImpl leaf = (RepositoryNodeImpl) createLeaf("http://www.example.com/test1", "test stream",
                null);
        leaf.deactivateContent();
        assertTrue(leaf.isContentInactive());
        assertEquals(RepositoryNodeImpl.INACTIVE_VERSION, leaf.getCurrentVersion());

        Properties props = new Properties();
        props.setProperty("test 1", "value 2");
        leaf.makeNewVersion();
        leaf.setNewProperties(props);
        writeToLeaf(leaf, "test stream 2");
        leaf.sealNewVersion();
        assertFalse(leaf.isContentInactive());
        assertEquals(2, leaf.getCurrentVersion());
        String resultStr = getLeafContent(leaf);
        assertEquals("test stream 2", resultStr);

        File lastProps = new File(leaf.contentDir, "1.props");
        assertTrue(lastProps.exists());
        InputStream is = new BufferedInputStream(new FileInputStream(lastProps));
        props.load(is);
        is.close();
        // make sure the 'was inactive' property hasn't been lost
        assertEquals("true", props.getProperty(RepositoryNodeImpl.NODE_WAS_INACTIVE_PROPERTY));
    }

    public void testAbandonReactivateViaNewVersion() throws Exception {
        RepositoryNode leaf = createLeaf("http://www.example.com/test1", "test stream", null);
        leaf.deactivateContent();
        assertTrue(leaf.isContentInactive());
        assertEquals(RepositoryNodeImpl.INACTIVE_VERSION, leaf.getCurrentVersion());

        Properties props = new Properties();
        props.setProperty("test 1", "value 2");
        leaf.makeNewVersion();
        leaf.setNewProperties(props);
        writeToLeaf(leaf, "test stream 2");
        leaf.abandonNewVersion();
        assertTrue(leaf.isContentInactive());
        assertEquals(RepositoryNodeImpl.INACTIVE_VERSION, leaf.getCurrentVersion());
    }

    public void testIsLeaf() throws Exception {
        createLeaf("http://www.example.com/testDir/test1", "test stream", null);
        createLeaf("http://www.example.com/testDir/branch1", "test stream", null);
        createLeaf("http://www.example.com/testDir/branch1/test4", "test stream", null);

        RepositoryNode leaf = repo.getNode("http://www.example.com/testDir/test1");
        assertTrue(leaf.isLeaf());
        leaf = repo.getNode("http://www.example.com/testDir/branch1");
        assertFalse(leaf.isLeaf());
    }

    public void testListInactiveNodes() throws Exception {
        createLeaf("http://www.example.com/testDir/test1", "test stream", null);
        createLeaf("http://www.example.com/testDir/test2", "test stream", null);
        createLeaf("http://www.example.com/testDir/test3", "test stream", null);
        createLeaf("http://www.example.com/testDir/branch1", "test stream", null);
        createLeaf("http://www.example.com/testDir/branch1/test4", "test stream", null);
        createLeaf("http://www.example.com/testDir/branch2", "test stream", null);
        createLeaf("http://www.example.com/testDir/branch2/test5", "test stream", null);

        RepositoryNode dirEntry = repo.getNode("http://www.example.com/testDir");
        Iterator childIt = dirEntry.listChildren(null, false);
        ArrayList childL = new ArrayList(3);
        while (childIt.hasNext()) {
            RepositoryNode node = (RepositoryNode) childIt.next();
            childL.add(node.getNodeUrl());
        }
        String[] expectedA = new String[] { "http://www.example.com/testDir/branch1",
                "http://www.example.com/testDir/branch2", "http://www.example.com/testDir/test1",
                "http://www.example.com/testDir/test2", "http://www.example.com/testDir/test3" };
        assertSameElements(expectedA, childL);

        RepositoryNode leaf = repo.getNode("http://www.example.com/testDir/test2");
        leaf.deactivateContent();
        // this next shouldn't be excluded since it isn't a leaf node
        leaf = repo.getNode("http://www.example.com/testDir/branch1");
        leaf.deactivateContent();
        // this next should be excluded because it's deleted
        leaf = repo.getNode("http://www.example.com/testDir/branch2");
        leaf.markAsDeleted();

        childIt = dirEntry.listChildren(null, false);
        childL = new ArrayList(2);
        while (childIt.hasNext()) {
            RepositoryNode node = (RepositoryNode) childIt.next();
            childL.add(node.getNodeUrl());
        }
        expectedA = new String[] { "http://www.example.com/testDir/branch1", "http://www.example.com/testDir/test1",
                "http://www.example.com/testDir/test3" };
        assertSameElements("Excluding inactive nodes failed.", expectedA, childL);

        childIt = dirEntry.listChildren(null, true);
        childL = new ArrayList(3);
        while (childIt.hasNext()) {
            RepositoryNode node = (RepositoryNode) childIt.next();
            childL.add(node.getNodeUrl());
        }
        expectedA = new String[] { "http://www.example.com/testDir/branch1", "http://www.example.com/testDir/test1",
                "http://www.example.com/testDir/test2", "http://www.example.com/testDir/test3" };
        assertSameElements("Including inactive nodes failed.", expectedA, childL);
    }

    public void testDeleteInnerNode() throws Exception {
        createLeaf("http://www.example.com/testDir/test1", "test stream", null);
        createLeaf("http://www.example.com/testDir/test2", "test stream", null);

        RepositoryNode dirEntry = repo.getNode("http://www.example.com/testDir");
        assertFalse(dirEntry.isDeleted());
        dirEntry.markAsDeleted();
        assertTrue(dirEntry.isDeleted());
        dirEntry.markAsNotDeleted();
        assertFalse(dirEntry.isDeleted());
    }

    public void testGetFileStrings() throws Exception {
        RepositoryNodeImpl node = (RepositoryNodeImpl) repo.createNewNode("http://www.example.com/test.url");
        node.initNodeRoot();
        String contentStr = FileUtil.sysDepPath(node.nodeLocation + "/#content");
        contentStr = contentStr.replaceAll("\\//", "\\\\/"); // sysDepPath mangles our backslashes
        assertEquals(contentStr, node.getContentDir().toString());
        contentStr = contentStr + File.separator;
        String expectedStr = contentStr + "123";
        assertEquals(expectedStr, node.getVersionedCacheFile(123).getAbsolutePath());
        expectedStr = contentStr + "123.props";
        assertEquals(expectedStr, node.getVersionedPropsFile(123).getAbsolutePath());
        expectedStr = contentStr + "inactive";
        assertEquals(expectedStr, node.getInactiveCacheFile().getAbsolutePath());
        expectedStr = contentStr + "inactive.props";
        assertEquals(expectedStr, node.getInactivePropsFile().getAbsolutePath());
    }

    public void testCheckNodeConsistency() throws Exception {
        // check returns proper values for errors
        MyMockRepositoryNode leaf = new MyMockRepositoryNode(
                (RepositoryNodeImpl) repo.createNewNode("http://www.example.com/testDir"));
        leaf.makeNewVersion();
        // should abort and return true since version open
        leaf.failRootConsist = true;
        assertTrue(leaf.checkNodeConsistency());

        // finish write
        leaf.setNewProperties(new Properties());
        writeToLeaf(leaf, "test stream");
        leaf.sealNewVersion();

        // should return false if node root fails
        assertFalse(leaf.checkNodeConsistency());
        leaf.failRootConsist = false;
        assertTrue(leaf.checkNodeConsistency());

        // check returns false if content fails
        leaf.failContentConsist = true;
        assertFalse(leaf.checkNodeConsistency());
        leaf.failContentConsist = false;
        assertTrue(leaf.checkNodeConsistency());

        // check returns false if current info load fails
        leaf.failEnsureCurrentLoaded = true;
        assertFalse(leaf.checkNodeConsistency());
        leaf.failEnsureCurrentLoaded = false;
        assertTrue(leaf.checkNodeConsistency());
    }

    public void testCheckNodeRootConsistency() throws Exception {
        MyMockRepositoryNode leaf = new MyMockRepositoryNode(
                (RepositoryNodeImpl) repo.createNewNode("http://www.example.com/testDir"));
        leaf.createNodeLocation();
        assertTrue(leaf.nodeRootFile.exists());
        // returns true when normal
        assertTrue(leaf.checkNodeRootConsistency());

        leaf.nodeRootFile.delete();
        assertFalse(leaf.nodeRootFile.exists());
        // creates dir, returns true when missing
        assertTrue(leaf.checkNodeRootConsistency());
        assertTrue(leaf.nodeRootFile.exists());
        assertTrue(leaf.nodeRootFile.isDirectory());

        // fail node props load
        leaf.getChildCount();
        assertTrue(leaf.nodePropsFile.exists());
        File renameFile = new File(leaf.nodePropsFile.getAbsolutePath() + RepositoryNodeImpl.FAULTY_FILE_EXTENSION);
        assertFalse(renameFile.exists());
        leaf.failPropsLoad = true;
        assertTrue(leaf.checkNodeRootConsistency());
        assertFalse(leaf.nodePropsFile.exists());
        assertTrue(renameFile.exists());
    }

    public void testCheckContentConsistency() throws Exception {
        MyMockRepositoryNode leaf = new MyMockRepositoryNode(
                (RepositoryNodeImpl) createLeaf("http://www.example.com/testDir", "test stream", null));
        leaf.ensureCurrentInfoLoaded();

        // should return false if content dir fails
        MyMockRepositoryNode.failEnsureDirExists = true;
        assertFalse(leaf.checkContentConsistency());
        MyMockRepositoryNode.failEnsureDirExists = false;
        assertTrue(leaf.checkContentConsistency());

        // should return false if content file absent
        File renameFile = new File(leaf.currentCacheFile.getAbsolutePath() + "RENAME");
        assertTrue(PlatformUtil.updateAtomically(leaf.currentCacheFile, renameFile));
        assertFalse(leaf.checkContentConsistency());
        PlatformUtil.updateAtomically(renameFile, leaf.currentCacheFile);
        assertTrue(leaf.checkContentConsistency());

        // should return false if content props absent
        PlatformUtil.updateAtomically(leaf.currentPropsFile, renameFile);
        assertFalse(leaf.checkContentConsistency());
        PlatformUtil.updateAtomically(renameFile, leaf.currentPropsFile);
        assertTrue(leaf.checkContentConsistency());

        // should return false if inactive and files missing
        leaf.currentVersion = RepositoryNodeImpl.INACTIVE_VERSION;
        assertFalse(leaf.checkContentConsistency());
        PlatformUtil.updateAtomically(leaf.currentPropsFile, leaf.getInactivePropsFile());
        assertFalse(leaf.checkContentConsistency());
        PlatformUtil.updateAtomically(leaf.currentCacheFile, leaf.getInactiveCacheFile());
        assertTrue(leaf.checkContentConsistency());
        PlatformUtil.updateAtomically(leaf.getInactivePropsFile(), leaf.currentPropsFile);
        assertFalse(leaf.checkContentConsistency());
        // finish restoring
        PlatformUtil.updateAtomically(leaf.getInactiveCacheFile(), leaf.currentCacheFile);
        leaf.currentVersion = 1;
        assertTrue(leaf.checkContentConsistency());

        // remove residual files
        // - create files
        FileOutputStream fos = new FileOutputStream(leaf.tempCacheFile);
        StringInputStream sis = new StringInputStream("test stream");
        StreamUtil.copy(sis, fos);
        fos.close();
        sis.close();

        fos = new FileOutputStream(leaf.tempPropsFile);
        sis = new StringInputStream("test stream");
        StreamUtil.copy(sis, fos);
        fos.close();
        sis.close();

        // should be removed
        assertTrue(leaf.tempCacheFile.exists());
        assertTrue(leaf.tempPropsFile.exists());
        assertTrue(leaf.checkContentConsistency());
        assertFalse(leaf.tempCacheFile.exists());
        assertFalse(leaf.tempPropsFile.exists());
    }

    public void testEnsureDirExists() throws Exception {
        RepositoryNodeImpl leaf = (RepositoryNodeImpl) createLeaf("http://www.example.com", null, null);
        File testDir = new File(tempDirPath, "testDir");
        // should return true if dir exists
        testDir.mkdir();
        assertTrue(testDir.exists());
        assertTrue(testDir.isDirectory());
        assertTrue(leaf.ensureDirExists(testDir));

        // should create dir, return true if not exists
        testDir.delete();
        assertFalse(testDir.exists());
        assertTrue(leaf.ensureDirExists(testDir));
        assertTrue(testDir.exists());
        assertTrue(testDir.isDirectory());

        // should rename file, create dir, return true if file exists
        // -create file
        testDir.delete();
        FileOutputStream fos = new FileOutputStream(testDir);
        StringInputStream sis = new StringInputStream("test stream");
        StreamUtil.copy(sis, fos);
        fos.close();
        sis.close();
        assertTrue(testDir.exists());
        assertTrue(testDir.isFile());

        // rename via 'ensureDirExists()'
        File renameFile = new File(tempDirPath, "testDir" + RepositoryNodeImpl.FAULTY_FILE_EXTENSION);
        assertFalse(renameFile.exists());
        assertTrue(leaf.ensureDirExists(testDir));
        assertTrue(testDir.isDirectory());
        assertEquals("test stream", StringUtil.fromFile(renameFile));
    }

    public void testCheckFileExists() throws Exception {
        // return false if doesn't exist
        File testFile = new File(tempDirPath, "testFile");
        assertFalse(testFile.exists());
        assertFalse(RepositoryNodeImpl.checkFileExists(testFile, "test file"));

        // rename if dir (to make room for file creation), then return false
        testFile.mkdir();
        File renameDir = new File(tempDirPath, "testFile" + RepositoryNodeImpl.FAULTY_FILE_EXTENSION);
        assertTrue(testFile.exists());
        assertTrue(testFile.isDirectory());
        assertFalse(renameDir.exists());
        assertFalse(RepositoryNodeImpl.checkFileExists(testFile, "test file"));
        assertFalse(testFile.exists());
        assertTrue(renameDir.exists());
        assertTrue(renameDir.isDirectory());

        // return true if exists
        FileOutputStream fos = new FileOutputStream(testFile);
        StringInputStream sis = new StringInputStream("test stream");
        StreamUtil.copy(sis, fos);
        fos.close();
        sis.close();
        assertTrue(testFile.exists());
        assertTrue(testFile.isFile());
        assertTrue(RepositoryNodeImpl.checkFileExists(testFile, "test file"));
        assertEquals("test stream", StringUtil.fromFile(testFile));
    }

    public void testCheckChildCountCacheAccuracy() throws Exception {
        createLeaf("http://www.example.com/testDir/branch2", "test stream", null);
        createLeaf("http://www.example.com/testDir/branch3", "test stream", null);

        RepositoryNodeImpl dirEntry = (RepositoryNodeImpl) repo.getNode("http://www.example.com/testDir");
        assertEquals(2, dirEntry.getChildCount());
        assertEquals("2", dirEntry.nodeProps.getProperty(RepositoryNodeImpl.CHILD_COUNT_PROPERTY));

        // check that no change to valid count cache
        dirEntry.checkChildCountCacheAccuracy();
        log.debug2("child count: " + dirEntry.nodeProps.getProperty(RepositoryNodeImpl.CHILD_COUNT_PROPERTY));
        assertEquals("2", dirEntry.nodeProps.getProperty(RepositoryNodeImpl.CHILD_COUNT_PROPERTY));

        // check that invalid cache removed
        dirEntry.nodeProps.setProperty(RepositoryNodeImpl.CHILD_COUNT_PROPERTY, "3");
        dirEntry.checkChildCountCacheAccuracy();
        assertEquals(RepositoryNodeImpl.INVALID,
                dirEntry.nodeProps.getProperty(RepositoryNodeImpl.CHILD_COUNT_PROPERTY));
    }

    public void testEncodeUrl() {
        assertEquals(null, RepositoryNodeImpl.encodeUrl(null));
        assertEquals("", RepositoryNodeImpl.encodeUrl(""));
        assertEquals("www.example.com", RepositoryNodeImpl.encodeUrl("www.example.com"));
        assertEquals("www.example.com/val", RepositoryNodeImpl.encodeUrl("www.example.com/val"));
        assertEquals("www.example.com%5Cval", RepositoryNodeImpl.encodeUrl("www.example.com\\val"));
        assertEquals("www.example.com/val%5Cval", RepositoryNodeImpl.encodeUrl("www.example.com/val\\val"));
        assertEquals("www.example.com/val/val", RepositoryNodeImpl.encodeUrl("www.example.com/val/val"));
        assertEquals("www.example.com/val%5C%5Cval", RepositoryNodeImpl.encodeUrl("www.example.com/val\\\\val"));
        assertEquals("www.example.com/val/val", RepositoryNodeImpl.encodeUrl("www.example.com/val/val"));
        assertEquals("www.example.com/val/val/", RepositoryNodeImpl.encodeUrl("www.example.com/val/val/"));
        assertEquals("www.example.com/val/val%5C", RepositoryNodeImpl.encodeUrl("www.example.com/val/val\\"));
        assertEquals("www.example.com%5Cval%5Cval%5C", RepositoryNodeImpl.encodeUrl("www.example.com\\val\\val\\"));
    }

    public void testEncodeUrlUnicode() {
        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_MAX_COMPONENT_LENGTH, "30");

        assertEquals("www.example.com/val/12345678901234567890123456789\\/0123",
                RepositoryNodeImpl.encodeUrl("www.example.com/val/123456789012345678901234567890123"));
        assertEquals("www.example.com/val/123\u00E7567890123456789012345678\\/90123",
                RepositoryNodeImpl.encodeUrl("www.example.com/val/123\u00E756789012345678901234567890123"));
    }

    public void testEncodeUrlCompatibility() {
        ConfigurationUtil.addFromArgs(RepositoryManager.PARAM_ENABLE_LONG_COMPONENTS_COMPATIBILITY, "true");
        assertEquals(null, RepositoryNodeImpl.encodeUrl(null));
        assertEquals("", RepositoryNodeImpl.encodeUrl(""));
        assertEquals("www.example.com", RepositoryNodeImpl.encodeUrl("www.example.com"));
        assertEquals("www.example.com/val", RepositoryNodeImpl.encodeUrl("www.example.com/val"));
        assertEquals("www.example.com%5cval", RepositoryNodeImpl.encodeUrl("www.example.com\\val"));
        assertEquals("www.example.com/val%5cval", RepositoryNodeImpl.encodeUrl("www.example.com/val\\val"));
        assertEquals("www.example.com/val/val", RepositoryNodeImpl.encodeUrl("www.example.com/val/val"));
        assertEquals("www.example.com/val%5c%5cval", RepositoryNodeImpl.encodeUrl("www.example.com/val\\\\val"));
        assertEquals("www.example.com/val/val", RepositoryNodeImpl.encodeUrl("www.example.com/val/val"));
        assertEquals("www.example.com/val/val/", RepositoryNodeImpl.encodeUrl("www.example.com/val/val/"));
        assertEquals("www.example.com/val/val%5c", RepositoryNodeImpl.encodeUrl("www.example.com/val/val\\"));
        assertEquals("www.example.com%5cval%5cval%5c", RepositoryNodeImpl.encodeUrl("www.example.com\\val\\val\\"));
    }

    public void testShortDecodeUrl() {
        assertEquals(null, RepositoryNodeImpl.decodeUrl(null));
        assertEquals("", RepositoryNodeImpl.decodeUrl(""));
        assertEquals("www.example.com", RepositoryNodeImpl.decodeUrl("www.example.com"));
        assertEquals("www.example.com/val", RepositoryNodeImpl.decodeUrl("www.example.com/val"));
        assertEquals("www.example.com%5Cval", RepositoryNodeImpl.decodeUrl("www.example.com%5Cval"));
        assertEquals("www.example.com/val%5Cval", RepositoryNodeImpl.decodeUrl("www.example.com/val%5Cval"));
        assertEquals("www.example.com/val/val", RepositoryNodeImpl.decodeUrl("www.example.com/val/val"));
        assertEquals("www.example.com/val%5C%5Cval", RepositoryNodeImpl.decodeUrl("www.example.com/val%5C%5Cval"));
        assertEquals("www.example.com/val/val", RepositoryNodeImpl.decodeUrl("www.example.com/val/val"));
        assertEquals("www.example.com/val/val/", RepositoryNodeImpl.decodeUrl("www.example.com/val/val/"));
        assertEquals("www.example.com/val/val%5C", RepositoryNodeImpl.decodeUrl("www.example.com/val/val%5C"));
        assertEquals("www.example.com%5cval%5Cval%5c",
                RepositoryNodeImpl.decodeUrl("www.example.com%5cval%5Cval%5c"));
    }

    public void testLongDecodeUrl() {
        StringBuilder longUrl = new StringBuilder();
        longUrl.append("www.example.com/");
        for (int i = 0; i < 218; i++) {
            longUrl.append(i + ",");
        }
        longUrl.append(".");
        String result = RepositoryNodeImpl.encodeUrl(longUrl.toString());
        log.debug2(longUrl.toString());
        log.debug2(result);
        String result2 = RepositoryNodeImpl.decodeUrl(result);
        log.debug2(result2);
        assertTrue(longUrl.toString().equals(result2));
    }

    private RepositoryNode createLeaf(String url, String content, Properties props) throws Exception {
        return createLeaf(repo, url, content, props);
    }

    public static RepositoryNode createLeaf(LockssRepository repo, String url, String content, Properties props)
            throws Exception {
        RepositoryNode leaf = repo.createNewNode(url);
        createContentVersion(leaf, content, props);
        return leaf;
    }

    public static RepositoryNode createLeaf(LockssRepository repo, String url, InputStream contentStream,
            Properties props) throws Exception {
        RepositoryNode leaf = repo.createNewNode(url);
        createContentVersion(leaf, contentStream, props);
        return leaf;
    }

    public static void createContentVersion(RepositoryNode leaf, String content, Properties props)
            throws Exception {
        leaf.makeNewVersion();
        writeToLeaf(leaf, content);
        if (props == null) {
            props = new Properties();
        }
        leaf.setNewProperties(props);
        leaf.sealNewVersion();
    }

    public static void createContentVersion(RepositoryNode leaf, InputStream contentStream, Properties props)
            throws Exception {
        leaf.makeNewVersion();
        writeToLeaf(leaf, contentStream);
        if (props == null) {
            props = new Properties();
        }
        leaf.setNewProperties(props);
        leaf.sealNewVersion();
    }

    public static void writeToLeaf(RepositoryNode leaf, String content) throws Exception {
        if (content == null) {
            content = "";
        }
        OutputStream os = leaf.getNewOutputStream();
        InputStream is = new StringInputStream(content);
        StreamUtil.copy(is, os);
        os.close();
        is.close();
    }

    public static void writeToLeaf(RepositoryNode leaf, InputStream contentStream) throws Exception {
        OutputStream os = leaf.getNewOutputStream();
        StreamUtil.copy(contentStream, os);
        os.close();
        contentStream.close();
    }

    public static String getLeafContent(RepositoryNodeVersion leaf) throws IOException {
        return getRNCContent(leaf.getNodeContents());
    }

    public static String getRNCContent(RepositoryNode.RepositoryNodeContents rnc) throws IOException {
        InputStream is = rnc.getInputStream();
        OutputStream baos = new ByteArrayOutputStream(20);
        StreamUtil.copy(is, baos);
        is.close();
        String resultStr = baos.toString();
        baos.close();
        return resultStr;
    }

    public static void main(String[] argv) {
        String[] testCaseList = { TestRepositoryNodeImpl.class.getName() };
        junit.swingui.TestRunner.main(testCaseList);
    }

    // this class overrides 'getDatedVersionedPropsFile()' so I can
    // manipulate the file names for testing.  Also allows 'loadNodeProps()
    // to fail on demand
    static class MyMockRepositoryNode extends RepositoryNodeImpl {
        long dateValue;
        boolean failPropsLoad = false;
        boolean failRootConsist = false;
        boolean failContentConsist = false;
        boolean failEnsureCurrentLoaded = false;
        static boolean failEnsureDirExists = false;

        MyMockRepositoryNode(RepositoryNodeImpl nodeImpl) {
            super(nodeImpl.url, nodeImpl.nodeLocation, nodeImpl.repository);
        }

        File getDatedVersionedPropsFile(int version, long date) {
            StringBuilder buffer = new StringBuilder();
            buffer.append(version);
            buffer.append(PROPS_EXTENSION);
            buffer.append("-");
            buffer.append(dateValue);
            return new File(getContentDir(), buffer.toString());
        }

        void loadNodeProps(boolean okIfNotThere) {
            if (failPropsLoad) {
                throw new LockssRepository.RepositoryStateException("Couldn't load properties file.");
            } else {
                super.loadNodeProps(okIfNotThere);
            }
        }

        boolean checkNodeRootConsistency() {
            if (failRootConsist) {
                return false;
            } else {
                return super.checkNodeRootConsistency();
            }
        }

        boolean checkContentConsistency() {
            if (failContentConsist) {
                return false;
            } else {
                return super.checkContentConsistency();
            }
        }

        void ensureCurrentInfoLoaded() {
            if (failEnsureCurrentLoaded) {
                throw new LockssRepository.RepositoryStateException("Couldn't load current info.");
            } else {
                super.ensureCurrentInfoLoaded();
            }
        }

        boolean ensureDirExists(File dirFile) {
            if (failEnsureDirExists) {
                return false;
            } else {
                return super.ensureDirExists(dirFile);
            }
        }
    }

    static class MyLockssRepositoryImpl extends LockssRepositoryImpl {
        boolean dontNormalize = false;

        void setDontNormalize(boolean val) {
            dontNormalize = val;
        }

        MyLockssRepositoryImpl(String rootPath) {
            super(rootPath);
        }

        public String canonicalizePath(String url) throws MalformedURLException {
            if (dontNormalize)
                return url;
            return super.canonicalizePath(url);
        }

        public static LockssRepository createNewLockssRepository(ArchivalUnit au) {
            String root = getRepositoryRoot(au);
            if (root == null) {
                throw new LockssRepository.RepositoryStateException("null root");
            }
            String auDir = LockssRepositoryImpl.mapAuToFileLocation(root, au);
            log.debug("repo: " + auDir + ", au: " + au.getName());
            //       staticCacheLocation = extendCacheLocation(root);
            LockssRepositoryImpl repo = new MyLockssRepositoryImpl(auDir);
            Plugin plugin = au.getPlugin();
            if (plugin != null) {
                LockssDaemon daemon = plugin.getDaemon();
                if (daemon != null) {
                    RepositoryManager mgr = daemon.getRepositoryManager();
                    if (mgr != null) {
                        mgr.setRepositoryForPath(auDir, repo);
                    }
                }
            }
            return repo;
        }

    }
}