tvEntries) {
+ super(tvEntries.size());
+
+ for (int i = 0; i < tvEntries.size(); i++) {
+ TermVectorEntry entry = tvEntries.get(i);
+
+ String termText = entry.getTermText();
+ long freq = tvEntries.get(i).getFreq();
+ String positions = String.join(",",
+ entry.getPositions().stream()
+ .map(pos -> Integer.toString(pos.getPosition()))
+ .collect(Collectors.toList()));
+ String offsets = String.join(",",
+ entry.getPositions().stream()
+ .filter(pos -> pos.getStartOffset().isPresent() && pos.getEndOffset().isPresent())
+ .map(pos -> Integer.toString(pos.getStartOffset().orElse(-1)) + "-" + Integer.toString(pos.getEndOffset().orElse(-1)))
+ .collect(Collectors.toList())
+ );
+
+ data[i] = new Object[]{termText, freq, positions, offsets};
+ }
+
+ }
+
+ @Override
+ protected Column[] columnInfos() {
+ return Column.values();
+ }
+ }
+}
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/documents/package-info.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/documents/package-info.java
new file mode 100644
index 00000000000..9c641f99469
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/documents/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Dialogs used in the Documents tab */
+package org.apache.lucene.luke.app.desktop.components.dialog.documents;
\ No newline at end of file
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/AboutDialogFactory.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/AboutDialogFactory.java
new file mode 100644
index 00000000000..e9d9c9731a6
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/AboutDialogFactory.java
@@ -0,0 +1,200 @@
+/*
+ * 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.dialog.menubar;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JEditorPane;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.ScrollPaneConstants;
+import javax.swing.SwingUtilities;
+import javax.swing.event.HyperlinkEvent;
+import javax.swing.event.HyperlinkListener;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Desktop;
+import java.awt.Dialog;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.Window;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Objects;
+
+import org.apache.lucene.LucenePackage;
+import org.apache.lucene.luke.app.desktop.Preferences;
+import org.apache.lucene.luke.app.desktop.PreferencesFactory;
+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.ImageUtils;
+import org.apache.lucene.luke.app.desktop.util.MessageUtils;
+import org.apache.lucene.luke.app.desktop.util.URLLabel;
+import org.apache.lucene.luke.models.LukeException;
+
+/** Factory of about dialog */
+public final class AboutDialogFactory implements DialogOpener.DialogFactory {
+
+ private static AboutDialogFactory instance;
+
+ private final Preferences prefs;
+
+ private JDialog dialog;
+
+ public synchronized static AboutDialogFactory getInstance() throws IOException {
+ if (instance == null) {
+ instance = new AboutDialogFactory();
+ }
+ return instance;
+ }
+
+ private AboutDialogFactory() throws IOException {
+ this.prefs = PreferencesFactory.getInstance();
+ }
+
+ @Override
+ public JDialog create(Window owner, String title, int width, int height) {
+ dialog = new JDialog(owner, title, Dialog.ModalityType.APPLICATION_MODAL);
+ dialog.add(content());
+ dialog.setSize(new Dimension(width, height));
+ dialog.setLocationRelativeTo(owner);
+ dialog.getContentPane().setBackground(prefs.getColorTheme().getBackgroundColor());
+ return dialog;
+ }
+
+ private JPanel content() {
+ JPanel panel = new JPanel(new BorderLayout());
+ panel.setOpaque(false);
+ panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
+
+ panel.add(header(), BorderLayout.PAGE_START);
+ panel.add(center(), BorderLayout.CENTER);
+ panel.add(footer(), BorderLayout.PAGE_END);
+
+ return panel;
+ }
+
+ private JPanel header() {
+ JPanel panel = new JPanel(new GridLayout(3, 1));
+ panel.setOpaque(false);
+
+ JPanel logo = new JPanel(new FlowLayout(FlowLayout.CENTER));
+ logo.setOpaque(false);
+ logo.add(new JLabel(ImageUtils.createImageIcon("luke-logo.gif", 200, 40)));
+ panel.add(logo);
+
+ JPanel project = new JPanel(new FlowLayout(FlowLayout.CENTER));
+ project.setOpaque(false);
+ JLabel projectLbl = new JLabel("Lucene Toolbox Project");
+ projectLbl.setFont(new Font(projectLbl.getFont().getFontName(), Font.BOLD, 32));
+ projectLbl.setForeground(Color.decode("#5aaa88"));
+ project.add(projectLbl);
+ panel.add(project);
+
+ JPanel desc = new JPanel();
+ desc.setOpaque(false);
+ desc.setLayout(new BoxLayout(desc, BoxLayout.PAGE_AXIS));
+
+ JPanel subTitle = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 5));
+ subTitle.setOpaque(false);
+ JLabel subTitleLbl = new JLabel("GUI client of the best Java search library Apache Lucene");
+ subTitleLbl.setFont(new Font(subTitleLbl.getFont().getFontName(), Font.PLAIN, 20));
+ subTitle.add(subTitleLbl);
+ subTitle.add(new JLabel(ImageUtils.createImageIcon("lucene-logo.gif", 100, 15)));
+ desc.add(subTitle);
+
+ JPanel link = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 5));
+ link.setOpaque(false);
+ JLabel linkLbl = FontUtils.toLinkText(new URLLabel("https://lucene.apache.org/"));
+ link.add(linkLbl);
+ desc.add(link);
+
+ panel.add(desc);
+
+ return panel;
+ }
+
+ private JScrollPane center() {
+ JEditorPane editorPane = new JEditorPane();
+ editorPane.setOpaque(false);
+ editorPane.setMargin(new Insets(0, 5, 2, 5));
+ editorPane.setContentType("text/html");
+ editorPane.setText(LICENSE_NOTICE);
+ editorPane.setEditable(false);
+ editorPane.addHyperlinkListener(hyperlinkListener);
+ JScrollPane scrollPane = new JScrollPane(editorPane, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+ scrollPane.setBorder(BorderFactory.createLineBorder(Color.gray));
+ SwingUtilities.invokeLater(() -> {
+ // Set the scroll bar position to top
+ scrollPane.getVerticalScrollBar().setValue(0);
+ });
+ return scrollPane;
+ }
+
+ private JPanel footer() {
+ JPanel panel = new JPanel(new FlowLayout(FlowLayout.TRAILING));
+ panel.setOpaque(false);
+ JButton closeBtn = new JButton(MessageUtils.getLocalizedMessage("button.close"));
+ closeBtn.setMargin(new Insets(5, 5, 5, 5));
+ if (closeBtn.getActionListeners().length == 0) {
+ closeBtn.addActionListener(e -> dialog.dispose());
+ }
+ panel.add(closeBtn);
+ return panel;
+ }
+
+ private static final String LUCENE_IMPLEMENTATION_VERSION = LucenePackage.get().getImplementationVersion();
+
+ private static final String LICENSE_NOTICE =
+ "[Implementation Version]
" +
+ "" + (Objects.nonNull(LUCENE_IMPLEMENTATION_VERSION) ? LUCENE_IMPLEMENTATION_VERSION : "") + "
" +
+ "[License]
" +
+ "Luke is distributed under Apache License Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) " +
+ "and includes The Elegant Icon Font (https://www.elegantthemes.com/blog/resources/elegant-icon-font) " +
+ "licensed under MIT (https://opensource.org/licenses/MIT)
" +
+ "[Brief history]
" +
+ "" +
+ "- The original author is Andrzej Bialecki
" +
+ "- The project has been mavenized by Neil Ireson
" +
+ "- The project has been ported to Lucene trunk (marked as 5.0 at the time) by Dmitry Kan\n
" +
+ "- The project has been back-ported to Lucene 4.3 by sonarname
" +
+ "- There are updates to the (non-mavenized) project done by tarzanek
" +
+ "- The UI and core components has been re-implemented on top of Swing by Tomoko Uchida
" +
+ "
"
+ ;
+
+
+ private static final HyperlinkListener hyperlinkListener = e -> {
+ if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED)
+ if (Desktop.isDesktopSupported()) {
+ try {
+ Desktop.getDesktop().browse(e.getURL().toURI());
+ } catch (IOException | URISyntaxException ex) {
+ throw new LukeException(ex.getMessage(), ex);
+ }
+ }
+ };
+
+
+}
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/CheckIndexDialogFactory.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/CheckIndexDialogFactory.java
new file mode 100644
index 00000000000..3928ba699b3
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/CheckIndexDialogFactory.java
@@ -0,0 +1,387 @@
+/*
+ * 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.dialog.menubar;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
+import javax.swing.JTextArea;
+import javax.swing.SwingWorker;
+import java.awt.BorderLayout;
+import java.awt.Dialog;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.invoke.MethodHandles;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.lucene.index.CheckIndex;
+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.util.DialogOpener;
+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.StyleConstants;
+import org.apache.lucene.luke.app.desktop.util.TextAreaPrintStream;
+import org.apache.lucene.luke.models.tools.IndexTools;
+import org.apache.lucene.luke.models.tools.IndexToolsFactory;
+import org.apache.lucene.luke.util.LoggerFactory;
+import org.apache.lucene.util.NamedThreadFactory;
+
+/** Factory of check index dialog */
+public final class CheckIndexDialogFactory implements DialogOpener.DialogFactory {
+
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static CheckIndexDialogFactory instance;
+
+ private final Preferences prefs;
+
+ private final IndexToolsFactory indexToolsFactory;
+
+ private final DirectoryHandler directoryHandler;
+
+ private final IndexHandler indexHandler;
+
+ private final JLabel resultLbl = new JLabel();
+
+ private final JLabel statusLbl = new JLabel();
+
+ private final JLabel indicatorLbl = new JLabel();
+
+ private final JButton repairBtn = new JButton();
+
+ private final JTextArea logArea = new JTextArea();
+
+ private JDialog dialog;
+
+ private LukeState lukeState;
+
+ private CheckIndex.Status status;
+
+ private IndexTools toolsModel;
+
+ private final ListenerFunctions listeners = new ListenerFunctions();
+
+ public synchronized static CheckIndexDialogFactory getInstance() throws IOException {
+ if (instance == null) {
+ instance = new CheckIndexDialogFactory();
+ }
+ return instance;
+ }
+
+ private CheckIndexDialogFactory() throws IOException {
+ this.prefs = PreferencesFactory.getInstance();
+ this.indexToolsFactory = new IndexToolsFactory();
+ this.indexHandler = IndexHandler.getInstance();
+ this.directoryHandler = DirectoryHandler.getInstance();
+
+ indexHandler.addObserver(new Observer());
+ directoryHandler.addObserver(new Observer());
+
+ initialize();
+ }
+
+ private void initialize() {
+ repairBtn.setText(FontUtils.elegantIconHtml("", MessageUtils.getLocalizedMessage("checkidx.button.fix")));
+ repairBtn.setFont(StyleConstants.FONT_BUTTON_LARGE);
+ repairBtn.setMargin(new Insets(3, 3, 3, 3));
+ repairBtn.setEnabled(false);
+ repairBtn.addActionListener(listeners::repairIndex);
+
+ indicatorLbl.setIcon(ImageUtils.createImageIcon("indicator.gif", 20, 20));
+
+ logArea.setEditable(false);
+ }
+
+
+ @Override
+ public JDialog create(Window owner, String title, int width, int height) {
+ dialog = new JDialog(owner, title, Dialog.ModalityType.APPLICATION_MODAL);
+ dialog.add(content());
+ dialog.setSize(new Dimension(width, height));
+ dialog.setLocationRelativeTo(owner);
+ dialog.getContentPane().setBackground(prefs.getColorTheme().getBackgroundColor());
+ return dialog;
+ }
+
+ private JPanel content() {
+ JPanel panel = new JPanel();
+ panel.setOpaque(false);
+ panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
+ panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
+
+ panel.add(controller());
+ panel.add(new JSeparator(JSeparator.HORIZONTAL));
+ panel.add(logs());
+
+ return panel;
+ }
+
+ private JPanel controller() {
+ JPanel panel = new JPanel(new GridLayout(3, 1));
+ panel.setOpaque(false);
+
+ JPanel idxPath = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ idxPath.setOpaque(false);
+ idxPath.add(new JLabel(MessageUtils.getLocalizedMessage("checkidx.label.index_path")));
+ JLabel idxPathLbl = new JLabel(lukeState.getIndexPath());
+ idxPathLbl.setToolTipText(lukeState.getIndexPath());
+ idxPath.add(idxPathLbl);
+ panel.add(idxPath);
+
+ JPanel results = new JPanel(new GridLayout(2, 1));
+ results.setOpaque(false);
+ results.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0));
+ results.add(new JLabel(MessageUtils.getLocalizedMessage("checkidx.label.results")));
+ results.add(resultLbl);
+ panel.add(results);
+
+ JPanel execButtons = new JPanel(new FlowLayout(FlowLayout.TRAILING));
+ execButtons.setOpaque(false);
+ JButton checkBtn = new JButton(FontUtils.elegantIconHtml("", MessageUtils.getLocalizedMessage("checkidx.button.check")));
+ checkBtn.setFont(StyleConstants.FONT_BUTTON_LARGE);
+ checkBtn.setMargin(new Insets(3, 0, 3, 0));
+ checkBtn.addActionListener(listeners::checkIndex);
+ execButtons.add(checkBtn);
+
+ JButton closeBtn = new JButton(MessageUtils.getLocalizedMessage("button.close"));
+ closeBtn.setFont(StyleConstants.FONT_BUTTON_LARGE);
+ closeBtn.setMargin(new Insets(3, 0, 3, 0));
+ closeBtn.addActionListener(e -> dialog.dispose());
+ execButtons.add(closeBtn);
+ panel.add(execButtons);
+
+ return panel;
+ }
+
+ private JPanel logs() {
+ JPanel panel = new JPanel(new BorderLayout());
+ panel.setOpaque(false);
+
+ JPanel header = new JPanel();
+ header.setOpaque(false);
+ header.setLayout(new BoxLayout(header, BoxLayout.PAGE_AXIS));
+
+ JPanel repair = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ repair.setOpaque(false);
+ repair.add(repairBtn);
+
+ JTextArea warnArea = new JTextArea(MessageUtils.getLocalizedMessage("checkidx.label.warn"), 3, 30);
+ warnArea.setLineWrap(true);
+ warnArea.setEditable(false);
+ warnArea.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+
+ repair.add(warnArea);
+ header.add(repair);
+
+ JPanel note = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ note.setOpaque(false);
+ note.add(new JLabel(MessageUtils.getLocalizedMessage("checkidx.label.note")));
+ header.add(note);
+
+ JPanel status = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ status.setOpaque(false);
+ status.add(new JLabel(MessageUtils.getLocalizedMessage("label.status")));
+ statusLbl.setText("Idle");
+ status.add(statusLbl);
+ indicatorLbl.setVisible(false);
+ status.add(indicatorLbl);
+ header.add(status);
+
+ panel.add(header, BorderLayout.PAGE_START);
+
+ logArea.setText("");
+ panel.add(new JScrollPane(logArea), BorderLayout.CENTER);
+
+ return panel;
+ }
+
+ private class Observer implements IndexObserver, DirectoryObserver {
+
+ @Override
+ public void openIndex(LukeState state) {
+ lukeState = state;
+ toolsModel = indexToolsFactory.newInstance(state.getIndexReader(), state.useCompound(), state.keepAllCommits());
+ }
+
+ @Override
+ public void closeIndex() {
+ close();
+ }
+
+ @Override
+ public void openDirectory(LukeState state) {
+ lukeState = state;
+ toolsModel = indexToolsFactory.newInstance(state.getDirectory());
+ }
+
+ @Override
+ public void closeDirectory() {
+ close();
+ }
+
+ private void close() {
+ toolsModel = null;
+ }
+ }
+
+ private class ListenerFunctions {
+
+ void checkIndex(ActionEvent e) {
+ ExecutorService executor = Executors.newFixedThreadPool(1, new NamedThreadFactory("check-index-dialog-check"));
+
+ SwingWorker task = new SwingWorker() {
+
+ @Override
+ protected CheckIndex.Status doInBackground() {
+ setProgress(0);
+ statusLbl.setText("Running...");
+ indicatorLbl.setVisible(true);
+ TextAreaPrintStream ps;
+ try {
+ ps = new TextAreaPrintStream(logArea);
+ CheckIndex.Status status = toolsModel.checkIndex(ps);
+ ps.flush();
+ return status;
+ } catch (UnsupportedEncodingException e) {
+ // will not reach
+ } catch (Exception e) {
+ statusLbl.setText(MessageUtils.getLocalizedMessage("message.error.unknown"));
+ throw e;
+ } finally {
+ setProgress(100);
+ }
+ return null;
+ }
+
+ @Override
+ protected void done() {
+ try {
+ CheckIndex.Status st = get();
+ resultLbl.setText(createResultsMessage(st));
+ indicatorLbl.setVisible(false);
+ statusLbl.setText("Done");
+ if (!st.clean) {
+ repairBtn.setEnabled(true);
+ }
+ status = st;
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ statusLbl.setText(MessageUtils.getLocalizedMessage("message.error.unknown"));
+ }
+ }
+ };
+
+ executor.submit(task);
+ executor.shutdown();
+ }
+
+ private String createResultsMessage(CheckIndex.Status status) {
+ String msg;
+ if (status == null) {
+ msg = "?";
+ } else if (status.clean) {
+ msg = "OK";
+ } else if (status.toolOutOfDate) {
+ msg = "ERROR: Can't check - tool out-of-date";
+ } else {
+ StringBuilder sb = new StringBuilder("BAD:");
+ if (status.missingSegments) {
+ sb.append(" Missing segments.");
+ }
+ if (status.numBadSegments > 0) {
+ sb.append(" numBadSegments=");
+ sb.append(status.numBadSegments);
+ }
+ if (status.totLoseDocCount > 0) {
+ sb.append(" totLoseDocCount=");
+ sb.append(status.totLoseDocCount);
+ }
+ msg = sb.toString();
+ }
+ return msg;
+ }
+
+ void repairIndex(ActionEvent e) {
+ if (status == null) {
+ return;
+ }
+
+ ExecutorService executor = Executors.newFixedThreadPool(1, new NamedThreadFactory("check-index-dialog-repair"));
+
+ SwingWorker task = new SwingWorker() {
+
+ @Override
+ protected CheckIndex.Status doInBackground() {
+ setProgress(0);
+ statusLbl.setText("Running...");
+ indicatorLbl.setVisible(true);
+ logArea.setText("");
+ TextAreaPrintStream ps;
+ try {
+ ps = new TextAreaPrintStream(logArea);
+ toolsModel.repairIndex(status, ps);
+ statusLbl.setText("Done");
+ ps.flush();
+ return status;
+ } catch (UnsupportedEncodingException e) {
+ // will not occur
+ } catch (Exception e) {
+ statusLbl.setText(MessageUtils.getLocalizedMessage("message.error.unknown"));
+ throw e;
+ } finally {
+ setProgress(100);
+ }
+ return null;
+ }
+
+ @Override
+ protected void done() {
+ indexHandler.open(lukeState.getIndexPath(), lukeState.getDirImpl());
+ logArea.append("Repairing index done.");
+ resultLbl.setText("");
+ indicatorLbl.setVisible(false);
+ repairBtn.setEnabled(false);
+ }
+ };
+
+ executor.submit(task);
+ executor.shutdown();
+ }
+ }
+
+}
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/CreateIndexDialogFactory.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/CreateIndexDialogFactory.java
new file mode 100644
index 00000000000..03c6262af7c
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/CreateIndexDialogFactory.java
@@ -0,0 +1,356 @@
+/*
+ * 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.dialog.menubar;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JSeparator;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+import javax.swing.SwingWorker;
+import java.awt.BorderLayout;
+import java.awt.Dialog;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.lucene.luke.app.IndexHandler;
+import org.apache.lucene.luke.app.desktop.Preferences;
+import org.apache.lucene.luke.app.desktop.PreferencesFactory;
+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.ImageUtils;
+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.URLLabel;
+import org.apache.lucene.luke.models.tools.IndexTools;
+import org.apache.lucene.luke.models.tools.IndexToolsFactory;
+import org.apache.lucene.luke.util.LoggerFactory;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.apache.lucene.util.NamedThreadFactory;
+import org.apache.lucene.util.SuppressForbidden;
+
+/** Factory of create index dialog */
+public class CreateIndexDialogFactory implements DialogOpener.DialogFactory {
+
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static CreateIndexDialogFactory instance;
+
+ private final Preferences prefs;
+
+ private final IndexHandler indexHandler;
+
+ private final JTextField locationTF = new JTextField();
+
+ private final JButton browseBtn = new JButton();
+
+ private final JTextField dirnameTF = new JTextField();
+
+ private final JTextField dataDirTF = new JTextField();
+
+ private final JButton dataBrowseBtn = new JButton();
+
+ private final JButton clearBtn = new JButton();
+
+ private final JLabel indicatorLbl = new JLabel();
+
+ private final JButton createBtn = new JButton();
+
+ private final JButton cancelBtn = new JButton();
+
+ private final ListenerFunctions listeners = new ListenerFunctions();
+
+ private JDialog dialog;
+
+ public synchronized static CreateIndexDialogFactory getInstance() throws IOException {
+ if (instance == null) {
+ instance = new CreateIndexDialogFactory();
+ }
+ return instance;
+ }
+
+ private CreateIndexDialogFactory() throws IOException {
+ this.prefs = PreferencesFactory.getInstance();
+ this.indexHandler = IndexHandler.getInstance();
+ initialize();
+ }
+
+ private void initialize() {
+ locationTF.setPreferredSize(new Dimension(360, 30));
+ locationTF.setText(System.getProperty("user.home"));
+ locationTF.setEditable(false);
+
+ browseBtn.setText(FontUtils.elegantIconHtml("n", MessageUtils.getLocalizedMessage("button.browse")));
+ browseBtn.setFont(StyleConstants.FONT_BUTTON_LARGE);
+ browseBtn.setPreferredSize(new Dimension(120, 30));
+ browseBtn.addActionListener(listeners::browseLocationDirectory);
+
+ dirnameTF.setPreferredSize(new Dimension(200, 30));
+
+ dataDirTF.setPreferredSize(new Dimension(250, 30));
+ dataDirTF.setEditable(false);
+
+ clearBtn.setText(MessageUtils.getLocalizedMessage("button.clear"));
+ clearBtn.setPreferredSize(new Dimension(70, 30));
+ clearBtn.addActionListener(listeners::clearDataDir);
+
+ dataBrowseBtn.setText(FontUtils.elegantIconHtml("n", MessageUtils.getLocalizedMessage("button.browse")));
+ dataBrowseBtn.setFont(StyleConstants.FONT_BUTTON_LARGE);
+ dataBrowseBtn.setPreferredSize(new Dimension(100, 30));
+ dataBrowseBtn.addActionListener(listeners::browseDataDirectory);
+
+ indicatorLbl.setIcon(ImageUtils.createImageIcon("indicator.gif", 20, 20));
+ indicatorLbl.setVisible(false);
+
+ createBtn.setText(MessageUtils.getLocalizedMessage("button.create"));
+ createBtn.addActionListener(listeners::createIndex);
+
+ cancelBtn.setText(MessageUtils.getLocalizedMessage("button.cancel"));
+ cancelBtn.addActionListener(e -> dialog.dispose());
+ }
+
+ @Override
+ public JDialog create(Window owner, String title, int width, int height) {
+ dialog = new JDialog(owner, title, Dialog.ModalityType.APPLICATION_MODAL);
+ dialog.add(content());
+ dialog.setSize(new Dimension(width, height));
+ dialog.setLocationRelativeTo(owner);
+ dialog.getContentPane().setBackground(prefs.getColorTheme().getBackgroundColor());
+ return dialog;
+ }
+
+ private JPanel content() {
+ JPanel panel = new JPanel();
+ panel.setOpaque(false);
+ panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
+ panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+
+ panel.add(basicSettings());
+ panel.add(new JSeparator(JSeparator.HORIZONTAL));
+ panel.add(optionalSettings());
+ panel.add(new JSeparator(JSeparator.HORIZONTAL));
+ panel.add(buttons());
+
+ return panel;
+ }
+
+ private JPanel basicSettings() {
+ JPanel panel = new JPanel(new GridLayout(2, 1));
+ panel.setOpaque(false);
+
+ JPanel locPath = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ locPath.setOpaque(false);
+ locPath.add(new JLabel(MessageUtils.getLocalizedMessage("createindex.label.location")));
+ locPath.add(locationTF);
+ locPath.add(browseBtn);
+ panel.add(locPath);
+
+ JPanel dirName = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ dirName.setOpaque(false);
+ dirName.add(new JLabel(MessageUtils.getLocalizedMessage("createindex.label.dirname")));
+ dirName.add(dirnameTF);
+ panel.add(dirName);
+
+ return panel;
+ }
+
+ private JPanel optionalSettings() {
+ JPanel panel = new JPanel(new BorderLayout());
+ panel.setOpaque(false);
+
+ JPanel description = new JPanel();
+ description.setLayout(new BoxLayout(description, BoxLayout.Y_AXIS));
+ description.setOpaque(false);
+
+ JPanel name = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ name.setOpaque(false);
+ JLabel nameLbl = new JLabel(MessageUtils.getLocalizedMessage("createindex.label.option"));
+ name.add(nameLbl);
+ description.add(name);
+
+ JTextArea descTA1 = new JTextArea(MessageUtils.getLocalizedMessage("createindex.textarea.data_help1"));
+ descTA1.setPreferredSize(new Dimension(550, 20));
+ descTA1.setBorder(BorderFactory.createEmptyBorder(2, 10, 10, 5));
+ descTA1.setOpaque(false);
+ descTA1.setLineWrap(true);
+ descTA1.setEditable(false);
+ description.add(descTA1);
+
+ JPanel link = new JPanel(new FlowLayout(FlowLayout.LEADING, 10, 1));
+ link.setOpaque(false);
+ JLabel linkLbl = FontUtils.toLinkText(new URLLabel(MessageUtils.getLocalizedMessage("createindex.label.data_link")));
+ link.add(linkLbl);
+ description.add(link);
+
+ JTextArea descTA2 = new JTextArea(MessageUtils.getLocalizedMessage("createindex.textarea.data_help2"));
+ descTA2.setPreferredSize(new Dimension(550, 50));
+ descTA2.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 5));
+ descTA2.setOpaque(false);
+ descTA2.setLineWrap(true);
+ descTA2.setEditable(false);
+ description.add(descTA2);
+
+ panel.add(description, BorderLayout.PAGE_START);
+
+ JPanel dataDirPath = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ dataDirPath.setOpaque(false);
+ dataDirPath.add(new JLabel(MessageUtils.getLocalizedMessage("createindex.label.datadir")));
+ dataDirPath.add(dataDirTF);
+ dataDirPath.add(dataBrowseBtn);
+
+ dataDirPath.add(clearBtn);
+ panel.add(dataDirPath, BorderLayout.CENTER);
+
+ return panel;
+ }
+
+ private JPanel buttons() {
+ JPanel panel = new JPanel(new FlowLayout(FlowLayout.TRAILING));
+ panel.setOpaque(false);
+ panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 10, 20));
+
+ panel.add(indicatorLbl);
+ panel.add(createBtn);
+ panel.add(cancelBtn);
+
+ return panel;
+ }
+
+ private class ListenerFunctions {
+
+ void browseLocationDirectory(ActionEvent e) {
+ browseDirectory(locationTF);
+ }
+
+ void browseDataDirectory(ActionEvent e) {
+ browseDirectory(dataDirTF);
+ }
+
+ @SuppressForbidden(reason = "JFilechooser#getSelectedFile() returns java.io.File")
+ private void browseDirectory(JTextField tf) {
+ JFileChooser fc = new JFileChooser(new File(tf.getText()));
+ fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+ fc.setFileHidingEnabled(false);
+ int retVal = fc.showOpenDialog(dialog);
+ if (retVal == JFileChooser.APPROVE_OPTION) {
+ File dir = fc.getSelectedFile();
+ tf.setText(dir.getAbsolutePath());
+ }
+ }
+
+ void createIndex(ActionEvent e) {
+ Path path = Paths.get(locationTF.getText(), dirnameTF.getText());
+ if (Files.exists(path)) {
+ String message = "The directory " + path.toAbsolutePath().toString() + " already exists.";
+ JOptionPane.showMessageDialog(dialog, message, "Empty index path", JOptionPane.ERROR_MESSAGE);
+ } else {
+ // create new index asynchronously
+ ExecutorService executor = Executors.newFixedThreadPool(1, new NamedThreadFactory("create-index-dialog"));
+
+ SwingWorker task = new SwingWorker() {
+
+ @Override
+ protected Void doInBackground() throws Exception {
+ setProgress(0);
+ indicatorLbl.setVisible(true);
+ createBtn.setEnabled(false);
+
+ try {
+ Directory dir = FSDirectory.open(path);
+ IndexTools toolsModel = new IndexToolsFactory().newInstance(dir);
+
+ if (dataDirTF.getText().isEmpty()) {
+ // without sample documents
+ toolsModel.createNewIndex();
+ } else {
+ // with sample documents
+ Path dataPath = Paths.get(dataDirTF.getText());
+ toolsModel.createNewIndex(dataPath.toAbsolutePath().toString());
+ }
+
+ indexHandler.open(path.toAbsolutePath().toString(), null, false, false, false);
+ prefs.addHistory(path.toAbsolutePath().toString());
+
+ dirnameTF.setText("");
+ closeDialog();
+ } catch (Exception ex) {
+ // cleanup
+ try {
+ Files.walkFileTree(path, new SimpleFileVisitor() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ Files.delete(file);
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ Files.deleteIfExists(path);
+ } catch (IOException ex2) {
+ }
+
+ log.error("Cannot create index", ex);
+ String message = "See Logs tab or log file for more details.";
+ JOptionPane.showMessageDialog(dialog, message, "Cannot create index", JOptionPane.ERROR_MESSAGE);
+ } finally {
+ setProgress(100);
+ }
+ return null;
+ }
+
+ @Override
+ protected void done() {
+ indicatorLbl.setVisible(false);
+ createBtn.setEnabled(true);
+ }
+ };
+
+ executor.submit(task);
+ executor.shutdown();
+ }
+ }
+
+ private void clearDataDir(ActionEvent e) {
+ dataDirTF.setText("");
+ }
+
+ private void closeDialog() {
+ dialog.dispose();
+ }
+ }
+}
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/OpenIndexDialogFactory.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/OpenIndexDialogFactory.java
new file mode 100644
index 00000000000..782827d9744
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/OpenIndexDialogFactory.java
@@ -0,0 +1,385 @@
+/*
+ * 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.dialog.menubar;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JSeparator;
+import java.awt.Dialog;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.lucene.luke.app.DirectoryHandler;
+import org.apache.lucene.luke.app.IndexHandler;
+import org.apache.lucene.luke.app.desktop.Preferences;
+import org.apache.lucene.luke.app.desktop.PreferencesFactory;
+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.models.LukeException;
+import org.apache.lucene.luke.util.LoggerFactory;
+import org.apache.lucene.luke.util.reflection.ClassScanner;
+import org.apache.lucene.store.FSDirectory;
+import org.apache.lucene.util.NamedThreadFactory;
+import org.apache.lucene.util.SuppressForbidden;
+
+/** Factory of open index dialog */
+public final class OpenIndexDialogFactory implements DialogOpener.DialogFactory {
+
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static OpenIndexDialogFactory instance;
+
+ private final Preferences prefs;
+
+ private final DirectoryHandler directoryHandler;
+
+ private final IndexHandler indexHandler;
+
+ private final JComboBox idxPathCombo = new JComboBox<>();
+
+ private final JButton browseBtn = new JButton();
+
+ private final JCheckBox readOnlyCB = new JCheckBox();
+
+ private final JComboBox dirImplCombo = new JComboBox<>();
+
+ private final JCheckBox noReaderCB = new JCheckBox();
+
+ private final JCheckBox useCompoundCB = new JCheckBox();
+
+ private final JRadioButton keepLastCommitRB = new JRadioButton();
+
+ private final JRadioButton keepAllCommitsRB = new JRadioButton();
+
+ private final ListenerFunctions listeners = new ListenerFunctions();
+
+ private JDialog dialog;
+
+ public synchronized static OpenIndexDialogFactory getInstance() throws IOException {
+ if (instance == null) {
+ instance = new OpenIndexDialogFactory();
+ }
+ return instance;
+ }
+
+ private OpenIndexDialogFactory() throws IOException {
+ this.prefs = PreferencesFactory.getInstance();
+ this.directoryHandler = DirectoryHandler.getInstance();
+ this.indexHandler = IndexHandler.getInstance();
+ initialize();
+ }
+
+ private void initialize() {
+ idxPathCombo.setPreferredSize(new Dimension(360, 40));
+
+ browseBtn.setText(FontUtils.elegantIconHtml("n", MessageUtils.getLocalizedMessage("button.browse")));
+ browseBtn.setFont(StyleConstants.FONT_BUTTON_LARGE);
+ browseBtn.setPreferredSize(new Dimension(120, 40));
+ browseBtn.addActionListener(listeners::browseDirectory);
+
+ readOnlyCB.setText(MessageUtils.getLocalizedMessage("openindex.checkbox.readonly"));
+ readOnlyCB.setSelected(prefs.isReadOnly());
+ readOnlyCB.addActionListener(listeners::toggleReadOnly);
+ readOnlyCB.setOpaque(false);
+
+ // Scanning all Directory types will take time...
+ ExecutorService executorService = Executors.newFixedThreadPool(1, new NamedThreadFactory("load-directory-types"));
+ executorService.execute(() -> {
+ for (String clazzName : supportedDirImpls()) {
+ dirImplCombo.addItem(clazzName);
+ }
+ });
+ executorService.shutdown();
+ dirImplCombo.setPreferredSize(new Dimension(350, 30));
+ dirImplCombo.setSelectedItem(prefs.getDirImpl());
+
+ noReaderCB.setText(MessageUtils.getLocalizedMessage("openindex.checkbox.no_reader"));
+ noReaderCB.setSelected(prefs.isNoReader());
+ noReaderCB.setOpaque(false);
+
+ useCompoundCB.setText(MessageUtils.getLocalizedMessage("openindex.checkbox.use_compound"));
+ useCompoundCB.setSelected(prefs.isUseCompound());
+ useCompoundCB.setOpaque(false);
+
+ keepLastCommitRB.setText(MessageUtils.getLocalizedMessage("openindex.radio.keep_only_last_commit"));
+ keepLastCommitRB.setSelected(!prefs.isKeepAllCommits());
+ keepLastCommitRB.setOpaque(false);
+
+ keepAllCommitsRB.setText(MessageUtils.getLocalizedMessage("openindex.radio.keep_all_commits"));
+ keepAllCommitsRB.setSelected(prefs.isKeepAllCommits());
+ keepAllCommitsRB.setOpaque(false);
+
+ }
+
+ @Override
+ public JDialog create(Window owner, String title, int width, int height) {
+ dialog = new JDialog(owner, title, Dialog.ModalityType.APPLICATION_MODAL);
+ dialog.add(content());
+ dialog.setSize(new Dimension(width, height));
+ dialog.setLocationRelativeTo(owner);
+ dialog.getContentPane().setBackground(prefs.getColorTheme().getBackgroundColor());
+ return dialog;
+ }
+
+ private JPanel content() {
+ JPanel panel = new JPanel();
+ panel.setOpaque(false);
+ panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
+ panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+
+ panel.add(basicSettings());
+ panel.add(new JSeparator(JSeparator.HORIZONTAL));
+ panel.add(expertSettings());
+ panel.add(new JSeparator(JSeparator.HORIZONTAL));
+ panel.add(buttons());
+
+ return panel;
+ }
+
+ private JPanel basicSettings() {
+ JPanel panel = new JPanel(new GridLayout(2, 1));
+ panel.setOpaque(false);
+
+ JPanel idxPath = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ idxPath.setOpaque(false);
+ idxPath.add(new JLabel(MessageUtils.getLocalizedMessage("openindex.label.index_path")));
+
+ idxPathCombo.removeAllItems();
+ for (String path : prefs.getHistory()) {
+ idxPathCombo.addItem(path);
+ }
+ idxPath.add(idxPathCombo);
+
+ idxPath.add(browseBtn);
+
+ panel.add(idxPath);
+
+ JPanel readOnly = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ readOnly.setOpaque(false);
+ readOnly.add(readOnlyCB);
+ JLabel roIconLB = new JLabel(FontUtils.elegantIconHtml(""));
+ readOnly.add(roIconLB);
+ panel.add(readOnly);
+
+ return panel;
+ }
+
+ private JPanel expertSettings() {
+ JPanel panel = new JPanel(new GridLayout(6, 1));
+ panel.setOpaque(false);
+
+ JPanel header = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ header.setOpaque(false);
+ header.add(new JLabel(MessageUtils.getLocalizedMessage("openindex.label.expert")));
+ panel.add(header);
+
+ JPanel dirImpl = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ dirImpl.setOpaque(false);
+ dirImpl.add(new JLabel(MessageUtils.getLocalizedMessage("openindex.label.dir_impl")));
+ dirImpl.add(dirImplCombo);
+ panel.add(dirImpl);
+
+ JPanel noReader = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ noReader.setOpaque(false);
+ noReader.add(noReaderCB);
+ JLabel noReaderIcon = new JLabel(FontUtils.elegantIconHtml(""));
+ noReader.add(noReaderIcon);
+ panel.add(noReader);
+
+ JPanel iwConfig = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ iwConfig.setOpaque(false);
+ iwConfig.add(new JLabel(MessageUtils.getLocalizedMessage("openindex.label.iw_config")));
+ panel.add(iwConfig);
+
+ JPanel compound = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ compound.setOpaque(false);
+ compound.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
+ compound.add(useCompoundCB);
+ panel.add(compound);
+
+ JPanel keepCommits = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ keepCommits.setOpaque(false);
+ keepCommits.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
+ keepCommits.add(keepLastCommitRB);
+ keepCommits.add(keepAllCommitsRB);
+
+ ButtonGroup group = new ButtonGroup();
+ group.add(keepLastCommitRB);
+ group.add(keepAllCommitsRB);
+
+ panel.add(keepCommits);
+
+ return panel;
+ }
+
+ private String[] supportedDirImpls() {
+ // supports FS-based built-in implementations
+ ClassScanner scanner = new ClassScanner("org.apache.lucene.store", getClass().getClassLoader());
+ Set> clazzSet = scanner.scanSubTypes(FSDirectory.class);
+
+ List clazzNames = new ArrayList<>();
+ clazzNames.add(FSDirectory.class.getName());
+ clazzNames.addAll(clazzSet.stream().map(Class::getName).collect(Collectors.toList()));
+
+ String[] result = new String[clazzNames.size()];
+ return clazzNames.toArray(result);
+ }
+
+ private JPanel buttons() {
+ JPanel panel = new JPanel(new FlowLayout(FlowLayout.TRAILING));
+ panel.setOpaque(false);
+ panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 10, 20));
+
+ JButton okBtn = new JButton(MessageUtils.getLocalizedMessage("button.ok"));
+ okBtn.addActionListener(listeners::openIndexOrDirectory);
+ panel.add(okBtn);
+
+ JButton cancelBtn = new JButton(MessageUtils.getLocalizedMessage("button.cancel"));
+ cancelBtn.addActionListener(e -> dialog.dispose());
+ panel.add(cancelBtn);
+
+ return panel;
+ }
+
+ private class ListenerFunctions {
+
+ @SuppressForbidden(reason = "FileChooser#getSelectedFile() returns java.io.File")
+ void browseDirectory(ActionEvent e) {
+ File currentDir = getLastOpenedDirectory();
+ JFileChooser fc = currentDir == null ? new JFileChooser() : new JFileChooser(currentDir);
+ fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+ fc.setFileHidingEnabled(false);
+ int retVal = fc.showOpenDialog(dialog);
+ if (retVal == JFileChooser.APPROVE_OPTION) {
+ File dir = fc.getSelectedFile();
+ idxPathCombo.insertItemAt(dir.getAbsolutePath(), 0);
+ idxPathCombo.setSelectedIndex(0);
+ }
+ }
+
+ @SuppressForbidden(reason = "JFileChooser constructor takes java.io.File")
+ private File getLastOpenedDirectory() {
+ List history = prefs.getHistory();
+ if (!history.isEmpty()) {
+ Path path = Paths.get(history.get(0));
+ if (Files.exists(path)) {
+ return path.getParent().toAbsolutePath().toFile();
+ }
+ }
+ return null;
+ }
+
+ void toggleReadOnly(ActionEvent e) {
+ setWriterConfigEnabled(!isReadOnly());
+ }
+
+ private void setWriterConfigEnabled(boolean enable) {
+ useCompoundCB.setEnabled(enable);
+ keepLastCommitRB.setEnabled(enable);
+ keepAllCommitsRB.setEnabled(enable);
+ }
+
+ void openIndexOrDirectory(ActionEvent e) {
+ try {
+ if (directoryHandler.directoryOpened()) {
+ directoryHandler.close();
+ }
+ if (indexHandler.indexOpened()) {
+ indexHandler.close();
+ }
+
+ String selectedPath = (String) idxPathCombo.getSelectedItem();
+ String dirImplClazz = (String) dirImplCombo.getSelectedItem();
+ if (selectedPath == null || selectedPath.length() == 0) {
+ String message = MessageUtils.getLocalizedMessage("openindex.message.index_path_not_selected");
+ JOptionPane.showMessageDialog(dialog, message, "Empty index path", JOptionPane.ERROR_MESSAGE);
+ } else if (isNoReader()) {
+ directoryHandler.open(selectedPath, dirImplClazz);
+ addHistory(selectedPath);
+ } else {
+ indexHandler.open(selectedPath, dirImplClazz, isReadOnly(), useCompound(), keepAllCommits());
+ addHistory(selectedPath);
+ }
+ prefs.setIndexOpenerPrefs(
+ isReadOnly(), dirImplClazz,
+ isNoReader(), useCompound(), keepAllCommits());
+ closeDialog();
+ } catch (LukeException ex) {
+ String message = ex.getMessage() + System.lineSeparator() + "See Logs tab or log file for more details.";
+ JOptionPane.showMessageDialog(dialog, message, "Invalid index path", JOptionPane.ERROR_MESSAGE);
+ } catch (Throwable cause) {
+ JOptionPane.showMessageDialog(dialog, MessageUtils.getLocalizedMessage("message.error.unknown"), "Unknown Error", JOptionPane.ERROR_MESSAGE);
+ log.error(cause.getMessage(), cause);
+ }
+ }
+
+ private boolean isNoReader() {
+ return noReaderCB.isSelected();
+ }
+
+ private boolean isReadOnly() {
+ return readOnlyCB.isSelected();
+ }
+
+ private boolean useCompound() {
+ return useCompoundCB.isSelected();
+ }
+
+ private boolean keepAllCommits() {
+ return keepAllCommitsRB.isSelected();
+ }
+
+ private void closeDialog() {
+ dialog.dispose();
+ }
+
+ private void addHistory(String indexPath) throws IOException {
+ prefs.addHistory(indexPath);
+ }
+
+ }
+
+}
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/OptimizeIndexDialogFactory.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/OptimizeIndexDialogFactory.java
new file mode 100644
index 00000000000..e5543d86856
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/OptimizeIndexDialogFactory.java
@@ -0,0 +1,263 @@
+/*
+ * 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.dialog.menubar;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
+import javax.swing.JSpinner;
+import javax.swing.JTextArea;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.SwingWorker;
+import java.awt.BorderLayout;
+import java.awt.Dialog;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.invoke.MethodHandles;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.apache.logging.log4j.Logger;
+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.util.DialogOpener;
+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.StyleConstants;
+import org.apache.lucene.luke.app.desktop.util.TextAreaPrintStream;
+import org.apache.lucene.luke.models.tools.IndexTools;
+import org.apache.lucene.luke.models.tools.IndexToolsFactory;
+import org.apache.lucene.luke.util.LoggerFactory;
+import org.apache.lucene.util.NamedThreadFactory;
+
+/** Factory of optimize index dialog */
+public final class OptimizeIndexDialogFactory implements DialogOpener.DialogFactory {
+
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static OptimizeIndexDialogFactory instance;
+
+ private final Preferences prefs;
+
+ private final IndexToolsFactory indexToolsFactory = new IndexToolsFactory();
+
+ private final IndexHandler indexHandler;
+
+ private final JCheckBox expungeCB = new JCheckBox();
+
+ private final JSpinner maxSegSpnr = new JSpinner();
+
+ private final JLabel statusLbl = new JLabel();
+
+ private final JLabel indicatorLbl = new JLabel();
+
+ private final JTextArea logArea = new JTextArea();
+
+ private final ListenerFunctions listeners = new ListenerFunctions();
+
+ private JDialog dialog;
+
+ private IndexTools toolsModel;
+
+ public synchronized static OptimizeIndexDialogFactory getInstance() throws IOException {
+ if (instance == null) {
+ instance = new OptimizeIndexDialogFactory();
+ }
+ return instance;
+ }
+
+ private OptimizeIndexDialogFactory() throws IOException {
+ this.prefs = PreferencesFactory.getInstance();
+ this.indexHandler = IndexHandler.getInstance();
+ indexHandler.addObserver(new Observer());
+
+ initialize();
+ }
+
+ private void initialize() {
+ expungeCB.setText(MessageUtils.getLocalizedMessage("optimize.checkbox.expunge"));
+ expungeCB.setOpaque(false);
+
+ maxSegSpnr.setModel(new SpinnerNumberModel(1, 1, 100, 1));
+ maxSegSpnr.setPreferredSize(new Dimension(100, 30));
+
+ indicatorLbl.setIcon(ImageUtils.createImageIcon("indicator.gif", 20, 20));
+
+ logArea.setEditable(false);
+ }
+
+ @Override
+ public JDialog create(Window owner, String title, int width, int height) {
+ dialog = new JDialog(owner, title, Dialog.ModalityType.APPLICATION_MODAL);
+ dialog.add(content());
+ dialog.setSize(new Dimension(width, height));
+ dialog.setLocationRelativeTo(owner);
+ dialog.getContentPane().setBackground(prefs.getColorTheme().getBackgroundColor());
+ return dialog;
+ }
+
+ private JPanel content() {
+ JPanel panel = new JPanel();
+ panel.setOpaque(false);
+ panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
+ panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
+
+ panel.add(controller());
+ panel.add(new JSeparator(JSeparator.HORIZONTAL));
+ panel.add(logs());
+
+ return panel;
+ }
+
+ private JPanel controller() {
+ JPanel panel = new JPanel(new GridLayout(4, 1));
+ panel.setOpaque(false);
+
+ JPanel idxPath = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ idxPath.setOpaque(false);
+ idxPath.add(new JLabel(MessageUtils.getLocalizedMessage("optimize.label.index_path")));
+ JLabel idxPathLbl = new JLabel(indexHandler.getState().getIndexPath());
+ idxPathLbl.setToolTipText(indexHandler.getState().getIndexPath());
+ idxPath.add(idxPathLbl);
+ panel.add(idxPath);
+
+ JPanel expunge = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ expunge.setOpaque(false);
+
+ expunge.add(expungeCB);
+ panel.add(expunge);
+
+ JPanel maxSegs = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ maxSegs.setOpaque(false);
+ maxSegs.add(new JLabel(MessageUtils.getLocalizedMessage("optimize.label.max_segments")));
+ maxSegs.add(maxSegSpnr);
+ panel.add(maxSegs);
+
+ JPanel execButtons = new JPanel(new FlowLayout(FlowLayout.TRAILING));
+ execButtons.setOpaque(false);
+ JButton optimizeBtn = new JButton(FontUtils.elegantIconHtml("", MessageUtils.getLocalizedMessage("optimize.button.optimize")));
+ optimizeBtn.setFont(StyleConstants.FONT_BUTTON_LARGE);
+ optimizeBtn.setMargin(new Insets(3, 0, 3, 0));
+ optimizeBtn.addActionListener(listeners::optimize);
+ execButtons.add(optimizeBtn);
+ JButton closeBtn = new JButton(MessageUtils.getLocalizedMessage("button.close"));
+ closeBtn.setFont(StyleConstants.FONT_BUTTON_LARGE);
+ closeBtn.setMargin(new Insets(3, 0, 3, 0));
+ closeBtn.addActionListener(e -> dialog.dispose());
+ execButtons.add(closeBtn);
+ panel.add(execButtons);
+
+ return panel;
+ }
+
+ private JPanel logs() {
+ JPanel panel = new JPanel(new BorderLayout());
+ panel.setOpaque(false);
+
+ JPanel header = new JPanel(new GridLayout(2, 1));
+ header.setOpaque(false);
+ header.add(new JLabel(MessageUtils.getLocalizedMessage("optimize.label.note")));
+ JPanel status = new JPanel(new FlowLayout(FlowLayout.LEADING));
+ status.setOpaque(false);
+ status.add(new JLabel(MessageUtils.getLocalizedMessage("label.status")));
+ statusLbl.setText("Idle");
+ status.add(statusLbl);
+ indicatorLbl.setVisible(false);
+ status.add(indicatorLbl);
+ header.add(status);
+ panel.add(header, BorderLayout.PAGE_START);
+
+ logArea.setText("");
+ panel.add(new JScrollPane(logArea), BorderLayout.CENTER);
+
+ return panel;
+ }
+
+ private class ListenerFunctions {
+
+ void optimize(ActionEvent e) {
+ ExecutorService executor = Executors.newFixedThreadPool(1, new NamedThreadFactory("optimize-index-dialog"));
+
+ SwingWorker task = new SwingWorker() {
+
+ @Override
+ protected Void doInBackground() {
+ setProgress(0);
+ statusLbl.setText("Running...");
+ indicatorLbl.setVisible(true);
+ TextAreaPrintStream ps;
+ try {
+ ps = new TextAreaPrintStream(logArea);
+ toolsModel.optimize(expungeCB.isSelected(), (int) maxSegSpnr.getValue(), ps);
+ ps.flush();
+ } catch (UnsupportedEncodingException e) {
+ // will not reach
+ } catch (Exception e) {
+ statusLbl.setText(MessageUtils.getLocalizedMessage("message.error.unknown"));
+ throw e;
+ } finally {
+ setProgress(100);
+ }
+ return null;
+ }
+
+ @Override
+ protected void done() {
+ indicatorLbl.setVisible(false);
+ statusLbl.setText("Done");
+ indexHandler.reOpen();
+ }
+ };
+
+ executor.submit(task);
+ executor.shutdown();
+ }
+
+ }
+
+ private class Observer implements IndexObserver {
+
+ @Override
+ public void openIndex(LukeState state) {
+ toolsModel = indexToolsFactory.newInstance(state.getIndexReader(), state.useCompound(), state.keepAllCommits());
+ }
+
+ @Override
+ public void closeIndex() {
+ toolsModel = null;
+ }
+
+ }
+
+}
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/package-info.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/package-info.java
new file mode 100644
index 00000000000..72a2d3fc7d5
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/menubar/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Dialogs used in the menu bar */
+package org.apache.lucene.luke.app.desktop.components.dialog.menubar;
\ No newline at end of file
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/package-info.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/package-info.java
new file mode 100644
index 00000000000..44ad40b04fd
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Dialogs */
+package org.apache.lucene.luke.app.desktop.components.dialog;
\ No newline at end of file
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/search/ExplainDialogFactory.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/search/ExplainDialogFactory.java
new file mode 100644
index 00000000000..66d558d2866
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/search/ExplainDialogFactory.java
@@ -0,0 +1,182 @@
+/*
+ * 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.dialog.search;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTree;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeCellRenderer;
+import java.awt.BorderLayout;
+import java.awt.Dialog;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.Toolkit;
+import java.awt.Window;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.StringSelection;
+import java.io.IOException;
+import java.util.Objects;
+import java.util.stream.IntStream;
+
+import org.apache.lucene.luke.app.desktop.Preferences;
+import org.apache.lucene.luke.app.desktop.PreferencesFactory;
+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.search.Explanation;
+
+/** Factory of explain dialog */
+public final class ExplainDialogFactory implements DialogOpener.DialogFactory {
+
+ private static ExplainDialogFactory instance;
+
+ private final Preferences prefs;
+
+ private JDialog dialog;
+
+ private int docid = -1;
+
+ private Explanation explanation;
+
+ public synchronized static ExplainDialogFactory getInstance() throws IOException {
+ if (instance == null) {
+ instance = new ExplainDialogFactory();
+ }
+ return instance;
+ }
+
+ private ExplainDialogFactory() throws IOException {
+ this.prefs = PreferencesFactory.getInstance();
+ }
+
+ public void setDocid(int docid) {
+ this.docid = docid;
+ }
+
+ public void setExplanation(Explanation explanation) {
+ this.explanation = explanation;
+ }
+
+ @Override
+ public JDialog create(Window owner, String title, int width, int height) {
+ if (docid < 0 || Objects.isNull(explanation)) {
+ throw new IllegalStateException("docid and/or explanation is not set.");
+ }
+
+ dialog = new JDialog(owner, title, Dialog.ModalityType.APPLICATION_MODAL);
+ dialog.add(content());
+ dialog.setSize(new Dimension(width, height));
+ dialog.setLocationRelativeTo(owner);
+ dialog.getContentPane().setBackground(prefs.getColorTheme().getBackgroundColor());
+ return dialog;
+ }
+
+ private JPanel content() {
+ JPanel panel = new JPanel(new BorderLayout());
+ panel.setOpaque(false);
+ panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
+
+ JPanel header = new JPanel(new FlowLayout(FlowLayout.LEADING, 5, 10));
+ header.setOpaque(false);
+ header.add(new JLabel(MessageUtils.getLocalizedMessage("search.explanation.description")));
+ header.add(new JLabel(String.valueOf(docid)));
+ panel.add(header, BorderLayout.PAGE_START);
+
+ JPanel center = new JPanel(new GridLayout(1, 1));
+ center.setOpaque(false);
+ center.add(new JScrollPane(createExplanationTree()));
+ panel.add(center, BorderLayout.CENTER);
+
+ JPanel footer = new JPanel(new FlowLayout(FlowLayout.TRAILING, 5, 5));
+ footer.setOpaque(false);
+
+ JButton copyBtn = new JButton(FontUtils.elegantIconHtml("", MessageUtils.getLocalizedMessage("button.copy")));
+ copyBtn.setMargin(new Insets(3, 3, 3, 3));
+ copyBtn.addActionListener(e -> {
+ Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ StringSelection selection = new StringSelection(explanationToString());
+ clipboard.setContents(selection, null);
+ });
+ footer.add(copyBtn);
+
+ JButton closeBtn = new JButton(MessageUtils.getLocalizedMessage("button.close"));
+ closeBtn.setMargin(new Insets(3, 3, 3, 3));
+ closeBtn.addActionListener(e -> dialog.dispose());
+ footer.add(closeBtn);
+ panel.add(footer, BorderLayout.PAGE_END);
+
+ return panel;
+ }
+
+ private JTree createExplanationTree() {
+ DefaultMutableTreeNode top = createNode(explanation);
+ traverse(top, explanation.getDetails());
+
+ JTree tree = new JTree(top);
+ tree.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+ DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();
+ renderer.setOpenIcon(null);
+ renderer.setClosedIcon(null);
+ renderer.setLeafIcon(null);
+ tree.setCellRenderer(renderer);
+ // expand all nodes
+ for (int row = 0; row < tree.getRowCount(); row++) {
+ tree.expandRow(row);
+ }
+ return tree;
+ }
+
+ private void traverse(DefaultMutableTreeNode parent, Explanation[] explanations) {
+ for (Explanation explanation : explanations) {
+ DefaultMutableTreeNode node = createNode(explanation);
+ parent.add(node);
+ traverse(node, explanation.getDetails());
+ }
+ }
+
+ private DefaultMutableTreeNode createNode(Explanation explanation) {
+ return new DefaultMutableTreeNode(format(explanation));
+ }
+
+ private String explanationToString() {
+ StringBuilder sb = new StringBuilder(format(explanation));
+ sb.append(System.lineSeparator());
+ traverseToCopy(sb, 1, explanation.getDetails());
+ return sb.toString();
+ }
+
+ private void traverseToCopy(StringBuilder sb, int depth, Explanation[] explanations) {
+ for (Explanation explanation : explanations) {
+ IntStream.range(0, depth).forEach(i -> sb.append(" "));
+ sb.append(format(explanation));
+ sb.append("\n");
+ traverseToCopy(sb, depth + 1, explanation.getDetails());
+ }
+ }
+
+ private String format(Explanation explanation) {
+ return explanation.getValue() + " " + explanation.getDescription();
+ }
+}
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/search/package-info.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/search/package-info.java
new file mode 100644
index 00000000000..7af5fb1f80b
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/dialog/search/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Dialogs used in the Search tab */
+package org.apache.lucene.luke.app.desktop.components.dialog.search;
\ No newline at end of file
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/fragments/analysis/CustomAnalyzerPanelOperator.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/fragments/analysis/CustomAnalyzerPanelOperator.java
new file mode 100644
index 00000000000..54451beaae2
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/fragments/analysis/CustomAnalyzerPanelOperator.java
@@ -0,0 +1,45 @@
+/*
+ * 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.fragments.analysis;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.lucene.luke.app.desktop.components.ComponentOperatorRegistry;
+import org.apache.lucene.luke.models.analysis.Analysis;
+
+/** Operator of the custom analyzer panel */
+public interface CustomAnalyzerPanelOperator extends ComponentOperatorRegistry.ComponentOperator {
+ void setAnalysisModel(Analysis analysisModel);
+
+ void resetAnalysisComponents();
+
+ void updateCharFilters(List deletedIndexes);
+
+ void updateTokenFilters(List deletedIndexes);
+
+ Map getCharFilterParams(int index);
+
+ void updateCharFilterParams(int index, Map updatedParams);
+
+ void updateTokenizerParams(Map updatedParams);
+
+ Map getTokenFilterParams(int index);
+
+ void updateTokenFilterParams(int index, Map updatedParams);
+}
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/fragments/analysis/CustomAnalyzerPanelProvider.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/fragments/analysis/CustomAnalyzerPanelProvider.java
new file mode 100644
index 00000000000..4b1bc22fcf8
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/fragments/analysis/CustomAnalyzerPanelProvider.java
@@ -0,0 +1,751 @@
+/*
+ * 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.fragments.analysis;
+
+import javax.swing.BorderFactory;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
+import javax.swing.JTextField;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+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.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.apache.lucene.luke.app.desktop.MessageBroker;
+import org.apache.lucene.luke.app.desktop.components.AnalysisTabOperator;
+import org.apache.lucene.luke.app.desktop.components.ComponentOperatorRegistry;
+import org.apache.lucene.luke.app.desktop.components.dialog.analysis.EditFiltersDialogFactory;
+import org.apache.lucene.luke.app.desktop.components.dialog.analysis.EditFiltersMode;
+import org.apache.lucene.luke.app.desktop.components.dialog.analysis.EditParamsDialogFactory;
+import org.apache.lucene.luke.app.desktop.components.dialog.analysis.EditParamsMode;
+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.ListUtils;
+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.lang.Callable;
+import org.apache.lucene.luke.models.analysis.Analysis;
+import org.apache.lucene.luke.models.analysis.CustomAnalyzerConfig;
+import org.apache.lucene.util.SuppressForbidden;
+
+/** Provider of the custom analyzer panel */
+public final class CustomAnalyzerPanelProvider implements CustomAnalyzerPanelOperator {
+
+ private final ComponentOperatorRegistry operatorRegistry;
+
+ private final EditParamsDialogFactory editParamsDialogFactory;
+
+ private final EditFiltersDialogFactory editFiltersDialogFactory;
+
+ private final MessageBroker messageBroker;
+
+ private final JTextField confDirTF = new JTextField();
+
+ private final JFileChooser fileChooser = new JFileChooser();
+
+ private final JButton confDirBtn = new JButton();
+
+ private final JButton buildBtn = new JButton();
+
+ private final JLabel loadJarLbl = new JLabel();
+
+ private final JList selectedCfList = new JList<>(new String[]{});
+
+ private final JButton cfEditBtn = new JButton();
+
+ private final JComboBox cfFactoryCombo = new JComboBox<>();
+
+ private final JTextField selectedTokTF = new JTextField();
+
+ private final JButton tokEditBtn = new JButton();
+
+ private final JComboBox tokFactoryCombo = new JComboBox<>();
+
+ private final JList selectedTfList = new JList<>(new String[]{});
+
+ private final JButton tfEditBtn = new JButton();
+
+ private final JComboBox tfFactoryCombo = new JComboBox<>();
+
+ private final ListenerFunctions listeners = new ListenerFunctions();
+
+ private final List