Java tutorial
/* * QueryEditorAutoCompletePopupProvider.java * * Copyright (C) 2002-2015 Takis Diakoumis * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 3 * of the License, or any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ package org.executequery.gui.editor.autocomplete; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.InputMap; import javax.swing.KeyStroke; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import javax.swing.text.BadLocationException; import javax.swing.text.Caret; import javax.swing.text.Document; import javax.swing.text.JTextComponent; import org.apache.commons.lang.StringUtils; import org.executequery.databasemediators.DatabaseConnection; import org.executequery.databaseobjects.DatabaseHost; import org.executequery.databaseobjects.DatabaseObjectFactory; import org.executequery.databaseobjects.impl.DatabaseObjectFactoryImpl; import org.executequery.gui.editor.ConnectionChangeListener; import org.executequery.gui.editor.QueryEditor; import org.executequery.log.Log; import org.executequery.sql.DerivedQuery; import org.executequery.sql.QueryTable; import org.executequery.util.UserProperties; import org.underworldlabs.swing.util.SwingWorker; public class QueryEditorAutoCompletePopupProvider implements AutoCompletePopupProvider, AutoCompletePopupListener, CaretListener, ConnectionChangeListener, FocusListener { private static final KeyStroke KEY_STROKE_ENTER = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); private static final KeyStroke KEY_STROKE_DOWN = KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0); private static final KeyStroke KEY_STROKE_UP = KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0); private static final KeyStroke KEY_STROKE_PAGE_DOWN = KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0); private static final KeyStroke KEY_STROKE_PAGE_UP = KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0); private static final KeyStroke KEY_STROKE_TAB = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0); private static final String LIST_FOCUS_ACTION_KEY = "focusActionKey"; private static final String LIST_SCROLL_ACTION_KEY_DOWN = "scrollActionKeyDown"; private static final String LIST_SCROLL_ACTION_KEY_UP = "scrollActionKeyUp"; private static final String LIST_SCROLL_ACTION_KEY_PAGE_DOWN = "scrollActionKeyPageDown"; private static final String LIST_SCROLL_ACTION_KEY_PAGE_UP = "scrollActionKeyPageUp"; private static final String LIST_SELECTION_ACTION_KEY = "selectionActionKey"; private QueryEditor queryEditor; private AutoCompleteSelectionsFactory selectionsFactory; private AutoCompletePopupAction autoCompletePopupAction; private QueryEditorAutoCompletePopupPanel autoCompletePopup; private DatabaseObjectFactory databaseObjectFactory; private DatabaseHost databaseHost; private List<AutoCompleteListItem> autoCompleteListItems; private boolean autoCompleteKeywords; private boolean autoCompleteSchema; public QueryEditorAutoCompletePopupProvider(QueryEditor queryEditor) { super(); this.queryEditor = queryEditor; selectionsFactory = new AutoCompleteSelectionsFactory(this); databaseObjectFactory = new DatabaseObjectFactoryImpl(); setAutoCompleteOptionFlags(); autoCompleteListItems = new ArrayList<AutoCompleteListItem>(); queryEditor.addConnectionChangeListener(this); queryEditorTextComponent().addFocusListener(this); autoCompletePopupAction = new AutoCompletePopupAction(this); autoCompleteListItems = new ArrayList<AutoCompleteListItem>(); } public void setAutoCompleteOptionFlags() { UserProperties userProperties = UserProperties.getInstance(); autoCompleteKeywords = userProperties.getBooleanProperty("editor.autocomplete.keywords.on"); autoCompleteSchema = userProperties.getBooleanProperty("editor.autocomplete.schema.on"); } public void reset() { connectionChanged(databaseHost.getDatabaseConnection()); } public Action getPopupAction() { return autoCompletePopupAction; } public void firePopupTrigger() { try { final JTextComponent textComponent = queryEditorTextComponent(); Caret caret = textComponent.getCaret(); final Rectangle caretCoords = textComponent.modelToView(caret.getDot()); addFocusActions(); resetCount = 0; captureAndResetListValues(); popupMenu().show(textComponent, caretCoords.x, caretCoords.y + caretCoords.height); textComponent.requestFocus(); } catch (BadLocationException e) { debug("Error on caret coordinates", e); } } private QueryEditorAutoCompletePopupPanel popupMenu() { if (autoCompletePopup == null) { autoCompletePopup = new QueryEditorAutoCompletePopupPanel(); autoCompletePopup.addAutoCompletePopupListener(this); autoCompletePopup.addPopupMenuListener(new PopupMenuListener() { public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { popupHidden(); } public void popupMenuCanceled(PopupMenuEvent e) { popupHidden(); } public void popupMenuWillBecomeVisible(PopupMenuEvent e) { } }); } return autoCompletePopup; } private JTextComponent queryEditorTextComponent() { return queryEditor.getEditorTextComponent(); } private void focusAndSelectList() { autoCompletePopup.focusAndSelectList(); } private Object existingKeyStrokeDownAction; private Object existingKeyStrokeUpAction; private Object existingKeyStrokePageDownAction; private Object existingKeyStrokePageUpAction; private Object existingKeyStrokeTabAction; private Object existingKeyStrokeEnterAction; private boolean editorActionsSaved; private void addFocusActions() { JTextComponent textComponent = queryEditorTextComponent(); ActionMap actionMap = textComponent.getActionMap(); actionMap.put(LIST_FOCUS_ACTION_KEY, listFocusAction); actionMap.put(LIST_SCROLL_ACTION_KEY_DOWN, listScrollActionDown); actionMap.put(LIST_SCROLL_ACTION_KEY_UP, listScrollActionUp); actionMap.put(LIST_SELECTION_ACTION_KEY, listSelectionAction); actionMap.put(LIST_SCROLL_ACTION_KEY_PAGE_DOWN, listScrollActionPageDown); actionMap.put(LIST_SCROLL_ACTION_KEY_PAGE_UP, listScrollActionPageUp); InputMap inputMap = textComponent.getInputMap(); saveExistingActions(inputMap); inputMap.put(KEY_STROKE_DOWN, LIST_SCROLL_ACTION_KEY_DOWN); inputMap.put(KEY_STROKE_UP, LIST_SCROLL_ACTION_KEY_UP); inputMap.put(KEY_STROKE_PAGE_DOWN, LIST_SCROLL_ACTION_KEY_PAGE_DOWN); inputMap.put(KEY_STROKE_PAGE_UP, LIST_SCROLL_ACTION_KEY_PAGE_UP); inputMap.put(KEY_STROKE_TAB, LIST_FOCUS_ACTION_KEY); inputMap.put(KEY_STROKE_ENTER, LIST_SELECTION_ACTION_KEY); textComponent.addCaretListener(this); } private void saveExistingActions(InputMap inputMap) { if (!editorActionsSaved) { existingKeyStrokeDownAction = inputMap.get(KEY_STROKE_DOWN); existingKeyStrokeUpAction = inputMap.get(KEY_STROKE_UP); existingKeyStrokePageDownAction = inputMap.get(KEY_STROKE_PAGE_DOWN); existingKeyStrokePageUpAction = inputMap.get(KEY_STROKE_PAGE_UP); existingKeyStrokeEnterAction = inputMap.get(KEY_STROKE_ENTER); existingKeyStrokeTabAction = inputMap.get(KEY_STROKE_TAB); editorActionsSaved = true; } } private boolean canExecutePopupActions() { if (popupMenu().isVisible()) { return true; } resetEditorActions(); return false; } private void resetEditorActions() { JTextComponent textComponent = queryEditorTextComponent(); ActionMap actionMap = textComponent.getActionMap(); actionMap.remove(LIST_FOCUS_ACTION_KEY); actionMap.remove(LIST_SELECTION_ACTION_KEY); actionMap.remove(LIST_SCROLL_ACTION_KEY_DOWN); actionMap.remove(LIST_SCROLL_ACTION_KEY_UP); InputMap inputMap = textComponent.getInputMap(); inputMap.remove(KEY_STROKE_DOWN); inputMap.remove(KEY_STROKE_UP); inputMap.remove(KEY_STROKE_PAGE_DOWN); inputMap.remove(KEY_STROKE_PAGE_UP); inputMap.remove(KEY_STROKE_ENTER); inputMap.remove(KEY_STROKE_TAB); inputMap.put(KEY_STROKE_DOWN, existingKeyStrokeDownAction); inputMap.put(KEY_STROKE_UP, existingKeyStrokeUpAction); inputMap.put(KEY_STROKE_PAGE_DOWN, existingKeyStrokePageDownAction); inputMap.put(KEY_STROKE_PAGE_UP, existingKeyStrokePageUpAction); inputMap.put(KEY_STROKE_TAB, existingKeyStrokeTabAction); inputMap.put(KEY_STROKE_ENTER, existingKeyStrokeEnterAction); textComponent.removeCaretListener(this); } private void popupHidden() { resetEditorActions(); queryEditorTextComponent().requestFocus(); } public void refocus() { queryEditorTextComponent().requestFocus(); } private void captureAndResetListValues() { String wordAtCursor = queryEditor.getWordToCursor(); trace("Capturing and resetting list values for word [ " + wordAtCursor + " ]"); DerivedQuery derivedQuery = new DerivedQuery(queryEditor.getQueryAtCursor().getQuery()); List<QueryTable> tables = derivedQuery.tableForWord(wordAtCursor); List<AutoCompleteListItem> itemsStartingWith = itemsStartingWith(tables, wordAtCursor); if (itemsStartingWith.isEmpty()) { noProposalsAvailable(itemsStartingWith); } if (rebuildingList) { popupMenu().scheduleReset(itemsStartingWith); } else { popupMenu().reset(itemsStartingWith); } } // TODO: determine query being executed and suggest based on that // introduce query types (select, insert etc) // track columns/tables in statement ???? private static final int MINIMUM_CHARS_FOR_DATABASE_LOOKUP = 2; private List<AutoCompleteListItem> itemsStartingWith(List<QueryTable> tables, String prefix) { boolean hasTables = hasTables(tables); if (StringUtils.isBlank(prefix) && !hasTables) { return selectionsFactory.buildKeywords(databaseHost, autoCompleteKeywords); } trace("Building list of items starting with [ " + prefix + " ] from table list with size " + tables.size()); List<AutoCompleteListItem> searchList = autoCompleteListItems; String wordPrefix = prefix.trim().toUpperCase(); int dotIndex = prefix.indexOf('.'); boolean hasDotIndex = (dotIndex != -1); if (hasDotIndex) { wordPrefix = wordPrefix.substring(dotIndex + 1); } else if (wordPrefix.length() < MINIMUM_CHARS_FOR_DATABASE_LOOKUP && !hasTables) { return buildItemsStartingWithForList( selectionsFactory.buildKeywords(databaseHost, autoCompleteKeywords), tables, wordPrefix, false); } List<AutoCompleteListItem> itemsStartingWith = buildItemsStartingWithForList(searchList, tables, wordPrefix, hasDotIndex); if (itemsStartingWith.isEmpty()) { // do it one more time without the tables... itemsStartingWith = buildItemsStartingWithForList(searchList, null, wordPrefix, hasDotIndex); if (itemsStartingWith.isEmpty()) { // now bail... noProposalsAvailable(itemsStartingWith); } return itemsStartingWith; } /* ----- might be a little sluggish right now ... else { // add other entities starting with at the end of the list (??) itemsStartingWith.addAll(buildItemsStartingWithForList( autoCompleteListItems, null, wordPrefix, hasDotIndex)); } */ if (rebuildingList) { itemsStartingWith.add(0, buildingProposalsListItem()); } return itemsStartingWith; } private boolean hasTables(List<QueryTable> tables) { return (tables != null && !tables.isEmpty()); } private List<AutoCompleteListItem> buildItemsStartingWithForList(List<AutoCompleteListItem> items, List<QueryTable> tables, String prefix, boolean prefixHadAlias) { String searchPattern = prefix; if (prefix.startsWith("(")) { searchPattern = prefix.substring(1); } List<AutoCompleteListItem> itemsStartingWith = new ArrayList<AutoCompleteListItem>(); if (items != null) { for (int i = 0, n = items.size(); i < n; i++) { AutoCompleteListItem item = items.get(i); if (item.isForPrefix(tables, searchPattern, prefixHadAlias)) { itemsStartingWith.add(item); } } } Collections.sort(itemsStartingWith, autoCompleteListItemComparator); return itemsStartingWith; } private AutoCompleteListItemComparator autoCompleteListItemComparator = new AutoCompleteListItemComparator(); static class AutoCompleteListItemComparator implements Comparator<AutoCompleteListItem> { public int compare(AutoCompleteListItem o1, AutoCompleteListItem o2) { if (o1.isSchemaObject() && o2.isSchemaObject()) { return o1.getInsertionValue().compareTo(o2.getInsertionValue()); } else if (o1.isSchemaObject() && !o2.isSchemaObject()) { return -1; } else if (o2.isSchemaObject() && !o1.isSchemaObject()) { return 1; } return o1.getUpperCaseValue().compareTo(o2.getUpperCaseValue()); } } private void noProposalsAvailable(List<AutoCompleteListItem> itemsStartingWith) { if (rebuildingList) { debug("Suggestions list still in progress"); itemsStartingWith.add(buildingProposalsListItem()); } else { debug("Suggestions list completed - no matches found for input"); itemsStartingWith.add(noProposalsListItem()); } } private AutoCompleteListItem buildingProposalsAutoCompleteListItem; private AutoCompleteListItem buildingProposalsListItem() { if (buildingProposalsAutoCompleteListItem == null) { buildingProposalsAutoCompleteListItem = new AutoCompleteListItem(null, "Please wait. Generating proposals...", null, AutoCompleteListItemType.GENERATING_LIST); } return buildingProposalsAutoCompleteListItem; } private AutoCompleteListItem noProposalsAutoCompleteListItem; private AutoCompleteListItem noProposalsListItem() { if (noProposalsAutoCompleteListItem == null) { noProposalsAutoCompleteListItem = new AutoCompleteListItem(null, "No Proposals Available", null, AutoCompleteListItemType.NOTHING_PROPOSED); } return noProposalsAutoCompleteListItem; } private final ListFocusAction listFocusAction = new ListFocusAction(); class ListFocusAction extends AbstractAction { public void actionPerformed(ActionEvent e) { if (!canExecutePopupActions()) { return; } focusAndSelectList(); } } // ListFocusAction private final ListSelectionAction listSelectionAction = new ListSelectionAction(); class ListSelectionAction extends AbstractAction { public void actionPerformed(ActionEvent e) { if (!canExecutePopupActions()) { return; } popupSelectionMade(); } } // ListSelectionAction enum ListScrollType { UP, DOWN, PAGE_DOWN, PAGE_UP; } private final ListScrollAction listScrollActionDown = new ListScrollAction(ListScrollType.DOWN); private final ListScrollAction listScrollActionUp = new ListScrollAction(ListScrollType.UP); private final ListScrollAction listScrollActionPageDown = new ListScrollAction(ListScrollType.PAGE_DOWN); private final ListScrollAction listScrollActionPageUp = new ListScrollAction(ListScrollType.PAGE_UP); class ListScrollAction extends AbstractAction { private final ListScrollType direction; ListScrollAction(ListScrollType direction) { this.direction = direction; } public void actionPerformed(ActionEvent e) { if (!canExecutePopupActions()) { return; } switch (direction) { case DOWN: autoCompletePopup.scrollSelectedIndexDown(); break; case UP: autoCompletePopup.scrollSelectedIndexUp(); break; case PAGE_DOWN: autoCompletePopup.scrollSelectedIndexPageDown(); break; case PAGE_UP: autoCompletePopup.scrollSelectedIndexPageUp(); break; } } } // ListScrollAction public void popupClosed() { // popupHidden(); } public void popupSelectionCancelled() { queryEditorTextComponent().requestFocus(); } private boolean isAllLowerCase(String text) { for (char character : text.toCharArray()) { if (Character.isUpperCase(character)) { return false; } } return true; } public void popupSelectionMade() { AutoCompleteListItem selectedListItem = (AutoCompleteListItem) autoCompletePopup.getSelectedItem(); if (selectedListItem == null || selectedListItem.isNothingProposed()) { return; } String selectedValue = selectedListItem.getInsertionValue(); try { JTextComponent textComponent = queryEditorTextComponent(); Document document = textComponent.getDocument(); int caretPosition = textComponent.getCaretPosition(); String wordAtCursor = queryEditor.getWordToCursor(); if (StringUtils.isNotBlank(wordAtCursor)) { int wordAtCursorLength = wordAtCursor.length(); int insertionIndex = caretPosition - wordAtCursorLength; if (selectedListItem.isKeyword() && isAllLowerCase(wordAtCursor)) { selectedValue = selectedValue.toLowerCase(); } if (!Character.isLetterOrDigit(wordAtCursor.charAt(0))) { // cases where you might have a.column_name or similar insertionIndex++; wordAtCursorLength--; } else if (wordAtCursor.contains(".")) { int index = wordAtCursor.indexOf("."); insertionIndex += index + 1; wordAtCursorLength -= index + 1; } document.remove(insertionIndex, wordAtCursorLength); document.insertString(insertionIndex, selectedValue, null); } else { document.insertString(caretPosition, selectedValue, null); } } catch (BadLocationException e) { debug("Error on autocomplete insertion", e); } finally { autoCompletePopup.hidePopup(); } } public void caretUpdate(CaretEvent e) { captureAndResetListValues(); } public void connectionChanged(DatabaseConnection databaseConnection) { if (worker != null) { worker.interrupt(); } if (autoCompleteListItems != null) { autoCompleteListItems.clear(); } if (databaseHost != null) { databaseHost.close(); } if (databaseConnection != null) { databaseHost = databaseObjectFactory.createDatabaseHost(databaseConnection); scheduleListItemLoad(); } } public void focusGained(FocusEvent e) { if (e.getSource() == queryEditorTextComponent()) { scheduleListItemLoad(); } } private boolean rebuildListSelectionsItems() { DatabaseConnection selectedConnection = queryEditor.getSelectedConnection(); if (selectedConnection == null) { databaseHost = null; } else if (databaseHost == null) { databaseHost = databaseObjectFactory.createDatabaseHost(selectedConnection); } selectionsFactory.build(databaseHost, autoCompleteKeywords, autoCompleteSchema); return true; } public void addListItems(List<AutoCompleteListItem> items) { if (autoCompleteListItems == null) { autoCompleteListItems = new ArrayList<AutoCompleteListItem>(); } autoCompleteListItems.addAll(items); // Collections.sort(autoCompleteListItems, autoCompleteListItemComparatorByValue); reapplyIfVisible(); } private int resetCount; private static final int RESET_COUNT_THRESHOLD = 20; // apply every 5 calls private void reapplyIfVisible() { if (popupMenu().isVisible()) { if (++resetCount == RESET_COUNT_THRESHOLD) { trace("Reset count reached -- Resetting autocomplete popup list values"); captureAndResetListValues(); resetCount = 0; } } } private boolean rebuildingList; private SwingWorker worker; private void scheduleListItemLoad() { if (rebuildingList || !autoCompleteListItems.isEmpty()) { return; } worker = new SwingWorker() { public Object construct() { try { debug("Rebuilding suggestions list..."); rebuildingList = true; rebuildListSelectionsItems(); return "done"; } finally { rebuildingList = false; } } public void finished() { try { rebuildingList = false; debug("Rebuilding suggestions list complete"); // force resetCount = RESET_COUNT_THRESHOLD - 1; reapplyIfVisible(); } finally { popupMenu().done(); } } }; debug("Starting worker thread for suggestions list"); worker.start(); } public void focusLost(FocusEvent e) { } static class AutoCompleteListItemComparatorByValue implements Comparator<AutoCompleteListItem> { public int compare(AutoCompleteListItem o1, AutoCompleteListItem o2) { return o1.getValue().toUpperCase().compareTo(o2.getValue().toUpperCase()); } } private void debug(String message) { Log.debug(message); } private void trace(String message) { Log.trace(message); } private void debug(String message, Throwable e) { Log.debug(message, e); } }