You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by to...@apache.org on 2022/06/29 09:43:24 UTC

[lucene-jira-archive] branch main created (now d2454ff)

This is an automated email from the ASF dual-hosted git repository.

tomoko pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/lucene-jira-archive.git


      at d2454ff  initial import

This branch includes the following new commits:

     new d2454ff  initial import

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[lucene-jira-archive] 01/01: initial import

Posted by to...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

tomoko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/lucene-jira-archive.git

commit d2454ff143ead0c15b8e5dd4fb72ed6e8e182f08
Author: Tomoko Uchida <to...@gmail.com>
AuthorDate: Wed Jun 29 18:43:01 2022 +0900

    initial import
---
 README.md                                          |    10 +
 attachments/LUCENE-2562/LUCENE-2562-Ivy.patch      |  3361 +++
 attachments/LUCENE-2562/LUCENE-2562-ivy.patch      |   252 +
 attachments/LUCENE-2562/LUCENE-2562.patch          | 24845 +++++++++++++++++++
 attachments/LUCENE-2562/Luke-ALE-1.png             |   Bin 0 -> 130060 bytes
 attachments/LUCENE-2562/Luke-ALE-2.png             |   Bin 0 -> 58325 bytes
 attachments/LUCENE-2562/Luke-ALE-3.png             |   Bin 0 -> 39806 bytes
 attachments/LUCENE-2562/Luke-ALE-4.png             |   Bin 0 -> 36067 bytes
 attachments/LUCENE-2562/Luke-ALE-5.png             |   Bin 0 -> 46718 bytes
 attachments/LUCENE-2562/luke-javafx1.png           |   Bin 0 -> 315297 bytes
 attachments/LUCENE-2562/luke-javafx2.png           |   Bin 0 -> 261032 bytes
 attachments/LUCENE-2562/luke-javafx3.png           |   Bin 0 -> 340626 bytes
 attachments/LUCENE-2562/luke1.jpg                  |   Bin 0 -> 193624 bytes
 attachments/LUCENE-2562/luke2.jpg                  |   Bin 0 -> 69429 bytes
 attachments/LUCENE-2562/luke3.jpg                  |   Bin 0 -> 125988 bytes
 attachments/LUCENE-2562/lukeALE-documents.png      |   Bin 0 -> 256617 bytes
 attachments/LUCENE-2562/screenshot-1.png           |   Bin 0 -> 245170 bytes
 ...343\203\203\343\203\210 2018-11-05 9.19.47.png" |   Bin 0 -> 248415 bytes
 attachments/LUCENE-4051/LUCENE-4051.patch          |   814 +
 attachments/LUCENE-9221/LuceneLogo.png             |   Bin 0 -> 167600 bytes
 .../Screen Shot 2020-04-10 at 8.29.32 AM.png       |   Bin 0 -> 395961 bytes
 .../LUCENE-9221/image-2020-04-10-07-04-00-267.png  |   Bin 0 -> 163261 bytes
 attachments/LUCENE-9221/image.png                  |   Bin 0 -> 148903 bytes
 attachments/LUCENE-9221/lucene-invert-a.png        |   Bin 0 -> 3853 bytes
 attachments/LUCENE-9221/lucene_logo1.pdf           |   Bin 0 -> 6585 bytes
 attachments/LUCENE-9221/lucene_logo1_full.pdf      |   Bin 0 -> 19055 bytes
 attachments/LUCENE-9221/lucene_logo2.pdf           |   Bin 0 -> 7753 bytes
 attachments/LUCENE-9221/lucene_logo2_full.pdf      |   Bin 0 -> 8138 bytes
 attachments/LUCENE-9221/lucene_logo3.pdf           |   Bin 0 -> 6424 bytes
 attachments/LUCENE-9221/lucene_logo3_1.pdf         |   Bin 0 -> 6524 bytes
 attachments/LUCENE-9221/lucene_logo3_full.pdf      |   Bin 0 -> 32424 bytes
 attachments/LUCENE-9221/lucene_logo4.pdf           |   Bin 0 -> 55904 bytes
 attachments/LUCENE-9221/lucene_logo4_full.pdf      |   Bin 0 -> 56043 bytes
 attachments/LUCENE-9221/lucene_logo5.pdf           |   Bin 0 -> 27368 bytes
 attachments/LUCENE-9221/lucene_logo5_full.pdf      |   Bin 0 -> 30105 bytes
 attachments/LUCENE-9221/lucene_logo6.pdf           |   Bin 0 -> 16717 bytes
 attachments/LUCENE-9221/lucene_logo6_full.pdf      |   Bin 0 -> 24269 bytes
 attachments/LUCENE-9221/lucene_logo7.pdf           |   Bin 0 -> 28703 bytes
 attachments/LUCENE-9221/lucene_logo7_full.pdf      |   Bin 0 -> 29398 bytes
 attachments/LUCENE-9221/lucene_logo8.pdf           |   Bin 0 -> 32568 bytes
 attachments/LUCENE-9221/lucene_logo8_full.pdf      |   Bin 0 -> 33017 bytes
 attachments/LUCENE-9221/zabetak-1-7.pdf            |   Bin 0 -> 22850 bytes
 migration/.env.example                             |     4 +
 migration/.gitignore                               |    16 +
 migration/.python-version                          |     1 +
 migration/README.md                                |   126 +
 migration/mappings-data/account-map.csv.example    |     1 +
 migration/mappings-data/issue-map.csv.example      |     1 +
 migration/requirements.txt                         |     7 +
 migration/src/__init__.py                          |     0
 migration/src/common.py                            |   151 +
 migration/src/download_jira.py                     |   120 +
 migration/src/github_issues_util.py                |    97 +
 migration/src/import_github_issues.py              |   101 +
 migration/src/jira2github_import.py                |   216 +
 migration/src/jira_util.py                         |   198 +
 migration/src/py.typed                             |     0
 migration/src/update_issue_links.py                |    94 +
 58 files changed, 30415 insertions(+)

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..274cbc0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,10 @@
+# Jira archive for Apache Lucene
+
+This repository serves for:
+
+https://issues.apache.org/jira/browse/LUCENE-10557
+
+- Archive Jira attachments
+- Drafting Label management
+- Drafting Issue templates
+- [Migration script](./migration/)
diff --git a/attachments/LUCENE-2562/LUCENE-2562-Ivy.patch b/attachments/LUCENE-2562/LUCENE-2562-Ivy.patch
new file mode 100644
index 0000000..72c640f
--- /dev/null
+++ b/attachments/LUCENE-2562/LUCENE-2562-Ivy.patch
@@ -0,0 +1,3361 @@
+Index: src/org/apache/lucene/luke/core/HighFreqTerms.java
+===================================================================
+--- src/org/apache/lucene/luke/core/HighFreqTerms.java	(revision 1655665)
++++ src/org/apache/lucene/luke/core/HighFreqTerms.java	(working copy)
+@@ -100,10 +100,10 @@
+   }
+   
+   /**
+-   * 
+-   * @param reader
+-   * @param numTerms
+-   * @param field
++   * // TODO move this method to org.apache.lucene.misc.HighFreqTerms
++   * @param reader IndexReader
++   * @param numTerms the max number of terms
++   * @param fieldNames tye array of field names
+    * @return TermStats[] ordered by terms with highest docFreq first.
+    * @throws Exception
+    */
+Index: src/org/apache/lucene/luke/core/IndexInfo.java
+===================================================================
+--- src/org/apache/lucene/luke/core/IndexInfo.java	(revision 1655665)
++++ src/org/apache/lucene/luke/core/IndexInfo.java	(working copy)
+@@ -177,6 +177,7 @@
+       }
+     }
+   }
++
+   
+   /**
+    * @return the reader
+Index: src/org/apache/lucene/luke/core/TableComparator.java
+===================================================================
+--- src/org/apache/lucene/luke/core/TableComparator.java	(revision 1655665)
++++ src/org/apache/lucene/luke/core/TableComparator.java	(working copy)
+@@ -1,63 +0,0 @@
+-package org.apache.lucene.luke.core;
+-
+-/*
+- * Licensed to the Apache Software Foundation (ASF) under one or more
+- * contributor license agreements.  See the NOTICE file distributed with
+- * this work for additional information regarding copyright ownership.
+- * The ASF licenses this file to you under the Apache License,
+- * Version 2.0 (the "License"); you may not use this file except in
+- * compliance with the License.  You may obtain a copy of the License at
+- *
+- *     http://www.apache.org/licenses/LICENSE-2.0
+- *
+- * Unless required by applicable law or agreed to in writing, software
+- * distributed under the License is distributed on an "AS IS" BASIS,
+- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+- * See the License for the specific language governing permissions and
+- * limitations under the License.
+- */
+-
+-import java.util.Comparator;
+-
+-import org.apache.pivot.collections.Dictionary;
+-import org.apache.pivot.collections.Map;
+-import org.apache.pivot.wtk.SortDirection;
+-import org.apache.pivot.wtk.TableView;
+-
+-public class TableComparator implements Comparator<Map<String,String>> {
+-  private TableView tableView;
+-  
+-  public TableComparator(TableView fieldsTable) {
+-    if (fieldsTable == null) {
+-      throw new IllegalArgumentException();
+-    }
+-    
+-    this.tableView = fieldsTable;
+-  }
+-  
+-  @Override
+-  public int compare(Map<String,String> o1, Map<String,String> o2) {
+-    Dictionary.Pair<String, SortDirection> sort = tableView.getSort().get(0);
+-
+-    int result;
+-    if (sort.key.equals("name")) {
+-      // sort by name
+-      result = o1.get(sort.key).compareTo(o2.get(sort.key));
+-    } else if (sort.key.equals("termCount")) {
+-      // sort by termCount
+-      Integer c1 = Integer.parseInt(o1.get(sort.key));
+-      Integer c2 = Integer.parseInt(o2.get(sort.key));
+-      result = c1.compareTo(c2);
+-    } else {
+-      // other (ignored)
+-      result = 0;
+-    }
+-    //int result = o1.get("name").compareTo(o2.get("name"));
+-    //SortDirection sortDirection = tableView.getSort().get("name");
+-    SortDirection sortDirection = sort.value;
+-    result *= (sortDirection == SortDirection.DESCENDING ? 1 : -1);
+-
+-    return result * -1;
+-  }
+-  
+-}
+\ No newline at end of file
+Index: src/org/apache/lucene/luke/core/TermStats.java
+===================================================================
+--- src/org/apache/lucene/luke/core/TermStats.java	(revision 1655665)
++++ src/org/apache/lucene/luke/core/TermStats.java	(working copy)
+@@ -26,13 +26,15 @@
+   public long totalTermFreq;
+   
+   TermStats(String field, BytesRef termtext, int df) {
+-    this.termtext = (BytesRef)termtext.clone();
++    //this.termtext = (BytesRef)termtext.clone();
++    this.termtext = BytesRef.deepCopyOf(termtext);
+     this.field = field;
+     this.docFreq = df;
+   }
+   
+   TermStats(String field, BytesRef termtext, int df, long tf) {
+-    this.termtext = (BytesRef)termtext.clone();
++    //this.termtext = (BytesRef)termtext.clone();
++    this.termtext = BytesRef.deepCopyOf(termtext);
+     this.field = field;
+     this.docFreq = df;
+     this.totalTermFreq = tf;
+Index: src/org/apache/lucene/luke/core/Util.java
+===================================================================
+--- src/org/apache/lucene/luke/core/Util.java	(revision 1655665)
++++ src/org/apache/lucene/luke/core/Util.java	(working copy)
+@@ -19,11 +19,19 @@
+ 
+ import java.io.ByteArrayOutputStream;
+ import java.io.File;
++import java.lang.reflect.Constructor;
++import java.lang.reflect.Method;
++import java.util.ArrayList;
++import java.util.Arrays;
+ import java.util.HashMap;
++import java.util.List;
+ 
+ import org.apache.lucene.document.DateTools.Resolution;
++import org.apache.lucene.index.FieldInfo;
+ import org.apache.lucene.index.FieldInfo.IndexOptions;
+ import org.apache.lucene.index.IndexableField;
++import org.apache.lucene.luke.core.decoders.*;
++import org.apache.lucene.search.similarities.TFIDFSimilarity;
+ import org.apache.lucene.store.Directory;
+ import org.apache.lucene.store.FSDirectory;
+ import org.apache.lucene.store.MMapDirectory;
+@@ -161,18 +169,20 @@
+     return sb.toString();
+   }
+   
+-  public static String fieldFlags(IndexableField f) {
+-    if (f == null) {
+-      return "-----------";
+-    }
++  public static String fieldFlags(IndexableField f, FieldInfo info) {
++    //if (f == null) {
++    //  return "-----------";
++    //}
+     StringBuffer flags = new StringBuffer();
+-    if (f != null && f.fieldType().indexed()) flags.append("I");
++    //if (f != null && f.fieldType().indexed()) flags.append("I");
++    if (info != null && info.isIndexed()) flags.append("I");
+     else flags.append("-");
+     if (f != null && f.fieldType().tokenized()) flags.append("T");
+     else flags.append("-");
+     if (f != null && f.fieldType().stored()) flags.append("S");
+     else flags.append("-");
+-    if (f != null && f.fieldType().storeTermVectors()) flags.append("V");
++    //if (f != null && f.fieldType().storeTermVectors()) flags.append("V");
++    if (info != null && info.hasVectors()) flags.append("V");
+     else flags.append("-");
+     if (f != null && f.fieldType().storeTermVectorOffsets()) flags.append("o");
+     else flags.append("-");
+@@ -180,9 +190,13 @@
+     else flags.append("-");
+     if (f != null && f.fieldType().storeTermVectorPayloads()) flags.append("a");
+     else flags.append("-");
+-    IndexOptions opts = f.fieldType().indexOptions();
++    if (info != null && info.hasPayloads()) flags.append("P");
++    else flags.append("-");
++    //IndexOptions opts = f.fieldType().indexOptions();
++    IndexOptions opts = info.getIndexOptions();
+     // TODO: how to handle these codes
+-    if (f.fieldType().indexed() && opts != null) {
++    //if (f.fieldType().indexed() && opts != null) {
++    if (info.isIndexed() && opts != null) {
+       switch (opts) {
+       case DOCS_ONLY:
+         flags.append("1");
+@@ -199,13 +213,32 @@
+     } else {
+       flags.append("-");
+     }
+-    if (f != null && f.fieldType().omitNorms()) flags.append("O");
++    //if (f != null && f.fieldType().omitNorms()) flags.append("O");
++    if (info != null && !info.hasNorms()) flags.append("O");
+     else flags.append("-");
++    // TODO lazy
++    flags.append("-");
++    if (f != null && f.binaryValue() != null) flags.append("B");
++    else flags.append("-");
+ 
+ 
+     return flags.toString();
+   }
+-  
++
++  public static String docValuesType(FieldInfo info) {
++    if (info == null || !info.hasDocValues() || info.getDocValuesType() == null) {
++      return "---";
++    }
++    return info.getDocValuesType().name();
++  }
++
++  public static String normType(FieldInfo info) {
++    if (info == null || !info.hasNorms() || info.getNormType() == null) {
++      return "---";
++    }
++    return info.getNormType().name();
++  }
++
+   public static Resolution getResolution(String key) {
+     if (key == null || key.trim().length() == 0) {
+       return Resolution.MILLISECOND;
+@@ -270,4 +303,56 @@
+       return String.valueOf(len / 1048576);
+     }
+   }
++
++  public static float decodeNormValue(long v, String fieldName, TFIDFSimilarity sim) throws Exception {
++    try {
++      return sim.decodeNormValue(v);
++    } catch (Exception e) {
++      throw new Exception("ERROR decoding norm for field "  + fieldName + ":" + e.toString());
++    }
++  }
++
++  public static long encodeNormValue(float v, String fieldName, TFIDFSimilarity sim) throws Exception {
++    try {
++      return sim.encodeNormValue(v);
++    } catch (Exception e) {
++      throw new Exception("ERROR encoding norm for field "  + fieldName + ":" + e.toString());
++    }
++  }
++
++
++  public static List<Decoder> loadDecoders() {
++    List decoders = new ArrayList();
++    // default decoders
++    decoders.add(new BinaryDecoder());
++    decoders.add(new DateDecoder());
++    decoders.add(new NumDoubleDecoder());
++    decoders.add(new NumFloatDecoder());
++    decoders.add(new NumIntDecoder());
++    decoders.add(new NumLongDecoder());
++    decoders.add(new StringDecoder());
++
++    // load external decoders
++    try {
++      String extLoaders = System.getProperty("luke.ext.decoder.loader");
++      if (extLoaders != null) {
++        String[] classes = extLoaders.split(",");
++        for (String className : classes) {
++          Class clazz = Class.forName(className);
++          Class[] interfaces = clazz.getInterfaces();
++          if (Arrays.asList(interfaces).indexOf(DecoderLoader.class) < 0) {
++            throw new Exception(className + " is not a DecoderLoader.");
++          }
++          DecoderLoader loader = (DecoderLoader)clazz.newInstance();
++          List<Decoder> extDecoders = loader.loadDecoders();
++          for (Decoder dec : extDecoders) {
++            decoders.add(dec);
++          }
++        }
++      }
++    } catch (Exception e) {
++      e.printStackTrace();
++    }
++    return decoders;
++  }
+ }
+Index: src/org/apache/lucene/luke/core/decoders/BinaryDecoder.java
+===================================================================
+--- src/org/apache/lucene/luke/core/decoders/BinaryDecoder.java	(revision 1655665)
++++ src/org/apache/lucene/luke/core/decoders/BinaryDecoder.java	(working copy)
+@@ -19,23 +19,18 @@
+ 
+ import org.apache.lucene.document.Field;
+ import org.apache.lucene.luke.core.Util;
++import org.apache.lucene.util.BytesRef;
+ 
+ public class BinaryDecoder implements Decoder {
+ 
+   @Override
+-  public String decodeTerm(String fieldName, Object value) throws Exception {
+-    byte[] data;
+-    if (value instanceof byte[]) {
+-      data = (byte[])value;
+-    } else {
+-      data = value.toString().getBytes();
+-    }
+-    return Util.bytesToHex(data, 0, data.length, false);
++  public String decodeTerm(String fieldName, BytesRef value) throws Exception {
++    return Util.bytesToHex(value.bytes, 0, value.length, false);
+   }
+ 
+   @Override
+   public String decodeStored(String fieldName, Field value) throws Exception {
+-    return decodeTerm(fieldName, value);
++    return decodeTerm(fieldName, new BytesRef(value.stringValue()));
+   }
+   
+   public String toString() {
+Index: src/org/apache/lucene/luke/core/decoders/DateDecoder.java
+===================================================================
+--- src/org/apache/lucene/luke/core/decoders/DateDecoder.java	(revision 1655665)
++++ src/org/apache/lucene/luke/core/decoders/DateDecoder.java	(working copy)
+@@ -19,17 +19,18 @@
+ 
+ import org.apache.lucene.document.DateTools;
+ import org.apache.lucene.document.Field;
++import org.apache.lucene.util.BytesRef;
+ 
+ public class DateDecoder implements Decoder {
+ 
+   @Override
+-  public String decodeTerm(String fieldName, Object value) throws Exception {
++  public String decodeTerm(String fieldName, BytesRef value) throws Exception {
+     return DateTools.stringToDate(value.toString()).toString();
+   }
+   
+   @Override
+   public String decodeStored(String fieldName, Field value) throws Exception {
+-    return decodeTerm(fieldName, value.stringValue());
++    return decodeTerm(fieldName, new BytesRef(value.stringValue()));
+   }
+   
+   public String toString() {
+Index: src/org/apache/lucene/luke/core/decoders/Decoder.java
+===================================================================
+--- src/org/apache/lucene/luke/core/decoders/Decoder.java	(revision 1655665)
++++ src/org/apache/lucene/luke/core/decoders/Decoder.java	(working copy)
+@@ -18,9 +18,10 @@
+  */
+ 
+ import org.apache.lucene.document.Field;
++import org.apache.lucene.util.BytesRef;
+ 
+ public interface Decoder {
+   
+-  public String decodeTerm(String fieldName, Object value) throws Exception;
++  public String decodeTerm(String fieldName, BytesRef value) throws Exception;
+   public String decodeStored(String fieldName, Field value) throws Exception;
+ }
+Index: src/org/apache/lucene/luke/core/decoders/DecoderLoader.java
+===================================================================
+--- src/org/apache/lucene/luke/core/decoders/DecoderLoader.java	(revision 0)
++++ src/org/apache/lucene/luke/core/decoders/DecoderLoader.java	(working copy)
+@@ -0,0 +1,24 @@
++package org.apache.lucene.luke.core.decoders;
++
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++import java.util.List;
++
++public interface DecoderLoader {
++  public List<Decoder> loadDecoders();
++}
+Index: src/org/apache/lucene/luke/core/decoders/NumDoubleDecoder.java
+===================================================================
+--- src/org/apache/lucene/luke/core/decoders/NumDoubleDecoder.java	(revision 1655665)
++++ src/org/apache/lucene/luke/core/decoders/NumDoubleDecoder.java	(working copy)
+@@ -1,5 +1,22 @@
+ package org.apache.lucene.luke.core.decoders;
+ 
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
+ import org.apache.lucene.document.Field;
+ import org.apache.lucene.util.BytesRef;
+ import org.apache.lucene.util.NumericUtils;
+@@ -7,9 +24,8 @@
+ public class NumDoubleDecoder implements Decoder {
+ 
+   @Override
+-  public String decodeTerm(String fieldName, Object value) {
+-    BytesRef ref = new BytesRef(value.toString());
+-    return Double.toString(NumericUtils.sortableLongToDouble(NumericUtils.prefixCodedToLong(ref)));
++  public String decodeTerm(String fieldName, BytesRef value) {
++    return Double.toString(NumericUtils.sortableLongToDouble(NumericUtils.prefixCodedToLong(value)));
+   }
+ 
+   @Override
+@@ -18,7 +34,7 @@
+   }
+ 
+   public String toString() {
+-    return "numeric-double";
++    return "numeric double";
+   }
+ 
+ }
+Index: src/org/apache/lucene/luke/core/decoders/NumFloatDecoder.java
+===================================================================
+--- src/org/apache/lucene/luke/core/decoders/NumFloatDecoder.java	(revision 1655665)
++++ src/org/apache/lucene/luke/core/decoders/NumFloatDecoder.java	(working copy)
+@@ -6,9 +6,9 @@
+ 
+ public class NumFloatDecoder implements Decoder {
+   @Override
+-  public String decodeTerm(String fieldName, Object value) {
+-    BytesRef ref = new BytesRef(value.toString());
+-    return Float.toString(NumericUtils.sortableIntToFloat(NumericUtils.prefixCodedToInt(ref)));
++  public String decodeTerm(String fieldName, BytesRef value) {
++    //BytesRef ref = new BytesRef(value.toString());
++    return Float.toString(NumericUtils.sortableIntToFloat(NumericUtils.prefixCodedToInt(value)));
+   }
+ 
+   @Override
+@@ -17,7 +17,7 @@
+   }
+ 
+   public String toString() {
+-    return "numeric-float";
++    return "numeric float";
+   }
+ 
+ }
+Index: src/org/apache/lucene/luke/core/decoders/NumIntDecoder.java
+===================================================================
+--- src/org/apache/lucene/luke/core/decoders/NumIntDecoder.java	(revision 1655665)
++++ src/org/apache/lucene/luke/core/decoders/NumIntDecoder.java	(working copy)
+@@ -24,9 +24,8 @@
+ public class NumIntDecoder implements Decoder {
+ 
+   @Override
+-  public String decodeTerm(String fieldName, Object value) {
+-    BytesRef ref = new BytesRef(value.toString());
+-    return Integer.toString(NumericUtils.prefixCodedToInt(ref));
++  public String decodeTerm(String fieldName, BytesRef value) {
++    return Integer.toString(NumericUtils.prefixCodedToInt(value));
+   }
+   
+   @Override
+@@ -35,7 +34,7 @@
+   }
+   
+   public String toString() {
+-    return "numeric-int";
++    return "numeric int";
+   }
+ 
+ }
+Index: src/org/apache/lucene/luke/core/decoders/NumLongDecoder.java
+===================================================================
+--- src/org/apache/lucene/luke/core/decoders/NumLongDecoder.java	(revision 1655665)
++++ src/org/apache/lucene/luke/core/decoders/NumLongDecoder.java	(working copy)
+@@ -24,9 +24,8 @@
+ public class NumLongDecoder implements Decoder {
+ 
+   @Override
+-  public String decodeTerm(String fieldName, Object value) {
+-    BytesRef ref = new BytesRef(value.toString());
+-    return Long.toString(NumericUtils.prefixCodedToLong(ref));
++  public String decodeTerm(String fieldName, BytesRef value) {
++    return Long.toString(NumericUtils.prefixCodedToLong(value));
+   }
+   
+   @Override
+@@ -35,7 +34,7 @@
+   }
+   
+   public String toString() {
+-    return "numeric-long";
++    return "numeric long";
+   }
+ 
+ }
+Index: src/org/apache/lucene/luke/core/decoders/SolrDecoder.java
+===================================================================
+--- src/org/apache/lucene/luke/core/decoders/SolrDecoder.java	(revision 1655665)
++++ src/org/apache/lucene/luke/core/decoders/SolrDecoder.java	(working copy)
+@@ -24,8 +24,14 @@
+ 
+ import org.apache.lucene.document.Field;
+ import org.apache.lucene.luke.core.ClassFinder;
++import org.apache.lucene.util.BytesRef;
++import org.apache.lucene.util.CharsRef;
+ import org.apache.solr.schema.FieldType;
+ 
++/**
++ * NOT Used.
++ * The logic here has moved to org.apache.lucene.ext.SolrDecoderLoader.
++ */
+ public class SolrDecoder implements Decoder {
+   private static final String solr_prefix = "org.apache.solr.schema.";
+   
+@@ -86,8 +92,9 @@
+     name = type;
+   }
+   
+-  public String decodeTerm(String fieldName, Object value) throws Exception {
+-    return fieldType.indexedToReadable(value.toString());
++  public String decodeTerm(String fieldName, BytesRef value) throws Exception {
++    CharsRef chars = fieldType.indexedToReadable(value, new CharsRef());
++    return chars.toString();
+   }
+   
+   public String decodeStored(String fieldName, Field value)
+@@ -100,3 +107,4 @@
+   }
+   
+ }
++
+Index: src/org/apache/lucene/luke/core/decoders/StringDecoder.java
+===================================================================
+--- src/org/apache/lucene/luke/core/decoders/StringDecoder.java	(revision 1655665)
++++ src/org/apache/lucene/luke/core/decoders/StringDecoder.java	(working copy)
+@@ -18,17 +18,18 @@
+  */
+ 
+ import org.apache.lucene.document.Field;
++import org.apache.lucene.util.BytesRef;
+ 
+ public class StringDecoder implements Decoder {
+ 
+   @Override
+-  public String decodeTerm(String fieldName, Object value) {
+-    return value != null ? value.toString() : "(null)";
++  public String decodeTerm(String fieldName, BytesRef value) {
++    return value != null ? value.utf8ToString() : "(null)";
+   }
+   
+   @Override
+   public String decodeStored(String fieldName, Field value) {
+-    return decodeTerm(fieldName, value.stringValue());
++    return value.stringValue();
+   }
+ 
+   public String toString() {
+Index: src/org/apache/lucene/luke/ext/SolrDecoderLoader.java
+===================================================================
+--- src/org/apache/lucene/luke/ext/SolrDecoderLoader.java	(revision 0)
++++ src/org/apache/lucene/luke/ext/SolrDecoderLoader.java	(working copy)
+@@ -0,0 +1,81 @@
++package org.apache.lucene.luke.ext;
++
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++import org.apache.lucene.document.Field;
++import org.apache.lucene.luke.core.ClassFinder;
++import org.apache.lucene.luke.core.decoders.Decoder;
++import org.apache.lucene.luke.core.decoders.DecoderLoader;
++import org.apache.lucene.util.BytesRef;
++import org.apache.lucene.util.CharsRef;
++import org.apache.solr.schema.FieldType;
++
++import java.util.ArrayList;
++import java.util.List;
++
++public class SolrDecoderLoader implements DecoderLoader {
++  private static final String solr_prefix = "org.apache.solr.schema.";
++
++  @Override
++  public List<Decoder> loadDecoders() {
++    List<Decoder> decoders = new ArrayList<Decoder>();
++    try {
++      Class[] classes = ClassFinder.getInstantiableSubclasses(FieldType.class);
++      if (classes == null || classes.length == 0) {
++        throw new ClassNotFoundException("Missing Solr types???");
++      }
++      for (Class cls : classes) {
++        FieldType ft = (FieldType) cls.newInstance();
++        if (cls.getName().startsWith(solr_prefix)) {
++          String name = "solr." + cls.getName().substring(solr_prefix.length());
++          decoders.add(new SolrDecoder(name, ft));
++        }
++      }
++    } catch (Exception e) {
++      // TODO Auto-generated catch block
++      e.printStackTrace();
++    }
++    return decoders;
++  }
++}
++
++class SolrDecoder implements Decoder {
++  private String name;
++  private FieldType fieldType;
++
++  public SolrDecoder(String name, FieldType fieldType) {
++    this.name = name;
++    this.fieldType = fieldType;
++  }
++
++  public String decodeTerm(String fieldName, BytesRef value) throws Exception {
++    CharsRef chars = fieldType.indexedToReadable(value, new CharsRef());
++    return chars.toString();
++  }
++
++  public String decodeStored(String fieldName, Field value)
++    throws Exception {
++    return fieldType.storedToReadable(value);
++  }
++
++  public String toString() {
++    return name;
++  }
++
++}
++
+Index: src/org/apache/lucene/luke/ui/DocumentsTab.bxml
+===================================================================
+--- src/org/apache/lucene/luke/ui/DocumentsTab.bxml	(revision 1655665)
++++ src/org/apache/lucene/luke/ui/DocumentsTab.bxml	(working copy)
+@@ -1,169 +1,259 @@
+ <?xml version="1.0" encoding="UTF-8"?>
+ 
+ <luke:DocumentsTab bxml:id="DocumentsTab"
+-	styles="{verticalSpacing:2,horizontalSpacing:2,padding:4,backgroundColor:11}"
+ 	xmlns:bxml="http://pivot.apache.org/bxml" xmlns:content="org.apache.pivot.wtk.content"
+-	xmlns="org.apache.pivot.wtk" xmlns:luke="org.apache.lucene.luke.ui">
++	xmlns="org.apache.pivot.wtk" xmlns:luke="org.apache.lucene.luke.ui"
++	orientation="vertical" splitRatio="0.30">
+ 
++	<bxml:define>
++		<bxml:include bxml:id="posAndOffsetsWindow" src="PosAndOffsetsWindow.bxml" />
++	</bxml:define>
++	<bxml:define>
++		<bxml:include bxml:id="tvWindow" src="TermVectorWindow.bxml" />
++	</bxml:define>
++	<bxml:define>
++		<bxml:include bxml:id="fieldDataWindow" src="FieldDataWindow.bxml" />
++	</bxml:define>
++	<bxml:define>
++		<bxml:include bxml:id="fieldNormWindow" src="FieldNormWindow.bxml" />
++	</bxml:define>
+ 
+-	<columns>
+-		<TablePane.Column width="1*" />
+-	</columns>
+-	<rows>
+-		<TablePane.Row>
+-			<FlowPane styles="{padding:10}">
+-				<BoxPane orientation="vertical">
+-					<Label text="%documentsTab_browseByDocNum" />
+-					<BoxPane>
+-						<Label text="Doc. #:" />
+-						<Label text="0" />
+-						<PushButton preferredHeight="20" action="prevDoc">
+-							<buttonData>
+-								<content:ButtonData icon="/img/prev.png" />
+-							</buttonData>
+-						</PushButton>
+-						<TextInput preferredWidth="48" bxml:id="docNum" />
+-						<PushButton preferredHeight="20" action="nextDoc">
+-							<buttonData>
+-								<content:ButtonData icon="/img/next.png" />
+-							</buttonData>
+-						</PushButton>
+-						<Label bxml:id="maxDocs" text="?" />
++	<top>
++		<SplitPane orientation="horizontal" splitRatio="0.20" styles="{useShadow:true}">
++			<left>
++				<Border styles="{padding:1}">
++				<content>
++				<TablePane styles="{verticalSpacing:1,horizontalSpacing:1,padding:5,backgroundColor:11}">
++					<columns>
++						<TablePane.Column width="1*" />
++					</columns>
++					<rows>
++						<TablePane.Row>
++							<BoxPane orientation="vertical">
++								<Label text="%documentsTab_browseByDocNum" styles="{padding:2,font:{bold:true}}"/>
++								<Label text="Doc. #" styles="{padding:2}"/>
++								<BoxPane orientation="horizontal">
++									<Label text="0" styles="{padding:2}"/>
++									<PushButton preferredHeight="20" action="prevDoc">
++										<buttonData>
++											<content:ButtonData icon="/img/prev.png" />
++										</buttonData>
++									</PushButton>
++									<TextInput preferredWidth="48" bxml:id="docNum" />
++									<PushButton preferredHeight="20" action="nextDoc">
++										<buttonData>
++											<content:ButtonData icon="/img/next.png" />
++										</buttonData>
++									</PushButton>
++									<Label bxml:id="maxDocs" text="?" styles="{padding:2}"/>
++								</BoxPane>
++							</BoxPane>
++						</TablePane.Row>
++					</rows>
++				</TablePane>
++				</content>
++				</Border>
++			</left>
++			<right>
++				<SplitPane orientation="horizontal" splitRatio="0.50" styles="{useShadow:true}">
++					<left>
++						<Border styles="{padding:1}">
++						<content>
++						<TablePane styles="{verticalSpacing:2,horizontalSpacing:1,padding:5,backgroundColor:11}">
++							<columns>
++								<TablePane.Column width="1*" />
++							</columns>
++							<rows>
++								<TablePane.Row>
++									<BoxPane orientation="vertical">
++										<Label text="%documentsTab_browseByTerm" styles="{padding:1,font:{bold:true}}"/>
++										<Label text="%documentsTab_selectField" styles="{wrapText:true}"/>
++										<Label text="%documentsTab_enterTermHint" styles="{wrapText:true}"/>
++										<ListButton bxml:id="fieldsList" listSize="20" />
++										<Label text="%documentsTab_term" />
++										<BoxPane>
++											<PushButton buttonData="%documentsTab_firstTerm"
++																	action="showFirstTerm" />
++											<TextInput bxml:id="termText" />
++											<PushButton action="showNextTerm">
++												<buttonData>
++													<content:ButtonData icon="/img/next.png" />
++												</buttonData>
++											</PushButton>
++										</BoxPane>
++									</BoxPane>
++								</TablePane.Row>
+ 
+-					</BoxPane>
+-				</BoxPane>
++								<TablePane.Row>
++									<BoxPane>
++										<Label text="%documentsTab_decodedValue" />
++										<TextArea bxml:id="decText" />
++									</BoxPane>
++								</TablePane.Row>
++							</rows>
++						</TablePane>
++						</content>
++						</Border>
++					</left>
++					<right>
++						<Border styles="{padding:1}">
++						<content>
++						<TablePane styles="{verticalSpacing:2,horizontalSpacing:1,padding:5,backgroundColor:11}">
++							<columns>
++								<TablePane.Column width="1*" />
++							</columns>
++							<rows>
++								<TablePane.Row>
++									<BoxPane orientation="vertical">
++										<Label text="%documentsTab_browseDocsWithTerm" styles="{padding:1,font:{bold:true}}"/>
++										<Label text="%documentsTab_selectTerm" styles="{wrapText:true}"/>
++										<BoxPane>
++											<!--Label text="%documentsTab_document" /-->
++											<PushButton buttonData="%documentsTab_firstDoc" action="showFirstTermDoc" />
++											<PushButton action="showNextTermDoc">
++												<buttonData>
++													<content:ButtonData icon="/img/next.png" />
++												</buttonData>
++											</PushButton>
+ 
+-				<BoxPane orientation="vertical">
+-					<Label text="%documentsTab_browseByTerm" />
+-					<Label text="%documentsTab_enterTermHint" />
+-					<BoxPane>
+-						<PushButton buttonData="%documentsTab_firstTerm"
+-							action="showFirstTerm" />
+-						<Label text="%documentsTab_term" />
+-						<ListButton bxml:id="fieldsList" listSize="20" />
+-						<TextInput bxml:id="termText" />
+-						<PushButton action="showNextTerm">
+-							<buttonData>
+-								<content:ButtonData icon="/img/next.png" />
+-							</buttonData>
+-						</PushButton>
+-					</BoxPane>
+-				</BoxPane>
++											<Label text=" ("/>
++											<Label bxml:id="tdNum" text="?" />
++											<Label text=" of " />
++											<Label bxml:id="tdMax" text="?" />
++											<Label text=" documents )" />
++										</BoxPane>
++									</BoxPane>
++								</TablePane.Row>
+ 
++								<TablePane.Row>
++									<BoxPane orientation="vertical">
++										<BoxPane>
++											<Label text="%documentsTab_termFreqInDoc" />
++											<Label bxml:id="tFreq" text="?" />
++										</BoxPane>
++										<PushButton bxml:id="bPos" buttonData="%documentsTab_showPositions"
++																action="showPositions" />
++									</BoxPane>
++								</TablePane.Row>
+ 
+-				<!-- second row -->
+-				<Label text="%documentsTab_decodedValue" />
+-				<TextArea bxml:id="decText" />
++								<TablePane.Row>
++									<Separator/>
++								</TablePane.Row>
+ 
+-				<Separator />
+-				<BoxPane orientation="vertical">
+-					<BoxPane>
+-						<Label text="%documentsTab_browseDocsWithTerm" />
+-						<Label text="( " />
+-						<Label bxml:id="dFreq" text="0" />
+-						<Label text=" documents)" />
+-					</BoxPane>
++								<TablePane.Row>
++									<BoxPane>
++										<PushButton buttonData="%documentsTab_showAllDocs"
++																action="showAllTermDoc"/>
++										<BoxPane>
++											<PushButton action="deleteTermDoc">
++												<buttonData>
++													<content:ButtonData icon="/img/delete.gif" />
++												</buttonData>
++											</PushButton>
++											<Label text="%documentsTab_deleteAllDocs" styles="{padding:1}"/>
++										</BoxPane>
++									</BoxPane>
++								</TablePane.Row>
+ 
+-					<BoxPane>
+-						<Label text="%documentsTab_document" />
+-						<Label bxml:id="tdNum" text="?" />
+-						<Label text=" of " />
+-						<Label bxml:id="tdMax" text="?" />
+-						<PushButton buttonData="%documentsTab_firstDoc" action="showFirstTermDoc" />
+-						<PushButton action="showNextTermDoc">
+-							<buttonData>
+-								<content:ButtonData icon="/img/next.png" />
+-							</buttonData>
+-						</PushButton>
+-					</BoxPane>
++							</rows>
++						</TablePane>
++						</content>
++						</Border>
++					</right>
++				</SplitPane>
++			</right>
++		</SplitPane>
++	</top>
+ 
+-				</BoxPane>
+-				<BoxPane orientation="vertical">
+-					<PushButton buttonData="%documentsTab_showAllDocs"
+-						action="showAllTermDoc" />
+-					<PushButton buttonData="%documentsTab_deleteAllDocs"
+-						action="deleteTermDoc">
+-						<buttonData>
+-							<content:ButtonData icon="/img/delete.gif" />
+-						</buttonData>
+-					</PushButton>
+-				</BoxPane>
+-				<Label text=" " />
+-				<Label text="%documentsTab_termFreqInDoc" />
+-
+-				<Label bxml:id="tFreq" text="?" />
+-				<PushButton bxml:id="bPos" buttonData="%documentsTab_showPositions"
+-					action="showPositions" />
+-			</FlowPane>
+-		</TablePane.Row>
+-		<TablePane.Row>
+-			<TablePane styles="{verticalSpacing:1, horizontalSpacing:1}">
+-				<columns>
+-					<TablePane.Column width="1*" />
+-					<TablePane.Column />
+-				</columns>
+-				<rows>
+-					<TablePane.Row>
+-						<FlowPane>
+-							<Label text="Doc #:" />
+-							<Label bxml:id="docNum2" text="?" />
+-							<Label text=" " />
+-						</FlowPane>
+-						<FlowPane>
+-							<TablePane styles="{verticalSpacing:1, horizontalSpacing:1}">
+-								<columns>
+-									<TablePane.Column />
+-									<TablePane.Column />
+-									<TablePane.Column />
+-									<TablePane.Column />
+-									<TablePane.Column />
+-									<TablePane.Column />
+-								</columns>
+-								<rows>
+-									<TablePane.Row>
+-										<Label text="Flags: " />
+-										<Label text=" I - Indexed " />
+-										<Label text="  T - Tokenized " />
+-										<Label text="  S - Stored " />
+-										<Label text="  V - Term Vector " />
+-										<Label text=" (o - offsets; p - positions) " />
+-									</TablePane.Row>
+-									<TablePane.Row>
+-										<TablePane.Filler />
+-
+-										<Label text=" O - Omit Norms " />
+-										<Label text="  f - Omit TF " />
+-										<Label text="  L - Lazy " />
+-										<Label text="  B - Binary " />
+-									</TablePane.Row>
+-								</rows>
+-							</TablePane>
+-						</FlowPane>
+-					</TablePane.Row>
+-				</rows>
+-			</TablePane>
+-		</TablePane.Row>
+-		<TablePane.Row height="1*">
+-			<ScrollPane horizontalScrollBarPolicy="fill_to_capacity" styles="{backgroundColor:11}">
+-				<view>
+-					<TableView bxml:id="docTable">
++	<bottom>
++		<TablePane styles="{verticalSpacing:5,horizontalSpacing:1,padding:5,backgroundColor:11}">
++			<columns>
++				<TablePane.Column width="1*" />
++			</columns>
++			<rows>
++				<TablePane.Row>
++					<TablePane>
+ 						<columns>
+-							<TableView.Column name="field"
+-								headerData="%documentsTab_docTable_col1" />
+-							<TableView.Column name="itsvopfolb"
+-								headerData="%documentsTab_docTable_col2" />
+-							<TableView.Column name="norm"
+-								headerData="%documentsTab_docTable_col3" />
+-							<TableView.Column name="value"
+-								headerData="%documentsTab_docTable_col4" width="1*" />
++							<TablePane.Column width="1*" />
++							<TablePane.Column />
+ 						</columns>
++						<rows>
++							<TablePane.Row>
++								<FlowPane>
++									<Label text="Doc #:" />
++									<Label bxml:id="docNum2" text="?" />
++									<Label text=" " />
++								</FlowPane>
++								<FlowPane>
++									<TablePane styles="{verticalSpacing:1, horizontalSpacing:1}">
++										<columns>
++											<TablePane.Column />
++											<TablePane.Column />
++											<TablePane.Column />
++											<TablePane.Column />
++											<TablePane.Column />
++											<TablePane.Column />
++										</columns>
++										<rows>
++											<TablePane.Row>
++												<Label text="Flags: " />
++												<Label text=" I - Indexed " />
++												<Label text=" T - Tokenized " />
++												<Label text=" S - Stored " />
++												<Label text=" V - Term Vector " />
++												<Label text=" (o - offsets; p - positions; a - payloads) " />
++											</TablePane.Row>
++											<TablePane.Row>
++												<TablePane.Filler />
++												<Label text=" P - Payloads" />
++												<Label text=" t - Index options" />
++												<Label text=" O - Omit Norms " />
++												<!--Label text="  f - Omit TF " /-->
++												<Label text=" L - Lazy " />
++												<Label text=" B - Binary " />
++											</TablePane.Row>
++										</rows>
++									</TablePane>
++								</FlowPane>
++							</TablePane.Row>
++						</rows>
++					</TablePane>
++				</TablePane.Row>
++				<TablePane.Row height="1*">
++					<Border styles="{padding:1}">
++						<content>
++							<ScrollPane horizontalScrollBarPolicy="fill_to_capacity" styles="{backgroundColor:11}">
++								<view>
++									<TableView bxml:id="docTable">
++										<columns>
++											<TableView.Column name="name"
++																				headerData="%documentsTab_docTable_col1" />
++											<TableView.Column name="itsvopatolb"
++																				headerData="%documentsTab_docTable_col2" />
++											<TableView.Column name="docvaluestype"
++																				headerData="%documentsTab_docTable_col3" />
++											<TableView.Column name="norm"
++																				headerData="%documentsTab_docTable_col4" />
++											<TableView.Column name="value"
++																				headerData="%documentsTab_docTable_col5" width="1*" />
++										</columns>
+ 
+-					</TableView>
+-				</view>
+-				<columnHeader>
+-					<TableViewHeader tableView="$docTable" />
+-				</columnHeader>
+-			</ScrollPane>
+-		</TablePane.Row>
+-	</rows>
++									</TableView>
++								</view>
++								<columnHeader>
++									<TableViewHeader tableView="$docTable" />
++								</columnHeader>
++							</ScrollPane>
++						</content>
++					</Border>
++				</TablePane.Row>
++				<TablePane.Row>
++					<BoxPane orientation="vertical">
++						<Label text="%documentsTab_indexOptionsNote1" styles="{wrapText:true}"/>
++						<Label text="%documentsTab_indexOptionsNote2" styles="{wrapText:true}"/>
++					</BoxPane>
++				</TablePane.Row>
++			</rows>
++		</TablePane>
++	</bottom>
+ </luke:DocumentsTab>
+Index: src/org/apache/lucene/luke/ui/DocumentsTab.java
+===================================================================
+--- src/org/apache/lucene/luke/ui/DocumentsTab.java	(revision 1655665)
++++ src/org/apache/lucene/luke/ui/DocumentsTab.java	(working copy)
+@@ -17,8 +17,9 @@
+  * limitations under the License.
+  */
+ 
+-import java.io.IOException;
++import java.io.*;
+ import java.net.URL;
++import java.util.Arrays;
+ 
+ import org.apache.lucene.document.Document;
+ import org.apache.lucene.document.Field;
+@@ -41,31 +42,25 @@
+ import org.apache.lucene.luke.core.Util;
+ import org.apache.lucene.luke.core.decoders.Decoder;
+ import org.apache.lucene.luke.ui.LukeWindow.LukeMediator;
++import org.apache.lucene.search.DocIdSetIterator;
+ import org.apache.lucene.search.IndexSearcher;
+ import org.apache.lucene.search.Query;
+ import org.apache.lucene.search.TermQuery;
++import org.apache.lucene.search.similarities.DefaultSimilarity;
++import org.apache.lucene.search.similarities.Similarity;
++import org.apache.lucene.search.similarities.TFIDFSimilarity;
++import org.apache.lucene.util.Bits;
+ import org.apache.lucene.util.BytesRef;
+ import org.apache.pivot.beans.BXML;
+ import org.apache.pivot.beans.Bindable;
+-import org.apache.pivot.collections.ArrayList;
+-import org.apache.pivot.collections.HashMap;
+-import org.apache.pivot.collections.List;
+-import org.apache.pivot.collections.Map;
++import org.apache.pivot.collections.*;
+ import org.apache.pivot.util.Resources;
+ import org.apache.pivot.util.concurrent.Task;
+ import org.apache.pivot.util.concurrent.TaskExecutionException;
+ import org.apache.pivot.util.concurrent.TaskListener;
+-import org.apache.pivot.wtk.Action;
+-import org.apache.pivot.wtk.Component;
+-import org.apache.pivot.wtk.Label;
+-import org.apache.pivot.wtk.ListButton;
+-import org.apache.pivot.wtk.TablePane;
+-import org.apache.pivot.wtk.TableView;
+-import org.apache.pivot.wtk.TaskAdapter;
+-import org.apache.pivot.wtk.TextArea;
+-import org.apache.pivot.wtk.TextInput;
++import org.apache.pivot.wtk.*;
+ 
+-public class DocumentsTab extends TablePane implements Bindable {
++public class DocumentsTab extends SplitPane implements Bindable {
+ 
+   private int iNum;
+   @BXML
+@@ -91,6 +86,20 @@
+   @BXML
+   private TextArea decText;
+ 
++  @BXML
++  private PushButton bPos;
++  @BXML
++  private PosAndOffsetsWindow posAndOffsetsWindow;
++
++  @BXML
++  private TermVectorWindow tvWindow;
++
++  @BXML
++  private FieldDataWindow fieldDataWindow;
++
++  @BXML
++  private FieldNormWindow fieldNormWindow;
++
+   private java.util.List<String> fieldNames = null;
+ 
+   // this gets injected by LukeWindow at init
+@@ -99,7 +108,8 @@
+   private Resources resources;
+ 
+   private TermsEnum te;
+-  private DocsAndPositionsEnum td;
++  //private DocsAndPositionsEnum td;
++  private DocsEnum td;
+ 
+   private String fld;
+   private Term lastTerm;
+@@ -218,6 +228,15 @@
+       fieldsList.setSelectedIndex(0);
+     }
+     maxDocs.setText(String.valueOf(ir.maxDoc() - 1));
++
++    bPos.setAction(new Action() {
++      @Override
++      public void perform(Component component) {
++        showPositionsWindow();
++      }
++    });
++
++    addlListenerToDocTable();
+   }
+ 
+   private void showDoc(int incr) {
+@@ -242,6 +261,11 @@
+       }
+       docNum.setText(String.valueOf(iNum));
+ 
++      td = null;
++      tdNum.setText("?");
++      tFreq.setText("?");
++      tdMax.setText("?");
++
+       org.apache.lucene.util.Bits live = ar.getLiveDocs();
+       if (live == null || live.get(iNum)) {
+         Task<Object> populateTableTask = new Task<Object>() {
+@@ -314,7 +338,7 @@
+ 
+   public void popTableWithDoc(int docid, Document doc) {
+     docNum.setText(String.valueOf(docid));
+-    List<Map<String,String>> tableData = new ArrayList<Map<String,String>>();
++    List<Map<String,Object>> tableData = new ArrayList<Map<String,Object>>();
+     docTable.setTableData(tableData);
+ 
+     // putProperty(table, "doc", doc);
+@@ -326,10 +350,9 @@
+ 
+     docNum2.setText(String.valueOf(docid));
+     for (int i = 0; i < indexFields.size(); i++) {
+-      Map<String,String> row = new HashMap<String,String>();
+-
+       IndexableField[] fields = doc.getFields(indexFields.get(i));
+-      if (fields == null) {
++      if (fields == null || fields.length == 0) {
++        Map<String,Object> row = new HashMap<String,Object>();
+         tableData.add(row);
+         addFieldRow(row, indexFields.get(i), null, docid);
+         continue;
+@@ -339,6 +362,7 @@
+         // System.out.println("f.len=" + fields[j].getBinaryLength() +
+         // ", doc.len=" + doc.getBinaryValue(indexFields[i]).length);
+         // }
++        Map<String,Object> row = new HashMap<String,Object>();
+         tableData.add(row);
+         addFieldRow(row, indexFields.get(i), fields[j], docid);
+       }
+@@ -345,7 +369,14 @@
+     }
+   }
+ 
+-  private void addFieldRow(Map<String,String> row, String fName, IndexableField field, int docid) {
++  private static final String FIELDROW_KEY_NAME = "name";
++  private static final String FIELDROW_KEY_FLAGS = "itsvopatolb";
++  private static final String FIELDROW_KEY_DVTYPE = "docvaluestype";
++  private static final String FIELDROW_KEY_NORM = "norm";
++  private static final String FIELDROW_KEY_VALUE = "value";
++  private static final String FIELDROW_KEY_FIELD = "field";
++
++  private void addFieldRow(Map<String,Object> row, String fName, IndexableField field, int docid) {
+     java.util.Map<String,Decoder> decoders = lukeMediator.getDecoders();
+     Decoder defDecoder = lukeMediator.getDefDecoder();
+ 
+@@ -353,29 +384,32 @@
+     // putProperty(row, "field", f);
+     // putProperty(row, "fName", fName);
+ 
+-    row.put("field", fName);
+-    row.put("itsvopfolb", Util.fieldFlags(f));
++    row.put(FIELDROW_KEY_FIELD, field);
+ 
++    row.put(FIELDROW_KEY_NAME, fName);
++    row.put(FIELDROW_KEY_FLAGS, Util.fieldFlags(f, infos.fieldInfo(fName)));
++    row.put(FIELDROW_KEY_DVTYPE, Util.docValuesType(infos.fieldInfo(fName)));
++
+     // if (f == null) {
+     // setBoolean(cell, "enabled", false);
+     // }
+ 
+-    if (f != null) {
++    if (fName != null) {
+       try {
+         FieldInfo info = infos.fieldInfo(fName);
+         if (info.hasNorms()) {
+           NumericDocValues norms = ar.getNormValues(fName);
+-          String val = Long.toString(norms.get(docid));
+-          row.put("norm", String.valueOf(norms.get(docid)));
++          String norm = String.valueOf(norms.get(docid)) + " (" + Util.normType(info) + ")";
++          row.put(FIELDROW_KEY_NORM, norm);
+         } else {
+-          row.put("norm", "---");
++          row.put(FIELDROW_KEY_NORM, "---");
+         }
+       } catch (IOException ioe) {
+         ioe.printStackTrace();
+-        row.put("norm", "!?!");
++        row.put(FIELDROW_KEY_NORM, "!?!");
+       }
+     } else {
+-      row.put("norm", "---");
++      row.put(FIELDROW_KEY_NORM, "---");
+       // setBoolean(cell, "enabled", false);
+     }
+ 
+@@ -395,15 +429,16 @@
+         if (f.fieldType().stored()) {
+           text = dec.decodeStored(f.name(), f);
+         } else {
+-          text = dec.decodeTerm(f.name(), text);
++          //text = dec.decodeTerm(f.name(), text);
++          text = dec.decodeTerm(f.name(), f.binaryValue());
+         }
+       } catch (Throwable e) {
+         // TODO:
+         // setColor(cell, "foreground", Color.RED);
+       }
+-      row.put("value", Util.escape(text));
++      row.put(FIELDROW_KEY_VALUE, Util.escape(text));
+     } else {
+-      row.put("value", "<not present or not stored>");
++      row.put(FIELDROW_KEY_VALUE, "<not present or not stored>");
+       // setBoolean(cell, "enabled", false);
+     }
+   }
+@@ -428,7 +463,7 @@
+         try {
+ 
+           fld = (String) fieldsList.getSelectedItem();
+-          System.out.println("fld:" + fld);
++          //System.out.println("fld:" + fld);
+           Terms terms = MultiFields.getTerms(ir, fld);
+           te = terms.iterator(null);
+           BytesRef term = te.next();
+@@ -472,7 +507,7 @@
+       @Override
+       public void taskExecuted(Task<Object> task) {
+         try {
+-          DocsAndPositionsEnum td = MultiFields.getTermPositionsEnum(ir, null, lastTerm.field(), lastTerm.bytes());
++          DocsEnum td = MultiFields.getTermDocsEnum(ir, null, lastTerm.field(), lastTerm.bytes());
+           td.nextDoc();
+           tdNum.setText("1");
+           DocumentsTab.this.td = td;
+@@ -549,7 +584,7 @@
+ 
+   }
+ 
+-  private void showTerm(final Term t) {
++  protected void showTerm(final Term t) {
+     if (t == null) {
+       // TODO:
+       // showStatus("No terms?!");
+@@ -571,7 +606,8 @@
+     String s = null;
+     boolean decodeErr = false;
+     try {
+-      s = dec.decodeTerm(t.field(), t.text());
++      //s = dec.decodeTerm(t.field(), t.text());
++      s = dec.decodeTerm(t.field(), t.bytes());
+     } catch (Throwable e) {
+       s = e.getMessage();
+       decodeErr = true;
+@@ -580,7 +616,8 @@
+     termText.setText(t.text());
+ 
+     if (!s.equals(t.text())) {
+-      decText.setText(s);
++      String decoded = s + " (by " + dec.toString() + ")";
++      decText.setText(decoded);
+ 
+       if (decodeErr) {
+         // setColor(rawText, "foreground", Color.RED);
+@@ -613,14 +650,11 @@
+ 
+         try {
+           int freq = ir.docFreq(t);
+-          dFreq.setText(String.valueOf(freq));
+-
+           tdMax.setText(String.valueOf(freq));
+         } catch (Exception e) {
+           e.printStackTrace();
+           // TODO:
+           // showStatus(e.getMessage());
+-          dFreq.setText("?");
+         }
+         // ai.setActive(false);
+       }
+@@ -670,17 +704,20 @@
+           String rawString = rawTerm != null ? rawTerm.utf8ToString() : null;
+ 
+           if (te == null || !DocumentsTab.this.fld.equals(fld) || !text.equals(rawString)) {
++            // seek for requested term
+             Terms terms = MultiFields.getTerms(ir, fld);
+             te = terms.iterator(null);
+ 
+             DocumentsTab.this.fld = fld;
+             status = te.seekCeil(new BytesRef(text));
+-            if (status.equals(SeekStatus.FOUND)) {
++            if (status.equals(SeekStatus.FOUND) || status.equals(SeekStatus.NOT_FOUND)) {
++              // precise term or different term after the requested term was found.
+               rawTerm = te.term();
+             } else {
+               rawTerm = null;
+             }
+           } else {
++            // move to next term
+             rawTerm = te.next();
+           }
+           if (rawTerm == null) { // proceed to next field
+@@ -696,7 +733,7 @@
+               te = terms.iterator(null);
+               rawTerm = te.next();
+               DocumentsTab.this.fld = fld;
+-              break;
++              //break;
+             }
+           }
+           if (rawTerm == null) {
+@@ -744,6 +781,7 @@
+         try {
+           Document doc = ir.document(td.docID());
+           docNum.setText(String.valueOf(td.docID()));
++          iNum = td.docID();
+ 
+           tFreq.setText(String.valueOf(td.freq()));
+ 
+@@ -767,6 +805,38 @@
+ 
+   }
+ 
++  private void showPositionsWindow() {
++    try {
++      if (td == null) {
++        Alert.alert(MessageType.WARNING, (String)resources.get("documentsTab_msg_docNotSelected"), getWindow());
++      } else {
++        // create new Enum to show positions info
++        DocsAndPositionsEnum pe = MultiFields.getTermPositionsEnum(ir, null, lastTerm.field(), lastTerm.bytes());
++        if (pe == null) {
++          Alert.alert(MessageType.INFO, (String)resources.get("documentsTab_msg_positionNotIndexed"), getWindow());
++        } else {
++          // enumerate docId to the current doc
++          while(pe.docID() != td.docID()) {
++            if (pe.nextDoc() == DocIdSetIterator.NO_MORE_DOCS) {
++              // this must not happen!
++              Alert.alert(MessageType.ERROR, (String)resources.get("documentsTab_msg_noPositionInfo"), getWindow());
++            }
++          }
++          try {
++            posAndOffsetsWindow.initPositionInfo(pe, lastTerm);
++            posAndOffsetsWindow.open(getDisplay(), getWindow());
++          } catch (Exception e) {
++            // TODO:
++            e.printStackTrace();
++          }
++        }
++      }
++    } catch (Exception e) {
++      // TODO
++      e.printStackTrace();
++    }
++  }
++
+   public void showAllTermDoc() {
+     final IndexReader ir = lukeMediator.getIndexInfo().getReader();
+     if (ir == null) {
+@@ -825,4 +895,178 @@
+ 
+   }
+ 
++  private void addlListenerToDocTable() {
++    docTable.getComponentMouseButtonListeners().add(new ComponentMouseButtonListener.Adapter() {
++      @Override
++      public boolean mouseClick(Component component, Mouse.Button button, int i, int i1, int i2) {
++        final Map<String, Object> row = (Map<String, Object>) docTable.getSelectedRow();
++        if (row == null) {
++          System.out.println("No field selected.");
++          return false;
++        }
++        if (button.name().equals(Mouse.Button.RIGHT.name())) {
++          MenuPopup popup = new MenuPopup();
++          Menu menu = new Menu();
++          Menu.Section section = new Menu.Section();
++          Menu.Item item1 = new Menu.Item(resources.get("documentsTab_docTable_popup_menu1"));
++          item1.setAction(new Action() {
++            @Override
++            public void perform(Component component) {
++              String name = (String)row.get(FIELDROW_KEY_NAME);
++              try {
++                Terms terms = ir.getTermVector(iNum, name);
++                if (terms == null) {
++                  String msg = "DocId: " + iNum + ", field: " + name;
++                  Alert.alert(MessageType.WARNING, "Term vector not avalable for " + msg, getWindow());
++                } else {
++                  showTermVectorWindow(name, terms);
++                }
++              } catch (IOException e) {
++                // TODO:
++                e.printStackTrace();
++              }
++
++            }
++          });
++          Menu.Item item2 = new Menu.Item(resources.get("documentsTab_docTable_popup_menu2"));
++          item2.setAction(new Action() {
++            @Override
++            public void perform(Component component) {
++              String name = (String)row.get(FIELDROW_KEY_NAME);
++              IndexableField field = (IndexableField)row.get(FIELDROW_KEY_FIELD);
++              if (field == null) {
++                Alert.alert(MessageType.WARNING, (String)resources.get("documentsTab_msg_noDataAvailable"), getWindow());
++              } else {
++                showFieldDataWindow(name, field);
++              }
++            }
++          });
++          Menu.Item item3 = new Menu.Item(resources.get("documentsTab_docTable_popup_menu3"));
++          item3.setAction(new Action() {
++            @Override
++            public void perform(Component component) {
++              String name = (String)row.get(FIELDROW_KEY_NAME);
++              IndexableField field = (IndexableField)row.get(FIELDROW_KEY_FIELD);
++              FieldInfo info = infos.fieldInfo(name);
++              if (field == null) {
++                Alert.alert(MessageType.WARNING, (String)resources.get("documentsTab_msg_noDataAvailable"), getWindow());
++              } else if (!info.isIndexed() || !info.hasNorms()) {
++                Alert.alert(MessageType.WARNING, (String)resources.get("documentsTab_msg_noNorm"), getWindow());
++              } else {
++                showFieldNormWindow(name);
++              }
++            }
++          });
++          Menu.Item item4 = new Menu.Item(resources.get("documentsTab_docTable_popup_menu4"));
++          item4.setAction(new Action() {
++            @Override
++            public void perform(Component component) {
++              String name = (String)row.get(FIELDROW_KEY_NAME);
++              IndexableField field = (IndexableField)row.get(FIELDROW_KEY_FIELD);
++              if (ir == null) {
++                Alert.alert(MessageType.ERROR, (String)resources.get("documentsTab_noOrClosedIndex"), getWindow());
++              } else if (field == null) {
++                Alert.alert(MessageType.WARNING, (String)resources.get("documentsTab_msg_noDataAvailable"), getWindow());
++              } else {
++                saveFieldData(field);
++              }
++            }
++          });
++          section.add(item1);
++          section.add(item2);
++          section.add(item3);
++          section.add(item4);
++          menu.getSections().add(section);
++          popup.setMenu(menu);
++          popup.open(getWindow(), getMouseLocation().x + 20, getMouseLocation().y + 50);
++          return true;
++        }
++        return false;
++      }
++    });
++
++  }
++
++  private void showTermVectorWindow(String fieldName, Terms tv) {
++    try {
++      tvWindow.initTermVector(fieldName, tv);
++    } catch (IOException e) {
++      // TODO
++      e.printStackTrace();
++    }
++    tvWindow.open(getDisplay(), getWindow());
++  }
++
++  private void showFieldDataWindow(String fieldName, IndexableField field) {
++    fieldDataWindow.initFieldData(fieldName, field);
++    fieldDataWindow.open(getDisplay(), getWindow());
++  }
++
++  private static TFIDFSimilarity defaultSimilarity = new DefaultSimilarity();
++  private void showFieldNormWindow(String fieldName) {
++    if (ar != null) {
++      try {
++        NumericDocValues norms = ar.getNormValues(fieldName);
++        fieldNormWindow.initFieldNorm(iNum, fieldName, norms);
++        fieldNormWindow.open(getDisplay(), getWindow());
++      } catch (Exception e) {
++        Alert.alert(MessageType.ERROR, (String)resources.get("documentsTab_msg_errorNorm"), getWindow());
++        e.printStackTrace();
++      }
++    }
++  }
++
++  private void saveFieldData(IndexableField field) {
++    byte[] data = null;
++    if (field.binaryValue() != null) {
++      BytesRef bytes = field.binaryValue();
++      data = new byte[bytes.length];
++      System.arraycopy(bytes.bytes, bytes.offset, data, 0,
++        bytes.length);
++    }
++    else {
++      try {
++        data = field.stringValue().getBytes("UTF-8");
++      } catch (UnsupportedEncodingException uee) {
++        uee.printStackTrace();
++        data = field.stringValue().getBytes();
++      }
++    }
++    if (data == null || data.length == 0) {
++      Alert.alert(MessageType.WARNING, (String)resources.get("documentsTab_msg_noDataAvailable"), getWindow());
++    }
++
++    final byte[] fieldData = Arrays.copyOf(data, data.length);
++    final FileBrowserSheet fileBrowserSheet = new FileBrowserSheet(FileBrowserSheet.Mode.SAVE_AS);
++    fileBrowserSheet.open(getWindow(), new SheetCloseListener() {
++      @Override
++      public void sheetClosed(Sheet sheet) {
++        if (sheet.getResult()) {
++          Sequence<File> selectedFiles = fileBrowserSheet.getSelectedFiles();
++          File file = selectedFiles.get(0);
++          try {
++            OutputStream os = new FileOutputStream(file);
++            int delta = fieldData.length / 100;
++            if (delta == 0) delta = 1;
++            for (int i = 0; i < fieldData.length; i++) {
++              os.write(fieldData[i]);
++              // TODO: show progress
++              //if (i % delta == 0) {
++              // setInteger(bar, "value", i / delta);
++              //}
++            }
++            os.flush();
++            os.close();
++            Alert.alert(MessageType.INFO, "Saved to " + file.getAbsolutePath(), getWindow());
++          } catch (IOException e) {
++            e.printStackTrace();
++            Alert.alert(MessageType.ERROR, "Can't save to : " + file.getAbsoluteFile(), getWindow());
++          }
++        } else {
++          Alert.alert(MessageType.INFO, "You didn't select anything.", getWindow());
++        }
++
++      }
++    });
++  }
+ }
+Index: src/org/apache/lucene/luke/ui/FieldDataWindow.bxml
+===================================================================
+--- src/org/apache/lucene/luke/ui/FieldDataWindow.bxml	(revision 0)
++++ src/org/apache/lucene/luke/ui/FieldDataWindow.bxml	(working copy)
+@@ -0,0 +1,57 @@
++<luke:FieldDataWindow bxml:id="fieldData" icon="/img/luke.gif"
++													title="%fieldDataWindow_title" xmlns:bxml="http://pivot.apache.org/bxml"
++													xmlns:luke="org.apache.lucene.luke.ui" xmlns:content="org.apache.pivot.wtk.content"
++													xmlns="org.apache.pivot.wtk">
++	<content>
++		<TablePane styles="{verticalSpacing:10}">
++			<columns>
++				<TablePane.Column width="1*"/>
++			</columns>
++			<rows>
++				<TablePane.Row>
++					<TablePane styles="{verticalSpacing:1,horizontalSpacing:1}">
++						<columns>
++							<TablePane.Column />
++							<TablePane.Column width="1*"/>
++						</columns>
++						<rows>
++							<TablePane.Row>
++								<Label text="Field name:" styles="{font:{bold:true},backgroundColor:'#dce0e7',padding:2}"/>
++								<Label bxml:id="name" text="?" styles="{backgroundColor:'#fcfdfd',padding:2}"/>
++							</TablePane.Row>
++							<TablePane.Row>
++								<Label text="Field length: " styles="{font:{bold:true},backgroundColor:'#f1f1f1',padding:2}"/>
++								<Label bxml:id="length" text="?" styles="{backgroundColor:11,padding:2}"/>
++							</TablePane.Row>
++							<TablePane.Row>
++								<Label text="Show content as: " styles="{font:{bold:true},backgroundColor:'#dce0e7',padding:2}"/>
++								<Spinner bxml:id="cDecoder" />
++							</TablePane.Row>
++						</rows>
++					</TablePane>
++				</TablePane.Row>
++
++				<TablePane.Row>
++					<Label bxml:id="error" text="%fieldDataWindow_decodeError" visible="false"
++								 styles="{color:'red',padding:2,wrapText:true}" preferredWidth="500"/>
++				</TablePane.Row>
++
++				<TablePane.Row>
++					<Border styles="{padding:1}">
++						<ScrollPane>
++							<TextArea bxml:id="data" preferredWidth="500" preferredHeight="250" editable="false"/>
++						</ScrollPane>
++					</Border>
++				</TablePane.Row>
++
++				<TablePane.Row>
++					<BoxPane orientation="horizontal" styles="{horizontalAlignment:'right'}">
++						<PushButton buttonData="%label_ok"
++												ButtonPressListener.buttonPressed="fieldData.close()">
++						</PushButton>
++					</BoxPane>
++				</TablePane.Row>
++			</rows>
++		</TablePane>
++	</content>
++</luke:FieldDataWindow>
+\ No newline at end of file
+
+Property changes on: src/org/apache/lucene/luke/ui/FieldDataWindow.bxml
+___________________________________________________________________
+Added: svn:mime-type
+## -0,0 +1 ##
++text/xml
+\ No newline at end of property
+Index: src/org/apache/lucene/luke/ui/FieldDataWindow.java
+===================================================================
+--- src/org/apache/lucene/luke/ui/FieldDataWindow.java	(revision 0)
++++ src/org/apache/lucene/luke/ui/FieldDataWindow.java	(working copy)
+@@ -0,0 +1,222 @@
++package org.apache.lucene.luke.ui;
++
++import org.apache.lucene.analysis.payloads.PayloadHelper;
++import org.apache.lucene.document.DateTools;
++import org.apache.lucene.document.Field;
++import org.apache.lucene.index.IndexableField;
++import org.apache.lucene.luke.core.Util;
++import org.apache.lucene.util.BytesRef;
++import org.apache.lucene.util.NumericUtils;
++import org.apache.pivot.beans.BXML;
++import org.apache.pivot.beans.Bindable;
++import org.apache.pivot.collections.ArrayList;
++import org.apache.pivot.collections.List;
++import org.apache.pivot.collections.Map;
++import org.apache.pivot.serialization.SerializationException;
++import org.apache.pivot.util.Resources;
++import org.apache.pivot.wtk.*;
++
++import java.io.UnsupportedEncodingException;
++import java.net.URL;
++import java.util.Date;
++
++public class FieldDataWindow extends Dialog implements Bindable {
++
++  @BXML
++  private Label name;
++  @BXML
++  private Label length;
++  @BXML
++  private Spinner cDecoder;
++  @BXML
++  private Label error;
++  @BXML
++  private TextArea data;
++
++  private Resources resources;
++
++  private IndexableField field;
++
++  @Override
++  public void initialize(Map<String, Object> map, URL url, Resources resources) {
++    this.resources = resources;
++  }
++
++  public void initFieldData(String fieldName, IndexableField field) {
++    this.field = field;
++
++    setContentDecoders();
++
++    name.setText(fieldName);
++    ContentDecoder dec = ContentDecoder.defDecoder();
++    dec.decode(field);
++    data.setText(String.valueOf(dec.value));
++    length.setText(Integer.toString(dec.len));
++  }
++
++  private void setContentDecoders() {
++    ArrayList<Object> decoders = new ArrayList<Object>();
++    ContentDecoder[] contentDecoders = ContentDecoder.values();
++    for (int i = contentDecoders.length - 1; i >= 0; i--) {
++      decoders.add(contentDecoders[i]);
++    }
++    cDecoder.setSpinnerData(decoders);
++    cDecoder.setSelectedItem(ContentDecoder.STRING_UTF8);
++
++    cDecoder.getSpinnerSelectionListeners().add(new SpinnerSelectionListener.Adapter() {
++      @Override
++      public void selectedItemChanged(Spinner spinner, Object o) {
++        ContentDecoder dec = (ContentDecoder) spinner.getSelectedItem();
++        if (dec == null) {
++          dec = ContentDecoder.defDecoder();
++        }
++        dec.decode(field);
++        data.setText(dec.value);
++        length.setText(Integer.toString(dec.len));
++        if (dec.warn) {
++          error.setVisible(true);
++          try {
++            data.setStyles("{color:'#bdbdbd'}");
++          } catch (SerializationException e) {
++            e.printStackTrace();
++          }
++          data.setEnabled(false);
++        } else {
++          error.setVisible(false);
++          try {
++            data.setStyles("{color:'#000000'}");
++          } catch (SerializationException e) {
++            e.printStackTrace();
++          }
++          data.setEnabled(true);
++        }
++      }
++    });
++  }
++
++
++  enum ContentDecoder {
++    STRING_UTF8("String UTF-8"),
++    STRING("String default enc."),
++    HEXDUMP("Hexdump"),
++    DATETIME("Date / Time"),
++    NUMERIC("Numeric"),
++    LONG("Long (prefix-coded)"),
++    ARRAY_OF_INT("Array of int"),
++    ARRAY_OF_FLOAT("Array of float");
++
++    private String strExpr;
++    ContentDecoder(String strExpr) {
++      this.strExpr = strExpr;
++    }
++
++    @Override
++    public String toString() {
++      return strExpr;
++    }
++
++    public static ContentDecoder defDecoder() {
++      return STRING_UTF8;
++    }
++
++    String value = "";  // decoded value
++    int len;       // length of decoded value
++    boolean warn;  // set to true if decode failed
++
++    public void decode(IndexableField field) {
++      if (field == null) {
++        return ;
++      }
++      warn = false;
++      byte[] data = null;
++      if (field.binaryValue() != null) {
++        BytesRef bytes = field.binaryValue();
++        data = new byte[bytes.length];
++        System.arraycopy(bytes.bytes, bytes.offset, data, 0,
++          bytes.length);
++      }
++      else if (field.stringValue() != null) {
++        try {
++          data = field.stringValue().getBytes("UTF-8");
++        } catch (UnsupportedEncodingException uee) {
++          warn = true;
++          uee.printStackTrace();
++          data = field.stringValue().getBytes();
++        }
++      }
++      if (data == null) data = new byte[0];
++
++      switch(this) {
++        case STRING_UTF8:
++          value = field.stringValue();
++          if (value != null) len = value.length();
++          break;
++        case STRING:
++          value = new String(data);
++          len = value.length();
++          break;
++        case HEXDUMP:
++          value = Util.bytesToHex(data, 0, data.length, true);
++          len = data.length;
++          break;
++        case DATETIME:
++          try {
++            Date d = DateTools.stringToDate(field.stringValue());
++            value = d.toString();
++            len = 1;
++          } catch (Exception e) {
++            warn = true;
++            value = Util.bytesToHex(data, 0, data.length, true);
++          }
++          break;
++        case NUMERIC:
++          if (field.numericValue() != null) {
++            value = field.numericValue().toString() + " (" + field.numericValue().getClass().getSimpleName() + ")";
++          } else {
++            warn = true;
++            value = Util.bytesToHex(data, 0, data.length, true);
++          }
++          break;
++        case LONG:
++          try {
++            long num = NumericUtils.prefixCodedToLong(new BytesRef(field.stringValue()));
++            value = String.valueOf(num);
++            len = 1;
++          } catch (Exception e) {
++            warn = true;
++            value = Util.bytesToHex(data, 0, data.length, true);
++          }
++          break;
++        case ARRAY_OF_INT:
++          if (data.length % 4 == 0) {
++            len = data.length / 4;
++            StringBuilder sb = new StringBuilder();
++            for (int k = 0; k < data.length; k += 4) {
++              if (k > 0) sb.append(',');
++              sb.append(String.valueOf(PayloadHelper.decodeInt(data, k)));
++            }
++            value = sb.toString();
++          } else {
++            warn = true;
++            value = Util.bytesToHex(data, 0, data.length, true);
++          }
++          break;
++        case ARRAY_OF_FLOAT:
++          if (data.length % 4 == 0) {
++            len = data.length / 4;
++            StringBuilder sb = new StringBuilder();
++            for (int k = 0; k < data.length; k += 4) {
++              if (k > 0) sb.append(',');
++              sb.append(String.valueOf(PayloadHelper.decodeFloat(data, k)));
++            }
++            value = sb.toString();
++          } else {
++            warn = true;
++            value = Util.bytesToHex(data, 0, data.length, true);
++          }
++          break;
++      }
++    }
++
++  }
++}
+Index: src/org/apache/lucene/luke/ui/FieldNormWindow.bxml
+===================================================================
+--- src/org/apache/lucene/luke/ui/FieldNormWindow.bxml	(revision 0)
++++ src/org/apache/lucene/luke/ui/FieldNormWindow.bxml	(working copy)
+@@ -0,0 +1,75 @@
++<luke:FieldNormWindow bxml:id="fieldNorm" icon="/img/luke.gif"
++											 title="%fieldNormWindow_title" xmlns:bxml="http://pivot.apache.org/bxml"
++											 xmlns:luke="org.apache.lucene.luke.ui" xmlns:content="org.apache.pivot.wtk.content"
++											 xmlns="org.apache.pivot.wtk">
++	<content>
++		<TablePane styles="{verticalSpacing:10}">
++			<columns>
++				<TablePane.Column width="1*"/>
++			</columns>
++			<rows>
++				<TablePane.Row>
++					<TablePane styles="{verticalSpacing:5,horizontalSpacing:5}">
++						<columns>
++							<TablePane.Column />
++							<TablePane.Column width="1*"/>
++						</columns>
++						<rows>
++							<TablePane.Row>
++								<Label text="Field name: " />
++								<Label bxml:id="field" text="?" styles="{font:{bold:true}}"/>
++							</TablePane.Row>
++							<TablePane.Row>
++								<Label text="Field norm: " />
++								<Label bxml:id="normVal" text="?" styles="{font:{bold:true}}"/>
++							</TablePane.Row>
++						</rows>
++					</TablePane>
++				</TablePane.Row>
++
++				<TablePane.Row>
++					<Separator/>
++				</TablePane.Row>
++
++				<TablePane.Row>
++					<BoxPane orientation="vertical" styles="{fill:true}">
++						<Label text="%fieldNormWindow_simClass"/>
++						<BoxPane styles="{fill:true}">
++							<TextInput bxml:id="simclass" preferredWidth="400"/>
++							<PushButton bxml:id="refreshButton">
++								<buttonData>
++									<content:ButtonData icon="/img/refresh.png" />
++								</buttonData>
++							</PushButton>
++						</BoxPane>
++						<Label bxml:id="simErr" text="" styles="{color:'red'}" visible="false"/>
++					  <TablePane styles="{verticalSpacing:5,horizontalSpacing:5}">
++							<columns>
++								<TablePane.Column />
++								<TablePane.Column width="1*"/>
++							</columns>
++							<rows>
++								<TablePane.Row>
++									<Label text="%fieldNormWindow_otherNorm"/>
++									<TextInput bxml:id="otherNorm" />
++								</TablePane.Row>
++								<TablePane.Row>
++									<Label text="%fieldNormWindow_encNorm"/>
++									<Label bxml:id="encNorm" text="?"/>
++								</TablePane.Row>
++							</rows>
++						</TablePane>
++					</BoxPane>
++				</TablePane.Row>
++
++				<TablePane.Row>
++					<BoxPane orientation="horizontal" styles="{horizontalAlignment:'right'}">
++						<PushButton buttonData="%label_ok"
++												ButtonPressListener.buttonPressed="fieldNorm.close()">
++						</PushButton>
++					</BoxPane>
++				</TablePane.Row>
++			</rows>
++		</TablePane>
++	</content>
++</luke:FieldNormWindow>
+\ No newline at end of file
+
+Property changes on: src/org/apache/lucene/luke/ui/FieldNormWindow.bxml
+___________________________________________________________________
+Added: svn:mime-type
+## -0,0 +1 ##
++text/xml
+\ No newline at end of property
+Index: src/org/apache/lucene/luke/ui/FieldNormWindow.java
+===================================================================
+--- src/org/apache/lucene/luke/ui/FieldNormWindow.java	(revision 0)
++++ src/org/apache/lucene/luke/ui/FieldNormWindow.java	(working copy)
+@@ -0,0 +1,122 @@
++package org.apache.lucene.luke.ui;
++
++import org.apache.lucene.index.NumericDocValues;
++import org.apache.lucene.luke.core.Util;
++import org.apache.lucene.search.similarities.DefaultSimilarity;
++import org.apache.lucene.search.similarities.Similarity;
++import org.apache.lucene.search.similarities.TFIDFSimilarity;
++import org.apache.pivot.beans.BXML;
++import org.apache.pivot.beans.Bindable;
++import org.apache.pivot.collections.Map;
++import org.apache.pivot.util.Resources;
++import org.apache.pivot.wtk.*;
++
++import java.net.URL;
++
++public class FieldNormWindow extends Dialog implements Bindable {
++
++  @BXML
++  private Label field;
++  @BXML
++  private Label normVal;
++  @BXML
++  private TextInput simclass;
++  @BXML
++  private Label simErr;
++  @BXML
++  private PushButton refreshButton;
++  @BXML
++  private TextInput otherNorm;
++  @BXML
++  private Label encNorm;
++
++  private Resources resources;
++
++  private String fieldName;
++
++  private static TFIDFSimilarity defaultSimilarity = new DefaultSimilarity();
++
++  @Override
++  public void initialize(Map<String, Object> map, URL url, Resources resources) {
++    this.resources = resources;
++  }
++
++  public void initFieldNorm(int docId, String fieldName, NumericDocValues norms) throws Exception {
++    this.fieldName = fieldName;
++    TFIDFSimilarity sim = defaultSimilarity;
++    byte curBVal = (byte) norms.get(docId);
++    float curFVal = Util.decodeNormValue(curBVal, fieldName, sim);
++    field.setText(fieldName);
++    normVal.setText(Float.toString(curFVal));
++    simclass.setText(sim.getClass().getName());
++    otherNorm.setText(Float.toString(curFVal));
++    encNorm.setText(Float.toString(curFVal) + " (0x" + Util.byteToHex(curBVal) + ")");
++
++    refreshButton.setAction(new Action() {
++      @Override
++      public void perform(Component component) {
++        changeNorms();
++      }
++    });
++    otherNorm.getTextInputContentListeners().add(new TextInputContentListener.Adapter(){
++      @Override
++      public void textChanged(TextInput textInput) {
++        changeNorms();
++      }
++    });
++  }
++
++  private void changeNorms() {
++    String simClassString = simclass.getText();
++
++    Similarity sim = createSimilarity(simClassString);
++    TFIDFSimilarity s = null;
++    if (sim != null && (sim instanceof TFIDFSimilarity)) {
++      s = (TFIDFSimilarity)sim;
++    } else {
++      s = defaultSimilarity;
++    }
++    if (s == null) {
++      s = defaultSimilarity;
++    }
++    //setString(sim, "text", s.getClass().getName());
++    simclass.setText(s.getClass().getName());
++    try {
++      float newFVal = Float.parseFloat(otherNorm.getText());
++      long newBVal = Util.encodeNormValue(newFVal, fieldName, s);
++      float encFVal = Util.decodeNormValue(newBVal, fieldName, s);
++      encNorm.setText(String.valueOf(encFVal) + " (0x" + Util.byteToHex((byte) (newBVal & 0xFF)) + ")");
++    } catch (Exception e) {
++      // TODO:
++      e.printStackTrace();
++    }
++  }
++
++  public Similarity createSimilarity(String simClass) {
++    //Object ckSimDef = find(srchOpts, "ckSimDef");
++    //Object ckSimSweet = find(srchOpts, "ckSimSweet");
++    //Object ckSimOther = find(srchOpts, "ckSimOther");
++    //Object simClass = find(srchOpts, "simClass");
++    //Object ckSimCust = find(srchOpts, "ckSimCust");
++    //if (getBoolean(ckSimDef, "selected")) {
++    //      return new DefaultSimilarity();
++    //} else if (getBoolean(ckSimSweet, "selected")) {
++    //  return new SweetSpotSimilarity();
++    //} else if (getBoolean(ckSimOther, "selected")) {
++    try {
++      Class clazz = Class.forName(simClass);
++      if (Similarity.class.isAssignableFrom(clazz)) {
++        Similarity sim = (Similarity) clazz.newInstance();
++        simErr.setVisible(false);
++        return sim;
++      } else {
++        simErr.setText("Not a subclass of Similarity: " + clazz.getName());
++        simErr.setVisible(true);
++      }
++    } catch (Exception e) {
++      simErr.setText("Invalid similarity class " + simClass + ", using DefaultSimilarity.");
++      simErr.setVisible(true);
++    }
++    return new DefaultSimilarity();
++  }
++}
+Index: src/org/apache/lucene/luke/ui/LukeApplication_en.json
+===================================================================
+--- src/org/apache/lucene/luke/ui/LukeApplication_en.json	(revision 1655665)
++++ src/org/apache/lucene/luke/ui/LukeApplication_en.json	(working copy)
+@@ -26,6 +26,9 @@
+  sandstoneTheme: "Sandstone Theme",
+  skyTheme: "Sky Theme",
+  navyTheme: "Navy Theme",
++
++ label_ok: "OK",
++ label_clipboard: "Copy to Clipboard",
+  
+  lukeInitWindow_title: "Path to index directory:",
+  lukeInitWindow_path: "Path:",
+@@ -58,9 +61,10 @@
+  overviewTab_userData: "Current commit user data:",
+  
+  overviewTab_fieldsAndTermCounts: "Available fields and term counts per field:",
+- overviewTab_topRankingTerms: "Top Ranking Terms (Right click for more options)",
++ overviewTab_topRankingTerms: "Top Ranking Terms (Select a row and right click for more options)",
+  overviewTab_decoderWarn: "Tokens marked in red indicate decoding errors, likely due to a mismatched decoder.",
+  overviewTab_fieldSelect: "Select fields from the list below, and press button to view top terms in these fields. No selection means all fields.",
++ overviewTab_fieldsHintDecoder: "Hint: Double click 'Decoder' column, select decoder class, and press Enter to set the suitable decoder.",
+  overviewTab_topTermsHint: "Hint: use Shift-Click to select ranges, or Ctrl-Click to select multiple fields (or unselect all).",
+  overviewTab_showTopTerms: "Show top terms >>",
+  
+@@ -68,28 +72,63 @@
+  overviewTab_topTermsTable_col2: "DF",
+  overviewTab_topTermsTable_col3: "Field",
+  overviewTab_topTermsTable_col4: "Text",
++
++ overviewTab_topTermTable_popup_menu1: "Browse term docs",
++ overviewTab_topTermTable_popup_menu2: "Show all term docs",
++ overviewTab_topTermTable_popup_menu3: "Copy to clipboard",
+  
+  documentsTab_noOrClosedIndex: "FAILED: No index, or index is closed. Reopen it.",
+  documentsTab_docNumOutsideRange: "Document number outside valid range.",
+  documentsTab_browseByDocNum: "Browse by document number:",
+  documentsTab_browseByTerm: "Browse by term:",
++ documentsTab_selectField: "Select a field from the spinner below, press Next to browse terms.",
+  documentsTab_enterTermHint: "(Hint: enter a substring and press Next to start at the nearest term).",
+  documentsTab_firstTerm: "First Term",
+  documentsTab_term: "Term:",
+  documentsTab_decodedValue: "Decoded value:",
+- documentsTab_browseDocsWithTerm: "Browse documents with this term",
++ documentsTab_browseDocsWithTerm: "Browse documents with this term:",
++ documentsTab_selectTerm: "After select term, press Next to browse docs with the term.",
+  documentsTab_showAllDocs: "Show All Docs",
+  documentsTab_deleteAllDocs: "Delete All Docs",
+  documentsTab_document: "Document:",
+  documentsTab_firstDoc: "First Doc",
+  documentsTab_termFreqInDoc: "Term freq in this doc:",
+- documentsTab_showPositions: "Show Positions",
+- 
++ documentsTab_showPositions: "Show Positions and Offsets",
++ documentsTab_indexOptionsNote1: "Note: flag 't - Index options' means, 1: DOCS_ONLY; 2:DOCS_AND_FREQS; 3: DOCS_AND_FREQS_AND_POSITIONS; 4: DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS.",
++ documentsTab_indexOptionsNote2: "(See Javadocs about FieldInfo.IndexOptions for more info.)",
++
+  documentsTab_docTable_col1: "Field",
+- documentsTab_docTable_col2: "ITSVopfOLB",
+- documentsTab_docTable_col3: "Norm",
+- documentsTab_docTable_col4: "Value",
+- 
++ documentsTab_docTable_col2: "ITSVopaPtOLB",
++ documentsTab_docTable_col3: "DocValues Type",
++ documentsTab_docTable_col4: "Norm (Norm Type)",
++ documentsTab_docTable_col5: "Value",
++
++ documentsTab_docTable_popup_menu1: "Field's Term Vector",
++ documentsTab_docTable_popup_menu2: "Show Full Text",
++ documentsTab_docTable_popup_menu3: "Set Norm",
++ documentsTab_docTable_popup_menu4: "Save Field",
++
++ documentsTab_msg_docNotSelected: "Please select a term and a document for showing term positions.",
++ documentsTab_msg_positionNotIndexed: "Positions are not indexed for this term.",
++ documentsTab_msg_noPositionInfo: "No positions info ???",
++ documentsTab_msg_noDataAvailable: "No data available for this field.",
++ documentsTab_msg_noNorm: "Cannot examine norm value - this field is not indexed.",
++ documentsTab_msg_errorNorm: "Error reading norm: ",
++ documentsTab_msg_cantOverwriteDir: "Can't overwrite a directory.",
++
++ posAndOffsetsWindow_title: "Term Positions and Offsets",
++
++ termVectorWindow_title: "Term Vector",
++ termVectorWindow_field: "Term vector for the field: ",
++
++ fieldDataWindow_title: "Field Data",
++ fieldDataWindow_decodeError: "Some values could not be properly represented in this format. They are marked in grey and presented as a hex dump.",
++
++ fieldNormWindow_title: "Field Norm",
++ fieldNormWindow_simClass: "Encode other field norm using this TFIDFSimilarity (full class name): ",
++ fieldNormWindow_otherNorm: "Enter norm value: ",
++ fieldNormWindow_encNorm: "Encoded value rounded to: ",
++
+  searchTab_searchPrompt: "Enter search expression here:",
+  searchTab_update: "Update",
+  searchTab_explainStructure: "Explain Structure",
+Index: src/org/apache/lucene/luke/ui/LukeWindow.bxml
+===================================================================
+--- src/org/apache/lucene/luke/ui/LukeWindow.bxml	(revision 1655665)
++++ src/org/apache/lucene/luke/ui/LukeWindow.bxml	(working copy)
+@@ -103,7 +103,7 @@
+ 								</content>
+ 							</Border>
+ 
+-							<Border>
++							<Border styles="{backgroundColor:11,thickness:0}">
+ 								<TabPane.tabData>
+ 									<content:ButtonData icon="/img/docs.gif"
+ 										text="%lukeWindow_documentsTabText" />
+@@ -160,7 +160,10 @@
+ 					</TabPane>
+ 				</TablePane.Row>
+ 				<TablePane.Row>
+-					<Label bxml:id="statusLabel" text="" styles="{padding:2}" />
++					<BoxPane>
++						<Label bxml:id="indexName" text="" styles="{padding:2}"/>
++						<Label bxml:id="statusLabel" text="" styles="{padding:2}" />
++					</BoxPane>
+ 				</TablePane.Row>
+ 			</rows>
+ 		</TablePane>
+Index: src/org/apache/lucene/luke/ui/LukeWindow.java
+===================================================================
+--- src/org/apache/lucene/luke/ui/LukeWindow.java	(revision 1655665)
++++ src/org/apache/lucene/luke/ui/LukeWindow.java	(working copy)
+@@ -23,7 +23,6 @@
+ import java.lang.reflect.Constructor;
+ import java.net.URL;
+ import java.util.Arrays;
+-import java.util.HashMap;
+ import java.util.HashSet;
+ 
+ import org.apache.lucene.analysis.Analyzer;
+@@ -99,6 +98,8 @@
+   @BXML
+   private LukeInitWindow lukeInitWindow;
+   @BXML
++  private TabPane tabPane;
++  @BXML
+   private FilesTab filesTab;
+   @BXML
+   private DocumentsTab documentsTab;
+@@ -108,6 +109,8 @@
+   private OverviewTab overviewTab;
+   @BXML
+   private AnalyzersTab analyzersTab;
++  @BXML
++  private Label indexName;
+ 
+   private LukeMediator lukeMediator = new LukeMediator();
+ 
+@@ -439,6 +442,7 @@
+ 
+       // initPlugins();
+       showStatus("Index successfully open.");
++      indexName.setText("Index path: " + indexPath);
+     } catch (Exception e) {
+       e.printStackTrace();
+       errorMsg(e.getMessage());
+@@ -622,8 +626,13 @@
+       setComponentColor(component, "scrollButtonBackgroundColor", theme[2]);
+       setComponentColor(component, "borderColor", theme[3]);
+     } else if (component instanceof PushButton || component instanceof ListButton) {
+-      component.getComponentMouseButtonListeners().add(mouseButtonPressedListener);
+-      component.getComponentMouseListeners().add(mouseMoveListener);
++      // Listeners are added at start-up time only.
++      if (component.getComponentMouseButtonListeners().isEmpty()) {
++        component.getComponentMouseButtonListeners().add(mouseButtonPressedListener);
++      }
++      if (component.getComponentMouseListeners().isEmpty()) {
++        component.getComponentMouseListeners().add(mouseMoveListener);
++      }
+       setComponentColor(component, "color", theme[1]);
+       setComponentColor(component, "backgroundColor", theme[0]);
+       setComponentColor(component, "borderColor", theme[3]);
+@@ -679,7 +688,7 @@
+ 
+   private Directory directory;
+ 
+-  class LukeMediator {
++  public class LukeMediator {
+ 
+     // populated by LukeWindow#openIndex
+     private IndexInfo indexInfo;
+@@ -700,6 +709,14 @@
+       return overviewTab;
+     }
+ 
++    public DocumentsTab getDocumentsTab() {
++      return documentsTab;
++    }
++
++    public TabPane getTabPane() {
++      return tabPane;
++    }
++
+     public LukeWindow getLukeWindow() {
+       return LukeWindow.this;
+     }
+Index: src/org/apache/lucene/luke/ui/OverviewTab.bxml
+===================================================================
+--- src/org/apache/lucene/luke/ui/OverviewTab.bxml	(revision 1655665)
++++ src/org/apache/lucene/luke/ui/OverviewTab.bxml	(working copy)
+@@ -148,9 +148,12 @@
+ 											</columns>
+ 											<rows>
+ 												<TablePane.Row height="-1">
+-													<Label styles="{backgroundColor:11,padding:2}" text="%overviewTab_fieldSelect" />
++													<Label styles="{backgroundColor:11,padding:2,wrapText:true}" text="%overviewTab_fieldSelect" />
+ 												</TablePane.Row>
+ 												<TablePane.Row height="-1">
++													<Label styles="{backgroundColor:11,padding:2,wrapText:true}" text="%overviewTab_fieldsHintDecoder" />
++												</TablePane.Row>
++												<TablePane.Row height="-1">
+ 													<Label styles="{backgroundColor:11,font:{bold:true}}"
+ 														text="%overviewTab_fieldsAndTermCounts" />
+ 												</TablePane.Row>
+Index: src/org/apache/lucene/luke/ui/OverviewTab.java
+===================================================================
+--- src/org/apache/lucene/luke/ui/OverviewTab.java	(revision 1655665)
++++ src/org/apache/lucene/luke/ui/OverviewTab.java	(working copy)
+@@ -21,19 +21,16 @@
+ import java.text.NumberFormat;
+ import java.util.Collections;
+ 
+-import org.apache.lucene.index.AtomicReaderContext;
+-import org.apache.lucene.index.DirectoryReader;
+-import org.apache.lucene.index.IndexCommit;
+-import org.apache.lucene.index.IndexReader;
+-import org.apache.lucene.index.SegmentReader;
+-import org.apache.lucene.luke.core.FieldTermCount;
++import org.apache.lucene.index.*;
++import org.apache.lucene.luke.core.*;
+ import org.apache.lucene.luke.core.HighFreqTerms;
+-import org.apache.lucene.luke.core.IndexInfo;
+-import org.apache.lucene.luke.core.TableComparator;
+ import org.apache.lucene.luke.core.TermStats;
+-import org.apache.lucene.luke.core.decoders.Decoder;
++import org.apache.lucene.luke.core.decoders.*;
+ import org.apache.lucene.luke.ui.LukeWindow.LukeMediator;
++import org.apache.lucene.luke.ui.util.FieldsTableRow;
++import org.apache.lucene.luke.ui.util.TableComparator;
+ import org.apache.lucene.store.Directory;
++import org.apache.lucene.util.BytesRef;
+ import org.apache.pivot.beans.BXML;
+ import org.apache.pivot.beans.Bindable;
+ import org.apache.pivot.collections.*;
+@@ -43,6 +40,7 @@
+ import org.apache.pivot.util.concurrent.TaskExecutionException;
+ import org.apache.pivot.util.concurrent.TaskListener;
+ import org.apache.pivot.wtk.*;
++import org.apache.pivot.wtk.content.TableViewRowEditor;
+ 
+ 
+ public class OverviewTab extends SplitPane implements Bindable {
+@@ -230,7 +228,6 @@
+ 
+             iTerms.setText(String.valueOf(numTerms));
+             initFieldList(null, null);
+-
+           } catch (Exception e) {
+             // showStatus("ERROR: can't count terms per field");
+             numTerms = -1;
+@@ -261,6 +258,7 @@
+       };
+ 
+       fListTask.execute(new TaskAdapter<String>(taskListener));
++      clearFieldsTableStatus();
+ 
+       String sDel = ir.hasDeletions() ? "Yes (" + ir.numDeletedDocs() + ")" : "No";
+       IndexCommit ic = ir instanceof DirectoryReader ? ((DirectoryReader) ir).getIndexCommit() : null;
+@@ -347,16 +345,24 @@
+ 
+     Sequence<?> fields = fieldsTable.getSelectedRows();
+ 
+-    String[] flds = null;
++    final java.util.Map<String, Decoder> fldDecMap = new java.util.HashMap<String, Decoder>();
+     if (fields == null || fields.getLength() == 0) {
+-      flds = indexInfo.getFieldNames().toArray(new String[0]);
++      // no fields selected
++      for (String fld : indexInfo.getFieldNames()) {
++        Decoder dec = lukeMediator.getDecoders().get(fld);
++        if (dec == null) {
++          dec = lukeMediator.getDefDecoder();
++        }
++        fldDecMap.put(fld, dec);
++      }
+     } else {
+-      flds = new String[fields.getLength()];
++      // some fields selected
+       for (int i = 0; i < fields.getLength(); i++) {
+-        flds[i] = ((Map<String,String>) fields.get(i)).get("name");
++        String fld = ((FieldsTableRow)fields.get(i)).getName();
++        Decoder dec = ((FieldsTableRow)fields.get(i)).getDecoder();
++        fldDecMap.put(fld, dec);
+       }
+     }
+-    final String[] fflds = flds;
+ 
+     tTable.setTableData(new ArrayList(0));
+ 
+@@ -387,6 +393,7 @@
+       public void taskExecuted(Task<Object> task) {
+         // this must happen here rather than in the task because it must happen in the UI dispatch thread
+         try {
++          final String[] fflds = fldDecMap.keySet().toArray(new String[0]);
+           TermStats[] topTerms = HighFreqTerms.getHighFreqTerms(ir, ndoc, fflds);
+ 
+           List<Map<String,String>> tableData = new ArrayList<Map<String,String>>();
+@@ -409,12 +416,11 @@
+ 
+             row.put("field", topTerms[i].field);
+ 
+-            Decoder dec = lukeMediator.getDecoders().get(topTerms[i].field);
+-            if (dec == null)
+-              dec = lukeMediator.getDefDecoder();
++            Decoder dec = fldDecMap.get(topTerms[i].field);
++
+             String s;
+             try {
+-              s = dec.decodeTerm(topTerms[i].field, topTerms[i].termtext.utf8ToString());
++              s = dec.decodeTerm(topTerms[i].field, topTerms[i].termtext);
+             } catch (Throwable e) {
+               // e.printStackTrace();
+               s = topTerms[i].termtext.utf8ToString();
+@@ -422,6 +428,8 @@
+               // setColor(cell, "foreground", Color.RED);
+             }
+             row.put("text", s);
++            // hidden field. would be used when the user select 'Browse term docs' menu at top terms table.
++            row.put("rawterm", topTerms[i].termtext.utf8ToString());
+             tableData.add(row);
+           }
+           tTable.setTableData(tableData);
+@@ -443,8 +451,74 @@
+     };
+ 
+     topTermsTask.execute(new TaskAdapter<Object>(taskListener));
++
++    addListenerToTopTermsTable();
+   }
+ 
++  private void addListenerToTopTermsTable() {
++    // register mouse button listener for more options.
++    tTable.getComponentMouseButtonListeners().add(new ComponentMouseButtonListener.Adapter(){
++      @Override
++      public boolean mouseClick(Component component, Mouse.Button button, int x, int y, int count) {
++        final Map<String, String> row = (Map<String, String>) tTable.getSelectedRow();
++        if (row == null) {
++          System.out.println("No term selected.");
++          return false;
++        }
++        if (button.name().equals(Mouse.Button.RIGHT.name())) {
++          MenuPopup popup = new MenuPopup();
++          Menu menu = new Menu();
++          Menu.Section section1 = new Menu.Section();
++          Menu.Section section2 = new Menu.Section();
++          Menu.Item item1 = new Menu.Item(resources.get("overviewTab_topTermTable_popup_menu1"));
++          item1.setAction(new Action() {
++            @Override
++            public void perform(Component component) {
++              // 'Browse term docs' menu selected. switch to Documents tab.
++              Term term = new Term(row.get("field"), new BytesRef(row.get("rawterm")));
++              lukeMediator.getDocumentsTab().showTerm(term);
++              // TODO: index access isn't good...
++              lukeMediator.getTabPane().setSelectedIndex(1);
++            }
++          });
++          Menu.Item item2 = new Menu.Item(resources.get("overviewTab_topTermTable_popup_menu2"));
++          item2.setAction(new Action() {
++            @Override
++            public void perform(Component component) {
++              // 'Show all term docs' menu selected. switch to Search tab.
++              // TODO
++            }
++          });
++          Menu.Item item3 = new Menu.Item(resources.get("overviewTab_topTermTable_popup_menu3"));
++          item3.setAction(new Action() {
++            @Override
++            public void perform(Component component) {
++              // 'Copy to clipboard' menu selected.
++              StringBuilder sb = new StringBuilder();
++              sb.append(row.get("num") + "\t");
++              sb.append(row.get("df") + "\t");
++              sb.append(row.get("field") + "\t");
++              sb.append(row.get("text") + "\t");
++              LocalManifest content = new LocalManifest();
++              content.putText(sb.toString());
++              Clipboard.setContent(content);
++            }
++          });
++          section1.add(item1);
++          section1.add(item2);
++          section2.add(item3);
++          menu.getSections().add(section1);
++          menu.getSections().add(section2);
++          popup.setMenu(menu);
++
++          popup.open(getWindow(), getMouseLocation().x + 20, getMouseLocation().y);
++          return true;
++        }
++        return false;
++      }
++    });
++  }
++
+   private void initFieldList(Object fCombo, Object defFld) {
+     // removeAll(fieldsTable);
+     // removeAll(defFld);
+@@ -454,11 +528,12 @@
+     NumberFormat percentFormat = NumberFormat.getNumberInstance();
+     intCountFormat.setGroupingUsed(true);
+     percentFormat.setMaximumFractionDigits(2);
++    // sort listener
+     fieldsTable.getTableViewSortListeners().add(new TableViewSortListener.Adapter() {
+       @Override
+       public void sortChanged(TableView tableView) {
+         @SuppressWarnings("unchecked")
+-        List<Map<String, String>> tableData = (List<Map<String, String>>) tableView.getTableData();
++        List<FieldsTableRow> tableData = (List<FieldsTableRow>) tableView.getTableData();
+         tableData.setComparator(new TableComparator(tableView));
+       }
+     });
+@@ -465,34 +540,39 @@
+     // default sort : sorted by name in ascending order
+     fieldsTable.setSort("name", SortDirection.ASCENDING);
+ 
+-    for (String s : indexInfo.getFieldNames()) {
+-      Map<String,String> row = new HashMap<String,String>();
++    // row editor for decoders
++    List decoders = new ArrayList();
++    for (Decoder dec : Util.loadDecoders()) {
++      decoders.add(dec);
++    }
++    ListButton decodersButton = new ListButton(decoders);
++    decodersButton.setSelectedItemKey("decoder");
++    TableViewRowEditor rowEditor = new TableViewRowEditor();
++    rowEditor.getCellEditors().put("decoder", decodersButton);
++    fieldsTable.setRowEditor(rowEditor);
+ 
+-      row.put("name", s);
+ 
+-      FieldTermCount ftc = termCounts.get(s);
++    for (String fname : indexInfo.getFieldNames()) {
++      FieldsTableRow row = new FieldsTableRow(lukeMediator);
++      row.setName(fname);
++      FieldTermCount ftc = termCounts.get(fname);
+       if (ftc != null) {
+         long cnt = ftc.termCount;
+-
+-        row.put("termCount", intCountFormat.format(cnt));
+-
++        row.setTermCount(intCountFormat.format(cnt));
+         float pcent = (float) (cnt * 100) / (float) numTerms;
+-
+-        row.put("percent", percentFormat.format(pcent) + " %");
+-
++        row.setPercent(percentFormat.format(pcent) + " %");
+       } else {
+-        row.put("termCount", "0");
+-        row.put("percent", "0.00%");
++        row.setTermCount("0");
++        row.setPercent("0.00%");
+       }
+ 
+-      //tableData.add(row);
+-      List<Map<String, String>> tableData = (List<Map<String, String>>)fieldsTable.getTableData();
++      List<FieldsTableRow> tableData = (List<FieldsTableRow>)fieldsTable.getTableData();
+       tableData.add(row);
+ 
+-      Decoder dec = lukeMediator.getDecoders().get(s);
++      Decoder dec = lukeMediator.getDecoders().get(fname);
+       if (dec == null)
+         dec = lukeMediator.getDefDecoder();
+-      row.put("decoder", dec.toString());
++      row.setDecoder(dec);
+ 
+       // populate combos
+       // Object choice = create("choice");
+@@ -504,9 +584,14 @@
+       // setString(choice, "text", s);
+       // putProperty(choice, "fName", s);
+     }
+-    //fieldsTable.setTableData(tableData);
++
+   }
+ 
++  private void clearFieldsTableStatus() {
++    // clear the fields table view status
++    fieldsTable.clearSelection();
++  }
++
+   private int getNTerms() {
+     final int nTermsInt = nTerms.getSelectedIndex();
+     return nTermsInt;
+Index: src/org/apache/lucene/luke/ui/PosAndOffsetsWindow.bxml
+===================================================================
+--- src/org/apache/lucene/luke/ui/PosAndOffsetsWindow.bxml	(revision 0)
++++ src/org/apache/lucene/luke/ui/PosAndOffsetsWindow.bxml	(working copy)
+@@ -0,0 +1,77 @@
++<?xml version="1.0" encoding="UTF-8"?>
++
++<luke:PosAndOffsetsWindow bxml:id="posAndOffsets" icon="/img/luke.gif"
++										 title="%posAndOffsetsWindow_title" xmlns:bxml="http://pivot.apache.org/bxml"
++										 xmlns:luke="org.apache.lucene.luke.ui" xmlns:content="org.apache.pivot.wtk.content"
++										 xmlns="org.apache.pivot.wtk">
++	<content>
++		<TablePane styles="{verticalSpacing:10}">
++			<columns>
++				<TablePane.Column width="1*"/>
++			</columns>
++			<rows>
++				<TablePane.Row>
++					<TablePane styles="{verticalSpacing:1,horizontalSpacing:1}">
++						<columns>
++							<TablePane.Column />
++							<TablePane.Column width="1*"/>
++						</columns>
++						<rows>
++							<TablePane.Row>
++								<Label text="Document #" styles="{font:{bold:true},backgroundColor:'#dce0e7',padding:2}"/>
++								<Label bxml:id="docNum" text="?" styles="{backgroundColor:'#fcfdfd',padding:2}"/>
++							</TablePane.Row>
++							<TablePane.Row>
++								<Label text="Term positions for term: " styles="{font:{bold:true},backgroundColor:'#f1f1f1',padding:2}"/>
++								<Label bxml:id="term" text="?" styles="{backgroundColor:11,padding:2}"/>
++							</TablePane.Row>
++							<TablePane.Row>
++								<Label text="Term Frequency: " styles="{font:{bold:true},backgroundColor:'#dce0e7',padding:2}"/>
++								<Label bxml:id="tf" text="?" styles="{backgroundColor:'#fcfdfd',padding:2}"/>
++							</TablePane.Row>
++							<TablePane.Row>
++								<Label text="Offsets: " styles="{font:{bold:true},backgroundColor:'#f1f1f1',padding:2}"/>
++								<Label bxml:id="offsets" text="?" styles="{backgroundColor:11,padding:2}"/>
++							</TablePane.Row>
++							<TablePane.Row>
++								<Label text="Show payload as: " styles="{font:{bold:true},backgroundColor:'#dce0e7',padding:2}"/>
++								<Spinner bxml:id="pDecoder" />
++							</TablePane.Row>
++						</rows>
++					</TablePane>
++				</TablePane.Row>
++
++				<TablePane.Row>
++					<Border styles="{padding:1}">
++						<ScrollPane horizontalScrollBarPolicy="fill_to_capacity" styles="{backgroundColor:11}">
++							<view>
++								<TableView bxml:id="posTable" selectMode="multi">
++									<columns>
++										<TableView.Column name="pos"
++																			headerData="Position" width="50"/>
++										<TableView.Column name="offsets"
++																			headerData="Offsets" width="100"/>
++										<TableView.Column name="payloadStr"
++																			headerData="Payload" width="300"/>
++									</columns>
++								</TableView>
++							</view>
++							<columnHeader>
++								<TableViewHeader tableView="$posTable" />
++							</columnHeader>
++						</ScrollPane>
++					</Border>
++				</TablePane.Row>
++
++				<TablePane.Row>
++					<BoxPane orientation="horizontal" styles="{horizontalAlignment:'right'}">
++						<PushButton buttonData="%label_ok"
++												ButtonPressListener.buttonPressed="posAndOffsets.close()">
++						</PushButton>
++						<PushButton bxml:id="posCopyButton" buttonData="%label_clipboard"/>
++					</BoxPane>
++				</TablePane.Row>
++			</rows>
++		</TablePane>
++	</content>
++</luke:PosAndOffsetsWindow>
+\ No newline at end of file
+
+Property changes on: src/org/apache/lucene/luke/ui/PosAndOffsetsWindow.bxml
+___________________________________________________________________
+Added: svn:mime-type
+## -0,0 +1 ##
++text/xml
+\ No newline at end of property
+Index: src/org/apache/lucene/luke/ui/PosAndOffsetsWindow.java
+===================================================================
+--- src/org/apache/lucene/luke/ui/PosAndOffsetsWindow.java	(revision 0)
++++ src/org/apache/lucene/luke/ui/PosAndOffsetsWindow.java	(working copy)
+@@ -0,0 +1,208 @@
++package org.apache.lucene.luke.ui;
++
++import org.apache.lucene.analysis.payloads.PayloadHelper;
++import org.apache.lucene.index.DocsAndPositionsEnum;
++import org.apache.lucene.index.Term;
++import org.apache.lucene.luke.core.Util;
++import org.apache.lucene.util.BytesRef;
++import org.apache.pivot.beans.BXML;
++import org.apache.pivot.beans.Bindable;
++import org.apache.pivot.collections.ArrayList;
++import org.apache.pivot.collections.List;
++import org.apache.pivot.collections.Map;
++import org.apache.pivot.collections.Sequence;
++import org.apache.pivot.util.Resources;
++import org.apache.pivot.wtk.*;
++
++import java.net.URL;
++
++public class PosAndOffsetsWindow extends Dialog implements Bindable {
++
++  @BXML
++  private TableView posTable;
++  @BXML
++  private Label docNum;
++  @BXML
++  private Label term;
++  @BXML
++  private Label tf;
++  @BXML
++  private Label offsets;
++  @BXML
++  private Spinner pDecoder;
++  @BXML
++  private PushButton posCopyButton;
++
++  private Resources resources;
++
++  private List<PositionAndOffset> tableData;
++
++  @Override
++  public void initialize(Map<String, Object> map, URL url, Resources resources) {
++    this.resources = resources;
++  }
++
++  public void initPositionInfo(DocsAndPositionsEnum pe, Term lastTerm) throws Exception {
++    setPayloadDecoders();
++    tableData = new ArrayList<PositionAndOffset>(getTermPositionAndOffsets(pe));
++    docNum.setText(String.valueOf(pe.docID()));
++    term.setText(lastTerm.field() + ":" + lastTerm.text());
++    tf.setText(String.valueOf(pe.freq()));
++    if (!tableData.isEmpty()) {
++      offsets.setText(String.valueOf(tableData.get(0).hasOffsets));
++    }
++    posTable.setTableData(tableData);
++    addPushButtonListener();
++  }
++
++  private void setPayloadDecoders() {
++    ArrayList<Object> decoders = new ArrayList<Object>();
++    decoders.add(PayloadDecoder.ARRAY_OF_FLOAT);
++    decoders.add(PayloadDecoder.ARRAY_OF_INT);
++    decoders.add(PayloadDecoder.HEXDUMP);
++    decoders.add(PayloadDecoder.STRING);
++    decoders.add(PayloadDecoder.STRING_UTF8);
++    pDecoder.setSpinnerData(decoders);
++    pDecoder.setSelectedItem(PayloadDecoder.STRING_UTF8);
++
++    pDecoder.getSpinnerSelectionListeners().add(new SpinnerSelectionListener.Adapter() {
++      @Override
++      public void selectedItemChanged(Spinner spinner, Object o) {
++        try {
++          for (PositionAndOffset row : tableData) {
++            PayloadDecoder dec = (PayloadDecoder) spinner.getSelectedItem();
++            if (dec == null) {
++              dec = PayloadDecoder.defDecoder();
++            }
++            row.payloadStr = dec.decode(row.payload);
++          }
++          posTable.repaint();  // update table data
++        } catch (Exception e) {
++          // TODO:
++          e.printStackTrace();
++        }
++      }
++    });
++  }
++
++  public class PositionAndOffset {
++    public int pos = -1;
++    public boolean hasOffsets = false;
++    public String offsets = "----";
++    public BytesRef payload = null;
++    public String payloadStr = "----";
++  }
++
++  private PositionAndOffset[] getTermPositionAndOffsets(DocsAndPositionsEnum pe) throws Exception {
++    int freq = pe.freq();
++
++    PositionAndOffset[] res = new PositionAndOffset[freq];
++    for (int i = 0; i < freq; i++) {
++      PositionAndOffset po = new PositionAndOffset();
++      po.pos = pe.nextPosition();
++      if (pe.startOffset() >= 0 && pe.endOffset() >= 0) {
++        // retrieve start and end offsets
++        po.hasOffsets = true;
++        po.offsets = String.valueOf(pe.startOffset()) + " - " + String.valueOf(pe.endOffset());
++      }
++      if (pe.getPayload() != null) {
++        po.payload = pe.getPayload();
++        po.payloadStr = ((PayloadDecoder) pDecoder.getSelectedItem()).decode(pe.getPayload());
++      }
++      res[i] = po;
++    }
++    return res;
++  }
++
++  enum PayloadDecoder {
++    STRING_UTF8("String UTF-8"),
++    STRING("String default enc."),
++    HEXDUMP("Hexdump"),
++    ARRAY_OF_INT("Array of int"),
++    ARRAY_OF_FLOAT("Array of float");
++
++    private String strExpr = null;
++    PayloadDecoder(String expr) {
++      this.strExpr = expr;
++    }
++
++    @Override
++    public String toString() {
++      return strExpr;
++    }
++
++    public static PayloadDecoder defDecoder() {
++      return STRING_UTF8;
++    }
++
++    public String decode(BytesRef payload) {
++      String val = "----";
++      StringBuilder sb = null;
++      if (payload == null) {
++        return val;
++      }
++      switch(this) {
++        case STRING_UTF8:
++          try {
++            val = new String(payload.bytes, payload.offset, payload.length, "UTF-8");
++          } catch (Exception e) {
++            e.printStackTrace();
++            val = new String(payload.bytes, payload.offset, payload.length);
++          }
++          break;
++        case STRING:
++          val = new String(payload.bytes, payload.offset, payload.length);
++          break;
++        case HEXDUMP:
++          val = Util.bytesToHex(payload.bytes, payload.offset, payload.length, false);
++          break;
++        case ARRAY_OF_INT:
++          sb = new StringBuilder();
++          for (int k = payload.offset; k < payload.offset + payload.length; k += 4) {
++            if (k > 0) sb.append(',');
++            sb.append(String.valueOf(PayloadHelper.decodeInt(payload.bytes, k)));
++          }
++          val = sb.toString();
++          break;
++        case ARRAY_OF_FLOAT:
++          sb = new StringBuilder();
++          for (int k = payload.offset; k < payload.offset + payload.length; k += 4) {
++            if (k > 0) sb.append(',');
++            sb.append(String.valueOf(PayloadHelper.decodeFloat(payload.bytes, k)));
++          }
++          val = sb.toString();
++          break;
++      }
++      return val;
++    }
++  }
++
++  private void addPushButtonListener() {
++
++    posCopyButton.getButtonPressListeners().add(new ButtonPressListener() {
++      @Override
++      public void buttonPressed(Button button) {
++        // fired when 'Copy to Clipboard' button pressed
++        Sequence<PositionAndOffset> selectedRows = (Sequence<PositionAndOffset>) posTable.getSelectedRows();
++        if (selectedRows == null || selectedRows.getLength() == 0) {
++          Alert.alert(MessageType.INFO, "No rows selected.", getWindow());
++        } else {
++          StringBuilder sb = new StringBuilder();
++          for (int i = 0; i < selectedRows.getLength(); i++) {
++            PositionAndOffset row = selectedRows.get(i);
++            sb.append(row.pos + "\t");
++            sb.append(row.offsets + "\t");
++            sb.append(row.payloadStr);
++            if (i < selectedRows.getLength() - 1) {
++              sb.append("\n");
++            }
++          }
++          LocalManifest content = new LocalManifest();
++          content.putText(sb.toString());
++          Clipboard.setContent(content);
++        }
++      }
++    });
++  }
++
++}
+Index: src/org/apache/lucene/luke/ui/TermVectorWindow.bxml
+===================================================================
+--- src/org/apache/lucene/luke/ui/TermVectorWindow.bxml	(revision 0)
++++ src/org/apache/lucene/luke/ui/TermVectorWindow.bxml	(working copy)
+@@ -0,0 +1,54 @@
++<?xml version="1.0" encoding="UTF-8"?>
++
++<luke:TermVectorWindow bxml:id="termVector" icon="/img/luke.gif"
++													title="%termVectorWindow_title" xmlns:bxml="http://pivot.apache.org/bxml"
++													xmlns:luke="org.apache.lucene.luke.ui" xmlns:content="org.apache.pivot.wtk.content"
++													xmlns="org.apache.pivot.wtk">
++	<content>
++		<TablePane styles="{verticalSpacing:10}">
++			<columns>
++				<TablePane.Column width="1*"/>
++			</columns>
++			<rows>
++				<TablePane.Row>
++					<BoxPane styles="{fill:true}">
++						<ImageView image="/img/info.gif"/>
++						<Label text="%termVectorWindow_field" />
++						<Label bxml:id="field" text="?" styles="{font:{bold:true}}"/>
++					</BoxPane>
++				</TablePane.Row>
++				<TablePane.Row>
++					<Border styles="{padding:1}">
++						<ScrollPane horizontalScrollBarPolicy="fill_to_capacity" styles="{backgroundColor:11}">
++							<view>
++								<TableView bxml:id="tvTable" selectMode="multi">
++									<columns>
++										<TableView.Column name="term"
++																			headerData="Term" width="100"/>
++										<TableView.Column name="freq"
++																			headerData="Freq." width="50"/>
++										<TableView.Column name="pos"
++																			headerData="Positions" width="100"/>
++										<TableView.Column name="offsets"
++																			headerData="Offsets" width="100" />
++									</columns>
++								</TableView>
++							</view>
++							<columnHeader>
++								<TableViewHeader tableView="$tvTable" sortMode="single_column" />
++							</columnHeader>
++						</ScrollPane>
++					</Border>
++				</TablePane.Row>
++				<TablePane.Row>
++					<BoxPane orientation="horizontal" styles="{horizontalAlignment:'right'}">
++						<PushButton buttonData="%label_ok"
++												ButtonPressListener.buttonPressed="termVector.close()">
++						</PushButton>
++						<PushButton bxml:id="tvCopyButton" buttonData="%label_clipboard"/>
++					</BoxPane>
++				</TablePane.Row>
++			</rows>
++		</TablePane>
++	</content>
++</luke:TermVectorWindow>
+\ No newline at end of file
+
+Property changes on: src/org/apache/lucene/luke/ui/TermVectorWindow.bxml
+___________________________________________________________________
+Added: svn:mime-type
+## -0,0 +1 ##
++text/xml
+\ No newline at end of property
+Index: src/org/apache/lucene/luke/ui/TermVectorWindow.java
+===================================================================
+--- src/org/apache/lucene/luke/ui/TermVectorWindow.java	(revision 0)
++++ src/org/apache/lucene/luke/ui/TermVectorWindow.java	(working copy)
+@@ -0,0 +1,143 @@
++package org.apache.lucene.luke.ui;
++
++import org.apache.lucene.index.DocsAndPositionsEnum;
++import org.apache.lucene.index.DocsEnum;
++import org.apache.lucene.index.Terms;
++import org.apache.lucene.index.TermsEnum;
++import org.apache.lucene.luke.ui.util.TermVectorTableComparator;
++import org.apache.lucene.search.DocIdSetIterator;
++import org.apache.lucene.util.Bits;
++import org.apache.lucene.util.BytesRef;
++import org.apache.pivot.beans.BXML;
++import org.apache.pivot.beans.Bindable;
++import org.apache.pivot.collections.*;
++import org.apache.pivot.util.Resources;
++import org.apache.pivot.wtk.*;
++
++import java.io.IOException;
++import java.net.URL;
++
++public class TermVectorWindow extends Dialog implements Bindable{
++
++  @BXML
++  private Label field;
++  @BXML
++  private TableView tvTable;
++  @BXML
++  private PushButton tvCopyButton;
++
++  private Resources resources;
++
++  private List<Map<String, String>> tableData;
++
++  public static String TVROW_KEY_TERM = "term";
++  public static String TVROW_KEY_FREQ = "freq";
++  public static String TVROW_KEY_POSITION = "pos";
++  public static String TVROW_KEY_OFFSETS = "offsets";
++
++  @Override
++  public void initialize(Map<String, Object> map, URL url, Resources resources) {
++    this.resources = resources;
++  }
++
++  public void initTermVector(String fieldName, Terms tv) throws IOException {
++    field.setText(fieldName);
++    tableData = new ArrayList<Map<String, String>>();
++    TermsEnum te = tv.iterator(null);
++    BytesRef term = null;
++
++    // populate table data with term vector info
++    while((term = te.next()) != null) {
++      Map<String, String> row = new HashMap<String, String>();
++      tableData.add(row);
++      row.put(TVROW_KEY_TERM, term.utf8ToString());
++      // try to get DocsAndPositionsEnum
++      DocsEnum de = te.docsAndPositions(null, null);
++      if (de == null) {
++        // if positions are not indexed, get DocsEnum
++        de = te.docs(null, null);
++      }
++      // must have one doc
++      if (de.nextDoc() == DocIdSetIterator.NO_MORE_DOCS) {
++        continue;
++      }
++      row.put(TVROW_KEY_FREQ, String.valueOf(de.freq()));
++      if (de instanceof DocsAndPositionsEnum) {
++        // positions are available
++        DocsAndPositionsEnum dpe = (DocsAndPositionsEnum) de;
++        StringBuilder bufPos = new StringBuilder();
++        StringBuilder bufOff = new StringBuilder();
++        // enumerate all positions info
++        for (int i = 0; i < de.freq(); i++) {
++          int pos = dpe.nextPosition();
++          bufPos.append(String.valueOf(pos));
++          if (i < de.freq() - 1) {
++            bufPos.append((","));
++          }
++          // offsets are indexed?
++          int sOffset = dpe.startOffset();
++          int eOffset = dpe.endOffset();
++          if (sOffset >= 0 && eOffset >= 0) {
++            String offsets = String.valueOf(sOffset) + "-" + String.valueOf(eOffset);
++            bufOff.append(offsets);
++            if (i < de.freq() - 1) {
++              bufOff.append(",");
++            }
++          }
++        }
++        row.put(TVROW_KEY_POSITION, bufPos.toString());
++        row.put(TVROW_KEY_OFFSETS, (bufOff.length() == 0) ? "----" : bufOff.toString());
++      } else {
++        // positions are not available
++        row.put(TVROW_KEY_POSITION, "----");
++        row.put(TVROW_KEY_OFFSETS, "----");
++      }
++    }
++    // register sort listener
++    tvTable.getTableViewSortListeners().add(new TableViewSortListener.Adapter() {
++      @Override
++      public void sortChanged(TableView tableView) {
++        List<Map<String, String>> tableData = (List<Map<String, String>>) tableView.getTableData();
++        tableData.setComparator(new TermVectorTableComparator(tableView));
++      }
++    });
++    // default sort : by ascending order of term
++    Sequence<Dictionary.Pair<String, SortDirection>> sort = new ArrayList<Dictionary.Pair<String, SortDirection>>();
++    sort.add(new Dictionary.Pair<String, SortDirection>(TVROW_KEY_TERM, SortDirection.ASCENDING));
++    sort.add(new Dictionary.Pair<String, SortDirection>(TVROW_KEY_FREQ, SortDirection.DESCENDING));
++    tvTable.setSort(sort);
++
++    tvTable.setTableData(tableData);
++    addPushButtonListener();
++  }
++
++  private void addPushButtonListener() {
++
++    tvCopyButton.getButtonPressListeners().add(new ButtonPressListener() {
++      @Override
++      public void buttonPressed(Button button) {
++        // fired when 'Copy to Clipboard' button pressed
++        Sequence<Map<String, String>> selectedRows = (Sequence<Map<String, String>>) tvTable.getSelectedRows();
++        if (selectedRows == null || selectedRows.getLength() == 0) {
++          Alert.alert(MessageType.INFO, "No rows selected.", getWindow());
++        } else {
++          StringBuilder sb = new StringBuilder();
++          for (int i = 0; i < selectedRows.getLength(); i++) {
++            Map<String, String> row = selectedRows.get(i);
++            sb.append(row.get(TVROW_KEY_TERM) + "\t");
++            sb.append(row.get(TVROW_KEY_FREQ) + "\t");
++            sb.append(row.get(TVROW_KEY_POSITION) + "\t");
++            sb.append(row.get(TVROW_KEY_OFFSETS));
++            if (i < selectedRows.getLength() - 1) {
++              sb.append("\n");
++            }
++          }
++          LocalManifest content = new LocalManifest();
++          content.putText(sb.toString());
++          Clipboard.setContent(content);
++        }
++      }
++    });
++  }
++
++}
+Index: src/org/apache/lucene/luke/ui/util/FieldsTableRow.java
+===================================================================
+--- src/org/apache/lucene/luke/ui/util/FieldsTableRow.java	(revision 0)
++++ src/org/apache/lucene/luke/ui/util/FieldsTableRow.java	(working copy)
+@@ -0,0 +1,61 @@
++package org.apache.lucene.luke.ui.util;
++
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++import org.apache.lucene.luke.core.decoders.Decoder;
++import org.apache.lucene.luke.ui.LukeWindow;
++
++public class FieldsTableRow {
++  private String name;
++  private String termCount;
++  private String percent;
++  private Decoder decoder;
++
++  private LukeWindow.LukeMediator lukeMediator;
++
++  public FieldsTableRow(LukeWindow.LukeMediator lukeMediator) {
++    this.lukeMediator = lukeMediator;
++  }
++
++  public String getName() {
++    return name;
++  }
++  public void setName(String name) {
++    this.name = name;
++  }
++  public String getTermCount() {
++    return termCount;
++  }
++  public void setTermCount(String termCount) {
++    this.termCount = termCount;
++  }
++  public String getPercent() {
++    return percent;
++  }
++  public void setPercent(String percent) {
++    this.percent = percent;
++  }
++  public Decoder getDecoder() {
++    return decoder;
++  }
++  public void setDecoder(Decoder decoder) {
++    this.decoder = decoder;
++    this.lukeMediator.getDecoders().put(name, decoder);
++  }
++
++}
+Index: src/org/apache/lucene/luke/ui/util/TableComparator.java
+===================================================================
+--- src/org/apache/lucene/luke/ui/util/TableComparator.java	(revision 0)
++++ src/org/apache/lucene/luke/ui/util/TableComparator.java	(working copy)
+@@ -0,0 +1,102 @@
++package org.apache.lucene.luke.ui.util;
++
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to you under the Apache License,
++ * Version 2.0 (the "License"); you may not use this file except in
++ * compliance with the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++
++import java.util.Comparator;
++
++import org.apache.pivot.collections.Dictionary;
++import org.apache.pivot.wtk.SortDirection;
++import org.apache.pivot.wtk.TableView;
++
++public class TableComparator implements Comparator<FieldsTableRow> {
++  private TableView tableView;
++
++  public TableComparator(TableView fieldsTable) {
++    if (fieldsTable == null) {
++      throw new IllegalArgumentException();
++    }
++
++    this.tableView = fieldsTable;
++  }
++
++  @Override
++  public int compare(FieldsTableRow row1, FieldsTableRow row2) {
++    Dictionary.Pair<String, SortDirection> sort = tableView.getSort().get(0);
++
++    int result;
++    if (sort.key.equals("name")) {
++      // sort by name
++      result = row1.getName().compareTo(row2.getName());
++    } else if (sort.key.equals("termCount")) {
++      // sort by termCount
++      Integer c1 = Integer.parseInt(row1.getTermCount());
++      Integer c2 = Integer.parseInt(row2.getTermCount());
++      result = c1.compareTo(c2);
++    } else {
++      // other (ignored)
++      result = 0;
++    }
++    //int result = o1.get("name").compareTo(o2.get("name"));
++    //SortDirection sortDirection = tableView.getSort().get("name");
++    SortDirection sortDirection = sort.value;
++    result *= (sortDirection == SortDirection.DESCENDING ? 1 : -1);
++
++    return result * -1;
++  }
++}
++
++/*
++public class TableComparator implements Comparator<Map<String,String>> {
++  private TableView tableView;
++  
++  public TableComparator(TableView fieldsTable) {
++    if (fieldsTable == null) {
++      throw new IllegalArgumentException();
++    }
++    
++    this.tableView = fieldsTable;
++  }
++  
++  @Override
++  public int compare(Map<String,String> o1, Map<String,String> o2) {
++    Dictionary.Pair<String, SortDirection> sort = tableView.getSort().get(0);
++
++    int result;
++    if (sort.key.equals("name")) {
++      // sort by name
++      result = o1.get(sort.key).compareTo(o2.get(sort.key));
++    } else if (sort.key.equals("termCount")) {
++      // sort by termCount
++      Integer c1 = Integer.parseInt(o1.get(sort.key));
++      Integer c2 = Integer.parseInt(o2.get(sort.key));
++      result = c1.compareTo(c2);
++    } else {
++      // other (ignored)
++      result = 0;
++    }
++    //int result = o1.get("name").compareTo(o2.get("name"));
++    //SortDirection sortDirection = tableView.getSort().get("name");
++    SortDirection sortDirection = sort.value;
++    result *= (sortDirection == SortDirection.DESCENDING ? 1 : -1);
++
++    return result * -1;
++  }
++  
++}
++*/
+Index: src/org/apache/lucene/luke/ui/util/TermVectorTableComparator.java
+===================================================================
+--- src/org/apache/lucene/luke/ui/util/TermVectorTableComparator.java	(revision 0)
++++ src/org/apache/lucene/luke/ui/util/TermVectorTableComparator.java	(working copy)
+@@ -0,0 +1,46 @@
++package org.apache.lucene.luke.ui.util;
++
++import org.apache.pivot.collections.Dictionary;
++import org.apache.pivot.collections.Map;
++import org.apache.pivot.wtk.SortDirection;
++import org.apache.pivot.wtk.TableView;
++
++import java.util.Comparator;
++
++import static org.apache.lucene.luke.ui.TermVectorWindow.TVROW_KEY_FREQ;
++import static org.apache.lucene.luke.ui.TermVectorWindow.TVROW_KEY_TERM;
++
++
++public class TermVectorTableComparator implements Comparator<Map<String, String>> {
++  private TableView tableView;
++
++  public TermVectorTableComparator(TableView tableView) {
++    if (tableView == null) {
++      throw new IllegalArgumentException();
++    }
++    this.tableView = tableView;
++  }
++
++  @Override
++  public int compare(Map<String, String> row1, Map<String, String> row2) {
++    Dictionary.Pair<String, SortDirection> sort = tableView.getSort().get(0);
++
++    int result;
++    if (sort.key.equals(TVROW_KEY_TERM)) {
++      // sort by name
++      result = row1.get(TVROW_KEY_TERM).compareTo(row2.get(TVROW_KEY_TERM));
++    } else if (sort.key.equals(TVROW_KEY_FREQ)) {
++      // sort by termCount
++      Integer f1 = Integer.parseInt(row1.get(TVROW_KEY_FREQ));
++      Integer f2 = Integer.parseInt(row2.get(TVROW_KEY_FREQ));
++      result = f1.compareTo(f2);
++    } else {
++      // other (ignored)
++      result = 0;
++    }
++    SortDirection sortDirection = sort.value;
++    result *= (sortDirection == SortDirection.DESCENDING ? 1 : -1);
++
++    return result * -1;
++  }
++}
diff --git a/attachments/LUCENE-2562/LUCENE-2562-ivy.patch b/attachments/LUCENE-2562/LUCENE-2562-ivy.patch
new file mode 100644
index 0000000..80382ac
--- /dev/null
+++ b/attachments/LUCENE-2562/LUCENE-2562-ivy.patch
@@ -0,0 +1,252 @@
+Index: build.xml
+===================================================================
+--- build.xml	(revision 1652561)
++++ build.xml	(working copy)
+@@ -1,8 +1,9 @@
+-<project name="Luke" default="dist">
++<project name="Luke" default="dist" xmlns:ivy="antlib:org.apache.ivy.ant">
+   <defaultexcludes add="**/CVS" />
+   <property name="build.dir" value="build" />
+-  <property name="build.ver" value="4.3.1" />
++  <property name="build.ver" value="4.10.3" />
+   <property name="dist.dir" value="dist" />
++  <property name="ivy.lib.dir" value="lib-ivy" />
+   <property name="jarfile" value="${build.dir}/luke-${build.ver}.jar" />
+   <property name="jarallfile" value="${build.dir}/lukeall-${build.ver}.jar" />
+   <property name="jarminfile" value="${build.dir}/lukemin-${build.ver}.jar" />
+@@ -19,10 +20,26 @@
+     <delete dir="${dist.dir}" />
+   </target>
+ 
++  <!-- resolve dependencies -->
++  <path id="ivy.lib.path">
++    <fileset dir="lib/tools" includes="*.jar"/>
++  </path>
++  <taskdef resource="org/apache/ivy/ant/antlib.xml"
++           uri="antlib:org.apache.ivy.ant" classpathref="ivy.lib.path"/>
++  <target name="ivy-resolve">
++    <ivy:retrieve conf="lucene" pattern="${ivy.lib.dir}/[artifact].[ext]"/>
++    <ivy:retrieve conf="pivot" pattern="${ivy.lib.dir}/[artifact].[ext]"/>
++    <ivy:retrieve conf="solr" pattern="${ivy.lib.dir}/[conf]/[artifact].[ext]"/>
++    <ivy:retrieve conf="hadoop" pattern="${ivy.lib.dir}/[conf]/[artifact].[ext]"/>
++  </target>
++  <target name="ivy-clean">
++    <delete dir="${ivy.lib.dir}"/>
++  </target>
++
+   <target name="compile" depends="init">
+     <javac classpath="${classpath}" sourcepath="" source="1.5" target="1.5" srcdir="src" destdir="${build.dir}">
+       <classpath>
+-        <fileset dir="lib">
++        <fileset dir="${ivy.lib.dir}">
+           <include name="**/*.jar" />
+         </fileset>
+       </classpath>
+@@ -33,7 +50,7 @@
+   <target name="javadoc" depends="init">
+     <javadoc sourcepath="src" packagenames="org.*" destdir="${build.dir}/api">
+       <classpath>
+-        <fileset dir="lib">
++        <fileset dir="${ivy.lib.dir}">
+           <include name="**/*.jar" />
+         </fileset>
+       </classpath>
+@@ -53,7 +70,7 @@
+       </manifest>
+     </jar>
+     <unjar dest="${build.dir}">
+-      <fileset dir="lib" includes="lucene-*.jar" />
++      <fileset dir="${ivy.lib.dir}" includes="lucene-*.jar" />
+     </unjar>
+     <jar basedir="${build.dir}" jarfile="${jarminfile}" includes=".plugins,img/,org/" excludes="org/mozilla/,org/apache/lucene/luke/plugins,**/*.js">
+       <manifest>
+@@ -62,20 +79,20 @@
+       </manifest>
+     </jar>
+     <unjar dest="${build.dir}">
+-      <fileset dir="lib" includes="pivot*.jar" />
++      <fileset dir="${ivy.lib.dir}" includes="pivot*.jar" />
+     </unjar>
+     <unjar dest="${build.dir}">
+       <fileset dir="lib" includes="js.jar" />
+-      <fileset dir="lib" includes="lucene*.jar" />
++      <fileset dir="${ivy.lib.dir}" includes="lucene*.jar" />
+     </unjar>
+     <unjar dest="${build.dir}">
+-      <fileset dir="lib" includes="hadoop/*.jar" />
++      <fileset dir="${ivy.lib.dir}" includes="hadoop/*.jar" />
+     </unjar>
+     <unjar dest="${build.dir}">
+-      <fileset dir="lib" includes="solr/*.jar" />
++      <fileset dir="${ivy.lib.dir}" includes="solr/*.jar" />
+     </unjar>
+     <unjar dest="${build.dir}">
+-      <fileset dir="lib" includes="lucene-core-*.jar" />
++      <fileset dir="${ivy.lib.dir}" includes="lucene-core-*.jar" />
+       <patternset>
+         <include name="META-INF/MANIFEST.MF" />
+       </patternset>
+@@ -99,7 +116,8 @@
+       </patternset>
+     </fileset>
+     <copy todir="${dist.dir}">
+-      <fileset dir="lib" />
++      <fileset dir="lib" includes="js.jar"/>
++      <fileset dir="${ivy.lib.dir}" />
+       <fileset file="${jarfile}" />
+       <fileset file="${jarallfile}" />
+       <fileset file="${jarminfile}" />
+Index: ivy.xml
+===================================================================
+--- ivy.xml	(revision 0)
++++ ivy.xml	(working copy)
+@@ -0,0 +1,57 @@
++<ivy-module version="2.0">
++  <info organisation="org.apache.lucene" module="luke"/>
++  <configurations>
++    <conf name="lucene" description="for Lucene jars"/>
++    <conf name="pivot" description="for Pivot jars"/>
++    <conf name="solr" description="for Solr jars"/>
++    <conf name="hadoop" description="for Hadoop jars"/>
++  </configurations>
++  <dependencies>
++    <!-- apache lucene -->
++    <dependency org="org.apache.lucene" name="lucene-analyzers-common" rev="4.10.3"
++                conf="lucene->*,!sources,!javadoc"/>
++    <dependency org="org.apache.lucene" name="lucene-codecs" rev="4.10.3"
++                conf="lucene->*,!sources,!javadoc"/>
++    <dependency org="org.apache.lucene" name="lucene-core" rev="4.10.3"
++                conf="lucene->*,!sources,!javadoc"/>
++    <dependency org="org.apache.lucene" name="lucene-misc" rev="4.10.3"
++                conf="lucene->*,!sources,!javadoc"/>
++    <dependency org="org.apache.lucene" name="lucene-queries" rev="4.10.3"
++                conf="lucene->*,!sources,!javadoc"/>
++    <dependency org="org.apache.lucene" name="lucene-queryparser" rev="4.10.3"
++                conf="lucene->*,!sources,!javadoc"/>
++
++    <!-- apache pivot -->
++    <dependency org="org.apache.pivot" name="pivot-charts" rev="2.0.4"
++                conf="pivot->*,!sources,!javadoc"/>
++    <dependency org="org.apache.pivot" name="pivot-core" rev="2.0.4"
++                conf="pivot->*,!sources,!javadoc"/>
++    <dependency org="org.apache.pivot" name="pivot-web" rev="2.0.4"
++                conf="pivot->*,!sources,!javadoc"/>
++    <dependency org="org.apache.pivot" name="pivot-web-server" rev="2.0.4"
++                conf="pivot->*,!sources,!javadoc"/>
++    <dependency org="org.apache.pivot" name="pivot-wtk" rev="2.0.4"
++                conf="pivot->*,!sources,!javadoc"/>
++    <dependency org="org.apache.pivot" name="pivot-wtk-terra" rev="2.0.4"
++                conf="pivot->*,!sources,!javadoc"/>
++
++    <!-- apache solr -->
++    <dependency org="org.apache.solr" name="solr-core" rev="4.10.3"
++                transitive="false"
++                conf="solr->*,!sources,!javadoc"/>
++    <dependency org="org.apache.solr" name="solr-solrj" rev="4.10.3"
++                transitive="false"
++                conf="solr->*,!sources,!javadoc"/>
++
++    <!-- apache hadoop -->
++    <dependency org="org.apache.hadoop" name="hadoop-core" rev="0.20.2"
++                conf="hadoop->*,!sources,!javadoc"/>
++      <dependency org="org.slf4j" name="slf4j-api" rev="1.4.3"
++                  conf="hadoop->*,!sources,!javadoc"/>
++      <dependency org="org.slf4j" name="slf4j-log4j12" rev="1.4.3"
++                  conf="hadoop->*,!sources,!javadoc"/>
++      <dependency org="net.sf.ehcache" name="ehcache" rev="1.6.0"
++                  conf="hadoop->*,!sources,!javadoc"/>
++
++  </dependencies>
++</ivy-module>
+\ No newline at end of file
+Index: lib/tools/ivy-2.3.0.jar
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/jar
+Index: lib/tools/ivy-2.3.0.jar
+===================================================================
+--- lib/tools/ivy-2.3.0.jar	(revision 0)
++++ lib/tools/ivy-2.3.0.jar	(working copy)
+
+Property changes on: lib/tools/ivy-2.3.0.jar
+___________________________________________________________________
+Added: svn:mime-type
+## -0,0 +1 ##
++application/jar
+\ No newline at end of property
+Index: src/org/apache/lucene/index/IndexGate.java
+===================================================================
+--- src/org/apache/lucene/index/IndexGate.java	(revision 1652561)
++++ src/org/apache/lucene/index/IndexGate.java	(working copy)
+@@ -146,7 +146,8 @@
+     infos.read(dir);
+     int compound = 0, nonCompound = 0;
+     for (int i = 0; i < infos.size(); i++) {
+-      if (((SegmentInfoPerCommit)infos.info(i)).info.getUseCompoundFile()) {
++      //if (((SegmentInfoPerCommit)infos.info(i)).info.getUseCompoundFile()) {
++      if (infos.info(i).info.getUseCompoundFile()) {
+         compound++;
+       } else {
+         nonCompound++;
+Index: src/org/apache/lucene/luke/core/IndexInfo.java
+===================================================================
+--- src/org/apache/lucene/luke/core/IndexInfo.java	(revision 1652561)
++++ src/org/apache/lucene/luke/core/IndexInfo.java	(working copy)
+@@ -74,7 +74,8 @@
+     
+     AtomicReader r;
+     if (reader instanceof CompositeReader) {
+-      r = new SlowCompositeReaderWrapper((CompositeReader)reader);
++      //r = new SlowCompositeReaderWrapper((CompositeReader)reader);
++      r = SlowCompositeReaderWrapper.wrap(reader);
+     } else {
+       r = (AtomicReader)reader;
+     }
+Index: src/org/apache/lucene/luke/ui/AnalyzersTab.java
+===================================================================
+--- src/org/apache/lucene/luke/ui/AnalyzersTab.java	(revision 1652561)
++++ src/org/apache/lucene/luke/ui/AnalyzersTab.java	(working copy)
+@@ -68,7 +68,15 @@
+         .getAnalyzerNames()));
+     analyzersListButton.setSelectedIndex(0);
+     List<String> versions = new ArrayList<String>();
+-    Version[] values = Version.values();
++    // TODO: Version.values() was removed, and Version.LUCENE_X_X_X were all depricated. How do we fix this line?
++    //Version[] values = Version.values();
++    Version[] values = {
++      Version.LUCENE_3_0_0, Version.LUCENE_3_1_0, Version.LUCENE_3_2_0, Version.LUCENE_3_3_0,
++      Version.LUCENE_3_4_0, Version.LUCENE_3_5_0, Version.LUCENE_3_6_0,
++      Version.LUCENE_4_1_0, Version.LUCENE_4_2_0, Version.LUCENE_4_3_0, Version.LUCENE_4_4_0,
++      Version.LUCENE_4_5_0, Version.LUCENE_4_6_0, Version.LUCENE_4_7_0, Version.LUCENE_4_8_0,
++      Version.LUCENE_4_9_0, Version.LUCENE_4_10_0
++    };
+     for (int i = 0; i < values.length; i++) {
+       Version v = values[i];
+       versions.add(v.toString());
+@@ -116,8 +124,10 @@
+   
+   public void analyze() {
+     try {
+-      Version v = Version.valueOf((String) luceneVersionListButton
+-          .getSelectedItem());
++      //Version v = Version.valueOf((String) luceneVersionListButton
++      // .getSelectedItem());
++      Version v = Version.parseLeniently((String) luceneVersionListButton
++        .getSelectedItem());
+       Class clazz = Class.forName((String) analyzersListButton
+           .getSelectedItem());
+       Analyzer analyzer = null;
+Index: src/org/apache/lucene/luke/ui/DocumentsTab.java
+===================================================================
+--- src/org/apache/lucene/luke/ui/DocumentsTab.java	(revision 1652561)
++++ src/org/apache/lucene/luke/ui/DocumentsTab.java	(working copy)
+@@ -192,7 +192,8 @@
+     this.idxInfo = lukeMediator.getIndexInfo();
+     this.ir = idxInfo.getReader();
+     if (ir instanceof CompositeReader) {
+-      ar = new SlowCompositeReaderWrapper((CompositeReader) ir);
++      //ar = new SlowCompositeReaderWrapper((CompositeReader) ir);
++      ar = SlowCompositeReaderWrapper.wrap(ir);
+     } else if (ir instanceof AtomicReader) {
+       ar = (AtomicReader) ir;
+     }
diff --git a/attachments/LUCENE-2562/LUCENE-2562.patch b/attachments/LUCENE-2562/LUCENE-2562.patch
new file mode 100644
index 0000000..e3498db
--- /dev/null
+++ b/attachments/LUCENE-2562/LUCENE-2562.patch
@@ -0,0 +1,24845 @@
+diff --git a/dev-tools/idea/.idea/ant.xml b/dev-tools/idea/.idea/ant.xml
+index 229d83203c6..d3f96556df8 100644
+--- a/dev-tools/idea/.idea/ant.xml
++++ b/dev-tools/idea/.idea/ant.xml
+@@ -24,6 +24,7 @@
+     <buildFile url="file://$PROJECT_DIR$/lucene/grouping/build.xml" />
+     <buildFile url="file://$PROJECT_DIR$/lucene/highlighter/build.xml" />
+     <buildFile url="file://$PROJECT_DIR$/lucene/join/build.xml" />
++    <buildFile url="file://$PROJECT_DIR$/lucene/luke/build.xml" />
+     <buildFile url="file://$PROJECT_DIR$/lucene/memory/build.xml" />
+     <buildFile url="file://$PROJECT_DIR$/lucene/misc/build.xml" />
+     <buildFile url="file://$PROJECT_DIR$/lucene/queries/build.xml" />
+diff --git a/dev-tools/idea/.idea/modules.xml b/dev-tools/idea/.idea/modules.xml
+index 65b57fb03d5..4974f19668e 100644
+--- a/dev-tools/idea/.idea/modules.xml
++++ b/dev-tools/idea/.idea/modules.xml
+@@ -30,6 +30,7 @@
+       <module group="Lucene/Other" filepath="$PROJECT_DIR$/lucene/grouping/grouping.iml" />
+       <module group="Lucene/Other" filepath="$PROJECT_DIR$/lucene/highlighter/highlighter.iml" />
+       <module group="Lucene/Other" filepath="$PROJECT_DIR$/lucene/join/join.iml" />
++      <module group="Lucene/Other" filepath="$PROJECT_DIR$/lucene/luke/luke.iml" />
+       <module group="Lucene/Other" filepath="$PROJECT_DIR$/lucene/memory/memory.iml" />
+       <module group="Lucene/Other" filepath="$PROJECT_DIR$/lucene/misc/misc.iml" />
+       <module group="Lucene/Other" filepath="$PROJECT_DIR$/lucene/queries/queries.iml" />
+diff --git a/dev-tools/idea/.idea/workspace.xml b/dev-tools/idea/.idea/workspace.xml
+index 6a1fd0ad879..bbc271ee28c 100644
+--- a/dev-tools/idea/.idea/workspace.xml
++++ b/dev-tools/idea/.idea/workspace.xml
+@@ -148,6 +148,14 @@
+       <option name="TEST_SEARCH_SCOPE"><value defaultName="singleModule" /></option>
+       <patterns><pattern testClass=".*\.Test[^.]*|.*\.[^.]*Test" /></patterns>
+     </configuration>
++    <configuration default="false" name="Module luke" type="JUnit" factoryName="JUnit">
++      <module name="luke" />
++      <option name="TEST_OBJECT" value="pattern" />
++      <option name="WORKING_DIRECTORY" value="file://$PROJECT_DIR$/idea-build/lucene/luke" />
++      <option name="VM_PARAMETERS" value="-ea -DtempDir=temp" />
++      <option name="TEST_SEARCH_SCOPE"><value defaultName="singleModule" /></option>
++      <patterns><pattern testClass=".*\.Test[^.]*|.*\.[^.]*Test" /></patterns>
++    </configuration>
+     <configuration default="false" name="Module memory" type="JUnit" factoryName="JUnit">
+       <module name="memory" />
+       <option name="TEST_OBJECT" value="pattern" />
+diff --git a/dev-tools/idea/lucene/luke/luke.iml b/dev-tools/idea/lucene/luke/luke.iml
+new file mode 100644
+index 00000000000..9bd08ef4ab1
+--- /dev/null
++++ b/dev-tools/idea/lucene/luke/luke.iml
+@@ -0,0 +1,33 @@
++<?xml version="1.0" encoding="UTF-8"?>
++<module type="JAVA_MODULE" version="4">
++  <component name="NewModuleRootManager" inherit-compiler-output="false">
++    <output url="file://$MODULE_DIR$/../../idea-build/lucene/luke/classes/java" />
++    <output-test url="file://$MODULE_DIR$/../../idea-build/lucene/luke/classes/test" />
++    <exclude-output />
++    <content url="file://$MODULE_DIR$">
++      <sourceFolder url="file://$MODULE_DIR$/src/java" isTestSource="false" />
++      <sourceFolder url="file://$MODULE_DIR$/src/resources" isTestSource="false" />
++      <sourceFolder url="file://$MODULE_DIR$/src/test" isTestSource="true" />
++      <excludeFolder url="file://$MODULE_DIR$/work" />
++    </content>
++    <orderEntry type="inheritedJdk" />
++    <orderEntry type="sourceFolder" forTests="false" />
++    <orderEntry type="module-library">
++      <library>
++        <CLASSES>
++          <root url="file://$MODULE_DIR$/lib" />
++        </CLASSES>
++        <JAVADOC />
++        <SOURCES />
++        <jarDirectory url="file://$MODULE_DIR$/lib" recursive="false" />
++      </library>
++    </orderEntry>
++    <orderEntry type="library" scope="TEST" name="JUnit" level="project" />
++    <orderEntry type="module" scope="TEST" module-name="lucene-test-framework" />
++    <orderEntry type="module" module-name="lucene-core" />
++    <orderEntry type="module" module-name="analysis-common" />
++    <orderEntry type="module" module-name="misc" />
++    <orderEntry type="module" module-name="queries" />
++    <orderEntry type="module" module-name="queryparser" />
++  </component>
++</module>
+diff --git a/lucene/build.xml b/lucene/build.xml
+index 3c1439c7e26..e3cf905c971 100644
+--- a/lucene/build.xml
++++ b/lucene/build.xml
+@@ -287,6 +287,7 @@
+       <zipfileset prefix="lucene-${version}" dir="${build.dir}">
+         <patternset refid="binary.build.dist.patterns"/>
+       </zipfileset>
++      <zipfileset prefix="lucene-${version}" dir="${build.dir}" includes="**/*.sh,**/*.bat" filemode="755"/>
+     </zip>
+     <make-checksums file="${dist.dir}/lucene-${version}.zip"/>
+   </target>
+@@ -310,6 +311,7 @@
+       <tarfileset prefix="lucene-${version}" dir="${build.dir}">
+         <patternset refid="binary.build.dist.patterns"/>
+       </tarfileset>
++      <tarfileset prefix="lucene-${version}" dir="${build.dir}" includes="**/*.sh,**/*.bat" filemode="755"/>
+     </tar>
+     <make-checksums file="${dist.dir}/lucene-${version}.tgz"/>
+   </target>
+diff --git a/lucene/ivy-ignore-conflicts.properties b/lucene/ivy-ignore-conflicts.properties
+index 6300bdf6d6f..df3a2e5a43b 100644
+--- a/lucene/ivy-ignore-conflicts.properties
++++ b/lucene/ivy-ignore-conflicts.properties
+@@ -10,4 +10,5 @@
+ # trigger a conflict) when the ant check-lib-versions target is run.
+ 
+ /com.google.guava/guava = 16.0.1
+-/org.ow2.asm/asm = 5.0_BETA
+\ No newline at end of file
++/org.ow2.asm/asm = 5.0_BETA
++
+diff --git a/lucene/licenses/elegant-icon-font-LICENSE-MIT.txt b/lucene/licenses/elegant-icon-font-LICENSE-MIT.txt
+new file mode 100644
+index 00000000000..effefee5f0c
+--- /dev/null
++++ b/lucene/licenses/elegant-icon-font-LICENSE-MIT.txt
+@@ -0,0 +1,21 @@
++The MIT License (MIT)
++
++Copyright (c) <2013> <Elegant Themes, Inc.>
++
++Permission is hereby granted, free of charge, to any person obtaining a copy
++of this software and associated documentation files (the "Software"), to deal
++in the Software without restriction, including without limitation the rights
++to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
++copies of the Software, and to permit persons to whom the Software is
++furnished to do so, subject to the following conditions:
++
++The above copyright notice and this permission notice shall be included in
++all copies or substantial portions of the Software.
++
++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
++IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
++FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
++AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
++LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
++OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
++THE SOFTWARE.
+\ No newline at end of file
+diff --git a/lucene/licenses/elegant-icon-font-NOTICE.txt b/lucene/licenses/elegant-icon-font-NOTICE.txt
+new file mode 100644
+index 00000000000..ea97d9b601c
+--- /dev/null
++++ b/lucene/licenses/elegant-icon-font-NOTICE.txt
+@@ -0,0 +1,3 @@
++The Elegant Icon Font web page: https://www.elegantthemes.com/blog/resources/elegant-icon-font
++
++These icons are dual licensed under the GPL 2.0 and MIT, and are completely free to use.
+diff --git a/lucene/licenses/log4j-LICENSE-ASL.txt b/lucene/licenses/log4j-LICENSE-ASL.txt
+new file mode 100644
+index 00000000000..d6456956733
+--- /dev/null
++++ b/lucene/licenses/log4j-LICENSE-ASL.txt
+@@ -0,0 +1,202 @@
++
++                                 Apache License
++                           Version 2.0, January 2004
++                        http://www.apache.org/licenses/
++
++   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
++
++   1. Definitions.
++
++      "License" shall mean the terms and conditions for use, reproduction,
++      and distribution as defined by Sections 1 through 9 of this document.
++
++      "Licensor" shall mean the copyright owner or entity authorized by
++      the copyright owner that is granting the License.
++
++      "Legal Entity" shall mean the union of the acting entity and all
++      other entities that control, are controlled by, or are under common
++      control with that entity. For the purposes of this definition,
++      "control" means (i) the power, direct or indirect, to cause the
++      direction or management of such entity, whether by contract or
++      otherwise, or (ii) ownership of fifty percent (50%) or more of the
++      outstanding shares, or (iii) beneficial ownership of such entity.
++
++      "You" (or "Your") shall mean an individual or Legal Entity
++      exercising permissions granted by this License.
++
++      "Source" form shall mean the preferred form for making modifications,
++      including but not limited to software source code, documentation
++      source, and configuration files.
++
++      "Object" form shall mean any form resulting from mechanical
++      transformation or translation of a Source form, including but
++      not limited to compiled object code, generated documentation,
++      and conversions to other media types.
++
++      "Work" shall mean the work of authorship, whether in Source or
++      Object form, made available under the License, as indicated by a
++      copyright notice that is included in or attached to the work
++      (an example is provided in the Appendix below).
++
++      "Derivative Works" shall mean any work, whether in Source or Object
++      form, that is based on (or derived from) the Work and for which the
++      editorial revisions, annotations, elaborations, or other modifications
++      represent, as a whole, an original work of authorship. For the purposes
++      of this License, Derivative Works shall not include works that remain
++      separable from, or merely link (or bind by name) to the interfaces of,
++      the Work and Derivative Works thereof.
++
++      "Contribution" shall mean any work of authorship, including
++      the original version of the Work and any modifications or additions
++      to that Work or Derivative Works thereof, that is intentionally
++      submitted to Licensor for inclusion in the Work by the copyright owner
++      or by an individual or Legal Entity authorized to submit on behalf of
++      the copyright owner. For the purposes of this definition, "submitted"
++      means any form of electronic, verbal, or written communication sent
++      to the Licensor or its representatives, including but not limited to
++      communication on electronic mailing lists, source code control systems,
++      and issue tracking systems that are managed by, or on behalf of, the
++      Licensor for the purpose of discussing and improving the Work, but
++      excluding communication that is conspicuously marked or otherwise
++      designated in writing by the copyright owner as "Not a Contribution."
++
++      "Contributor" shall mean Licensor and any individual or Legal Entity
++      on behalf of whom a Contribution has been received by Licensor and
++      subsequently incorporated within the Work.
++
++   2. Grant of Copyright License. Subject to the terms and conditions of
++      this License, each Contributor hereby grants to You a perpetual,
++      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
++      copyright license to reproduce, prepare Derivative Works of,
++      publicly display, publicly perform, sublicense, and distribute the
++      Work and such Derivative Works in Source or Object form.
++
++   3. Grant of Patent License. Subject to the terms and conditions of
++      this License, each Contributor hereby grants to You a perpetual,
++      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
++      (except as stated in this section) patent license to make, have made,
++      use, offer to sell, sell, import, and otherwise transfer the Work,
++      where such license applies only to those patent claims licensable
++      by such Contributor that are necessarily infringed by their
++      Contribution(s) alone or by combination of their Contribution(s)
++      with the Work to which such Contribution(s) was submitted. If You
++      institute patent litigation against any entity (including a
++      cross-claim or counterclaim in a lawsuit) alleging that the Work
++      or a Contribution incorporated within the Work constitutes direct
++      or contributory patent infringement, then any patent licenses
++      granted to You under this License for that Work shall terminate
++      as of the date such litigation is filed.
++
++   4. Redistribution. You may reproduce and distribute copies of the
++      Work or Derivative Works thereof in any medium, with or without
++      modifications, and in Source or Object form, provided that You
++      meet the following conditions:
++
++      (a) You must give any other recipients of the Work or
++          Derivative Works a copy of this License; and
++
++      (b) You must cause any modified files to carry prominent notices
++          stating that You changed the files; and
++
++      (c) You must retain, in the Source form of any Derivative Works
++          that You distribute, all copyright, patent, trademark, and
++          attribution notices from the Source form of the Work,
++          excluding those notices that do not pertain to any part of
++          the Derivative Works; and
++
++      (d) If the Work includes a "NOTICE" text file as part of its
++          distribution, then any Derivative Works that You distribute must
++          include a readable copy of the attribution notices contained
++          within such NOTICE file, excluding those notices that do not
++          pertain to any part of the Derivative Works, in at least one
++          of the following places: within a NOTICE text file distributed
++          as part of the Derivative Works; within the Source form or
++          documentation, if provided along with the Derivative Works; or,
++          within a display generated by the Derivative Works, if and
++          wherever such third-party notices normally appear. The contents
++          of the NOTICE file are for informational purposes only and
++          do not modify the License. You may add Your own attribution
++          notices within Derivative Works that You distribute, alongside
++          or as an addendum to the NOTICE text from the Work, provided
++          that such additional attribution notices cannot be construed
++          as modifying the License.
++
++      You may add Your own copyright statement to Your modifications and
++      may provide additional or different license terms and conditions
++      for use, reproduction, or distribution of Your modifications, or
++      for any such Derivative Works as a whole, provided Your use,
++      reproduction, and distribution of the Work otherwise complies with
++      the conditions stated in this License.
++
++   5. Submission of Contributions. Unless You explicitly state otherwise,
++      any Contribution intentionally submitted for inclusion in the Work
++      by You to the Licensor shall be under the terms and conditions of
++      this License, without any additional terms or conditions.
++      Notwithstanding the above, nothing herein shall supersede or modify
++      the terms of any separate license agreement you may have executed
++      with Licensor regarding such Contributions.
++
++   6. Trademarks. This License does not grant permission to use the trade
++      names, trademarks, service marks, or product names of the Licensor,
++      except as required for reasonable and customary use in describing the
++      origin of the Work and reproducing the content of the NOTICE file.
++
++   7. Disclaimer of Warranty. Unless required by applicable law or
++      agreed to in writing, Licensor provides the Work (and each
++      Contributor provides its Contributions) on an "AS IS" BASIS,
++      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
++      implied, including, without limitation, any warranties or conditions
++      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
++      PARTICULAR PURPOSE. You are solely responsible for determining the
++      appropriateness of using or redistributing the Work and assume any
++      risks associated with Your exercise of permissions under this License.
++
++   8. Limitation of Liability. In no event and under no legal theory,
++      whether in tort (including negligence), contract, or otherwise,
++      unless required by applicable law (such as deliberate and grossly
++      negligent acts) or agreed to in writing, shall any Contributor be
++      liable to You for damages, including any direct, indirect, special,
++      incidental, or consequential damages of any character arising as a
++      result of this License or out of the use or inability to use the
++      Work (including but not limited to damages for loss of goodwill,
++      work stoppage, computer failure or malfunction, or any and all
++      other commercial damages or losses), even if such Contributor
++      has been advised of the possibility of such damages.
++
++   9. Accepting Warranty or Additional Liability. While redistributing
++      the Work or Derivative Works thereof, You may choose to offer,
++      and charge a fee for, acceptance of support, warranty, indemnity,
++      or other liability obligations and/or rights consistent with this
++      License. However, in accepting such obligations, You may act only
++      on Your own behalf and on Your sole responsibility, not on behalf
++      of any other Contributor, and only if You agree to indemnify,
++      defend, and hold each Contributor harmless for any liability
++      incurred by, or claims asserted against, such Contributor by reason
++      of your accepting any such warranty or additional liability.
++
++   END OF TERMS AND CONDITIONS
++
++   APPENDIX: How to apply the Apache License to your work.
++
++      To apply the Apache License to your work, attach the following
++      boilerplate notice, with the fields enclosed by brackets "[]"
++      replaced with your own identifying information. (Don't include
++      the brackets!)  The text should be enclosed in the appropriate
++      comment syntax for the file format. We also recommend that a
++      file or class name and description of purpose be included on the
++      same "printed page" as the copyright notice for easier
++      identification within third-party archives.
++
++   Copyright [yyyy] [name of copyright owner]
++
++   Licensed under the Apache License, Version 2.0 (the "License");
++   you may not use this file except in compliance with the License.
++   You may obtain a copy of the License at
++
++       http://www.apache.org/licenses/LICENSE-2.0
++
++   Unless required by applicable law or agreed to in writing, software
++   distributed under the License is distributed on an "AS IS" BASIS,
++   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++   See the License for the specific language governing permissions and
++   limitations under the License.
+diff --git a/lucene/licenses/log4j-NOTICE.txt b/lucene/licenses/log4j-NOTICE.txt
+new file mode 100644
+index 00000000000..d697542317c
+--- /dev/null
++++ b/lucene/licenses/log4j-NOTICE.txt
+@@ -0,0 +1,5 @@
++Apache log4j
++Copyright 2010 The Apache Software Foundation
++
++This product includes software developed at
++The Apache Software Foundation (http://www.apache.org/).
+diff --git a/lucene/licenses/log4j-api-2.11.2.jar.sha1 b/lucene/licenses/log4j-api-2.11.2.jar.sha1
+new file mode 100644
+index 00000000000..0cdea100b72
+--- /dev/null
++++ b/lucene/licenses/log4j-api-2.11.2.jar.sha1
+@@ -0,0 +1 @@
++f5e9a2ffca496057d6891a3de65128efc636e26e
+diff --git a/lucene/licenses/log4j-api-LICENSE-ASL.txt b/lucene/licenses/log4j-api-LICENSE-ASL.txt
+new file mode 100644
+index 00000000000..f49a4e16e68
+--- /dev/null
++++ b/lucene/licenses/log4j-api-LICENSE-ASL.txt
+@@ -0,0 +1,201 @@
++                                 Apache License
++                           Version 2.0, January 2004
++                        http://www.apache.org/licenses/
++
++   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
++
++   1. Definitions.
++
++      "License" shall mean the terms and conditions for use, reproduction,
++      and distribution as defined by Sections 1 through 9 of this document.
++
++      "Licensor" shall mean the copyright owner or entity authorized by
++      the copyright owner that is granting the License.
++
++      "Legal Entity" shall mean the union of the acting entity and all
++      other entities that control, are controlled by, or are under common
++      control with that entity. For the purposes of this definition,
++      "control" means (i) the power, direct or indirect, to cause the
++      direction or management of such entity, whether by contract or
++      otherwise, or (ii) ownership of fifty percent (50%) or more of the
++      outstanding shares, or (iii) beneficial ownership of such entity.
++
++      "You" (or "Your") shall mean an individual or Legal Entity
++      exercising permissions granted by this License.
++
++      "Source" form shall mean the preferred form for making modifications,
++      including but not limited to software source code, documentation
++      source, and configuration files.
++
++      "Object" form shall mean any form resulting from mechanical
++      transformation or translation of a Source form, including but
++      not limited to compiled object code, generated documentation,
++      and conversions to other media types.
++
++      "Work" shall mean the work of authorship, whether in Source or
++      Object form, made available under the License, as indicated by a
++      copyright notice that is included in or attached to the work
++      (an example is provided in the Appendix below).
++
++      "Derivative Works" shall mean any work, whether in Source or Object
++      form, that is based on (or derived from) the Work and for which the
++      editorial revisions, annotations, elaborations, or other modifications
++      represent, as a whole, an original work of authorship. For the purposes
++      of this License, Derivative Works shall not include works that remain
++      separable from, or merely link (or bind by name) to the interfaces of,
++      the Work and Derivative Works thereof.
++
++      "Contribution" shall mean any work of authorship, including
++      the original version of the Work and any modifications or additions
++      to that Work or Derivative Works thereof, that is intentionally
++      submitted to Licensor for inclusion in the Work by the copyright owner
++      or by an individual or Legal Entity authorized to submit on behalf of
++      the copyright owner. For the purposes of this definition, "submitted"
++      means any form of electronic, verbal, or written communication sent
++      to the Licensor or its representatives, including but not limited to
++      communication on electronic mailing lists, source code control systems,
++      and issue tracking systems that are managed by, or on behalf of, the
++      Licensor for the purpose of discussing and improving the Work, but
++      excluding communication that is conspicuously marked or otherwise
++      designated in writing by the copyright owner as "Not a Contribution."
++
++      "Contributor" shall mean Licensor and any individual or Legal Entity
++      on behalf of whom a Contribution has been received by Licensor and
++      subsequently incorporated within the Work.
++
++   2. Grant of Copyright License. Subject to the terms and conditions of
++      this License, each Contributor hereby grants to You a perpetual,
++      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
++      copyright license to reproduce, prepare Derivative Works of,
++      publicly display, publicly perform, sublicense, and distribute the
++      Work and such Derivative Works in Source or Object form.
++
++   3. Grant of Patent License. Subject to the terms and conditions of
++      this License, each Contributor hereby grants to You a perpetual,
++      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
++      (except as stated in this section) patent license to make, have made,
++      use, offer to sell, sell, import, and otherwise transfer the Work,
++      where such license applies only to those patent claims licensable
++      by such Contributor that are necessarily infringed by their
++      Contribution(s) alone or by combination of their Contribution(s)
++      with the Work to which such Contribution(s) was submitted. If You
++      institute patent litigation against any entity (including a
++      cross-claim or counterclaim in a lawsuit) alleging that the Work
++      or a Contribution incorporated within the Work constitutes direct
++      or contributory patent infringement, then any patent licenses
++      granted to You under this License for that Work shall terminate
++      as of the date such litigation is filed.
++
++   4. Redistribution. You may reproduce and distribute copies of the
++      Work or Derivative Works thereof in any medium, with or without
++      modifications, and in Source or Object form, provided that You
++      meet the following conditions:
++
++      (a) You must give any other recipients of the Work or
++          Derivative Works a copy of this License; and
++
++      (b) You must cause any modified files to carry prominent notices
++          stating that You changed the files; and
++
++      (c) You must retain, in the Source form of any Derivative Works
++          that You distribute, all copyright, patent, trademark, and
++          attribution notices from the Source form of the Work,
++          excluding those notices that do not pertain to any part of
++          the Derivative Works; and
++
++      (d) If the Work includes a "NOTICE" text file as part of its
++          distribution, then any Derivative Works that You distribute must
++          include a readable copy of the attribution notices contained
++          within such NOTICE file, excluding those notices that do not
++          pertain to any part of the Derivative Works, in at least one
++          of the following places: within a NOTICE text file distributed
++          as part of the Derivative Works; within the Source form or
++          documentation, if provided along with the Derivative Works; or,
++          within a display generated by the Derivative Works, if and
++          wherever such third-party notices normally appear. The contents
++          of the NOTICE file are for informational purposes only and
++          do not modify the License. You may add Your own attribution
++          notices within Derivative Works that You distribute, alongside
++          or as an addendum to the NOTICE text from the Work, provided
++          that such additional attribution notices cannot be construed
++          as modifying the License.
++
++      You may add Your own copyright statement to Your modifications and
++      may provide additional or different license terms and conditions
++      for use, reproduction, or distribution of Your modifications, or
++      for any such Derivative Works as a whole, provided Your use,
++      reproduction, and distribution of the Work otherwise complies with
++      the conditions stated in this License.
++
++   5. Submission of Contributions. Unless You explicitly state otherwise,
++      any Contribution intentionally submitted for inclusion in the Work
++      by You to the Licensor shall be under the terms and conditions of
++      this License, without any additional terms or conditions.
++      Notwithstanding the above, nothing herein shall supersede or modify
++      the terms of any separate license agreement you may have executed
++      with Licensor regarding such Contributions.
++
++   6. Trademarks. This License does not grant permission to use the trade
++      names, trademarks, service marks, or product names of the Licensor,
++      except as required for reasonable and customary use in describing the
++      origin of the Work and reproducing the content of the NOTICE file.
++
++   7. Disclaimer of Warranty. Unless required by applicable law or
++      agreed to in writing, Licensor provides the Work (and each
++      Contributor provides its Contributions) on an "AS IS" BASIS,
++      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
++      implied, including, without limitation, any warranties or conditions
++      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
++      PARTICULAR PURPOSE. You are solely responsible for determining the
++      appropriateness of using or redistributing the Work and assume any
++      risks associated with Your exercise of permissions under this License.
++
++   8. Limitation of Liability. In no event and under no legal theory,
++      whether in tort (including negligence), contract, or otherwise,
++      unless required by applicable law (such as deliberate and grossly
++      negligent acts) or agreed to in writing, shall any Contributor be
++      liable to You for damages, including any direct, indirect, special,
++      incidental, or consequential damages of any character arising as a
++      result of this License or out of the use or inability to use the
++      Work (including but not limited to damages for loss of goodwill,
++      work stoppage, computer failure or malfunction, or any and all
++      other commercial damages or losses), even if such Contributor
++      has been advised of the possibility of such damages.
++
++   9. Accepting Warranty or Additional Liability. While redistributing
++      the Work or Derivative Works thereof, You may choose to offer,
++      and charge a fee for, acceptance of support, warranty, indemnity,
++      or other liability obligations and/or rights consistent with this
++      License. However, in accepting such obligations, You may act only
++      on Your own behalf and on Your sole responsibility, not on behalf
++      of any other Contributor, and only if You agree to indemnify,
++      defend, and hold each Contributor harmless for any liability
++      incurred by, or claims asserted against, such Contributor by reason
++      of your accepting any such warranty or additional liability.
++
++   END OF TERMS AND CONDITIONS
++
++   APPENDIX: How to apply the Apache License to your work.
++
++      To apply the Apache License to your work, attach the following
++      boilerplate notice, with the fields enclosed by brackets "[]"
++      replaced with your own identifying information. (Don't include
++      the brackets!)  The text should be enclosed in the appropriate
++      comment syntax for the file format. We also recommend that a
++      file or class name and description of purpose be included on the
++      same "printed page" as the copyright notice for easier
++      identification within third-party archives.
++
++   Copyright [yyyy] [name of copyright owner]
++
++   Licensed under the Apache License, Version 2.0 (the "License");
++   you may not use this file except in compliance with the License.
++   You may obtain a copy of the License at
++
++       http://www.apache.org/licenses/LICENSE-2.0
++
++   Unless required by applicable law or agreed to in writing, software
++   distributed under the License is distributed on an "AS IS" BASIS,
++   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++   See the License for the specific language governing permissions and
++   limitations under the License.
+\ No newline at end of file
+diff --git a/lucene/licenses/log4j-api-NOTICE.txt b/lucene/licenses/log4j-api-NOTICE.txt
+new file mode 100644
+index 00000000000..ebba5ac0018
+--- /dev/null
++++ b/lucene/licenses/log4j-api-NOTICE.txt
+@@ -0,0 +1,17 @@
++Apache Log4j
++Copyright 1999-2017 Apache Software Foundation
++
++This product includes software developed at
++The Apache Software Foundation (http://www.apache.org/).
++
++ResolverUtil.java
++Copyright 2005-2006 Tim Fennell
++
++Dumbster SMTP test server
++Copyright 2004 Jason Paul Kitchen
++
++TypeUtil.java
++Copyright 2002-2012 Ramnivas Laddad, Juergen Hoeller, Chris Beams
++
++picocli (http://picocli.info)
++Copyright 2017 Remko Popma
+\ No newline at end of file
+diff --git a/lucene/licenses/log4j-core-2.11.2.jar.sha1 b/lucene/licenses/log4j-core-2.11.2.jar.sha1
+new file mode 100644
+index 00000000000..ec2acae4df7
+--- /dev/null
++++ b/lucene/licenses/log4j-core-2.11.2.jar.sha1
+@@ -0,0 +1 @@
++6c2fb3f5b7cd27504726aef1b674b542a0c9cf53
+diff --git a/lucene/licenses/log4j-core-LICENSE-ASL.txt b/lucene/licenses/log4j-core-LICENSE-ASL.txt
+new file mode 100644
+index 00000000000..f49a4e16e68
+--- /dev/null
++++ b/lucene/licenses/log4j-core-LICENSE-ASL.txt
+@@ -0,0 +1,201 @@
++                                 Apache License
++                           Version 2.0, January 2004
++                        http://www.apache.org/licenses/
++
++   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
++
++   1. Definitions.
++
++      "License" shall mean the terms and conditions for use, reproduction,
++      and distribution as defined by Sections 1 through 9 of this document.
++
++      "Licensor" shall mean the copyright owner or entity authorized by
++      the copyright owner that is granting the License.
++
++      "Legal Entity" shall mean the union of the acting entity and all
++      other entities that control, are controlled by, or are under common
++      control with that entity. For the purposes of this definition,
++      "control" means (i) the power, direct or indirect, to cause the
++      direction or management of such entity, whether by contract or
++      otherwise, or (ii) ownership of fifty percent (50%) or more of the
++      outstanding shares, or (iii) beneficial ownership of such entity.
++
++      "You" (or "Your") shall mean an individual or Legal Entity
++      exercising permissions granted by this License.
++
++      "Source" form shall mean the preferred form for making modifications,
++      including but not limited to software source code, documentation
++      source, and configuration files.
++
++      "Object" form shall mean any form resulting from mechanical
++      transformation or translation of a Source form, including but
++      not limited to compiled object code, generated documentation,
++      and conversions to other media types.
++
++      "Work" shall mean the work of authorship, whether in Source or
++      Object form, made available under the License, as indicated by a
++      copyright notice that is included in or attached to the work
++      (an example is provided in the Appendix below).
++
++      "Derivative Works" shall mean any work, whether in Source or Object
++      form, that is based on (or derived from) the Work and for which the
++      editorial revisions, annotations, elaborations, or other modifications
++      represent, as a whole, an original work of authorship. For the purposes
++      of this License, Derivative Works shall not include works that remain
++      separable from, or merely link (or bind by name) to the interfaces of,
++      the Work and Derivative Works thereof.
++
++      "Contribution" shall mean any work of authorship, including
++      the original version of the Work and any modifications or additions
++      to that Work or Derivative Works thereof, that is intentionally
++      submitted to Licensor for inclusion in the Work by the copyright owner
++      or by an individual or Legal Entity authorized to submit on behalf of
++      the copyright owner. For the purposes of this definition, "submitted"
++      means any form of electronic, verbal, or written communication sent
++      to the Licensor or its representatives, including but not limited to
++      communication on electronic mailing lists, source code control systems,
++      and issue tracking systems that are managed by, or on behalf of, the
++      Licensor for the purpose of discussing and improving the Work, but
++      excluding communication that is conspicuously marked or otherwise
++      designated in writing by the copyright owner as "Not a Contribution."
++
++      "Contributor" shall mean Licensor and any individual or Legal Entity
++      on behalf of whom a Contribution has been received by Licensor and
++      subsequently incorporated within the Work.
++
++   2. Grant of Copyright License. Subject to the terms and conditions of
++      this License, each Contributor hereby grants to You a perpetual,
++      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
++      copyright license to reproduce, prepare Derivative Works of,
++      publicly display, publicly perform, sublicense, and distribute the
++      Work and such Derivative Works in Source or Object form.
++
++   3. Grant of Patent License. Subject to the terms and conditions of
++      this License, each Contributor hereby grants to You a perpetual,
++      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
++      (except as stated in this section) patent license to make, have made,
++      use, offer to sell, sell, import, and otherwise transfer the Work,
++      where such license applies only to those patent claims licensable
++      by such Contributor that are necessarily infringed by their
++      Contribution(s) alone or by combination of their Contribution(s)
++      with the Work to which such Contribution(s) was submitted. If You
++      institute patent litigation against any entity (including a
++      cross-claim or counterclaim in a lawsuit) alleging that the Work
++      or a Contribution incorporated within the Work constitutes direct
++      or contributory patent infringement, then any patent licenses
++      granted to You under this License for that Work shall terminate
++      as of the date such litigation is filed.
++
++   4. Redistribution. You may reproduce and distribute copies of the
++      Work or Derivative Works thereof in any medium, with or without
++      modifications, and in Source or Object form, provided that You
++      meet the following conditions:
++
++      (a) You must give any other recipients of the Work or
++          Derivative Works a copy of this License; and
++
++      (b) You must cause any modified files to carry prominent notices
++          stating that You changed the files; and
++
++      (c) You must retain, in the Source form of any Derivative Works
++          that You distribute, all copyright, patent, trademark, and
++          attribution notices from the Source form of the Work,
++          excluding those notices that do not pertain to any part of
++          the Derivative Works; and
++
++      (d) If the Work includes a "NOTICE" text file as part of its
++          distribution, then any Derivative Works that You distribute must
++          include a readable copy of the attribution notices contained
++          within such NOTICE file, excluding those notices that do not
++          pertain to any part of the Derivative Works, in at least one
++          of the following places: within a NOTICE text file distributed
++          as part of the Derivative Works; within the Source form or
++          documentation, if provided along with the Derivative Works; or,
++          within a display generated by the Derivative Works, if and
++          wherever such third-party notices normally appear. The contents
++          of the NOTICE file are for informational purposes only and
++          do not modify the License. You may add Your own attribution
++          notices within Derivative Works that You distribute, alongside
++          or as an addendum to the NOTICE text from the Work, provided
++          that such additional attribution notices cannot be construed
++          as modifying the License.
++
++      You may add Your own copyright statement to Your modifications and
++      may provide additional or different license terms and conditions
++      for use, reproduction, or distribution of Your modifications, or
++      for any such Derivative Works as a whole, provided Your use,
++      reproduction, and distribution of the Work otherwise complies with
++      the conditions stated in this License.
++
++   5. Submission of Contributions. Unless You explicitly state otherwise,
++      any Contribution intentionally submitted for inclusion in the Work
++      by You to the Licensor shall be under the terms and conditions of
++      this License, without any additional terms or conditions.
++      Notwithstanding the above, nothing herein shall supersede or modify
++      the terms of any separate license agreement you may have executed
++      with Licensor regarding such Contributions.
++
++   6. Trademarks. This License does not grant permission to use the trade
++      names, trademarks, service marks, or product names of the Licensor,
++      except as required for reasonable and customary use in describing the
++      origin of the Work and reproducing the content of the NOTICE file.
++
++   7. Disclaimer of Warranty. Unless required by applicable law or
++      agreed to in writing, Licensor provides the Work (and each
++      Contributor provides its Contributions) on an "AS IS" BASIS,
++      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
++      implied, including, without limitation, any warranties or conditions
++      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
++      PARTICULAR PURPOSE. You are solely responsible for determining the
++      appropriateness of using or redistributing the Work and assume any
++      risks associated with Your exercise of permissions under this License.
++
++   8. Limitation of Liability. In no event and under no legal theory,
++      whether in tort (including negligence), contract, or otherwise,
++      unless required by applicable law (such as deliberate and grossly
++      negligent acts) or agreed to in writing, shall any Contributor be
++      liable to You for damages, including any direct, indirect, special,
++      incidental, or consequential damages of any character arising as a
++      result of this License or out of the use or inability to use the
++      Work (including but not limited to damages for loss of goodwill,
++      work stoppage, computer failure or malfunction, or any and all
++      other commercial damages or losses), even if such Contributor
++      has been advised of the possibility of such damages.
++
++   9. Accepting Warranty or Additional Liability. While redistributing
++      the Work or Derivative Works thereof, You may choose to offer,
++      and charge a fee for, acceptance of support, warranty, indemnity,
++      or other liability obligations and/or rights consistent with this
++      License. However, in accepting such obligations, You may act only
++      on Your own behalf and on Your sole responsibility, not on behalf
++      of any other Contributor, and only if You agree to indemnify,
++      defend, and hold each Contributor harmless for any liability
++      incurred by, or claims asserted against, such Contributor by reason
++      of your accepting any such warranty or additional liability.
++
++   END OF TERMS AND CONDITIONS
++
++   APPENDIX: How to apply the Apache License to your work.
++
++      To apply the Apache License to your work, attach the following
++      boilerplate notice, with the fields enclosed by brackets "[]"
++      replaced with your own identifying information. (Don't include
++      the brackets!)  The text should be enclosed in the appropriate
++      comment syntax for the file format. We also recommend that a
++      file or class name and description of purpose be included on the
++      same "printed page" as the copyright notice for easier
++      identification within third-party archives.
++
++   Copyright [yyyy] [name of copyright owner]
++
++   Licensed under the Apache License, Version 2.0 (the "License");
++   you may not use this file except in compliance with the License.
++   You may obtain a copy of the License at
++
++       http://www.apache.org/licenses/LICENSE-2.0
++
++   Unless required by applicable law or agreed to in writing, software
++   distributed under the License is distributed on an "AS IS" BASIS,
++   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++   See the License for the specific language governing permissions and
++   limitations under the License.
+\ No newline at end of file
+diff --git a/lucene/licenses/log4j-core-NOTICE.txt b/lucene/licenses/log4j-core-NOTICE.txt
+new file mode 100644
+index 00000000000..ebba5ac0018
+--- /dev/null
++++ b/lucene/licenses/log4j-core-NOTICE.txt
+@@ -0,0 +1,17 @@
++Apache Log4j
++Copyright 1999-2017 Apache Software Foundation
++
++This product includes software developed at
++The Apache Software Foundation (http://www.apache.org/).
++
++ResolverUtil.java
++Copyright 2005-2006 Tim Fennell
++
++Dumbster SMTP test server
++Copyright 2004 Jason Paul Kitchen
++
++TypeUtil.java
++Copyright 2002-2012 Ramnivas Laddad, Juergen Hoeller, Chris Beams
++
++picocli (http://picocli.info)
++Copyright 2017 Remko Popma
+\ No newline at end of file
+diff --git a/lucene/luke/bin/luke.bat b/lucene/luke/bin/luke.bat
+new file mode 100644
+index 00000000000..4d83d8bf319
+--- /dev/null
++++ b/lucene/luke/bin/luke.bat
+@@ -0,0 +1,13 @@
++@echo off
++@setlocal enabledelayedexpansion
++
++cd /d %~dp0
++
++set JAVA_OPTIONS=%JAVA_OPTIONS% -Xmx1024m -Xms512m -XX:MaxMetaspaceSize=256m
++
++set CLASSPATHS=.\*;.\lib\*;..\core\*;..\codecs\*;..\backward-codecs\*;..\queries\*;..\queryparser\*;..\suggest\*;..\misc\*
++for /d %%A in (..\analysis\*) do (
++    set "CLASSPATHS=!CLASSPATHS!;%%A\*;%%A\lib\*"
++)
++
++start javaw -cp %CLASSPATHS% %JAVA_OPTIONS% org.apache.lucene.luke.app.desktop.LukeMain
+diff --git a/lucene/luke/bin/luke.sh b/lucene/luke/bin/luke.sh
+new file mode 100755
+index 00000000000..7c7d9191056
+--- /dev/null
++++ b/lucene/luke/bin/luke.sh
+@@ -0,0 +1,18 @@
++#!/bin/bash
++
++LUKE_HOME=$(cd $(dirname $0) && pwd)
++cd ${LUKE_HOME}
++
++JAVA_OPTIONS="${JAVA_OPTIONS} -Xmx1024m -Xms512m -XX:MaxMetaspaceSize=256m"
++
++CLASSPATHS="./*:./lib/*:../core/*:../codecs/*:../backward-codecs/*:../queries/*:../queryparser/*:../suggest/*:../misc/*"
++for dir in `ls ../analysis`; do
++  CLASSPATHS="${CLASSPATHS}:../analysis/${dir}/*:../analysis/${dir}/lib/*"
++done
++
++LOG_DIR=${HOME}/.luke.d/
++ if [[ ! -d ${LOG_DIR} ]]; then
++   mkdir ${LOG_DIR}
++ fi
++
++nohup java -cp ${CLASSPATHS} ${JAVA_OPTIONS} org.apache.lucene.luke.app.desktop.LukeMain > ${LOG_DIR}/luke_out.log 2>&1 &
+diff --git a/lucene/luke/build.xml b/lucene/luke/build.xml
+new file mode 100644
+index 00000000000..9064d26e488
+--- /dev/null
++++ b/lucene/luke/build.xml
+@@ -0,0 +1,77 @@
++<?xml version="1.0"?>
++
++<!--
++    Licensed to the Apache Software Foundation (ASF) under one or more
++    contributor license agreements.  See the NOTICE file distributed with
++    this work for additional information regarding copyright ownership.
++    The ASF licenses this file to You under the Apache License, Version 2.0
++    the "License"); you may not use this file except in compliance with
++    the License.  You may obtain a copy of the License at
++ 
++        http://www.apache.org/licenses/LICENSE-2.0
++ 
++    Unless required by applicable law or agreed to in writing, software
++    distributed under the License is distributed on an "AS IS" BASIS,
++    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++    See the License for the specific language governing permissions and
++    limitations under the License.
++ -->
++
++<project name="luke" default="default">
++
++  <description>
++    Luke - Lucene Toolbox
++  </description>
++
++  <!-- use full Java SE API (project default 'compact2' does not include Swing) -->
++  <property name="javac.profile.args" value=""/>
++
++  <import file="../module-build.xml"/>
++
++  <target name="init" depends="module-build.init,jar-lucene-core"/>
++
++  <path id="classpath">
++    <pathelement path="${lucene-core.jar}"/>
++    <pathelement path="${codecs.jar}"/>
++    <pathelement path="${backward-codecs.jar}"/>
++    <pathelement path="${analyzers-common.jar}"/>
++    <pathelement path="${misc.jar}"/>
++    <pathelement path="${queryparser.jar}"/>
++    <pathelement path="${queries.jar}"/>
++    <fileset dir="lib"/>
++    <path refid="base.classpath"/>
++  </path>
++
++  <target name="javadocs" depends="compile-core,javadocs-lucene-core,javadocs-analyzers-common,check-javadocs-uptodate"
++          unless="javadocs-uptodate-${name}">
++    <invoke-module-javadoc>
++      <links>
++        <link href="../analyzers-common"/>
++      </links>
++    </invoke-module-javadoc>
++  </target>
++
++  <target name="build-artifacts-and-tests" depends="jar, compile-test">
++    <!-- copy start scripts -->
++    <copy todir="${build.dir}">
++      <fileset dir="${common.dir}/luke/bin">
++        <include name="**/*.sh"/>
++        <include name="**/*.bat"/>
++      </fileset>
++    </copy>
++  </target>
++
++  <!-- launch Luke -->
++  <target name="run" depends="compile-core" description="Launch Luke GUI">
++    <java classname="org.apache.lucene.luke.app.desktop.LukeMain"
++          classpath="${build.dir}/classes/java"
++          fork="true"
++          maxmemory="512m">
++      <classpath refid="classpath"/>
++    </java>
++  </target>
++  
++  <target name="compile-core"
++          depends="jar-codecs,jar-backward-codecs,jar-analyzers-common,jar-misc,jar-queryparser,jar-queries,jar-misc,common.compile-core"/>
++
++</project>
+diff --git a/lucene/luke/ivy.xml b/lucene/luke/ivy.xml
+new file mode 100644
+index 00000000000..88d9d8c63b6
+--- /dev/null
++++ b/lucene/luke/ivy.xml
+@@ -0,0 +1,34 @@
++<!--
++   Licensed to the Apache Software Foundation (ASF) under one
++   or more contributor license agreements.  See the NOTICE file
++   distributed with this work for additional information
++   regarding copyright ownership.  The ASF licenses this file
++   to you under the Apache License, Version 2.0 (the
++   "License"); you may not use this file except in compliance
++   with the License.  You may obtain a copy of the License at
++
++     http://www.apache.org/licenses/LICENSE-2.0
++
++   Unless required by applicable law or agreed to in writing,
++   software distributed under the License is distributed on an
++   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
++   KIND, either express or implied.  See the License for the
++   specific language governing permissions and limitations
++   under the License.    
++-->
++<ivy-module version="2.0">
++  <info organisation="org.apache.lucene" module="luke"/>
++
++  <configurations defaultconfmapping="compile->default;logging->default">
++    <conf name="compile" transitive="false"/>
++    <conf name="logging" transitive="false"/>
++  </configurations>
++
++  <dependencies>
++    <dependency org="org.apache.logging.log4j" name="log4j-api" rev="${/org.apache.logging.log4j/log4j-api}"
++                conf="logging"/>
++    <dependency org="org.apache.logging.log4j" name="log4j-core" rev="${/org.apache.logging.log4j/log4j-core}"
++                conf="logging"/>
++    <exclude org="*" ext="*" matcher="regexp" type="${ivy.exclude.types}"/>
++  </dependencies>
++</ivy-module>
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/AbstractHandler.java b/lucene/luke/src/java/org/apache/lucene/luke/app/AbstractHandler.java
+new file mode 100644
+index 00000000000..ab967a8d149
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/AbstractHandler.java
+@@ -0,0 +1,47 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app;
++
++import java.lang.invoke.MethodHandles;
++import java.util.ArrayList;
++import java.util.List;
++
++import org.apache.logging.log4j.Logger;
++import org.apache.lucene.luke.util.LoggerFactory;
++
++/** Abstract handler class */
++public abstract class AbstractHandler<T extends Observer> {
++
++  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
++
++  private List<T> observers = new ArrayList<>();
++
++  public void addObserver(T observer) {
++    observers.add(observer);
++    log.debug("{} registered.", observer.getClass().getName());
++  }
++
++  void notifyObservers() {
++    for (T observer : observers) {
++      notifyOne(observer);
++    }
++  }
++
++  protected abstract void notifyOne(T observer);
++
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/DirectoryHandler.java b/lucene/luke/src/java/org/apache/lucene/luke/app/DirectoryHandler.java
+new file mode 100644
+index 00000000000..ec4e7e5d23a
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/DirectoryHandler.java
+@@ -0,0 +1,112 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app;
++
++import java.io.IOException;
++import java.util.Objects;
++
++import org.apache.lucene.luke.app.desktop.util.MessageUtils;
++import org.apache.lucene.luke.models.LukeException;
++import org.apache.lucene.luke.models.util.IndexUtils;
++import org.apache.lucene.store.Directory;
++
++/** Directory open/close handler */
++public final class DirectoryHandler extends AbstractHandler<DirectoryObserver> {
++
++  private static final DirectoryHandler instance = new DirectoryHandler();
++
++  private LukeStateImpl state;
++
++  public static DirectoryHandler getInstance() {
++    return instance;
++  }
++
++  @Override
++  protected void notifyOne(DirectoryObserver observer) {
++    if (state.closed) {
++      observer.closeDirectory();
++    } else {
++      observer.openDirectory(state);
++    }
++  }
++
++  public boolean directoryOpened() {
++    return state != null && !state.closed;
++  }
++
++  public void open(String indexPath, String dirImpl) {
++    Objects.requireNonNull(indexPath);
++
++    if (directoryOpened()) {
++      close();
++    }
++
++    Directory dir;
++    try {
++      dir = IndexUtils.openDirectory(indexPath, dirImpl);
++    } catch (IOException e) {
++      throw new LukeException(MessageUtils.getLocalizedMessage("openindex.message.index_path_invalid", indexPath), e);
++    }
++
++    state = new LukeStateImpl();
++    state.indexPath = indexPath;
++    state.dirImpl = dirImpl;
++    state.dir = dir;
++
++    notifyObservers();
++  }
++
++  public void close() {
++    if (state == null) {
++      return;
++    }
++
++    IndexUtils.close(state.dir);
++
++    state.closed = true;
++    notifyObservers();
++  }
++
++  public LukeState getState() {
++    return state;
++  }
++
++  private static class LukeStateImpl implements LukeState {
++    private boolean closed = false;
++
++    private String indexPath;
++    private String dirImpl;
++    private Directory dir;
++
++    @Override
++    public String getIndexPath() {
++      return indexPath;
++    }
++
++    @Override
++    public String getDirImpl() {
++      return dirImpl;
++    }
++
++    @Override
++    public Directory getDirectory() {
++      return dir;
++    }
++  }
++
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/DirectoryObserver.java b/lucene/luke/src/java/org/apache/lucene/luke/app/DirectoryObserver.java
+new file mode 100644
+index 00000000000..64371150f87
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/DirectoryObserver.java
+@@ -0,0 +1,27 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app;
++
++/** Directory open/close observer */
++public interface DirectoryObserver extends Observer {
++
++  void openDirectory(LukeState state);
++
++  void closeDirectory();
++
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/IndexHandler.java b/lucene/luke/src/java/org/apache/lucene/luke/app/IndexHandler.java
+new file mode 100644
+index 00000000000..17e407043e1
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/IndexHandler.java
+@@ -0,0 +1,147 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app;
++
++import java.lang.invoke.MethodHandles;
++import java.util.Objects;
++
++import org.apache.logging.log4j.Logger;
++import org.apache.lucene.index.IndexReader;
++import org.apache.lucene.luke.app.desktop.util.MessageUtils;
++import org.apache.lucene.luke.models.LukeException;
++import org.apache.lucene.luke.models.util.IndexUtils;
++import org.apache.lucene.luke.util.LoggerFactory;
++
++/** Index open/close handler */
++public final class IndexHandler extends AbstractHandler<IndexObserver> {
++
++  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
++
++  private static final IndexHandler instance = new IndexHandler();
++
++  private LukeStateImpl state;
++
++  public static IndexHandler getInstance() {
++    return instance;
++  }
++
++  @Override
++  protected void notifyOne(IndexObserver observer) {
++    if (state.closed) {
++      observer.closeIndex();
++    } else {
++      observer.openIndex(state);
++    }
++  }
++
++  public boolean indexOpened() {
++    return state != null && !state.closed;
++  }
++
++  public void open(String indexPath, String dirImpl) {
++    open(indexPath, dirImpl, false, false, false);
++  }
++
++  public void open(String indexPath, String dirImpl, boolean readOnly, boolean useCompound, boolean keepAllCommits) {
++    Objects.requireNonNull(indexPath);
++
++    if (indexOpened()) {
++      close();
++    }
++
++    IndexReader reader;
++    try {
++      reader = IndexUtils.openIndex(indexPath, dirImpl);
++    } catch (Exception e) {
++      log.error(e.getMessage(), e);
++      throw new LukeException(MessageUtils.getLocalizedMessage("openindex.message.index_path_invalid", indexPath), e);
++    }
++
++    state = new LukeStateImpl();
++    state.indexPath = indexPath;
++    state.reader = reader;
++    state.dirImpl = dirImpl;
++    state.readOnly = readOnly;
++    state.useCompound = useCompound;
++    state.keepAllCommits = keepAllCommits;
++
++    notifyObservers();
++  }
++
++  public void close() {
++    if (state == null) {
++      return;
++    }
++
++    IndexUtils.close(state.reader);
++
++    state.closed = true;
++    notifyObservers();
++  }
++
++  public void reOpen() {
++    close();
++    open(state.getIndexPath(), state.getDirImpl(), state.readOnly(), state.useCompound(), state.keepAllCommits());
++  }
++
++  public LukeState getState() {
++    return state;
++  }
++
++  private static class LukeStateImpl implements LukeState {
++
++    private boolean closed = false;
++
++    private String indexPath;
++    private IndexReader reader;
++    private String dirImpl;
++    private boolean readOnly;
++    private boolean useCompound;
++    private boolean keepAllCommits;
++
++    @Override
++    public String getIndexPath() {
++      return indexPath;
++    }
++
++    @Override
++    public IndexReader getIndexReader() {
++      return reader;
++    }
++
++    @Override
++    public String getDirImpl() {
++      return dirImpl;
++    }
++
++    @Override
++    public boolean readOnly() {
++      return readOnly;
++    }
++
++    @Override
++    public boolean useCompound() {
++      return useCompound;
++    }
++
++    @Override
++    public boolean keepAllCommits() {
++      return keepAllCommits;
++    }
++  }
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/IndexObserver.java b/lucene/luke/src/java/org/apache/lucene/luke/app/IndexObserver.java
+new file mode 100644
+index 00000000000..599b1090c4d
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/IndexObserver.java
+@@ -0,0 +1,27 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app;
++
++/** Index open/close observer */
++public interface IndexObserver extends Observer {
++
++  void openIndex(LukeState state);
++
++  void closeIndex();
++
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/LukeState.java b/lucene/luke/src/java/org/apache/lucene/luke/app/LukeState.java
+new file mode 100644
+index 00000000000..33ca829bca5
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/LukeState.java
+@@ -0,0 +1,57 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app;
++
++import org.apache.lucene.index.DirectoryReader;
++import org.apache.lucene.index.IndexReader;
++import org.apache.lucene.store.Directory;
++
++/**
++ * Holder for current index/directory.
++ */
++public interface LukeState {
++
++  String getIndexPath();
++
++  String getDirImpl();
++
++  default Directory getDirectory() {
++    throw new UnsupportedOperationException();
++  }
++
++  default IndexReader getIndexReader() {
++    throw new UnsupportedOperationException();
++  }
++
++  default boolean readOnly() {
++    throw new UnsupportedOperationException();
++  }
++
++  default boolean useCompound() {
++    throw new UnsupportedOperationException();
++  }
++
++  default boolean keepAllCommits() {
++    throw new UnsupportedOperationException();
++  }
++
++  default boolean hasDirectoryReader() {
++    return getIndexReader() instanceof DirectoryReader;
++  }
++
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/Observer.java b/lucene/luke/src/java/org/apache/lucene/luke/app/Observer.java
+new file mode 100644
+index 00000000000..290865b8986
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/Observer.java
+@@ -0,0 +1,22 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app;
++
++/** Marker interface for observers */
++public interface Observer {
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/LukeMain.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/LukeMain.java
+new file mode 100644
+index 00000000000..fae52f29abd
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/LukeMain.java
+@@ -0,0 +1,94 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop;
++
++import javax.swing.JFrame;
++import javax.swing.UIManager;
++import java.awt.GraphicsEnvironment;
++import java.io.IOException;
++import java.lang.invoke.MethodHandles;
++import java.nio.file.FileSystems;
++
++import org.apache.logging.log4j.Logger;
++import org.apache.lucene.luke.app.desktop.components.LukeWindowProvider;
++import org.apache.lucene.luke.app.desktop.components.dialog.menubar.OpenIndexDialogFactory;
++import org.apache.lucene.luke.app.desktop.util.DialogOpener;
++import org.apache.lucene.luke.app.desktop.util.FontUtils;
++import org.apache.lucene.luke.app.desktop.util.MessageUtils;
++import org.apache.lucene.luke.util.LoggerFactory;
++
++import static org.apache.lucene.luke.app.desktop.util.ExceptionHandler.handle;
++
++/** Entry class for desktop Luke */
++public class LukeMain {
++
++  public static final String LOG_FILE = System.getProperty("user.home") +
++      FileSystems.getDefault().getSeparator() + ".luke.d" +
++      FileSystems.getDefault().getSeparator() + "luke.log";
++
++  static {
++    LoggerFactory.initGuiLogging(LOG_FILE);
++  }
++  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
++  
++  private static JFrame frame;
++
++  public static JFrame getOwnerFrame() {
++    return frame;
++  }
++
++  private static void createAndShowGUI() {
++    // uncaught error handler
++    MessageBroker messageBroker = MessageBroker.getInstance();
++    Thread.setDefaultUncaughtExceptionHandler((thread, cause) ->
++        handle(cause, messageBroker)
++    );
++
++    try {
++      frame = new LukeWindowProvider().get();
++      frame.setLocation(200, 100);
++      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
++      frame.pack();
++      frame.setVisible(true);
++
++      // show open index dialog
++      OpenIndexDialogFactory openIndexDialogFactory = OpenIndexDialogFactory.getInstance();
++      new DialogOpener<>(openIndexDialogFactory).open(MessageUtils.getLocalizedMessage("openindex.dialog.title"), 600, 420,
++          (factory) -> {
++          });
++    } catch (IOException e) {
++      messageBroker.showUnknownErrorMessage();
++      log.error("Cannot initialize components.", e);
++    }
++  }
++
++  public static void main(String[] args) throws Exception {
++    String lookAndFeelClassName = UIManager.getSystemLookAndFeelClassName();
++    if (!lookAndFeelClassName.contains("AquaLookAndFeel") && !lookAndFeelClassName.contains("PlasticXPLookAndFeel")) {
++      // may be running on linux platform
++      lookAndFeelClassName = "javax.swing.plaf.metal.MetalLookAndFeel";
++    }
++    UIManager.setLookAndFeel(lookAndFeelClassName);
++
++    GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
++    genv.registerFont(FontUtils.createElegantIconFont());
++
++    javax.swing.SwingUtilities.invokeLater(LukeMain::createAndShowGUI);
++
++  }
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/MessageBroker.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/MessageBroker.java
+new file mode 100644
+index 00000000000..9609a2f56ef
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/MessageBroker.java
+@@ -0,0 +1,65 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop;
++
++import java.util.ArrayList;
++import java.util.List;
++
++/** Message broker */
++public class MessageBroker {
++
++  private static final MessageBroker instance = new MessageBroker();
++
++  private List<MessageReceiver> receivers = new ArrayList<>();
++
++  public static MessageBroker getInstance() {
++    return instance;
++  }
++
++  public void registerReceiver(MessageReceiver receiver) {
++    receivers.add(receiver);
++  }
++
++  public void showStatusMessage(String message) {
++    for (MessageReceiver receiver : receivers) {
++      receiver.showStatusMessage(message);
++    }
++  }
++
++  public void showUnknownErrorMessage() {
++    for (MessageReceiver receiver : receivers) {
++      receiver.showUnknownErrorMessage();
++    }
++  }
++
++  public void clearStatusMessage() {
++    for (MessageReceiver receiver : receivers) {
++      receiver.clearStatusMessage();
++    }
++  }
++
++  /** Message receiver in charge of rendering the message. */
++  public interface MessageReceiver {
++    void showStatusMessage(String message);
++
++    void showUnknownErrorMessage();
++
++    void clearStatusMessage();
++  }
++
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/Preferences.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/Preferences.java
+new file mode 100644
+index 00000000000..b0df6607403
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/Preferences.java
+@@ -0,0 +1,69 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop;
++
++import java.awt.Color;
++import java.io.IOException;
++import java.util.List;
++
++/** Preference */
++public interface Preferences {
++
++  List<String> getHistory();
++
++  void addHistory(String indexPath) throws IOException;
++
++  boolean isReadOnly();
++
++  String getDirImpl();
++
++  boolean isNoReader();
++
++  boolean isUseCompound();
++
++  boolean isKeepAllCommits();
++
++  void setIndexOpenerPrefs(boolean readOnly, String dirImpl, boolean noReader, boolean useCompound, boolean keepAllCommits) throws IOException;
++
++  ColorTheme getColorTheme();
++
++  void setColorTheme(ColorTheme theme) throws IOException;
++
++  /** color themes */
++  enum ColorTheme {
++
++    /* Gray theme */
++    GRAY(Color.decode("#e6e6e6")),
++    /* Classic theme */
++    CLASSIC(Color.decode("#ece9d0")),
++    /* Sandstone theme */
++    SANDSTONE(Color.decode("#ddd9d4")),
++    /* Navy theme */
++    NAVY(Color.decode("#e6e6ff"));
++
++    private Color backgroundColor;
++
++    ColorTheme(Color backgroundColor) {
++      this.backgroundColor = backgroundColor;
++    }
++
++    public Color getBackgroundColor() {
++      return backgroundColor;
++    }
++  }
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/PreferencesFactory.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/PreferencesFactory.java
+new file mode 100644
+index 00000000000..2502553297f
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/PreferencesFactory.java
+@@ -0,0 +1,34 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop;
++
++import java.io.IOException;
++
++/** Factory of {@link Preferences} */
++public class PreferencesFactory {
++
++  private static Preferences prefs;
++
++  public synchronized static Preferences getInstance() throws IOException {
++    if (prefs == null) {
++      prefs = new PreferencesImpl();
++    }
++    return prefs;
++  }
++
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/PreferencesImpl.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/PreferencesImpl.java
+new file mode 100644
+index 00000000000..ebf78c5a57b
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/PreferencesImpl.java
+@@ -0,0 +1,143 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop;
++
++import java.io.IOException;
++import java.nio.file.FileSystems;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.List;
++
++import org.apache.lucene.luke.app.desktop.util.inifile.IniFile;
++import org.apache.lucene.luke.app.desktop.util.inifile.SimpleIniFile;
++import org.apache.lucene.store.FSDirectory;
++
++/** Default implementation of {@link Preferences} */
++public final class PreferencesImpl implements Preferences {
++
++  private static final String CONFIG_DIR = System.getProperty("user.home") + FileSystems.getDefault().getSeparator() + ".luke.d";
++  private static final String INIT_FILE = "luke.ini";
++  private static final String HISTORY_FILE = "history";
++  private static final int MAX_HISTORY = 10;
++
++  private final IniFile ini = new SimpleIniFile();
++
++
++  private final List<String> history = new ArrayList<>();
++
++  public PreferencesImpl() throws IOException {
++    // create config dir if not exists
++    Path confDir = FileSystems.getDefault().getPath(CONFIG_DIR);
++    if (!Files.exists(confDir)) {
++      Files.createDirectory(confDir);
++    }
++
++    // load configs
++    if (Files.exists(iniFile())) {
++      ini.load(iniFile());
++    } else {
++      ini.store(iniFile());
++    }
++
++    // load history
++    Path histFile = historyFile();
++    if (Files.exists(histFile)) {
++      List<String> allHistory = Files.readAllLines(histFile);
++      history.addAll(allHistory.subList(0, Math.min(MAX_HISTORY, allHistory.size())));
++    }
++
++  }
++
++  public List<String> getHistory() {
++    return history;
++  }
++
++  @Override
++  public void addHistory(String indexPath) throws IOException {
++    if (history.indexOf(indexPath) >= 0) {
++      history.remove(indexPath);
++    }
++    history.add(0, indexPath);
++    saveHistory();
++  }
++
++  private void saveHistory() throws IOException {
++    Files.write(historyFile(), history);
++  }
++
++  private Path historyFile() {
++    return FileSystems.getDefault().getPath(CONFIG_DIR, HISTORY_FILE);
++  }
++
++  @Override
++  public ColorTheme getColorTheme() {
++    String theme = ini.getString("settings", "theme");
++    return (theme == null) ? ColorTheme.GRAY : ColorTheme.valueOf(theme);
++  }
++
++  @Override
++  public void setColorTheme(ColorTheme theme) throws IOException {
++    ini.put("settings", "theme", theme.name());
++    ini.store(iniFile());
++  }
++
++  @Override
++  public boolean isReadOnly() {
++    Boolean readOnly = ini.getBoolean("opener", "readOnly");
++    return (readOnly == null) ? false : readOnly;
++  }
++
++  @Override
++  public String getDirImpl() {
++    String dirImpl = ini.getString("opener", "dirImpl");
++    return (dirImpl == null) ? FSDirectory.class.getName() : dirImpl;
++  }
++
++  @Override
++  public boolean isNoReader() {
++    Boolean noReader = ini.getBoolean("opener", "noReader");
++    return (noReader == null) ? false : noReader;
++  }
++
++  @Override
++  public boolean isUseCompound() {
++    Boolean useCompound = ini.getBoolean("opener", "useCompound");
++    return (useCompound == null) ? false : useCompound;
++  }
++
++  @Override
++  public boolean isKeepAllCommits() {
++    Boolean keepAllCommits = ini.getBoolean("opener", "keepAllCommits");
++    return (keepAllCommits == null) ? false : keepAllCommits;
++  }
++
++  @Override
++  public void setIndexOpenerPrefs(boolean readOnly, String dirImpl, boolean noReader, boolean useCompound, boolean keepAllCommits) throws IOException {
++    ini.put("opener", "readOnly", readOnly);
++    ini.put("opener", "dirImpl", dirImpl);
++    ini.put("opener", "noReader", noReader);
++    ini.put("opener", "useCompound", useCompound);
++    ini.put("opener", "keepAllCommits", keepAllCommits);
++    ini.store(iniFile());
++  }
++
++  private Path iniFile() {
++    return FileSystems.getDefault().getPath(CONFIG_DIR, INIT_FILE);
++  }
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/AnalysisPanelProvider.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/AnalysisPanelProvider.java
+new file mode 100644
+index 00000000000..70c2291bbca
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/AnalysisPanelProvider.java
+@@ -0,0 +1,441 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop.components;
++
++import javax.swing.BorderFactory;
++import javax.swing.ButtonGroup;
++import javax.swing.JButton;
++import javax.swing.JLabel;
++import javax.swing.JPanel;
++import javax.swing.JRadioButton;
++import javax.swing.JScrollPane;
++import javax.swing.JSplitPane;
++import javax.swing.JTable;
++import javax.swing.JTextArea;
++import javax.swing.ListSelectionModel;
++import java.awt.BorderLayout;
++import java.awt.Color;
++import java.awt.FlowLayout;
++import java.awt.GridLayout;
++import java.awt.Insets;
++import java.awt.event.ActionEvent;
++import java.awt.event.MouseAdapter;
++import java.awt.event.MouseEvent;
++import java.io.IOException;
++import java.util.List;
++import java.util.Objects;
++import java.util.concurrent.ExecutorService;
++import java.util.concurrent.Executors;
++import java.util.stream.Collectors;
++
++import org.apache.lucene.analysis.Analyzer;
++import org.apache.lucene.analysis.custom.CustomAnalyzer;
++import org.apache.lucene.analysis.standard.StandardAnalyzer;
++import org.apache.lucene.luke.app.desktop.MessageBroker;
++import org.apache.lucene.luke.app.desktop.components.dialog.analysis.AnalysisChainDialogFactory;
++import org.apache.lucene.luke.app.desktop.components.dialog.analysis.TokenAttributeDialogFactory;
++import org.apache.lucene.luke.app.desktop.components.dialog.documents.AddDocumentDialogOperator;
++import org.apache.lucene.luke.app.desktop.components.fragments.analysis.CustomAnalyzerPanelOperator;
++import org.apache.lucene.luke.app.desktop.components.fragments.analysis.CustomAnalyzerPanelProvider;
++import org.apache.lucene.luke.app.desktop.components.fragments.analysis.PresetAnalyzerPanelOperator;
++import org.apache.lucene.luke.app.desktop.components.fragments.analysis.PresetAnalyzerPanelProvider;
++import org.apache.lucene.luke.app.desktop.components.fragments.search.AnalyzerTabOperator;
++import org.apache.lucene.luke.app.desktop.components.fragments.search.MLTTabOperator;
++import org.apache.lucene.luke.app.desktop.util.DialogOpener;
++import org.apache.lucene.luke.app.desktop.util.FontUtils;
++import org.apache.lucene.luke.app.desktop.util.MessageUtils;
++import org.apache.lucene.luke.app.desktop.util.StyleConstants;
++import org.apache.lucene.luke.app.desktop.util.TableUtils;
++import org.apache.lucene.luke.models.analysis.Analysis;
++import org.apache.lucene.luke.models.analysis.AnalysisFactory;
++import org.apache.lucene.luke.models.analysis.CustomAnalyzerConfig;
++import org.apache.lucene.util.NamedThreadFactory;
++
++/** Provider of the Analysis panel */
++public final class AnalysisPanelProvider implements AnalysisTabOperator {
++
++  private static final String TYPE_PRESET = "preset";
++
++  private static final String TYPE_CUSTOM = "custom";
++
++  private final ComponentOperatorRegistry operatorRegistry;
++
++  private final AnalysisChainDialogFactory analysisChainDialogFactory;
++
++  private final TokenAttributeDialogFactory tokenAttrDialogFactory;
++
++  private final MessageBroker messageBroker;
++
++  private final JPanel mainPanel = new JPanel();
++
++  private final JPanel preset;
++
++  private final JPanel custom;
++
++  private final JRadioButton presetRB = new JRadioButton();
++
++  private final JRadioButton customRB = new JRadioButton();
++
++  private final JLabel analyzerNameLbl = new JLabel();
++
++  private final JLabel showChainLbl = new JLabel();
++
++  private final JTextArea inputArea = new JTextArea();
++
++  private final JTable tokensTable = new JTable();
++
++  private final ListenerFunctions listeners = new ListenerFunctions();
++
++  private List<Analysis.Token> tokens;
++
++  private Analysis analysisModel;
++
++  public AnalysisPanelProvider() throws IOException {
++    this.preset = new PresetAnalyzerPanelProvider().get();
++    this.custom = new CustomAnalyzerPanelProvider().get();
++
++    this.operatorRegistry = ComponentOperatorRegistry.getInstance();
++    this.analysisChainDialogFactory = AnalysisChainDialogFactory.getInstance();
++    this.tokenAttrDialogFactory = TokenAttributeDialogFactory.getInstance();
++    this.messageBroker = MessageBroker.getInstance();
++
++    this.analysisModel = new AnalysisFactory().newInstance();
++    analysisModel.createAnalyzerFromClassName(StandardAnalyzer.class.getName());
++
++    operatorRegistry.register(AnalysisTabOperator.class, this);
++
++    operatorRegistry.get(PresetAnalyzerPanelOperator.class).ifPresent(operator -> {
++      // Scanning all Analyzer types will take time...
++      ExecutorService executorService = Executors.newFixedThreadPool(1, new NamedThreadFactory("load-preset-analyzer-types"));
++      executorService.execute(() -> {
++        operator.setPresetAnalyzers(analysisModel.getPresetAnalyzerTypes());
++        operator.setSelectedAnalyzer(analysisModel.currentAnalyzer().getClass());
++      });
++      executorService.shutdown();
++    });
++  }
++
++  public JPanel get() {
++    JPanel panel = new JPanel(new GridLayout(1, 1));
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createLineBorder(Color.gray));
++
++    JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, initUpperPanel(), initLowerPanel());
++    splitPane.setOpaque(false);
++    splitPane.setDividerLocation(320);
++    panel.add(splitPane);
++
++    return panel;
++  }
++
++  private JPanel initUpperPanel() {
++    mainPanel.setOpaque(false);
++    mainPanel.setLayout(new BorderLayout());
++    mainPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
++
++    mainPanel.add(initSwitcherPanel(), BorderLayout.PAGE_START);
++    mainPanel.add(preset, BorderLayout.CENTER);
++
++    return mainPanel;
++  }
++
++  private JPanel initSwitcherPanel() {
++    JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    panel.setOpaque(false);
++
++    presetRB.setText(MessageUtils.getLocalizedMessage("analysis.radio.preset"));
++    presetRB.setActionCommand(TYPE_PRESET);
++    presetRB.addActionListener(listeners::toggleMainPanel);
++    presetRB.setOpaque(false);
++    presetRB.setSelected(true);
++
++    customRB.setText(MessageUtils.getLocalizedMessage("analysis.radio.custom"));
++    customRB.setActionCommand(TYPE_CUSTOM);
++    customRB.addActionListener(listeners::toggleMainPanel);
++    customRB.setOpaque(false);
++    customRB.setSelected(false);
++
++    ButtonGroup group = new ButtonGroup();
++    group.add(presetRB);
++    group.add(customRB);
++
++    panel.add(presetRB);
++    panel.add(customRB);
++
++    return panel;
++  }
++
++  private JPanel initLowerPanel() {
++    JPanel inner1 = new JPanel(new BorderLayout());
++    inner1.setOpaque(false);
++
++    JPanel analyzerName = new JPanel(new FlowLayout(FlowLayout.LEADING, 10, 2));
++    analyzerName.setOpaque(false);
++    analyzerName.add(new JLabel(MessageUtils.getLocalizedMessage("analysis.label.selected_analyzer")));
++    analyzerNameLbl.setText(analysisModel.currentAnalyzer().getClass().getName());
++    analyzerName.add(analyzerNameLbl);
++    showChainLbl.setText(MessageUtils.getLocalizedMessage("analysis.label.show_chain"));
++    showChainLbl.addMouseListener(new MouseAdapter() {
++      @Override
++      public void mouseClicked(MouseEvent e) {
++        listeners.showAnalysisChain(e);
++      }
++    });
++    showChainLbl.setVisible(analysisModel.currentAnalyzer() instanceof CustomAnalyzer);
++    analyzerName.add(FontUtils.toLinkText(showChainLbl));
++    inner1.add(analyzerName, BorderLayout.PAGE_START);
++
++    JPanel input = new JPanel(new FlowLayout(FlowLayout.LEADING, 5, 2));
++    input.setOpaque(false);
++    inputArea.setRows(3);
++    inputArea.setColumns(50);
++    inputArea.setLineWrap(true);
++    inputArea.setWrapStyleWord(true);
++    inputArea.setText(MessageUtils.getLocalizedMessage("analysis.textarea.prompt"));
++    input.add(new JScrollPane(inputArea));
++
++    JButton executeBtn = new JButton(FontUtils.elegantIconHtml("&#xe007;", MessageUtils.getLocalizedMessage("analysis.button.test")));
++    executeBtn.setFont(StyleConstants.FONT_BUTTON_LARGE);
++    executeBtn.setMargin(new Insets(3, 3, 3, 3));
++    executeBtn.addActionListener(listeners::executeAnalysis);
++    input.add(executeBtn);
++
++    JButton clearBtn = new JButton(MessageUtils.getLocalizedMessage("button.clear"));
++    clearBtn.setFont(StyleConstants.FONT_BUTTON_LARGE);
++    clearBtn.setMargin(new Insets(5, 5, 5, 5));
++    clearBtn.addActionListener(e -> {
++      inputArea.setText("");
++      TableUtils.setupTable(tokensTable, ListSelectionModel.SINGLE_SELECTION, new TokensTableModel(),
++          null,
++          TokensTableModel.Column.TERM.getColumnWidth(),
++          TokensTableModel.Column.ATTR.getColumnWidth());
++    });
++    input.add(clearBtn);
++
++    inner1.add(input, BorderLayout.CENTER);
++
++    JPanel inner2 = new JPanel(new BorderLayout());
++    inner2.setOpaque(false);
++
++    JPanel hint = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    hint.setOpaque(false);
++    hint.add(new JLabel(MessageUtils.getLocalizedMessage("analysis.hint.show_attributes")));
++    inner2.add(hint, BorderLayout.PAGE_START);
++
++
++    TableUtils.setupTable(tokensTable, ListSelectionModel.SINGLE_SELECTION, new TokensTableModel(),
++        new MouseAdapter() {
++          @Override
++          public void mouseClicked(MouseEvent e) {
++            listeners.showAttributeValues(e);
++          }
++        },
++        TokensTableModel.Column.TERM.getColumnWidth(),
++        TokensTableModel.Column.ATTR.getColumnWidth());
++    inner2.add(new JScrollPane(tokensTable), BorderLayout.CENTER);
++
++    JPanel panel = new JPanel(new BorderLayout());
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
++    panel.add(inner1, BorderLayout.PAGE_START);
++    panel.add(inner2, BorderLayout.CENTER);
++
++    return panel;
++  }
++
++  // control methods
++
++  void toggleMainPanel(String command) {
++    if (command.equalsIgnoreCase(TYPE_PRESET)) {
++      mainPanel.remove(custom);
++      mainPanel.add(preset, BorderLayout.CENTER);
++
++      operatorRegistry.get(PresetAnalyzerPanelOperator.class).ifPresent(operator -> {
++        operator.setPresetAnalyzers(analysisModel.getPresetAnalyzerTypes());
++        operator.setSelectedAnalyzer(analysisModel.currentAnalyzer().getClass());
++      });
++
++    } else if (command.equalsIgnoreCase(TYPE_CUSTOM)) {
++      mainPanel.remove(preset);
++      mainPanel.add(custom, BorderLayout.CENTER);
++
++      operatorRegistry.get(CustomAnalyzerPanelOperator.class).ifPresent(operator -> {
++        operator.setAnalysisModel(analysisModel);
++        operator.resetAnalysisComponents();
++      });
++    }
++    mainPanel.setVisible(false);
++    mainPanel.setVisible(true);
++  }
++
++  void executeAnalysis() {
++    String text = inputArea.getText();
++    if (Objects.isNull(text) || text.isEmpty()) {
++      messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("analysis.message.empry_input"));
++    }
++
++    tokens = analysisModel.analyze(text);
++    tokensTable.setModel(new TokensTableModel(tokens));
++    tokensTable.setShowGrid(true);
++    tokensTable.getColumnModel().getColumn(TokensTableModel.Column.TERM.getIndex()).setPreferredWidth(TokensTableModel.Column.TERM.getColumnWidth());
++    tokensTable.getColumnModel().getColumn(TokensTableModel.Column.ATTR.getIndex()).setPreferredWidth(TokensTableModel.Column.ATTR.getColumnWidth());
++  }
++
++  void showAnalysisChainDialog() {
++    if (getCurrentAnalyzer() instanceof CustomAnalyzer) {
++      CustomAnalyzer analyzer = (CustomAnalyzer) getCurrentAnalyzer();
++      new DialogOpener<>(analysisChainDialogFactory).open("Analysis chain", 600, 320,
++          (factory) -> {
++            factory.setAnalyzer(analyzer);
++          });
++    }
++  }
++
++  void showAttributeValues(int selectedIndex) {
++    String term = tokens.get(selectedIndex).getTerm();
++    List<Analysis.TokenAttribute> attributes = tokens.get(selectedIndex).getAttributes();
++    new DialogOpener<>(tokenAttrDialogFactory).open("Token Attributes", 650, 400,
++        factory -> {
++          factory.setTerm(term);
++          factory.setAttributes(attributes);
++        });
++  }
++
++
++  @Override
++  public void setAnalyzerByType(String analyzerType) {
++    analysisModel.createAnalyzerFromClassName(analyzerType);
++    analyzerNameLbl.setText(analysisModel.currentAnalyzer().getClass().getName());
++    showChainLbl.setVisible(false);
++    operatorRegistry.get(AnalyzerTabOperator.class).ifPresent(operator ->
++        operator.setAnalyzer(analysisModel.currentAnalyzer()));
++    operatorRegistry.get(MLTTabOperator.class).ifPresent(operator ->
++        operator.setAnalyzer(analysisModel.currentAnalyzer()));
++    operatorRegistry.get(AddDocumentDialogOperator.class).ifPresent(operator ->
++        operator.setAnalyzer(analysisModel.currentAnalyzer()));
++  }
++
++  @Override
++  public void setAnalyzerByCustomConfiguration(CustomAnalyzerConfig config) {
++    analysisModel.buildCustomAnalyzer(config);
++    analyzerNameLbl.setText(analysisModel.currentAnalyzer().getClass().getName());
++    showChainLbl.setVisible(true);
++    operatorRegistry.get(AnalyzerTabOperator.class).ifPresent(operator ->
++        operator.setAnalyzer(analysisModel.currentAnalyzer()));
++    operatorRegistry.get(MLTTabOperator.class).ifPresent(operator ->
++        operator.setAnalyzer(analysisModel.currentAnalyzer()));
++    operatorRegistry.get(AddDocumentDialogOperator.class).ifPresent(operator ->
++        operator.setAnalyzer(analysisModel.currentAnalyzer()));
++  }
++
++  @Override
++  public Analyzer getCurrentAnalyzer() {
++    return analysisModel.currentAnalyzer();
++  }
++
++  private class ListenerFunctions {
++
++    void toggleMainPanel(ActionEvent e) {
++      AnalysisPanelProvider.this.toggleMainPanel(e.getActionCommand());
++    }
++
++    void showAnalysisChain(MouseEvent e) {
++      AnalysisPanelProvider.this.showAnalysisChainDialog();
++    }
++
++    void executeAnalysis(ActionEvent e) {
++      AnalysisPanelProvider.this.executeAnalysis();
++    }
++
++    void showAttributeValues(MouseEvent e) {
++      if (e.getClickCount() != 2 || e.isConsumed()) {
++        return;
++      }
++      int selectedIndex = tokensTable.rowAtPoint(e.getPoint());
++      if (selectedIndex < 0 || selectedIndex >= tokensTable.getRowCount()) {
++        return;
++      }
++      AnalysisPanelProvider.this.showAttributeValues(selectedIndex);
++    }
++
++  }
++
++  static final class TokensTableModel extends TableModelBase<TokensTableModel.Column> {
++
++    enum Column implements TableColumnInfo {
++      TERM("Term", 0, String.class, 150),
++      ATTR("Attributes", 1, String.class, 1000);
++
++      private final String colName;
++      private final int index;
++      private final Class<?> type;
++      private final int width;
++
++      Column(String colName, int index, Class<?> type, int width) {
++        this.colName = colName;
++        this.index = index;
++        this.type = type;
++        this.width = width;
++      }
++
++      @Override
++      public String getColName() {
++        return colName;
++      }
++
++      @Override
++      public int getIndex() {
++        return index;
++      }
++
++      @Override
++      public Class<?> getType() {
++        return type;
++      }
++
++      @Override
++      public int getColumnWidth() {
++        return width;
++      }
++    }
++
++    TokensTableModel() {
++      super();
++    }
++
++    TokensTableModel(List<Analysis.Token> tokens) {
++      super(tokens.size());
++      for (int i = 0; i < tokens.size(); i++) {
++        Analysis.Token token = tokens.get(i);
++        data[i][Column.TERM.getIndex()] = token.getTerm();
++        List<String> attValues = token.getAttributes().stream()
++            .flatMap(att -> att.getAttValues().entrySet().stream()
++                .map(e -> e.getKey() + "=" + e.getValue()))
++            .collect(Collectors.toList());
++        data[i][Column.ATTR.getIndex()] = String.join(",", attValues);
++      }
++    }
++
++    @Override
++    protected Column[] columnInfos() {
++      return Column.values();
++    }
++  }
++
++}
++
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/AnalysisTabOperator.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/AnalysisTabOperator.java
+new file mode 100644
+index 00000000000..555f1c0245c
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/AnalysisTabOperator.java
+@@ -0,0 +1,33 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop.components;
++
++import org.apache.lucene.analysis.Analyzer;
++import org.apache.lucene.luke.models.analysis.CustomAnalyzerConfig;
++
++/** Operator for the Analysis tab */
++public interface AnalysisTabOperator extends ComponentOperatorRegistry.ComponentOperator {
++
++  void setAnalyzerByType(String analyzerType);
++
++  void setAnalyzerByCustomConfiguration(CustomAnalyzerConfig config);
++
++  Analyzer getCurrentAnalyzer();
++
++}
++
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/CommitsPanelProvider.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/CommitsPanelProvider.java
+new file mode 100644
+index 00000000000..d06abcc0789
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/CommitsPanelProvider.java
+@@ -0,0 +1,575 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop.components;
++
++import javax.swing.BorderFactory;
++import javax.swing.BoxLayout;
++import javax.swing.ButtonGroup;
++import javax.swing.DefaultComboBoxModel;
++import javax.swing.DefaultListModel;
++import javax.swing.JComboBox;
++import javax.swing.JLabel;
++import javax.swing.JList;
++import javax.swing.JPanel;
++import javax.swing.JRadioButton;
++import javax.swing.JScrollPane;
++import javax.swing.JSplitPane;
++import javax.swing.JTable;
++import javax.swing.JTextArea;
++import javax.swing.ListSelectionModel;
++import java.awt.BorderLayout;
++import java.awt.Color;
++import java.awt.FlowLayout;
++import java.awt.GridBagConstraints;
++import java.awt.GridBagLayout;
++import java.awt.GridLayout;
++import java.awt.event.ActionEvent;
++import java.awt.event.MouseAdapter;
++import java.awt.event.MouseEvent;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Map;
++
++import org.apache.lucene.index.DirectoryReader;
++import org.apache.lucene.luke.app.DirectoryHandler;
++import org.apache.lucene.luke.app.DirectoryObserver;
++import org.apache.lucene.luke.app.IndexHandler;
++import org.apache.lucene.luke.app.IndexObserver;
++import org.apache.lucene.luke.app.LukeState;
++import org.apache.lucene.luke.app.desktop.util.MessageUtils;
++import org.apache.lucene.luke.app.desktop.util.TableUtils;
++import org.apache.lucene.luke.models.commits.Commit;
++import org.apache.lucene.luke.models.commits.Commits;
++import org.apache.lucene.luke.models.commits.CommitsFactory;
++import org.apache.lucene.luke.models.commits.File;
++import org.apache.lucene.luke.models.commits.Segment;
++
++/** Provider of the Commits panel */
++public final class CommitsPanelProvider {
++
++  private final CommitsFactory commitsFactory = new CommitsFactory();
++
++  private final JComboBox<Long> commitGenCombo = new JComboBox<>();
++
++  private final JLabel deletedLbl = new JLabel();
++
++  private final JLabel segCntLbl = new JLabel();
++
++  private final JTextArea userDataTA = new JTextArea();
++
++  private final JTable filesTable = new JTable();
++
++  private final JTable segmentsTable = new JTable();
++
++  private final JRadioButton diagRB = new JRadioButton();
++
++  private final JRadioButton attrRB = new JRadioButton();
++
++  private final JRadioButton codecRB = new JRadioButton();
++
++  private final ButtonGroup rbGroup = new ButtonGroup();
++
++  private final JList<String> segDetailList = new JList<>();
++
++  private ListenerFunctions listeners = new ListenerFunctions();
++
++  private Commits commitsModel;
++
++  public CommitsPanelProvider() {
++    IndexHandler.getInstance().addObserver(new Observer());
++    DirectoryHandler.getInstance().addObserver(new Observer());
++  }
++
++  public JPanel get() {
++    JPanel panel = new JPanel(new GridLayout(1, 1));
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createLineBorder(Color.gray));
++
++    JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, initUpperPanel(), initLowerPanel());
++    splitPane.setOpaque(false);
++    splitPane.setBorder(BorderFactory.createEmptyBorder());
++    splitPane.setDividerLocation(120);
++    panel.add(splitPane);
++
++    return panel;
++  }
++
++  private JPanel initUpperPanel() {
++    JPanel panel = new JPanel(new BorderLayout(20, 0));
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
++
++    JPanel left = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    left.setOpaque(false);
++    left.add(new JLabel(MessageUtils.getLocalizedMessage("commits.label.select_gen")));
++    commitGenCombo.addActionListener(listeners::selectGeneration);
++    left.add(commitGenCombo);
++    panel.add(left, BorderLayout.LINE_START);
++
++    JPanel right = new JPanel(new GridBagLayout());
++    right.setOpaque(false);
++    GridBagConstraints c1 = new GridBagConstraints();
++    c1.ipadx = 5;
++    c1.ipady = 5;
++
++    c1.gridx = 0;
++    c1.gridy = 0;
++    c1.weightx = 0.2;
++    c1.anchor = GridBagConstraints.EAST;
++    right.add(new JLabel(MessageUtils.getLocalizedMessage("commits.label.deleted")), c1);
++
++    c1.gridx = 1;
++    c1.gridy = 0;
++    c1.weightx = 0.5;
++    c1.anchor = GridBagConstraints.WEST;
++    right.add(deletedLbl, c1);
++
++    c1.gridx = 0;
++    c1.gridy = 1;
++    c1.weightx = 0.2;
++    c1.anchor = GridBagConstraints.EAST;
++    right.add(new JLabel(MessageUtils.getLocalizedMessage("commits.label.segcount")), c1);
++
++    c1.gridx = 1;
++    c1.gridy = 1;
++    c1.weightx = 0.5;
++    c1.anchor = GridBagConstraints.WEST;
++    right.add(segCntLbl, c1);
++
++    c1.gridx = 0;
++    c1.gridy = 2;
++    c1.weightx = 0.2;
++    c1.anchor = GridBagConstraints.EAST;
++    right.add(new JLabel(MessageUtils.getLocalizedMessage("commits.label.userdata")), c1);
++
++    userDataTA.setRows(3);
++    userDataTA.setColumns(30);
++    userDataTA.setLineWrap(true);
++    userDataTA.setWrapStyleWord(true);
++    userDataTA.setEditable(false);
++    JScrollPane userDataScroll = new JScrollPane(userDataTA, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
++    c1.gridx = 1;
++    c1.gridy = 2;
++    c1.weightx = 0.5;
++    c1.anchor = GridBagConstraints.WEST;
++    right.add(userDataScroll, c1);
++
++    panel.add(right, BorderLayout.CENTER);
++
++    return panel;
++  }
++
++  private JPanel initLowerPanel() {
++    JPanel panel = new JPanel(new GridLayout(1, 1));
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
++
++    JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, initFilesPanel(), initSegmentsPanel());
++    splitPane.setOpaque(false);
++    splitPane.setBorder(BorderFactory.createEmptyBorder());
++    splitPane.setDividerLocation(300);
++    panel.add(splitPane);
++    return panel;
++  }
++
++  private JPanel initFilesPanel() {
++    JPanel panel = new JPanel(new BorderLayout());
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
++
++    JPanel header = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    header.setOpaque(false);
++    header.add(new JLabel(MessageUtils.getLocalizedMessage("commits.label.files")));
++    panel.add(header, BorderLayout.PAGE_START);
++
++    TableUtils.setupTable(filesTable, ListSelectionModel.SINGLE_SELECTION, new FilesTableModel(), null, FilesTableModel.Column.FILENAME.getColumnWidth());
++    panel.add(new JScrollPane(filesTable), BorderLayout.CENTER);
++
++    return panel;
++  }
++
++  private JPanel initSegmentsPanel() {
++    JPanel panel = new JPanel();
++    panel.setOpaque(false);
++    panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
++
++    JPanel segments = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    segments.setOpaque(false);
++    segments.add(new JLabel(MessageUtils.getLocalizedMessage("commits.label.segments")));
++    panel.add(segments);
++
++    TableUtils.setupTable(segmentsTable, ListSelectionModel.SINGLE_SELECTION, new SegmentsTableModel(),
++        new MouseAdapter() {
++          @Override
++          public void mouseClicked(MouseEvent e) {
++            listeners.showSegmentDetails(e);
++          }
++        },
++        SegmentsTableModel.Column.NAME.getColumnWidth(),
++        SegmentsTableModel.Column.MAXDOCS.getColumnWidth(),
++        SegmentsTableModel.Column.DELS.getColumnWidth(),
++        SegmentsTableModel.Column.DELGEN.getColumnWidth(),
++        SegmentsTableModel.Column.VERSION.getColumnWidth(),
++        SegmentsTableModel.Column.CODEC.getColumnWidth());
++    panel.add(new JScrollPane(segmentsTable));
++
++    JPanel segDetails = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    segDetails.setOpaque(false);
++    segDetails.add(new JLabel(MessageUtils.getLocalizedMessage("commits.label.segdetails")));
++    panel.add(segDetails);
++
++    JPanel buttons = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    buttons.setOpaque(false);
++
++    diagRB.setText("Diagnostics");
++    diagRB.setActionCommand(ActionCommand.DIAGNOSTICS.name());
++    diagRB.setSelected(true);
++    diagRB.setEnabled(false);
++    diagRB.setOpaque(false);
++    diagRB.addMouseListener(new MouseAdapter() {
++      @Override
++      public void mouseClicked(MouseEvent e) {
++        listeners.showSegmentDetails(e);
++      }
++    });
++    buttons.add(diagRB);
++
++    attrRB.setText("Attributes");
++    attrRB.setActionCommand(ActionCommand.ATTRIBUTES.name());
++    attrRB.setSelected(false);
++    attrRB.setEnabled(false);
++    attrRB.setOpaque(false);
++    attrRB.addMouseListener(new MouseAdapter() {
++      @Override
++      public void mouseClicked(MouseEvent e) {
++        listeners.showSegmentDetails(e);
++      }
++    });
++    buttons.add(attrRB);
++
++    codecRB.setText("Codec");
++    codecRB.setActionCommand(ActionCommand.CODEC.name());
++    codecRB.setSelected(false);
++    codecRB.setEnabled(false);
++    codecRB.setOpaque(false);
++    codecRB.addMouseListener(new MouseAdapter() {
++      @Override
++      public void mouseClicked(MouseEvent e) {
++        listeners.showSegmentDetails(e);
++      }
++    });
++    buttons.add(codecRB);
++
++    rbGroup.add(diagRB);
++    rbGroup.add(attrRB);
++    rbGroup.add(codecRB);
++
++    panel.add(buttons);
++
++    segDetailList.setVisibleRowCount(10);
++    panel.add(new JScrollPane(segDetailList));
++
++    return panel;
++  }
++
++  // control methods
++
++  private void selectGeneration() {
++    diagRB.setEnabled(false);
++    attrRB.setEnabled(false);
++    codecRB.setEnabled(false);
++    segDetailList.setModel(new DefaultListModel<>());
++
++    long commitGen = (long) commitGenCombo.getSelectedItem();
++    commitsModel.getCommit(commitGen).ifPresent(commit -> {
++      deletedLbl.setText(String.valueOf(commit.isDeleted()));
++      segCntLbl.setText(String.valueOf(commit.getSegCount()));
++      userDataTA.setText(commit.getUserData());
++    });
++
++    filesTable.setModel(new FilesTableModel(commitsModel.getFiles(commitGen)));
++    filesTable.setShowGrid(true);
++    filesTable.getColumnModel().getColumn(FilesTableModel.Column.FILENAME.getIndex()).setPreferredWidth(FilesTableModel.Column.FILENAME.getColumnWidth());
++
++    segmentsTable.setModel(new SegmentsTableModel(commitsModel.getSegments(commitGen)));
++    segmentsTable.setShowGrid(true);
++    segmentsTable.getColumnModel().getColumn(SegmentsTableModel.Column.NAME.getIndex()).setPreferredWidth(SegmentsTableModel.Column.NAME.getColumnWidth());
++    segmentsTable.getColumnModel().getColumn(SegmentsTableModel.Column.MAXDOCS.getIndex()).setPreferredWidth(SegmentsTableModel.Column.MAXDOCS.getColumnWidth());
++    segmentsTable.getColumnModel().getColumn(SegmentsTableModel.Column.DELS.getIndex()).setPreferredWidth(SegmentsTableModel.Column.DELS.getColumnWidth());
++    segmentsTable.getColumnModel().getColumn(SegmentsTableModel.Column.DELGEN.getIndex()).setPreferredWidth(SegmentsTableModel.Column.DELGEN.getColumnWidth());
++    segmentsTable.getColumnModel().getColumn(SegmentsTableModel.Column.VERSION.getIndex()).setPreferredWidth(SegmentsTableModel.Column.VERSION.getColumnWidth());
++    segmentsTable.getColumnModel().getColumn(SegmentsTableModel.Column.CODEC.getIndex()).setPreferredWidth(SegmentsTableModel.Column.CODEC.getColumnWidth());
++  }
++
++  private void showSegmentDetails() {
++    int selectedRow = segmentsTable.getSelectedRow();
++    if (commitGenCombo.getSelectedItem() == null ||
++        selectedRow < 0 || selectedRow >= segmentsTable.getRowCount()) {
++      return;
++    }
++
++    diagRB.setEnabled(true);
++    attrRB.setEnabled(true);
++    codecRB.setEnabled(true);
++
++    long commitGen = (long) commitGenCombo.getSelectedItem();
++    String segName = (String) segmentsTable.getValueAt(selectedRow, SegmentsTableModel.Column.NAME.getIndex());
++    ActionCommand command = ActionCommand.valueOf(rbGroup.getSelection().getActionCommand());
++
++    final DefaultListModel<String> detailsModel = new DefaultListModel<>();
++    switch (command) {
++      case DIAGNOSTICS:
++        commitsModel.getSegmentDiagnostics(commitGen, segName).entrySet().stream()
++            .map(entry -> entry.getKey() + " = " + entry.getValue())
++            .forEach(detailsModel::addElement);
++        break;
++      case ATTRIBUTES:
++        commitsModel.getSegmentAttributes(commitGen, segName).entrySet().stream()
++            .map(entry -> entry.getKey() + " = " + entry.getValue())
++            .forEach(detailsModel::addElement);
++        break;
++      case CODEC:
++        commitsModel.getSegmentCodec(commitGen, segName).ifPresent(codec -> {
++          Map<String, String> map = new HashMap<>();
++          map.put("Codec name", codec.getName());
++          map.put("Codec class name", codec.getClass().getName());
++          map.put("Compound format", codec.compoundFormat().getClass().getName());
++          map.put("DocValues format", codec.docValuesFormat().getClass().getName());
++          map.put("FieldInfos format", codec.fieldInfosFormat().getClass().getName());
++          map.put("LiveDocs format", codec.liveDocsFormat().getClass().getName());
++          map.put("Norms format", codec.normsFormat().getClass().getName());
++          map.put("Points format", codec.pointsFormat().getClass().getName());
++          map.put("Postings format", codec.postingsFormat().getClass().getName());
++          map.put("SegmentInfo format", codec.segmentInfoFormat().getClass().getName());
++          map.put("StoredFields format", codec.storedFieldsFormat().getClass().getName());
++          map.put("TermVectors format", codec.termVectorsFormat().getClass().getName());
++          map.entrySet().stream()
++              .map(entry -> entry.getKey() + " = " + entry.getValue()).forEach(detailsModel::addElement);
++        });
++        break;
++    }
++    segDetailList.setModel(detailsModel);
++
++  }
++
++  private class ListenerFunctions {
++
++    void selectGeneration(ActionEvent e) {
++      CommitsPanelProvider.this.selectGeneration();
++    }
++
++    void showSegmentDetails(MouseEvent e) {
++      CommitsPanelProvider.this.showSegmentDetails();
++    }
++
++  }
++
++  private class Observer implements IndexObserver, DirectoryObserver {
++
++    @Override
++    public void openDirectory(LukeState state) {
++      commitsModel = commitsFactory.newInstance(state.getDirectory(), state.getIndexPath());
++      populateCommitGenerations();
++    }
++
++    @Override
++    public void closeDirectory() {
++      close();
++    }
++
++    @Override
++    public void openIndex(LukeState state) {
++      if (state.hasDirectoryReader()) {
++        DirectoryReader dr = (DirectoryReader) state.getIndexReader();
++        commitsModel = commitsFactory.newInstance(dr, state.getIndexPath());
++        populateCommitGenerations();
++      }
++    }
++
++    @Override
++    public void closeIndex() {
++      close();
++    }
++
++    private void populateCommitGenerations() {
++      DefaultComboBoxModel<Long> segGenList = new DefaultComboBoxModel<>();
++      for (Commit commit : commitsModel.listCommits()) {
++        segGenList.addElement(commit.getGeneration());
++      }
++      commitGenCombo.setModel(segGenList);
++
++      if (segGenList.getSize() > 0) {
++        commitGenCombo.setSelectedIndex(0);
++      }
++    }
++
++    private void close() {
++      commitsModel = null;
++
++      commitGenCombo.setModel(new DefaultComboBoxModel<>());
++      deletedLbl.setText("");
++      segCntLbl.setText("");
++      userDataTA.setText("");
++      TableUtils.setupTable(filesTable, ListSelectionModel.SINGLE_SELECTION, new FilesTableModel(), null, FilesTableModel.Column.FILENAME.getColumnWidth());
++      TableUtils.setupTable(segmentsTable, ListSelectionModel.SINGLE_SELECTION, new SegmentsTableModel(), null,
++          SegmentsTableModel.Column.NAME.getColumnWidth(),
++          SegmentsTableModel.Column.MAXDOCS.getColumnWidth(),
++          SegmentsTableModel.Column.DELS.getColumnWidth(),
++          SegmentsTableModel.Column.DELGEN.getColumnWidth(),
++          SegmentsTableModel.Column.VERSION.getColumnWidth(),
++          SegmentsTableModel.Column.CODEC.getColumnWidth());
++      diagRB.setEnabled(false);
++      attrRB.setEnabled(false);
++      codecRB.setEnabled(false);
++      segDetailList.setModel(new DefaultListModel<>());
++    }
++  }
++
++  enum ActionCommand {
++    DIAGNOSTICS, ATTRIBUTES, CODEC;
++  }
++
++  static final class FilesTableModel extends TableModelBase<FilesTableModel.Column> {
++
++    enum Column implements TableColumnInfo {
++
++      FILENAME("Filename", 0, String.class, 200),
++      SIZE("Size", 1, String.class, Integer.MAX_VALUE);
++
++      private final String colName;
++      private final int index;
++      private final Class<?> type;
++      private final int width;
++
++      Column(String colName, int index, Class<?> type, int width) {
++        this.colName = colName;
++        this.index = index;
++        this.type = type;
++        this.width = width;
++      }
++
++      @Override
++      public String getColName() {
++        return colName;
++      }
++
++      @Override
++      public int getIndex() {
++        return index;
++      }
++
++      @Override
++      public Class<?> getType() {
++        return type;
++      }
++
++      @Override
++      public int getColumnWidth() {
++        return width;
++      }
++    }
++
++    FilesTableModel() {
++      super();
++    }
++
++    FilesTableModel(List<File> files) {
++      super(files.size());
++      for (int i = 0; i < files.size(); i++) {
++        File file = files.get(i);
++        data[i][Column.FILENAME.getIndex()] = file.getFileName();
++        data[i][Column.SIZE.getIndex()] = file.getDisplaySize();
++      }
++    }
++
++    @Override
++    protected Column[] columnInfos() {
++      return Column.values();
++    }
++  }
++
++  static final class SegmentsTableModel extends TableModelBase<SegmentsTableModel.Column> {
++
++    enum Column implements TableColumnInfo {
++
++      NAME("Name", 0, String.class, 60),
++      MAXDOCS("Max docs", 1, Integer.class, 60),
++      DELS("Dels", 2, Integer.class, 60),
++      DELGEN("Del gen", 3, Long.class, 60),
++      VERSION("Lucene ver.", 4, String.class, 60),
++      CODEC("Codec", 5, String.class, 100),
++      SIZE("Size", 6, String.class, 150);
++
++      private final String colName;
++      private final int index;
++      private final Class<?> type;
++      private final int width;
++
++      Column(String colName, int index, Class<?> type, int width) {
++        this.colName = colName;
++        this.index = index;
++        this.type = type;
++        this.width = width;
++      }
++
++      @Override
++      public String getColName() {
++        return colName;
++      }
++
++      @Override
++      public int getIndex() {
++        return index;
++      }
++
++      @Override
++      public Class<?> getType() {
++        return type;
++      }
++
++      @Override
++      public int getColumnWidth() {
++        return width;
++      }
++    }
++
++    SegmentsTableModel() {
++      super();
++    }
++
++    SegmentsTableModel(List<Segment> segments) {
++      super(segments.size());
++      for (int i = 0; i < segments.size(); i++) {
++        Segment segment = segments.get(i);
++        data[i][Column.NAME.getIndex()] = segment.getName();
++        data[i][Column.MAXDOCS.getIndex()] = segment.getMaxDoc();
++        data[i][Column.DELS.getIndex()] = segment.getDelCount();
++        data[i][Column.DELGEN.getIndex()] = segment.getDelGen();
++        data[i][Column.VERSION.getIndex()] = segment.getLuceneVer();
++        data[i][Column.CODEC.getIndex()] = segment.getCodecName();
++        data[i][Column.SIZE.getIndex()] = segment.getDisplaySize();
++      }
++    }
++
++    @Override
++    protected Column[] columnInfos() {
++      return Column.values();
++    }
++  }
++}
++
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/ComponentOperatorRegistry.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/ComponentOperatorRegistry.java
+new file mode 100644
+index 00000000000..0d9c99b0ec7
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/ComponentOperatorRegistry.java
+@@ -0,0 +1,50 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop.components;
++
++import java.util.HashMap;
++import java.util.Map;
++import java.util.Optional;
++
++/** An utility class for interaction between components */
++public class ComponentOperatorRegistry {
++
++  private static final ComponentOperatorRegistry instance = new ComponentOperatorRegistry();
++
++  private final Map<Class<?>, Object> operators = new HashMap<>();
++
++  public static ComponentOperatorRegistry getInstance() {
++    return instance;
++  }
++
++  public <T extends ComponentOperator> void register(Class<T> type, T operator) {
++    if (!operators.containsKey(type)) {
++      operators.put(type, operator);
++    }
++  }
++
++  @SuppressWarnings("unchecked")
++  public <T extends ComponentOperator> Optional<T> get(Class<T> type) {
++    return Optional.ofNullable((T) operators.get(type));
++  }
++
++  /** marker interface for operators */
++  public interface ComponentOperator {
++  }
++
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/DocumentsPanelProvider.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/DocumentsPanelProvider.java
+new file mode 100644
+index 00000000000..e9daece4db4
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/DocumentsPanelProvider.java
+@@ -0,0 +1,1115 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop.components;
++
++import javax.swing.BorderFactory;
++import javax.swing.BoxLayout;
++import javax.swing.JButton;
++import javax.swing.JComboBox;
++import javax.swing.JComponent;
++import javax.swing.JLabel;
++import javax.swing.JList;
++import javax.swing.JMenuItem;
++import javax.swing.JPanel;
++import javax.swing.JPopupMenu;
++import javax.swing.JScrollPane;
++import javax.swing.JSpinner;
++import javax.swing.JSplitPane;
++import javax.swing.JTable;
++import javax.swing.JTextField;
++import javax.swing.ListSelectionModel;
++import javax.swing.SpinnerModel;
++import javax.swing.SpinnerNumberModel;
++import javax.swing.event.ChangeEvent;
++import javax.swing.table.TableCellRenderer;
++import java.awt.BorderLayout;
++import java.awt.Color;
++import java.awt.Dimension;
++import java.awt.FlowLayout;
++import java.awt.GridBagConstraints;
++import java.awt.GridBagLayout;
++import java.awt.GridLayout;
++import java.awt.Insets;
++import java.awt.Toolkit;
++import java.awt.datatransfer.Clipboard;
++import java.awt.datatransfer.StringSelection;
++import java.awt.event.ActionEvent;
++import java.awt.event.MouseAdapter;
++import java.awt.event.MouseEvent;
++import java.io.IOException;
++import java.math.BigDecimal;
++import java.math.BigInteger;
++import java.util.List;
++import java.util.Objects;
++import java.util.Optional;
++
++import org.apache.lucene.index.DocValuesType;
++import org.apache.lucene.index.IndexOptions;
++import org.apache.lucene.index.Term;
++import org.apache.lucene.luke.app.IndexHandler;
++import org.apache.lucene.luke.app.IndexObserver;
++import org.apache.lucene.luke.app.LukeState;
++import org.apache.lucene.luke.app.desktop.MessageBroker;
++import org.apache.lucene.luke.app.desktop.components.dialog.HelpDialogFactory;
++import org.apache.lucene.luke.app.desktop.components.dialog.documents.AddDocumentDialogFactory;
++import org.apache.lucene.luke.app.desktop.components.dialog.documents.DocValuesDialogFactory;
++import org.apache.lucene.luke.app.desktop.components.dialog.documents.StoredValueDialogFactory;
++import org.apache.lucene.luke.app.desktop.components.dialog.documents.TermVectorDialogFactory;
++import org.apache.lucene.luke.app.desktop.util.DialogOpener;
++import org.apache.lucene.luke.app.desktop.util.FontUtils;
++import org.apache.lucene.luke.app.desktop.util.HelpHeaderRenderer;
++import org.apache.lucene.luke.app.desktop.util.MessageUtils;
++import org.apache.lucene.luke.app.desktop.util.StyleConstants;
++import org.apache.lucene.luke.app.desktop.util.TableUtils;
++import org.apache.lucene.luke.models.documents.DocValues;
++import org.apache.lucene.luke.models.documents.DocumentField;
++import org.apache.lucene.luke.models.documents.Documents;
++import org.apache.lucene.luke.models.documents.DocumentsFactory;
++import org.apache.lucene.luke.models.documents.TermPosting;
++import org.apache.lucene.luke.models.documents.TermVectorEntry;
++import org.apache.lucene.luke.util.BytesRefUtils;
++
++/** Provider of the Documents panel */
++public final class DocumentsPanelProvider implements DocumentsTabOperator {
++
++  private final DocumentsFactory documentsFactory = new DocumentsFactory();
++
++  private final MessageBroker messageBroker;
++
++  private final ComponentOperatorRegistry operatorRegistry;
++
++  private final TabSwitcherProxy tabSwitcher;
++
++  private final AddDocumentDialogFactory addDocDialogFactory;
++
++  private final TermVectorDialogFactory tvDialogFactory;
++
++  private final DocValuesDialogFactory dvDialogFactory;
++
++  private final StoredValueDialogFactory valueDialogFactory;
++
++  private final TableCellRenderer tableHeaderRenderer;
++
++  private final JComboBox<String> fieldsCombo = new JComboBox<>();
++
++  private final JButton firstTermBtn = new JButton();
++
++  private final JTextField termTF = new JTextField();
++
++  private final JButton nextTermBtn = new JButton();
++
++  private final JTextField selectedTermTF = new JTextField();
++
++  private final JButton firstTermDocBtn = new JButton();
++
++  private final JTextField termDocIdxTF = new JTextField();
++
++  private final JButton nextTermDocBtn = new JButton();
++
++  private final JLabel termDocsNumLbl = new JLabel();
++
++  private final JTable posTable = new JTable();
++
++  private final JSpinner docNumSpnr = new JSpinner();
++
++  private final JLabel maxDocsLbl = new JLabel();
++
++  private final JButton mltBtn = new JButton();
++
++  private final JButton addDocBtn = new JButton();
++
++  private final JButton copyDocValuesBtn = new JButton();
++
++  private final JTable documentTable = new JTable();
++
++  private final JPopupMenu documentContextMenu = new JPopupMenu();
++
++  private final ListenerFunctions listeners = new ListenerFunctions();
++
++  private Documents documentsModel;
++
++  public DocumentsPanelProvider() throws IOException {
++    this.messageBroker = MessageBroker.getInstance();
++    this.operatorRegistry = ComponentOperatorRegistry.getInstance();
++    this.tabSwitcher = TabSwitcherProxy.getInstance();
++    this.addDocDialogFactory = AddDocumentDialogFactory.getInstance();
++    this.tvDialogFactory = TermVectorDialogFactory.getInstance();
++    this.dvDialogFactory = DocValuesDialogFactory.getInstance();
++    this.valueDialogFactory = StoredValueDialogFactory.getInstance();
++    HelpDialogFactory helpDialogFactory = HelpDialogFactory.getInstance();
++    this.tableHeaderRenderer = new HelpHeaderRenderer(
++        "About Flags", "Format: IdfpoNPSB#txxVDtxxxxTx/x",
++        createFlagsHelpDialog(), helpDialogFactory);
++
++    IndexHandler.getInstance().addObserver(new Observer());
++    operatorRegistry.register(DocumentsTabOperator.class, this);
++  }
++
++  private JComponent createFlagsHelpDialog() {
++    String[] values = new String[]{
++        "I - index options(docs, frequencies, positions, offsets)",
++        "N - norms",
++        "P - payloads",
++        "S - stored",
++        "B - binary stored values",
++        "#txx - numeric stored values(type, precision)",
++        "V - term vectors",
++        "Dtxxxxx - doc values(type)",
++        "Tx/x - point values(num bytes/dimension)"
++    };
++    JList<String> list = new JList<>(values);
++    return new JScrollPane(list);
++  }
++
++  public JPanel get() {
++    JPanel panel = new JPanel(new GridLayout(1, 1));
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createLineBorder(Color.gray));
++
++    JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, initUpperPanel(), initLowerPanel());
++    splitPane.setOpaque(false);
++    splitPane.setDividerLocation(0.4);
++    panel.add(splitPane);
++
++    setUpDocumentContextMenu();
++
++    return panel;
++  }
++
++  private JPanel initUpperPanel() {
++    JPanel panel = new JPanel(new GridBagLayout());
++    panel.setOpaque(false);
++    GridBagConstraints c = new GridBagConstraints();
++
++    c.gridx = 0;
++    c.gridy = 0;
++    c.weightx = 0.5;
++    c.anchor = GridBagConstraints.FIRST_LINE_START;
++    c.fill = GridBagConstraints.HORIZONTAL;
++    panel.add(initBrowseTermsPanel(), c);
++
++    c.gridx = 1;
++    c.gridy = 0;
++    c.weightx = 0.5;
++    c.anchor = GridBagConstraints.FIRST_LINE_START;
++    c.fill = GridBagConstraints.HORIZONTAL;
++    panel.add(initBrowseDocsByTermPanel(), c);
++
++    return panel;
++  }
++
++  private JPanel initBrowseTermsPanel() {
++    JPanel panel = new JPanel(new BorderLayout());
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
++
++    JPanel top = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    top.setOpaque(false);
++    JLabel label = new JLabel(MessageUtils.getLocalizedMessage("documents.label.browse_terms"));
++    top.add(label);
++
++    panel.add(top, BorderLayout.PAGE_START);
++
++    JPanel center = new JPanel(new GridBagLayout());
++    center.setOpaque(false);
++    GridBagConstraints c = new GridBagConstraints();
++    c.fill = GridBagConstraints.BOTH;
++
++    fieldsCombo.addActionListener(listeners::showFirstTerm);
++    c.gridx = 0;
++    c.gridy = 0;
++    c.insets = new Insets(5, 5, 5, 5);
++    c.weightx = 0.0;
++    c.gridwidth = 2;
++    center.add(fieldsCombo, c);
++
++    firstTermBtn.setText(FontUtils.elegantIconHtml("&#x38;", MessageUtils.getLocalizedMessage("documents.button.first_term")));
++    firstTermBtn.setMaximumSize(new Dimension(80, 30));
++    firstTermBtn.addActionListener(listeners::showFirstTerm);
++    c.gridx = 0;
++    c.gridy = 1;
++    c.insets = new Insets(5, 5, 5, 5);
++    c.weightx = 0.2;
++    c.gridwidth = 1;
++    center.add(firstTermBtn, c);
++
++    termTF.setColumns(20);
++    termTF.setMinimumSize(new Dimension(50, 25));
++    termTF.setFont(StyleConstants.FONT_MONOSPACE_LARGE);
++    termTF.addActionListener(listeners::seekNextTerm);
++    c.gridx = 1;
++    c.gridy = 1;
++    c.insets = new Insets(5, 5, 5, 5);
++    c.weightx = 0.5;
++    c.gridwidth = 1;
++    center.add(termTF, c);
++
++    nextTermBtn.setText(MessageUtils.getLocalizedMessage("documents.button.next"));
++    nextTermBtn.addActionListener(listeners::showNextTerm);
++    c.gridx = 2;
++    c.gridy = 1;
++    c.insets = new Insets(5, 5, 5, 5);
++    c.weightx = 0.1;
++    c.gridwidth = 1;
++    center.add(nextTermBtn, c);
++
++    panel.add(center, BorderLayout.CENTER);
++
++    JPanel footer = new JPanel(new FlowLayout(FlowLayout.LEADING, 20, 5));
++    footer.setOpaque(false);
++    JLabel hintLbl = new JLabel(MessageUtils.getLocalizedMessage("documents.label.browse_terms_hint"));
++    footer.add(hintLbl);
++    panel.add(footer, BorderLayout.PAGE_END);
++
++    return panel;
++  }
++
++  private JPanel initBrowseDocsByTermPanel() {
++    JPanel panel = new JPanel(new BorderLayout());
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
++
++    JPanel center = new JPanel(new GridBagLayout());
++    center.setOpaque(false);
++    GridBagConstraints c = new GridBagConstraints();
++    c.fill = GridBagConstraints.BOTH;
++
++    JLabel label = new JLabel(MessageUtils.getLocalizedMessage("documents.label.browse_doc_by_term"));
++    c.gridx = 0;
++    c.gridy = 0;
++    c.weightx = 0.0;
++    c.gridwidth = 2;
++    c.insets = new Insets(5, 5, 5, 5);
++    center.add(label, c);
++
++    selectedTermTF.setColumns(20);
++    selectedTermTF.setFont(StyleConstants.FONT_MONOSPACE_LARGE);
++    selectedTermTF.setEditable(false);
++    selectedTermTF.setBackground(Color.white);
++    c.gridx = 0;
++    c.gridy = 1;
++    c.weightx = 0.0;
++    c.gridwidth = 2;
++    c.insets = new Insets(5, 5, 5, 5);
++    center.add(selectedTermTF, c);
++
++    firstTermDocBtn.setText(FontUtils.elegantIconHtml("&#x38;", MessageUtils.getLocalizedMessage("documents.button.first_termdoc")));
++    firstTermDocBtn.addActionListener(listeners::showFirstTermDoc);
++    c.gridx = 0;
++    c.gridy = 2;
++    c.weightx = 0.2;
++    c.gridwidth = 1;
++    c.insets = new Insets(5, 3, 5, 5);
++    center.add(firstTermDocBtn, c);
++
++    termDocIdxTF.setEditable(false);
++    termDocIdxTF.setBackground(Color.white);
++    c.gridx = 1;
++    c.gridy = 2;
++    c.weightx = 0.5;
++    c.gridwidth = 1;
++    c.insets = new Insets(5, 5, 5, 5);
++    center.add(termDocIdxTF, c);
++
++    nextTermDocBtn.setText(MessageUtils.getLocalizedMessage("documents.button.next"));
++    nextTermDocBtn.addActionListener(listeners::showNextTermDoc);
++    c.gridx = 2;
++    c.gridy = 2;
++    c.weightx = 0.2;
++    c.gridwidth = 1;
++    c.insets = new Insets(5, 5, 5, 5);
++    center.add(nextTermDocBtn, c);
++
++    termDocsNumLbl.setText("in ? docs");
++    c.gridx = 3;
++    c.gridy = 2;
++    c.weightx = 0.3;
++    c.gridwidth = 1;
++    c.insets = new Insets(5, 5, 5, 5);
++    center.add(termDocsNumLbl, c);
++
++    TableUtils.setupTable(posTable, ListSelectionModel.SINGLE_SELECTION, new PosTableModel(), null,
++        PosTableModel.Column.POSITION.getColumnWidth(), PosTableModel.Column.OFFSETS.getColumnWidth(), PosTableModel.Column.PAYLOAD.getColumnWidth());
++    JScrollPane scrollPane = new JScrollPane(posTable);
++    scrollPane.setMinimumSize(new Dimension(100, 100));
++    c.gridx = 0;
++    c.gridy = 3;
++    c.gridwidth = 4;
++    c.insets = new Insets(5, 5, 5, 5);
++    center.add(scrollPane, c);
++
++    panel.add(center, BorderLayout.CENTER);
++
++    return panel;
++  }
++
++  private JPanel initLowerPanel() {
++    JPanel panel = new JPanel(new BorderLayout());
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
++
++    JPanel browseDocsPanel = new JPanel();
++    browseDocsPanel.setOpaque(false);
++    browseDocsPanel.setLayout(new BoxLayout(browseDocsPanel, BoxLayout.PAGE_AXIS));
++    browseDocsPanel.add(initBrowseDocsBar());
++
++    JPanel browseDocsNote1 = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    browseDocsNote1.setOpaque(false);
++    browseDocsNote1.add(new JLabel(MessageUtils.getLocalizedMessage("documents.label.doc_table_note1")));
++    browseDocsPanel.add(browseDocsNote1);
++
++    JPanel browseDocsNote2 = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    browseDocsNote2.setOpaque(false);
++    browseDocsNote2.add(new JLabel(MessageUtils.getLocalizedMessage("documents.label.doc_table_note2")));
++    browseDocsPanel.add(browseDocsNote2);
++
++    panel.add(browseDocsPanel, BorderLayout.PAGE_START);
++
++    TableUtils.setupTable(documentTable, ListSelectionModel.MULTIPLE_INTERVAL_SELECTION, new DocumentsTableModel(), new MouseAdapter() {
++          @Override
++          public void mouseClicked(MouseEvent e) {
++            listeners.showDocumentContextMenu(e);
++          }
++        },
++        DocumentsTableModel.Column.FIELD.getColumnWidth(),
++        DocumentsTableModel.Column.FLAGS.getColumnWidth(),
++        DocumentsTableModel.Column.NORM.getColumnWidth(),
++        DocumentsTableModel.Column.VALUE.getColumnWidth());
++    JPanel flagsHeader = new JPanel(new FlowLayout(FlowLayout.CENTER));
++    flagsHeader.setOpaque(false);
++    flagsHeader.add(new JLabel("Flags"));
++    flagsHeader.add(new JLabel("Help"));
++    documentTable.getColumnModel().getColumn(DocumentsTableModel.Column.FLAGS.getIndex()).setHeaderValue(flagsHeader);
++
++    JScrollPane scrollPane = new JScrollPane(documentTable);
++    scrollPane.getHorizontalScrollBar().setAutoscrolls(false);
++    panel.add(scrollPane, BorderLayout.CENTER);
++
++    return panel;
++  }
++
++  private JPanel initBrowseDocsBar() {
++    JPanel panel = new JPanel(new GridLayout(1, 2));
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 5));
++
++    JPanel left = new JPanel(new FlowLayout(FlowLayout.LEADING, 10, 2));
++    left.setOpaque(false);
++    JLabel label = new JLabel(FontUtils.elegantIconHtml("&#x68;", MessageUtils.getLocalizedMessage("documents.label.browse_doc_by_idx")));
++    label.setHorizontalTextPosition(JLabel.LEFT);
++    left.add(label);
++    docNumSpnr.setPreferredSize(new Dimension(100, 25));
++    docNumSpnr.addChangeListener(listeners::showCurrentDoc);
++    left.add(docNumSpnr);
++    maxDocsLbl.setText("in ? docs");
++    left.add(maxDocsLbl);
++    panel.add(left);
++
++    JPanel right = new JPanel(new FlowLayout(FlowLayout.TRAILING));
++    right.setOpaque(false);
++    copyDocValuesBtn.setText(FontUtils.elegantIconHtml("&#xe0e6;", MessageUtils.getLocalizedMessage("documents.buttont.copy_values")));
++    copyDocValuesBtn.setMargin(new Insets(5, 0, 5, 0));
++    copyDocValuesBtn.addActionListener(listeners::copySelectedOrAllStoredValues);
++    right.add(copyDocValuesBtn);
++    mltBtn.setText(FontUtils.elegantIconHtml("&#xe030;", MessageUtils.getLocalizedMessage("documents.button.mlt")));
++    mltBtn.setMargin(new Insets(5, 0, 5, 0));
++    mltBtn.addActionListener(listeners::mltSearch);
++    right.add(mltBtn);
++    addDocBtn.setText(FontUtils.elegantIconHtml("&#x59;", MessageUtils.getLocalizedMessage("documents.button.add")));
++    addDocBtn.setMargin(new Insets(5, 0, 5, 0));
++    addDocBtn.addActionListener(listeners::showAddDocumentDialog);
++    right.add(addDocBtn);
++    panel.add(right);
++
++    return panel;
++  }
++
++  private void setUpDocumentContextMenu() {
++    // show term vector
++    JMenuItem item1 = new JMenuItem(MessageUtils.getLocalizedMessage("documents.doctable.menu.item1"));
++    item1.addActionListener(listeners::showTermVectorDialog);
++    documentContextMenu.add(item1);
++
++    // show doc values
++    JMenuItem item2 = new JMenuItem(MessageUtils.getLocalizedMessage("documents.doctable.menu.item2"));
++    item2.addActionListener(listeners::showDocValuesDialog);
++    documentContextMenu.add(item2);
++
++    // show stored value
++    JMenuItem item3 = new JMenuItem(MessageUtils.getLocalizedMessage("documents.doctable.menu.item3"));
++    item3.addActionListener(listeners::showStoredValueDialog);
++    documentContextMenu.add(item3);
++
++    // copy stored value to clipboard
++    JMenuItem item4 = new JMenuItem(MessageUtils.getLocalizedMessage("documents.doctable.menu.item4"));
++    item4.addActionListener(listeners::copyStoredValue);
++    documentContextMenu.add(item4);
++  }
++
++  // control methods
++
++  private void showFirstTerm() {
++    String fieldName = (String) fieldsCombo.getSelectedItem();
++    if (fieldName == null || fieldName.length() == 0) {
++      messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("documents.field.message.not_selected"));
++      return;
++    }
++
++    termDocIdxTF.setText("");
++    clearPosTable();
++
++    Optional<Term> firstTerm = documentsModel.firstTerm(fieldName);
++    String firstTermText = firstTerm.map(Term::text).orElse("");
++    termTF.setText(firstTermText);
++    selectedTermTF.setText(firstTermText);
++    if (firstTerm.isPresent()) {
++      String num = documentsModel.getDocFreq().map(String::valueOf).orElse("?");
++      termDocsNumLbl.setText("in " + num + " docs");
++
++      nextTermBtn.setEnabled(true);
++      termTF.setEditable(true);
++      firstTermDocBtn.setEnabled(true);
++    } else {
++      nextTermBtn.setEnabled(false);
++      termTF.setEditable(false);
++      firstTermDocBtn.setEnabled(false);
++    }
++    nextTermDocBtn.setEnabled(false);
++    messageBroker.clearStatusMessage();
++  }
++
++  private void showNextTerm() {
++    termDocIdxTF.setText("");
++    clearPosTable();
++
++    Optional<Term> nextTerm = documentsModel.nextTerm();
++    String nextTermText = nextTerm.map(Term::text).orElse("");
++    termTF.setText(nextTermText);
++    selectedTermTF.setText(nextTermText);
++    if (nextTerm.isPresent()) {
++      String num = documentsModel.getDocFreq().map(String::valueOf).orElse("?");
++      termDocsNumLbl.setText("in " + num + " docs");
++
++      termTF.setEditable(true);
++      firstTermDocBtn.setEnabled(true);
++    } else {
++      nextTermBtn.setEnabled(false);
++      termTF.setEditable(false);
++      firstTermDocBtn.setEnabled(false);
++    }
++    nextTermDocBtn.setEnabled(false);
++    messageBroker.clearStatusMessage();
++  }
++
++  @Override
++  public void seekNextTerm() {
++    termDocIdxTF.setText("");
++    posTable.setModel(new PosTableModel());
++
++    String termText = termTF.getText();
++
++    Optional<Term> nextTerm = documentsModel.seekTerm(termText);
++    String nextTermText = nextTerm.map(Term::text).orElse("");
++    termTF.setText(nextTermText);
++    selectedTermTF.setText(nextTermText);
++    if (nextTerm.isPresent()) {
++      String num = documentsModel.getDocFreq().map(String::valueOf).orElse("?");
++      termDocsNumLbl.setText("in " + num + " docs");
++
++      termTF.setEditable(true);
++      firstTermDocBtn.setEnabled(true);
++    } else {
++      nextTermBtn.setEnabled(false);
++      termTF.setEditable(false);
++      firstTermDocBtn.setEnabled(false);
++    }
++    nextTermDocBtn.setEnabled(false);
++    messageBroker.clearStatusMessage();
++  }
++
++
++  private void clearPosTable() {
++    TableUtils.setupTable(posTable, ListSelectionModel.SINGLE_SELECTION, new PosTableModel(), null,
++        PosTableModel.Column.POSITION.getColumnWidth(),
++        PosTableModel.Column.OFFSETS.getColumnWidth(),
++        PosTableModel.Column.PAYLOAD.getColumnWidth());
++  }
++
++  @Override
++  public void showFirstTermDoc() {
++    int docid = documentsModel.firstTermDoc().orElse(-1);
++    if (docid < 0) {
++      nextTermDocBtn.setEnabled(false);
++      messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("documents.termdocs.message.not_available"));
++      return;
++    }
++    termDocIdxTF.setText(String.valueOf(1));
++    displayDoc(docid);
++
++    List<TermPosting> postings = documentsModel.getTermPositions();
++    posTable.setModel(new PosTableModel(postings));
++    posTable.getColumnModel().getColumn(PosTableModel.Column.POSITION.getIndex()).setPreferredWidth(PosTableModel.Column.POSITION.getColumnWidth());
++    posTable.getColumnModel().getColumn(PosTableModel.Column.OFFSETS.getIndex()).setPreferredWidth(PosTableModel.Column.OFFSETS.getColumnWidth());
++    posTable.getColumnModel().getColumn(PosTableModel.Column.PAYLOAD.getIndex()).setPreferredWidth(PosTableModel.Column.PAYLOAD.getColumnWidth());
++
++    nextTermDocBtn.setEnabled(true);
++    messageBroker.clearStatusMessage();
++  }
++
++  private void showNextTermDoc() {
++    int docid = documentsModel.nextTermDoc().orElse(-1);
++    if (docid < 0) {
++      nextTermDocBtn.setEnabled(false);
++      messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("documents.termdocs.message.not_available"));
++      return;
++    }
++    int curIdx = Integer.parseInt(termDocIdxTF.getText());
++    termDocIdxTF.setText(String.valueOf(curIdx + 1));
++    displayDoc(docid);
++
++    List<TermPosting> postings = documentsModel.getTermPositions();
++    posTable.setModel(new PosTableModel(postings));
++
++    nextTermDocBtn.setDefaultCapable(true);
++    messageBroker.clearStatusMessage();
++  }
++
++  private void showCurrentDoc() {
++    int docid = (Integer) docNumSpnr.getValue();
++    displayDoc(docid);
++  }
++
++  private void mltSearch() {
++    int docNum = (int) docNumSpnr.getValue();
++    operatorRegistry.get(SearchTabOperator.class).ifPresent(operator -> {
++      operator.mltSearch(docNum);
++      tabSwitcher.switchTab(TabbedPaneProvider.Tab.SEARCH);
++    });
++  }
++
++  private void showAddDocumentDialog() {
++    new DialogOpener<>(addDocDialogFactory).open("Add document", 600, 500,
++        (factory) -> {
++        });
++  }
++
++  private void showTermVectorDialog() {
++    int docid = (Integer) docNumSpnr.getValue();
++    String field = (String) documentTable.getModel().getValueAt(documentTable.getSelectedRow(), DocumentsTableModel.Column.FIELD.getIndex());
++    List<TermVectorEntry> tvEntries = documentsModel.getTermVectors(docid, field);
++    if (tvEntries.isEmpty()) {
++      messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("documents.termvector.message.not_available", field, docid));
++      return;
++    }
++
++    new DialogOpener<>(tvDialogFactory).open(
++        "Term Vector", 600, 400,
++        (factory) -> {
++          factory.setField(field);
++          factory.setTvEntries(tvEntries);
++        });
++    messageBroker.clearStatusMessage();
++  }
++
++  private void showDocValuesDialog() {
++    int docid = (Integer) docNumSpnr.getValue();
++    String field = (String) documentTable.getModel().getValueAt(documentTable.getSelectedRow(), DocumentsTableModel.Column.FIELD.getIndex());
++    Optional<DocValues> docValues = documentsModel.getDocValues(docid, field);
++    if (docValues.isPresent()) {
++      new DialogOpener<>(dvDialogFactory).open(
++          "Doc Values", 400, 300,
++          (factory) -> {
++            factory.setValue(field, docValues.get());
++          });
++      messageBroker.clearStatusMessage();
++    } else {
++      messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("documents.docvalues.message.not_available", field, docid));
++    }
++  }
++
++  private void showStoredValueDialog() {
++    int docid = (Integer) docNumSpnr.getValue();
++    String field = (String) documentTable.getModel().getValueAt(documentTable.getSelectedRow(), DocumentsTableModel.Column.FIELD.getIndex());
++    String value = (String) documentTable.getModel().getValueAt(documentTable.getSelectedRow(), DocumentsTableModel.Column.VALUE.getIndex());
++    if (Objects.isNull(value)) {
++      messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("documents.stored.message.not_availabe", field, docid));
++      return;
++    }
++    new DialogOpener<>(valueDialogFactory).open(
++        "Stored Value", 400, 300,
++        (factory) -> {
++          factory.setField(field);
++          factory.setValue(value);
++        });
++    messageBroker.clearStatusMessage();
++  }
++
++  private void copyStoredValue() {
++    int docid = (Integer) docNumSpnr.getValue();
++    String field = (String) documentTable.getModel().getValueAt(documentTable.getSelectedRow(), DocumentsTableModel.Column.FIELD.getIndex());
++    String value = (String) documentTable.getModel().getValueAt(documentTable.getSelectedRow(), DocumentsTableModel.Column.VALUE.getIndex());
++    if (Objects.isNull(value)) {
++      messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("documents.stored.message.not_availabe", field, docid));
++      return;
++    }
++    Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
++    StringSelection selection = new StringSelection(value);
++    clipboard.setContents(selection, null);
++    messageBroker.clearStatusMessage();
++  }
++
++  private void copySelectedOrAllStoredValues() {
++    StringSelection selection;
++    if (documentTable.getSelectedRowCount() == 0) {
++      selection = copyAllValues();
++    } else {
++      selection = copySelectedValues();
++    }
++    Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
++    clipboard.setContents(selection, null);
++    messageBroker.clearStatusMessage();
++  }
++
++  private StringSelection copyAllValues() {
++    StringBuilder sb = new StringBuilder();
++    for (int i = 0; i < documentTable.getRowCount(); i++) {
++      String value = (String) documentTable.getModel().getValueAt(i, DocumentsTableModel.Column.VALUE.getIndex());
++      if (Objects.nonNull(value)) {
++        sb.append((i == 0) ? value : System.lineSeparator() + value);
++      }
++    }
++    return new StringSelection(sb.toString());
++  }
++
++  private StringSelection copySelectedValues() {
++    StringBuilder sb = new StringBuilder();
++    boolean isFirst = true;
++    for (int rowIndex : documentTable.getSelectedRows()) {
++      String value = (String) documentTable.getModel().getValueAt(rowIndex, DocumentsTableModel.Column.VALUE.getIndex());
++      if (Objects.nonNull(value)) {
++        sb.append(isFirst ? value : System.lineSeparator() + value);
++        isFirst = false;
++      }
++    }
++    return new StringSelection(sb.toString());
++  }
++
++  @Override
++  public void browseTerm(String field, String term) {
++    fieldsCombo.setSelectedItem(field);
++    termTF.setText(term);
++    seekNextTerm();
++    showFirstTermDoc();
++  }
++
++  @Override
++  public void displayLatestDoc() {
++    int docid = documentsModel.getMaxDoc() - 1;
++    showDoc(docid);
++  }
++
++  @Override
++  public void displayDoc(int docid) {
++    showDoc(docid);
++  }
++
++  ;
++
++  private void showDoc(int docid) {
++    docNumSpnr.setValue(docid);
++
++    List<DocumentField> doc = documentsModel.getDocumentFields(docid);
++    documentTable.setModel(new DocumentsTableModel(doc));
++    documentTable.setFont(StyleConstants.FONT_MONOSPACE_LARGE);
++    documentTable.getColumnModel().getColumn(DocumentsTableModel.Column.FIELD.getIndex()).setPreferredWidth(DocumentsTableModel.Column.FIELD.getColumnWidth());
++    documentTable.getColumnModel().getColumn(DocumentsTableModel.Column.FLAGS.getIndex()).setMinWidth(DocumentsTableModel.Column.FLAGS.getColumnWidth());
++    documentTable.getColumnModel().getColumn(DocumentsTableModel.Column.FLAGS.getIndex()).setMaxWidth(DocumentsTableModel.Column.FIELD.getColumnWidth());
++    documentTable.getColumnModel().getColumn(DocumentsTableModel.Column.NORM.getIndex()).setMinWidth(DocumentsTableModel.Column.NORM.getColumnWidth());
++    documentTable.getColumnModel().getColumn(DocumentsTableModel.Column.NORM.getIndex()).setMaxWidth(DocumentsTableModel.Column.NORM.getColumnWidth());
++    documentTable.getColumnModel().getColumn(DocumentsTableModel.Column.VALUE.getIndex()).setPreferredWidth(DocumentsTableModel.Column.VALUE.getColumnWidth());
++    documentTable.getColumnModel().getColumn(DocumentsTableModel.Column.FLAGS.getIndex()).setHeaderRenderer(tableHeaderRenderer);
++
++    messageBroker.clearStatusMessage();
++  }
++
++  private class ListenerFunctions {
++
++    void showFirstTerm(ActionEvent e) {
++      DocumentsPanelProvider.this.showFirstTerm();
++    }
++
++    void seekNextTerm(ActionEvent e) {
++      DocumentsPanelProvider.this.seekNextTerm();
++    }
++
++    void showNextTerm(ActionEvent e) {
++      DocumentsPanelProvider.this.showNextTerm();
++    }
++
++    void showFirstTermDoc(ActionEvent e) {
++      DocumentsPanelProvider.this.showFirstTermDoc();
++    }
++
++    void showNextTermDoc(ActionEvent e) {
++      DocumentsPanelProvider.this.showNextTermDoc();
++    }
++
++    void showCurrentDoc(ChangeEvent e) {
++      DocumentsPanelProvider.this.showCurrentDoc();
++    }
++
++    void mltSearch(ActionEvent e) {
++      DocumentsPanelProvider.this.mltSearch();
++    }
++
++    void showAddDocumentDialog(ActionEvent e) {
++      DocumentsPanelProvider.this.showAddDocumentDialog();
++    }
++
++    void showDocumentContextMenu(MouseEvent e) {
++      if (e.getClickCount() == 2 && !e.isConsumed()) {
++        int row = documentTable.rowAtPoint(e.getPoint());
++        if (row != documentTable.getSelectedRow()) {
++          documentTable.changeSelection(row, documentTable.getSelectedColumn(), false, false);
++        }
++        documentContextMenu.show(e.getComponent(), e.getX(), e.getY());
++      }
++    }
++
++    void showTermVectorDialog(ActionEvent e) {
++      DocumentsPanelProvider.this.showTermVectorDialog();
++    }
++
++    void showDocValuesDialog(ActionEvent e) {
++      DocumentsPanelProvider.this.showDocValuesDialog();
++    }
++
++    void showStoredValueDialog(ActionEvent e) {
++      DocumentsPanelProvider.this.showStoredValueDialog();
++    }
++
++    void copyStoredValue(ActionEvent e) {
++      DocumentsPanelProvider.this.copyStoredValue();
++    }
++
++    void copySelectedOrAllStoredValues(ActionEvent e) {
++      DocumentsPanelProvider.this.copySelectedOrAllStoredValues();
++    }
++
++  }
++
++  private class Observer implements IndexObserver {
++
++    @Override
++    public void openIndex(LukeState state) {
++      documentsModel = documentsFactory.newInstance(state.getIndexReader());
++
++      addDocBtn.setEnabled(!state.readOnly() && state.hasDirectoryReader());
++
++      int maxDoc = documentsModel.getMaxDoc();
++      maxDocsLbl.setText("in " + maxDoc + " docs");
++      if (maxDoc > 0) {
++        int max = Math.max(maxDoc - 1, 0);
++        SpinnerModel spinnerModel = new SpinnerNumberModel(0, 0, max, 1);
++        docNumSpnr.setModel(spinnerModel);
++        docNumSpnr.setEnabled(true);
++        displayDoc(0);
++      } else {
++        docNumSpnr.setEnabled(false);
++      }
++
++      documentsModel.getFieldNames().stream().sorted().forEach(fieldsCombo::addItem);
++    }
++
++    @Override
++    public void closeIndex() {
++      maxDocsLbl.setText("in ? docs");
++      docNumSpnr.setEnabled(false);
++      fieldsCombo.removeAllItems();
++      termTF.setText("");
++      selectedTermTF.setText("");
++      termDocsNumLbl.setText("");
++      termDocIdxTF.setText("");
++
++      posTable.setModel(new PosTableModel());
++      documentTable.setModel(new DocumentsTableModel());
++    }
++  }
++
++  static final class PosTableModel extends TableModelBase<PosTableModel.Column> {
++
++    enum Column implements TableColumnInfo {
++
++      POSITION("Position", 0, Integer.class, 80),
++      OFFSETS("Offsets", 1, String.class, 120),
++      PAYLOAD("Payload", 2, String.class, 300);
++
++      private final String colName;
++      private final int index;
++      private final Class<?> type;
++      private final int width;
++
++      Column(String colName, int index, Class<?> type, int width) {
++        this.colName = colName;
++        this.index = index;
++        this.type = type;
++        this.width = width;
++      }
++
++      @Override
++      public String getColName() {
++        return colName;
++      }
++
++      @Override
++      public int getIndex() {
++        return index;
++      }
++
++      @Override
++      public Class<?> getType() {
++        return type;
++      }
++
++      @Override
++      public int getColumnWidth() {
++        return width;
++      }
++    }
++
++    PosTableModel() {
++      super();
++    }
++
++    PosTableModel(List<TermPosting> postings) {
++      super(postings.size());
++
++      for (int i = 0; i < postings.size(); i++) {
++        TermPosting p = postings.get(i);
++
++        int position = postings.get(i).getPosition();
++        String offset = null;
++        if (p.getStartOffset() >= 0 && p.getEndOffset() >= 0) {
++          offset = p.getStartOffset() + "-" + p.getEndOffset();
++        }
++        String payload = null;
++        if (p.getPayload() != null) {
++          payload = BytesRefUtils.decode(p.getPayload());
++        }
++
++        data[i] = new Object[]{position, offset, payload};
++      }
++    }
++
++    @Override
++    protected Column[] columnInfos() {
++      return Column.values();
++    }
++  }
++
++  static final class DocumentsTableModel extends TableModelBase<DocumentsTableModel.Column> {
++
++    enum Column implements TableColumnInfo {
++      FIELD("Field", 0, String.class, 150),
++      FLAGS("Flags", 1, String.class, 200),
++      NORM("Norm", 2, Long.class, 80),
++      VALUE("Value", 3, String.class, 500);
++
++      private final String colName;
++      private final int index;
++      private final Class<?> type;
++      private final int width;
++
++      Column(String colName, int index, Class<?> type, int width) {
++        this.colName = colName;
++        this.index = index;
++        this.type = type;
++        this.width = width;
++      }
++
++      @Override
++      public String getColName() {
++        return colName;
++      }
++
++      @Override
++      public int getIndex() {
++        return index;
++      }
++
++      @Override
++      public Class<?> getType() {
++        return type;
++      }
++
++      @Override
++      public int getColumnWidth() {
++        return width;
++      }
++    }
++
++    DocumentsTableModel() {
++      super();
++    }
++
++    DocumentsTableModel(List<DocumentField> doc) {
++      super(doc.size());
++
++      for (int i = 0; i < doc.size(); i++) {
++        DocumentField docField = doc.get(i);
++        String field = docField.getName();
++        String flags = flags(docField);
++        long norm = docField.getNorm();
++        String value = null;
++        if (docField.getStringValue() != null) {
++          value = docField.getStringValue();
++        } else if (docField.getNumericValue() != null) {
++          value = String.valueOf(docField.getNumericValue());
++        } else if (docField.getBinaryValue() != null) {
++          value = String.valueOf(docField.getBinaryValue());
++        }
++        data[i] = new Object[]{field, flags, norm, value};
++      }
++    }
++
++    private static String flags(org.apache.lucene.luke.models.documents.DocumentField f) {
++      StringBuilder sb = new StringBuilder();
++      // index options
++      if (f.getIdxOptions() == null || f.getIdxOptions() == IndexOptions.NONE) {
++        sb.append("-----");
++      } else {
++        sb.append("I");
++        switch (f.getIdxOptions()) {
++          case DOCS:
++            sb.append("d---");
++            break;
++          case DOCS_AND_FREQS:
++            sb.append("df--");
++            break;
++          case DOCS_AND_FREQS_AND_POSITIONS:
++            sb.append("dfp-");
++            break;
++          case DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS:
++            sb.append("dfpo");
++            break;
++          default:
++            sb.append("----");
++        }
++      }
++      // has norm?
++      if (f.hasNorms()) {
++        sb.append("N");
++      } else {
++        sb.append("-");
++      }
++      // has payloads?
++      if (f.hasPayloads()) {
++        sb.append("P");
++      } else {
++        sb.append("-");
++      }
++      // stored?
++      if (f.isStored()) {
++        sb.append("S");
++      } else {
++        sb.append("-");
++      }
++      // binary?
++      if (f.getBinaryValue() != null) {
++        sb.append("B");
++      } else {
++        sb.append("-");
++      }
++      // numeric?
++      if (f.getNumericValue() == null) {
++        sb.append("----");
++      } else {
++        sb.append("#");
++        // try faking it
++        Number numeric = f.getNumericValue();
++        if (numeric instanceof Integer) {
++          sb.append("i32");
++        } else if (numeric instanceof Long) {
++          sb.append("i64");
++        } else if (numeric instanceof Float) {
++          sb.append("f32");
++        } else if (numeric instanceof Double) {
++          sb.append("f64");
++        } else if (numeric instanceof Short) {
++          sb.append("i16");
++        } else if (numeric instanceof Byte) {
++          sb.append("i08");
++        } else if (numeric instanceof BigDecimal) {
++          sb.append("b^d");
++        } else if (numeric instanceof BigInteger) {
++          sb.append("b^i");
++        } else {
++          sb.append("???");
++        }
++      }
++      // has term vector?
++      if (f.hasTermVectors()) {
++        sb.append("V");
++      } else {
++        sb.append("-");
++      }
++      // doc values
++      if (f.getDvType() == null || f.getDvType() == DocValuesType.NONE) {
++        sb.append("-------");
++      } else {
++        sb.append("D");
++        switch (f.getDvType()) {
++          case NUMERIC:
++            sb.append("number");
++            break;
++          case BINARY:
++            sb.append("binary");
++            break;
++          case SORTED:
++            sb.append("sorted");
++            break;
++          case SORTED_NUMERIC:
++            sb.append("srtnum");
++            break;
++          case SORTED_SET:
++            sb.append("srtset");
++            break;
++          default:
++            sb.append("??????");
++        }
++      }
++      // point values
++      if (f.getPointDimensionCount() == 0) {
++        sb.append("----");
++      } else {
++        sb.append("T");
++        sb.append(f.getPointNumBytes());
++        sb.append("/");
++        sb.append(f.getPointDimensionCount());
++      }
++      return sb.toString();
++    }
++
++    @Override
++    protected Column[] columnInfos() {
++      return Column.values();
++    }
++  }
++
++}
++
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/DocumentsTabOperator.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/DocumentsTabOperator.java
+new file mode 100644
+index 00000000000..a0618da1f0a
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/DocumentsTabOperator.java
+@@ -0,0 +1,31 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop.components;
++
++/** Operator for the Documents tab */
++public interface DocumentsTabOperator extends ComponentOperatorRegistry.ComponentOperator {
++  void browseTerm(String field, String term);
++
++  void displayLatestDoc();
++
++  void displayDoc(int donid);
++
++  void seekNextTerm();
++
++  void showFirstTermDoc();
++}
+\ No newline at end of file
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/LogsPanelProvider.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/LogsPanelProvider.java
+new file mode 100644
+index 00000000000..1d27cea9ff3
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/LogsPanelProvider.java
+@@ -0,0 +1,58 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop.components;
++
++import javax.swing.BorderFactory;
++import javax.swing.JLabel;
++import javax.swing.JPanel;
++import javax.swing.JScrollPane;
++import javax.swing.JTextArea;
++import java.awt.BorderLayout;
++import java.awt.FlowLayout;
++
++import org.apache.lucene.luke.app.desktop.LukeMain;
++import org.apache.lucene.luke.app.desktop.util.MessageUtils;
++
++/** Provider of the Logs panel */
++public final class LogsPanelProvider {
++
++  private final JTextArea logTextArea;
++
++  public LogsPanelProvider(JTextArea logTextArea) {
++    this.logTextArea = logTextArea;
++  }
++
++  public JPanel get() {
++    JPanel panel = new JPanel(new BorderLayout());
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
++
++    JPanel header = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    header.setOpaque(false);
++    header.add(new JLabel(MessageUtils.getLocalizedMessage("logs.label.see_also")));
++
++    JLabel logPathLabel = new JLabel(LukeMain.LOG_FILE);
++    header.add(logPathLabel);
++
++    panel.add(header, BorderLayout.PAGE_START);
++
++    panel.add(new JScrollPane(logTextArea), BorderLayout.CENTER);
++    return panel;
++  }
++
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/LukeWindowOperator.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/LukeWindowOperator.java
+new file mode 100644
+index 00000000000..ecc51c88140
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/LukeWindowOperator.java
+@@ -0,0 +1,25 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop.components;
++
++import org.apache.lucene.luke.app.desktop.Preferences;
++
++/** Operator for the root window */
++public interface LukeWindowOperator extends ComponentOperatorRegistry.ComponentOperator {
++  void setColorTheme(Preferences.ColorTheme theme);
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/LukeWindowProvider.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/LukeWindowProvider.java
+new file mode 100644
+index 00000000000..faf5c1c1e27
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/LukeWindowProvider.java
+@@ -0,0 +1,250 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop.components;
++
++import javax.swing.BorderFactory;
++import javax.swing.JFrame;
++import javax.swing.JLabel;
++import javax.swing.JMenuBar;
++import javax.swing.JPanel;
++import javax.swing.JTabbedPane;
++import javax.swing.JTextArea;
++import javax.swing.WindowConstants;
++import java.awt.BorderLayout;
++import java.awt.Color;
++import java.awt.Dimension;
++import java.awt.FlowLayout;
++import java.awt.GridBagConstraints;
++import java.awt.GridBagLayout;
++import java.awt.GridLayout;
++import java.io.IOException;
++
++import org.apache.lucene.luke.app.DirectoryHandler;
++import org.apache.lucene.luke.app.DirectoryObserver;
++import org.apache.lucene.luke.app.IndexHandler;
++import org.apache.lucene.luke.app.IndexObserver;
++import org.apache.lucene.luke.app.LukeState;
++import org.apache.lucene.luke.app.desktop.MessageBroker;
++import org.apache.lucene.luke.app.desktop.Preferences;
++import org.apache.lucene.luke.app.desktop.PreferencesFactory;
++import org.apache.lucene.luke.app.desktop.util.FontUtils;
++import org.apache.lucene.luke.app.desktop.util.ImageUtils;
++import org.apache.lucene.luke.app.desktop.util.MessageUtils;
++import org.apache.lucene.luke.app.desktop.util.TextAreaAppender;
++import org.apache.lucene.util.Version;
++
++/** Provider of the root window */
++public final class LukeWindowProvider implements LukeWindowOperator {
++
++  private static final String WINDOW_TITLE = MessageUtils.getLocalizedMessage("window.title") + " - v" + Version.LATEST.toString();
++
++  private final Preferences prefs;
++
++  private final MessageBroker messageBroker;
++
++  private final TabSwitcherProxy tabSwitcher;
++
++  private final JMenuBar menuBar;
++
++  private final JTabbedPane tabbedPane;
++
++  private final JLabel messageLbl = new JLabel();
++
++  private final JLabel multiIcon = new JLabel();
++
++  private final JLabel readOnlyIcon = new JLabel();
++
++  private final JLabel noReaderIcon = new JLabel();
++
++  private JFrame frame = new JFrame();
++
++  public LukeWindowProvider() throws IOException {
++    // prepare log4j appender for Logs tab.
++    JTextArea logTextArea = new JTextArea();
++    logTextArea.setEditable(false);
++    TextAreaAppender.setTextArea(logTextArea);
++
++    this.prefs = PreferencesFactory.getInstance();
++    this.menuBar = new MenuBarProvider().get();
++    this.tabbedPane = new TabbedPaneProvider(logTextArea).get();
++    this.messageBroker = MessageBroker.getInstance();
++    this.tabSwitcher = TabSwitcherProxy.getInstance();
++
++    ComponentOperatorRegistry.getInstance().register(LukeWindowOperator.class, this);
++    Observer observer = new Observer();
++    DirectoryHandler.getInstance().addObserver(observer);
++    IndexHandler.getInstance().addObserver(observer);
++
++    messageBroker.registerReceiver(new MessageReceiverImpl());
++  }
++
++  public JFrame get() {
++    frame.setTitle(WINDOW_TITLE);
++    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
++
++    frame.setJMenuBar(menuBar);
++    frame.add(initMainPanel(), BorderLayout.CENTER);
++    frame.add(initMessagePanel(), BorderLayout.PAGE_END);
++
++    frame.setPreferredSize(new Dimension(950, 680));
++    frame.getContentPane().setBackground(prefs.getColorTheme().getBackgroundColor());
++
++    return frame;
++  }
++
++  private JPanel initMainPanel() {
++    JPanel panel = new JPanel(new GridLayout(1, 1));
++
++    tabbedPane.setEnabledAt(TabbedPaneProvider.Tab.OVERVIEW.index(), false);
++    tabbedPane.setEnabledAt(TabbedPaneProvider.Tab.DOCUMENTS.index(), false);
++    tabbedPane.setEnabledAt(TabbedPaneProvider.Tab.SEARCH.index(), false);
++    tabbedPane.setEnabledAt(TabbedPaneProvider.Tab.COMMITS.index(), false);
++
++    panel.add(tabbedPane);
++
++    panel.setOpaque(false);
++    return panel;
++  }
++
++  private JPanel initMessagePanel() {
++    JPanel panel = new JPanel(new GridLayout(1, 1));
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createEmptyBorder(0, 2, 2, 2));
++
++    JPanel innerPanel = new JPanel(new GridBagLayout());
++    innerPanel.setOpaque(false);
++    innerPanel.setBorder(BorderFactory.createLineBorder(Color.gray));
++    GridBagConstraints c = new GridBagConstraints();
++    c.fill = GridBagConstraints.HORIZONTAL;
++
++    JPanel msgPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
++    msgPanel.setOpaque(false);
++    msgPanel.add(messageLbl);
++
++    c.gridx = 0;
++    c.gridy = 0;
++    c.weightx = 0.8;
++    innerPanel.add(msgPanel, c);
++
++    JPanel iconPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
++    iconPanel.setOpaque(false);
++
++    multiIcon.setText(FontUtils.elegantIconHtml("&#xe08c;"));
++    multiIcon.setToolTipText(MessageUtils.getLocalizedMessage("tooltip.multi_reader"));
++    multiIcon.setVisible(false);
++    iconPanel.add(multiIcon);
++
++
++    readOnlyIcon.setText(FontUtils.elegantIconHtml("&#xe06c;"));
++    readOnlyIcon.setToolTipText(MessageUtils.getLocalizedMessage("tooltip.read_only"));
++    readOnlyIcon.setVisible(false);
++    iconPanel.add(readOnlyIcon);
++
++    noReaderIcon.setText(FontUtils.elegantIconHtml("&#xe077;"));
++    noReaderIcon.setToolTipText(MessageUtils.getLocalizedMessage("tooltip.no_reader"));
++    noReaderIcon.setVisible(false);
++    iconPanel.add(noReaderIcon);
++
++    JLabel luceneIcon = new JLabel(ImageUtils.createImageIcon("lucene.gif", "lucene", 16, 16));
++    iconPanel.add(luceneIcon);
++
++    c.gridx = 1;
++    c.gridy = 0;
++    c.weightx = 0.2;
++    innerPanel.add(iconPanel);
++    panel.add(innerPanel);
++
++    return panel;
++  }
++
++  @Override
++  public void setColorTheme(Preferences.ColorTheme theme) {
++    frame.getContentPane().setBackground(theme.getBackgroundColor());
++  }
++
++  private class Observer implements IndexObserver, DirectoryObserver {
++
++    @Override
++    public void openDirectory(LukeState state) {
++      multiIcon.setVisible(false);
++      readOnlyIcon.setVisible(false);
++      noReaderIcon.setVisible(true);
++
++      tabSwitcher.switchTab(TabbedPaneProvider.Tab.COMMITS);
++
++      messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("message.directory_opened"));
++    }
++
++    @Override
++    public void closeDirectory() {
++      multiIcon.setVisible(false);
++      readOnlyIcon.setVisible(false);
++      noReaderIcon.setVisible(false);
++
++      messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("message.directory_closed"));
++    }
++
++    @Override
++    public void openIndex(LukeState state) {
++      multiIcon.setVisible(!state.hasDirectoryReader());
++      readOnlyIcon.setVisible(state.readOnly());
++      noReaderIcon.setVisible(false);
++
++      if (state.readOnly()) {
++        messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("message.index_opened_ro"));
++      } else if (!state.hasDirectoryReader()) {
++        messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("message.index_opened_multi"));
++      } else {
++        messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("message.index_opened"));
++      }
++    }
++
++    @Override
++    public void closeIndex() {
++      multiIcon.setVisible(false);
++      readOnlyIcon.setVisible(false);
++      noReaderIcon.setVisible(false);
++
++      messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("message.index_closed"));
++    }
++
++  }
++
++  private class MessageReceiverImpl implements MessageBroker.MessageReceiver {
++
++    @Override
++    public void showStatusMessage(String message) {
++      messageLbl.setText(message);
++    }
++
++    @Override
++    public void showUnknownErrorMessage() {
++      messageLbl.setText(MessageUtils.getLocalizedMessage("message.error.unknown"));
++    }
++
++    @Override
++    public void clearStatusMessage() {
++      messageLbl.setText("");
++    }
++
++    private MessageReceiverImpl() {
++    }
++
++  }
++
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/MenuBarProvider.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/MenuBarProvider.java
+new file mode 100644
+index 00000000000..2a5008f4c2b
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/MenuBarProvider.java
+@@ -0,0 +1,303 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop.components;
++
++import javax.swing.JMenu;
++import javax.swing.JMenuBar;
++import javax.swing.JMenuItem;
++import java.awt.event.ActionEvent;
++import java.io.IOException;
++
++import org.apache.lucene.luke.app.DirectoryHandler;
++import org.apache.lucene.luke.app.DirectoryObserver;
++import org.apache.lucene.luke.app.IndexHandler;
++import org.apache.lucene.luke.app.IndexObserver;
++import org.apache.lucene.luke.app.LukeState;
++import org.apache.lucene.luke.app.desktop.Preferences;
++import org.apache.lucene.luke.app.desktop.PreferencesFactory;
++import org.apache.lucene.luke.app.desktop.components.dialog.menubar.AboutDialogFactory;
++import org.apache.lucene.luke.app.desktop.components.dialog.menubar.CheckIndexDialogFactory;
++import org.apache.lucene.luke.app.desktop.components.dialog.menubar.CreateIndexDialogFactory;
++import org.apache.lucene.luke.app.desktop.components.dialog.menubar.OpenIndexDialogFactory;
++import org.apache.lucene.luke.app.desktop.components.dialog.menubar.OptimizeIndexDialogFactory;
++import org.apache.lucene.luke.app.desktop.util.DialogOpener;
++import org.apache.lucene.luke.app.desktop.util.MessageUtils;
++import org.apache.lucene.luke.models.LukeException;
++import org.apache.lucene.util.Version;
++
++/** Provider of the MenuBar */
++public final class MenuBarProvider {
++
++  private final Preferences prefs;
++
++  private final ComponentOperatorRegistry operatorRegistry;
++
++  private final DirectoryHandler directoryHandler;
++
++  private final IndexHandler indexHandler;
++
++  private final OpenIndexDialogFactory openIndexDialogFactory;
++
++  private final CreateIndexDialogFactory createIndexDialogFactory;
++
++  private final OptimizeIndexDialogFactory optimizeIndexDialogFactory;
++
++  private final CheckIndexDialogFactory checkIndexDialogFactory;
++
++  private final AboutDialogFactory aboutDialogFactory;
++
++  private final JMenuItem openIndexMItem = new JMenuItem();
++
++  private final JMenuItem reopenIndexMItem = new JMenuItem();
++
++  private final JMenuItem createIndexMItem = new JMenuItem();
++
++  private final JMenuItem closeIndexMItem = new JMenuItem();
++
++  private final JMenuItem grayThemeMItem = new JMenuItem();
++
++  private final JMenuItem classicThemeMItem = new JMenuItem();
++
++  private final JMenuItem sandstoneThemeMItem = new JMenuItem();
++
++  private final JMenuItem navyThemeMItem = new JMenuItem();
++
++  private final JMenuItem exitMItem = new JMenuItem();
++
++  private final JMenuItem optimizeIndexMItem = new JMenuItem();
++
++  private final JMenuItem checkIndexMItem = new JMenuItem();
++
++  private final JMenuItem aboutMItem = new JMenuItem();
++
++  private final ListenerFunctions listeners = new ListenerFunctions();
++
++  public MenuBarProvider() throws IOException {
++    this.prefs = PreferencesFactory.getInstance();
++    this.directoryHandler = DirectoryHandler.getInstance();
++    this.indexHandler = IndexHandler.getInstance();
++    this.operatorRegistry = ComponentOperatorRegistry.getInstance();
++    this.openIndexDialogFactory = OpenIndexDialogFactory.getInstance();
++    this.createIndexDialogFactory = CreateIndexDialogFactory.getInstance();
++    this.optimizeIndexDialogFactory = OptimizeIndexDialogFactory.getInstance();
++    this.checkIndexDialogFactory = CheckIndexDialogFactory.getInstance();
++    this.aboutDialogFactory = AboutDialogFactory.getInstance();
++
++    Observer observer = new Observer();
++    directoryHandler.addObserver(observer);
++    indexHandler.addObserver(observer);
++  }
++
++  public JMenuBar get() {
++    JMenuBar menuBar = new JMenuBar();
++
++    menuBar.add(createFileMenu());
++    menuBar.add(createToolsMenu());
++    menuBar.add(createHelpMenu());
++
++    return menuBar;
++  }
++
++  private JMenu createFileMenu() {
++    JMenu fileMenu = new JMenu(MessageUtils.getLocalizedMessage("menu.file"));
++
++    openIndexMItem.setText(MessageUtils.getLocalizedMessage("menu.item.open_index"));
++    openIndexMItem.addActionListener(listeners::showOpenIndexDialog);
++    fileMenu.add(openIndexMItem);
++
++    reopenIndexMItem.setText(MessageUtils.getLocalizedMessage("menu.item.reopen_index"));
++    reopenIndexMItem.setEnabled(false);
++    reopenIndexMItem.addActionListener(listeners::reopenIndex);
++    fileMenu.add(reopenIndexMItem);
++
++    createIndexMItem.setText(MessageUtils.getLocalizedMessage("menu.item.create_index"));
++    createIndexMItem.addActionListener(listeners::showCreateIndexDialog);
++    fileMenu.add(createIndexMItem);
++
++
++    closeIndexMItem.setText(MessageUtils.getLocalizedMessage("menu.item.close_index"));
++    closeIndexMItem.setEnabled(false);
++    closeIndexMItem.addActionListener(listeners::closeIndex);
++    fileMenu.add(closeIndexMItem);
++
++    fileMenu.addSeparator();
++
++    JMenu settingsMenu = new JMenu(MessageUtils.getLocalizedMessage("menu.settings"));
++    JMenu themeMenu = new JMenu(MessageUtils.getLocalizedMessage("menu.color"));
++    grayThemeMItem.setText(MessageUtils.getLocalizedMessage("menu.item.theme_gray"));
++    grayThemeMItem.addActionListener(listeners::changeThemeToGray);
++    themeMenu.add(grayThemeMItem);
++    classicThemeMItem.setText(MessageUtils.getLocalizedMessage("menu.item.theme_classic"));
++    classicThemeMItem.addActionListener(listeners::changeThemeToClassic);
++    themeMenu.add(classicThemeMItem);
++    sandstoneThemeMItem.setText(MessageUtils.getLocalizedMessage("menu.item.theme_sandstone"));
++    sandstoneThemeMItem.addActionListener(listeners::changeThemeToSandstone);
++    themeMenu.add(sandstoneThemeMItem);
++    navyThemeMItem.setText(MessageUtils.getLocalizedMessage("menu.item.theme_navy"));
++    navyThemeMItem.addActionListener(listeners::changeThemeToNavy);
++    themeMenu.add(navyThemeMItem);
++    settingsMenu.add(themeMenu);
++    fileMenu.add(settingsMenu);
++
++    fileMenu.addSeparator();
++
++    exitMItem.setText(MessageUtils.getLocalizedMessage("menu.item.exit"));
++    exitMItem.addActionListener(listeners::exit);
++    fileMenu.add(exitMItem);
++
++    return fileMenu;
++  }
++
++  private JMenu createToolsMenu() {
++    JMenu toolsMenu = new JMenu(MessageUtils.getLocalizedMessage("menu.tools"));
++    optimizeIndexMItem.setText(MessageUtils.getLocalizedMessage("menu.item.optimize"));
++    optimizeIndexMItem.setEnabled(false);
++    optimizeIndexMItem.addActionListener(listeners::showOptimizeIndexDialog);
++    toolsMenu.add(optimizeIndexMItem);
++    checkIndexMItem.setText(MessageUtils.getLocalizedMessage("menu.item.check_index"));
++    checkIndexMItem.setEnabled(false);
++    checkIndexMItem.addActionListener(listeners::showCheckIndexDialog);
++    toolsMenu.add(checkIndexMItem);
++    return toolsMenu;
++  }
++
++  private JMenu createHelpMenu() {
++    JMenu helpMenu = new JMenu(MessageUtils.getLocalizedMessage("menu.help"));
++    aboutMItem.setText(MessageUtils.getLocalizedMessage("menu.item.about"));
++    aboutMItem.addActionListener(listeners::showAboutDialog);
++    helpMenu.add(aboutMItem);
++    return helpMenu;
++  }
++
++  private class ListenerFunctions {
++
++    void showOpenIndexDialog(ActionEvent e) {
++      new DialogOpener<>(openIndexDialogFactory).open(MessageUtils.getLocalizedMessage("openindex.dialog.title"), 600, 420,
++          (factory) -> {});
++    }
++
++    void showCreateIndexDialog(ActionEvent e) {
++      new DialogOpener<>(createIndexDialogFactory).open(MessageUtils.getLocalizedMessage("createindex.dialog.title"), 600, 360,
++          (factory) -> {});
++    }
++
++    void reopenIndex(ActionEvent e) {
++      indexHandler.reOpen();
++    }
++
++    void closeIndex(ActionEvent e) {
++      close();
++    }
++
++    void changeThemeToGray(ActionEvent e) {
++      changeTheme(Preferences.ColorTheme.GRAY);
++    }
++
++    void changeThemeToClassic(ActionEvent e) {
++      changeTheme(Preferences.ColorTheme.CLASSIC);
++    }
++
++    void changeThemeToSandstone(ActionEvent e) {
++      changeTheme(Preferences.ColorTheme.SANDSTONE);
++    }
++
++    void changeThemeToNavy(ActionEvent e) {
++      changeTheme(Preferences.ColorTheme.NAVY);
++    }
++
++    private void changeTheme(Preferences.ColorTheme theme) {
++      try {
++        prefs.setColorTheme(theme);
++        operatorRegistry.get(LukeWindowOperator.class).ifPresent(operator -> operator.setColorTheme(theme));
++      } catch (IOException e) {
++        throw new LukeException("Failed to set color theme : " + theme.name(), e);
++      }
++    }
++
++    void exit(ActionEvent e) {
++      close();
++      System.exit(0);
++    }
++
++    private void close() {
++      directoryHandler.close();
++      indexHandler.close();
++    }
++
++    void showOptimizeIndexDialog(ActionEvent e) {
++      new DialogOpener<>(optimizeIndexDialogFactory).open("Optimize index", 600, 600,
++          factory -> {
++          });
++    }
++
++    void showCheckIndexDialog(ActionEvent e) {
++      new DialogOpener<>(checkIndexDialogFactory).open("Check index", 600, 600,
++          factory -> {
++          });
++    }
++
++    void showAboutDialog(ActionEvent e) {
++      final String title = "About Luke v" + Version.LATEST.toString();
++      new DialogOpener<>(aboutDialogFactory).open(title, 800, 480,
++          factory -> {
++          });
++    }
++
++  }
++
++  private class Observer implements IndexObserver, DirectoryObserver {
++
++    @Override
++    public void openDirectory(LukeState state) {
++      reopenIndexMItem.setEnabled(false);
++      closeIndexMItem.setEnabled(false);
++      optimizeIndexMItem.setEnabled(false);
++      checkIndexMItem.setEnabled(true);
++    }
++
++    @Override
++    public void closeDirectory() {
++      close();
++    }
++
++    @Override
++    public void openIndex(LukeState state) {
++      reopenIndexMItem.setEnabled(true);
++      closeIndexMItem.setEnabled(true);
++      if (!state.readOnly() && state.hasDirectoryReader()) {
++        optimizeIndexMItem.setEnabled(true);
++      }
++      if (state.hasDirectoryReader()) {
++        checkIndexMItem.setEnabled(true);
++      }
++    }
++
++    @Override
++    public void closeIndex() {
++      close();
++    }
++
++    private void close() {
++      reopenIndexMItem.setEnabled(false);
++      closeIndexMItem.setEnabled(false);
++      optimizeIndexMItem.setEnabled(false);
++      checkIndexMItem.setEnabled(false);
++    }
++
++  }
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/OverviewPanelProvider.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/OverviewPanelProvider.java
+new file mode 100644
+index 00000000000..c85e93bcd7c
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/OverviewPanelProvider.java
+@@ -0,0 +1,644 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop.components;
++
++import javax.swing.BorderFactory;
++import javax.swing.BoxLayout;
++import javax.swing.JButton;
++import javax.swing.JLabel;
++import javax.swing.JMenuItem;
++import javax.swing.JPanel;
++import javax.swing.JPopupMenu;
++import javax.swing.JScrollPane;
++import javax.swing.JSpinner;
++import javax.swing.JSplitPane;
++import javax.swing.JTable;
++import javax.swing.JTextField;
++import javax.swing.ListSelectionModel;
++import javax.swing.SpinnerNumberModel;
++import javax.swing.table.DefaultTableCellRenderer;
++import javax.swing.table.TableRowSorter;
++import java.awt.BorderLayout;
++import java.awt.Color;
++import java.awt.Dimension;
++import java.awt.FlowLayout;
++import java.awt.GridBagConstraints;
++import java.awt.GridBagLayout;
++import java.awt.GridLayout;
++import java.awt.Insets;
++import java.awt.event.ActionEvent;
++import java.awt.event.MouseAdapter;
++import java.awt.event.MouseEvent;
++import java.util.List;
++import java.util.Locale;
++import java.util.Map;
++
++import org.apache.lucene.luke.app.IndexHandler;
++import org.apache.lucene.luke.app.IndexObserver;
++import org.apache.lucene.luke.app.LukeState;
++import org.apache.lucene.luke.app.desktop.MessageBroker;
++import org.apache.lucene.luke.app.desktop.util.MessageUtils;
++import org.apache.lucene.luke.app.desktop.util.StyleConstants;
++import org.apache.lucene.luke.app.desktop.util.TableUtils;
++import org.apache.lucene.luke.models.overview.Overview;
++import org.apache.lucene.luke.models.overview.OverviewFactory;
++import org.apache.lucene.luke.models.overview.TermCountsOrder;
++import org.apache.lucene.luke.models.overview.TermStats;
++
++/** Provider of the Overview panel */
++public final class OverviewPanelProvider {
++
++  private static final int GRIDX_DESC = 0;
++  private static final int GRIDX_VAL = 1;
++  private static final double WEIGHTX_DESC = 0.1;
++  private static final double WEIGHTX_VAL = 0.9;
++
++  private final OverviewFactory overviewFactory = new OverviewFactory();
++
++  private final ComponentOperatorRegistry operatorRegistry;
++
++  private final TabSwitcherProxy tabSwitcher;
++
++  private final MessageBroker messageBroker;
++
++  private final JPanel panel = new JPanel();
++
++  private final JLabel indexPathLbl = new JLabel();
++
++  private final JLabel numFieldsLbl = new JLabel();
++
++  private final JLabel numDocsLbl = new JLabel();
++
++  private final JLabel numTermsLbl = new JLabel();
++
++  private final JLabel delOptLbl = new JLabel();
++
++  private final JLabel indexVerLbl = new JLabel();
++
++  private final JLabel indexFmtLbl = new JLabel();
++
++  private final JLabel dirImplLbl = new JLabel();
++
++  private final JLabel commitPointLbl = new JLabel();
++
++  private final JLabel commitUserDataLbl = new JLabel();
++
++  private final JTable termCountsTable = new JTable();
++
++  private final JTextField selectedField = new JTextField();
++
++  private final JButton showTopTermsBtn = new JButton();
++
++  private final JSpinner numTopTermsSpnr = new JSpinner();
++
++  private final JTable topTermsTable = new JTable();
++
++  private final JPopupMenu topTermsContextMenu = new JPopupMenu();
++
++  private final ListenerFunctions listeners = new ListenerFunctions();
++
++  private Overview overviewModel;
++
++  public OverviewPanelProvider() {
++    this.messageBroker = MessageBroker.getInstance();
++    this.operatorRegistry = ComponentOperatorRegistry.getInstance();
++    this.tabSwitcher = TabSwitcherProxy.getInstance();
++
++    IndexHandler.getInstance().addObserver(new Observer());
++  }
++
++  public JPanel get() {
++    panel.setOpaque(false);
++    panel.setLayout(new GridLayout(1, 1));
++    panel.setBorder(BorderFactory.createLineBorder(Color.gray));
++
++    JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, initUpperPanel(), initLowerPanel());
++    splitPane.setDividerLocation(0.4);
++    splitPane.setOpaque(false);
++    panel.add(splitPane);
++
++    setUpTopTermsContextMenu();
++
++    return panel;
++  }
++
++  private JPanel initUpperPanel() {
++    JPanel panel = new JPanel(new GridBagLayout());
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
++
++    GridBagConstraints c = new GridBagConstraints();
++    c.fill = GridBagConstraints.HORIZONTAL;
++    c.insets = new Insets(2, 10, 2, 2);
++    c.gridy = 0;
++
++    c.gridx = GRIDX_DESC;
++    c.weightx = WEIGHTX_DESC;
++    panel.add(new JLabel(MessageUtils.getLocalizedMessage("overview.label.index_path"), JLabel.RIGHT), c);
++
++    c.gridx = GRIDX_VAL;
++    c.weightx = WEIGHTX_VAL;
++    indexPathLbl.setText("?");
++    panel.add(indexPathLbl, c);
++
++    c.gridx = GRIDX_DESC;
++    c.gridy += 1;
++    c.weightx = WEIGHTX_DESC;
++    panel.add(new JLabel(MessageUtils.getLocalizedMessage("overview.label.num_fields"), JLabel.RIGHT), c);
++
++    c.gridx = GRIDX_VAL;
++    c.weightx = WEIGHTX_VAL;
++    numFieldsLbl.setText("?");
++    panel.add(numFieldsLbl, c);
++
++    c.gridx = GRIDX_DESC;
++    c.gridy += 1;
++    c.weightx = WEIGHTX_DESC;
++    panel.add(new JLabel(MessageUtils.getLocalizedMessage("overview.label.num_docs"), JLabel.RIGHT), c);
++
++    c.gridx = GRIDX_VAL;
++    c.weightx = WEIGHTX_VAL;
++    numDocsLbl.setText("?");
++    panel.add(numDocsLbl, c);
++
++    c.gridx = GRIDX_DESC;
++    c.gridy += 1;
++    c.weightx = WEIGHTX_DESC;
++    panel.add(new JLabel(MessageUtils.getLocalizedMessage("overview.label.num_terms"), JLabel.RIGHT), c);
++
++    c.gridx = GRIDX_VAL;
++    c.weightx = WEIGHTX_VAL;
++    numTermsLbl.setText("?");
++    panel.add(numTermsLbl, c);
++
++    c.gridx = GRIDX_DESC;
++    c.gridy += 1;
++    c.weightx = WEIGHTX_DESC;
++    panel.add(new JLabel(MessageUtils.getLocalizedMessage("overview.label.del_opt"), JLabel.RIGHT), c);
++
++    c.gridx = GRIDX_VAL;
++    c.weightx = WEIGHTX_VAL;
++    delOptLbl.setText("?");
++    panel.add(delOptLbl, c);
++
++    c.gridx = GRIDX_DESC;
++    c.gridy += 1;
++    c.weightx = WEIGHTX_DESC;
++    panel.add(new JLabel(MessageUtils.getLocalizedMessage("overview.label.index_version"), JLabel.RIGHT), c);
++
++    c.gridx = GRIDX_VAL;
++    c.weightx = WEIGHTX_VAL;
++    indexVerLbl.setText("?");
++    panel.add(indexVerLbl, c);
++
++    c.gridx = GRIDX_DESC;
++    c.gridy += 1;
++    c.weightx = WEIGHTX_DESC;
++    panel.add(new JLabel(MessageUtils.getLocalizedMessage("overview.label.index_format"), JLabel.RIGHT), c);
++
++    c.gridx = GRIDX_VAL;
++    c.weightx = WEIGHTX_VAL;
++    indexFmtLbl.setText("?");
++    panel.add(indexFmtLbl, c);
++
++    c.gridx = GRIDX_DESC;
++    c.gridy += 1;
++    c.weightx = WEIGHTX_DESC;
++    panel.add(new JLabel(MessageUtils.getLocalizedMessage("overview.label.dir_impl"), JLabel.RIGHT), c);
++
++    c.gridx = GRIDX_VAL;
++    c.weightx = WEIGHTX_VAL;
++    dirImplLbl.setText("?");
++    panel.add(dirImplLbl, c);
++
++    c.gridx = GRIDX_DESC;
++    c.gridy += 1;
++    c.weightx = WEIGHTX_DESC;
++    panel.add(new JLabel(MessageUtils.getLocalizedMessage("overview.label.commit_point"), JLabel.RIGHT), c);
++
++    c.gridx = GRIDX_VAL;
++    c.weightx = WEIGHTX_VAL;
++    commitPointLbl.setText("?");
++    panel.add(commitPointLbl, c);
++
++    c.gridx = GRIDX_DESC;
++    c.gridy += 1;
++    c.weightx = WEIGHTX_DESC;
++    panel.add(new JLabel(MessageUtils.getLocalizedMessage("overview.label.commit_userdata"), JLabel.RIGHT), c);
++
++    c.gridx = GRIDX_VAL;
++    c.weightx = WEIGHTX_VAL;
++    commitUserDataLbl.setText("?");
++    panel.add(commitUserDataLbl, c);
++
++    return panel;
++  }
++
++  private JPanel initLowerPanel() {
++    JPanel panel = new JPanel(new BorderLayout());
++    panel.setOpaque(false);
++
++    JLabel label = new JLabel(MessageUtils.getLocalizedMessage("overview.label.select_fields"));
++    label.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10));
++    panel.add(label, BorderLayout.PAGE_START);
++
++    JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, initTermCountsPanel(), initTopTermsPanel());
++    splitPane.setOpaque(false);
++    splitPane.setDividerLocation(320);
++    splitPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
++    panel.add(splitPane, BorderLayout.CENTER);
++
++    return panel;
++  }
++
++  private JPanel initTermCountsPanel() {
++    JPanel panel = new JPanel(new BorderLayout());
++    panel.setOpaque(false);
++
++    JLabel label = new JLabel(MessageUtils.getLocalizedMessage("overview.label.available_fields"));
++    label.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
++    panel.add(label, BorderLayout.PAGE_START);
++
++    TableUtils.setupTable(termCountsTable, ListSelectionModel.SINGLE_SELECTION, new TermCountsTableModel(),
++        new MouseAdapter() {
++          @Override
++          public void mouseClicked(MouseEvent e) {
++            listeners.selectField(e);
++          }
++        }, TermCountsTableModel.Column.NAME.getColumnWidth(), TermCountsTableModel.Column.TERM_COUNT.getColumnWidth());
++    JScrollPane scrollPane = new JScrollPane(termCountsTable);
++    panel.add(scrollPane, BorderLayout.CENTER);
++
++    panel.setOpaque(false);
++    return panel;
++  }
++
++  private JPanel initTopTermsPanel() {
++    JPanel panel = new JPanel(new GridLayout(1, 1));
++    panel.setOpaque(false);
++
++    JPanel selectedPanel = new JPanel(new BorderLayout());
++    selectedPanel.setOpaque(false);
++    JPanel innerPanel = new JPanel();
++    innerPanel.setOpaque(false);
++    innerPanel.setLayout(new BoxLayout(innerPanel, BoxLayout.PAGE_AXIS));
++    innerPanel.setBorder(BorderFactory.createEmptyBorder(20, 0, 0, 0));
++    selectedPanel.add(innerPanel, BorderLayout.PAGE_START);
++
++    JPanel innerPanel1 = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    innerPanel1.setOpaque(false);
++    innerPanel1.add(new JLabel(MessageUtils.getLocalizedMessage("overview.label.selected_field")));
++    innerPanel.add(innerPanel1);
++
++    selectedField.setColumns(20);
++    selectedField.setPreferredSize(new Dimension(100, 30));
++    selectedField.setFont(StyleConstants.FONT_MONOSPACE_LARGE);
++    selectedField.setEditable(false);
++    selectedField.setBackground(Color.white);
++    JPanel innerPanel2 = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    innerPanel2.setOpaque(false);
++    innerPanel2.add(selectedField);
++    innerPanel.add(innerPanel2);
++
++    showTopTermsBtn.setText(MessageUtils.getLocalizedMessage("overview.button.show_terms"));
++    showTopTermsBtn.setPreferredSize(new Dimension(170, 40));
++    showTopTermsBtn.setFont(StyleConstants.FONT_BUTTON_LARGE);
++    showTopTermsBtn.addActionListener(listeners::showTopTerms);
++    showTopTermsBtn.setEnabled(false);
++    JPanel innerPanel3 = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    innerPanel3.setOpaque(false);
++    innerPanel3.add(showTopTermsBtn);
++    innerPanel.add(innerPanel3);
++
++    JPanel innerPanel4 = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    innerPanel4.setOpaque(false);
++    innerPanel4.add(new JLabel(MessageUtils.getLocalizedMessage("overview.label.num_top_terms")));
++    innerPanel.add(innerPanel4);
++
++    SpinnerNumberModel numberModel = new SpinnerNumberModel(50, 0, 1000, 1);
++    numTopTermsSpnr.setPreferredSize(new Dimension(80, 30));
++    numTopTermsSpnr.setModel(numberModel);
++    JPanel innerPanel5 = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    innerPanel5.setOpaque(false);
++    innerPanel5.add(numTopTermsSpnr);
++    innerPanel.add(innerPanel5);
++
++    JPanel termsPanel = new JPanel(new BorderLayout());
++    termsPanel.setOpaque(false);
++    JLabel label = new JLabel(MessageUtils.getLocalizedMessage("overview.label.top_terms"));
++    label.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
++    termsPanel.add(label, BorderLayout.PAGE_START);
++
++    TableUtils.setupTable(topTermsTable, ListSelectionModel.SINGLE_SELECTION, new TopTermsTableModel(),
++        new MouseAdapter() {
++          @Override
++          public void mouseClicked(MouseEvent e) {
++            listeners.showTopTermsContextMenu(e);
++          }
++        }, TopTermsTableModel.Column.RANK.getColumnWidth(), TopTermsTableModel.Column.FREQ.getColumnWidth());
++    JScrollPane scrollPane = new JScrollPane(topTermsTable);
++    termsPanel.add(scrollPane, BorderLayout.CENTER);
++
++    JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, selectedPanel, termsPanel);
++    splitPane.setOpaque(false);
++    splitPane.setDividerLocation(180);
++    splitPane.setBorder(BorderFactory.createEmptyBorder());
++    panel.add(splitPane);
++
++    return panel;
++  }
++
++  private void setUpTopTermsContextMenu() {
++    JMenuItem item1 = new JMenuItem(MessageUtils.getLocalizedMessage("overview.toptermtable.menu.item1"));
++    item1.addActionListener(listeners::browseByTerm);
++    topTermsContextMenu.add(item1);
++
++    JMenuItem item2 = new JMenuItem(MessageUtils.getLocalizedMessage("overview.toptermtable.menu.item2"));
++    item2.addActionListener(listeners::searchByTerm);
++    topTermsContextMenu.add(item2);
++  }
++
++  // control methods
++
++  private void selectField() {
++    String field = getSelectedField();
++    selectedField.setText(field);
++    showTopTermsBtn.setEnabled(true);
++  }
++
++  private void showTopTerms() {
++    String field = getSelectedField();
++    int numTerms = (int) numTopTermsSpnr.getModel().getValue();
++    List<TermStats> termStats = overviewModel.getTopTerms(field, numTerms);
++
++    // update top terms table
++    topTermsTable.setModel(new TopTermsTableModel(termStats, numTerms));
++    topTermsTable.getColumnModel().getColumn(TopTermsTableModel.Column.RANK.getIndex()).setMaxWidth(TopTermsTableModel.Column.RANK.getColumnWidth());
++    topTermsTable.getColumnModel().getColumn(TopTermsTableModel.Column.FREQ.getIndex()).setMaxWidth(TopTermsTableModel.Column.FREQ.getColumnWidth());
++    messageBroker.clearStatusMessage();
++  }
++
++  private void browseByTerm() {
++    String field = getSelectedField();
++    String term = getSelectedTerm();
++    operatorRegistry.get(DocumentsTabOperator.class).ifPresent(operator -> {
++      operator.browseTerm(field, term);
++      tabSwitcher.switchTab(TabbedPaneProvider.Tab.DOCUMENTS);
++    });
++  }
++
++  private void searchByTerm() {
++    String field = getSelectedField();
++    String term = getSelectedTerm();
++    operatorRegistry.get(SearchTabOperator.class).ifPresent(operator -> {
++      operator.searchByTerm(field, term);
++      tabSwitcher.switchTab(TabbedPaneProvider.Tab.SEARCH);
++    });
++  }
++
++  private String getSelectedField() {
++    int selected = termCountsTable.getSelectedRow();
++    // need to convert selected row index to underlying model index
++    // https://docs.oracle.com/javase/8/docs/api/javax/swing/table/TableRowSorter.html
++    int row = termCountsTable.convertRowIndexToModel(selected);
++    if (row < 0 || row >= termCountsTable.getRowCount()) {
++      throw new IllegalStateException("Field is not selected.");
++    }
++    return (String) termCountsTable.getModel().getValueAt(row, TermCountsTableModel.Column.NAME.getIndex());
++  }
++
++  private String getSelectedTerm() {
++    int rowTerm = topTermsTable.getSelectedRow();
++    if (rowTerm < 0 || rowTerm >= topTermsTable.getRowCount()) {
++      throw new IllegalStateException("Term is not selected.");
++    }
++    return (String) topTermsTable.getModel().getValueAt(rowTerm, TopTermsTableModel.Column.TEXT.getIndex());
++  }
++
++  private class ListenerFunctions {
++
++    void selectField(MouseEvent e) {
++      OverviewPanelProvider.this.selectField();
++    }
++
++    void showTopTerms(ActionEvent e) {
++      OverviewPanelProvider.this.showTopTerms();
++    }
++
++    void showTopTermsContextMenu(MouseEvent e) {
++      if (e.getClickCount() == 2 && !e.isConsumed()) {
++        int row = topTermsTable.rowAtPoint(e.getPoint());
++        if (row != topTermsTable.getSelectedRow()) {
++          topTermsTable.changeSelection(row, topTermsTable.getSelectedColumn(), false, false);
++        }
++        topTermsContextMenu.show(e.getComponent(), e.getX(), e.getY());
++      }
++    }
++
++    void browseByTerm(ActionEvent e) {
++      OverviewPanelProvider.this.browseByTerm();
++    }
++
++    void searchByTerm(ActionEvent e) {
++      OverviewPanelProvider.this.searchByTerm();
++    }
++
++  }
++
++  private class Observer implements IndexObserver {
++
++    @Override
++    public void openIndex(LukeState state) {
++      overviewModel = overviewFactory.newInstance(state.getIndexReader(), state.getIndexPath());
++
++      indexPathLbl.setText(overviewModel.getIndexPath());
++      indexPathLbl.setToolTipText(overviewModel.getIndexPath());
++      numFieldsLbl.setText(Integer.toString(overviewModel.getNumFields()));
++      numDocsLbl.setText(Integer.toString(overviewModel.getNumDocuments()));
++      numTermsLbl.setText(Long.toString(overviewModel.getNumTerms()));
++      String del = overviewModel.hasDeletions() ? String.format(Locale.ENGLISH, "Yes (%d)", overviewModel.getNumDeletedDocs()) : "No";
++      String opt = overviewModel.isOptimized().map(b -> b ? "Yes" : "No").orElse("?");
++      delOptLbl.setText(del + " / " + opt);
++      indexVerLbl.setText(overviewModel.getIndexVersion().map(v -> Long.toString(v)).orElse("?"));
++      indexFmtLbl.setText(overviewModel.getIndexFormat().orElse(""));
++      dirImplLbl.setText(overviewModel.getDirImpl().orElse(""));
++      commitPointLbl.setText(overviewModel.getCommitDescription().orElse("---"));
++      commitUserDataLbl.setText(overviewModel.getCommitUserData().orElse("---"));
++
++      // term counts table
++      Map<String, Long> termCounts = overviewModel.getSortedTermCounts(TermCountsOrder.COUNT_DESC);
++      long numTerms = overviewModel.getNumTerms();
++      termCountsTable.setModel(new TermCountsTableModel(numTerms, termCounts));
++      termCountsTable.setRowSorter(new TableRowSorter<>(termCountsTable.getModel()));
++      termCountsTable.getColumnModel().getColumn(TermCountsTableModel.Column.NAME.getIndex()).setMaxWidth(TermCountsTableModel.Column.NAME.getColumnWidth());
++      termCountsTable.getColumnModel().getColumn(TermCountsTableModel.Column.TERM_COUNT.getIndex()).setMaxWidth(TermCountsTableModel.Column.TERM_COUNT.getColumnWidth());
++      DefaultTableCellRenderer rightRenderer = new DefaultTableCellRenderer();
++      rightRenderer.setHorizontalAlignment(JLabel.RIGHT);
++      termCountsTable.getColumnModel().getColumn(TermCountsTableModel.Column.RATIO.getIndex()).setCellRenderer(rightRenderer);
++
++      // top terms table
++      topTermsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
++      topTermsTable.getColumnModel().getColumn(TopTermsTableModel.Column.RANK.getIndex()).setMaxWidth(TopTermsTableModel.Column.RANK.getColumnWidth());
++      topTermsTable.getColumnModel().getColumn(TopTermsTableModel.Column.FREQ.getIndex()).setMaxWidth(TopTermsTableModel.Column.FREQ.getColumnWidth());
++      topTermsTable.getColumnModel().setColumnMargin(StyleConstants.TABLE_COLUMN_MARGIN_DEFAULT);
++    }
++
++    @Override
++    public void closeIndex() {
++      indexPathLbl.setText("");
++      numFieldsLbl.setText("");
++      numDocsLbl.setText("");
++      numTermsLbl.setText("");
++      delOptLbl.setText("");
++      indexVerLbl.setText("");
++      indexFmtLbl.setText("");
++      dirImplLbl.setText("");
++      commitPointLbl.setText("");
++      commitUserDataLbl.setText("");
++
++      selectedField.setText("");
++      showTopTermsBtn.setEnabled(false);
++
++      termCountsTable.setRowSorter(null);
++      termCountsTable.setModel(new TermCountsTableModel());
++      topTermsTable.setModel(new TopTermsTableModel());
++    }
++
++  }
++
++  static final class TermCountsTableModel extends TableModelBase<TermCountsTableModel.Column> {
++
++    enum Column implements TableColumnInfo {
++
++      NAME("Name", 0, String.class, 150),
++      TERM_COUNT("Term count", 1, Long.class, 100),
++      RATIO("%", 2, String.class, Integer.MAX_VALUE);
++
++      private final String colName;
++      private final int index;
++      private final Class<?> type;
++      private final int width;
++
++      Column(String colName, int index, Class<?> type, int width) {
++        this.colName = colName;
++        this.index = index;
++        this.type = type;
++        this.width = width;
++      }
++
++      @Override
++      public String getColName() {
++        return colName;
++      }
++
++      @Override
++      public int getIndex() {
++        return index;
++      }
++
++      @Override
++      public Class<?> getType() {
++        return type;
++      }
++
++      @Override
++      public int getColumnWidth() {
++        return width;
++      }
++    }
++
++    TermCountsTableModel() {
++      super();
++    }
++
++    TermCountsTableModel(double numTerms, Map<String, Long> termCounts) {
++      super(termCounts.size());
++      int i = 0;
++      for (Map.Entry<String, Long> e : termCounts.entrySet()) {
++        String term = e.getKey();
++        Long count = e.getValue();
++        data[i++] = new Object[]{term, count, String.format(Locale.ENGLISH, "%.2f %%", count / numTerms * 100)};
++      }
++    }
++
++    @Override
++    protected Column[] columnInfos() {
++      return Column.values();
++    }
++  }
++
++  static final class TopTermsTableModel extends TableModelBase<TopTermsTableModel.Column> {
++
++    enum Column implements TableColumnInfo {
++      RANK("Rank", 0, Integer.class, 50),
++      FREQ("Freq", 1, Integer.class, 80),
++      TEXT("Text", 2, String.class, Integer.MAX_VALUE);
++
++      private final String colName;
++      private final int index;
++      private final Class<?> type;
++      private final int width;
++
++      Column(String colName, int index, Class<?> type, int width) {
++        this.colName = colName;
++        this.index = index;
++        this.type = type;
++        this.width = width;
++      }
++
++      @Override
++      public String getColName() {
++        return colName;
++      }
++
++      @Override
++      public int getIndex() {
++        return index;
++      }
++
++      @Override
++      public Class<?> getType() {
++        return type;
++      }
++
++      @Override
++      public int getColumnWidth() {
++        return width;
++      }
++    }
++
++    TopTermsTableModel() {
++      super();
++    }
++
++    TopTermsTableModel(List<TermStats> termStats, int numTerms) {
++      super(Math.min(numTerms, termStats.size()));
++      for (int i = 0; i < data.length; i++) {
++        int rank = i + 1;
++        int freq = termStats.get(i).getDocFreq();
++        String termText = termStats.get(i).getDecodedTermText();
++        data[i] = new Object[]{rank, freq, termText};
++      }
++    }
++
++    @Override
++    protected Column[] columnInfos() {
++      return Column.values();
++    }
++  }
++}
+diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/SearchPanelProvider.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/SearchPanelProvider.java
+new file mode 100644
+index 00000000000..f94517a813e
+--- /dev/null
++++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/SearchPanelProvider.java
+@@ -0,0 +1,834 @@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements.  See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++
++package org.apache.lucene.luke.app.desktop.components;
++
++import javax.swing.BorderFactory;
++import javax.swing.JButton;
++import javax.swing.JCheckBox;
++import javax.swing.JFormattedTextField;
++import javax.swing.JLabel;
++import javax.swing.JMenuItem;
++import javax.swing.JPanel;
++import javax.swing.JPopupMenu;
++import javax.swing.JScrollPane;
++import javax.swing.JSeparator;
++import javax.swing.JSplitPane;
++import javax.swing.JTabbedPane;
++import javax.swing.JTable;
++import javax.swing.JTextArea;
++import javax.swing.ListSelectionModel;
++import java.awt.BorderLayout;
++import java.awt.Color;
++import java.awt.Dimension;
++import java.awt.FlowLayout;
++import java.awt.GridBagConstraints;
++import java.awt.GridBagLayout;
++import java.awt.GridLayout;
++import java.awt.Insets;
++import java.awt.event.ActionEvent;
++import java.awt.event.MouseAdapter;
++import java.awt.event.MouseEvent;
++import java.io.IOException;
++import java.util.Arrays;
++import java.util.Collections;
++import java.util.List;
++import java.util.Locale;
++import java.util.Objects;
++import java.util.Set;
++import java.util.stream.Collectors;
++
++import org.apache.lucene.analysis.Analyzer;
++import org.apache.lucene.analysis.standard.StandardAnalyzer;
++import org.apache.lucene.index.Term;
++import org.apache.lucene.luke.app.IndexHandler;
++import org.apache.lucene.luke.app.IndexObserver;
++import org.apache.lucene.luke.app.LukeState;
++import org.apache.lucene.luke.app.desktop.MessageBroker;
++import org.apache.lucene.luke.app.desktop.components.dialog.ConfirmDialogFactory;
++import org.apache.lucene.luke.app.desktop.components.dialog.search.ExplainDialogFactory;
++import org.apache.lucene.luke.app.desktop.components.fragments.search.AnalyzerPaneProvider;
++import org.apache.lucene.luke.app.desktop.components.fragments.search.FieldValuesPaneProvider;
++import org.apache.lucene.luke.app.desktop.components.fragments.search.FieldValuesTabOperator;
++import org.apache.lucene.luke.app.desktop.components.fragments.search.MLTPaneProvider;
++import org.apache.lucene.luke.app.desktop.components.fragments.search.MLTTabOperator;
++import org.apache.lucene.luke.app.desktop.components.fragments.search.QueryParserPaneProvider;
++import org.apache.lucene.luke.app.desktop.components.fragments.search.QueryParserTabOperator;
++import org.apache.lucene.luke.app.desktop.components.fragments.search.SimilarityPaneProvider;
++import org.apache.lucene.luke.app.desktop.components.fragments.search.SimilarityTabOperator;
++import org.apache.lucene.luke.app.desktop.components.fragments.search.SortPaneProvider;
++import org.apache.lucene.luke.app.desktop.components.fragments.search.SortTabOperator;
++import org.apache.lucene.luke.app.desktop.util.DialogOpener;
++import org.apache.lucene.luke.app.desktop.util.FontUtils;
++import org.apache.lucene.luke.app.desktop.util.MessageUtils;
++import org.apache.lucene.luke.app.desktop.util.StringUtils;
++import org.apache.lucene.luke.app.desktop.util.StyleConstants;
++import org.apache.lucene.luke.app.desktop.util.TabUtils;
++import org.apache.lucene.luke.app.desktop.util.TableUtils;
++import org.apache.lucene.luke.models.LukeException;
++import org.apache.lucene.luke.models.search.MLTConfig;
++import org.apache.lucene.luke.models.search.QueryParserConfig;
++import org.apache.lucene.luke.models.search.Search;
++import org.apache.lucene.luke.models.search.SearchFactory;
++import org.apache.lucene.luke.models.search.SearchResults;
++import org.apache.lucene.luke.models.search.SimilarityConfig;
++import org.apache.lucene.luke.models.tools.IndexTools;
++import org.apache.lucene.luke.models.tools.IndexToolsFactory;
++import org.apache.lucene.search.Explanation;
++import org.apache.lucene.search.Query;
++import org.apache.lucene.search.Sort;
++import org.apache.lucene.search.TermQuery;
++import org.apache.lucene.search.TotalHits;
++
++/** Provider of the Search panel */
++public final class SearchPanelProvider implements SearchTabOperator {
++
++  private static final int DEFAULT_PAGE_SIZE = 10;
++
++  private final SearchFactory searchFactory;
++
++  private final IndexToolsFactory toolsFactory;
++
++  private final IndexHandler indexHandler;
++
++  private final MessageBroker messageBroker;
++
++  private final TabSwitcherProxy tabSwitcher;
++
++  private final ComponentOperatorRegistry operatorRegistry;
++
++  private final ConfirmDialogFactory confirmDialogFactory;
++
++  private final ExplainDialogFactory explainDialogProvider;
++
++  private final JTabbedPane tabbedPane = new JTabbedPane();
++
++  private final JScrollPane qparser;
++
++  private final JScrollPane analyzer;
++
++  private final JScrollPane similarity;
++
++  private final JScrollPane sort;
++
++  private final JScrollPane values;
++
++  private final JScrollPane mlt;
++
++  private final JCheckBox termQueryCB = new JCheckBox();
++
++  private final JTextArea queryStringTA = new JTextArea();
++
++  private final JTextArea parsedQueryTA = new JTextArea();
++
++  private final JButton parseBtn = new JButton();
++
++  private final JCheckBox rewriteCB = new JCheckBox();
++
++  private final JButton searchBtn = new JButton();
++
++  private JCheckBox exactHitsCntCB = new JCheckBox();
++
++  private final JButton mltBtn = new JButton();
++
++  private final JFormattedTextField mltDocFTF = new JFormattedTextField();
++
++  private final JLabel totalHitsLbl = new JLabel();
++
++  private final JLabel startLbl = new JLabel();
++
++  private final JLabel endLbl = new JLabel();
++
++  private final JButton prevBtn = new JButton();
++
++  private final JButton nextBtn = new JButton();
++
++  private final JButton delBtn = new JButton();
++
++  private final JTable resultsTable = new JTable();
++
++  private final ListenerFunctions listeners = new ListenerFunctions();
++
++  private Search searchModel;
++
++  private IndexTools toolsModel;
++
++  public SearchPanelProvider() throws IOException {
++    this.searchFactory = new SearchFactory();
++    this.toolsFactory = new IndexToolsFactory();
++    this.indexHandler = IndexHandler.getInstance();
++    this.messageBroker = MessageBroker.getInstance();
++    this.tabSwitcher = TabSwitcherProxy.getInstance();
++    this.operatorRegistry = ComponentOperatorRegistry.getInstance();
++    this.confirmDialogFactory = ConfirmDialogFactory.getInstance();
++    this.explainDialogProvider = ExplainDialogFactory.getInstance();
++    this.qparser = new QueryParserPaneProvider().get();
++    this.analyzer = new AnalyzerPaneProvider().get();
++    this.similarity = new SimilarityPaneProvider().get();
++    this.sort = new SortPaneProvider().get();
++    this.values = new FieldValuesPaneProvider().get();
++    this.mlt = new MLTPaneProvider().get();
++
++    indexHandler.addObserver(new Observer());
++    operatorRegistry.register(SearchTabOperator.class, this);
++  }
++
++  public JPanel get() {
++    JPanel panel = new JPanel(new GridLayout(1, 1));
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createLineBorder(Color.gray));
++
++    JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, initUpperPanel(), initLowerPanel());
++    splitPane.setOpaque(false);
++    splitPane.setDividerLocation(350);
++    panel.add(splitPane);
++
++    return panel;
++  }
++
++  private JSplitPane initUpperPanel() {
++    JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, initQuerySettingsPane(), initQueryPane());
++    splitPane.setOpaque(false);
++    splitPane.setDividerLocation(570);
++    return splitPane;
++  }
++
++  private JPanel initQuerySettingsPane() {
++    JPanel panel = new JPanel(new BorderLayout());
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
++
++    JLabel label = new JLabel(MessageUtils.getLocalizedMessage("search.label.settings"));
++    panel.add(label, BorderLayout.PAGE_START);
++
++    tabbedPane.addTab("Query Parser", qparser);
++    tabbedPane.addTab("Analyzer", analyzer);
++    tabbedPane.addTab("Similarity", similarity);
++    tabbedPane.addTab("Sort", sort);
++    tabbedPane.addTab("Field Values", values);
++    tabbedPane.addTab("More Like This", mlt);
++
++    TabUtils.forceTransparent(tabbedPane);
++
++    panel.add(tabbedPane, BorderLayout.CENTER);
++
++    return panel;
++  }
++
++  private JPanel initQueryPane() {
++    JPanel panel = new JPanel(new GridBagLayout());
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
++    GridBagConstraints c = new GridBagConstraints();
++    c.fill = GridBagConstraints.HORIZONTAL;
++    c.anchor = GridBagConstraints.LINE_START;
++
++    JLabel labelQE = new JLabel(MessageUtils.getLocalizedMessage("search.label.expression"));
++    c.gridx = 0;
++    c.gridy = 0;
++    c.gridwidth = 2;
++    c.weightx = 0.5;
++    c.insets = new Insets(2, 0, 2, 2);
++    panel.add(labelQE, c);
++
++    termQueryCB.setText(MessageUtils.getLocalizedMessage("search.checkbox.term"));
++    termQueryCB.addActionListener(listeners::toggleTermQuery);
++    termQueryCB.setOpaque(false);
++    c.gridx = 2;
++    c.gridy = 0;
++    c.gridwidth = 1;
++    c.weightx = 0.2;
++    c.insets = new Insets(2, 0, 2, 2);
++    panel.add(termQueryCB, c);
++
++    queryStringTA.setRows(4);
++    queryStringTA.setLineWrap(true);
++    queryStringTA.setText("*:*");
++    c.gridx = 0;
++    c.gridy = 1;
++    c.gridwidth = 3;
++    c.weightx = 0.0;
++    c.insets = new Insets(2, 0, 2, 2);
++    panel.add(new JScrollPane(queryStringTA, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), c);
++
++    JLabel labelPQ = new JLabel(MessageUtils.getLocalizedMessage("search.label.parsed"));
++    c.gridx = 0;
++    c.gridy = 2;
++    c.gridwidth = 3;
++    c.weightx = 0.0;
++    c.insets = new Insets(8, 0, 2, 2);
++    panel.add(labelPQ, c);
++
++    parsedQueryTA.setRows(4);
++    parsedQueryTA.setLineWrap(true);
++    parsedQueryTA.setEditable(false);
++    c.gridx = 0;
++    c.gridy = 3;
++    c.gridwidth = 3;
++    c.weightx = 0.0;
++    c.insets = new Insets(2, 0, 2, 2);
++    panel.add(new JScrollPane(parsedQueryTA), c);
++
++    parseBtn.setText(FontUtils.elegantIconHtml("&#xe0df;", MessageUtils.getLocalizedMessage("search.button.parse")));
++    parseBtn.setFont(StyleConstants.FONT_BUTTON_LARGE);
++    parseBtn.setMargin(new Insets(3, 0, 3, 0));
++    parseBtn.addActionListener(listeners::execParse);
++    c.gridx = 0;
++    c.gridy = 4;
++    c.gridwidth = 1;
++    c.weightx = 0.2;
++    c.insets = new Insets(5, 0, 0, 2);
++    panel.add(parseBtn, c);
++
++    rewriteCB.setText(MessageUtils.getLocalizedMessage("search.checkbox.rewrite"));
++    rewriteCB.setOpaque(false);
++    c.gridx = 1;
++    c.gridy = 4;
++    c.gridwidth = 2;
++    c.weightx = 0.2;
++    c.insets = new Insets(5, 0, 0, 2);
++    panel.add(rewriteCB, c);
++
++    searchBtn.setText(FontUtils.elegantIconHtml("&#x55;", MessageUtils.getLocalizedMessage("search.button.search")));
++    searchBtn.setFont(StyleConstants.FONT_BUTTON_LARGE);
++    searchBtn.setMargin(new Insets(3, 0, 3, 0));
++    searchBtn.addActionListener(listeners::execSearch);
++    c.gridx = 0;
++    c.gridy = 5;
++    c.gridwidth = 1;
++    c.weightx = 0.2;
++    c.insets = new Insets(5, 0, 5, 0);
++    panel.add(searchBtn, c);
++
++    exactHitsCntCB.setText(MessageUtils.getLocalizedMessage("search.checkbox.exact_hits_cnt"));
++    exactHitsCntCB.setOpaque(false);
++    c.gridx = 1;
++    c.gridy = 5;
++    c.gridwidth = 2;
++    c.weightx = 0.2;
++    c.insets = new Insets(5, 0, 0, 2);
++    panel.add(exactHitsCntCB, c);
++
++    mltBtn.setText(FontUtils.elegantIconHtml("&#xe030;", MessageUtils.getLocalizedMessage("search.button.mlt")));
++    mltBtn.setFont(StyleConstants.FONT_BUTTON_LARGE);
++    mltBtn.setMargin(new Insets(3, 0, 3, 0));
++    mltBtn.addActionListener(listeners::execMLTSearch);
++    c.gridx = 0;
++    c.gridy = 6;
++    c.gridwidth = 1;
++    c.weightx = 0.3;
++    c.insets = new Insets(10, 0, 2, 0);
++    panel.add(mltBtn, c);
++
++    JPanel docNo = new JPanel(new FlowLayout(FlowLayout.LEADING));
++    docNo.setOpaque(false);
++    JLabel docNoLabel = new JLabel("with doc #");
++    docNo.add(docNoLabel);
++    mltDocFTF.setColumns(8);
++    mltDocFTF.setValue(0);
++    docNo.add(mltDocFTF);
++    c.gridx = 1;
++    c.gridy = 6;
++    c.gridwidth = 2;
++    c.weightx = 0.3;
++    c.insets = new Insets(8, 0, 0, 2);
++    panel.add(docNo, c);
++
++    return panel;
++  }
++
++  private JPanel initLowerPanel() {
++    JPanel panel = new JPanel(new BorderLayout());
++    panel.setOpaque(false);
++    panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
++
++    panel.add(initSearchResultsHeaderPane(), BorderLayout.PAGE_START);
++    panel.add(initSearchResultsTablePane(), BorderLayout.CENTER);
++
++    return panel;
++  }
++
++  private JPanel initSearchResultsHeaderPane() {
++    JPanel panel = new JPanel(new GridLayout(1, 2));
++    panel.setOpaque(false);
++
++    JLabel label = new JLabel(FontUtils.elegantIconHtml("&#xe025;", MessageUtils.getLocalizedMessage("search.label.results")));
++    label.setHorizontalTextPosition(JLabel.LEFT);
++    label.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0));
++    panel.add(label);
++
++    JPanel resultsInfo = new JPanel(new FlowLayout(FlowLayout.TRAILING));
++    resultsInfo.setOpaque(false);
++    resultsInfo.setOpaque(false);
++
++    JLabel totalLabel = new JLabel(MessageUtils.getLocalizedMessage("search.label.total"));
++    resultsInfo.add(totalLabel);
++
++    totalHitsLbl.setText("?");
++    resultsInfo.add(totalHitsLbl);
++
++    prevBtn.setText(FontUtils.elegantIconHtml("&#x44;"));
++    prevBtn.setMargin(new Insets(5, 0, 5, 0));
++    prevBtn.setPreferredSize(new Dimension(30, 20));
++    prevBtn.setEnabled(false);
++    prevBtn.addActionListener(listeners::prevPage);
++    resultsInfo.add(prevBtn);
++
++    startLbl.setText("0");
++    resultsInfo.add(startLbl);
++
++    resultsInfo.add(new JLabel(" ~ "));
++
++    endLbl.setText("0");
++    resultsInfo.add(endLbl);
++
++    nextBtn.setText(FontUtils.elegantIconHtml("&#x45;"));
++    nextBtn.setMargin(new Insets(3, 0, 3, 0));
++    nextBtn.setPreferredSize(new Dimension(30, 20));
++    nextBtn.setEnabled(false);
++    nextBtn.addActionListener(listeners::nextPage);
++    resultsInfo.add(nextBtn);
++
++    JSeparator sep = new JSeparator(JSeparator.VERTICAL);
++    sep.setPreferredSize(new Dimension(5, 1));
++    resultsInfo.add(sep);
++
++    delBtn.setText(FontUtils.elegantIconHtml("&#xe07d;", MessageUtils.getLocalizedMessage("search.button.del_all")));
++    delBtn.setMargin(new Insets(5, 0, 5, 0));
++    delBtn.setEnabled(false);
++    delBtn.addActionListener(listeners::confirmDeletion);
++    resultsInfo.add(delBtn);
++
++    panel.add(resultsInfo, BorderLayout.CENTER);
++
++    return panel;
++  }
++
++  private JPanel initSearchResultsTablePane() {
++    JPanel panel = new JPanel(new BorderLayout());
++    panel.setOpaque(false);
++
++    JPanel note = new JPanel(new FlowLayout(FlowLayout.LEADING, 5, 2));
++    note.setOpaque(false);
++    note.add(new JLabel(MessageUtils.getLocalizedMessage("search.label.results.note")));
++    panel.add(note, BorderLayout.PAGE_START);
++
++    TableUtils.setupTable(resultsTable, ListSelectionModel.SINGLE_SELECTION, new SearchResultsTableModel(),
++        new MouseAdapter() {
++          @Override
++          public void mousePressed(MouseEvent e) {
++            listeners.showContextMenuInResultsTable(e);
++          }
++        },
++        SearchResultsTableModel.Column.DOCID.getColumnWidth(),
++        SearchResultsTableModel.Column.SCORE.getColumnWidth());
++    JScrollPane scrollPane = new JScrollPane(resultsTable);
++    panel.add(scrollPane, BorderLayout.CENTER);
++
++    return panel;
++  }
++
++  // control methods
++
++  private void toggleTermQuery() {
++    if (termQueryCB.isSelected()) {
++      enableTermQuery();
++    } else {
++      disableTermQuery();
++    }
++  }
++
++  private void enableTermQuery() {
++    tabbedPane.setEnabledAt(Tab.QPARSER.index(), false);
++    tabbedPane.setEnabledAt(Tab.ANALYZER.index(), false);
++    tabbedPane.setEnabledAt(Tab.SIMILARITY.index(), false);
++    if (tabbedPane.getSelectedIndex() == Tab.QPARSER.index() ||
++        tabbedPane.getSelectedIndex() == Tab.ANALYZER.index() ||
++        tabbedPane.getSelectedIndex() == Tab.SIMILARITY.index() ||
++        tabbedPane.getSelectedIndex() == Tab.MLT.index()) {
++      tabbedPane.setSelectedIndex(Tab.SORT.index());
++    }
++    parseBtn.setEnabled(false);
++    rewriteCB.setEnabled(false);
++  }
++
++  private void disableTermQuery() {
++    tabbedPane.setEnabledAt(Tab.QPARSER.index(), true);
++    tabbedPane.setEnabledAt(Tab.ANALYZER.index(), true);
++    tabbedPane.setEnabledAt(Tab.SIMILARITY.index(), true);
++    parseBtn.setEnabled(true);
++    rewriteCB.setEnabled(true);
++  }
++
++  private void execParse() {
++    Query query = parse(rewriteCB.isSelected());
++    parsedQueryTA.setText(query.toString());
++    messageBroker.clearStatusMessage();
++  }
++
++  private void doSearch() {
++    Query query;
++    if (termQueryCB.isSelected()) {
++      // term query
++      if (StringUtils.isNullOrEmpty(queryStringTA.getText())) {
++        throw new LukeException("Query is not set.");
++      }
++      String[] tmp = queryStringTA.getText().split(":");
++      if (tmp.length < 2) {
++        throw new LukeException(String.format(Locale.ENGLISH, "Invalid query [ %s ]", queryStringTA.getText()));
++      }
++      query = new TermQuery(new Term(tmp[0].trim(), tmp[1].trim()));
++    } else {
++      query = parse(false);
++    }
++    SimilarityConfig simConfig = operatorRegistry.get(SimilarityTabOperator.class)
++        .map(SimilarityTabOperator::getConfig)
++        .orElse(new SimilarityConfig.Builder().build());
++    Sort sort = operatorRegistry.get(SortTabOperator.class)
++        .map(SortTabOperator::getSort)
++        .orElse(null);
++    Set<String> fieldsToLoad = operatorRegistry.get(FieldValuesTabOperator.class)
++        .map(FieldValuesTabOperator::getFieldsToLoad)
++        .orElse(Collections.emptySet());
++    SearchResults results = searchModel.search(query, simConfig, sort, fieldsToLoad, DEFAULT_PAGE_SIZE, exactHitsCntCB.isSelected());
++
++    TableUtils.setupTable(resultsTable, ListSelectionModel.SINGLE_SELECTION, new SearchResultsTableModel(), null,
++        SearchResultsTableModel.Column.DOCID.getColumnWidth(),
++        SearchResultsTableModel.Column.SCORE.getColumnWidth());
++    populateResults(results);
++
++    messageBroker.clearStatusMessage();
++  }
++
++  private void nextPage() {
++    searchModel.nextPage().ifPresent(this::populateResults);
++    messageBroker.clearStatusMessage();
++  }
++
++  private void prevPage() {
++    searchModel.prevPage().ifPresent(this::populateResults);
++    messageBroker.clearStatusMessage();
++  }
++
++  private void doMLTSearch() {
++    if (Objects.isNull(mltDocFTF.getValue())) {
++      throw new LukeException("Doc num is not set.");
++    }
++    int docNum = (int) mltDocFTF.getValue();
++    MLTConfig mltConfig = operatorRegistry.get(MLTTabOperator.class)
++        .map(MLTTabOperator::getConfig)
++        .orElse(new MLTConfig.Builder().build());
++    Analyzer analyzer = operatorRegistry.get(AnalysisTabOperator.class)
++        .map(AnalysisTabOperator::getCurrentAnalyzer)
++        .orElse(new StandardAnalyzer());
++    Query query = searchModel.mltQuery(docNum, mltConfig, analyzer);
++    Set<String> fieldsToLoad = operatorRegistry.get(FieldValuesTabOperator.class)
++        .map(FieldValuesTabOperator::getFieldsToLoad)
++        .orElse(Collections.emptySet());
++    SearchResults results = searchModel.search(query, new SimilarityConfig.Builder().build(), fieldsToLoad, DEFAULT_PAGE_SIZE, false);
++
++    TableUtils.setupTable(resultsTable, ListSelectionModel.SINGLE_SELECTION, new SearchResultsTableModel(), null,
++        SearchResultsTableModel.Column.DOCID.getColumnWidth(),
++        SearchResultsTableModel.Column.SCORE.getColumnWidth());
++    populateResults(results);
++
++    messageBroker.clearStatusMessage();
++  }
++
++  private Query parse(boolean rewrite) {
++    String expr = StringUtils.isNullOrEmpty(queryStringTA.getText()) ? "*:*" : queryStringTA.getText();
++    String df = operatorRegistry.get(QueryParserTabOperator.class)
++        .map(QueryParserTabOperator::getDefaultField)
++        .orElse("");
++    QueryParserConfig config = operatorRegistry.get(QueryParserTabOperator.class)
++        .map(QueryParserTabOperator::getConfig)
++        .orElse(new QueryParserConfig.Builder().build());
++    Analyzer analyzer = operatorRegistry.get(AnalysisTabOperator.class)
++        .map(AnalysisTabOperator::getCurrentAnalyzer)
++        .orElse(new StandardAnalyzer());
++    return searchModel.parseQuery(expr, df, analyzer, config, rewrite);
++  }
++
++  private void populateResults(SearchResults res) {
++    totalHitsLbl.setText(String.valueOf(res.getTotalHits()));
++    if (res.getTotalHits().value > 0) {
++      startLbl.setText(String.valueOf(res.getOffset() + 1));
++      endLbl.setText(String.valueOf(res.getOffset() + res.size()));
++
++      prevBtn.setEnabled(res.getOffset() > 0);
++      nextBtn.setEnabled(res.getTotalHits().relation == TotalHits.Relation.GREATER_THAN_OR_EQUAL_TO || res.getTotalHits().value > res.getOffset() + res.size());
++
++      if (!indexHandler.getState().readOnly() && indexHandler.getState().hasDirectoryReader()) {
++        delBtn.setEnabled(true);
++      }
++
++      resultsTable.setModel(new SearchResultsTableModel(res));
++      resultsTable.getColumnModel().getColumn(SearchResultsTableModel.Column.DOCID.getIndex()).setPreferredWidth(SearchResultsTableModel.Column.DOCID.getColumnWidth());
++      resultsTable.getColumnModel().getColumn(SearchResultsTableModel.Column.SCORE.getIndex()).setPreferredWidth(SearchResultsTableModel.Column.SCORE.getColumnWidth());
++      resultsTable.getColumnModel().getColumn(SearchResultsTableModel.Column.VALUE.getIndex()).setPreferredWidth(SearchResultsTableModel.Column.VALUE.getColumnWidth());
++    } else {
++      startLbl.setText("0");
++      endLbl.setText("0");
++      prevBtn.setEnabled(false);
++      nextBtn.setEnabled(false);
++      delBtn.setEnabled(false);
++    }
++  }
++
++  private void confirmDeletion() {
++    new DialogOpener<>(confirmDialogFactory).open("Confirm Deletion", 400, 200, (factory) -> {
++      factory.setMessage(MessageUtils.getLocalizedMessage("search.message.delete_confirm"));
++      factory.setCallback(this::deleteDocs);
++    });
++  }
++
++  private void deleteDocs() {
++    Query query = searchModel.getCurrentQuery();
++    if (query != null) {
++      toolsModel.deleteDocuments(query);
++      indexHandler.reOpen();
++      messageBroker.showStatusMessage(MessageUtils.getLocalizedMessage("search.message.delete_success", query.toString()));
++    }
++    delBtn.setEnabled(false);
++  }
++
++  private JPopupMenu setupResultsContextMenuPopup() {
++    JPopupMenu popup = new JPopupMenu();
++
++    // show explanation
++    JMenuItem item1 = new JMenuItem(MessageUtils.getLocalizedMessage("search.results.menu.explain"));
++    item1.addActionListener(e -> {
++      int docid = (int) resultsTable.getModel().getValueAt(resultsTable.getSelectedRow(), SearchResultsTableModel.Column.DOCID.getIndex());
++      Explanation explanation = searchModel.explain(parse(false), docid);
++      new DialogOpener<>(explainDialogProvider).open("Explanation", 600, 400,
++          (factory) -> {
++            factory.setDocid(docid);
++            factory.setExplanation(explanation);
++          });
++    });
++    popup.add(item1);
++
++    // show all fields
++    JMenuItem item2 = new JMenuItem(MessageUtils.getLocalizedMessage("search.results.menu.showdoc"));
++    item2.addActionListener(e -> {
++      int docid = (int) resultsTable.getModel().getValueAt(resultsTable.getSelectedRow(), SearchResultsTableModel.Column.DOCID.getIndex());
++      operatorRegistry.get(DocumentsTabOperator.class).ifPresent(operator -> operator.displayDoc(docid));
++      tabSwitcher.switchTab(TabbedPaneProvider.Tab.DOCUMENTS);
++    });
++    popup.add(item2);
++
++    return popup;
++  }
++
++  @Override
++  public void searchByTerm(String field, String term) {
++    termQueryCB.setSelected(true);
++    enableTermQuery();
++    queryStringTA.setText(field + ":" + term);
++    doSearch();
++  }
++
++  @Override
++  public void mltSearch(int docNum) {
++    mltDocFTF.setValue(docNum);
++    doMLTSearch();
++    tabbedPane.setSelectedIndex(Tab.MLT.index());
++  }
++
++  @Override
++  public void enableExactHitsCB(boolean value) {
++    exactHitsCntCB.setEnabled(value);
++  }
++
++  @Override
++  public void setExactHits(boolean value) {
++    exactHitsCntCB.setSelected(value);
++  }
++
++  private class ListenerFunctions {
++
++    void toggleTermQuery(ActionEvent e) {
++      SearchPanelProvider.this.toggleTermQuery();
++    }
++
++    void execParse(ActionEvent e) {
++      SearchPanelProvider.this.execParse();
++    }
++
++    void execSearch(ActionEvent e) {
++      SearchPanelProvider.this.doSearch();
++    }
++
++    void nextPage(ActionEvent e) {
++      SearchPanelProvider.this.nextPage();
++    }
++
++    void prevPage(ActionEvent e) {
++      SearchPanelProvider.this.prevPage();
++    }
++
++    void execMLTSearch(ActionEvent e) {
++      SearchPanelProvider.this.doMLTSearch();
++    }
++
++    void confirmDeletion(ActionEvent e) {
++      SearchPanelProvider.this.confirmDeletion();
++    }
++
++    void showContextMenuInResultsTable(MouseEvent e) {
++      if (e.getClickCount() == 2 && !e.isConsumed()) {
++        SearchPanelProvider.this.setupResultsContextMenuPopup().show(e.getComponent(), e.getX(), e.getY());
++        setupResultsContextMenuPopup().show(e.getComponent(), e.getX(), e.getY());
++      }
++    }
++
++  }
++
++  private class Observer implements IndexObserver {
++
++    @Override
++    public void openIndex(LukeState state) {
++      searchModel = searchFactory.newInstance(state.getIndexReader());
++      toolsModel = toolsFactory.newInstance(state.getIndexReader(), state.useCompound(), state.keepAllCommits());
++      operatorRegistry.get(QueryParserTabOperator.class).ifPresent(operator -> {
++        operator.setSearchableFields(searchModel.getSearchableFieldNames());
++        operator.setRangeSearchableFields(searchModel.getRangeSearchableFieldNames());
++      });
++      operatorRegistry.get(SortTabOperator.class).ifPresent(operator -> {
++        operator.setSearchModel(searchModel);
++        operator.setSortableFields(searchModel.getSortableFieldNames());
++      });
++      operatorRegistry.get(FieldValuesTabOperator.class).ifPresent(operator -> {
++        operator.setFields(searchModel.getFieldNames());
++      });
++      operatorRegistry.get(MLTTabOperator.class).ifPresent(operator -> {
++        operator.setFields(searchModel.getFieldNames());
++      });
++
++      queryStringTA.setText("*:*");
++      parsedQueryTA.setText("");
++      parseBtn.setEnabled(true);
++      searchBtn.setEnabled(true);
++      mltBtn.setEnabled(true);
++    }
++
++    @Override
++    public void closeIndex() {
++      searchModel = null;
++      toolsModel = null;
++
++      queryStringTA.setText("");
++      parsedQueryTA.setText("");
++      parseBtn.setEnabled(false);
++      searchBtn.setEnabled(false);
++      mltBtn.setEnabled(false);
++      totalHitsLbl.setText("0");
++      startLbl.setText("0");
++      endLbl.setText("0");
++      nextBtn.setEnabled(false);
++      prevBtn.setEnabled(false);
++      delBtn.setEnabled(false);
++      TableUtils.setupTable(resultsTable, ListSelectionModel.SINGLE_SELECTION, new SearchResultsTableModel(), null,
++          SearchResultsTableModel.Column.DOCID.getColumnWidth(),
++          SearchResultsTableModel.Column.SCORE.getColumnWidth());
++    }
++
++  }
++
++  /** tabs in the Search panel */
++  public enum Tab {
++    QPARSER(0), ANALYZER(1), SIMILARITY(2), SORT(3), VALUES(4), MLT(5);
++
++    private int tabIdx;
++
++    Tab(int tabIdx) {
++      this.tabIdx = tabIdx;
... 20756 lines suppressed ...