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("", 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("8", 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("8", 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("h", 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("", 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("", MessageUtils.getLocalizedMessage("documents.button.mlt")));
++ mltBtn.setMargin(new Insets(5, 0, 5, 0));
++ mltBtn.addActionListener(listeners::mltSearch);
++ right.add(mltBtn);
++ addDocBtn.setText(FontUtils.elegantIconHtml("Y", 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(""));
++ multiIcon.setToolTipText(MessageUtils.getLocalizedMessage("tooltip.multi_reader"));
++ multiIcon.setVisible(false);
++ iconPanel.add(multiIcon);
++
++
++ readOnlyIcon.setText(FontUtils.elegantIconHtml(""));
++ readOnlyIcon.setToolTipText(MessageUtils.getLocalizedMessage("tooltip.read_only"));
++ readOnlyIcon.setVisible(false);
++ iconPanel.add(readOnlyIcon);
++
++ noReaderIcon.setText(FontUtils.elegantIconHtml(""));
++ 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("", 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("U", 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("", 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("", 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("D"));
++ 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("E"));
++ 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("", 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 ...