/** Return a drill-down {@link Term} for a category. */
 public static Term term(FacetIndexingParams iParams, CategoryPath path) {
   CategoryListParams clp = iParams.getCategoryListParams(path);
   char[] buffer = new char[path.fullPathLength()];
   iParams.drillDownTermText(path, buffer);
   return new Term(clp.field, String.valueOf(buffer));
 }
  public void testConcurrency() throws Exception {
    final int ncats = atLeast(100000); // add many categories
    final int range = ncats * 3; // affects the categories selection
    final AtomicInteger numCats = new AtomicInteger(ncats);
    final Directory dir = newDirectory();
    final ConcurrentHashMap<String, String> values = new ConcurrentHashMap<String, String>();
    final double d = random().nextDouble();
    final TaxonomyWriterCache cache;
    if (d < 0.7) {
      // this is the fastest, yet most memory consuming
      cache = new Cl2oTaxonomyWriterCache(1024, 0.15f, 3);
    } else if (TEST_NIGHTLY && d > 0.98) {
      // this is the slowest, but tests the writer concurrency when no caching is done.
      // only pick it during NIGHTLY tests, and even then, with very low chances.
      cache = NO_OP_CACHE;
    } else {
      // this is slower than CL2O, but less memory consuming, and exercises finding categories on
      // disk too.
      cache = new LruTaxonomyWriterCache(ncats / 10);
    }
    final DirectoryTaxonomyWriter tw = new DirectoryTaxonomyWriter(dir, OpenMode.CREATE, cache);
    Thread[] addThreads = new Thread[atLeast(4)];
    for (int z = 0; z < addThreads.length; z++) {
      addThreads[z] =
          new Thread() {
            @Override
            public void run() {
              Random random = random();
              while (numCats.decrementAndGet() > 0) {
                try {
                  int value = random.nextInt(range);
                  CategoryPath cp =
                      new CategoryPath(
                          Integer.toString(value / 1000),
                          Integer.toString(value / 10000),
                          Integer.toString(value / 100000),
                          Integer.toString(value));
                  int ord = tw.addCategory(cp);
                  assertTrue(
                      "invalid parent for ordinal " + ord + ", category " + cp,
                      tw.getParent(ord) != -1);
                  String l1 = cp.subpath(1).toString('/');
                  String l2 = cp.subpath(2).toString('/');
                  String l3 = cp.subpath(3).toString('/');
                  String l4 = cp.subpath(4).toString('/');
                  values.put(l1, l1);
                  values.put(l2, l2);
                  values.put(l3, l3);
                  values.put(l4, l4);
                } catch (IOException e) {
                  throw new RuntimeException(e);
                }
              }
            }
          };
    }

    for (Thread t : addThreads) t.start();
    for (Thread t : addThreads) t.join();
    tw.close();

    DirectoryTaxonomyReader dtr = new DirectoryTaxonomyReader(dir);
    assertEquals(
        "mismatch number of categories", values.size() + 1, dtr.getSize()); // +1 for root category
    int[] parents = dtr.getParallelTaxonomyArrays().parents();
    for (String cat : values.keySet()) {
      CategoryPath cp = new CategoryPath(cat, '/');
      assertTrue("category not found " + cp, dtr.getOrdinal(cp) > 0);
      int level = cp.length;
      int parentOrd = 0; // for root, parent is always virtual ROOT (ord=0)
      CategoryPath path = CategoryPath.EMPTY;
      for (int i = 0; i < level; i++) {
        path = cp.subpath(i + 1);
        int ord = dtr.getOrdinal(path);
        assertEquals("invalid parent for cp=" + path, parentOrd, parents[ord]);
        parentOrd = ord; // next level should have this parent
      }
    }
    dtr.close();

    dir.close();
  }