/** Export to an ARC file */
public class ArcExporter extends Exporter {

  private static Logger log = Logger.getLogger("ArcExporter");

  protected CIProperties arcProps = null;
  String arcFilePrefix = "SimulatedCrawl";
  AtomicInteger serialNo = new AtomicInteger(0);
  ARCWriter aw;
  boolean isResponse;

  public ArcExporter(LockssDaemon daemon, ArchivalUnit au, boolean isResponse) {
    super(daemon, au);
    this.isResponse = isResponse;
  }

  protected void start() {
    aw = makeARCWriter();
  }

  protected void finish() throws IOException {
    aw.close();
  }

  private ARCWriter makeARCWriter() {
    return new ARCWriter(
        serialNo, ListUtil.list(dir), prefix, compress, maxSize >= 0 ? maxSize : Long.MAX_VALUE);
  }

  protected void writeCu(CachedUrl cu) throws IOException {
    String url = cu.getUrl();
    long contentSize = cu.getContentSize();
    CIProperties props = cu.getProperties();
    long fetchTime = Long.parseLong(props.getProperty(CachedUrl.PROPERTY_FETCH_TIME));
    InputStream contentIn = cu.getUnfilteredInputStream();
    try {
      if (isResponse) {
        String hdrString = getHttpResponseString(cu);
        long size = contentSize + hdrString.length();
        InputStream headerIn = new ReaderInputStream(new StringReader(hdrString));
        InputStream concat = new SequenceInputStream(headerIn, contentIn);
        try {
          aw.write(xlateFilename(url), cu.getContentType(), getHostIp(), fetchTime, size, concat);
        } finally {
          IOUtil.safeClose(concat);
        }
      } else {
        aw.write(
            xlateFilename(url),
            cu.getContentType(),
            getHostIp(),
            fetchTime,
            cu.getContentSize(),
            contentIn);
      }
    } finally {
      AuUtil.safeRelease(cu);
    }
  }
}
Exemple #2
0
/** JUnitTest case for class: org.lockss.poller.Poll */
public class TestPoll extends LockssTestCase {
  private static Logger log = Logger.getLogger("TestPoll");
  private static String[] rootV1urls = {
    "http://www.test.org", "http://www.test1.org", "http://www.test2.org"
  };
  private static String lwrbnd = "test1.doc";
  private static String uprbnd = "test3.doc";
  private static long testduration = Constants.DAY;

  //   protected ArchivalUnit testau =
  //       PollTestPlugin.PTArchivalUnit.createFromListOfRootUrls(rootV1urls);
  protected MockArchivalUnit testau;
  private IdentityManager idmgr;
  private MockLockssDaemon theDaemon;
  private ArrayList agree_entries = makeEntries(10, 50);
  private ArrayList disagree_entries = makeEntries(15, 57);
  private ArrayList dissenting_entries = makeEntries(7, 50);

  protected PeerIdentity testID;
  protected PeerIdentity testID1;
  protected V1LcapMessage[] testV1msg;
  protected V1Poll[] testV1polls;
  protected PollManager pollmanager;

  protected void setUp() throws Exception {
    super.setUp();
    TimeBase.setSimulated();

    initRequiredServices();

    testau.setPlugin(new MockPlugin());

    initTestPeerIDs();
    initTestMsg();
    initTestPolls();
  }

  /**
   * tearDown method for test case
   *
   * @throws Exception if removePoll failed
   */
  public void tearDown() throws Exception {
    pollmanager.stopService();
    theDaemon.getLockssRepository(testau).stopService();
    theDaemon.getHashService().stopService();
    theDaemon.getDatagramRouterManager().stopService();
    theDaemon.getRouterManager().stopService();
    theDaemon.getSystemMetrics().stopService();
    TimeBase.setReal();
    for (int i = 0; i < testV1msg.length; i++) {
      if (testV1msg[i] != null) pollmanager.removePoll(testV1msg[i].getKey());
    }
    super.tearDown();
  }

  /** test for method scheduleVote(..) */
  public void testScheduleVote() {
    V1Poll p = testV1polls[1];
    assertTrue(p instanceof V1ContentPoll);
    log.debug3("testScheduleVote 1");
    p.scheduleVote();
    log.debug3("testScheduleVote 2");
    assertNotNull(p.m_voteTime);
    assertTrue(p.m_voteTime.getRemainingTime() < p.m_deadline.getRemainingTime());
    log.debug3("at end of testScheduleVote");
  }

  /** test for method checkVote(..) */
  public void testCheckVote() throws Exception {
    V1LcapMessage msg = null;
    log.debug3("starting testCheeckVote");
    msg =
        V1LcapMessage.makeReplyMsg(
            testV1polls[0].getMessage(),
            ByteArray.makeRandomBytes(20),
            ByteArray.makeRandomBytes(20),
            null,
            V1LcapMessage.NAME_POLL_REP,
            testduration,
            testID);
    log.debug3("testCheeckVote 2");
    V1Poll p = null;
    p = createCompletedPoll(theDaemon, testau, msg, 8, 2, pollmanager);
    assertTrue(p instanceof V1NamePoll);
    log.debug3("testCheeckVote 3");
    assertNotNull(p);
    PeerIdentity id = msg.getOriginatorId();
    assertNotNull(id);
    assertNotNull(p.m_tally);
    int rep = p.m_tally.wtAgree + idmgr.getReputation(id);

    // good vote check

    p.checkVote(msg.getHashed(), new Vote(msg, false));
    assertEquals(9, p.m_tally.numAgree);
    assertEquals(2, p.m_tally.numDisagree);
    assertEquals(rep, p.m_tally.wtAgree);

    rep = p.m_tally.wtDisagree + idmgr.getReputation(id);

    // bad vote check
    p.checkVote(ByteArray.makeRandomBytes(20), new Vote(msg, false));
    assertEquals(9, p.m_tally.numAgree);
    assertEquals(3, p.m_tally.numDisagree);
    assertEquals(rep, p.m_tally.wtDisagree);
  }

  /** test for method tally(..) */
  public void testTally() {
    V1Poll p = testV1polls[0];
    LcapMessage msg = p.getMessage();
    PeerIdentity id = msg.getOriginatorId();
    p.m_tally.addVote(new Vote(msg, false), id, false);
    p.m_tally.addVote(new Vote(msg, false), id, false);
    p.m_tally.addVote(new Vote(msg, false), id, false);
    assertEquals(0, p.m_tally.numAgree);
    assertEquals(0, p.m_tally.wtAgree);
    assertEquals(3, p.m_tally.numDisagree);
    assertEquals(1500, p.m_tally.wtDisagree);

    p = testV1polls[1];
    msg = p.getMessage();
    p.m_tally.addVote(new Vote(msg, true), id, false);
    p.m_tally.addVote(new Vote(msg, true), id, false);
    p.m_tally.addVote(new Vote(msg, true), id, false);
    assertEquals(3, p.m_tally.numAgree);
    assertEquals(1500, p.m_tally.wtAgree);
    assertEquals(0, p.m_tally.numDisagree);
    assertEquals(0, p.m_tally.wtDisagree);
  }

  public void testNamePollTally() throws Exception {
    V1NamePoll np;
    // test a name poll we won
    np = makeCompletedNamePoll(4, 1, 0);
    assertEquals(5, np.m_tally.numAgree);
    assertEquals(1, np.m_tally.numDisagree);
    assertEquals(Tallier.RESULT_WON, np.m_tally.getTallyResult());

    // test a name poll we lost with a dissenting vote
    np = makeCompletedNamePoll(1, 8, 1);

    assertEquals(2, np.m_tally.numAgree);
    assertEquals(9, np.m_tally.numDisagree);

    // build a master list
    np.buildPollLists(np.m_tally.pollVotes.iterator());

    // these should be different since we lost the poll
    assertFalse(CollectionUtil.isIsomorphic(np.m_tally.localEntries, np.m_tally.votedEntries));

    // the expected "correct" set is in our disagree msg
    assertTrue(CollectionUtil.isIsomorphic(disagree_entries, np.m_tally.votedEntries));
  }

  /** test for method vote(..) */
  public void testVote() {
    V1Poll p = testV1polls[1];
    p.m_hash = ByteArray.makeRandomBytes(20);
    try {
      p.castOurVote();
    } catch (IllegalStateException e) {
      // the socket isn't inited and should squack
    }
    p.m_pollstate = V1Poll.PS_COMPLETE;
  }

  /** test for method voteInPoll(..) */
  public void testVoteInPoll() {
    V1Poll p = testV1polls[1];
    p.m_tally.quorum = 10;
    p.m_tally.numAgree = 5;
    p.m_tally.numDisagree = 2;
    p.m_tally.wtAgree = 2000;
    p.m_tally.wtDisagree = 200;
    p.m_hash = ByteArray.makeRandomBytes(20);
    try {
      p.voteInPoll();
    } catch (IllegalStateException e) {
      // the socket isn't inited and should squack
    }

    p.m_tally.numAgree = 20;
    try {
      p.voteInPoll();
    } catch (NullPointerException npe) {
      // the socket isn't inited and should squack
    }
    p.m_pollstate = V1Poll.PS_COMPLETE;
  }

  public void testStartPoll() {
    V1Poll p = testV1polls[0];
    p.startPoll();
    assertEquals(V1Poll.PS_WAIT_HASH, p.m_pollstate);
    p.m_pollstate = V1Poll.PS_COMPLETE;
  }

  public void testScheduleOurHash() {
    V1Poll p = testV1polls[0];
    p.m_pollstate = V1Poll.PS_WAIT_HASH;
    // no time has elapsed - so we should be able to schedule our hash
    assertTrue(p.scheduleOurHash());
    // half the time has elapsed so we should be able to schedule our hash
    TimeBase.step(p.m_deadline.getRemainingTime() / 2);
    assertTrue(p.scheduleOurHash());

    // all of the time has elapsed we should not be able to schedule our hash
    TimeBase.step(p.m_deadline.getRemainingTime() - 1000);
    assertFalse(p.scheduleOurHash());
    p.m_pollstate = V1Poll.PS_COMPLETE;
  }

  /** test for method stopPoll(..) */
  public void testStopPoll() {
    V1Poll p = testV1polls[1];
    p.m_tally.quorum = 10;
    p.m_tally.numAgree = 7;
    p.m_tally.numDisagree = 3;
    p.m_pollstate = V1Poll.PS_WAIT_TALLY;
    p.stopPoll();
    assertTrue(p.m_pollstate == V1Poll.PS_COMPLETE);
    p.startPoll();
    assertTrue(p.m_pollstate == V1Poll.PS_COMPLETE);
  }

  /** test for method startVoteCheck(..) */
  public void testStartVote() {
    V1Poll p = testV1polls[0];
    p.m_pendingVotes = 3;
    p.startVoteCheck();
    assertEquals(4, p.m_pendingVotes);
    p.m_pollstate = V1Poll.PS_COMPLETE;
  }

  /** test for method stopVote(..) */
  public void testStopVote() {
    V1Poll p = testV1polls[1];
    p.m_pendingVotes = 3;
    p.stopVoteCheck();
    assertEquals(2, p.m_pendingVotes);
    p.m_pollstate = V1Poll.PS_COMPLETE;
  }

  private V1NamePoll makeCompletedNamePoll(int numAgree, int numDisagree, int numDissenting)
      throws Exception {
    V1NamePoll np = null;
    V1LcapMessage agree_msg = null;
    V1LcapMessage disagree_msg1 = null;
    V1LcapMessage disagree_msg2 = null;

    Plugin plugin = testau.getPlugin();
    PollSpec spec = new MockPollSpec(testau, rootV1urls[0], null, null, Poll.V1_NAME_POLL);
    ((MockCachedUrlSet) spec.getCachedUrlSet()).setHasContent(false);
    V1LcapMessage poll_msg =
        V1LcapMessage.makeRequestMsg(
            spec,
            null,
            ByteArray.makeRandomBytes(20),
            ByteArray.makeRandomBytes(20),
            V1LcapMessage.NAME_POLL_REQ,
            testduration,
            testID);

    // make our poll
    np =
        (V1NamePoll)
            new V1NamePoll(
                spec,
                pollmanager,
                poll_msg.getOriginatorId(),
                poll_msg.getChallenge(),
                poll_msg.getDuration(),
                poll_msg.getHashAlgorithm());
    np.setMessage(poll_msg);

    // generate agree vote msg
    agree_msg =
        V1LcapMessage.makeReplyMsg(
            poll_msg,
            ByteArray.makeRandomBytes(20),
            poll_msg.getVerifier(),
            agree_entries,
            V1LcapMessage.NAME_POLL_REP,
            testduration,
            testID);

    // generate a disagree vote msg
    disagree_msg1 =
        V1LcapMessage.makeReplyMsg(
            poll_msg,
            ByteArray.makeRandomBytes(20),
            ByteArray.makeRandomBytes(20),
            disagree_entries,
            V1LcapMessage.NAME_POLL_REP,
            testduration,
            testID1);
    // generate a losing disagree vote msg
    disagree_msg2 =
        V1LcapMessage.makeReplyMsg(
            poll_msg,
            ByteArray.makeRandomBytes(20),
            ByteArray.makeRandomBytes(20),
            dissenting_entries,
            V1LcapMessage.NAME_POLL_REP,
            testduration,
            testID1);

    // add our vote
    V1LcapMessage msg = (V1LcapMessage) (np.getMessage());
    PeerIdentity id = msg.getOriginatorId();
    np.m_tally.addVote(np.makeNameVote(msg, true), id, true);

    // add the agree votes
    id = agree_msg.getOriginatorId();
    for (int i = 0; i < numAgree; i++) {
      np.m_tally.addVote(np.makeNameVote(agree_msg, true), id, false);
    }

    // add the disagree votes
    id = disagree_msg1.getOriginatorId();
    for (int i = 0; i < numDisagree; i++) {
      np.m_tally.addVote(np.makeNameVote(disagree_msg1, false), id, false);
    }

    // add dissenting disagree vote
    id = disagree_msg2.getOriginatorId();
    for (int i = 0; i < numDissenting; i++) {
      np.m_tally.addVote(np.makeNameVote(disagree_msg2, false), id, false);
    }
    np.m_pollstate = V1Poll.PS_COMPLETE;
    np.m_tally.tallyVotes();
    return np;
  }

  public static V1Poll createCompletedPoll(
      LockssDaemon daemon,
      ArchivalUnit au,
      V1LcapMessage testmsg,
      int numAgree,
      int numDisagree,
      PollManager pollmanager)
      throws Exception {
    log.debug(
        "createCompletedPoll: au: "
            + au.toString()
            + " peer "
            + testmsg.getOriginatorId()
            + " votes "
            + numAgree
            + "/"
            + numDisagree);
    CachedUrlSetSpec cusSpec = null;
    if ((testmsg.getLwrBound() != null)
        && (testmsg.getLwrBound().equals(PollSpec.SINGLE_NODE_LWRBOUND))) {
      cusSpec = new SingleNodeCachedUrlSetSpec(testmsg.getTargetUrl());
    } else {
      cusSpec =
          new RangeCachedUrlSetSpec(
              testmsg.getTargetUrl(), testmsg.getLwrBound(), testmsg.getUprBound());
    }
    CachedUrlSet cus = au.makeCachedUrlSet(cusSpec);
    PollSpec spec = new PollSpec(cus, Poll.V1_CONTENT_POLL);
    ((MockCachedUrlSet) spec.getCachedUrlSet()).setHasContent(false);
    V1Poll p = null;
    if (testmsg.isContentPoll()) {
      p =
          new V1ContentPoll(
              spec,
              pollmanager,
              testmsg.getOriginatorId(),
              testmsg.getChallenge(),
              testmsg.getDuration(),
              testmsg.getHashAlgorithm());
    } else if (testmsg.isNamePoll()) {
      p =
          new V1NamePoll(
              spec,
              pollmanager,
              testmsg.getOriginatorId(),
              testmsg.getChallenge(),
              testmsg.getDuration(),
              testmsg.getHashAlgorithm());
    } else if (testmsg.isVerifyPoll()) {
      p =
          new V1VerifyPoll(
              spec,
              pollmanager,
              testmsg.getOriginatorId(),
              testmsg.getChallenge(),
              testmsg.getDuration(),
              testmsg.getHashAlgorithm(),
              testmsg.getVerifier());
    }
    assertNotNull(p);
    p.setMessage(testmsg);
    p.m_tally.quorum = numAgree + numDisagree;
    p.m_tally.numAgree = numAgree;
    p.m_tally.numDisagree = numDisagree;
    p.m_tally.wtAgree = 2000;
    p.m_tally.wtDisagree = 200;
    p.m_tally.localEntries = makeEntries(1, 3);
    p.m_tally.votedEntries = makeEntries(1, 5);
    p.m_tally.votedEntries.remove(1);
    p.m_pollstate = V1Poll.PS_COMPLETE;
    p.m_callerID = testmsg.getOriginatorId();
    log.debug3("poll " + p.toString());
    p.m_tally.tallyVotes();
    return p;
  }

  public static ArrayList makeEntries(int firstEntry, int lastEntry) {
    int numEntries = lastEntry - firstEntry + 1;
    ArrayList ret_arry = new ArrayList(numEntries);

    for (int i = 0; i < numEntries; i++) {
      String name = "/testentry" + (firstEntry + i) + ".html";
      ret_arry.add(new PollTally.NameListEntry(i % 2 == 1, name));
    }

    return ret_arry;
  }

  private void initRequiredServices() {
    theDaemon = getMockLockssDaemon();
    pollmanager = new LocalPollManager();
    pollmanager.initService(theDaemon);
    theDaemon.setPollManager(pollmanager);

    theDaemon.getPluginManager();
    testau = PollTestPlugin.PTArchivalUnit.createFromListOfRootUrls(rootV1urls);
    PluginTestUtil.registerArchivalUnit(testau);

    String tempDirPath = null;
    try {
      tempDirPath = getTempDir().getAbsolutePath() + File.separator;
    } catch (IOException ex) {
      fail("unable to create a temporary directory");
    }

    Properties p = new Properties();
    p.setProperty(IdentityManager.PARAM_IDDB_DIR, tempDirPath + "iddb");
    p.setProperty(LockssRepositoryImpl.PARAM_CACHE_LOCATION, tempDirPath);
    p.setProperty(ConfigManager.PARAM_PLATFORM_DISK_SPACE_LIST, tempDirPath);
    p.setProperty(IdentityManager.PARAM_LOCAL_IP, "127.0.0.1");
    p.setProperty(ConfigManager.PARAM_NEW_SCHEDULER, "false");
    // XXX we need to disable verification of votes because the
    // voter isn't really there
    p.setProperty(V1Poll.PARAM_AGREE_VERIFY, "0");
    p.setProperty(V1Poll.PARAM_DISAGREE_VERIFY, "0");
    ConfigurationUtil.setCurrentConfigFromProps(p);
    idmgr = theDaemon.getIdentityManager();
    idmgr.startService();
    // theDaemon.getSchedService().startService();
    theDaemon.getHashService().startService();
    theDaemon.getDatagramRouterManager().startService();
    theDaemon.getRouterManager().startService();
    theDaemon.getSystemMetrics().startService();
    theDaemon.getActivityRegulator(testau).startService();
    theDaemon.setNodeManager(new MockNodeManager(), testau);
    pollmanager.startService();
  }

  private void initTestPeerIDs() {
    try {
      testID = idmgr.stringToPeerIdentity("127.0.0.1");
      testID1 = idmgr.stringToPeerIdentity("1.1.1.1");
    } catch (IdentityManager.MalformedIdentityKeyException ex) {
      fail("can't open test host");
    }
  }

  private void initTestMsg() throws Exception {
    testV1msg = new V1LcapMessage[3];
    int[] pollType = {
      Poll.V1_NAME_POLL, Poll.V1_CONTENT_POLL, Poll.V1_VERIFY_POLL,
    };
    PollFactory ppf = pollmanager.getPollFactory(1);
    assertNotNull("PollFactory should not be null", ppf);
    // XXX V1 support mandatory
    assertTrue(ppf instanceof V1PollFactory);
    V1PollFactory pf = (V1PollFactory) ppf;

    for (int i = 0; i < testV1msg.length; i++) {
      PollSpec spec = new MockPollSpec(testau, rootV1urls[i], lwrbnd, uprbnd, pollType[i]);
      log.debug("Created poll spec: " + spec);
      ((MockCachedUrlSet) spec.getCachedUrlSet()).setHasContent(false);
      int opcode = V1LcapMessage.NAME_POLL_REQ + (i * 2);
      long duration = -1;
      //  NB calcDuration is not applied to Verify polls.
      switch (opcode) {
        case V1LcapMessage.NAME_POLL_REQ:
        case V1LcapMessage.CONTENT_POLL_REQ:
          // this will attempt to schedule and can return -1
          duration = Math.max(pf.calcDuration(spec, pollmanager), 1000);
          break;
        case V1LcapMessage.VERIFY_POLL_REQ:
        case V1LcapMessage.VERIFY_POLL_REP:
          duration = 100000; // Arbitrary
          break;
        default:
          fail("Bad opcode " + opcode);
          break;
      }

      testV1msg[i] =
          V1LcapMessage.makeRequestMsg(
              spec,
              agree_entries,
              pf.makeVerifier(100000),
              pf.makeVerifier(100000),
              opcode,
              duration,
              testID);
      assertNotNull(testV1msg[i]);
    }
  }

  private void initTestPolls() throws Exception {
    testV1polls = new V1Poll[testV1msg.length];
    for (int i = 0; i < testV1polls.length; i++) {
      log.debug3("initTestPolls: V1 " + i);
      BasePoll p = pollmanager.makePoll(testV1msg[i]);
      assertNotNull(p);
      assertNotNull(p.getMessage());
      log.debug("initTestPolls: V1 " + i + " returns " + p);
      assertTrue(p instanceof V1Poll);
      switch (i) {
        case 0:
          assertTrue(p instanceof V1NamePoll);
          break;
        case 1:
          assertTrue(p instanceof V1ContentPoll);
          break;
        case 2:
          assertTrue(p instanceof V1VerifyPoll);
          break;
      }
      testV1polls[i] = (V1Poll) p;
      assertNotNull(testV1polls[i]);
      log.debug3("initTestPolls: " + i + " " + p.toString());
    }
  }

  static class LocalPollManager extends PollManager {
    // ignore message sends
    public void sendMessage(V1LcapMessage msg, ArchivalUnit au) throws IOException {}
  }

  /**
   * Executes the test case
   *
   * @param argv array of Strings containing command line arguments
   */
  public static void main(String[] argv) {
    String[] testCaseList = {TestPoll.class.getName()};
    junit.swingui.TestRunner.main(testCaseList);
  }
}
/** Minimal fully functional plugin capable of serving a little static content. */
public class StaticContentPlugin extends BasePlugin implements PluginTestable {
  static Logger log = Logger.getLogger("StaticContentPlugin");

  Map cuMap = new HashMap();

  public StaticContentPlugin() {}

  public String getVersion() {
    throw new UnsupportedOperationException("Not implemented");
  }

  public String getPluginName() {
    return "Static Content";
  }

  public List getSupportedTitles() {
    throw new UnsupportedOperationException("Not implemented");
  }

  public List getLocalAuConfigDescrs() {
    return Collections.EMPTY_LIST;
    //    throw new UnsupportedOperationException("Not implemented");
  }

  protected ArchivalUnit createAu0(Configuration auConfig)
      throws ArchivalUnit.ConfigurationException {
    return new SAU(this);
  }

  public void registerArchivalUnit(ArchivalUnit au) {
    aus.add(au);
  }

  public void unregisterArchivalUnit(ArchivalUnit au) {
    aus.remove(au);
  }

  public class SAU extends BaseArchivalUnit {

    protected SAU(Plugin myPlugin) {
      super(myPlugin);
    }

    protected String makeName() {
      return "Static Content AU";
    }

    protected String makeStartUrl() {
      throw new UnsupportedOperationException("Not Implemented");
    }

    public CachedUrlSet makeCachedUrlSet(CachedUrlSetSpec cuss) {
      return new SCUS(this, cuss);
    }

    public CachedUrl makeCachedUrl(String url) {
      CachedUrl res = (CachedUrl) cuMap.get(url);
      log.debug("makeCachedUrl(" + url + ") = " + res);
      return (CachedUrl) cuMap.get(url);
    }

    public org.lockss.plugin.UrlCacher makeUrlCacher(String url) {
      throw new UnsupportedOperationException("Not implemented");
    }

    public boolean shouldBeCached(String url) {
      return cuMap.containsKey(url);
    }

    public List getNewContentCrawlUrls() {
      throw new UnsupportedOperationException("Not implemented");
    }

    public Collection getUrlStems() {
      throw new UnsupportedOperationException("Not implemented");
    }

    public CachedUrlSet cachedUrlSetFactory(ArchivalUnit owner, CachedUrlSetSpec cuss) {
      throw new UnsupportedOperationException("Not implemented");
    }

    public CachedUrl cachedUrlFactory(CachedUrlSet owner, String url) {
      throw new UnsupportedOperationException("Not implemented");
    }

    public UrlCacher urlCacherFactory(CachedUrlSet owner, String url) {
      throw new UnsupportedOperationException("Not implemented");
    }

    public String getManifestPage() {
      throw new UnsupportedOperationException("Not Implemented");
    }

    public FilterRule getFilterRule(String mimeType) {
      throw new UnsupportedOperationException("Not implemented");
    }

    /**
     * Create a CU with content and store it in AU
     *
     * @param owner the CUS owner
     * @param url the url
     * @param type the type
     * @param contents the contents
     */
    public void storeCachedUrl(CachedUrlSet owner, String url, String type, String contents) {
      SCU scu = new SCU(owner, url, type, contents);
      cuMap.put(scu.getUrl(), scu);
    }

    public void storeCachedUrl(String url, String type, String contents) {
      storeCachedUrl(null, url, type, contents);
    }

    public String toString() {
      return "[sau: " + cuMap + "]";
    }

    protected CrawlRule makeRules() {
      throw new UnsupportedOperationException("Not implemented");
    }

    /**
     * loadDefiningConfig
     *
     * @param config Configuration
     */
    protected void loadAuConfigDescrs(Configuration config) {}
  }

  public class SCU extends BaseCachedUrl {
    private String contents = null;
    private CIProperties props = new CIProperties();

    public SCU(CachedUrlSet owner, String url) {
      super(null, url);
    }

    /**
     * Create a CachedUrl with content
     *
     * @param owner the CUS owner
     * @param url the url
     * @param type the type
     * @param contents the contents
     */
    public SCU(CachedUrlSet owner, String url, String type, String contents) {
      this(owner, url);
      setContents(contents);
      props.setProperty(CachedUrl.PROPERTY_CONTENT_TYPE, type);
    }

    private void setContents(String s) {
      contents = s;
      props.setProperty("Content-Length", "" + s.length());
    }

    public String getUrl() {
      return url;
    }

    public boolean hasContent() {
      return contents != null;
    }

    public boolean isLeaf() {
      throw new UnsupportedOperationException("Not implemented");
    }

    public InputStream getUnfilteredInputStream() {
      return new StringInputStream(contents);
    }

    public InputStream openForHashing() {
      return getUnfilteredInputStream();
    }

    protected InputStream getFilteredStream() {
      throw new UnsupportedOperationException("Not implemented");
    }

    public Reader openForReading() {
      throw new UnsupportedOperationException("Not implemented");
    }

    public long getContentSize() {
      return contents == null ? 0 : contents.length();
    }

    public CIProperties getProperties() {
      return props;
    }
  }

  class SCUS extends BaseCachedUrlSet {
    public SCUS(ArchivalUnit owner, CachedUrlSetSpec spec) {
      super(owner, spec);
    }

    public void storeActualHashDuration(long elapsed, Exception err) {
      throw new UnsupportedOperationException("Not implemented");
    }

    public Iterator flatSetIterator() {
      throw new UnsupportedOperationException("Not implemented");
    }

    public Iterator contentHashIterator() {
      throw new UnsupportedOperationException("Not implemented");
    }

    public boolean isLeaf() {
      throw new UnsupportedOperationException("Not implemented");
    }

    public CachedUrlSetHasher getContentHasher(MessageDigest digest) {
      throw new UnsupportedOperationException("Not implemented");
    }

    public CachedUrlSetHasher getNameHasher(MessageDigest digest) {
      throw new UnsupportedOperationException("Not implemented");
    }

    public long estimatedHashDuration() {
      return 1000;
    }
  }
}
/** Central repository of loaded keystores */
public class LockssKeyStoreManager extends BaseLockssDaemonManager implements ConfigurableManager {

  protected static Logger log = Logger.getLogger("LockssKeyStoreManager");

  static final String PREFIX = Configuration.PREFIX + "keyMgr.";

  /** Default type for newly created keystores. */
  public static final String PARAM_DEFAULT_KEYSTORE_TYPE = PREFIX + "defaultKeyStoreType";

  public static final String DEFAULT_DEFAULT_KEYSTORE_TYPE = "JCEKS";

  /** Default keystore provider. */
  public static final String PARAM_DEFAULT_KEYSTORE_PROVIDER = PREFIX + "defaultKeyStoreProvider";

  public static final String DEFAULT_DEFAULT_KEYSTORE_PROVIDER = null;

  /**
   * Root of keystore definitions. For each keystore, pick a unique identifier and use it in place
   * of &lt;id&gt; in the following
   */
  public static final String PARAM_KEYSTORE = PREFIX + "keystore";

  /** If true the daemon will exit if a critical keystore is missing. */
  public static final String PARAM_EXIT_IF_MISSING_KEYSTORE = PREFIX + "exitIfMissingKeystore";

  public static final boolean DEFAULT_EXIT_IF_MISSING_KEYSTORE = true;

  /** keystore name, used by clients to refer to it */
  public static final String KEYSTORE_PARAM_NAME = "name";
  /** keystore file. Only one of file, resource or url should be set */
  public static final String KEYSTORE_PARAM_FILE = "file";
  /** keystore resource. Only one of file, resource or url should be set */
  public static final String KEYSTORE_PARAM_RESOURCE = "resource";
  /** keystore url. Only one of file, resource or url should be set */
  public static final String KEYSTORE_PARAM_URL = "url";
  /** keystore type */
  public static final String KEYSTORE_PARAM_TYPE = "type";
  /** keystore provider */
  public static final String KEYSTORE_PARAM_PROVIDER = "provider";
  /** keystore password */
  public static final String KEYSTORE_PARAM_PASSWORD = "******";
  /** private key password */
  public static final String KEYSTORE_PARAM_KEY_PASSWORD = "******";
  /** private key password file */
  public static final String KEYSTORE_PARAM_KEY_PASSWORD_FILE = "keyPasswordFile";
  /**
   * If true, and the keystore doesn't exist, a keystore with a self-signed certificate will be be
   * created.
   */
  public static final String KEYSTORE_PARAM_CREATE = "create";

  protected String defaultKeyStoreType = DEFAULT_DEFAULT_KEYSTORE_TYPE;
  protected String defaultKeyStoreProvider = DEFAULT_DEFAULT_KEYSTORE_PROVIDER;
  protected boolean paramExitIfMissingKeyStore = DEFAULT_EXIT_IF_MISSING_KEYSTORE;

  // Pseudo params for param doc
  public static final String DOC_PREFIX = PARAM_KEYSTORE + ".<id>.";

  /** Name by which daemon component(s) refer to this keystore */
  public static final String PARAM_KEYSTORE_NAME = DOC_PREFIX + KEYSTORE_PARAM_NAME;
  /** Keystore filename. Only one of file, resource or url should be set */
  public static final String PARAM_KEYSTORE_FILE = DOC_PREFIX + KEYSTORE_PARAM_FILE;
  /** Keystore resource. Only one of file, resource or url should be set */
  public static final String PARAM_KEYSTORE_RESOURCE = DOC_PREFIX + KEYSTORE_PARAM_RESOURCE;
  /** Keystore url. Only one of file, resource or url should be set */
  public static final String PARAM_KEYSTORE_URL = DOC_PREFIX + KEYSTORE_PARAM_URL;
  /** Keystore type (JKS, JCEKS, etc.) */
  public static final String PARAM_KEYSTORE_TYPE = DOC_PREFIX + KEYSTORE_PARAM_TYPE;
  /** Keystore provider (SunJCE, etc.) */
  public static final String PARAM_KEYSTORE_PROVIDER = DOC_PREFIX + KEYSTORE_PARAM_PROVIDER;
  /** Keystore password. Default is machine's fqdn */
  public static final String PARAM_KEYSTORE_PASSWORD = DOC_PREFIX + KEYSTORE_PARAM_PASSWORD;
  /** Private key password */
  public static final String PARAM_KEYSTORE_KEY_PASSWORD = DOC_PREFIX + KEYSTORE_PARAM_KEY_PASSWORD;
  /** private key password file */
  public static final String PARAM_KEYSTORE_KEY_PASSWORD_FILE =
      DOC_PREFIX + KEYSTORE_PARAM_KEY_PASSWORD_FILE;
  /**
   * If true, and the keystore doesn't exist, a keystore with a self-signed certificate will be be
   * created.
   */
  public static final String PARAM_KEYSTORE_CREATE = DOC_PREFIX + KEYSTORE_PARAM_CREATE;

  public static boolean DEFAULT_CREATE = false;

  protected Map<String, LockssKeyStore> keystoreMap = new HashMap<String, LockssKeyStore>();

  public void startService() {
    super.startService();
    loadKeyStores();
  }

  public synchronized void setConfig(
      Configuration config, Configuration prevConfig, Configuration.Differences changedKeys) {
    if (changedKeys.contains(PREFIX)) {
      defaultKeyStoreType = config.get(PARAM_DEFAULT_KEYSTORE_TYPE, DEFAULT_DEFAULT_KEYSTORE_TYPE);
      defaultKeyStoreProvider =
          config.get(PARAM_DEFAULT_KEYSTORE_PROVIDER, DEFAULT_DEFAULT_KEYSTORE_PROVIDER);
      paramExitIfMissingKeyStore =
          config.getBoolean(PARAM_EXIT_IF_MISSING_KEYSTORE, DEFAULT_EXIT_IF_MISSING_KEYSTORE);

      if (changedKeys.contains(PARAM_KEYSTORE)) {
        configureKeyStores(config);
        // defer initial set of keystore loading until startService
        if (isInited()) {
          // load any newly added keystores
          loadKeyStores();
        }
      }
    }
  }

  /**
   * Return the named LockssKeyStore or null
   *
   * @param name the keystore name
   */
  public LockssKeyStore getLockssKeyStore(String name) {
    return getLockssKeyStore(name, null);
  }

  /**
   * Return the named LockssKeyStore
   *
   * @param name the keystore name
   * @param criticalServiceName if non-null, this is a criticial keystore whose unavailability
   *     should cause the daemon to exit (if org.lockss.keyMgr.exitIfMissingKeystore is true)
   */
  public LockssKeyStore getLockssKeyStore(String name, String criticalServiceName) {
    LockssKeyStore res = keystoreMap.get(name);
    checkFact(res, name, criticalServiceName, null);
    return res;
  }

  /**
   * Convenience method to return the KeyManagerFactory from the named LockssKeyStore, or null
   *
   * @param name the keystore name
   */
  public KeyManagerFactory getKeyManagerFactory(String name) {
    return getKeyManagerFactory(name, null);
  }

  /**
   * Convenience method to return the KeyManagerFactory from the named LockssKeyStore.
   *
   * @param name the keystore name
   * @param criticalServiceName if non-null, this is a criticial keystore whose unavailability
   *     should cause the daemon to exit (if org.lockss.keyMgr.exitIfMissingKeystore is true)
   */
  public KeyManagerFactory getKeyManagerFactory(String name, String criticalServiceName) {
    LockssKeyStore lk = getLockssKeyStore(name, criticalServiceName);
    if (lk != null) {
      KeyManagerFactory fact = lk.getKeyManagerFactory();
      checkFact(fact, name, criticalServiceName, "found but contains no private keys");

      return fact;
    }
    return null;
  }

  /** Convenience method to return the TrustManagerFactory from the named LockssKeyStore, or null */
  public TrustManagerFactory getTrustManagerFactory(String name) {
    return getTrustManagerFactory(name, null);
  }

  /**
   * Convenience method to return the TrustManagerFactory from the named LockssKeyStore.
   *
   * @param name the keystore name
   * @param criticalServiceName if non-null, this is a criticial keystore whose unavailability
   *     should cause the daemon to exit (if org.lockss.keyMgr.exitIfMissingKeystore is true)
   */
  public TrustManagerFactory getTrustManagerFactory(String name, String criticalServiceName) {
    LockssKeyStore lk = getLockssKeyStore(name, criticalServiceName);
    if (lk != null) {
      TrustManagerFactory fact = lk.getTrustManagerFactory();
      checkFact(fact, name, criticalServiceName, "found but contains no trusted certificates");
      return fact;
    }
    return null;
  }

  private void checkFact(Object fact, String name, String criticalServiceName, String message) {
    if (fact == null && criticalServiceName != null) {
      String msg =
          StringUtil.isNullString(name)
              ? ("No keystore name given for critical keystore"
                  + " needed for service "
                  + criticalServiceName
                  + ", daemon exiting")
              : ("Critical keystore "
                  + name
                  + " "
                  + ((message != null) ? message : "not found or not loadable")
                  + " for service "
                  + criticalServiceName
                  + ", daemon exiting");
      log.critical(msg);

      if (paramExitIfMissingKeyStore) {
        System.exit(Constants.EXIT_CODE_KEYSTORE_MISSING);
      } else {
        throw new IllegalArgumentException(msg);
      }
    }
  }

  /** Create LockssKeystores from config subtree below {@link #PARAM_KEYSTORE} */
  void configureKeyStores(Configuration config) {
    Configuration allKs = config.getConfigTree(PARAM_KEYSTORE);
    for (Iterator iter = allKs.nodeIterator(); iter.hasNext(); ) {
      String id = (String) iter.next();
      Configuration oneKs = allKs.getConfigTree(id);
      try {
        LockssKeyStore lk = createLockssKeyStore(oneKs);
        String name = lk.getName();
        if (name == null) {
          log.error("KeyStore definition missing name: " + oneKs);
          continue;
        }
        LockssKeyStore old = keystoreMap.get(name);
        if (old != null && !lk.equals(old)) {
          log.warning(
              "Keystore "
                  + name
                  + " redefined.  "
                  + "New definition may not take effect until daemon restart");
        }

        log.debug("Adding keystore " + name);
        keystoreMap.put(name, lk);

      } catch (Exception e) {
        log.error("Couldn't create keystore: " + oneKs, e);
      }
    }
  }

  /** Create LockssKeystore from a config subtree */
  LockssKeyStore createLockssKeyStore(Configuration config) {
    log.debug2("Creating LockssKeyStore from config: " + config);
    String name = config.get(KEYSTORE_PARAM_NAME);
    LockssKeyStore lk = new LockssKeyStore(name);

    String file = config.get(KEYSTORE_PARAM_FILE);
    String resource = config.get(KEYSTORE_PARAM_RESOURCE);
    String url = config.get(KEYSTORE_PARAM_URL);

    if (!StringUtil.isNullString(file)) {
      lk.setLocation(file, LocationType.File);
    } else if (!StringUtil.isNullString(resource)) {
      lk.setLocation(resource, LocationType.Resource);
    } else if (!StringUtil.isNullString(url)) {
      lk.setLocation(url, LocationType.Url);
    }

    lk.setType(config.get(KEYSTORE_PARAM_TYPE, defaultKeyStoreType));
    lk.setProvider(config.get(KEYSTORE_PARAM_PROVIDER, defaultKeyStoreProvider));
    lk.setPassword(config.get(KEYSTORE_PARAM_PASSWORD));
    lk.setKeyPassword(config.get(KEYSTORE_PARAM_KEY_PASSWORD));
    lk.setKeyPasswordFile(config.get(KEYSTORE_PARAM_KEY_PASSWORD_FILE));
    lk.setMayCreate(config.getBoolean(KEYSTORE_PARAM_CREATE, DEFAULT_CREATE));
    return lk;
  }

  void loadKeyStores() {
    List<LockssKeyStore> lst = new ArrayList<LockssKeyStore>(keystoreMap.values());
    for (LockssKeyStore lk : lst) {
      try {
        lk.load();
      } catch (Exception e) {
        log.error("Can't load keystore " + lk.getName(), e);
        keystoreMap.remove(lk.getName());
      }
    }
  }
}
// SingleThreadModel causes servlet instances to be assigned to only a
// single thread (request) at a time.
public abstract class LockssServlet extends HttpServlet implements SingleThreadModel {
  protected static Logger log = Logger.getLogger("LockssServlet");

  // Constants
  static final String PARAM_LOCAL_IP = Configuration.PREFIX + "localIPAddress";

  static final String PARAM_PLATFORM_VERSION = Configuration.PREFIX + "platform.version";

  /** Inactive HTTP session (cookie) timeout */
  static final String PARAM_UI_SESSION_TIMEOUT = Configuration.PREFIX + "ui.sessionTimeout";

  static final long DEFAULT_UI_SESSION_TIMEOUT = 2 * Constants.DAY;

  /** Maximum size of uploaded file accepted */
  static final String PARAM_MAX_UPLOAD_FILE_SIZE = Configuration.PREFIX + "ui.maxUploadFileSize";

  static final int DEFAULT_MAX_UPLOAD_FILE_SIZE = 500000;

  /** The warning string to display when the UI is disabled. */
  static final String PARAM_UI_WARNING = Configuration.PREFIX + "ui.warning";

  // session keys
  static final String SESSION_KEY_OBJECT_ID = "obj_id";
  static final String SESSION_KEY_OBJ_MAP = "obj_map";
  public static final String SESSION_KEY_RUNNING_SERVLET = "running_servlet";
  public static final String SESSION_KEY_REQUEST_HOST = "request_host";

  // Name given to form element whose value is the action that should be
  // performed when the form is submitted.  (Not always the submit button.)
  public static final String ACTION_TAG = "lockssAction";

  public static final String JAVASCRIPT_RESOURCE = "org/lockss/htdocs/admin.js";

  public static final String ATTR_INCLUDE_SCRIPT = "IncludeScript";
  public static final String ATTR_ALLOW_ROLES = "AllowRoles";

  /** User may configure admin access (add/delete/modify users, set admin access list) */
  public static final String ROLE_USER_ADMIN = "userAdminRole";

  /** User may configure content access (set content access list) */
  public static final String ROLE_CONTENT_ADMIN = "contentAdminRole";

  /** User may change AU configuration (add/delete content) */
  public static final String ROLE_AU_ADMIN = "auAdminRole";

  public static final String ROLE_DEBUG = "debugRole";

  protected ServletContext context;

  private LockssApp theApp = null;
  private ServletManager servletMgr;
  private AccountManager acctMgr;

  // Request-local storage.  Convenient, but requires servlet instances
  // to be single threaded, and must ensure reset them to avoid carrying
  // over state between requests.
  protected HttpServletRequest req;
  protected HttpServletResponse resp;
  protected URL reqURL;
  protected HttpSession session;
  private String adminDir = null;
  protected String clientAddr; // client addr, even if no param
  protected String localAddr;
  protected MultiPartRequest multiReq;

  private Vector footnotes;
  private int footNumber;
  private int tabindex;
  ServletDescr _myServletDescr = null;
  private String myName = null;

  // number submit buttons sequentially so unit tests can find them
  protected int submitButtonNumber = 0;

  /** Run once when servlet loaded. */
  public void init(ServletConfig config) throws ServletException {
    super.init(config);
    context = config.getServletContext();
    theApp = (LockssApp) context.getAttribute(ServletManager.CONTEXT_ATTR_LOCKSS_APP);
    servletMgr = (ServletManager) context.getAttribute(ServletManager.CONTEXT_ATTR_SERVLET_MGR);
    if (theApp instanceof LockssDaemon) {
      acctMgr = getLockssDaemon().getAccountManager();
    }
  }

  public ServletManager getServletManager() {
    return servletMgr;
  }

  protected ServletDescr[] getServletDescrs() {
    return servletMgr.getServletDescrs();
  }

  /** Servlets must implement this method. */
  protected abstract void lockssHandleRequest() throws ServletException, IOException;

  /** Common request handling. */
  public void service(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    resetState();
    boolean success = false;
    HttpSession session = req.getSession(false);
    try {
      this.req = req;
      this.resp = resp;
      if (log.isDebug()) {
        logParams();
      }
      resp.setContentType("text/html");

      if (!mayPageBeCached()) {
        resp.setHeader("pragma", "no-cache");
        resp.setHeader("Cache-control", "no-cache");
      }

      reqURL = new URL(UrlUtil.getRequestURL(req));
      clientAddr = getLocalIPAddr();

      // check that current user has permission to run this servlet
      if (!isServletAllowed(myServletDescr())) {
        displayWarningInLieuOfPage("You are not authorized to use " + myServletDescr().heading);
        return;
      }

      // check whether servlet is disabled
      String reason = ServletUtil.servletDisabledReason(myServletDescr().getServletName());
      if (reason != null) {
        displayWarningInLieuOfPage("This function is disabled. " + reason);
        return;
      }
      if (session != null) {
        session.setAttribute(SESSION_KEY_RUNNING_SERVLET, getHeading());
        String reqHost = req.getRemoteHost();
        String forw = req.getHeader(HttpFields.__XForwardedFor);
        if (!StringUtil.isNullString(forw)) {
          reqHost += " (proxies for " + forw + ")";
        }
        session.setAttribute(SESSION_KEY_REQUEST_HOST, reqHost);
      }
      lockssHandleRequest();
      success = (errMsg == null);
    } catch (ServletException e) {
      log.error("Servlet threw", e);
      throw e;
    } catch (IOException e) {
      log.error("Servlet threw", e);
      throw e;
    } catch (RuntimeException e) {
      log.error("Servlet threw", e);
      throw e;
    } finally {
      if (session != null) {
        session.setAttribute(SESSION_KEY_RUNNING_SERVLET, null);
        session.setAttribute(LockssFormAuthenticator.__J_AUTH_ACTIVITY, TimeBase.nowMs());
      }
      if ("please".equalsIgnoreCase(req.getHeader("X-Lockss-Result"))) {
        log.debug3("X-Lockss-Result: " + (success ? "Ok" : "Fail"));
        resp.setHeader("X-Lockss-Result", success ? "Ok" : "Fail");
      }
      resetMyLocals();
      resetLocals();
    }
  }

  protected void resetState() {
    multiReq = null;
    footNumber = 0;
    submitButtonNumber = 0;
    tabindex = 1;
    statusMsg = null;
    errMsg = null;
    isFramed = false;
  }

  protected void resetLocals() {}

  protected void resetMyLocals() {
    // Don't hold on to stuff forever
    req = null;
    resp = null;
    session = null;
    reqURL = null;
    adminDir = null;
    localAddr = null;
    footnotes = null;
    _myServletDescr = null;
    myName = null;
    multiReq = null;
  }

  /**
   * Return true if generated page may be cached (e.g., by browser). Default is false as most
   * servlets generate dynamic results
   */
  protected boolean mayPageBeCached() {
    return false;
  }

  /** Set the session timeout to the configured value */
  protected void setSessionTimeout(HttpSession session) {
    Configuration config = CurrentConfig.getCurrentConfig();
    setSessionTimeout(
        session, config.getTimeInterval(PARAM_UI_SESSION_TIMEOUT, DEFAULT_UI_SESSION_TIMEOUT));
  }

  /** Set the session timeout */
  protected void setSessionTimeout(HttpSession session, long time) {
    session.setMaxInactiveInterval((int) (time / Constants.SECOND));
  }

  /** Get the current session, creating it if necessary (and set the timeout if so) */
  protected HttpSession getSession() {
    if (session == null) {
      session = req.getSession(true);
      if (session.isNew()) {
        setSessionTimeout(session);
      }
    }
    return session;
  }

  /** Return true iff a session has already been established */
  protected boolean hasSession() {
    return req.getSession(false) != null;
  }

  /** Get an unused ID string for storing an object in the session */
  protected String getNewSessionObjectId() {
    HttpSession session = getSession();
    synchronized (session) {
      Integer id = (Integer) getSession().getAttribute(SESSION_KEY_OBJECT_ID);
      if (id == null) {
        id = new Integer(1);
      }
      session.setAttribute(SESSION_KEY_OBJECT_ID, new Integer(id.intValue() + 1));
      return id.toString();
    }
  }

  /** Get the object associated with the ID in the session */
  protected Object getSessionIdObject(String id) {
    HttpSession session = getSession();
    synchronized (session) {
      BidiMap map = (BidiMap) session.getAttribute(SESSION_KEY_OBJ_MAP);
      if (map == null) {
        return null;
      }
      return map.getKey(id);
    }
  }

  /** Get the String associated with the ID in the session */
  protected String getSessionIdString(String id) {
    return (String) getSessionIdObject(id);
  }

  /** Get the ID with which the object is associated with the session, if any */
  protected String getSessionObjectId(Object obj) {
    HttpSession session = getSession();
    BidiMap map;
    synchronized (session) {
      map = (BidiMap) session.getAttribute(SESSION_KEY_OBJ_MAP);
      if (map == null) {
        map = new DualHashBidiMap();
        session.setAttribute(SESSION_KEY_OBJ_MAP, map);
      }
    }
    synchronized (map) {
      String id = (String) map.get(obj);
      if (id == null) {
        id = getNewSessionObjectId();
        map.put(obj, id);
      }
      return id;
    }
  }

  // Return descriptor of running servlet
  protected ServletDescr myServletDescr() {
    if (_myServletDescr == null) {
      _myServletDescr = servletMgr.findServletDescr(this);
    }
    return _myServletDescr;
  }

  // By default, servlet heading is in descr.  Override method to
  // compute other heading
  protected String getHeading(ServletDescr d) {
    if (d == null) return "Unknown Servlet";
    return d.heading;
  }

  protected String getHeading() {
    return getHeading(myServletDescr());
  }

  String getLocalIPAddr() {
    if (localAddr == null) {
      try {
        IPAddr localHost = IPAddr.getLocalHost();
        localAddr = localHost.getHostAddress();
      } catch (UnknownHostException e) {
        // shouldn't happen
        log.error("LockssServlet: getLocalHost: " + e.toString());
        return "???";
      }
    }
    return localAddr;
  }

  // Return IP addr used by LCAP.  If specified by (misleadingly named)
  // localIPAddress prop, might not really be our address (if we are
  // behind NAT).
  String getLcapIPAddr() {
    String ip = CurrentConfig.getParam(PARAM_LOCAL_IP);
    if (ip == null || ip.length() <= 0) {
      return getLocalIPAddr();
    }
    return ip;
  }

  String getRequestHost() {
    return reqURL.getHost();
  }

  String getMachineName() {
    return PlatformUtil.getLocalHostname();
  }

  //   String getMachineName0() {
  //     if (myName == null) {
  //       // Return the canonical name of the interface the request was aimed
  //       // at.  (localIPAddress prop isn't necessarily right here, as it
  //       // might be the address of a NAT that we're behind.)
  //       String host = reqURL.getHost();
  //       try {
  // 	IPAddr localHost = IPAddr.getByName(host);
  // 	String ip = localHost.getHostAddress();
  // 	myName = getMachineName(ip);
  //       } catch (UnknownHostException e) {
  // 	// shouldn't happen
  // 	log.error("getMachineName", e);
  // 	return host;
  //       }
  //     }
  //     return myName;
  //   }

  //   String getMachineName(String ip) {
  //     try {
  //       IPAddr inet = IPAddr.getByName(ip);
  //       return inet.getHostName();
  //     } catch (UnknownHostException e) {
  //       log.warning("getMachineName", e);
  //     }
  //     return ip;
  //   }

  // return IP given name or IP
  String getMachineIP(String name) {
    try {
      IPAddr inet = IPAddr.getByName(name);
      return inet.getHostAddress();
    } catch (UnknownHostException e) {
      return null;
    }
  }

  boolean isServletLinkInNav(ServletDescr d) {
    return !isThisServlet(d) || linkMeInNav();
  }

  boolean isThisServlet(ServletDescr d) {
    return d == myServletDescr();
  }

  /** servlets may override this to determine whether they should be a link in nav table */
  protected boolean linkMeInNav() {
    return false;
  }

  boolean isLargeLogo() {
    return myServletDescr().isLargeLogo();
  }

  // user predicates
  String getUsername() {
    Principal user = req.getUserPrincipal();
    return user != null ? user.toString() : null;
  }

  protected UserAccount getUserAccount() {
    if (acctMgr != null) {
      return acctMgr.getUser(getUsername());
    }
    return AccountManager.NOBODY_ACCOUNT;
  }

  protected boolean isDebugUser() {
    return doesUserHaveRole(ROLE_DEBUG);
  }

  protected boolean doesUserHaveRole(String role) {
    if ((req.isUserInRole(role) || req.isUserInRole(ROLE_USER_ADMIN)) && !hasNoRoleParsm(role)) {
      return true;
    }
    return hasTestRole(role);
  }

  static Map<String, String> noRoleParams = new HashMap<String, String>();

  static {
    noRoleParams.put(ROLE_USER_ADMIN, "noadmin");
    noRoleParams.put(ROLE_CONTENT_ADMIN, "nocontent");
    noRoleParams.put(ROLE_AU_ADMIN, "noau");
    noRoleParams.put(ROLE_DEBUG, "nodebug");
  }

  protected boolean hasNoRoleParsm(String roleName) {
    String noRoleParam = noRoleParams.get(roleName);
    return (noRoleParam != null && !StringUtil.isNullString(req.getParameter(noRoleParam)));
  }

  protected boolean hasTestRole(String role) {
    // Servlet test harness puts roles in context
    List roles = (List) context.getAttribute(ATTR_ALLOW_ROLES);
    return roles != null && (roles.contains(role) || roles.contains(ROLE_USER_ADMIN));
  }

  protected boolean isServletAllowed(ServletDescr d) {
    if (d.needsUserAdminRole() && !doesUserHaveRole(ROLE_USER_ADMIN)) return false;
    if (d.needsContentAdminRole() && !doesUserHaveRole(ROLE_CONTENT_ADMIN)) return false;
    if (d.needsAuAdminRole() && !doesUserHaveRole(ROLE_AU_ADMIN)) return false;

    return d.isEnabled(getLockssDaemon());
  }

  protected boolean isServletDisplayed(ServletDescr d) {
    if (!isServletAllowed(d)) return false;
    if (d.needsDebugRole() && !doesUserHaveRole(ROLE_DEBUG)) return false;
    return true;
  }

  protected boolean isServletInNav(ServletDescr d) {
    if (d.cls == ServletDescr.UNAVAILABLE_SERVLET_MARKER) return false;
    return d.isInNav(this) && isServletDisplayed(d);
  }

  // Called when a servlet doesn't get the parameters it expects/needs
  protected void paramError() throws IOException {
    // FIXME: As of 2006-03-15 this method and its only caller checkParam() are not called from
    // anywhere
    PrintWriter wrtr = resp.getWriter();
    Page page = new Page();
    // add referer, params, msg to contact lockss unless from old bookmark
    // or manually entered url
    page.add("Parameter error");
    page.write(wrtr);
  }

  // return true iff error
  protected boolean checkParam(boolean ok, String msg) throws IOException {
    if (ok) return false;
    log.error(myServletDescr().getPath() + ": " + msg);
    paramError();
    return true;
  }

  /** Construct servlet URL */
  String srvURL(ServletDescr d) {
    return srvURL((String) null, d, null);
  }

  /** Construct servlet URL with params */
  String srvURL(ServletDescr d, String params) {
    return srvURL((String) null, d, params);
  }

  /** Construct servlet URL with params */
  String srvURL(ServletDescr d, Properties params) {
    return srvURL(d, concatParams(params));
  }

  /** Construct servlet absolute URL, with params as necessary. */
  String srvAbsURL(ServletDescr d, String params) {
    return srvURL(getRequestHost(), d, params);
  }

  /**
   * Construct servlet URL, with params as necessary. Avoid generating a hostname different from
   * that used in the original request, or browsers will prompt again for login
   */
  String srvURL(String host, ServletDescr d, String params) {
    return srvURLFromStem(srvUrlStem(host), d, params);
  }

  String srvURL(PeerIdentity peer, ServletDescr d, String params) {
    return srvURLFromStem(peer.getUiUrlStem(reqURL.getPort()), d, params);
  }

  /**
   * Construct servlet URL, with params as necessary. Avoid generating a hostname different from
   * that used in the original request, or browsers will prompt again for login
   */
  String srvURLFromStem(String stem, ServletDescr d, String params) {
    if (d.isPathIsUrl()) {
      return d.getPath();
    }
    StringBuilder sb = new StringBuilder(80);
    if (stem != null) {
      sb.append(stem);
      if (stem.charAt(stem.length() - 1) != '/') {
        sb.append('/');
      }
    } else {
      // ensure absolute path even if no scheme/host/port
      sb.append('/');
    }
    sb.append(d.getPath());
    if (params != null) {
      sb.append('?');
      sb.append(params);
    }
    return sb.toString();
  }

  String srvUrlStem(String host) {
    if (host == null) {
      return null;
    }
    StringBuilder sb = new StringBuilder();
    sb.append(reqURL.getProtocol());
    sb.append("://");
    sb.append(host);
    sb.append(':');
    sb.append(reqURL.getPort());
    return sb.toString();
  }

  /** Return a link to a servlet */
  String srvLink(ServletDescr d, String text) {
    return srvLink(d, text, (String) null);
  }

  /** Return a link to a servlet with params */
  String srvLink(ServletDescr d, String text, String params) {
    return new Link(srvURL(d, params), (text != null ? text : d.heading)).toString();
  }

  /** Return a link to a servlet with params */
  String srvLink(ServletDescr d, String text, Properties params) {
    return new Link(srvURL(d, params), text).toString();
  }

  /** Return an absolute link to a servlet with params */
  String srvAbsLink(ServletDescr d, String text, Properties params) {
    return srvAbsLink(d, text, concatParams(params));
  }

  /** Return an absolute link to a servlet with params */
  String srvAbsLink(ServletDescr d, String text, String params) {
    return new Link(srvAbsURL(d, params), (text != null ? text : d.heading)).toString();
  }

  /** Return an absolute link to a servlet with params */
  String srvAbsLink(String host, ServletDescr d, String text, String params) {
    return new Link(srvURL(host, d, params), (text != null ? text : d.heading)).toString();
  }

  /** Return an absolute link to a servlet with params */
  String srvAbsLink(PeerIdentity peer, ServletDescr d, String text, String params) {
    return new Link(srvURL(peer, d, params), (text != null ? text : d.heading)).toString();
  }

  /** Return text as a link iff isLink */
  String conditionalSrvLink(ServletDescr d, String text, String params, boolean isLink) {
    if (isLink) {
      return srvLink(d, text, params);
    } else {
      return text;
    }
  }

  /** Return text as a link iff isLink */
  String conditionalSrvLink(ServletDescr d, String text, boolean isLink) {
    return conditionalSrvLink(d, text, null, isLink);
  }

  /** Concatenate params for URL string */
  static String concatParams(String p1, String p2) {
    if (StringUtil.isNullString(p1)) {
      return p2;
    }
    if (StringUtil.isNullString(p2)) {
      return p1;
    }
    return p1 + "&" + p2;
  }

  /** Concatenate params for URL string */
  String concatParams(Properties props) {
    if (props == null) {
      return null;
    }
    java.util.List list = new ArrayList();
    for (Iterator iter = props.keySet().iterator(); iter.hasNext(); ) {
      String key = (String) iter.next();
      String val = props.getProperty(key);
      if (!StringUtil.isNullString(val)) {
        list.add(key + "=" + urlEncode(val));
      }
    }
    return StringUtil.separatedString(list, "&");
  }

  String modifyParams(String key, String val) {
    Properties props = getParamsAsProps();
    props.setProperty(key, val);
    return concatParams(props);
  }

  /**
   * Return the request parameters as a Properties. Only the first value of multivalued parameters
   * is included.
   */
  Properties getParamsAsProps() {
    Properties props = new Properties();
    for (Enumeration en = req.getParameterNames(); en.hasMoreElements(); ) {
      String name = (String) en.nextElement();
      props.setProperty(name, req.getParameter(name));
    }
    return props;
  }

  /**
   * Return the request parameters as a Map<String,String>. Only the first value of multivalued
   * parameters is included.
   */
  Map<String, String> getParamsAsMap() {
    Map<String, String> map = new HashMap<String, String>();
    for (Enumeration en = req.getParameterNames(); en.hasMoreElements(); ) {
      String name = (String) en.nextElement();
      map.put(name, req.getParameter(name));
    }
    return map;
  }

  protected String urlEncode(String param) {
    return UrlUtil.encodeUrl(param);
  }

  protected String getRequestKey() {
    String key = req.getPathInfo();
    if (key != null && key.startsWith("/")) {
      return key.substring(1);
    }
    return key;
  }

  /** Common page setup. */
  protected Page newPage() {
    // Compute heading
    String heading = getHeading();
    if (heading == null) {
      heading = "Box Administration";
    }

    // Create page and layout header
    Page page = ServletUtil.doNewPage(getPageTitle(), isFramed());
    Iterator inNavIterator;
    if (myServletDescr().hasNoNavTable()) {
      inNavIterator = CollectionUtil.EMPTY_ITERATOR;
    } else {
      inNavIterator =
          new FilterIterator(
              new ObjectArrayIterator(getServletDescrs()),
              new Predicate() {
                public boolean evaluate(Object obj) {
                  return isServletInNav((ServletDescr) obj);
                }
              });
    }
    ServletUtil.layoutHeader(
        this,
        page,
        heading,
        isLargeLogo(),
        getMachineName(),
        getLockssApp().getStartDate(),
        inNavIterator);
    String warnMsg = CurrentConfig.getParam(PARAM_UI_WARNING);
    if (warnMsg != null) {
      Composite warning = new Composite();
      warning.add("<center><font color=red size=+1>");
      warning.add(warnMsg);
      warning.add("</font></center><br>");
      page.add(warning);
    }
    return page;
  }

  protected Page addBarePageHeading(Page page) {
    // FIXME: Move the following fragment elsewhere
    // It causes the doctype statement to appear in the middle,
    // after the <body> tag.
    page.add("<!doctype html public \"-//w3c//dtd html 4.0 transitional//en\">");
    page.addHeader("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\">");
    page.addHeader("<meta http-equiv=\"content-type\" content=\"text/html;charset=ISO-8859-1\">");
    page.addHeader("<link rel=\"shortcut icon\" href=\"/favicon.ico\" type=\"image/x-icon\" />");
    return page;
  }

  private boolean isFramed = false;

  protected String errMsg;

  protected String statusMsg;

  protected boolean isFramed() {
    return isFramed;
  }

  protected void setFramed(boolean v) {
    isFramed = v;
  }

  protected String getPageTitle() {
    String heading = getHeading();
    if (heading != null) {
      return "LOCKSS: " + heading;
    } else {
      return "LOCKSS";
    }
  }

  /** Return a button that invokes the javascript submit routine with the specified action */
  protected Element submitButton(String label, String action) {
    return submitButton(label, action, null, null);
  }

  /** Return a button that invokes javascript when clicked. */
  Input jsButton(String label, String js) {
    Input btn = new Input("button", null);
    btn.attribute("value", label);
    setTabOrder(btn);
    btn.attribute("onClick", js);
    return btn;
  }

  /**
   * Return a button that invokes the javascript submit routine with the specified action, first
   * storing the value in the specified form prop.
   */
  protected Element submitButton(String label, String action, String prop, String value) {
    StringBuilder sb = new StringBuilder(40);
    sb.append("lockssButton(this, '");
    sb.append(action);
    sb.append("'");
    if (prop != null && value != null) {
      sb.append(", '");
      sb.append(prop);
      sb.append("', '");
      sb.append(value);
      sb.append("'");
    }
    sb.append(")");
    Input btn = jsButton(label, sb.toString());
    btn.attribute("id", "lsb." + (++submitButtonNumber));
    return btn;
  }

  /**
   * Return a (possibly labelled) checkbox.
   *
   * @param label appears to right of checkbox if non null
   * @param value value included in result set if box checked
   * @param key form key to which result set is assigned
   * @param checked if true, box is initially checked
   * @return a checkbox Element
   */
  Element checkBox(String label, String value, String key, boolean checked) {
    Input in = new Input(Input.Checkbox, key, value);
    if (checked) {
      in.check();
    }
    setTabOrder(in);
    if (StringUtil.isNullString(label)) {
      return in;
    } else {
      Composite c = new Composite();
      c.add(in);
      c.add(" ");
      c.add(label);
      return c;
    }
  }

  /**
   * Return a labelled rasio button
   *
   * @param label label to right of circle, and form value if checked
   * @param key form key to which value is assigned
   * @param checked if true, is initially checked
   * @return a readio button Element
   */
  protected Element radioButton(String label, String key, boolean checked) {
    return radioButton(label, label, key, checked);
  }

  /**
   * Return a labelled rasio button
   *
   * @param label appears to right of circle if non null
   * @param value value assigned to key if box checked
   * @param key form key to which value is assigned
   * @param checked if true, is initially checked
   * @return a readio button Element
   */
  protected Element radioButton(String label, String value, String key, boolean checked) {
    Composite c = new Composite();
    Input in = new Input(Input.Radio, key, value);
    if (checked) {
      in.check();
    }
    setTabOrder(in);
    c.add(in);
    c.add(label);
    return c;
  }

  /** Add html tags to grey the text if isGrey is true */
  protected String greyText(String txt, boolean isGrey) {
    if (!isGrey) {
      return txt;
    }
    return "<font color=gray>" + txt + "</font>";
  }

  /**
   * Set this element next in the tab order. Returns the element for easier nesting in expressions.
   */
  protected Element setTabOrder(Element ele) {
    ele.attribute("tabindex", tabindex++);
    return ele;
  }

  /**
   * Store a footnote, assign it a number, return html for footnote reference. If footnote in null
   * or empty, no footnote is added and an empty string is returned. Footnote numbers get turned
   * into links; <b>Do not put the result of addFootnote inside a link!</b>.
   */
  protected String addFootnote(String s) {
    if (s == null || s.length() == 0) {
      return "";
    }
    if (footNumber == 0) {
      if (footnotes == null) {
        footnotes = new Vector(10, 10);
      } else {
        footnotes.removeAllElements();
      }
    }
    int n = footnotes.indexOf(s);
    if (n < 0) {
      n = footNumber++;
      footnotes.addElement(s);
    }
    return "<sup><font size=-1><a href=#foottag" + (n + 1) + ">" + (n + 1) + "</a></font></sup>";
  }

  /**
   * Add javascript to page. Normally adds a link to the script file, but can be told to include the
   * script directly in the page, to accomodate unit testing of individual servlets, when other
   * fetches won't work.
   */
  protected void addJavaScript(Composite comp) {
    String include = (String) context.getAttribute(ATTR_INCLUDE_SCRIPT);
    if (StringUtil.isNullString(include)) {
      linkToJavaScript(comp);
    } else {
      includeJavaScript0(comp);
    }
  }

  private void includeJavaScript0(Composite comp) {
    Script script = new Script(getJavascript());
    comp.add(script);
  }

  private void linkToJavaScript(Composite comp) {
    Script script = new Script("");
    script.attribute("src", "admin.js");
    comp.add(script);
  }

  private static String jstext = null;

  private static synchronized String getJavascript() {
    if (jstext == null) {
      InputStream istr = null;
      try {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        istr = loader.getResourceAsStream(JAVASCRIPT_RESOURCE);
        jstext = StringUtil.fromInputStream(istr);
        istr.close();
      } catch (Exception e) {
        log.error("Can't load javascript", e);
      } finally {
        IOUtil.safeClose(istr);
      }
    }
    return jstext;
  }

  /** Display a message in lieu of the normal page */
  protected void displayMsgInLieuOfPage(String msg) throws IOException {
    // TODO: Look at HTML
    Page page = newPage();
    Composite warning = new Composite();
    warning.add(msg);
    warning.add("<br>");
    page.add(warning);
    layoutFooter(page);
    page.write(resp.getWriter());
  }

  /** Display a warning in red, in lieu of the normal page */
  protected void displayWarningInLieuOfPage(String msg) throws IOException {
    displayMsgInLieuOfPage("<center><font color=red size=+1>" + msg + "</font></center>");
  }

  /** Display "The cache isn't ready yet, come back later" */
  protected void displayNotStarted() throws IOException {
    displayWarningInLieuOfPage(
        "This LOCKSS box is still starting.  Please "
            + srvLink(myServletDescr(), "try again", getParamsAsProps())
            + " in a moment.");
  }

  public MultiPartRequest getMultiPartRequest() throws FormDataTooLongException, IOException {
    int maxUpload =
        CurrentConfig.getIntParam(PARAM_MAX_UPLOAD_FILE_SIZE, DEFAULT_MAX_UPLOAD_FILE_SIZE);
    return getMultiPartRequest(maxUpload);
  }

  public MultiPartRequest getMultiPartRequest(int maxLen)
      throws FormDataTooLongException, IOException {
    if (req.getContentType() == null || !req.getContentType().startsWith("multipart/form-data")) {
      return null;
    }
    if (req.getContentLength() > maxLen) {
      throw new FormDataTooLongException(req.getContentLength() + " bytes, " + maxLen + " allowed");
    }
    MultiPartRequest multi = new MultiPartRequest(req);
    if (log.isDebug2()) {
      String[] parts = multi.getPartNames();
      log.debug3("Multipart request, " + parts.length + " parts");
      if (log.isDebug3()) {
        for (int p = 0; p < parts.length; p++) {
          String name = parts[p];
          String cont = multi.getString(parts[p]);
          log.debug3(name + ": " + cont);
        }
      }
    }
    multiReq = multi;
    return multi;
  }

  public String getParameter(String name) {
    String val = req.getParameter(name);
    if (val == null && multiReq != null) {
      val = multiReq.getString(name);
    }
    if (val == null) {
      return null;
    }
    val = StringUtils.strip(val, " \t");
    //     if (StringUtil.isNullString(val)) {
    if ("".equals(val)) {
      return null;
    }
    return val;
  }

  protected void layoutFooter(Page page) {
    ServletUtil.doLayoutFooter(
        page, (footnotes == null ? null : footnotes.iterator()), getLockssApp().getVersionInfo());
    if (footnotes != null) {
      footnotes.removeAllElements();
    }
  }

  /** Return the app instance. */
  protected LockssApp getLockssApp() {
    return theApp;
  }

  /**
   * Return the daemon instance, assumes that the servlet is running in the daemon.
   *
   * @throws ClassCastException if the servlet is running in an app other than the daemon
   */
  protected LockssDaemon getLockssDaemon() {
    return (LockssDaemon) theApp;
  }

  protected void logParams() {
    Enumeration en = req.getParameterNames();
    while (en.hasMoreElements()) {
      String name = (String) en.nextElement();
      String vals[];
      String dispval;
      if (StringUtil.indexOfIgnoreCase(name, "passw") >= 0) {
        dispval = req.getParameter(name).length() == 0 ? "" : "********";
      } else if (log.isDebug2() && (vals = req.getParameterValues(name)).length > 1) {
        dispval = StringUtil.separatedString(vals, ", ");
      } else {
        dispval = req.getParameter(name);
      }
      log.debug(name + " = " + dispval);
    }
  }

  /** Convenience method */
  protected String encodeText(String s) {
    return HtmlUtil.encode(s, HtmlUtil.ENCODE_TEXT);
  }

  /** Convenience method */
  protected String encodeTextArea(String s) {
    return HtmlUtil.encode(s, HtmlUtil.ENCODE_TEXTAREA);
  }

  /** Convenience method */
  protected String encodeAttr(String s) {
    return HtmlUtil.encode(s, HtmlUtil.ENCODE_ATTR);
  }

  /**
   * Create message and error message block
   *
   * @param composite TODO
   */
  protected void layoutErrorBlock(Composite composite) {
    if (errMsg != null || statusMsg != null) {
      ServletUtil.layoutErrorBlock(composite, errMsg, statusMsg);
    }
  }

  /** Exception thrown if multipart form data is longer than the caller-supplied max */
  public static class FormDataTooLongException extends Exception {
    public FormDataTooLongException(String message) {
      super(message);
    }
  }
}
Exemple #6
0
/** Export the contents and metadata from an AU */
public abstract class Exporter {

  private static final Logger log = Logger.getLogger(Exporter.class);

  static final String PREFIX = Configuration.PREFIX + "exporter.";

  /** Abort export after this many errors */
  public static final String PARAM_MAX_ERRORS = PREFIX + "maxErrors";

  public static final int DEFAULT_MAX_ERRORS = 5;

  protected static int maxErrors = DEFAULT_MAX_ERRORS;

  protected LockssDaemon daemon;
  protected ArchivalUnit au;
  protected File dir;
  protected String prefix;
  protected long maxSize = -1;
  protected int maxVersions = 1;
  protected boolean compress = false;
  protected boolean excludeDirNodes = false;
  protected FilenameTranslation xlate = FilenameTranslation.XLATE_NONE;
  protected List errors = new ArrayList();
  protected boolean isDiskFull = false;

  protected abstract void start() throws IOException;

  protected abstract void finish() throws IOException;

  protected abstract void writeCu(CachedUrl cu) throws IOException;

  protected Exporter(LockssDaemon daemon, ArchivalUnit au) {
    this.daemon = daemon;
    this.au = au;
  }

  /** Called by org.lockss.config.MiscConfig */
  public static void setConfig(
      Configuration config, Configuration oldConfig, Configuration.Differences diffs) {
    if (diffs.contains(PREFIX)) {
      maxErrors = config.getInt(PARAM_MAX_ERRORS, DEFAULT_MAX_ERRORS);
    }
  }

  public void setCompress(boolean val) {
    compress = val;
  }

  public boolean getCompress() {
    return compress;
  }

  public void setExcludeDirNodes(boolean val) {
    excludeDirNodes = val;
  }

  public boolean getExcludeDirNodes() {
    return excludeDirNodes;
  }

  public void setFilenameTranslation(FilenameTranslation val) {
    xlate = val;
  }

  public FilenameTranslation getFilenameTranslation() {
    return xlate;
  }

  public void setDir(File val) {
    dir = val;
  }

  public File getDir() {
    return dir;
  }

  public void setPrefix(String val) {
    prefix = val;
  }

  public String getPrefix() {
    return prefix;
  }

  public void setMaxSize(long val) {
    maxSize = val;
  }

  public long getMaxSize() {
    return maxSize;
  }

  public void setMaxVersions(int val) {
    maxVersions = val;
  }

  public int getMaxVersions() {
    return maxVersions;
  }

  public List getErrors() {
    return errors;
  }

  protected void checkArgs() {
    if (getDir() == null) {
      throw new IllegalArgumentException("Must supply output directory");
    }
    if (getPrefix() == null) {
      throw new IllegalArgumentException("Must supply file name/prefix");
    }
  }

  public void export() {
    log.debug("export(" + au.getName() + ")");
    log.debug(
        "dir: "
            + dir
            + ", pref: "
            + prefix
            + ", size: "
            + maxSize
            + ", ver: "
            + maxVersions
            + (compress ? ", (C)" : ""));
    checkArgs();
    try {
      start();
    } catch (IOException e) {
      recordError("Error opening file", e);
      return;
    }
    writeFiles();
    try {
      finish();
    } catch (IOException e) {
      if (!isDiskFull) {
        // If we already knew (and reported) disk full, also reporting it
        // as a close error is misleading.
        recordError("Error closing file", e);
      }
    }
  }

  protected String xlateFilename(String url) {
    return xlate.xlate(url);
  }

  protected void recordError(String msg, Throwable t) {
    log.error(msg, t);
    errors.add(msg + ": " + t.toString());
  }

  protected void recordError(String msg) {
    log.error(msg);
    errors.add(msg);
  }

  protected String getSoftwareVersion() {
    String releaseName = BuildInfo.getBuildProperty(BuildInfo.BUILD_RELEASENAME);
    StringBuilder sb = new StringBuilder();
    sb.append("LOCKSS Daemon ");
    if (releaseName != null) {
      sb.append(releaseName);
    }
    return sb.toString();
  }

  protected String getHostIp() {
    try {
      IPAddr localHost = IPAddr.getLocalHost();
      return localHost.getHostAddress();
    } catch (UnknownHostException e) {
      log.error("getHostIp()", e);
      return "1.1.1.1";
    }
  }

  protected String getHostName() {
    String res = ConfigManager.getPlatformHostname();
    if (res == null) {
      try {
        InetAddress inet = InetAddress.getLocalHost();
        return inet.getHostName();
      } catch (UnknownHostException e) {
        log.warning("Can't get hostname", e);
        return "unknown";
      }
    }
    return res;
  }

  protected Properties filterResponseProps(Properties props) {
    Properties res = new Properties();
    for (Map.Entry ent : props.entrySet()) {
      String key = (String) ent.getKey();
      if (StringUtil.startsWithIgnoreCase(key, "x-lockss")
          || StringUtil.startsWithIgnoreCase(key, "x_lockss")
          || key.equalsIgnoreCase("org.lockss.version.number")) {
        continue;
      }
      // We've lost the original case - capitalize them the way most people
      // expect
      res.put(StringUtil.titleCase(key, '-'), (String) ent.getValue());
    }
    return res;
  }

  protected String getHttpResponseString(CachedUrl cu) {
    Properties cuProps = cu.getProperties();
    Properties filteredProps = filterResponseProps(cuProps);
    String hdrString = PropUtil.toHeaderString(filteredProps);
    StringBuilder sb = new StringBuilder(hdrString.length() + 30);
    String line1 = inferHttpResponseCode(cu, cuProps);
    sb.append(line1);
    sb.append(Constants.CRLF);
    sb.append(hdrString);
    sb.append(Constants.CRLF);
    return sb.toString();
  }

  String inferHttpResponseCode(CachedUrl cu, Properties cuProps) {
    if (cuProps.get("location") == null) {
      return "HTTP/1.1 200 OK";
    } else {
      return "HTTP/1.1 302 Found";
    }
  }

  // return the next CU with content
  private CachedUrl getNextCu(CuIterator iter) {
    return iter.hasNext() ? iter.next() : null;
  }

  /**
   * Return true if, interpreting URLs as filenames, dirCu is a directory containing fileCu. Used to
   * exclude directory content from output files, so they can be unpacked by standard utilities
   * (e.g., unzip). Shouldn't be called with equal URLs, but return false in that case, as we
   * wouldn't want to exclude the URL
   */
  boolean isDirOf(CachedUrl dirCu, CachedUrl fileCu) {
    String dir = dirCu.getUrl();
    String file = fileCu.getUrl();
    if (!dir.endsWith("/")) {
      dir = dir + "/";
    }
    return file.startsWith(dir) && !file.equals(dir);
  }

  private void writeFiles() {
    PlatformUtil platutil = PlatformUtil.getInstance();
    CuIterator iter = AuUtil.getCuIterator(au);
    int errs = 0;
    CachedUrl curCu = null;
    CachedUrl nextCu = getNextCu(iter);
    while (nextCu != null) {
      curCu = nextCu;
      nextCu = getNextCu(iter);
      if (excludeDirNodes && nextCu != null && isDirOf(curCu, nextCu)) {
        continue;
      }
      CachedUrl[] cuVersions =
          curCu.getCuVersions(maxVersions > 0 ? maxVersions : Integer.MAX_VALUE);
      for (CachedUrl cu : cuVersions) {
        try {
          log.debug2("Exporting " + cu.getUrl());
          writeCu(cu);
        } catch (IOException e) {
          if (platutil.isDiskFullError(e)) {
            recordError("Disk full, can't write export file.");
            isDiskFull = true;
            return;
          }
        } catch (Exception e) {
          // XXX Would like to differentiate between errors opening or
          // reading CU, which shouldn't cause abort, and errors writing
          // to export file, which should.
          recordError("Unable to copy " + cu.getUrl(), e);
          if (errs++ >= maxErrors) {
            recordError("Aborting after " + errs + " errors");
            return;
          }
        }
      }
    }
  }

  private Set<File> fileSet = new HashSet<File>();
  private List<File> fileList = new ArrayList<File>();

  protected void recordExportFile(File file) {
    if (fileSet.add(file)) {
      fileList.add(file);
    }
  }

  /**
   * Return the list of export files written
   *
   * @return List of File written
   */
  public List<File> getExportFiles() {
    return fileList;
  }

  public interface Factory {
    public Exporter makeExporter(LockssDaemon daemon, ArchivalUnit au);
  }

  static final String WINDOWS_FROM = "?<>:*|\\";
  static final String WINDOWS_TO = "_______";
  static final String MAC_FROM = ":";
  static final String MAC_TO = "_";

  /** Enum of filename translation types */
  public static enum FilenameTranslation {
    XLATE_NONE("None") {
      public String xlate(String s) {
        return s;
      }
    },
    XLATE_WINDOWS("Windows") {
      public String xlate(String s) {
        return StringUtils.replaceChars(s, WINDOWS_FROM, WINDOWS_TO);
      }
    },
    XLATE_MAC("MacOS") {
      public String xlate(String s) {
        return StringUtils.replaceChars(s, MAC_FROM, MAC_TO);
      }
    };

    private final String label;

    FilenameTranslation(String label) {
      this.label = label;
    }

    public String getLabel() {
      return label;
    }

    public abstract String xlate(String s);
  };

  /** Enum of Exporter types, and factories */
  public static enum Type implements Factory {
    ARC_RESOURCE("ARC (content only)") {
      public Exporter makeExporter(LockssDaemon daemon, ArchivalUnit au) {
        return new ArcExporter(daemon, au, false);
      }
    },
    ARC_RESPONSE("ARC (response and content)") {
      public Exporter makeExporter(LockssDaemon daemon, ArchivalUnit au) {
        return new ArcExporter(daemon, au, true);
      }
    },
    WARC_RESOURCE("WARC (content only)") {
      public Exporter makeExporter(LockssDaemon daemon, ArchivalUnit au) {
        return new WarcExporter(daemon, au, false);
      }
    },
    WARC_RESPONSE("WARC (response and content)") {
      public Exporter makeExporter(LockssDaemon daemon, ArchivalUnit au) {
        return new WarcExporter(daemon, au, true);
      }
    },
    ZIP("ZIP") {
      public Exporter makeExporter(LockssDaemon daemon, ArchivalUnit au) {
        return new ZipExporter(daemon, au);
      }
    };

    private final String label;

    Type(String label) {
      this.label = label;
    }

    public abstract Exporter makeExporter(LockssDaemon daemon, ArchivalUnit au);

    public String getLabel() {
      return label;
    }
  };
}
/**
 * RepositoryManager is the center of the per AU repositories. It manages the repository config
 * parameters.
 */
public class RepositoryManager extends BaseLockssDaemonManager implements ConfigurableManager {

  private static Logger log = Logger.getLogger("RepositoryManager");

  public static final String PREFIX = Configuration.PREFIX + "repository.";

  /** Maximum size of per-AU repository node cache */
  public static final String PARAM_MAX_PER_AU_CACHE_SIZE = PREFIX + "nodeCache.size";

  public static final int DEFAULT_MAX_PER_AU_CACHE_SIZE = 10;

  /*
   * This needs to be a small multiple of the number of simultaneous
   * polls (poller and voter), as there is a cache entry per active AU.
   * Each poll will have one active AU at a time.
   */
  public static final String PARAM_MAX_SUSPECT_VERSIONS_CACHE_SIZE =
      PREFIX + "suspectVersionsCache.size";
  public static final int DEFAULT_MAX_SUSPECT_VERSIONS_CACHE_SIZE = 10;

  static final String GLOBAL_CACHE_PREFIX = PREFIX + "globalNodeCache.";
  public static final String PARAM_MAX_GLOBAL_CACHE_SIZE = GLOBAL_CACHE_PREFIX + "size";
  public static final int DEFAULT_MAX_GLOBAL_CACHE_SIZE = 500;

  public static final String PARAM_GLOBAL_CACHE_ENABLED = GLOBAL_CACHE_PREFIX + "enabled";
  public static final boolean DEFAULT_GLOBAL_CACHE_ENABLED = false;

  /** Max times to loop looking for unused AU directory. */
  public static final String PARAM_MAX_UNUSED_DIR_SEARCH = PREFIX + "maxUnusedDirSearch";

  public static final int DEFAULT_MAX_UNUSED_DIR_SEARCH = 30000;

  /** If true, LocalRepository keeps track of next subdir name to probe. */
  public static final String PARAM_IS_STATEFUL_UNUSED_DIR_SEARCH =
      PREFIX + "enableStatefulUnusedDirSearch";

  public static final boolean DEFAULT_IS_STATEFUL_UNUSED_DIR_SEARCH = true;

  /** Max percent of time size calculation thread may run. */
  public static final String PARAM_SIZE_CALC_MAX_LOAD = PREFIX + "sizeCalcMaxLoad";

  public static final float DEFAULT_SIZE_CALC_MAX_LOAD = 0.5F;

  /**
   * If true, path components longer than the maximum filesystem path component are encodes are
   * multiple levels of directories
   */
  public static final String PARAM_ENABLE_LONG_COMPONENTS = PREFIX + "enableLongComponents";

  public static final boolean DEFAULT_ENABLE_LONG_COMPONENTS = true;

  /**
   * Prior to 1.61.6, when long component support is enabled, backslahes were normalized to %5c
   * instead of %5C. This is harmless if checkUnnormalized is set to Fix, as they'll be normalized.
   * Otherwise, this can be set true for compatibility with old repositories. Setting
   * checkUnnormalized to Fix is preferred.
   */
  public static final String PARAM_ENABLE_LONG_COMPONENTS_COMPATIBILITY =
      PREFIX + "enableLongComponentsCompatibility";

  public static final boolean DEFAULT_ENABLE_LONG_COMPONENTS_COMPATIBILITY = false;

  /** Maximum length of a filesystem path component. */
  public static final String PARAM_MAX_COMPONENT_LENGTH = PREFIX + "maxComponentLength";

  public static final int DEFAULT_MAX_COMPONENT_LENGTH = 255;

  /** @see #PARAM_CHECK_UNNORMALIZED */
  public enum CheckUnnormalizedMode {
    No,
    Log,
    Fix
  };

  /**
   * Check for existing nodes with unnormalized names (created by very old daemon that didn't
   * normalize): None, Log, Fix
   */
  public static final String PARAM_CHECK_UNNORMALIZED = PREFIX + "checkUnnormalized";

  public static final CheckUnnormalizedMode DEFAULT_CHECK_UNNORMALIZED = CheckUnnormalizedMode.Log;

  static final String WDOG_PARAM_SIZE_CALC = "SizeCalc";
  static final long WDOG_DEFAULT_SIZE_CALC = Constants.DAY;

  static final String PRIORITY_PARAM_SIZE_CALC = "SizeCalc";
  static final int PRIORITY_DEFAULT_SIZE_CALC = Thread.NORM_PRIORITY - 1;

  static final String DISK_PREFIX = PREFIX + "diskSpace.";

  static final String PARAM_DISK_WARN_FRRE_MB = DISK_PREFIX + "warn.freeMB";
  static final int DEFAULT_DISK_WARN_FRRE_MB = 5000;
  static final String PARAM_DISK_FULL_FRRE_MB = DISK_PREFIX + "full.freeMB";
  static final int DEFAULT_DISK_FULL_FRRE_MB = 100;
  static final String PARAM_DISK_WARN_FRRE_PERCENT = DISK_PREFIX + "warn.freePercent";
  static final double DEFAULT_DISK_WARN_FRRE_PERCENT = .02;
  static final String PARAM_DISK_FULL_FRRE_PERCENT = DISK_PREFIX + "full.freePercent";
  static final double DEFAULT_DISK_FULL_FRRE_PERCENT = .01;

  private PlatformUtil platInfo = PlatformUtil.getInstance();
  private List repoList = Collections.EMPTY_LIST;
  int paramNodeCacheSize = DEFAULT_MAX_PER_AU_CACHE_SIZE;
  boolean paramIsGlobalNodeCache = DEFAULT_GLOBAL_CACHE_ENABLED;
  int paramGlobalNodeCacheSize = DEFAULT_MAX_GLOBAL_CACHE_SIZE;
  int paramSuspectVersionsCacheSize = DEFAULT_MAX_SUSPECT_VERSIONS_CACHE_SIZE;
  UniqueRefLruCache globalNodeCache = new UniqueRefLruCache(DEFAULT_MAX_GLOBAL_CACHE_SIZE);
  UniqueRefLruCache suspectVersionsCache =
      new UniqueRefLruCache(DEFAULT_MAX_SUSPECT_VERSIONS_CACHE_SIZE);
  Map localRepos = new HashMap();
  private static int maxUnusedDirSearch = DEFAULT_MAX_UNUSED_DIR_SEARCH;
  private static boolean isStatefulUnusedDirSearch = DEFAULT_IS_STATEFUL_UNUSED_DIR_SEARCH;
  private static boolean enableLongComponents = DEFAULT_ENABLE_LONG_COMPONENTS;
  private static boolean enableLongComponentsCompatibility =
      DEFAULT_ENABLE_LONG_COMPONENTS_COMPATIBILITY;
  private static int maxComponentLength = DEFAULT_MAX_COMPONENT_LENGTH;
  private static CheckUnnormalizedMode checkUnnormalized = DEFAULT_CHECK_UNNORMALIZED;

  PlatformUtil.DF paramDFWarn =
      PlatformUtil.DF.makeThreshold(DEFAULT_DISK_WARN_FRRE_MB, DEFAULT_DISK_WARN_FRRE_PERCENT);
  PlatformUtil.DF paramDFFull =
      PlatformUtil.DF.makeThreshold(DEFAULT_DISK_FULL_FRRE_MB, DEFAULT_DISK_FULL_FRRE_PERCENT);

  private float sizeCalcMaxLoad = DEFAULT_SIZE_CALC_MAX_LOAD;

  public void startService() {
    super.startService();
    localRepos = new HashMap();
  }

  public void setConfig(
      Configuration config, Configuration oldConfig, Configuration.Differences changedKeys) {
    //  Build list of repositories from list of disk (fs) paths).  Needs to
    //  be generalized if ever another repository implementation.
    if (changedKeys.contains(ConfigManager.PARAM_PLATFORM_DISK_SPACE_LIST)) {
      List lst = new ArrayList();
      String dspace = config.get(ConfigManager.PARAM_PLATFORM_DISK_SPACE_LIST, "");
      List paths = StringUtil.breakAt(dspace, ';');
      if (paths != null) {
        for (Iterator iter = paths.iterator(); iter.hasNext(); ) {
          lst.add("local:" + (String) iter.next());
        }
      }
      repoList = lst;
    }
    if (changedKeys.contains(PARAM_MAX_PER_AU_CACHE_SIZE)) {
      paramNodeCacheSize =
          config.getInt(PARAM_MAX_PER_AU_CACHE_SIZE, DEFAULT_MAX_PER_AU_CACHE_SIZE);
      for (Iterator iter = getDaemon().getAllLockssRepositories().iterator(); iter.hasNext(); ) {
        LockssRepository repo = (LockssRepository) iter.next();
        if (repo instanceof LockssRepositoryImpl) {
          LockssRepositoryImpl repoImpl = (LockssRepositoryImpl) repo;
          repoImpl.setNodeCacheSize(paramNodeCacheSize);
        }
      }
    }
    if (changedKeys.contains(PARAM_MAX_SUSPECT_VERSIONS_CACHE_SIZE)) {
      paramSuspectVersionsCacheSize =
          config.getInt(
              PARAM_MAX_SUSPECT_VERSIONS_CACHE_SIZE, DEFAULT_MAX_SUSPECT_VERSIONS_CACHE_SIZE);
      suspectVersionsCache.setMaxSize(paramSuspectVersionsCacheSize);
    }
    if (changedKeys.contains(GLOBAL_CACHE_PREFIX)) {
      paramIsGlobalNodeCache =
          config.getBoolean(PARAM_GLOBAL_CACHE_ENABLED, DEFAULT_GLOBAL_CACHE_ENABLED);
      if (paramIsGlobalNodeCache) {
        paramGlobalNodeCacheSize =
            config.getInt(PARAM_MAX_GLOBAL_CACHE_SIZE, DEFAULT_MAX_GLOBAL_CACHE_SIZE);
        log.debug("global node cache size: " + paramGlobalNodeCacheSize);
        globalNodeCache.setMaxSize(paramGlobalNodeCacheSize);
      }
    }
    if (changedKeys.contains(DISK_PREFIX)) {
      int minMB = config.getInt(PARAM_DISK_WARN_FRRE_MB, DEFAULT_DISK_WARN_FRRE_MB);
      double minPer =
          config.getPercentage(PARAM_DISK_WARN_FRRE_PERCENT, DEFAULT_DISK_WARN_FRRE_PERCENT);
      paramDFWarn = PlatformUtil.DF.makeThreshold(minMB, minPer);
      minMB = config.getInt(PARAM_DISK_FULL_FRRE_MB, DEFAULT_DISK_FULL_FRRE_MB);
      minPer = config.getPercentage(PARAM_DISK_FULL_FRRE_PERCENT, DEFAULT_DISK_FULL_FRRE_PERCENT);
      paramDFFull = PlatformUtil.DF.makeThreshold(minMB, minPer);
    }
    if (changedKeys.contains(PARAM_SIZE_CALC_MAX_LOAD)) {
      sizeCalcMaxLoad = config.getPercentage(PARAM_SIZE_CALC_MAX_LOAD, DEFAULT_SIZE_CALC_MAX_LOAD);
    }
    if (changedKeys.contains(PREFIX)) {
      maxUnusedDirSearch =
          config.getInt(PARAM_MAX_UNUSED_DIR_SEARCH, DEFAULT_MAX_UNUSED_DIR_SEARCH);
      isStatefulUnusedDirSearch =
          config.getBoolean(
              PARAM_IS_STATEFUL_UNUSED_DIR_SEARCH, DEFAULT_IS_STATEFUL_UNUSED_DIR_SEARCH);
      enableLongComponents =
          config.getBoolean(PARAM_ENABLE_LONG_COMPONENTS, DEFAULT_ENABLE_LONG_COMPONENTS);
      enableLongComponentsCompatibility =
          config.getBoolean(
              PARAM_ENABLE_LONG_COMPONENTS_COMPATIBILITY,
              DEFAULT_ENABLE_LONG_COMPONENTS_COMPATIBILITY);
      maxComponentLength = config.getInt(PARAM_MAX_COMPONENT_LENGTH, DEFAULT_MAX_COMPONENT_LENGTH);
      checkUnnormalized =
          (CheckUnnormalizedMode)
              config.getEnum(
                  CheckUnnormalizedMode.class,
                  PARAM_CHECK_UNNORMALIZED,
                  DEFAULT_CHECK_UNNORMALIZED);
    }
  }

  public static boolean isEnableLongComponents() {
    return enableLongComponents;
  }

  public static boolean isEnableLongComponentsCompatibility() {
    return enableLongComponentsCompatibility;
  }

  public static int getMaxComponentLength() {
    return maxComponentLength;
  }

  public static CheckUnnormalizedMode getCheckUnnormalizedMode() {
    return checkUnnormalized;
  }

  /**
   * Return list of known repository names. Needs a registration mechanism if ever another
   * repository implementation.
   */
  public List<String> getRepositoryList() {
    return repoList;
  }

  public PlatformUtil.DF getRepositoryDF(String repoName) {
    String path = LockssRepositoryImpl.getLocalRepositoryPath(repoName);
    log.debug("path: " + path);
    //    try {
    return platInfo.getJavaDF(path);
    //    } catch (PlatformUtil.UnsupportedException e) {
    //      return null;
    //    }
  }

  public Map<String, PlatformUtil.DF> getRepositoryMap() {
    Map<String, PlatformUtil.DF> repoMap = new LinkedMap();
    for (String repo : getRepositoryList()) {
      repoMap.put(repo, getRepositoryDF(repo));
    }
    return repoMap;
  }

  public String findLeastFullRepository() {
    return findLeastFullRepository(getRepositoryMap());
  }

  public String findLeastFullRepository(Map<String, PlatformUtil.DF> repoMap) {
    String mostFree = null;
    for (String repo : repoMap.keySet()) {
      PlatformUtil.DF df = repoMap.get(repo);
      if (df != null) {
        if (mostFree == null || (repoMap.get(mostFree)).getAvail() < df.getAvail()) {
          mostFree = repo;
        }
      }
    }
    return mostFree;
  }

  public PlatformUtil.DF getDiskWarnThreshold() {
    return paramDFWarn;
  }

  public PlatformUtil.DF getDiskFullThreshold() {
    return paramDFFull;
  }

  public static int getMaxUnusedDirSearch() {
    return maxUnusedDirSearch;
  }

  public static boolean isStatefulUnusedDirSearch() {
    return isStatefulUnusedDirSearch;
  }

  public List findExistingRepositoriesFor(String auid) {
    List res = null;
    for (Iterator iter = getRepositoryList().iterator(); iter.hasNext(); ) {
      String repoName = (String) iter.next();
      String path = LockssRepositoryImpl.getLocalRepositoryPath(repoName);
      if (LockssRepositoryImpl.doesAuDirExist(auid, path)) {
        if (res == null) {
          res = new ArrayList();
        }
        res.add(repoName);
      }
    }
    return res == null ? Collections.EMPTY_LIST : res;
  }

  // hack only local
  public synchronized LockssRepositoryImpl getRepositoryFromPath(String path) {
    LockssRepositoryImpl repo = (LockssRepositoryImpl) localRepos.get(path);
    if (repo == null) {
      repo = new LockssRepositoryImpl(path);
      repo.initService(getDaemon());
      repo.startService();
      localRepos.put(path, repo);
    }
    return repo;
  }

  /**
   * Return the disk space used by the AU, including all overhead, optionally calculating it if
   * necessary.
   *
   * @param repoAuPath the full path to an AU dir in a LockssRepositoryImpl
   * @param calcIfUnknown if true, size will calculated if unknown (time consumeing)
   * @return the AU's disk usage in bytes, or -1 if unknown
   */
  public long getRepoDiskUsage(String repoAuPath, boolean calcIfUnknown) {
    LockssRepository repo = getRepositoryFromPath(repoAuPath);
    if (repo != null) {
      try {
        RepositoryNode repoNode = repo.getNode(AuCachedUrlSetSpec.URL);
        if (repoNode instanceof AuNodeImpl) {
          return ((AuNodeImpl) repoNode).getDiskUsage(calcIfUnknown);
        }
      } catch (MalformedURLException ignore) {
      }
    }
    return -1;
  }

  public synchronized void setRepositoryForPath(String path, LockssRepositoryImpl repo) {
    localRepos.put(path, repo);
  }

  public boolean isGlobalNodeCache() {
    return paramIsGlobalNodeCache;
  }

  public UniqueRefLruCache getGlobalNodeCache() {
    return globalNodeCache;
  }

  public UniqueRefLruCache getSuspectVersionsCache() {
    return suspectVersionsCache;
  }

  // Background thread to (re)calculate AU size and disk usage.

  private Set sizeCalcQueue = new HashSet();
  private BinarySemaphore sizeCalcSem = new BinarySemaphore();
  private SizeCalcThread sizeCalcThread;

  /** engqueue a size calculation for the AU */
  public void queueSizeCalc(ArchivalUnit au) {
    queueSizeCalc(AuUtil.getAuRepoNode(au));
  }

  /** engqueue a size calculation for the node */
  public void queueSizeCalc(RepositoryNode node) {
    synchronized (sizeCalcQueue) {
      if (sizeCalcQueue.add(node)) {
        log.debug2("Queue size calc: " + node);
        startOrKickThread();
      }
    }
  }

  public int sizeCalcQueueLen() {
    synchronized (sizeCalcQueue) {
      return sizeCalcQueue.size();
    }
  }

  void startOrKickThread() {
    if (sizeCalcThread == null) {
      log.debug2("Starting thread");
      sizeCalcThread = new SizeCalcThread();
      sizeCalcThread.start();
      sizeCalcThread.waitRunning();
    }
    sizeCalcSem.give();
  }

  void stopThread() {
    if (sizeCalcThread != null) {
      log.debug2("Stopping thread");
      sizeCalcThread.stopSizeCalc();
      sizeCalcThread = null;
    }
  }

  void doSizeCalc(RepositoryNode node) {
    node.getTreeContentSize(null, true);
    if (node instanceof AuNodeImpl) {
      ((AuNodeImpl) node).getDiskUsage(true);
    }
  }

  long sleepTimeToAchieveLoad(long runDuration, float maxLoad) {
    return Math.round(((double) runDuration / maxLoad) - runDuration);
  }

  private class SizeCalcThread extends LockssThread {
    private volatile boolean goOn = true;

    private SizeCalcThread() {
      super("SizeCalc");
    }

    public void lockssRun() {
      setPriority(PRIORITY_PARAM_SIZE_CALC, PRIORITY_DEFAULT_SIZE_CALC);
      startWDog(WDOG_PARAM_SIZE_CALC, WDOG_DEFAULT_SIZE_CALC);
      triggerWDogOnExit(true);
      nowRunning();

      while (goOn) {
        try {
          pokeWDog();
          if (sizeCalcQueue.isEmpty()) {
            Deadline timeout = Deadline.in(Constants.HOUR);
            sizeCalcSem.take(timeout);
          }
          RepositoryNode node;
          synchronized (sizeCalcQueue) {
            node = (RepositoryNode) CollectionUtil.getAnElement(sizeCalcQueue);
          }
          if (node != null) {
            long start = TimeBase.nowMs();
            log.debug2("CalcSize start: " + node);
            long dur = 0;
            try {
              doSizeCalc(node);
              dur = TimeBase.nowMs() - start;
              log.debug2("CalcSize finish (" + StringUtil.timeIntervalToString(dur) + "): " + node);
            } catch (RuntimeException e) {
              log.warning("doSizeCalc: " + node, e);
            }
            synchronized (sizeCalcQueue) {
              sizeCalcQueue.remove(node);
            }
            pokeWDog();
            long sleep = sleepTimeToAchieveLoad(dur, sizeCalcMaxLoad);
            Deadline.in(sleep).sleep();
          }
        } catch (InterruptedException e) {
          // just wakeup and check for exit
        }
      }
      if (!goOn) {
        triggerWDogOnExit(false);
      }
    }

    private void stopSizeCalc() {
      goOn = false;
      interrupt();
    }
  }
}
Exemple #8
0
/** UI to invoke various daemon actions */
@SuppressWarnings("serial")
public class DebugPanel extends LockssServlet {

  public static final String PREFIX = Configuration.PREFIX + "debugPanel.";

  /** Priority for crawls started from the debug panel */
  public static final String PARAM_CRAWL_PRIORITY = PREFIX + "crawlPriority";

  public static final int DEFAULT_CRAWL_PRIORITY = 10;

  /** Priority for crawls started from the debug panel */
  public static final String PARAM_ENABLE_DEEP_CRAWL = PREFIX + "deepCrawlEnabled";

  private static final boolean DEFAULT_ENABLE_DEEP_CRAWL = false;

  static final String KEY_ACTION = "action";
  static final String KEY_MSG = "msg";
  static final String KEY_NAME_SEL = "name_sel";
  static final String KEY_NAME_TYPE = "name_type";
  static final String KEY_AUID = "auid";
  static final String KEY_URL = "url";
  static final String KEY_REFETCH_DEPTH = "depth";
  static final String KEY_TIME = "time";

  static final String ACTION_MAIL_BACKUP = "Mail Backup File";
  static final String ACTION_THROW_IOEXCEPTION = "Throw IOException";
  static final String ACTION_FIND_URL = "Find Preserved URL";

  public static final String ACTION_REINDEX_METADATA = "Reindex Metadata";
  public static final String ACTION_FORCE_REINDEX_METADATA = "Force Reindex Metadata";
  public static final String ACTION_START_V3_POLL = "Start V3 Poll";
  static final String ACTION_FORCE_START_V3_POLL = "Force V3 Poll";
  public static final String ACTION_START_CRAWL = "Start Crawl";
  public static final String ACTION_FORCE_START_CRAWL = "Force Start Crawl";
  public static final String ACTION_START_DEEP_CRAWL = "Deep Crawl";
  public static final String ACTION_FORCE_START_DEEP_CRAWL = "Force Deep Crawl";
  public static final String ACTION_CHECK_SUBSTANCE = "Check Substance";
  static final String ACTION_CRAWL_PLUGINS = "Crawl Plugins";
  static final String ACTION_RELOAD_CONFIG = "Reload Config";
  static final String ACTION_SLEEP = "Sleep";
  public static final String ACTION_DISABLE_METADATA_INDEXING = "Disable Indexing";

  /** Set of actions for which audit alerts shouldn't be generated */
  public static final Set noAuditActions = SetUtil.set(ACTION_FIND_URL);

  static final String COL2 = "colspan=2";
  static final String COL2CENTER = COL2 + " align=center";

  static Logger log = Logger.getLogger("DebugPanel");

  private LockssDaemon daemon;
  private PluginManager pluginMgr;
  private PollManager pollManager;
  private CrawlManager crawlMgr;
  private ConfigManager cfgMgr;
  private DbManager dbMgr;
  private MetadataManager metadataMgr;
  private RemoteApi rmtApi;

  boolean showResult;
  boolean showForcePoll;
  boolean showForceCrawl;
  boolean showForceReindexMetadata;

  String formAuid;
  String formDepth = "100";

  protected void resetLocals() {
    resetVars();
    super.resetLocals();
  }

  void resetVars() {
    formAuid = null;
    errMsg = null;
    statusMsg = null;
    showForcePoll = false;
    showForceCrawl = false;
    showForceReindexMetadata = false;
  }

  public void init(ServletConfig config) throws ServletException {
    super.init(config);
    daemon = getLockssDaemon();
    pluginMgr = daemon.getPluginManager();
    pollManager = daemon.getPollManager();
    crawlMgr = daemon.getCrawlManager();
    cfgMgr = daemon.getConfigManager();
    rmtApi = daemon.getRemoteApi();
    try {
      dbMgr = daemon.getDbManager();
      metadataMgr = daemon.getMetadataManager();
    } catch (IllegalArgumentException ex) {
    }
  }

  public void lockssHandleRequest() throws IOException {
    resetVars();
    boolean showForm = true;
    String action = getParameter(KEY_ACTION);

    if (!StringUtil.isNullString(action)) {

      formAuid = getParameter(KEY_AUID);
      formDepth = getParameter(KEY_REFETCH_DEPTH);

      UserAccount acct = getUserAccount();
      if (acct != null && !noAuditActions.contains(action)) {
        acct.auditableEvent("used debug panel action: " + action + " AU ID: " + formAuid);
      }
    }

    if (ACTION_MAIL_BACKUP.equals(action)) {
      doMailBackup();
    }
    if (ACTION_RELOAD_CONFIG.equals(action)) {
      doReloadConfig();
    }
    if (ACTION_SLEEP.equals(action)) {
      doSleep();
    }
    if (ACTION_THROW_IOEXCEPTION.equals(action)) {
      doThrow();
    }
    if (ACTION_START_V3_POLL.equals(action)) {
      doV3Poll();
    }
    if (ACTION_FORCE_START_V3_POLL.equals(action)) {
      forceV3Poll();
    }
    if (ACTION_START_CRAWL.equals(action)) {
      doCrawl(false, false);
    }
    if (ACTION_FORCE_START_CRAWL.equals(action)) {
      doCrawl(true, false);
    }
    if (ACTION_START_DEEP_CRAWL.equals(action)) {
      doCrawl(false, true);
    }
    if (ACTION_FORCE_START_DEEP_CRAWL.equals(action)) {
      doCrawl(true, true);
    }
    if (ACTION_CHECK_SUBSTANCE.equals(action)) {
      doCheckSubstance();
    }
    if (ACTION_CRAWL_PLUGINS.equals(action)) {
      crawlPluginRegistries();
    }
    if (ACTION_FIND_URL.equals(action)) {
      showForm = doFindUrl();
    }
    if (ACTION_REINDEX_METADATA.equals(action)) {
      doReindexMetadata();
    }
    if (ACTION_FORCE_REINDEX_METADATA.equals(action)) {
      forceReindexMetadata();
    }
    if (ACTION_DISABLE_METADATA_INDEXING.equals(action)) {
      doDisableMetadataIndexing();
    }
    if (showForm) {
      displayPage();
    }
  }

  private void doMailBackup() {
    try {
      rmtApi.createConfigBackupFile(RemoteApi.BackupFileDisposition.Mail);
    } catch (Exception e) {
      errMsg = "Error: " + e.getMessage();
    }
  }

  private void doReloadConfig() {
    cfgMgr.requestReload();
  }

  private void doThrow() throws IOException {
    String msg = getParameter(KEY_MSG);
    throw new IOException(msg != null ? msg : "Test message");
  }

  private void doSleep() throws IOException {
    String timestr = getParameter(KEY_TIME);
    try {
      long time = StringUtil.parseTimeInterval(timestr);
      Deadline.in(time).sleep();
      statusMsg = "Slept for " + StringUtil.timeIntervalToString(time);
    } catch (NumberFormatException e) {
      errMsg = "Illegal duration: " + e;
    } catch (InterruptedException e) {
      errMsg = "Interrupted: " + e;
    }
  }

  private void doReindexMetadata() {
    ArchivalUnit au = getAu();
    if (au == null) return;
    try {
      startReindexingMetadata(au, false);
    } catch (RuntimeException e) {
      log.error("Can't reindex metadata", e);
      errMsg = "Error: " + e.toString();
    }
  }

  private void forceReindexMetadata() {
    ArchivalUnit au = getAu();
    if (au == null) return;
    try {
      startReindexingMetadata(au, true);
    } catch (RuntimeException e) {
      log.error("Can't reindex metadata", e);
      errMsg = "Error: " + e.toString();
    }
  }

  private void doDisableMetadataIndexing() {
    ArchivalUnit au = getAu();
    if (au == null) return;
    try {
      disableMetadataIndexing(au, false);
    } catch (RuntimeException e) {
      log.error("Can't disable metadata indexing", e);
      errMsg = "Error: " + e.toString();
    }
  }

  private void doCrawl(boolean force, boolean deep) {
    ArchivalUnit au = getAu();
    if (au == null) return;
    try {
      startCrawl(au, force, deep);
    } catch (CrawlManagerImpl.NotEligibleException.RateLimiter e) {
      errMsg = "AU has crawled recently (" + e.getMessage() + ").  Click again to override.";
      showForceCrawl = true;
      return;
    } catch (CrawlManagerImpl.NotEligibleException e) {
      errMsg = "Can't enqueue crawl: " + e.getMessage();
    }
  }

  private void crawlPluginRegistries() {
    StringBuilder sb = new StringBuilder();
    for (ArchivalUnit au : pluginMgr.getAllRegistryAus()) {
      sb.append(au.getName());
      sb.append(": ");
      try {
        startCrawl(au, true, false);
        sb.append("Queued.");
      } catch (CrawlManagerImpl.NotEligibleException e) {
        sb.append("Failed: ");
        sb.append(e.getMessage());
      }
      sb.append("\n");
    }
    statusMsg = sb.toString();
  }

  private boolean startCrawl(ArchivalUnit au, boolean force, boolean deep)
      throws CrawlManagerImpl.NotEligibleException {
    CrawlManagerImpl cmi = (CrawlManagerImpl) crawlMgr;
    if (force) {
      RateLimiter limit = cmi.getNewContentRateLimiter(au);
      if (!limit.isEventOk()) {
        limit.unevent();
      }
    }
    cmi.checkEligibleToQueueNewContentCrawl(au);
    String delayMsg = "";
    String deepMsg = "";
    try {
      cmi.checkEligibleForNewContentCrawl(au);
    } catch (CrawlManagerImpl.NotEligibleException e) {
      delayMsg = ", Start delayed due to: " + e.getMessage();
    }
    Configuration config = ConfigManager.getCurrentConfig();
    int pri = config.getInt(PARAM_CRAWL_PRIORITY, DEFAULT_CRAWL_PRIORITY);

    CrawlReq req;
    try {
      req = new CrawlReq(au);
      req.setPriority(pri);
      if (deep) {
        int d = Integer.parseInt(formDepth);
        if (d < 0) {
          errMsg = "Illegal refetch depth: " + d;
          return false;
        }
        req.setRefetchDepth(d);
        deepMsg = "Deep (" + req.getRefetchDepth() + ") ";
      }
    } catch (NumberFormatException e) {
      errMsg = "Illegal refetch depth: " + formDepth;
      return false;
    } catch (RuntimeException e) {
      log.error("Couldn't create CrawlReq: " + au, e);
      errMsg = "Couldn't create CrawlReq: " + e.toString();
      return false;
    }
    cmi.startNewContentCrawl(req, null);
    statusMsg = deepMsg + "Crawl requested for " + au.getName() + delayMsg;
    return true;
  }

  private void doCheckSubstance() {
    ArchivalUnit au = getAu();
    if (au == null) return;
    try {
      checkSubstance(au);
    } catch (RuntimeException e) {
      log.error("Error in SubstanceChecker", e);
      errMsg = "Error in SubstanceChecker; see log.";
    }
  }

  private void checkSubstance(ArchivalUnit au) {
    SubstanceChecker subChecker = new SubstanceChecker(au);
    if (!subChecker.isEnabled()) {
      errMsg = "No substance patterns defined for plugin.";
      return;
    }
    AuState auState = AuUtil.getAuState(au);
    SubstanceChecker.State oldState = auState.getSubstanceState();
    SubstanceChecker.State newState = subChecker.findSubstance();
    String chtxt = (newState == oldState ? "(unchanged)" : "(was " + oldState.toString() + ")");
    switch (newState) {
      case Unknown:
        log.error("Shouldn't happen: SubstanceChecker returned Unknown");
        errMsg = "Error in SubstanceChecker; see log.";
        break;
      case Yes:
        statusMsg = "AU has substance " + chtxt + ": " + au.getName();
        auState.setSubstanceState(SubstanceChecker.State.Yes);
        break;
      case No:
        statusMsg = "AU has no substance " + chtxt + ": " + au.getName();
        auState.setSubstanceState(SubstanceChecker.State.No);
        break;
    }
  }

  private boolean startReindexingMetadata(ArchivalUnit au, boolean force) {
    if (metadataMgr == null) {
      errMsg = "Metadata processing is not enabled.";
      return false;
    }

    if (!force) {
      if (!AuUtil.hasCrawled(au)) {
        errMsg = "Au has never crawled. Click again to reindex metadata";
        showForceReindexMetadata = true;
        return false;
      }

      AuState auState = AuUtil.getAuState(au);
      switch (auState.getSubstanceState()) {
        case No:
          errMsg = "Au has no substance. Click again to reindex metadata";
          showForceReindexMetadata = true;
          return false;
        case Unknown:
          errMsg = "Unknown substance for Au. Click again to reindex metadata.";
          showForceReindexMetadata = true;
          return false;
        case Yes:
          // fall through
      }
    }

    // Fully reindex metadata with the highest priority.
    Connection conn = null;
    PreparedStatement insertPendingAuBatchStatement = null;

    try {
      conn = dbMgr.getConnection();
      insertPendingAuBatchStatement = metadataMgr.getPrioritizedInsertPendingAuBatchStatement(conn);

      if (metadataMgr.enableAndAddAuToReindex(
          au, conn, insertPendingAuBatchStatement, false, true)) {
        statusMsg = "Reindexing metadata for " + au.getName();
        return true;
      }
    } catch (DbException dbe) {
      log.error("Cannot reindex metadata for " + au.getName(), dbe);
    } finally {
      DbManager.safeCloseStatement(insertPendingAuBatchStatement);
      DbManager.safeRollbackAndClose(conn);
    }

    if (force) {
      errMsg = "Still cannot reindex metadata for " + au.getName();
    } else {
      errMsg = "Cannot reindex metadata for " + au.getName();
    }
    return false;
  }

  private boolean disableMetadataIndexing(ArchivalUnit au, boolean force) {
    if (metadataMgr == null) {
      errMsg = "Metadata processing is not enabled.";
      return false;
    }

    try {
      metadataMgr.disableAuIndexing(au);
      statusMsg = "Disabled metadata indexing for " + au.getName();
      return true;
    } catch (Exception e) {
      errMsg = "Cannot reindex metadata for " + au.getName() + ": " + e.getMessage();
      return false;
    }
  }

  private void doV3Poll() {
    ArchivalUnit au = getAu();
    if (au == null) return;
    try {
      callV3ContentPoll(au);
    } catch (PollManager.NotEligibleException e) {
      errMsg = "AU is not eligible for poll: " + e.getMessage();
      //       errMsg = "Ineligible: " + e.getMessage() +
      // 	"<br>Click again to force new poll.";
      //       showForcePoll = true;
      return;
    } catch (Exception e) {
      log.error("Can't start poll", e);
      errMsg = "Error: " + e.toString();
    }
  }

  private void forceV3Poll() {
    ArchivalUnit au = getAu();
    if (au == null) return;
    try {
      callV3ContentPoll(au);
    } catch (Exception e) {
      log.error("Can't start poll", e);
      errMsg = "Error: " + e.toString();
    }
  }

  private void callV3ContentPoll(ArchivalUnit au) throws PollManager.NotEligibleException {
    log.debug("Enqueuing a V3 Content Poll on " + au.getName());
    PollSpec spec = new PollSpec(au.getAuCachedUrlSet(), Poll.V3_POLL);
    pollManager.enqueueHighPriorityPoll(au, spec);
    statusMsg = "Enqueued V3 poll for " + au.getName();
  }

  private boolean doFindUrl() throws IOException {

    String url = getParameter(KEY_URL);

    String redir =
        srvURL(
            AdminServletManager.SERVLET_DAEMON_STATUS,
            PropUtil.fromArgs("table", ArchivalUnitStatus.AUS_WITH_URL_TABLE_NAME, "key", url));

    resp.setContentLength(0);
    //     resp.sendRedirect(resp.encodeRedirectURL(redir));
    resp.sendRedirect(redir);
    return false;
  }

  ArchivalUnit getAu() {
    if (StringUtil.isNullString(formAuid)) {
      errMsg = "Select an AU";
      return null;
    }
    ArchivalUnit au = pluginMgr.getAuFromId(formAuid);
    if (au == null) {
      errMsg = "No such AU.  Select an AU";
      return null;
    }
    return au;
  }

  private void displayPage() throws IOException {
    Page page = newPage();
    layoutErrorBlock(page);
    ServletUtil.layoutExplanationBlock(page, "Debug Actions");
    page.add(makeForm());
    page.add("<br>");
    endPage(page);
  }

  private Element makeForm() {
    Composite comp = new Composite();
    Form frm = new Form(srvURL(myServletDescr()));
    frm.method("POST");

    frm.add("<br><center>");
    Input reload = new Input(Input.Submit, KEY_ACTION, ACTION_RELOAD_CONFIG);
    setTabOrder(reload);
    frm.add(reload);
    frm.add(" ");
    Input backup = new Input(Input.Submit, KEY_ACTION, ACTION_MAIL_BACKUP);
    setTabOrder(backup);
    frm.add(backup);
    frm.add(" ");
    Input crawlplug = new Input(Input.Submit, KEY_ACTION, ACTION_CRAWL_PLUGINS);
    setTabOrder(crawlplug);
    frm.add(crawlplug);
    frm.add("</center>");
    ServletDescr d1 = AdminServletManager.SERVLET_HASH_CUS;
    if (isServletRunnable(d1)) {
      frm.add("<br><center>" + srvLink(d1, d1.heading) + "</center>");
    }
    Input findUrl = new Input(Input.Submit, KEY_ACTION, ACTION_FIND_URL);
    Input findUrlText = new Input(Input.Text, KEY_URL);
    findUrlText.setSize(50);
    setTabOrder(findUrl);
    setTabOrder(findUrlText);
    frm.add("<br><center>" + findUrl + " " + findUrlText + "</center>");

    Input thrw = new Input(Input.Submit, KEY_ACTION, ACTION_THROW_IOEXCEPTION);
    Input thmsg = new Input(Input.Text, KEY_MSG);
    setTabOrder(thrw);
    setTabOrder(thmsg);
    frm.add("<br><center>" + thrw + " " + thmsg + "</center>");

    frm.add("<br><center>AU Actions: select AU</center>");
    Composite ausel = ServletUtil.layoutSelectAu(this, KEY_AUID, formAuid);
    frm.add("<br><center>" + ausel + "</center>");
    setTabOrder(ausel);

    Input v3Poll =
        new Input(
            Input.Submit,
            KEY_ACTION,
            (showForcePoll ? ACTION_FORCE_START_V3_POLL : ACTION_START_V3_POLL));
    Input crawl =
        new Input(
            Input.Submit,
            KEY_ACTION,
            (showForceCrawl ? ACTION_FORCE_START_CRAWL : ACTION_START_CRAWL));

    frm.add("<br><center>");
    frm.add(v3Poll);
    frm.add(" ");
    frm.add(crawl);
    if (CurrentConfig.getBooleanParam(PARAM_ENABLE_DEEP_CRAWL, DEFAULT_ENABLE_DEEP_CRAWL)) {
      Input deepCrawl =
          new Input(
              Input.Submit,
              KEY_ACTION,
              (showForceCrawl ? ACTION_FORCE_START_DEEP_CRAWL : ACTION_START_DEEP_CRAWL));
      Input depthText = new Input(Input.Text, KEY_REFETCH_DEPTH, formDepth);
      depthText.setSize(4);
      setTabOrder(depthText);
      frm.add(" ");
      frm.add(deepCrawl);
      frm.add(depthText);
    }
    Input checkSubstance = new Input(Input.Submit, KEY_ACTION, ACTION_CHECK_SUBSTANCE);
    frm.add("<br>");
    frm.add(checkSubstance);
    if (metadataMgr != null) {
      Input reindex =
          new Input(
              Input.Submit,
              KEY_ACTION,
              (showForceReindexMetadata ? ACTION_FORCE_REINDEX_METADATA : ACTION_REINDEX_METADATA));
      frm.add(" ");
      frm.add(reindex);
      Input disableIndexing = new Input(Input.Submit, KEY_ACTION, ACTION_DISABLE_METADATA_INDEXING);
      frm.add(" ");
      frm.add(disableIndexing);
    }
    frm.add("</center>");

    comp.add(frm);
    return comp;
  }
}
Exemple #9
0
/**
 * Class implementing the concept of the set of URLs covered by a poll.
 *
 * @author Claire Griffin
 * @version 1.0
 */
public class PollSpec {
  /**
   * A lower bound value which indicates the poll should use a {@link SingleNodeCachedUrlSetSpec}
   * instead of a {@link RangeCachedUrlSetSpec}.
   */
  public static final String SINGLE_NODE_LWRBOUND = ".";

  public static final String DEFAULT_PLUGIN_VERSION = "1";

  private static Logger theLog = Logger.getLogger("PollSpec");

  private String auId;
  private String pluginVersion;
  private String url;
  private String uprBound = null;
  private String lwrBound = null;
  private CachedUrlSet cus = null;
  private PluginManager pluginMgr = null;
  private int protocolVersion; // poll protocol version
  private int pollType; // One of the types defined by Poll
  private V3Poller.PollVariant variant = V3Poller.PollVariant.PoR;

  /**
   * Construct a PollSpec from a CachedUrlSet and an upper and lower bound
   *
   * @param cus the CachedUrlSet
   * @param lwrBound the lower boundary
   * @param uprBound the upper boundary
   * @param pollType one of the types defined by Poll
   */
  public PollSpec(CachedUrlSet cus, String lwrBound, String uprBound, int pollType) {
    commonSetup(cus, lwrBound, uprBound, pollType);
  }

  /**
   * Construct a PollSpec from a CachedUrlSet.
   *
   * @param cus the CachedUrlSpec which defines the range of interest
   * @param pollType one of the types defined by Poll
   */
  public PollSpec(CachedUrlSet cus, int pollType) {
    CachedUrlSetSpec cuss = cus.getSpec();
    if (cuss instanceof RangeCachedUrlSetSpec) {
      RangeCachedUrlSetSpec rcuss = (RangeCachedUrlSetSpec) cuss;
      commonSetup(cus, rcuss.getLowerBound(), rcuss.getUpperBound(), pollType);
    } else if (cuss.isSingleNode()) {
      commonSetup(cus, SINGLE_NODE_LWRBOUND, null, pollType);
    } else {
      commonSetup(cus, null, null, pollType);
    }
  }

  /**
   * Construct a PollSpec from a V1 LcapMessage
   *
   * @param msg the LcapMessage which defines the range of interest
   */
  public PollSpec(V1LcapMessage msg) {
    auId = msg.getArchivalId();
    pluginVersion = msg.getPluginVersion();
    url = msg.getTargetUrl();
    uprBound = msg.getUprBound();
    lwrBound = msg.getLwrBound();
    protocolVersion = msg.getProtocolVersion();
    if (msg.isContentPoll()) {
      pollType = Poll.V1_CONTENT_POLL;
    } else if (msg.isNamePoll()) {
      pollType = Poll.V1_NAME_POLL;
    } else if (msg.isVerifyPoll()) {
      pollType = Poll.V1_VERIFY_POLL;
    } else {
      pollType = -1;
    }
    cus = getPluginManager().findCachedUrlSet(this);
  }

  public PollSpec(V3LcapMessage msg) {
    this(
        msg.getArchivalId(),
        (msg.getTargetUrl() == null) ? "lockssau:" : msg.getTargetUrl(),
        null,
        null,
        Poll.V3_POLL);
    protocolVersion = msg.getProtocolVersion();
    pluginVersion = msg.getPluginVersion();
  }

  /** Construct a PollSpec from explicit args */
  public PollSpec(String auId, String url, String lower, String upper, int pollType) {
    this.auId = auId;
    this.url = url;
    uprBound = upper;
    lwrBound = lower;
    this.pollType = pollType;
    cus = getPluginManager().findCachedUrlSet(this);
    this.protocolVersion = protocolVersionFromPollType(pollType);
  }

  /**
   * Construct a PollSpec from another PollSpec and a poll type XXX it seems that other constructors
   * are not setting all fields
   */
  public PollSpec(PollSpec ps, int pollType) {
    this.auId = ps.auId;
    this.pluginVersion = ps.pluginVersion;
    this.url = ps.url;
    this.uprBound = ps.uprBound;
    this.lwrBound = ps.lwrBound;
    this.cus = ps.cus;
    this.pluginMgr = ps.pluginMgr;
    this.protocolVersion = ps.protocolVersion;
    this.pollType = pollType;
  }

  /** Setup common to most constructors */
  private void commonSetup(CachedUrlSet cus, String lwrBound, String uprBound, int pollType) {
    CachedUrlSetSpec cuss = cus.getSpec();
    if (cuss instanceof PrunedCachedUrlSetSpec) {
      throw new IllegalArgumentException("Polls do not support PrunedCachedUrlSetSpec");
    }
    this.cus = cus;
    ArchivalUnit au = cus.getArchivalUnit();
    auId = au.getAuId();
    this.pluginVersion = AuUtil.getPollVersion(au);
    url = cuss.getUrl();
    this.lwrBound = lwrBound;
    this.uprBound = uprBound;
    this.protocolVersion = protocolVersionFromPollType(pollType);
    this.pollType = pollType;
  }

  public CachedUrlSet getCachedUrlSet() {
    return cus;
  }

  public String getAuId() {
    return auId;
  }

  public String getPluginVersion() {
    return (pluginVersion != null) ? pluginVersion : DEFAULT_PLUGIN_VERSION;
  }

  public String getUrl() {
    return url;
  }

  public String getLwrBound() {
    return lwrBound;
  }

  public String getUprBound() {
    return uprBound;
  }

  public String getRangeString() {
    if (StringUtil.equalStrings(lwrBound, SINGLE_NODE_LWRBOUND)) {
      return "single node";
    }
    if (lwrBound != null || uprBound != null) {
      String lwrDisplay = lwrBound;
      String uprDisplay = uprBound;
      if (lwrBound != null && lwrBound.startsWith("/")) {
        lwrDisplay = lwrBound.substring(1);
      }
      if (uprBound != null && uprBound.startsWith("/")) {
        uprDisplay = uprBound.substring(1);
      }
      return lwrDisplay + " - " + uprDisplay;
    }
    return null;
  }

  public int getProtocolVersion() {
    return protocolVersion;
  }

  public int getPollType() {
    return pollType;
  }

  public V3Poller.PollVariant getPollVariant() {
    return variant;
  }

  public void setPollVariant(V3Poller.PollVariant v) {
    variant = v;
  }

  private PluginManager getPluginManager() {
    if (pluginMgr == null) {
      pluginMgr = (PluginManager) LockssDaemon.getManager(LockssDaemon.PLUGIN_MANAGER);
    }
    return pluginMgr;
  }

  /**
   * Given a poll type, return the correct version of the protocol to use.
   *
   * @param pollType
   * @return The protocol version to use
   */
  private int protocolVersionFromPollType(int pollType) {
    switch (pollType) {
      case Poll.V1_CONTENT_POLL:
      case Poll.V1_NAME_POLL:
      case Poll.V1_VERIFY_POLL:
        return Poll.V1_PROTOCOL;
      case Poll.V3_POLL:
        return Poll.V3_PROTOCOL;
      default:
        return Poll.UNDEFINED_PROTOCOL;
    }
  }

  public String toString() {
    return "[PS: "
        + Poll.POLL_NAME[pollType]
        + " auid="
        + auId
        + ", url="
        + url
        + ", l="
        + lwrBound
        + ", u="
        + uprBound
        + ", type="
        + pollType
        + ", plugVer="
        + getPluginVersion()
        + ", protocol="
        + protocolVersion
        + "]";
  }
}
/**
 * DefinablePlugin: a plugin which uses the data stored in an ExternalizableMap to configure itself.
 *
 * @author Claire Griffin
 * @version 1.0
 */
public class DefinablePlugin extends BasePlugin {
  // configuration map keys
  public static final String KEY_PLUGIN_IDENTIFIER = "plugin_identifier";
  public static final String KEY_PLUGIN_NAME = "plugin_name";
  public static final String KEY_PLUGIN_VERSION = "plugin_version";
  public static final String KEY_PLUGIN_FEATURE_VERSION_MAP = "plugin_feature_version_map";
  public static final String KEY_REQUIRED_DAEMON_VERSION = "required_daemon_version";
  public static final String KEY_PUBLISHING_PLATFORM = "plugin_publishing_platform";
  public static final String KEY_PLUGIN_CONFIG_PROPS = "plugin_config_props";
  public static final String KEY_EXCEPTION_HANDLER = "plugin_cache_result_handler";
  public static final String KEY_EXCEPTION_LIST = "plugin_cache_result_list";
  public static final String KEY_PLUGIN_NOTES = "plugin_notes";
  public static final String KEY_CRAWL_TYPE = "plugin_crawl_type";
  public static final String KEY_FOLLOW_LINKS = "plugin_follow_link";
  /** Message to be displayed when user configures an AU with this plugin */
  public static final String KEY_PLUGIN_AU_CONFIG_USER_MSG = "plugin_au_config_user_msg";

  public static final String KEY_PER_HOST_PERMISSION_PATH = "plugin_per_host_permission_path";
  public static final String KEY_PLUGIN_PARENT = "plugin_parent";
  public static final String KEY_PLUGIN_PARENT_VERSION = "plugin_parent_version";
  public static final String KEY_PLUGIN_CRAWL_URL_COMPARATOR_FACTORY =
      "plugin_crawl_url_comparator_factory";
  public static final String KEY_PLUGIN_FETCH_RATE_LIMITER_SOURCE =
      "plugin_fetch_rate_limiter_source";

  public static final String KEY_PLUGIN_ARTICLE_ITERATOR_FACTORY =
      "plugin_article_iterator_factory";

  public static final String KEY_PLUGIN_ARTICLE_METADATA_EXTRACTOR_FACTORY =
      "plugin_article_metadata_extractor_factory";

  public static final String KEY_DEFAULT_ARTICLE_MIME_TYPE = "plugin_default_article_mime_type";

  public static final String KEY_ARTICLE_ITERATOR_ROOT = "plugin_article_iterator_root";

  public static final String KEY_ARTICLE_ITERATOR_PATTERN = "plugin_article_iterator_pattern";

  public static final String DEFAULT_PLUGIN_VERSION = "1";
  public static final String DEFAULT_REQUIRED_DAEMON_VERSION = "0.0.0";

  public static final String MAP_SUFFIX = ".xml";

  public static final String CRAWL_TYPE_HTML_LINKS = "HTML Links";
  public static final String CRAWL_TYPE_OAI = "OAI";
  public static final String[] CRAWL_TYPES = {
    CRAWL_TYPE_HTML_LINKS, CRAWL_TYPE_OAI,
  };
  public static final String DEFAULT_CRAWL_TYPE = CRAWL_TYPE_HTML_LINKS;

  protected String mapName = null;

  static Logger log = Logger.getLogger("DefinablePlugin");

  protected ExternalizableMap definitionMap = new ExternalizableMap();
  protected CacheResultHandler resultHandler = null;
  protected List<String> loadedFromUrls;
  protected CrawlWindow crawlWindow;
  protected Map<Plugin.Feature, String> featureVersion;

  public void initPlugin(LockssDaemon daemon, String extMapName) throws FileNotFoundException {
    initPlugin(daemon, extMapName, this.getClass().getClassLoader());
  }

  public void initPlugin(LockssDaemon daemon, String extMapName, ClassLoader loader)
      throws FileNotFoundException {
    // convert the plugin class name to an xml file name
    // load the configuration map from jar file
    ExternalizableMap defMap = loadMap(extMapName, loader);
    this.initPlugin(daemon, extMapName, defMap, loader);
  }

  public void initPlugin(
      LockssDaemon daemon, String extMapName, ExternalizableMap defMap, ClassLoader loader) {
    mapName = extMapName;
    this.classLoader = loader;
    this.definitionMap = defMap;
    super.initPlugin(daemon);
    initMimeMap();
    initFeatureVersions();
    initAuFeatureMap();
    checkParamAgreement();
  }

  private ExternalizableMap loadMap(String extMapName, ClassLoader loader)
      throws FileNotFoundException {
    String first = null;
    String next = extMapName;
    List<String> urls = new ArrayList<String>();
    ExternalizableMap res = null;
    while (next != null) {
      // convert the plugin class name to an xml file name
      String mapFile = next.replace('.', '/') + MAP_SUFFIX;
      URL url = loader.getResource(mapFile);
      if (url != null && urls.contains(url.toString())) {
        throw new PluginException.InvalidDefinition("Plugin inheritance loop: " + next);
      }
      // load into map
      ExternalizableMap oneMap = new ExternalizableMap();
      oneMap.loadMapFromResource(mapFile, loader);
      urls.add(url.toString());
      // apply overrides one plugin at a time in inheritance chain
      processOverrides(oneMap);
      if (res == null) {
        res = oneMap;
      } else {
        for (Map.Entry ent : oneMap.entrySet()) {
          String key = (String) ent.getKey();
          Object val = ent.getValue();
          if (!res.containsKey(key)) {
            res.setMapElement(key, val);
          }
        }
      }
      if (oneMap.containsKey(KEY_PLUGIN_PARENT)) {
        next = oneMap.getString(KEY_PLUGIN_PARENT);
      } else {
        next = null;
      }
    }
    loadedFromUrls = urls;
    return res;
  }

  /** If in testing mode FOO, copy values from FOO_override map, if any, to main map */
  void processOverrides(TypedEntryMap map) {
    String testMode = getTestingMode();
    if (StringUtil.isNullString(testMode)) {
      return;
    }
    Object o = map.getMapElement(testMode + DefinableArchivalUnit.SUFFIX_OVERRIDE);
    if (o == null) {
      return;
    }
    if (o instanceof Map) {
      Map overrideMap = (Map) o;
      for (Map.Entry entry : (Set<Map.Entry>) overrideMap.entrySet()) {
        String key = (String) entry.getKey();
        Object val = entry.getValue();
        log.debug(getDefaultPluginName() + ": Overriding " + key + " with " + val);
        map.setMapElement(key, val);
      }
    }
  }

  String getTestingMode() {
    return theDaemon == null ? null : theDaemon.getTestingMode();
  }

  // Used by tests

  public void initPlugin(LockssDaemon daemon, File file) throws PluginException {
    ExternalizableMap oneMap = new ExternalizableMap();
    oneMap.loadMap(file);
    if (oneMap.getErrorString() != null) {
      throw new PluginException(oneMap.getErrorString());
    }
    initPlugin(daemon, file.getPath(), oneMap, null);
  }

  void initPlugin(LockssDaemon daemon, ExternalizableMap defMap) {
    initPlugin(daemon, defMap, this.getClass().getClassLoader());
  }

  void initPlugin(LockssDaemon daemon, ExternalizableMap defMap, ClassLoader loader) {
    initPlugin(daemon, "Internal", defMap, loader);
  }

  enum PrintfContext {
    Regexp,
    URL,
    Display
  };

  void checkParamAgreement() {
    for (Map.Entry<String, PrintfContext> ent :
        DefinableArchivalUnit.printfKeysContext.entrySet()) {
      checkParamAgreement(ent.getKey(), ent.getValue());
    }
  }

  void checkParamAgreement(String key, PrintfContext context) {
    List<String> printfList = getElementList(key);
    if (printfList == null) {
      return;
    }
    for (String printf : printfList) {
      if (StringUtil.isNullString(printf)) {
        log.warning("Null printf string in " + key);
        continue;
      }
      PrintfUtil.PrintfData p_data = PrintfUtil.stringToPrintf(printf);
      Collection<String> p_args = p_data.getArguments();
      for (String arg : p_args) {
        ConfigParamDescr descr = findAuConfigDescr(arg);
        if (descr == null) {
          throw new PluginException.InvalidDefinition(
              "Not a declared parameter: " + arg + " in " + printf + " in " + getPluginName());
        }
        // ensure range and set params used only in legal context
        switch (context) {
          case Regexp:
          case Display:
            // everything is legal in a regexp or a display string
            break;
          case URL:
            // NUM_RANGE and SET legal because can enumerate.  Can't
            // enumerate RANGE
            switch (descr.getType()) {
              case ConfigParamDescr.TYPE_RANGE:
                throw new PluginException.InvalidDefinition(
                    "Range parameter ("
                        + arg
                        + ") used in illegal context in "
                        + getPluginName()
                        + ": "
                        + key
                        + ": "
                        + printf);
              default:
            }
        }
      }
    }
  }

  public List<String> getLoadedFromUrls() {
    return loadedFromUrls;
  }

  public String getPluginName() {
    if (definitionMap.containsKey(KEY_PLUGIN_NAME)) {
      return definitionMap.getString(KEY_PLUGIN_NAME);
    } else {
      return getDefaultPluginName();
    }
  }

  protected String getDefaultPluginName() {
    return StringUtil.shortName(getPluginId());
  }

  public String getVersion() {
    return definitionMap.getString(KEY_PLUGIN_VERSION, DEFAULT_PLUGIN_VERSION);
  }

  public String getFeatureVersion(Plugin.Feature feat) {
    if (featureVersion == null) {
      return null;
    }
    return featureVersion.get(feat);
  }

  public String getRequiredDaemonVersion() {
    return definitionMap.getString(KEY_REQUIRED_DAEMON_VERSION, DEFAULT_REQUIRED_DAEMON_VERSION);
  }

  public String getPublishingPlatform() {
    return definitionMap.getString(KEY_PUBLISHING_PLATFORM, null);
  }

  public String getPluginNotes() {
    return definitionMap.getString(KEY_PLUGIN_NOTES, null);
  }

  public String getDefaultArticleMimeType() {
    String ret = definitionMap.getString(KEY_DEFAULT_ARTICLE_MIME_TYPE, null);
    log.debug3("DefaultArticleMimeType " + ret);
    if (ret == null) {
      ret = super.getDefaultArticleMimeType();
      log.debug3("DefaultArticleMimeType from super " + ret);
    }
    return ret;
  }

  public List<String> getElementList(String key) {
    Object element = definitionMap.getMapElement(key);
    List<String> lst;

    if (element instanceof String) {
      return Collections.singletonList((String) element);
    } else if (element instanceof List) {
      return (List) element;
    } else {
      return null;
    }
  }

  public List getLocalAuConfigDescrs() throws PluginException.InvalidDefinition {
    List auConfigDescrs = (List) definitionMap.getCollection(KEY_PLUGIN_CONFIG_PROPS, null);
    if (auConfigDescrs == null) {
      throw new PluginException.InvalidDefinition(mapName + " missing ConfigParamDescrs");
    }
    return auConfigDescrs;
  }

  protected ArchivalUnit createAu0(Configuration auConfig)
      throws ArchivalUnit.ConfigurationException {
    DefinableArchivalUnit au = new DefinableArchivalUnit(this, definitionMap);
    au.setConfiguration(auConfig);
    return au;
  }

  public ExternalizableMap getDefinitionMap() {
    return definitionMap;
  }

  CacheResultHandler getCacheResultHandler() {
    return resultHandler;
  }

  String stripSuffix(String str, String suffix) {
    return str.substring(0, str.length() - suffix.length());
  }

  protected void initMimeMap() throws PluginException.InvalidDefinition {
    for (Iterator iter = definitionMap.entrySet().iterator(); iter.hasNext(); ) {
      Map.Entry ent = (Map.Entry) iter.next();
      String key = (String) ent.getKey();
      Object val = ent.getValue();
      if (key.endsWith(DefinableArchivalUnit.SUFFIX_LINK_EXTRACTOR_FACTORY)) {
        String mime = stripSuffix(key, DefinableArchivalUnit.SUFFIX_LINK_EXTRACTOR_FACTORY);
        if (val instanceof String) {
          String factName = (String) val;
          log.debug(mime + " link extractor: " + factName);
          MimeTypeInfo.Mutable mti = mimeMap.modifyMimeTypeInfo(mime);
          LinkExtractorFactory fact =
              (LinkExtractorFactory) newAuxClass(factName, LinkExtractorFactory.class);
          mti.setLinkExtractorFactory(fact);
        }
      } else if (key.endsWith(DefinableArchivalUnit.SUFFIX_CRAWL_FILTER_FACTORY)) {
        // XXX This clause must precede the one for SUFFIX_HASH_FILTER_FACTORY
        // XXX unless/until that key is changed to not be a terminal substring
        // XXX of this one
        String mime = stripSuffix(key, DefinableArchivalUnit.SUFFIX_CRAWL_FILTER_FACTORY);
        if (val instanceof String) {
          String factName = (String) val;
          log.debug(mime + " crawl filter: " + factName);
          MimeTypeInfo.Mutable mti = mimeMap.modifyMimeTypeInfo(mime);
          FilterFactory fact = (FilterFactory) newAuxClass(factName, FilterFactory.class);
          mti.setCrawlFilterFactory(fact);
        }
      } else if (key.endsWith(DefinableArchivalUnit.SUFFIX_HASH_FILTER_FACTORY)) {
        String mime = stripSuffix(key, DefinableArchivalUnit.SUFFIX_HASH_FILTER_FACTORY);
        if (val instanceof String) {
          String factName = (String) val;
          log.debug(mime + " filter: " + factName);
          MimeTypeInfo.Mutable mti = mimeMap.modifyMimeTypeInfo(mime);
          FilterFactory fact = (FilterFactory) newAuxClass(factName, FilterFactory.class);
          mti.setHashFilterFactory(fact);
        }
      } else if (key.endsWith(DefinableArchivalUnit.SUFFIX_FETCH_RATE_LIMIT)) {
        String mime = stripSuffix(key, DefinableArchivalUnit.SUFFIX_FETCH_RATE_LIMIT);
        if (val instanceof String) {
          String rate = (String) val;
          log.debug(mime + " fetch rate: " + rate);
          MimeTypeInfo.Mutable mti = mimeMap.modifyMimeTypeInfo(mime);
          RateLimiter limit = mti.getFetchRateLimiter();
          if (limit != null) {
            limit.setRate(rate);
          } else {
            mti.setFetchRateLimiter(new RateLimiter(rate));
          }
        }
      } else if (key.endsWith(DefinableArchivalUnit.SUFFIX_LINK_REWRITER_FACTORY)) {
        String mime = stripSuffix(key, DefinableArchivalUnit.SUFFIX_LINK_REWRITER_FACTORY);
        String factName = (String) val;
        log.debug(mime + " link rewriter: " + factName);
        MimeTypeInfo.Mutable mti = mimeMap.modifyMimeTypeInfo(mime);
        LinkRewriterFactory fact =
            (LinkRewriterFactory) newAuxClass(factName, LinkRewriterFactory.class);
        mti.setLinkRewriterFactory(fact);
      } else if (key.endsWith(DefinableArchivalUnit.SUFFIX_METADATA_EXTRACTOR_FACTORY_MAP)) {
        String mime = stripSuffix(key, DefinableArchivalUnit.SUFFIX_METADATA_EXTRACTOR_FACTORY_MAP);
        Map factNameMap = (Map) val;
        Map factClassMap = new HashMap();
        MimeTypeInfo.Mutable mti = mimeMap.modifyMimeTypeInfo(mime);
        for (Iterator it = factNameMap.keySet().iterator(); it.hasNext(); ) {
          String mdTypes = (String) it.next();
          String factName = (String) factNameMap.get(mdTypes);
          log.debug(mime + " (" + mdTypes + ") metadata extractor: " + factName);
          for (String mdType : (List<String>) StringUtil.breakAt(mdTypes, ";")) {
            setMdTypeFact(factClassMap, mdType, factName);
          }
        }
        mti.setFileMetadataExtractorFactoryMap(factClassMap);
      }
    }
  }

  private void setMdTypeFact(Map factClassMap, String mdType, String factName) {
    log.debug3("Metadata type: " + mdType + " factory " + factName);
    FileMetadataExtractorFactory fact =
        (FileMetadataExtractorFactory) newAuxClass(factName, FileMetadataExtractorFactory.class);
    factClassMap.put(mdType, fact);
  }

  protected void initResultMap() throws PluginException.InvalidDefinition {
    HttpResultMap hResultMap = new HttpResultMap();
    // XXX Currently this only allows a CacheResultHandler class to
    // initialize the result map.  Instead, don't use a CacheResultMap
    // directly, use either the plugin's CacheResultHandler, if specified,
    // or a default one that wraps the CacheResultMap

    String handler_class = null;
    handler_class = definitionMap.getString(KEY_EXCEPTION_HANDLER, null);
    if (handler_class != null) {
      try {
        resultHandler = (CacheResultHandler) newAuxClass(handler_class, CacheResultHandler.class);
        resultHandler.init(hResultMap);
      } catch (Exception ex) {
        throw new PluginException.InvalidDefinition(
            mapName + " has invalid Exception handler: " + handler_class, ex);
      } catch (LinkageError le) {
        throw new PluginException.InvalidDefinition(
            mapName + " has invalid Exception handler: " + handler_class, le);
      }
    } else {
      // Expect a list of mappings from either result code or exception
      // name to CacheException name
      Collection<String> mappings = definitionMap.getCollection(KEY_EXCEPTION_LIST, null);
      if (mappings != null) {
        // add each entry
        for (String entry : mappings) {
          if (log.isDebug2()) {
            log.debug2("initMap(" + entry + ")");
          }
          String first;
          String ceName;
          try {
            List<String> pair = StringUtil.breakAt(entry, '=', 2, true, true);
            first = pair.get(0);
            ceName = pair.get(1);
          } catch (Exception ex) {
            throw new PluginException.InvalidDefinition(
                "Invalid syntax: " + entry + "in " + mapName);
          }
          Object val;

          // Value should be either a CacheException or CacheResultHandler
          // class name.
          PluginFetchEventResponse resp =
              (PluginFetchEventResponse) newAuxClass(ceName, PluginFetchEventResponse.class, null);
          if (resp instanceof CacheException) {
            val = resp.getClass();
          } else if (resp instanceof CacheResultHandler) {
            val = WrapperUtil.wrap((CacheResultHandler) resp, CacheResultHandler.class);
          } else {
            throw new PluginException.InvalidDefinition(
                "Second arg not a "
                    + "CacheException or "
                    + "CacheResultHandler class: "
                    + entry
                    + ", in "
                    + mapName);
          }
          try {
            int code = Integer.parseInt(first);
            // If parseable as an integer, it's a result code.
            hResultMap.storeMapEntry(code, val);
          } catch (NumberFormatException e) {
            try {
              Class eClass = Class.forName(first);
              // If a class name, it should be an exception class
              if (Exception.class.isAssignableFrom(eClass)) {
                hResultMap.storeMapEntry(eClass, val);
              } else {
                throw new PluginException.InvalidDefinition(
                    "First arg not an " + "Exception class: " + entry + ", in " + mapName);
              }
            } catch (Exception ex) {
              throw new PluginException.InvalidDefinition(
                  "First arg not a " + "number or class: " + entry + ", in " + mapName);
            } catch (LinkageError le) {
              throw new PluginException.InvalidDefinition("Can't load " + first, le);
            }
          }
        }
      }
    }
    resultMap = hResultMap;
  }

  protected void initFeatureVersions() throws PluginException.InvalidDefinition {
    if (definitionMap.containsKey(KEY_PLUGIN_FEATURE_VERSION_MAP)) {
      Map<Plugin.Feature, String> map = new HashMap<Plugin.Feature, String>();
      Map<String, String> spec =
          (Map<String, String>) definitionMap.getMap(KEY_PLUGIN_FEATURE_VERSION_MAP);
      log.debug2("features: " + spec);
      for (Map.Entry<String, String> ent : spec.entrySet()) {
        try {
          // Prefix version string with feature name to create separate
          // namespace for each feature
          String key = ent.getKey();
          map.put(Plugin.Feature.valueOf(key), key + "_" + ent.getValue());
        } catch (RuntimeException e) {
          log.warning(
              getPluginName()
                  + " set unknown feature: "
                  + ent.getKey()
                  + " to version "
                  + ent.getValue(),
              e);
          throw new PluginException.InvalidDefinition("Unknown feature: " + ent.getKey(), e);
        }
      }
      featureVersion = map;
    } else {
      featureVersion = null;
    }
  }

  protected void initAuFeatureMap() {
    if (definitionMap.containsKey(DefinableArchivalUnit.KEY_AU_FEATURE_URL_MAP)) {
      Map<String, ?> featMap = definitionMap.getMap(DefinableArchivalUnit.KEY_AU_FEATURE_URL_MAP);
      for (Map.Entry ent : featMap.entrySet()) {
        Object val = ent.getValue();
        if (val instanceof Map) {
          ent.setValue(MapUtil.expandAlternativeKeyLists((Map) val));
        }
      }
    }
  }

  /** Create a CrawlWindow if necessary and return it. The CrawlWindow must be thread-safe. */
  protected CrawlWindow makeCrawlWindow() {
    if (crawlWindow != null) {
      return crawlWindow;
    }
    CrawlWindow window =
        (CrawlWindow) definitionMap.getMapElement(DefinableArchivalUnit.KEY_AU_CRAWL_WINDOW_SER);
    if (window == null) {
      String window_class =
          definitionMap.getString(DefinableArchivalUnit.KEY_AU_CRAWL_WINDOW, null);
      if (window_class != null) {
        ConfigurableCrawlWindow ccw =
            (ConfigurableCrawlWindow) newAuxClass(window_class, ConfigurableCrawlWindow.class);
        try {
          window = ccw.makeCrawlWindow();
        } catch (PluginException e) {
          throw new RuntimeException(e);
        }
      }
    }
    crawlWindow = window;
    return window;
  }

  LoginPageChecker loginChecker;

  protected LoginPageChecker makeLoginPageChecker() {
    if (loginChecker == null) {
      String loginPageCheckerClass =
          definitionMap.getString(DefinableArchivalUnit.KEY_AU_LOGIN_PAGE_CHECKER, null);
      if (loginPageCheckerClass != null) {
        loginChecker =
            (LoginPageChecker) newAuxClass(loginPageCheckerClass, LoginPageChecker.class);
      }
    }
    return loginChecker;
  }

  PermissionCheckerFactory permissionCheckerFact;

  protected PermissionCheckerFactory getPermissionCheckerFactory() {
    if (permissionCheckerFact == null) {
      String permissionCheckerFactoryClass =
          definitionMap.getString(DefinableArchivalUnit.KEY_AU_PERMISSION_CHECKER_FACTORY, null);
      if (permissionCheckerFactoryClass != null) {
        permissionCheckerFact =
            (PermissionCheckerFactory)
                newAuxClass(permissionCheckerFactoryClass, PermissionCheckerFactory.class);
        log.debug2("Loaded PermissionCheckerFactory: " + permissionCheckerFact);
      }
    }
    return permissionCheckerFact;
  }

  protected UrlNormalizer urlNorm;

  protected UrlNormalizer getUrlNormalizer() {
    if (urlNorm == null) {
      String normalizerClass =
          definitionMap.getString(DefinableArchivalUnit.KEY_AU_URL_NORMALIZER, null);
      if (normalizerClass != null) {
        urlNorm = (UrlNormalizer) newAuxClass(normalizerClass, UrlNormalizer.class);
      } else {
        urlNorm = NullUrlNormalizer.INSTANCE;
      }
    }
    return urlNorm;
  }

  protected ExploderHelper exploderHelper = null;

  protected ExploderHelper getExploderHelper() {
    if (exploderHelper == null) {
      String helperClass =
          definitionMap.getString(DefinableArchivalUnit.KEY_AU_EXPLODER_HELPER, null);
      if (helperClass != null) {
        exploderHelper = (ExploderHelper) newAuxClass(helperClass, ExploderHelper.class);
      }
    }
    return exploderHelper;
  }

  protected CrawlUrlComparatorFactory crawlUrlComparatorFactory = null;

  protected CrawlUrlComparatorFactory getCrawlUrlComparatorFactory() {
    if (crawlUrlComparatorFactory == null) {
      String factClass =
          definitionMap.getString(DefinablePlugin.KEY_PLUGIN_CRAWL_URL_COMPARATOR_FACTORY, null);
      if (factClass != null) {
        crawlUrlComparatorFactory =
            (CrawlUrlComparatorFactory) newAuxClass(factClass, CrawlUrlComparatorFactory.class);
      }
    }
    return crawlUrlComparatorFactory;
  }

  protected Comparator<CrawlUrl> getCrawlUrlComparator(ArchivalUnit au)
      throws PluginException.LinkageError {
    CrawlUrlComparatorFactory fact = getCrawlUrlComparatorFactory();
    if (fact == null) {
      return null;
    }
    return fact.createCrawlUrlComparator(au);
  }

  protected FilterRule constructFilterRule(String contentType) {
    String mimeType = HeaderUtil.getMimeTypeFromContentType(contentType);

    Object filter_el =
        definitionMap.getMapElement(mimeType + DefinableArchivalUnit.SUFFIX_FILTER_RULE);

    if (filter_el instanceof String) {
      log.debug("Loading filter " + filter_el);
      return (FilterRule) newAuxClass((String) filter_el, FilterRule.class);
    } else if (filter_el instanceof List) {
      if (((List) filter_el).size() > 0) {
        return new DefinableFilterRule((List) filter_el);
      }
    }
    return super.constructFilterRule(mimeType);
  }

  protected ArticleIteratorFactory articleIteratorFact = null;
  protected ArticleMetadataExtractorFactory articleMetadataFact = null;

  /**
   * Returns the plugin's article iterator factory, if any
   *
   * @return the ArticleIteratorFactory
   */
  public ArticleIteratorFactory getArticleIteratorFactory() {
    if (articleIteratorFact == null) {
      String factClass = definitionMap.getString(KEY_PLUGIN_ARTICLE_ITERATOR_FACTORY, null);
      if (factClass != null) {
        articleIteratorFact =
            (ArticleIteratorFactory) newAuxClass(factClass, ArticleIteratorFactory.class);
      }
    }
    return articleIteratorFact;
  }

  /**
   * Returns the article iterator factory for the content type, if any
   *
   * @param contentType the content type
   * @return the ArticleIteratorFactory
   */
  public ArticleMetadataExtractorFactory getArticleMetadataExtractorFactory(MetadataTarget target) {
    if (articleMetadataFact == null) {
      String factClass =
          definitionMap.getString(KEY_PLUGIN_ARTICLE_METADATA_EXTRACTOR_FACTORY, null);
      if (factClass != null) {
        articleMetadataFact =
            (ArticleMetadataExtractorFactory)
                newAuxClass(factClass, ArticleMetadataExtractorFactory.class);
      }
    }
    return articleMetadataFact;
  }

  public String getPluginId() {
    String className;
    if (mapName != null) {
      className = mapName;
    } else {
      // @TODO: eliminate this when we eliminate subclasses
      className = this.getClass().getName();
    }
    return className;
  }
}
/**
 * LockssRepository is used to organize the urls being cached. It keeps a memory cache of the most
 * recently used nodes as a least-recently-used map, and also caches weak references to the
 * instances as they're doled out. This ensures that two instances of the same node are never
 * created, as the weak references only disappear when the object is finalized (they go to null when
 * the last hard reference is gone, then are removed from the cache on finalize()).
 */
public class LockssRepositoryImpl extends BaseLockssDaemonManager implements LockssRepository {
  private static Logger logger = Logger.getLogger("LockssRepository");

  /** Configuration parameter name for Lockss cache location. */
  public static final String PARAM_CACHE_LOCATION = Configuration.PREFIX + "cache.location";

  /** Restores pre repo rescanning bugfix behavior (4050) */
  public static final String PARAM_CLEAR_DIR_MAP = RepositoryManager.PREFIX + "clearDirMapOnAuStop";

  public static final boolean DEFAULT_CLEAR_DIR_MAP = false;

  /** Name of top directory in which the urls are cached. */
  public static final String CACHE_ROOT_NAME = "cache";

  // XXX This is a remnant from the single-disk days, and should go away.
  // It is used only by unit tests, which want it set to the (last)
  // individual repository dir created.
  private static String staticCacheLocation = null;

  // Maps local repository name (disk path) to LocalRepository instance
  static Map localRepositories = new HashMap();

  // starts with a '#' so no possibility of clashing with a URL
  public static final String AU_ID_FILE = "#au_id_file";
  static final String AU_ID_PROP = "au.id";
  static final String PLUGIN_ID_PROP = "plugin.id";
  static final char ESCAPE_CHAR = '#';
  static final String ESCAPE_STR = "#";
  static final char ENCODED_SEPARATOR_CHAR = 's';
  static final String INITIAL_PLUGIN_DIR = String.valueOf((char) ('a' - 1));
  static String lastPluginDir = INITIAL_PLUGIN_DIR;
  // PJG: Windows prohibits use of ':' in file name -- replace with '~' for development
  static final String PORT_SEPARATOR = SystemUtils.IS_OS_WINDOWS ? "%" : ":";

  // this contains a '#' so that it's not defeatable by strings which
  // match the prefix in a url (like '../tmp/')
  private static final String TEST_PREFIX = "/#tmp";

  private RepositoryManager repoMgr;
  private String rootLocation;
  UniqueRefLruCache nodeCache;
  private boolean isGlobalNodeCache = RepositoryManager.DEFAULT_GLOBAL_CACHE_ENABLED;

  LockssRepositoryImpl(String rootPath) {
    if (rootPath.endsWith(File.separator)) {
      rootLocation = rootPath;
    } else {
      // shouldn't happen
      StringBuilder sb = new StringBuilder(rootPath.length() + File.separator.length());
      sb.append(rootPath);
      sb.append(File.separator);
      rootLocation = sb.toString();
    }
    // Test code still needs this.
    nodeCache = new UniqueRefLruCache(RepositoryManager.DEFAULT_MAX_PER_AU_CACHE_SIZE);
    rootLocation =
        rootLocation
            .replace("?", "")
            .replace("COM8", "COMEIGHT")
            .replace("%5c", "/"); // //windows folder structure fix
  }

  public void startService() {
    super.startService();
    repoMgr = getDaemon().getRepositoryManager();
    isGlobalNodeCache = repoMgr.isGlobalNodeCache();
    if (isGlobalNodeCache) {
      nodeCache = repoMgr.getGlobalNodeCache();
    } else {
      //       nodeCache =
      // 	new UniqueRefLruCache(repoMgr.paramNodeCacheSize);
      setNodeCacheSize(repoMgr.paramNodeCacheSize);
    }
  }

  public void stopService() {
    // mainly important in testing to blank this
    lastPluginDir = INITIAL_PLUGIN_DIR;
    if (CurrentConfig.getBooleanParam(PARAM_CLEAR_DIR_MAP, DEFAULT_CLEAR_DIR_MAP)) {
      // This should be in RepositoryManager.stopService()
      localRepositories = new HashMap();
    }
    super.stopService();
  }

  public void setNodeCacheSize(int size) {
    if (nodeCache != null && !isGlobalNodeCache && nodeCache.getMaxSize() != size) {
      nodeCache.setMaxSize(size);
    }
  }

  /**
   * Called between initService() and startService(), then whenever the AU's config changes.
   *
   * @param auConfig the new configuration
   */
  public void setAuConfig(Configuration auConfig) {}

  void queueSizeCalc(RepositoryNode node) {
    repoMgr.queueSizeCalc(node);
  }

  public RepositoryNode getNode(String url) throws MalformedURLException {
    return getNode(url, false);
  }

  public RepositoryNode createNewNode(String url) throws MalformedURLException {
    return getNode(url, true);
  }

  public void deleteNode(String url) throws MalformedURLException {
    RepositoryNode node = getNode(url, false);
    if (node != null) {
      node.markAsDeleted();
    }
  }

  public void deactivateNode(String url) throws MalformedURLException {
    RepositoryNode node = getNode(url, false);
    if (node != null) {
      node.deactivateContent();
    }
  }

  /**
   * This function returns a RepositoryNode with a canonicalized path.
   *
   * @param url the url in String form
   * @param create true iff the node should be created if absent
   * @return RepositoryNode the node
   * @throws MalformedURLException
   */
  private synchronized RepositoryNode getNode(String url, boolean create)
      throws MalformedURLException {
    String canonUrl;
    boolean isAuUrl = false;
    if (AuUrl.isAuUrl(url)) {
      // path information is lost here, but is unimportant if it's an AuUrl
      canonUrl = AuUrl.PROTOCOL;
      isAuUrl = true;
    } else {
      // create a canonical path, handling all illegal path traversal
      canonUrl = canonicalizePath(url);
    }

    // check LRUMap cache for node
    RepositoryNode node = (RepositoryNode) nodeCache.get(nodeCacheKey(canonUrl));
    if (node != null) {
      return node;
    }

    String nodeLocation;
    if (isAuUrl) {
      // base directory of ArchivalUnit
      nodeLocation = rootLocation;
      node = new AuNodeImpl(canonUrl, nodeLocation, this);
    } else {
      // determine proper node location
      nodeLocation =
          LockssRepositoryImpl.mapUrlToFileLocation(rootLocation, canonUrl)
              .replace("?", "")
              .replace("COM8", "COMEIGHT")
              .replace("%5c", "/"); // //windows folder structure fix
      node = new RepositoryNodeImpl(canonUrl, nodeLocation, this);
    }

    if (!create) {
      // if not creating, check for existence
      File nodeDir = new File(nodeLocation);
      if (!nodeDir.exists()) {
        // return null if the node doesn't exist and shouldn't be created
        return null;
      }
      if (!nodeDir.isDirectory()) {
        logger.error("Cache file not a directory: " + nodeLocation);
        throw new LockssRepository.RepositoryStateException("Invalid cache file.");
      }
    }

    // add to node cache
    nodeCache.put(nodeCacheKey(canonUrl), node);
    return node;
  }

  Object nodeCacheKey(String canonUrl) {
    if (isGlobalNodeCache) {
      return new KeyPair(this, canonUrl);
    }
    return canonUrl;
  }

  // functions for testing
  int getCacheHits() {
    return nodeCache.getCacheHits();
  }

  int getCacheMisses() {
    return nodeCache.getCacheMisses();
  }

  int getRefHits() {
    return nodeCache.getRefHits();
  }

  int getRefMisses() {
    return nodeCache.getRefMisses();
  }

  public void nodeConsistencyCheck() {
    // traverse the node tree from the top
    RepositoryNode topNode;
    try {
      topNode = getNode(AuUrl.PROTOCOL_COLON);
      recurseConsistencyCheck((RepositoryNodeImpl) topNode);
    } catch (MalformedURLException ignore) {
    }
  }

  /**
   * Checks the consistency of the node, and continues with its children if it's consistent.
   *
   * @param node RepositoryNodeImpl the node to check
   */
  private void recurseConsistencyCheck(RepositoryNodeImpl node) {
    logger.debug2("Checking node '" + node.getNodeUrl() + "'...");
    // check consistency at each node
    // correct/deactivate as necessary
    // 'checkNodeConsistency()' will repair if possible
    if (node.checkNodeConsistency()) {
      logger.debug3("Node consistent; recursing on children...");
      List children = node.getNodeList(null, false);
      Iterator iter = children.iterator();
      while (iter.hasNext()) {
        RepositoryNodeImpl child = (RepositoryNodeImpl) iter.next();
        recurseConsistencyCheck(child);
      }
    } else {
      logger.debug3("Node inconsistent; deactivating...");
      deactivateInconsistentNode(node);
    }
  }

  /**
   * This is called when a node is in an inconsistent state. It simply creates some necessary
   * directories and deactivates the node. Future polls should restore it properly.
   *
   * @param node the inconsistent node
   */
  void deactivateInconsistentNode(RepositoryNodeImpl node) {
    logger.warning("Deactivating inconsistent node.");
    FileUtil.ensureDirExists(node.contentDir);
    // manually deactivate
    node.deactivateContent();
  }

  /**
   * A method to remove any non-canonical '..' or '.' elements in the path, as well as protecting
   * against illegal path traversal.
   *
   * @param url the raw url
   * @return String the canonicalized url
   * @throws MalformedURLException
   */
  public String canonicalizePath(String url) throws MalformedURLException {
    String canonUrl = UrlUtil.normalizeUrl(url, UrlUtil.PATH_TRAVERSAL_ACTION_THROW);
    // canonicalize "dir" and "dir/"
    // XXX if these are ever two separate nodes, this is wrong
    if (canonUrl.endsWith(UrlUtil.URL_PATH_SEPARATOR)) {
      canonUrl = canonUrl.substring(0, canonUrl.length() - 1);
    }

    return canonUrl;
  }

  // static calls

  /**
   * Factory method to create new LockssRepository instances.
   *
   * @param au the {@link ArchivalUnit}
   * @return the new LockssRepository instance
   */
  public static LockssRepository createNewLockssRepository(ArchivalUnit au) {
    String root = getRepositoryRoot(au);
    if (root == null || root.equals("null")) {
      logger.error("No repository dir set in config");
      throw new LockssRepository.RepositoryStateException("No repository dir set in config");
    }
    String auDir = LockssRepositoryImpl.mapAuToFileLocation(root, au);
    if (logger.isDebug2()) {
      logger.debug2("repo: " + auDir + ", au: " + au.getName());
    }
    staticCacheLocation = extendCacheLocation(root);
    LockssRepositoryImpl repo = new LockssRepositoryImpl(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;
  }

  public static String getRepositorySpec(ArchivalUnit au) {
    Configuration auConfig = au.getConfiguration();
    if (auConfig != null) { // can be null in unit tests
      String repoSpec = auConfig.get(PluginManager.AU_PARAM_REPOSITORY);
      if (repoSpec != null && repoSpec.startsWith("local:")) {
        return repoSpec;
      }
    }
    return "local:" + CurrentConfig.getParam(PARAM_CACHE_LOCATION);
  }

  public static String getRepositoryRoot(ArchivalUnit au) {
    return getLocalRepositoryPath(getRepositorySpec(au));
  }

  public static String getLocalRepositoryPath(String repoSpec) {
    if (repoSpec != null) {
      if (repoSpec.startsWith("local:")) {
        return repoSpec.substring(6);
      }
    }
    return null;
  }

  // The OpenBSD platform has renamed the first disk from /cache to
  // /cache.wd0, leaving behind a symbolic link in /cache .  This is
  // transparent everywhere except the repository status table, which needs
  // to match AU configs with AUs it finds when enumerating the repository.
  // Existing AU configs have repository=local:/cache, so the relative link
  // needs to be resolved to detect that that's the same as
  // local:/cache.wd0

  private static Map canonicalRoots = new HashMap();

  public static boolean isDirInRepository(String dir, String repoRoot) {
    if (dir.startsWith(repoRoot)) {
      return true;
    }
    return canonRoot(dir).startsWith(canonRoot(repoRoot));
  }

  static String canonRoot(String root) {
    synchronized (canonicalRoots) {
      String canon = (String) canonicalRoots.get(root);
      if (canon == null) {
        try {
          canon = new File(root).getCanonicalPath();
          canonicalRoots.put(root, canon);
        } catch (IOException e) {
          logger.warning("Can't canonicalize: " + root, e);
          return root;
        }
      }
      return canon;
    }
  }

  static String getCacheLocation() {
    return staticCacheLocation;
  }

  /**
   * Adds the 'cache' directory to the HD location.
   *
   * @param cacheDir the root location.
   * @return String the extended location
   */
  static String extendCacheLocation(String cacheDir) {
    StringBuilder buffer = new StringBuilder(cacheDir);
    if (!cacheDir.endsWith(File.separator)) {
      buffer.append(File.separator);
    }
    buffer.append(CACHE_ROOT_NAME);
    buffer.append(File.separator);
    return buffer.toString();
  }

  /**
   * mapAuToFileLocation() is the method used to resolve {@link ArchivalUnit}s into directory names.
   * This maps a given au to directories, using the cache root as the base. Given an au with
   * PluginId of 'plugin' and AuId of 'au', it would return the string '<rootLocation>/plugin/au/'.
   *
   * @param repoRoot the root of a LOCKSS repository
   * @param au the ArchivalUnit to resolve
   * @return the directory location
   */
  public static String mapAuToFileLocation(String repoRoot, ArchivalUnit au) {
    return getAuDir(au, repoRoot, true);
  }

  /**
   * mapUrlToFileLocation() is the method used to resolve urls into file names. This maps a given
   * url to a file location, using the au top directory as the base. It creates directories which
   * mirror the html string, so 'http://www.journal.org/issue1/index.html' would be cached in the
   * file: <rootLocation>/www.journal.org/http/issue1/index.html
   *
   * @param rootLocation the top directory for ArchivalUnit this URL is in
   * @param urlStr the url to translate
   * @return the url file location
   * @throws java.net.MalformedURLException
   */
  public static String mapUrlToFileLocation(String rootLocation, String urlStr)
      throws MalformedURLException {
    int totalLength = rootLocation.length() + urlStr.length();
    URL url = new URL(urlStr);
    StringBuilder buffer = new StringBuilder(totalLength);
    buffer.append(rootLocation);
    if (!rootLocation.endsWith(File.separator)) {
      buffer.append(File.separator);
    }
    buffer.append(url.getHost().toLowerCase());
    int port = url.getPort();
    if (port != -1) {
      buffer.append(PORT_SEPARATOR);
      buffer.append(port);
    }
    buffer.append(File.separator);
    buffer.append(url.getProtocol());
    if (RepositoryManager.isEnableLongComponents()) {
      String escapedPath =
          escapePath(
              StringUtil.replaceString(url.getPath(), UrlUtil.URL_PATH_SEPARATOR, File.separator));
      String query = url.getQuery();
      if (query != null) {
        escapedPath = escapedPath + "?" + escapeQuery(query);
      }
      String encodedPath = RepositoryNodeImpl.encodeUrl(escapedPath);
      // encodeUrl strips leading / from path
      buffer.append(File.separator);
      buffer.append(encodedPath);
    } else {
      buffer.append(
          escapePath(
              StringUtil.replaceString(url.getPath(), UrlUtil.URL_PATH_SEPARATOR, File.separator)));
      String query = url.getQuery();
      if (query != null) {
        buffer.append("?");
        buffer.append(escapeQuery(query));
      }
    }
    return buffer.toString();
  }

  // name mapping functions

  /**
   * Return true iff a repository for the auid exists under the root
   *
   * @param auid
   * @param repoRoot the repository root
   * @return true iff a repository for the auid exists
   */
  static boolean doesAuDirExist(String auid, String repoRoot) {
    return null != getAuDir(auid, repoRoot, false);
  }

  /**
   * Finds the directory for this AU. If none found in the map, designates a new dir for it.
   *
   * @param au the AU
   * @param repoRoot root of the repository
   * @return the dir {@link String}
   */
  static String getAuDir(ArchivalUnit au, String repoRoot, boolean create) {
    return getAuDir(au.getAuId(), repoRoot, create);
  }

  /**
   * Finds the directory for this AU. If none found in the map, designates a new dir for it.
   *
   * @param auid AU id representing the au
   * @param repoRoot path to the root of the repository
   * @return the dir String
   */
  static String getAuDir(String auid, String repoRoot, boolean create) {
    String repoCachePath = extendCacheLocation(repoRoot);
    LocalRepository localRepo = getLocalRepository(repoRoot);
    synchronized (localRepo) {
      Map aumap = localRepo.getAuMap();
      String auPathSlash = (String) aumap.get(auid);
      if (auPathSlash != null) {
        return auPathSlash;
      }
      if (!create) {
        return null;
      }
      logger.debug3("Creating new au directory for '" + auid + "'.");
      String auDir = localRepo.getPrevAuDir();
      for (int cnt = RepositoryManager.getMaxUnusedDirSearch(); cnt > 0; cnt--) {
        // loop through looking for an available dir
        auDir = getNextDirName(auDir);
        File testDir = new File(repoCachePath, auDir);
        if (logger.isDebug3()) logger.debug3("Probe for unused: " + testDir);
        if (!testDir.exists()) {
          if (RepositoryManager.isStatefulUnusedDirSearch()) {
            localRepo.setPrevAuDir(auDir);
          }
          String auPath = testDir.toString();
          logger.debug3("New au directory: " + auPath);
          auPathSlash = auPath + File.separator;
          // write the new au property file to the new dir
          // XXX this data should be backed up elsewhere to avoid single-point
          // corruption
          Properties idProps = new Properties();
          idProps.setProperty(AU_ID_PROP, auid);
          saveAuIdProperties(auPath, idProps);
          aumap.put(auid, auPathSlash);
          return auPathSlash;
        } else {
          if (logger.isDebug3()) {
            logger.debug3("Existing directory found at '" + auDir + "'.  Checking next...");
          }
        }
      }
    }
    throw new RuntimeException(
        "Can't find unused repository dir after "
            + RepositoryManager.getMaxUnusedDirSearch()
            + " tries in "
            + repoCachePath);
  }

  static LocalRepository getLocalRepository(ArchivalUnit au) {
    return getLocalRepository(getRepositoryRoot(au));
  }

  static LocalRepository getLocalRepository(String repoRoot) {
    synchronized (localRepositories) {
      LocalRepository localRepo = (LocalRepository) localRepositories.get(repoRoot);
      if (localRepo == null) {
        logger.debug2("Creating LocalRepository(" + repoRoot + ")");
        localRepo = new LocalRepository(repoRoot);
        localRepositories.put(repoRoot, localRepo);
      }
      return localRepo;
    }
  }

  /** Return next string in the sequence "a", "b", ... "z", "aa", "ab", ... */
  static String getNextDirName(String old) {
    StringBuilder sb = new StringBuilder(old);
    // go through and increment the first non-'z' char
    // counts back from the last char, so 'aa'->'ab', not 'ba'
    for (int ii = sb.length() - 1; ii >= 0; ii--) {
      char curChar = sb.charAt(ii);
      if (curChar < 'z') {
        sb.setCharAt(ii, (char) (curChar + 1));
        return sb.toString();
      }
      sb.setCharAt(ii, 'a');
    }
    sb.insert(0, 'a');
    return sb.toString();
  }

  public static File getAuIdFile(String location) {
    return new File(location + File.separator + AU_ID_FILE);
  }

  static Properties getAuIdProperties(String location) {
    File propFile = new File(location + File.separator + AU_ID_FILE);
    try {
      InputStream is = new BufferedInputStream(new FileInputStream(propFile));
      Properties idProps = new Properties();
      idProps.load(is);
      is.close();
      return idProps;
    } catch (Exception e) {
      logger.warning("Error loading au id from " + propFile.getPath() + ".");
      return null;
    }
  }

  static void saveAuIdProperties(String location, Properties props) {
    // XXX these AU_ID_FILE entries need to be backed up elsewhere to avoid
    // single-point corruption
    File propDir = new File(location);
    if (!propDir.exists()) {
      logger.debug("Creating directory '" + propDir.getAbsolutePath() + "'");
      propDir.mkdirs();
    }
    File propFile = new File(propDir, AU_ID_FILE);
    try {
      logger.debug3("Saving au id properties at '" + location + "'.");
      OutputStream os = new BufferedOutputStream(new FileOutputStream(propFile));
      props.store(os, "ArchivalUnit id info");
      os.close();
      propFile.setReadOnly();
    } catch (IOException ioe) {
      logger.error("Couldn't write properties for " + propFile.getPath() + ".", ioe);
      throw new LockssRepository.RepositoryStateException("Couldn't write au id properties file.");
    }
  }

  // lockss filename-specific encoding methods

  /**
   * Escapes instances of the ESCAPE_CHAR from the path. This avoids name conflicts with the
   * repository files, such as '#nodestate.xml'.
   *
   * @param path the path
   * @return the escaped path
   */
  static String escapePath(String path) {
    // XXX escaping disabled because of URL encoding
    if (false && path.indexOf(ESCAPE_CHAR) >= 0) {
      return StringUtil.replaceString(path, ESCAPE_STR, ESCAPE_STR + ESCAPE_STR);
    } else {
      return path;
    }
  }

  /**
   * Escapes instances of File.separator from the query. These are safe from filename overlap, but
   * can't convert into extended paths and directories.
   *
   * @param query the query
   * @return the escaped query
   */
  static String escapeQuery(String query) {
    if (query.indexOf(File.separator) >= 0) {
      return StringUtil.replaceString(query, File.separator, ESCAPE_STR + ENCODED_SEPARATOR_CHAR);
    } else {
      return query;
    }
  }

  /**
   * Extracts '#x' encoding and converts back to 'x'.
   *
   * @param orig the original
   * @return the unescaped version.
   */
  static String unescape(String orig) {
    if (orig.indexOf(ESCAPE_CHAR) < 0) {
      // fast treatment of non-escaped strings
      return orig;
    }
    int index = -1;
    StringBuilder buffer = new StringBuilder(orig.length());
    String oldStr = orig;
    while ((index = oldStr.indexOf(ESCAPE_CHAR)) >= 0) {
      buffer.append(oldStr.substring(0, index));
      buffer.append(convertCode(oldStr.substring(index, index + 2)));
      if (oldStr.length() > 2) {
        oldStr = oldStr.substring(index + 2);
      } else {
        oldStr = "";
      }
    }
    buffer.append(oldStr);
    return buffer.toString();
  }

  /**
   * Returns the second char in the escaped segment, unless it is 's', which is a stand-in for the
   * File.separatorChar.
   *
   * @param code the code segment (length 2)
   * @return the encoded char
   */
  static char convertCode(String code) {
    char encodedChar = code.charAt(1);
    if (encodedChar == ENCODED_SEPARATOR_CHAR) {
      return File.separatorChar;
    } else {
      return encodedChar;
    }
  }

  public static class Factory implements LockssAuManager.Factory {
    public LockssAuManager createAuManager(ArchivalUnit au) {
      return createNewLockssRepository(au);
    }
  }

  /** Maintains state for a local repository root dir (<i>eg</i>, auid of each au subdir). */
  static class LocalRepository {
    String repoPath;
    File repoCacheFile;
    Map auMap;
    String prevAuDir;

    LocalRepository(String repoPath) {
      this.repoPath = repoPath;
      repoCacheFile = new File(repoPath, CACHE_ROOT_NAME);
    }

    public String getRepositoryPath() {
      return repoPath;
    }

    public String getPrevAuDir() {
      if (prevAuDir == null) {
        prevAuDir = lastPluginDir;
      }
      return prevAuDir;
    }

    public void setPrevAuDir(String dir) {
      prevAuDir = dir;
    }

    /**
     * Return the auid -> au-subdir-path mapping. Enumerating the directories if necessary to
     * initialize the map
     */
    Map getAuMap() {
      if (auMap == null) {
        logger.debug3("Loading name map for '" + repoCacheFile + "'.");
        auMap = new HashMap();
        if (!repoCacheFile.exists()) {
          logger.debug3("Creating cache dir:" + repoCacheFile + "'.");
          if (!repoCacheFile.mkdirs()) {
            logger.critical("Couldn't create directory, check owner/permissions: " + repoCacheFile);
            // return empty map
            return auMap;
          }
        } else {
          // read each dir's property file and store mapping auid -> dir
          File[] auDirs = repoCacheFile.listFiles();
          for (int ii = 0; ii < auDirs.length; ii++) {
            String dirName = auDirs[ii].getName();
            //       if (dirName.compareTo(lastPluginDir) == 1) {
            //         // adjust the 'lastPluginDir' upwards if necessary
            //         lastPluginDir = dirName;
            //       }

            String path = auDirs[ii].getAbsolutePath();
            Properties idProps = getAuIdProperties(path);
            if (idProps != null) {
              String auid = idProps.getProperty(AU_ID_PROP);
              StringBuilder sb = new StringBuilder(path.length() + File.separator.length());
              sb.append(path);
              sb.append(File.separator);
              auMap.put(auid, sb.toString());
              logger.debug3("Mapping to: " + auMap.get(auid) + ": " + auid);
            } else {
              logger.debug3("Not mapping " + path + ", no auid file.");
            }
          }
        }
      }
      return auMap;
    }

    public String toString() {
      return "[LR: " + repoPath + "]";
    }
  }

  String getRootLocation() {
    return rootLocation;
  }
}