You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by sa...@apache.org on 2018/07/17 21:12:44 UTC

[09/48] lucene-solr:jira/lucene-2562: Add luke as a sub-module.

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/dc72c4a0/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/DocumentsController.java
----------------------------------------------------------------------
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/DocumentsController.java b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/DocumentsController.java
new file mode 100644
index 0000000..4247459
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/DocumentsController.java
@@ -0,0 +1,581 @@
+/*
+ * 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.controllers;
+
+import com.google.inject.Inject;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.geometry.Insets;
+import javafx.geometry.Orientation;
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Hyperlink;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListView;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.Spinner;
+import javafx.scene.control.SpinnerValueFactory;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TextFormatter;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.Clipboard;
+import javafx.scene.input.ClipboardContent;
+import javafx.scene.input.KeyCode;
+import javafx.scene.layout.FlowPane;
+import javafx.stage.Stage;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.luke.app.IndexObserver;
+import org.apache.lucene.luke.app.LukeState;
+import org.apache.lucene.luke.app.controllers.dialog.documents.AddDocumentController;
+import org.apache.lucene.luke.app.controllers.dialog.documents.DocValuesController;
+import org.apache.lucene.luke.app.controllers.dialog.HelpController;
+import org.apache.lucene.luke.app.controllers.dialog.documents.StoredValueController;
+import org.apache.lucene.luke.app.controllers.dialog.documents.TermVectorController;
+import org.apache.lucene.luke.app.controllers.dto.documents.DocumentField;
+import org.apache.lucene.luke.app.controllers.dto.documents.TermPosting;
+import org.apache.lucene.luke.app.util.DialogOpener;
+import org.apache.lucene.luke.app.util.IntegerTextFormatter;
+import org.apache.lucene.luke.models.LukeException;
+import org.apache.lucene.luke.models.documents.DocValues;
+import org.apache.lucene.luke.models.documents.Documents;
+import org.apache.lucene.luke.models.documents.DocumentsFactory;
+import org.apache.lucene.luke.models.documents.TermVectorEntry;
+import org.apache.lucene.luke.app.util.MessageUtils;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static org.apache.lucene.luke.app.util.ExceptionHandler.runnableWrapper;
+
+public class DocumentsController extends ChildTabController implements IndexObserver {
+
+  private final DocumentsFactory documentsFactory;
+
+  private Documents documentsModel;
+
+  private Analyzer currentAnalyzer;
+
+  @FXML
+  private Spinner<Integer> docNum;
+
+  @FXML
+  private Hyperlink mltSearch;
+
+  @FXML
+  private Button addDoc;
+
+  @FXML
+  private Label maxDocs;
+
+  @FXML
+  private ChoiceBox<String> field;
+
+  @FXML
+  private TextField term;
+
+  @FXML
+  private Button firstTerm;
+
+  @FXML
+  private Button nextTerm;
+
+  @FXML
+  private Label showedDocNum;
+
+  @FXML
+  private TextField showedTerm;
+
+  @FXML
+  private Label termDocsNum;
+
+  @FXML
+  private TextField termDocIdx;
+
+  @FXML
+  private Button firstTermDoc;
+
+  @FXML
+  private Button nextTermDoc;
+
+  @FXML
+  private TableView<TermPosting> posTable;
+
+  @FXML
+  private TableColumn<TermPosting, Integer> posColumn;
+
+  @FXML
+  private TableColumn<TermPosting, String> offsetColumn;
+
+  @FXML
+  private TableColumn<TermPosting, String> payloadColumn;
+
+  private ObservableList<TermPosting> posList;
+
+  @FXML
+  private TableView<DocumentField> documentTable;
+
+  @FXML
+  private TableColumn<DocumentField, String> fieldColumn;
+
+  @FXML
+  private TableColumn<DocumentField, String> flagColumn;
+
+  @FXML
+  private TableColumn<DocumentField, Long> normColumn;
+
+  @FXML
+  private TableColumn<DocumentField, String> valueColumn;
+
+  private ObservableList<DocumentField> documentFieldList;
+
+  @Inject
+  public DocumentsController(DocumentsFactory documentsFactory) {
+    this.documentsFactory = documentsFactory;
+  }
+
+  @FXML
+  private void initialize() {
+    // initialize postings table view
+    posColumn.setCellValueFactory(new PropertyValueFactory<>("position"));
+    offsetColumn.setCellValueFactory(new PropertyValueFactory<>("offset"));
+    payloadColumn.setCellValueFactory(new PropertyValueFactory<>("payload"));
+    posList = FXCollections.observableArrayList();
+    posTable.setItems(posList);
+
+    // initialize documents table view
+    fieldColumn.setCellValueFactory(new PropertyValueFactory<>("field"));
+    flagColumn.setCellValueFactory(new PropertyValueFactory<>("flag"));
+    normColumn.setCellValueFactory(new PropertyValueFactory<>("norm"));
+    valueColumn.setCellValueFactory(new PropertyValueFactory<>("value"));
+    documentFieldList = FXCollections.observableArrayList();
+    documentTable.setItems(documentFieldList);
+    documentTable.setContextMenu(createDocTableMenu());
+
+    ImageView imageView = new ImageView(new Image("/img/icon_question_alt2.png"));
+    imageView.setFitWidth(12);
+    imageView.setFitHeight(12);
+    Hyperlink helpLink = new Hyperlink(MessageUtils.getLocalizedMessage("label.help"), imageView);
+    helpLink.setOnMouseClicked(e -> runnableWrapper(this::showFlagsHelpDialog));
+    Label flagLabel = new Label("Flags");
+    flagLabel.setPadding(new Insets(0, 30, 0, 0));
+    FlowPane flowPane = new FlowPane();
+    flowPane.setOrientation(Orientation.HORIZONTAL);
+    flowPane.setMaxWidth(Double.MAX_VALUE);
+    flowPane.setAlignment(Pos.CENTER);
+    flowPane.getChildren().addAll(flagLabel, helpLink);
+    flagColumn.setGraphic(flowPane);
+
+    firstTerm.setOnAction(e -> runnableWrapper(this::showFirstTerm));
+    nextTerm.setOnAction(e -> runnableWrapper(this::showNextTerm));
+    term.setOnKeyPressed(e -> runnableWrapper(() -> {
+      if (e.getCode() == KeyCode.ENTER) {
+        seekTermCeil();
+      }
+    }));
+
+    firstTermDoc.setOnAction(e -> runnableWrapper(this::showFirstTermDoc));
+    nextTermDoc.setOnAction(e -> runnableWrapper(this::showNextTermDoc));
+
+    nextTerm.setDisable(true);
+    term.setEditable(false);
+    firstTermDoc.setDisable(true);
+    nextTermDoc.setDisable(true);
+
+    mltSearch.setOnAction(e -> runnableWrapper(() -> {
+      getSearchController().mltSearch(docNum.getValue());
+      switchTab(LukeController.Tab.SEARCH);
+    }));
+
+    addDoc.setOnAction(e -> runnableWrapper(this::showAddDocumentDialog));
+  }
+
+  @Override
+  public void openIndex(LukeState state) throws LukeException {
+    documentsModel = documentsFactory.newInstance(state.getIndexReader());
+
+    addDoc.setDisable(state.readOnly() || !state.hasDirectoryReader());
+
+    int maxDoc = documentsModel.getMaxDoc();
+    maxDocs.setText(String.format("in %d docs", maxDoc));
+    if (maxDoc > 0) {
+      int max = Math.max(maxDoc - 1, 0);
+      SpinnerValueFactory.IntegerSpinnerValueFactory valueFactory =
+          new SpinnerValueFactory.IntegerSpinnerValueFactory(0, max, 0, 1);
+      docNum.setValueFactory(valueFactory);
+      TextFormatter<Integer> textFormatter = new IntegerTextFormatter(valueFactory.getConverter(), 0);
+      docNum.getEditor().setTextFormatter(textFormatter);
+      docNum.valueProperty().addListener(e -> runnableWrapper(() ->
+          showDoc(docNum.getValueFactory().getValue())
+      ));
+      docNum.focusedProperty().addListener((obs, oldV, newV) -> runnableWrapper(() -> {
+        if (newV) {
+          // won't not change value, but commit editor
+          // https://stackoverflow.com/questions/32340476/manually-typing-in-text-in-javafx-spinner-is-not-updating-the-value-unless-user
+          docNum.increment(0);
+          showDoc(docNum.getValueFactory().getValue());
+        }
+      }));
+    } else {
+      docNum.setDisable(true);
+    }
+
+    List<String> fields = documentsModel.getFieldNames().stream().sorted().collect(Collectors.toList());
+    field.getItems().addAll(fields);
+    if (fields.size() > 0) {
+      field.setValue(fields.get(0));
+    }
+    field.setOnAction(e -> runnableWrapper(this::showFirstTerm));
+
+    if (documentsModel.getMaxDoc() > 0) {
+      showDoc(0);
+    }
+  }
+
+  @Override
+  public void closeIndex() {
+    documentsModel = null;
+
+    maxDocs.setText("in ? docs");
+    docNum.getEditor().setText("");
+    field.getItems().clear();
+    term.setText("");
+    showedDocNum.setText("");
+    showedTerm.setText("");
+    termDocsNum.setText("");
+    termDocIdx.setText("");
+    posList.clear();
+    documentFieldList.clear();
+  }
+
+  private void showDoc(int docid) throws LukeException {
+    showedDocNum.setText(String.valueOf(docid));
+    documentFieldList.clear();
+
+    List<DocumentField> doc = documentsModel.getDocumentFields(docid).stream()
+            .map(DocumentField::of)
+            .collect(Collectors.toList());
+    documentFieldList.addAll(doc);
+    clearStatusMessage();
+  }
+
+  private void showFirstTerm() {
+    String fieldName = field.getValue();
+    if (fieldName == null || fieldName.length() == 0) {
+      showStatusMessage(MessageUtils.getLocalizedMessage("documents.field.message.not_selected"));
+      return;
+    }
+
+    termDocIdx.setText("");
+    posList.clear();
+
+    String firstTermText = documentsModel.firstTerm(fieldName).map(Term::text).orElse("");
+    term.setText(firstTermText);
+    showedTerm.setText(firstTermText);
+    if (showedTerm.getText().length() > 0) {
+      String num = documentsModel.getDocFreq().map(String::valueOf).orElse("?");
+      termDocsNum.setText(String.format("in %s docs", num));
+
+      nextTerm.setDisable(false);
+      term.setEditable(true);
+      firstTermDoc.setDisable(false);
+    } else {
+      nextTerm.setDisable(true);
+      term.setEditable(false);
+      firstTermDoc.setDisable(true);
+    }
+    nextTermDoc.setDisable(true);
+
+  }
+
+  private void showNextTerm() throws LukeException {
+    termDocIdx.setText("");
+    posList.clear();
+
+    String nextTermText = documentsModel.nextTerm().map(Term::text).orElse("");
+    term.setText(nextTermText);
+    showedTerm.setText(nextTermText);
+    if (showedTerm.getText().length() > 0) {
+      String num = documentsModel.getDocFreq().map(String::valueOf).orElse("?");
+      termDocsNum.setText(String.format("in %s docs", num));
+
+      term.setEditable(true);
+      firstTermDoc.setDisable(false);
+    } else {
+      nextTerm.setDisable(true);
+      term.setEditable(false);
+      firstTermDoc.setDisable(true);
+    }
+    nextTermDoc.setDisable(true);
+
+    clearStatusMessage();
+  }
+
+  private void seekTermCeil() throws LukeException {
+    termDocIdx.setText("");
+    posList.clear();
+    String termText = term.getText();
+
+    String nextTermText = documentsModel.seekTerm(termText).map(Term::text).orElse("");
+    term.setText(nextTermText);
+    showedTerm.setText(nextTermText);
+    if (showedTerm.getText().length() > 0) {
+      String num = documentsModel.getDocFreq().map(String::valueOf).orElse("?");
+      termDocsNum.setText(String.format("in %s docs", num));
+
+      term.setEditable(true);
+      firstTermDoc.setDisable(false);
+    } else {
+      nextTerm.setDisable(true);
+      term.setEditable(false);
+      firstTermDoc.setDisable(true);
+    }
+    nextTermDoc.setDisable(true);
+
+    clearStatusMessage();
+  }
+
+  private void showFirstTermDoc() throws LukeException {
+    int doc = documentsModel.firstTermDoc().orElse(-1);
+    if (doc < 0) {
+      nextTermDoc.setDisable(true);
+      showStatusMessage(MessageUtils.getLocalizedMessage("documents.termdocs.message.not_available"));
+      return;
+    }
+    termDocIdx.setText(String.valueOf(1));
+    docNum.getEditor().setText(String.valueOf(doc));
+    showDoc(doc);
+    List<org.apache.lucene.luke.models.documents.TermPosting> postings = documentsModel.getTermPositions();
+    posList.clear();
+    posList.addAll(
+        postings.stream()
+            .filter(p -> p.getPosition() >= 0)
+            .map(TermPosting::of)
+            .collect(Collectors.toList()));
+
+    nextTermDoc.setDisable(false);
+    clearStatusMessage();
+  }
+
+  private void showNextTermDoc() throws LukeException {
+    int doc = documentsModel.nextTermDoc().orElse(-1);
+    if (doc < 0) {
+      nextTermDoc.setDisable(true);
+      showStatusMessage(MessageUtils.getLocalizedMessage("documents.termdocs.message.not_available"));
+      return;
+    }
+    int curIdx = Integer.parseInt(termDocIdx.getCharacters().toString());
+    termDocIdx.setText(String.valueOf(curIdx + 1));
+    docNum.getEditor().setText(String.valueOf(doc));
+    showDoc(doc);
+    List<org.apache.lucene.luke.models.documents.TermPosting> postings = documentsModel.getTermPositions();
+    posList.clear();
+    posList.addAll(
+        postings.stream()
+            .filter(p -> p.getPosition() >= 0)
+            .map(TermPosting::of)
+            .collect(Collectors.toList()));
+
+    nextTermDoc.setDisable(false);
+    clearStatusMessage();
+  }
+
+  private Stage addDocumentDialog;
+
+  private void showAddDocumentDialog() throws Exception {
+    addDocumentDialog = new DialogOpener<AddDocumentController>(getParent()).show(
+        addDocumentDialog,
+        "Add Document",
+        "/fxml/dialog/documents/add_document.fxml",
+        600, 500,
+        (controller) -> {
+          controller.setParent(getParent(), this);
+          controller.setAnalyzer(currentAnalyzer);
+        }
+    );
+  }
+
+  private Stage flagsHelpDialog;
+
+  private void showFlagsHelpDialog() throws Exception {
+    String desc = "Format: IdfpoNPSB#txxVDtxxxxTx/x";
+    ObservableList<String> items = FXCollections.observableArrayList(
+        "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)"
+    );
+    ListView<String> content = new ListView<>(items);
+    flagsHelpDialog = new DialogOpener<HelpController>(getParent()).show(
+        flagsHelpDialog,
+        "About Flags",
+        "/fxml/dialog/help.fxml",
+        600, 350,
+        (controller) -> {
+          controller.setDescription(desc);
+          controller.setContent(content);
+        }
+    );
+  }
+
+  private ContextMenu createDocTableMenu() {
+    ContextMenu menu = new ContextMenu();
+    // show term vector
+    MenuItem item1 = new MenuItem(MessageUtils.getLocalizedMessage("documents.doctable.menu.item1"));
+    item1.setOnAction(event -> runnableWrapper(() -> {
+      String selectedField = documentTable.getSelectionModel().getSelectedItem().getField();
+      showTermVectorDialog(selectedField);
+    }));
+
+    // show doc values
+    MenuItem item2 = new MenuItem(MessageUtils.getLocalizedMessage("documents.doctable.menu.item2"));
+    item2.setOnAction(event -> runnableWrapper(() -> {
+      DocumentField selected = documentTable.getSelectionModel().getSelectedItem();
+      showDocValuesDialog(selected.getField());
+    }));
+
+    // show stored value
+    MenuItem item3 = new MenuItem(MessageUtils.getLocalizedMessage("documents.doctable.menu.item3"));
+    item3.setOnAction(event -> runnableWrapper(() -> {
+      DocumentField selected = documentTable.getSelectionModel().getSelectedItem();
+      showStoredValueDialog(selected.getField(), selected.getValue());
+    }));
+
+    // copy stored value to clipboard
+    MenuItem item4 = new MenuItem(MessageUtils.getLocalizedMessage("documents.doctable.menu.item4"));
+    item4.setOnAction(event -> {
+      DocumentField selected = documentTable.getSelectionModel().getSelectedItem();
+      copyStoredValue(selected.getField(), selected.getValue());
+    });
+
+    menu.getItems().addAll(item1, item2, item3, item4);
+    return menu;
+  }
+
+  private Stage termVectorDialog = null;
+
+  private void showTermVectorDialog(@Nonnull String field) throws Exception {
+    int docid = Integer.parseInt(showedDocNum.getText());
+    List<TermVectorEntry> tvEntries = documentsModel.getTermVectors(docid, field);
+    if (tvEntries.isEmpty()) {
+      showStatusMessage(MessageUtils.getLocalizedMessage("documents.termvector.message.not_available", field, docid));
+      return;
+    }
+
+    termVectorDialog = new DialogOpener<TermVectorController>(getParent()).show(
+        termVectorDialog,
+        "Term Vector",
+        "/fxml/dialog/documents/termvector.fxml",
+        400, 300,
+        (controller) -> controller.setTermVector(field, tvEntries));
+
+    clearStatusMessage();
+  }
+
+  private Stage docValuesDialog = null;
+
+  private void showDocValuesDialog(@Nonnull String field) throws Exception {
+    int docid = Integer.parseInt(showedDocNum.getText());
+    Optional<DocValues> docValues = documentsModel.getDocValues(docid, field);
+    if (docValues.isPresent()) {
+      docValuesDialog = new DialogOpener<DocValuesController>(getParent()).show(
+          docValuesDialog,
+          "Doc Values",
+          "/fxml/dialog/documents/docvalues.fxml",
+          400, 300,
+          (controller) -> controller.setValue(field, docValues.get()));
+      clearStatusMessage();
+    } else {
+      showStatusMessage(MessageUtils.getLocalizedMessage("documents.docvalues.message.not_available", field, docid));
+    }
+  }
+
+  private Stage storedValueDialog = null;
+
+  private void showStoredValueDialog(@Nonnull String field, @Nullable String stored) throws Exception {
+    if (stored == null || stored.length() == 0) {
+      int docid = Integer.parseInt(showedDocNum.getText());
+      showStatusMessage(MessageUtils.getLocalizedMessage("documents.stored.message.not_availabe", field, docid));
+      return;
+    }
+    storedValueDialog = new DialogOpener<StoredValueController>(getParent()).show(
+        storedValueDialog,
+        "Stored Value",
+        "/fxml/dialog/documents/stored.fxml",
+        400, 300,
+        (controller) -> controller.setValue(field, stored));
+
+    clearStatusMessage();
+  }
+
+  private void copyStoredValue(@Nonnull String field, @Nullable String stored) {
+    if (stored == null || stored.length() == 0) {
+      int docid = Integer.parseInt(showedDocNum.getText());
+      showStatusMessage(MessageUtils.getLocalizedMessage("documents.stored.message.not_availabe", field, docid));
+      return;
+    }
+
+    Clipboard clipboard = Clipboard.getSystemClipboard();
+    ClipboardContent content = new ClipboardContent();
+    content.putString(stored);
+    clipboard.setContent(content);
+
+    clearStatusMessage();
+  }
+
+  // -------------------------------------------------
+  // methods for interaction with other controllers
+  // -------------------------------------------------
+
+  void browseDocsByTerm(@Nonnull String fieldName, @Nonnull String termText) throws LukeException {
+    field.setValue(fieldName);
+    term.setText(termText);
+    seekTermCeil();
+    showFirstTermDoc();
+  }
+
+  void displayDoc(int docId) {
+    docNum.getEditor().setText(String.valueOf(docId));
+  }
+
+  public void displayLatestDoc() throws LukeException {
+    int docId = documentsModel.getMaxDoc() - 1;
+    docNum.getEditor().setText(String.valueOf(docId));
+    showDoc(docId);
+  }
+
+  void setCurrentAnalyzer(Analyzer analyzer) {
+    this.currentAnalyzer = analyzer;
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/dc72c4a0/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/LogsController.java
----------------------------------------------------------------------
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/LogsController.java b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/LogsController.java
new file mode 100644
index 0000000..f93b259
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/LogsController.java
@@ -0,0 +1,43 @@
+/*
+ * 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.controllers;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextArea;
+import org.apache.lucene.luke.app.util.TextAreaAppender;
+
+import java.io.File;
+
+public class LogsController {
+
+  private static final String LOG_FILE = System.getProperty("user.home") + File.separator + ".luke.d" + File.separator + "luke.log";
+
+  @FXML
+  private Label logFile;
+
+  @FXML
+  private TextArea textArea;
+
+  @FXML
+  private void initialize() {
+    logFile.setText(LOG_FILE);
+    TextAreaAppender.textArea = textArea;
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/dc72c4a0/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/LukeController.java
----------------------------------------------------------------------
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/LukeController.java b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/LukeController.java
new file mode 100644
index 0000000..e5df21f
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/LukeController.java
@@ -0,0 +1,304 @@
+/*
+ * 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.controllers;
+
+import com.google.inject.Inject;
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.control.MenuBar;
+import javafx.scene.control.TabPane;
+import javafx.scene.control.Tooltip;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.AnchorPane;
+import javafx.stage.Window;
+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.util.MessageUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+import static org.apache.lucene.luke.app.util.ExceptionHandler.runnableWrapper;
+
+public class LukeController implements IndexObserver, DirectoryObserver {
+
+  private static final Logger logger = LoggerFactory.getLogger(LukeController.class);
+
+  @FXML
+  private AnchorPane primary;
+
+  @FXML
+  private MenuBar menuBar;
+
+  @FXML
+  private TabPane tabPane;
+
+  @FXML
+  private AnchorPane overview;
+
+  @FXML
+  private MenubarController menubarController;
+
+  @FXML
+  private OverviewController overviewController;
+
+  @FXML
+  private DocumentsController documentsController;
+
+  @FXML
+  private SearchController searchController;
+
+  @FXML
+  private AnalysisController analysisController;
+
+  @FXML
+  private CommitsController commitsController;
+
+  @FXML
+  private Label statusMessage;
+
+  @FXML
+  private ImageView multiIcon;
+
+  @FXML
+  private ImageView roIcon;
+
+  @FXML
+  private ImageView noReaderIcon;
+
+  private Preferences prefs;
+
+  private DirectoryHandler directoryHandler;
+
+  private IndexHandler indexHandler;
+
+  @FXML
+  private void initialize() {
+    menubarController.setMainController(this);
+    overviewController.setParent(this);
+    documentsController.setParent(this);
+    searchController.setParent(this);
+    analysisController.setParent(this);
+
+    directoryHandler.addObserver(menubarController);
+    directoryHandler.addObserver(commitsController);
+    directoryHandler.addObserver(this);
+
+    indexHandler.addObserver(menubarController);
+    indexHandler.addObserver(overviewController);
+    indexHandler.addObserver(documentsController);
+    indexHandler.addObserver(searchController);
+    indexHandler.addObserver(commitsController);
+    indexHandler.addObserver(this);
+
+    // disable tabs until an index opened.
+    tabPane.getTabs().get(Tab.OVERVIEW.index()).setDisable(true);
+    tabPane.getTabs().get(Tab.DOCUMENTS.index()).setDisable(true);
+    tabPane.getTabs().get(Tab.SEARCH.index()).setDisable(true);
+    tabPane.getTabs().get(Tab.COMMITS.index()).setDisable(true);
+
+    tabPane.getSelectionModel().selectedIndexProperty().addListener((obs, oldV, newV) ->
+        runnableWrapper(() -> {
+          clearStatusMessage();
+          if (newV.equals(Tab.DOCUMENTS.index())) {
+            documentsController.setCurrentAnalyzer(analysisController.getCurrentAnalyzer());
+          }
+          if (newV.equals(Tab.SEARCH.index())) {
+            searchController.setCurrentAnalyzer(analysisController.getCurrentAnalyzer());
+          }
+        }));
+
+    multiIcon.setVisible(false);
+    Tooltip.install(multiIcon, new Tooltip(MessageUtils.getLocalizedMessage("tooltip.multi_reader")));
+
+    roIcon.setVisible(false);
+    Tooltip.install(roIcon, new Tooltip(MessageUtils.getLocalizedMessage("tooltip.read_only")));
+
+    noReaderIcon.setVisible(false);
+    Tooltip.install(noReaderIcon, new Tooltip(MessageUtils.getLocalizedMessage("tooltip.no_reader")));
+  }
+
+  public void showOpenIndexDialog() throws Exception {
+    menubarController.showOpenIndexDialog();
+  }
+
+  // -------------------------------------------------
+  // methods for interaction with other controllers
+  // -------------------------------------------------
+
+  public Window getPrimaryWindow() {
+    return primary.getScene().getWindow();
+  }
+
+  OverviewController getOverviewController() {
+    return overviewController;
+  }
+
+  DocumentsController getDocumentsController() {
+    return documentsController;
+  }
+
+  SearchController getSearchController() {
+    return searchController;
+  }
+
+  @Override
+  public void openDirectory(LukeState state) {
+    roIcon.setVisible(false);
+    noReaderIcon.setVisible(true);
+
+    tabPane.getTabs().get(Tab.COMMITS.index()).setDisable(false);
+    showStatusMessage(MessageUtils.getLocalizedMessage("message.directory_opened"));
+  }
+
+  @Override
+  public void closeDirectory() {
+    roIcon.setVisible(false);
+    noReaderIcon.setVisible(false);
+
+    switchTab(LukeController.Tab.OVERVIEW);
+    showStatusMessage(MessageUtils.getLocalizedMessage("message.directory_closed"));
+  }
+
+  @Override
+  public void openIndex(LukeState state) {
+
+    if (state.hasDirectoryReader()) {
+      multiIcon.setVisible(false);
+    } else {
+      multiIcon.setVisible(true);
+    }
+
+    if (state.readOnly()) {
+      roIcon.setVisible(true);
+    } else {
+      roIcon.setVisible(false);
+    }
+
+    noReaderIcon.setVisible(false);
+
+    // enable tabs
+    tabPane.getTabs().get(Tab.OVERVIEW.index()).setDisable(false);
+    tabPane.getTabs().get(Tab.DOCUMENTS.index()).setDisable(false);
+    tabPane.getTabs().get(Tab.SEARCH.index()).setDisable(false);
+    if (state.hasDirectoryReader()) {
+      tabPane.getTabs().get(Tab.COMMITS.index()).setDisable(false);
+    }
+
+    // show Overview tab
+    switchTab(LukeController.Tab.OVERVIEW);
+
+    if (state.readOnly()) {
+      showStatusMessage(MessageUtils.getLocalizedMessage("message.index_opened_ro"));
+    } else if (!state.hasDirectoryReader()) {
+      showStatusMessage(MessageUtils.getLocalizedMessage("message.index_opened_multi"));
+    } else {
+      showStatusMessage(MessageUtils.getLocalizedMessage("message.index_opened"));
+    }
+
+    documentsController.setCurrentAnalyzer(analysisController.getCurrentAnalyzer());
+    searchController.setCurrentAnalyzer(analysisController.getCurrentAnalyzer());
+  }
+
+  @Override
+  public void closeIndex() {
+    multiIcon.setVisible(false);
+    roIcon.setVisible(false);
+    noReaderIcon.setVisible(false);
+
+    // disable tabs until index re-opened.
+    tabPane.getTabs().get(Tab.OVERVIEW.index()).setDisable(true);
+    tabPane.getTabs().get(Tab.DOCUMENTS.index()).setDisable(true);
+    tabPane.getTabs().get(Tab.SEARCH.index()).setDisable(true);
+    tabPane.getTabs().get(Tab.COMMITS.index()).setDisable(true);
+
+    switchTab(LukeController.Tab.OVERVIEW);
+    showStatusMessage(MessageUtils.getLocalizedMessage("message.index_closed"));
+  }
+
+  public void switchTab(Tab tab) {
+    tabPane.getSelectionModel().select(tab.index());
+  }
+
+  @Inject
+  public LukeController(Preferences prefs, DirectoryHandler directoryHandler, IndexHandler indexHandler) {
+    this.prefs = prefs;
+    this.directoryHandler = directoryHandler;
+    this.indexHandler = indexHandler;
+  }
+
+  void setColorTheme(ColorTheme colorTheme) {
+    try {
+      prefs.setTheme(colorTheme);
+    } catch (IOException e) {
+      logger.error(e.getMessage(), e);
+    }
+    resetStyles();
+  }
+
+  public void resetStyles() {
+    getPrimaryWindow().getScene().getStylesheets().clear();
+    getPrimaryWindow().getScene().getStylesheets().addAll(
+        getClass().getResource("/styles/luke.css").toExternalForm(),
+        getClass().getResource(prefs.getTheme().resourceName()).toExternalForm()
+    );
+  }
+
+  public String getStyleResourceName() {
+    return prefs.getTheme().resourceName();
+  }
+
+  public void showStatusMessage(String message) {
+    statusMessage.setText(message);
+  }
+
+  public void showUnknownErrorMessage() {
+    statusMessage.setText(MessageUtils.getLocalizedMessage("message.error.unknown"));
+  }
+
+  public void clearStatusMessage() {
+    statusMessage.setText("");
+  }
+
+  public enum Tab {
+    OVERVIEW(0), DOCUMENTS(1), SEARCH(2), ANALYZER(3), COMMITS(4);
+
+    private int tabIdx;
+
+    Tab(int tabIdx) {
+      this.tabIdx = tabIdx;
+    }
+
+    int index() {
+      return tabIdx;
+    }
+  }
+
+  public enum ColorTheme {
+    GRAY, CLASSIC, SANDSTONE, NAVY;
+
+    String resourceName() {
+      return String.format("/styles/theme_%s.css", name().toLowerCase());
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/dc72c4a0/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/MenubarController.java
----------------------------------------------------------------------
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/MenubarController.java b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/MenubarController.java
new file mode 100644
index 0000000..2f0cb7b
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/MenubarController.java
@@ -0,0 +1,228 @@
+/*
+ * 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.controllers;
+
+import com.google.inject.Inject;
+import javafx.fxml.FXML;
+import javafx.scene.control.MenuItem;
+import javafx.stage.Stage;
+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.controllers.dialog.menubar.AboutController;
+import org.apache.lucene.luke.app.controllers.dialog.menubar.CheckIndexController;
+import org.apache.lucene.luke.app.controllers.dialog.menubar.OpenIndexController;
+import org.apache.lucene.luke.app.controllers.dialog.menubar.OptimizeController;
+import org.apache.lucene.luke.app.desktop.Preferences;
+import org.apache.lucene.luke.app.util.DialogOpener;
+import org.apache.lucene.luke.models.LukeException;
+import org.apache.lucene.util.Version;
+
+import java.io.IOException;
+
+import static org.apache.lucene.luke.app.util.ExceptionHandler.runnableWrapper;
+
+public class MenubarController implements IndexObserver, DirectoryObserver {
+
+  private final Preferences prefs;
+
+  private final DirectoryHandler directoryHandler;
+
+  private final IndexHandler indexHandler;
+
+  private LukeController mainController;
+
+  @FXML
+  private MenuItem menuOpenIndex;
+
+  @FXML
+  private MenuItem menuReopenIndex;
+
+  @FXML
+  private MenuItem menuCloseIndex;
+
+  @FXML
+  private MenuItem menuExit;
+
+  @FXML
+  private MenuItem menuOptimizeIndex;
+
+  @FXML
+  private MenuItem menuCheckIndex;
+
+  @FXML
+  private MenuItem menuThemeGray;
+
+  @FXML
+  private MenuItem menuThemeClassic;
+
+  @FXML
+  private MenuItem menuThemeSandstone;
+
+  @FXML
+  private MenuItem menuThemeNavy;
+
+  @FXML
+  private MenuItem menuAbout;
+
+  @Inject
+  public MenubarController(Preferences prefs, DirectoryHandler directoryHandler, IndexHandler indexHandler) {
+    this.prefs = prefs;
+    this.directoryHandler = directoryHandler;
+    this.indexHandler = indexHandler;
+  }
+
+  @FXML
+  private void initialize() {
+    menuOpenIndex.setOnAction(e -> runnableWrapper(this::showOpenIndexDialog));
+
+    menuReopenIndex.setOnAction(e -> runnableWrapper(() -> {
+      indexHandler.reOpen();
+    }));
+    menuReopenIndex.setDisable(true);
+
+    menuCloseIndex.setOnAction(e -> runnableWrapper(() -> {
+      directoryHandler.close();
+      indexHandler.close();
+    }));
+    menuCloseIndex.setDisable(true);
+
+    menuExit.setOnAction(e -> exit());
+
+    menuOptimizeIndex.setOnAction(e -> runnableWrapper(this::showOptimizeDialog));
+    menuOptimizeIndex.setDisable(true);
+
+    menuCheckIndex.setOnAction(e -> runnableWrapper(this::showCheckIndexDialog));
+    menuCheckIndex.setDisable(true);
+
+    menuThemeGray.setOnAction(e -> runnableWrapper(() -> changeTheme(LukeController.ColorTheme.GRAY)));
+    menuThemeClassic.setOnAction(e -> runnableWrapper(() -> changeTheme(LukeController.ColorTheme.CLASSIC)));
+    menuThemeSandstone.setOnAction(e -> runnableWrapper(() -> changeTheme(LukeController.ColorTheme.SANDSTONE)));
+    menuThemeNavy.setOnAction(e -> runnableWrapper(() -> changeTheme(LukeController.ColorTheme.NAVY)));
+
+    menuAbout.setOnAction(e -> runnableWrapper(this::showAboutDialog));
+  }
+
+  private Stage openIndexDialog = null;
+
+  public void showOpenIndexDialog() throws Exception {
+    openIndexDialog = new DialogOpener<OpenIndexController>(mainController).show(
+        openIndexDialog,
+        "Choose index directory path",
+        "/fxml/dialog/menubar/openindex.fxml",
+        600, 400,
+        (controller) -> {
+          controller.setMainController(mainController);
+        }
+    );
+  }
+
+  private Stage optimizeDialog = null;
+
+  private void showOptimizeDialog() throws Exception {
+    optimizeDialog = new DialogOpener<OptimizeController>(mainController).show(
+        optimizeDialog,
+        "Optimize index",
+        "/fxml/dialog/menubar/optimize.fxml",
+        600, 600,
+        (controller) -> {}
+    );
+  }
+
+  private Stage checkIndexDialog = null;
+
+  private void showCheckIndexDialog() throws Exception {
+    checkIndexDialog = new DialogOpener<CheckIndexController>(mainController).show(
+        checkIndexDialog,
+        "Check index",
+        "/fxml/dialog/menubar/checkindex.fxml",
+        600, 600,
+        (controller) -> {}
+    );
+  }
+
+  private void changeTheme(LukeController.ColorTheme theme) throws IOException {
+    prefs.setTheme(theme);
+    mainController.setColorTheme(theme);
+  }
+
+  private Stage aboutDialog = null;
+
+  private void showAboutDialog() throws Exception {
+    String version = Version.LATEST.toString();
+    aboutDialog = new DialogOpener<AboutController>(mainController).show(
+        aboutDialog,
+        "About Luke v" + version,
+        "/fxml/dialog/menubar/about.fxml",
+        1000, 480,
+        (controller) -> {},
+        "/styles/about.css"
+    );
+
+  }
+
+  private void exit() {
+    directoryHandler.close();
+    indexHandler.close();
+    ((Stage) mainController.getPrimaryWindow()).close();
+  }
+
+  @Override
+  public void openDirectory(LukeState state) {
+    menuReopenIndex.setDisable(true);
+    menuCloseIndex.setDisable(false);
+    menuOptimizeIndex.setDisable(true);
+    menuCheckIndex.setDisable(false);
+  }
+
+  @Override
+  public void openIndex(LukeState state) throws LukeException {
+    menuReopenIndex.setDisable(false);
+    menuCloseIndex.setDisable(false);
+    if (!state.readOnly() && state.hasDirectoryReader()) {
+      menuOptimizeIndex.setDisable(false);
+    }
+    if (state.hasDirectoryReader()) {
+      menuCheckIndex.setDisable(false);
+    }
+  }
+
+  @Override
+  public void closeDirectory() {
+    close();
+  }
+
+  @Override
+  public void closeIndex() {
+    close();
+  }
+
+  private void close() {
+    menuReopenIndex.setDisable(true);
+    menuCloseIndex.setDisable(true);
+    menuOptimizeIndex.setDisable(true);
+    menuCheckIndex.setDisable(true);
+  }
+
+  void setMainController(LukeController mainController) {
+    this.mainController = mainController;
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/dc72c4a0/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/OverviewController.java
----------------------------------------------------------------------
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/OverviewController.java b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/OverviewController.java
new file mode 100644
index 0000000..66634e9
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/OverviewController.java
@@ -0,0 +1,287 @@
+/*
+ * 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.controllers;
+
+import com.google.inject.Inject;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Label;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.Spinner;
+import javafx.scene.control.SpinnerValueFactory;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TextFormatter;
+import javafx.scene.control.Tooltip;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import org.apache.lucene.luke.app.IndexObserver;
+import org.apache.lucene.luke.app.LukeState;
+import org.apache.lucene.luke.app.controllers.dto.overview.TermCount;
+import org.apache.lucene.luke.app.controllers.dto.overview.TopTerm;
+import org.apache.lucene.luke.app.util.IntegerTextFormatter;
+import org.apache.lucene.luke.models.LukeException;
+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;
+import org.apache.lucene.luke.app.util.MessageUtils;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static org.apache.lucene.luke.app.util.ExceptionHandler.runnableWrapper;
+
+public class OverviewController extends ChildTabController implements IndexObserver {
+
+  private final OverviewFactory overviewFactory;
+
+  private Overview overviewModel;
+
+  @FXML
+  private Label indexPath;
+
+  @FXML
+  private Label mode;
+
+  @FXML
+  private Label numFields;
+
+  @FXML
+  private Label numDocs;
+
+  @FXML
+  private Label numTerms;
+
+  @FXML
+  private Label delOpt;
+
+  @FXML
+  private Label indexVersion;
+
+  @FXML
+  private Label indexFormat;
+
+  @FXML
+  private Label dirImpl;
+
+  @FXML
+  private Label commitPoint;
+
+  @FXML
+  private Label userData;
+
+  @FXML
+  private TableView<TermCount> termCountTable;
+
+  @FXML
+  private TableColumn<TermCount, String> fieldColumn;
+
+  @FXML
+  private TableColumn<TermCount, Long> countColumn;
+
+  @FXML
+  private TableColumn<TermCount, Double> ratioColumn;
+
+  private ObservableList<TermCount> termCountList;
+
+  @FXML
+  private TextField selectedField;
+
+  @FXML
+  private Button showTopTerms;
+
+  @FXML
+  private Spinner<Integer> numTopTerms;
+
+  @FXML
+  private TableView<TopTerm> topTermTable;
+
+  @FXML
+  private TableColumn<TopTerm, Integer> rankColumn;
+
+  @FXML
+  private TableColumn<TopTerm, Integer> freqColumn;
+
+  @FXML
+  private TableColumn<TopTerm, String> textColumn;
+
+  private ObservableList<TopTerm> topTermList;
+
+  @Inject
+  public OverviewController(OverviewFactory overviewFactory) {
+    this.overviewFactory = overviewFactory;
+  }
+
+  @FXML
+  private void initialize() {
+    // initialize term counts table view
+    fieldColumn.setCellValueFactory(new PropertyValueFactory<>("field"));
+    countColumn.setCellValueFactory(new PropertyValueFactory<>("count"));
+    ratioColumn.setCellValueFactory(new PropertyValueFactory<>("ratio"));
+    termCountList = FXCollections.observableArrayList();
+    termCountTable.setItems(termCountList);
+    termCountTable.getSelectionModel().selectedIndexProperty().addListener((obs, oldv, newV) -> {
+      TermCount selected = termCountTable.getSelectionModel().getSelectedItem();
+      if (selected != null) {
+        selectedField.setText(selected.getField());
+        showTopTerms.setDisable(false);
+      }
+    });
+
+    // initialize top terms table view
+    rankColumn.setCellValueFactory(new PropertyValueFactory<>("rank"));
+    freqColumn.setCellValueFactory(new PropertyValueFactory<>("freq"));
+    textColumn.setCellValueFactory(new PropertyValueFactory<>("text"));
+    topTermList = FXCollections.observableArrayList();
+    topTermTable.setItems(topTermList);
+    topTermTable.setContextMenu(createTopTermTableMenu());
+
+    // initialize num of terms spinner
+    SpinnerValueFactory.IntegerSpinnerValueFactory valueFactory =
+        new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 1000, 50, -1);
+    numTopTerms.setValueFactory(valueFactory);
+    TextFormatter<Integer> textFormatter = new IntegerTextFormatter(valueFactory.getConverter(), 50);
+    valueFactory.valueProperty().bindBidirectional(textFormatter.valueProperty());
+    numTopTerms.getEditor().setTextFormatter(textFormatter);
+    numTopTerms.focusedProperty().addListener((obs, oldV, newV) -> {
+      if (newV) {
+        // won't not change value, but commit editor
+        // https://stackoverflow.com/questions/32340476/manually-typing-in-text-in-javafx-spinner-is-not-updating-the-value-unless-user
+        numTopTerms.increment(0);
+      }
+    });
+
+    showTopTerms.setOnAction(e -> runnableWrapper(this::onShowTopTerms));
+  }
+
+  @Override
+  public void openIndex(LukeState state) throws LukeException {
+    overviewModel = overviewFactory.newInstance(state.getIndexReader(), state.getIndexPath());
+
+    indexPath.setText(state.getIndexPath());
+    indexPath.setTooltip(new Tooltip(state.getIndexPath()));
+    if (state.readOnly()) {
+      mode.setText("(read-only)");
+      ImageView imageView = new ImageView(new Image("/img/icon_lock.png"));
+      imageView.setFitWidth(12);
+      imageView.setFitHeight(12);
+      mode.setGraphic(imageView);
+      mode.setContentDisplay(ContentDisplay.RIGHT);
+      Tooltip.install(mode, new Tooltip(MessageUtils.getLocalizedMessage("tooltip.read_only")));
+    } else if (!state.hasDirectoryReader()) {
+      mode.setText("(multi-reader)");
+      ImageView imageView = new ImageView(new Image("/img/icon_grid-2x2.png"));
+      imageView.setFitWidth(12);
+      imageView.setFitHeight(12);
+      mode.setGraphic(imageView);
+      mode.setContentDisplay(ContentDisplay.RIGHT);
+      Tooltip.install(mode, new Tooltip(MessageUtils.getLocalizedMessage("tooltip.multi_reader")));
+    } else {
+      mode.setText("");
+      mode.setGraphic(null);
+    }
+    numFields.setText(String.valueOf(overviewModel.getNumFields()));
+    numDocs.setText(String.valueOf(overviewModel.getNumDocuments()));
+    numTerms.setText(String.valueOf(overviewModel.getNumTerms()));
+    String del = overviewModel.hasDeletions() ? String.format("Yes (%d)", overviewModel.getNumDeletedDocs()) : "No";
+    String opt = overviewModel.isOptimized().map(b -> b ? "Yes" : "No").orElse("?");
+    delOpt.setText(String.format("%s / %s", del, opt));
+    indexVersion.setText(overviewModel.getIndexVersion().map(v -> Long.toString(v)).orElse("?"));
+    indexFormat.setText(overviewModel.getIndexFormat().orElse(""));
+    dirImpl.setText(overviewModel.getDirImpl().orElse(""));
+    commitPoint.setText(overviewModel.getCommitDescription().orElse("---"));
+    userData.setText(overviewModel.getCommitUserData().orElse("---"));
+
+    // term counts
+    double numTerms = (double) overviewModel.getNumTerms();
+    Map<String, Long> termCounts = overviewModel.getSortedTermCounts(TermCountsOrder.COUNT_DESC);
+    termCountList.clear();
+    termCountList.addAll(
+        termCounts.entrySet()
+            .stream()
+            .map(e -> TermCount.of(e.getKey(), e.getValue(), numTerms))
+            .collect(Collectors.toList())
+    );
+
+    showTopTerms.setDisable(true);
+  }
+
+  @Override
+  public void closeIndex() {
+    indexPath.setText("");
+    indexPath.setTooltip(null);
+    numFields.setText("");
+    numDocs.setText("");
+    numTerms.setText("");
+    delOpt.setText("");
+    indexVersion.setText("");
+    indexFormat.setText("");
+    dirImpl.setText("");
+    commitPoint.setText("");
+    userData.setText("");
+    termCountList.clear();
+    selectedField.setText("");
+    topTermList.clear();
+  }
+
+  private void onShowTopTerms() throws LukeException {
+    String field = selectedField.getText();
+    Integer numTerms = numTopTerms.getValue();
+    List<TermStats> termStats = overviewModel.getTopTerms(field, numTerms);
+    topTermList.clear();
+    topTermList.addAll(
+        IntStream.range(0, Math.min(numTerms, termStats.size()))
+            .mapToObj(i -> TopTerm.of(i + 1, termStats.get(i)))
+            .collect(Collectors.toList())
+    );
+    clearStatusMessage();
+  }
+
+  private ContextMenu createTopTermTableMenu() {
+    ContextMenu menu = new ContextMenu();
+
+    // browse docs
+    MenuItem item1 = new MenuItem(MessageUtils.getLocalizedMessage("overview.toptermtable.menu.item1"));
+    item1.setOnAction(e -> runnableWrapper(() -> {
+      TopTerm selected = topTermTable.getSelectionModel().getSelectedItem();
+      getDocumentsController().browseDocsByTerm(selectedField.getText(), selected.getText());
+      switchTab(LukeController.Tab.DOCUMENTS);
+    }));
+
+    // search docs
+    MenuItem item2 = new MenuItem(MessageUtils.getLocalizedMessage("overview.toptermtable.menu.item2"));
+    item2.setOnAction(e -> runnableWrapper(() -> {
+      TopTerm selected = topTermTable.getSelectionModel().getSelectedItem();
+      getSearchController().searchByTerm(selectedField.getText(), selected.getText());
+      switchTab(LukeController.Tab.SEARCH);
+    }));
+
+    menu.getItems().addAll(item1, item2);
+    return menu;
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/dc72c4a0/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/SearchController.java
----------------------------------------------------------------------
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/SearchController.java b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/SearchController.java
new file mode 100644
index 0000000..ce2581a
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/SearchController.java
@@ -0,0 +1,446 @@
+/*
+ * 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.controllers;
+
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.scene.control.Accordion;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Label;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.TextArea;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TitledPane;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.stage.Stage;
+import javafx.util.converter.IntegerStringConverter;
+import org.apache.lucene.analysis.Analyzer;
+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.controllers.dialog.ConfirmController;
+import org.apache.lucene.luke.app.controllers.dialog.search.ExplanationController;
+import org.apache.lucene.luke.app.controllers.dto.search.SearchResult;
+import org.apache.lucene.luke.app.controllers.fragments.search.AnalyzerController;
+import org.apache.lucene.luke.app.controllers.fragments.search.FieldValuesController;
+import org.apache.lucene.luke.app.controllers.fragments.search.MLTController;
+import org.apache.lucene.luke.app.controllers.fragments.search.QueryParserController;
+import org.apache.lucene.luke.app.controllers.fragments.search.SimilarityController;
+import org.apache.lucene.luke.app.controllers.fragments.search.SortController;
+import org.apache.lucene.luke.app.util.DialogOpener;
+import org.apache.lucene.luke.app.util.IntegerTextFormatter;
+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.app.util.MessageUtils;
+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 javax.annotation.Nonnull;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.apache.lucene.luke.app.util.ExceptionHandler.runnableWrapper;
+
+public class SearchController extends ChildTabController implements IndexObserver {
+
+  private static final int DEFAULT_PAGE_SIZE = 10;
+
+  private final SearchFactory searchFactory;
+
+  private final IndexToolsFactory toolsFactory;
+
+  private final IndexHandler indexHandler;
+
+  private Search searchModel;
+
+  private IndexTools toolsModel;
+
+  private Analyzer curAnalyzer;
+
+  @FXML
+  private Accordion settings;
+
+  @FXML
+  private TitledPane parserPane;
+
+  @FXML
+  private TitledPane mltPane;
+
+  @FXML
+  private ScrollPane parser;
+
+  @FXML
+  private QueryParserController parserController;
+
+  @FXML
+  private ScrollPane analyzer;
+
+  @FXML
+  private AnalyzerController analyzerController;
+
+  @FXML
+  private ScrollPane similarity;
+
+  @FXML
+  private SimilarityController similarityController;
+
+  @FXML
+  private ScrollPane sort;
+
+  @FXML
+  private SortController sortController;
+
+  @FXML
+  private ScrollPane values;
+
+  @FXML
+  private FieldValuesController valuesController;
+
+  @FXML
+  private MLTController mltController;
+
+  @FXML
+  private CheckBox termQuery;
+
+  @FXML
+  private TextArea queryExpr;
+
+  @FXML
+  private TextArea parsedQuery;
+
+  @FXML
+  private Button parseBtn;
+
+  @FXML
+  private CheckBox rewrite;
+
+  @FXML
+  private Button searchBtn;
+
+  @FXML
+  private Button mltBtn;
+
+  @FXML
+  private TextField mltDoc;
+
+  @FXML
+  private Label totalHits;
+
+  @FXML
+  private Label start;
+
+  @FXML
+  private Label end;
+
+  @FXML
+  private Button prev;
+
+  @FXML
+  private Button next;
+
+  @FXML
+  private Button delAll;
+
+  @FXML
+  private TableView<SearchResult> resultsTable;
+
+  @FXML
+  private TableColumn<SearchResult, Integer> docIdColumn;
+
+  @FXML
+  private TableColumn<SearchResult, Float> scoreColumn;
+
+  @FXML
+  private TableColumn<SearchResult, String> valuesColumn;
+
+  private ObservableList<SearchResult> resultList;
+
+  @Inject
+  public SearchController(SearchFactory searchFactory, IndexToolsFactory toolsFactory, IndexHandler indexHandler) {
+    this.searchFactory = searchFactory;
+    this.toolsFactory = toolsFactory;
+    this.indexHandler = indexHandler;
+  }
+
+  @FXML
+  private void initialize() throws LukeException {
+    settings.setExpandedPane(parserPane);
+    parseBtn.setOnAction(e -> runnableWrapper(this::execParse));
+    searchBtn.setOnAction(e -> runnableWrapper(this::execSearch));
+    termQuery.setOnAction(e -> toggleTermQuery());
+
+    mltDoc.setTextFormatter(new IntegerTextFormatter(new IntegerStringConverter(), 0));
+    mltBtn.setOnAction(e -> runnableWrapper(this::execMLTSearch));
+
+    totalHits.setText("0");
+    start.setText("0");
+    end.setText("0");
+
+    next.setDisable(true);
+    next.setOnAction(e -> runnableWrapper(this::nextPage));
+
+    prev.setDisable(true);
+    prev.setOnAction(e -> runnableWrapper(this::prevPage));
+
+    delAll.setDisable(true);
+    delAll.setOnAction(e -> runnableWrapper(this::showDeleteConfirmDialog));
+
+    // initialize results table
+    docIdColumn.setCellValueFactory(new PropertyValueFactory<>("docId"));
+    scoreColumn.setCellValueFactory(new PropertyValueFactory<>("score"));
+    valuesColumn.setCellValueFactory(new PropertyValueFactory<>("values"));
+    resultList = FXCollections.observableArrayList();
+    resultsTable.setItems(resultList);
+    resultsTable.setContextMenu(createResultTableMenu());
+  }
+
+  private void toggleTermQuery() {
+    if (termQuery.isSelected()) {
+      settings.setDisable(true);
+      parseBtn.setDisable(true);
+      rewrite.setDisable(true);
+      mltBtn.setDisable(true);
+      mltDoc.setDisable(true);
+      parsedQuery.setText("");
+    } else {
+      settings.setDisable(false);
+      parseBtn.setDisable(false);
+      rewrite.setDisable(false);
+      mltBtn.setDisable(false);
+      mltDoc.setDisable(false);
+    }
+  }
+
+  @Override
+  public void openIndex(LukeState state) throws LukeException {
+    searchModel = searchFactory.newInstance(state.getIndexReader());
+    toolsModel = toolsFactory.newInstance(state.getIndexReader(), state.useCompound(), state.keepAllCommits());
+    sortController.setSearchModel(searchModel);
+
+    queryExpr.setText("*:*");
+    parserController.populateFields(searchModel.getSearchableFieldNames(), searchModel.getRangeSearchableFieldNames());
+    sortController.populateFields(searchModel.getSortableFieldNames());
+    valuesController.populateFields(searchModel.getFieldNames());
+    mltController.populateFields(searchModel.getFieldNames());
+  }
+
+  @Override
+  public void closeIndex() {
+    searchModel = null;
+    toolsModel = null;
+
+    queryExpr.setText("");
+    parsedQuery.setText("");
+    totalHits.setText("0");
+    start.setText("0");
+    end.setText("0");
+    next.setDisable(true);
+    prev.setDisable(true);
+    delAll.setDisable(true);
+    resultList.clear();
+  }
+
+  private void execParse() throws LukeException {
+    Query query = parse(rewrite.isSelected());
+    parsedQuery.setText(query.toString());
+    clearStatusMessage();
+  }
+
+  private void execSearch() throws LukeException {
+    Query query;
+    if (termQuery.isSelected()) {
+      // term query
+      if (Strings.isNullOrEmpty(queryExpr.getText())) {
+        throw new LukeException("Query is not set.");
+      }
+      String[] tmp = queryExpr.getText().split(":");
+      if (tmp.length < 2) {
+        throw new LukeException(String.format("Invalid query [ %s ]", queryExpr.getText()));
+      }
+      query = new TermQuery(new Term(tmp[0].trim(), tmp[1].trim()));
+    } else {
+      query = parse(false);
+    }
+    SimilarityConfig simConfig = similarityController.getConfig();
+    Sort sort = sortController.getSort();
+    Set<String> fieldsToLoad = valuesController.getFieldsToLoad();
+    resultList.clear();
+    SearchResults results = searchModel.search(query, simConfig, sort, fieldsToLoad, DEFAULT_PAGE_SIZE);
+    populateResults(results);
+  }
+
+  private void execMLTSearch() throws LukeException {
+    if (Strings.isNullOrEmpty(mltDoc.getText())) {
+      throw new LukeException("Doc num is not set.");
+    }
+    int docNum = Integer.parseInt(mltDoc.getText());
+    MLTConfig mltConfig = mltController.getMLTConfig();
+
+    Query query = searchModel.mltQuery(docNum, mltConfig, curAnalyzer);
+    Set<String> fieldsToLoad = valuesController.getFieldsToLoad();
+    resultList.clear();
+    SearchResults results = searchModel.search(query, new SimilarityConfig.Builder().build(), fieldsToLoad, DEFAULT_PAGE_SIZE);
+    populateResults(results);
+  }
+
+  private void nextPage() throws LukeException {
+    resultList.clear();
+    searchModel.nextPage().ifPresent(this::populateResults);
+  }
+
+  private void prevPage() throws LukeException {
+    resultList.clear();
+    searchModel.prevPage().ifPresent(this::populateResults);
+  }
+
+  private Query parse(boolean rewrite) throws LukeException {
+    String expr = Strings.isNullOrEmpty(queryExpr.getText()) ? "*:*" : queryExpr.getText();
+    String df = parserController.getDefField();
+    QueryParserConfig config = parserController.getConfig();
+    return searchModel.parseQuery(expr, df, curAnalyzer, config, rewrite);
+  }
+
+  private void populateResults(SearchResults res) {
+    totalHits.setText(String.valueOf(res.getTotalHits()));
+    if (res.getTotalHits() > 0) {
+      start.setText(String.valueOf(res.getOffset() + 1));
+      end.setText(String.valueOf(res.getOffset() + res.size()));
+      resultList.addAll(res.getHits().stream().map(SearchResult::of).collect(Collectors.toList()));
+
+      prev.setDisable(res.getOffset() == 0);
+      next.setDisable(res.getTotalHits() <= res.getOffset() + res.size());
+
+      if (!indexHandler.getState().readOnly() && indexHandler.getState().hasDirectoryReader()) {
+        delAll.setDisable(false);
+      }
+    } else {
+      start.setText("0");
+      end.setText("0");
+      prev.setDisable(true);
+      next.setDisable(true);
+      delAll.setDisable(true);
+    }
+  }
+
+  private Stage confirmDialog;
+
+  private void showDeleteConfirmDialog() throws Exception {
+    confirmDialog = new DialogOpener<ConfirmController>(getParent()).show(
+        confirmDialog,
+        "Confirm Deletion",
+        "/fxml/dialog/confirm.fxml",
+        400, 200,
+        (controller) -> {
+          controller.setContent(MessageUtils.getLocalizedMessage("search.message.delete_confirm"));
+          controller.setCallback(this::deleteAllDocs);
+        },
+        "/styles/confirm.css"
+    );
+  }
+
+  private void deleteAllDocs() throws LukeException {
+    Query query = searchModel.getCurrentQuery();
+    if (query != null) {
+      toolsModel.deleteDocuments(query);
+      indexHandler.reOpen();
+      showStatusMessage(MessageUtils.getLocalizedMessage("search.message.delete_success", query.toString()));
+    }
+    delAll.setDisable(true);
+  }
+
+  private Stage explanationDialog;
+
+  private ContextMenu createResultTableMenu() {
+    ContextMenu menu = new ContextMenu();
+    MenuItem item1 = new MenuItem(MessageUtils.getLocalizedMessage("search.results.menu.explain"));
+    item1.setOnAction(e -> runnableWrapper(() -> {
+      SearchResult result = resultsTable.getSelectionModel().getSelectedItem();
+      Explanation explanation = searchModel.explain(parse(false), result.getDocId());
+      explanationDialog = new DialogOpener<ExplanationController>(getParent()).show(
+          explanationDialog,
+          "Explanation",
+          "/fxml/dialog/search/explanation.fxml",
+          600, 400,
+          (controller) -> {
+            controller.setDocNum(result.getDocId());
+            controller.setExplanation(explanation);
+          }
+      );
+    }));
+
+    MenuItem item2 = new MenuItem(MessageUtils.getLocalizedMessage("search.results.menu.showdoc"));
+    item2.setOnAction(e -> runnableWrapper(() -> {
+      SearchResult result = resultsTable.getSelectionModel().getSelectedItem();
+      getDocumentsController().displayDoc(result.getDocId());
+      switchTab(LukeController.Tab.DOCUMENTS);
+    }));
+
+    menu.getItems().addAll(item1, item2);
+    return menu;
+  }
+
+  @Override
+  public void setParent(LukeController parent) {
+    super.setParent(parent);
+    analyzerController.setParent(parent);
+    mltController.setParent(parent);
+  }
+
+  // -------------------------------------------------
+  // methods for interaction with other controllers
+  // -------------------------------------------------
+
+  void setCurrentAnalyzer(Analyzer analyzer) {
+    this.curAnalyzer = analyzer;
+    analyzerController.setCurrentAnalyzer(analyzer);
+    mltController.setCurrentAnalyzer(analyzer);
+  }
+
+  void searchByTerm(@Nonnull String fieldName, @Nonnull String termText) throws LukeException {
+    termQuery.selectedProperty().setValue(true);
+    toggleTermQuery();
+    queryExpr.setText(String.format("%s:%s", fieldName, termText));
+    resultList.clear();
+    execSearch();
+  }
+
+  void mltSearch(int docNum) throws LukeException {
+    mltDoc.setText(String.valueOf(docNum));
+    settings.setExpandedPane(mltPane);
+    execMLTSearch();
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/dc72c4a0/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/ConfirmController.java
----------------------------------------------------------------------
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/ConfirmController.java b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/ConfirmController.java
new file mode 100644
index 0000000..887944b
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/ConfirmController.java
@@ -0,0 +1,62 @@
+/*
+ * 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.controllers.dialog;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import org.apache.lucene.luke.models.LukeException;
+
+import static org.apache.lucene.luke.app.util.ExceptionHandler.runnableWrapper;
+
+public class ConfirmController implements DialogWindowController {
+
+  @FXML
+  private Label message;
+
+  @FXML
+  private Button ok;
+
+  @FXML
+  private Button cancel;
+
+  @FXML
+  private void initialize() {
+    ok.setOnAction(e -> runnableWrapper(() -> {
+      callback.exec();
+      closeWindow(ok);
+    }));
+    cancel.setOnAction(e -> closeWindow(cancel));
+  }
+
+  public void setContent(String content) {
+    message.setText(content);
+  }
+
+  private Callback callback = () -> {
+  };
+
+  public void setCallback(Callback callback) {
+    this.callback = callback;
+  }
+
+  @FunctionalInterface
+  public interface Callback {
+    void exec() throws LukeException;
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/dc72c4a0/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/DialogWindowController.java
----------------------------------------------------------------------
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/DialogWindowController.java b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/DialogWindowController.java
new file mode 100644
index 0000000..ad3a8e9
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/DialogWindowController.java
@@ -0,0 +1,30 @@
+/*
+ * 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.controllers.dialog;
+
+import javafx.scene.Node;
+import javafx.stage.Stage;
+
+public interface DialogWindowController {
+
+  default void closeWindow(Node node) {
+    Stage stage = (Stage) node.getScene().getWindow();
+    stage.close();
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/dc72c4a0/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/HelpController.java
----------------------------------------------------------------------
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/HelpController.java b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/HelpController.java
new file mode 100644
index 0000000..8856994
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/HelpController.java
@@ -0,0 +1,53 @@
+/*
+ * 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.controllers.dialog;
+
+import javafx.fxml.FXML;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.layout.AnchorPane;
+
+public class HelpController implements DialogWindowController {
+
+  @FXML
+  private Label description;
+
+  @FXML
+  private AnchorPane content;
+
+  @FXML
+  private Button close;
+
+  @FXML
+  private void initialize() {
+    close.setOnAction(e -> closeWindow(close));
+  }
+
+  public void setDescription(String desc) {
+    description.setText(desc);
+  }
+
+  public void setContent(Node child) {
+    AnchorPane.setTopAnchor(child, 0.0);
+    AnchorPane.setBottomAnchor(child, 0.0);
+    AnchorPane.setLeftAnchor(child, 0.0);
+    AnchorPane.setRightAnchor(child, 0.0);
+    content.getChildren().add(child);
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/dc72c4a0/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/WarnController.java
----------------------------------------------------------------------
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/WarnController.java b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/WarnController.java
new file mode 100644
index 0000000..f633eff
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/WarnController.java
@@ -0,0 +1,41 @@
+/*
+ * 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.controllers.dialog;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+
+public class WarnController implements DialogWindowController {
+
+  @FXML
+  private Label message;
+
+  @FXML
+  private Button close;
+
+  @FXML
+  private void initialize() {
+    close.setOnAction(e -> closeWindow(close));
+  }
+
+  public void setContent(String content) {
+    message.setText(content);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/dc72c4a0/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/analysis/EditFiltersController.java
----------------------------------------------------------------------
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/analysis/EditFiltersController.java b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/analysis/EditFiltersController.java
new file mode 100644
index 0000000..e35e952
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/analysis/EditFiltersController.java
@@ -0,0 +1,137 @@
+/*
+ * 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.controllers.dialog.analysis;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.cell.CheckBoxTableCell;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.input.MouseButton;
+import org.apache.lucene.luke.app.controllers.dialog.DialogWindowController;
+import org.apache.lucene.luke.app.controllers.dto.analysis.FilterFactory;
+import org.apache.lucene.luke.app.controllers.fragments.analysis.CustomAnalyzerController;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static org.apache.lucene.luke.app.util.ExceptionHandler.runnableWrapper;
+
+public class EditFiltersController implements DialogWindowController {
+
+  private CustomAnalyzerController customAnalyzerController;
+
+  private Mode mode;
+
+  private Consumer<List<FilterFactory>> callback;
+
+  @FXML
+  private TableView<FilterFactory> filtersTable;
+
+  @FXML
+  private TableColumn<FilterFactory, Boolean> delColumn;
+
+  @FXML
+  private TableColumn<FilterFactory, Integer> orderColumn;
+
+  @FXML
+  private TableColumn<FilterFactory, String> factoryColumn;
+
+  private ObservableList<FilterFactory> filterList;
+
+  @FXML
+  private Button ok;
+
+  @FXML
+  private Button cancel;
+
+  @FXML
+  private void initialize() {
+    delColumn.setCellValueFactory(data -> {
+      BooleanProperty prop = data.getValue().getDeletedProperty();
+      prop.addListener((obs, oldV, newV) ->
+          filtersTable.getSelectionModel().getSelectedItem().setDeleted(newV)
+      );
+      return prop;
+    });
+    delColumn.setCellFactory(col -> {
+      CheckBoxTableCell<FilterFactory, Boolean> cell = new CheckBoxTableCell<>();
+      cell.setAlignment(Pos.CENTER);
+      return cell;
+    });
+    orderColumn.setCellValueFactory(new PropertyValueFactory<>("order"));
+    factoryColumn.setCellValueFactory(new PropertyValueFactory<>("factory"));
+    filterList = FXCollections.observableArrayList();
+    filtersTable.setItems(filterList);
+    filtersTable.setOnMouseClicked(e -> runnableWrapper(() -> {
+      if (e.getButton().equals(MouseButton.PRIMARY) &&
+          e.getClickCount() == 2) {
+        editParams();
+      }
+    }));
+
+    ok.setOnAction(e -> onOk());
+    cancel.setOnAction(e -> closeWindow(cancel));
+  }
+
+  private void editParams() throws Exception {
+    int selectedIdx = filtersTable.getSelectionModel().getFocusedIndex();
+    switch (mode) {
+      case CHAR_FILTER:
+        customAnalyzerController.editCharFilterParams(selectedIdx);
+        break;
+      case TOKEN_FILTER:
+        customAnalyzerController.editTokenFilterParams(selectedIdx);
+        break;
+      default:
+        break;
+    }
+  }
+
+  private void onOk() {
+    callback.accept(filterList);
+    closeWindow(ok);
+  }
+
+  public void populate(List<String> filters) {
+    List<FilterFactory> li = IntStream.range(0, filters.size())
+        .mapToObj(i -> FilterFactory.of(i + 1, filters.get(i)))
+        .collect(Collectors.toList());
+    filterList.addAll(li);
+  }
+
+  public void setCallback(Consumer<List<FilterFactory>> callback) {
+    this.callback = callback;
+  }
+
+  public void setParent(CustomAnalyzerController customAnalyzerController, Mode mode) {
+    this.customAnalyzerController = customAnalyzerController;
+    this.mode = mode;
+  }
+
+  public enum Mode {
+    CHAR_FILTER, TOKEN_FILTER
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/dc72c4a0/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/analysis/EditParamsController.java
----------------------------------------------------------------------
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/analysis/EditParamsController.java b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/analysis/EditParamsController.java
new file mode 100644
index 0000000..be6a167
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/analysis/EditParamsController.java
@@ -0,0 +1,134 @@
+/*
+ * 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.controllers.dialog.analysis;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.cell.CheckBoxTableCell;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.control.cell.TextFieldTableCell;
+import org.apache.lucene.luke.app.controllers.dialog.DialogWindowController;
+import org.apache.lucene.luke.app.controllers.dto.analysis.Param;
+
+import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+public class EditParamsController implements DialogWindowController {
+
+  private Consumer<List<Param>> callback;
+
+  @FXML
+  private Label target;
+
+  @FXML
+  private TableView<Param> paramsTable;
+
+  @FXML
+  private TableColumn<Param, Boolean> delColumn;
+
+  @FXML
+  private TableColumn<Param, String> nameColumn;
+
+  @FXML
+  private TableColumn<Param, String> valueColumn;
+
+  private ObservableList<Param> paramList;
+
+  @FXML
+  private Button ok;
+
+  @FXML
+  private Button cancel;
+
+  @FXML
+  private void initialize() {
+    delColumn.setCellValueFactory(data -> {
+      BooleanProperty prop = data.getValue().getDeletedProperty();
+      prop.addListener((obs, oldV, newV) ->
+          paramsTable.getSelectionModel().getSelectedItem().setDeleted(newV)
+      );
+      return prop;
+    });
+    delColumn.setCellFactory(col -> {
+      CheckBoxTableCell<Param, Boolean> cell = new CheckBoxTableCell<>();
+      cell.setAlignment(Pos.CENTER);
+      return cell;
+    });
+
+    nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
+    nameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
+    nameColumn.setOnEditCommit(e -> {
+      int rowIdx = paramsTable.getSelectionModel().getFocusedIndex();
+      paramList.get(rowIdx).setName(e.getNewValue());
+    });
+
+    valueColumn.setCellValueFactory(new PropertyValueFactory<>("value"));
+    valueColumn.setCellFactory(TextFieldTableCell.forTableColumn());
+    valueColumn.setOnEditCommit(e -> {
+      int rowIdx = paramsTable.getSelectionModel().getFocusedIndex();
+      paramList.get(rowIdx).setValue(e.getNewValue());
+    });
+
+    paramList = FXCollections.observableArrayList();
+    paramsTable.setItems(paramList);
+
+    ok.setOnAction(e -> onOk());
+    cancel.setOnAction(e -> closeWindow(cancel));
+  }
+
+  private void onOk() {
+    callback.accept(paramList);
+    closeWindow(ok);
+  }
+
+  public void setTarget(String targetName) {
+    this.target.setText(targetName);
+  }
+
+  public void setCallback(Consumer<List<Param>> callback) {
+    this.callback = callback;
+  }
+
+  public void populate(@Nullable Map<String, String> paramMap) {
+    List<Param> li;
+    if (paramMap != null) {
+      li = new ArrayList<>();
+      for (Map.Entry<String, String> entry : paramMap.entrySet()) {
+        li.add(Param.of(entry.getKey(), entry.getValue()));
+      }
+      li.addAll(IntStream.range(0, Math.max(10 - paramMap.size(), 0))
+          .mapToObj(i -> Param.newInstance())
+          .collect(Collectors.toList()));
+    } else {
+      li = IntStream.range(0, 10).mapToObj(i -> Param.newInstance()).collect(Collectors.toList());
+    }
+    paramList.addAll(li);
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/dc72c4a0/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/analysis/TokenAttributeController.java
----------------------------------------------------------------------
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/analysis/TokenAttributeController.java b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/analysis/TokenAttributeController.java
new file mode 100644
index 0000000..d6e7422
--- /dev/null
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/controllers/dialog/analysis/TokenAttributeController.java
@@ -0,0 +1,75 @@
+/*
+ * 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.controllers.dialog.analysis;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.cell.PropertyValueFactory;
+import org.apache.lucene.luke.app.controllers.dialog.DialogWindowController;
+import org.apache.lucene.luke.app.controllers.dto.analysis.TokenAttValue;
+import org.apache.lucene.luke.models.analysis.Analysis;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class TokenAttributeController implements DialogWindowController {
+
+  @FXML
+  private Label term;
+
+  @FXML
+  private TableView<TokenAttValue> attTable;
+
+  @FXML
+  private TableColumn<TokenAttValue, String> attClassColumn;
+
+  @FXML
+  private TableColumn<TokenAttValue, String> nameColumn;
+
+  @FXML
+  private TableColumn<TokenAttValue, String> valueColumn;
+
+  private ObservableList<TokenAttValue> attList;
+
+  @FXML
+  private Button ok;
+
+  @FXML
+  private void initialize() {
+    attClassColumn.setCellValueFactory(new PropertyValueFactory<>("attClass"));
+    nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
+    valueColumn.setCellValueFactory(new PropertyValueFactory<>("value"));
+    attList = FXCollections.observableArrayList();
+    attTable.setItems(attList);
+
+    ok.setOnAction(e -> closeWindow(ok));
+  }
+
+  public void populate(String termText, List<Analysis.TokenAttribute> attributes) {
+    term.setText(termText);
+    attList.addAll(attributes.stream()
+        .flatMap(att -> att.getAttValues().entrySet().stream()
+            .map(e -> TokenAttValue.of(att.getAttClass(), e.getKey(), e.getValue())
+            )).collect(Collectors.toList()));
+  }
+}