Java tutorial
/** * See the NOTICE file distributed with this work for additional information * regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This software 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 Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with this software; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF * site: http://www.fsf.org. */ package org.ut.biolab.medsavant.client.query; import com.healthmarketscience.sqlbuilder.ComboCondition; import com.healthmarketscience.sqlbuilder.Condition; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.io.PrintWriter; import java.text.ParseException; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.swing.SwingUtilities; import javax.swing.border.Border; import net.miginfocom.swing.MigLayout; import org.apache.commons.httpclient.NameValuePair; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.ut.biolab.medsavant.MedSavantClient; import org.ut.biolab.medsavant.client.filter.FilterController; import org.ut.biolab.medsavant.client.view.login.LoginController; import org.ut.biolab.medsavant.client.project.ProjectController; import org.ut.biolab.medsavant.client.reference.ReferenceController; import org.ut.biolab.medsavant.client.view.genetics.QueryUtils; import org.ut.biolab.medsavant.client.view.util.DialogUtils; import org.ut.biolab.medsavant.client.view.util.ViewUtil; import org.ut.biolab.medsavant.shared.model.GenomicRegion; import org.ut.biolab.medsavant.client.query.SearchConditionGroupItem.QueryRelation; import org.ut.biolab.medsavant.client.query.SearchConditionItem.SearchConditionListener; import org.ut.biolab.medsavant.client.query.medsavant.complex.ConditionUtils; import org.ut.biolab.medsavant.client.query.view.PillView; import org.ut.biolab.medsavant.client.query.view.ConditionPopupGenerator; import org.ut.biolab.medsavant.client.query.view.SearchConditionItemView; import org.ut.biolab.savant.analytics.savantanalytics.AnalyticsAgent; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * * @author mfiume */ public class QueryViewController extends JPanel implements SearchConditionListener { private static Log LOG = LogFactory.getLog(QueryViewController.class); //Width of the bottom (i.e. root group) search text box. private static final int BOTTOM_SEARCH_TEXTFIELD_WIDTH = 270; private final SearchConditionGroupItem rootGroup; private final HashMap<SearchConditionItem, SearchConditionItemView> itemToViewMap; private Map<SearchConditionGroupItem, Boolean> expandedItemsMap = new HashMap<SearchConditionGroupItem, Boolean>(); private final ConditionViewGenerator conditionViewGenerator; private boolean didChangeSinceLastApply; private JButton applyButton; private final JLabel warningText; public QueryViewController(SearchConditionGroupItem model, ConditionViewGenerator c) { this.rootGroup = model; this.conditionViewGenerator = c; model.addListener(this); this.setOpaque(false); this.setFocusable(true); warningText = new JLabel("search conditions have changed"); warningText.setFont(ViewUtil.getSmallTitleFont()); warningText.setForeground(ViewUtil.getSubtleTitleColor()); applyButton = new JButton("Search"); applyButton.setFocusable(false); setConditionsChanged(false); applyButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent ae) { applySearchConditions(); try { AnalyticsAgent.log(new NameValuePair[] { new NameValuePair("view-event", "SearchPerformed") }); } catch (Exception e) { } } }); this.refreshView(); itemToViewMap = new HashMap<SearchConditionItem, SearchConditionItemView>(); } /** * Re-executes the current query with the given genomic region restrictions, * and returns the results. Note the search bar is not changed. gr and alt * should have a one-to-one correspondence, where the alt for the ith * genomic region is in alt[i]. */ public List<Object[]> restrictToRegion(List<GenomicRegion> gr, List<String> alt, int limit) { try { long st = System.currentTimeMillis(); Condition r; if (rootGroup.getItems().size() > 0) { r = getSQLConditionsFrom(rootGroup); SearchConditionGroupItem rg = QueryUtils.getRegionGroup(gr.get(0), alt.get(0), false); r = ComboCondition.and(r, getSQLConditionsFrom(rg)); } else { SearchConditionGroupItem rg = QueryUtils.getRegionGroup(gr.get(0), alt.get(0), false); r = getSQLConditionsFrom(rg); } for (int i = 1; i < gr.size(); ++i) { SearchConditionGroupItem rg = QueryUtils.getRegionGroup(gr.get(i), alt.get(i), false); r = ComboCondition.and(r, getSQLConditionsFrom(rg)); } return MedSavantClient.VariantManager.getVariants(LoginController.getSessionID(), ProjectController.getInstance().getCurrentProjectID(), ReferenceController.getInstance().getCurrentReferenceID(), new Condition[][] { { r } }, 0, limit); //DEBUG CODE, sets limit to 10! } catch (Exception ex) { LOG.error(ex); ex.printStackTrace(); DialogUtils.displayException("Error", "There was an error performing your search", ex); } return null; } public List<Object[]> restrictToRegion(GenomicRegion gr, String alt, int limit) { List<GenomicRegion> grl = new ArrayList<GenomicRegion>(1); List<String> al = new ArrayList<String>(1); grl.add(gr); al.add(alt); return restrictToRegion(grl, al, limit); } private void applySearchConditions() { Thread t = new Thread(new Runnable() { @Override public void run() { try { Condition c; c = getSQLConditionsFrom(rootGroup); if (c == null) { c = ConditionUtils.TRUE_CONDITION; } //LOG.info(c.toString()); SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { warningText.setVisible(false); applyButton.setText("Searching..."); applyButton.setEnabled(false); applyButton.updateUI(); } }); FilterController.getInstance().setConditions(c); SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { applyButton.setText("Search"); applyButton.updateUI(); } }); } catch (final IllegalArgumentException ex) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { DialogUtils.displayError("Search Error", ex.getMessage()); } }); } catch (Exception ex) { LOG.info(ex); ex.printStackTrace(); DialogUtils.displayException("Error", "There was an error performing your search", ex); } } }); t.start(); } public void saveConditions(File file) { String xml = rootGroup.toXML(); try { PrintWriter out = new PrintWriter(file); out.println(xml); out.close(); } catch (Exception ex) { LOG.error(ex); DialogUtils.displayException("Error", "There was an error saving your search", ex); } } private SearchConditionItem getItemFromXML(Element element, SearchConditionGroupItem parentGroup) throws ParseException { String name = StringEscapeUtils.unescapeXml(element.getAttribute("name")); QueryRelation qr; if (element.getAttribute("queryRelation").equalsIgnoreCase("OR")) { qr = QueryRelation.OR; } else if (element.getAttribute("queryRelation").equalsIgnoreCase("AND")) { qr = QueryRelation.AND; } else { throw new ParseException("Malformed input file contains invalid query relation", 0); } SearchConditionItem sci = new SearchConditionItem(name, qr, parentGroup); String encodedConditions = StringEscapeUtils.unescapeXml(element.getAttribute("encodedConditions")); if (encodedConditions != null && encodedConditions.length() > 0) { sci.setSearchConditionEncoding(encodedConditions); } //The description does not need to be unescaped. String desc = element.getAttribute("description"); if (desc != null && desc.length() > 0) { sci.setDescription(desc); } generateItemViewAndAddToGroup(sci, parentGroup); return sci; } private SearchConditionGroupItem getGroupFromXML(Element rootElement, SearchConditionGroupItem parentGroup) throws ParseException { if (!rootElement.getNodeName().toLowerCase().equals("group")) { DialogUtils.displayError("ERROR: Malformed/Invalid Input file"); } QueryRelation qr; if (rootElement.getAttribute("queryRelation").equalsIgnoreCase("OR")) { qr = QueryRelation.OR; } else if (rootElement.getAttribute("queryRelation").equalsIgnoreCase("AND")) { qr = QueryRelation.AND; } else { throw new ParseException("Malformed input file contains invalid query relation", 0); } String desc = rootElement.getAttribute("description"); SearchConditionGroupItem scg; if (parentGroup == null) { scg = this.rootGroup; } else { scg = new SearchConditionGroupItem(qr, null, parentGroup); if (desc != null && desc.length() > 0) { scg.setDescription(desc); } } NodeList nl = rootElement.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { Node n = nl.item(i); if (n.getNodeType() == Node.ELEMENT_NODE) { Element el = (Element) n; if (el.getNodeName().equalsIgnoreCase("group")) { SearchConditionGroupItem childGroup = getGroupFromXML(el, scg); scg.addItem(childGroup); } else if (el.getNodeName().equalsIgnoreCase("item")) { SearchConditionItem sci = getItemFromXML(el, scg); //scg.addItem(sci); } else { throw new ParseException("Malformed input file contains invalid XML element", 0); } } } return scg; } public List<SearchConditionItem> getFirstLevelItemsByDesc(String itemDesc) { List<SearchConditionItem> results = new LinkedList<SearchConditionItem>(); SearchConditionItem sci = null; for (SearchConditionItem i : getQueryRootGroup().getItems()) { if (i.getDescription() != null && i.getDescription().equals(itemDesc)) { results.add(i); } } return results; } public List<SearchConditionItem> getFirstLevelItemsByName(String itemName) { List<SearchConditionItem> results = new LinkedList<SearchConditionItem>(); for (SearchConditionItem i : getQueryRootGroup().getItems()) { if (i.getName() != null && i.getName().equals(itemName)) { results.add(i); } } return results; } public void replaceFirstLevelItem(String name, String encodedConditions, String description) { List<SearchConditionItem> sciList = getFirstLevelItemsByName(name); for (SearchConditionItem sci : sciList) { getQueryRootGroup().removeItem(sci); if (sci instanceof SearchConditionGroupItem) { expandedItemsMap.remove((SearchConditionGroupItem) sci); } } SearchConditionItem sci = new SearchConditionItem(name, QueryRelation.AND, getQueryRootGroup()); generateItemViewAndAddToGroup(sci, getQueryRootGroup()); sci.setSearchConditionEncoding(encodedConditions); sci.setDescription(description); } /** * Replaces the items in the first level group (i.e. a group immediately * under the root node) with the description "groupDesc" with those in the * list 'sciList'. * * If the group does not exist, a new one is created under the root query * group. * * No views are created or updated by this function. * * @return The SearchConditionGroupItem matching the description, or the one * that was created. */ public SearchConditionGroupItem replaceFirstLevelGroup(String groupDesc, List<SearchConditionItem> sciList, QueryRelation qr, boolean listIsItems) { List<SearchConditionItem> scgList = getFirstLevelItemsByDesc(groupDesc); for (SearchConditionItem sci : scgList) { getQueryRootGroup().removeItem(sci); //expandedItemsMap.remove(getQueryRootGroup()); expandedItemsMap.remove(sci); } SearchConditionGroupItem scg = new SearchConditionGroupItem(qr, null, getQueryRootGroup()); scg.setDescription(groupDesc); getQueryRootGroup().addItem(scg); if (sciList != null) { for (SearchConditionItem sci : sciList) { sci.setParent(scg); if (listIsItems) { generateItemViewAndAddToGroup(sci, scg); } else { scg.addItem(sci); } } } return scg; } //clears all search terms, and refreshes the view only if refresh is set. private void clearSearch(boolean refresh) { try { AnalyticsAgent.log(new NameValuePair[] { new NameValuePair("view-event", "SearchCleared") }); } catch (Exception e) { } this.rootGroup.clearItems(); this.itemToViewMap.clear(); this.expandedItemsMap.clear(); if (refresh) { refreshView(); } } /** * Clears all search terms and refreshes the view */ public void clearSearch() { clearSearch(true); } public void loadConditions(File f) { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); try { DocumentBuilder db = dbf.newDocumentBuilder(); Element rootElement = db.parse(f).getDocumentElement(); clearSearch(false); getGroupFromXML(rootElement, null); refreshView(); } catch (Exception ex) { LOG.error(ex); DialogUtils.displayException("Error", "There was an error loading your search", ex); } } public final void refreshView() { List<JComponent> cs; cs = getComponentsFromQueryModel(rootGroup); this.setLayout(new BorderLayout()); JPanel p = ViewUtil.getClearPanel(); // p.setBorder(ViewUtil.getBottomLineBorder()); p.setLayout(new MigLayout("hidemode 1, nogrid, fillx, insets 12 12 12 20")); this.removeAll(); JLabel l = ViewUtil.getEmphasizedLabel("SEARCH CONDITIONS"); p.add(l, "left"); p.add(ViewUtil.getHelpButton("How to search", "Type a search condition into the search box, e.g. \"Chromosome\". " + "Press Enter / Return to accept the selected condition name. " + "You\'ll then be prompted to specify parameters for this condition."), "left, wrap"); for (JComponent c : cs) { p.add(c, "left, wrap"); } p.add(warningText, "left, wrap"); p.add(applyButton, "left, wrap, gapy n 1"); JScrollPane jsp = ViewUtil.getClearBorderlessScrollPane(p); jsp.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); this.add(jsp, BorderLayout.CENTER); this.invalidate(); this.updateUI(); } private String getGroupTitle(SearchConditionGroupItem scg) { if (scg.getDescription() != null && scg.getDescription().length() > 0) { return (scg.getParent().isFirstItem(scg) ? "" : scg.getRelation().toString()) + " " + scg.getDescription(); } else { return (scg.getParent().isFirstItem(scg) ? "" : scg.getRelation().toString()) + " " + scg.getItems().size() + " grouped condition(s)"; } } private List<JComponent> getComponentsFromQueryModel(SearchConditionGroupItem g) { return getComponentsFromQueryModel(g, 0); } private List<JComponent> getComponentsFromQueryModel(SearchConditionGroupItem g, int depth) { List<JComponent> components = new ArrayList<JComponent>(); int w = 0; for (final SearchConditionItem item : g.getItems()) { if (item instanceof SearchConditionGroupItem) { String addition = ""; if (item.getParent() != null) { addition = item.getRelation() + ""; } final JPanel p = ViewUtil.getClearPanel(); MigLayout ml = new MigLayout("wrap 1, hidemode 1, insets 2"); p.setLayout(ml); Border border = ViewUtil.getThickLeftLineBorder(); p.setBorder(border); final PillView pv = new PillView(true); pv.indent(depth); pv.setActivated(true); final ActionListener toggleGroupExpand = new ActionListener() { @Override public void actionPerformed(ActionEvent ae) { expandedItemsMap.put((SearchConditionGroupItem) item, !p.isVisible()); p.setVisible(!p.isVisible()); pv.setText(getGroupTitle((SearchConditionGroupItem) item)); } }; pv.setExpandListener(toggleGroupExpand); pv.setPopupGenerator(new ConditionPopupGenerator() { @Override public JPopupMenu generatePopup() { final SearchConditionGroupItem groupItem = (SearchConditionGroupItem) item; final JPopupMenu m = new JPopupMenu(); if (!item.getParent().isFirstItem(item)) { if (item.getRelation() == QueryRelation.AND) { JMenuItem b = new JMenuItem("Change to \"or\""); b.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent ae) { item.setRelation(QueryRelation.OR); pv.setText(getGroupTitle(groupItem)); } }); m.add(b); } else { JMenuItem b = new JMenuItem("Change to \"and\""); b.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent ae) { item.setRelation(QueryRelation.AND); pv.setText(getGroupTitle(groupItem)); } }); m.add(b); } } JMenuItem delgroup = new JMenuItem("Delete"); delgroup.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent ae) { groupItem.getParent().removeItem(groupItem); refreshView(); expandedItemsMap.remove((SearchConditionGroupItem) item); } }); m.add(delgroup); return m; } }); SearchConditionGroupItem g2 = (SearchConditionGroupItem) item; pv.setText(getGroupTitle(g2)); Boolean exp = expandedItemsMap.get((SearchConditionGroupItem) item); if (exp == null) { p.setVisible(false); pv.collapse(); } else { p.setVisible(true); pv.expand(); } components.add(pv); for (JComponent c : getComponentsFromQueryModel((SearchConditionGroupItem) item, depth + 1)) { p.add(c, "left"); } components.add(p); w = pv.getMaximumSize().width; } else { SearchConditionItemView sciv = itemToViewMap.get(item); ((PillView) sciv).indent(depth); w = sciv.getMaximumSize().width; sciv.refresh(); components.add(sciv); } } if (w == 0) { w = BOTTOM_SEARCH_TEXTFIELD_WIDTH; } JComponent c = getInputFieldForGroup(g, w); components.add(c); return components; } private int c = 0; @Override public void searchConditionsOrderChanged(SearchConditionItem m) { setConditionsChanged(true); this.refreshView(); } public void registerViewWithItem(SearchConditionItemView editor, SearchConditionItem item) { itemToViewMap.put(item, editor); } @Override public void searchConditionItemRemoved(SearchConditionItem m) { itemToViewMap.remove(m); } public void generateItemViewAndAddToGroup(SearchConditionItem sci, SearchConditionGroupItem parent) { SearchConditionItemView view = conditionViewGenerator.generateViewForItem(sci); addItemToGroup(sci, view, parent); } public SearchConditionItemView generateItemViewAndAddToGroup(String fieldName, SearchConditionGroupItem parent) { SearchConditionItem item = new SearchConditionItem(fieldName, parent); SearchConditionItemView view = conditionViewGenerator.generateViewForItem(item); addItemToGroup(item, view, parent); return view; } public void addItemToGroup(SearchConditionItem item, SearchConditionItemView view, SearchConditionGroupItem parent) { registerViewWithItem(view, item); parent.addItem(item); } public void addGroupToGroup(SearchConditionGroupItem child, SearchConditionGroupItem parent) { parent.addItem(child); } public SearchConditionGroupItem getQueryRootGroup() { return this.rootGroup; } private JComponent getInputFieldForGroup(final SearchConditionGroupItem g, int width) { final QueryViewController instance = this; final Map<String, List<String>> possible = conditionViewGenerator.getAllowableItemNames(); final CaseInsensitiveArrayList allPossible = new CaseInsensitiveArrayList(); for (String key : possible.keySet()) { allPossible.addAll(possible.get(key)); } final JButton addCondition = ViewUtil.getSoftButton("Add search condition..."); final CustomKeyListener keyListener = new CustomKeyListener(this, possible, allPossible, g); addCondition.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { keyListener.refreshPopup(addCondition); } }); JPanel p = ViewUtil.getClearPanel(); ViewUtil.applyHorizontalBoxLayout(p); p.add(addCondition); return p; } @Override public void searchConditionItemAdded(SearchConditionItem m) { } @Override public void searchConditionEdited(SearchConditionItem item) { setConditionsChanged(true); } private void setConditionsChanged(boolean b) { if (applyButton != null) { warningText.setVisible(b); applyButton.setSelected(b); applyButton.setEnabled(b); } } private Condition getSQLConditionsFrom(SearchConditionItem item) throws Exception { if (item instanceof SearchConditionGroupItem) { SearchConditionGroupItem group = (SearchConditionGroupItem) item; Condition childrenConditions = null; for (SearchConditionItem child : group.getItems()) { if (childrenConditions == null) { childrenConditions = getSQLConditionsFrom(child); } else { if (child.getRelation() == QueryRelation.AND) { childrenConditions = ComboCondition.and(childrenConditions, getSQLConditionsFrom(child)); } else if (child.getRelation() == QueryRelation.OR) { childrenConditions = ComboCondition.or(childrenConditions, getSQLConditionsFrom(child)); } } } return childrenConditions; } else { return conditionViewGenerator.generateConditionForItem(item); } } private static class CustomKeyListener { private final Map<String, List<String>> possible; private final CaseInsensitiveArrayList allPossible; private SearchConditionGroupItem group; private QueryViewController qvc; public CustomKeyListener(QueryViewController qvc, Map<String, List<String>> p, CaseInsensitiveArrayList ap, SearchConditionGroupItem g) { this.qvc = qvc; this.possible = p; this.allPossible = ap; this.group = g; } ; private ArrayList<JComponent> menuComponents; List<JMenuItem> menuItems = new ArrayList<JMenuItem>(); int currentlySelectedIndex; public void addItemBasedOnField(String validTerm) { final SearchConditionItemView view = qvc.generateItemViewAndAddToGroup(validTerm, group); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { view.showDialog(view.getLocationOnScreen()); } }); } public void refreshPopup(JComponent parent) { QueryConditionDialog m = new QueryConditionDialog(possible); m.setVisible(true); String choice = m.getChosenConditionName(); if (choice != null) { addItemBasedOnField(choice); } /* currentlySelectedIndex = -1; Dimension d = new Dimension(field.getWidth(), 23); menuComponents = new ArrayList<JComponent>(); menuItems.removeAll(menuItems); for (String key : possible.keySet()) { JMenuItem l = new JMenuItem(key.toUpperCase()); Font f = new Font(l.getFont().getFamily(), Font.PLAIN, 10); l.setFont(f); l.setEnabled(false); l.setBackground(new Color(179, 189, 199)); l.setForeground(Color.white); Dimension d2 = new Dimension(d.width, d.height - 5); l.setMinimumSize(d2); l.setMaximumSize(d2); l.setPreferredSize(d2); int headerIndex = menuComponents.size(); boolean sectionHasMatch = false; Collections.sort(possible.get(key)); for (String s : possible.get(key)) { int indexOfMatch = s.toLowerCase().indexOf(field.getText().toLowerCase()); if (indexOfMatch > -1) { sectionHasMatch = true; int to = indexOfMatch + field.getText().length(); final JMenuItem i = new JMenuItem("<html>" + s.substring(0, indexOfMatch) + "<b>" + s.substring(indexOfMatch, to) + "</b>" + s.substring(to) + "</html>"); fixSize(i, d); i.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent ae) { field.setText(i.getText().replaceAll("\\<.*?>", "")); } }); menuItems.add(i); menuComponents.add(i); } } if (sectionHasMatch) { menuComponents.add(headerIndex, l); } } int originalheight = 0; for (Component c : menuComponents) { originalheight += c.getPreferredSize().getHeight(); } //int maxHeight = 100; int adjustedheight = originalheight; m = new ScrollableJPopupMenu(15); m.setFocusable(false); m.removeAll(); for (JComponent c : menuComponents) { m.addComponent(c); } Dimension dadjusted = new Dimension(m.getPreferredSize().width, adjustedheight + 10); //fixSize(m, dadjusted); //m.pack(); m.setPopupSize(200, 400); m.show(field, 0, 23); m.updateUI(); */ } } public class CaseInsensitiveArrayList extends ArrayList<String> { @Override public boolean contains(Object o) { String paramStr = (String) o; for (String s : this) { if (paramStr.equalsIgnoreCase(s)) { return true; } } return false; } public String getInsensitiveMatch(String o) { String paramStr = (String) o; for (String s : this) { if (paramStr.equalsIgnoreCase(s)) { return s; } } return null; } } }