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:25 UTC

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

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 ...