/* * Copyright 2003-2010 Tufts University Licensed under the * Educational Community 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.osedu.org/licenses/ECL-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 edu.tufts.vue.metadata.action; import edu.tufts.vue.rdf.*; import edu.tufts.vue.layout.Cluster2Layout; import edu.tufts.vue.metadata.*; import java.awt.event.*; import java.awt.geom.Rectangle2D; import java.net.*; import java.util.*; import javax.swing.*; import tufts.vue.*; import tufts.vue.LWComponent.HideCause; import tufts.vue.LWComponent.Flag; import tufts.vue.LWMap.Layer; import tufts.vue.gui.GUI; import tufts.vue.gui.GUI.ComboKey; import tufts.Util; // todo: #data=foo, #content=bar, #key=, etc, should search (or: search syntax: #data/Spanish ?) // the entire DOMAINS of data-set data, resource meta-data, VUE keywords (if we keep that domain) // #data/Region=East Coast would work as well, etc. An EMPTY SEARCH in a domain would // actually in fact generate a hit on ALL nodes that had ANYTHING in that domain -- // very useful for discovering where data is in a map. Could even allow for // type=node/link/group/slide/image /** * SearchAction.java * * Created on July 27, 2007, 2:21 PM * * @author dhelle01 * * Todo: RDF is overkill for our purposes -- a simple parameterized traversal would be * much simpler. * * Some refactoring triage by S.Fraize 2012 * */ public class SearchAction extends AbstractAction { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(SearchAction.class); //---------------------------------------------------------------------------------------- // constants //---------------------------------------------------------------------------------------- public static final ComboKey SEARCH_SCOPE_CURRENT_MAP = new ComboKey("searchgui.currentmap"); public static final ComboKey SEARCH_SCOPE_ALL_OPEN_MAPS = new ComboKey("searchgui.allopenmaps"); /** search-types: which kind of fields to include in the search (domains/name-spaces) Note that Cat+Key actually modifies the search query, not the index set */ public static final SearchAction.SearchType SEARCH_EVERYTHING = new SearchType("searchgui.searcheverything"), SEARCH_ONLY_LABELS = new SearchType("searchgui.labels"), SEARCH_ONLY_KEYWORDS = new SearchType("searchgui.keywords"), // "VUE" keywords, which means just the VueMetadataElements / Keywords Widget SEARCH_WITH_CATEGORIES = new SearchType("searchgui.categories_keywords"); // search keywords with specific field labels named to search /** result-actions -- will appear in combo-boxes in order of declaration. */ public static final SearchAction.ResultOp RA_SELECT = new ResultOp("searchgui.select"), RA_SHOW = new ResultOp("searchgui.show"), RA_HIDE = new ResultOp("searchgui.hide"), RA_CLUSTER = new ResultOp("searchgui.cluster"), RA_LINK = new ResultOp("searchgui.link"), RA_COPY = new ResultOp("searchgui.copynewmap"); /** AND/OR keys for setting the cross-term logical operation */ public enum Operator { AND("searchgui.and"), OR("searchgui.or"); public final String key, localized; Operator(String k) { key = k; localized = VueResources.local(key); } /** return localized value -- handy for feeding to a JComboBox */ public String toString() { return localized; } }; public static final Object[] AllLogicOps = { Operator.OR, Operator.AND }; /** key that SearchAction uses to put clientData in the LWMap when it owns some filtering state */ public static final Object RevertableState = "search.revertable.state"; //---------------------------------------------------------------------------------------- // global state //---------------------------------------------------------------------------------------- private static Collection<LWComponent> FilteredBySearch; private static LWMap FilteredMap; private static ResultOp LastAction = RA_SELECT; private static int ResultMapCount = 1; //---------------------------------------------------------------------------------------- // implementation //---------------------------------------------------------------------------------------- private ComboKey searchScope = SEARCH_SCOPE_CURRENT_MAP; private Operator crossTermOperator = Operator.AND; private ResultOp resultAction = RA_SELECT; private final List<VueMetadataElement> searchTerms; private List<String> textToFind = new ArrayList<String>(); private boolean setBasic = true; private boolean textOnly = false; private boolean everything = false; // no longer makes functional difference private boolean metadataOnly = false; private boolean treatNoneSpecially = false; private boolean actualCriteriaAdded; private boolean waitCursorActivated; public SearchAction(List<edu.tufts.vue.metadata.VueMetadataElement> searchTerms, SearchType type) { super(VueResources.getString("searchgui.search")); //searchType = TYPE_QUERY; this.searchTerms = searchTerms; if (type != null) setParamsByType(type); } public SearchAction(List<edu.tufts.vue.metadata.VueMetadataElement> searchTerms) { this(searchTerms, null); } public void fire(Object source, String sourceTag) { actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, sourceTag)); } public void setLocationType(Object scope) { if (scope == SEARCH_SCOPE_CURRENT_MAP || scope == SEARCH_SCOPE_ALL_OPEN_MAPS) this.searchScope = (ComboKey) scope; else throw new IllegalArgumentException(GUI.name(scope)); } public void setOperator(Operator op) { this.crossTermOperator = op; } /** set the internal parameters automatically via the SearchType-domain */ public void setParamsByType(SearchType type) { setBasic = textOnly = metadataOnly = everything = false; // treatNoneSpecially is only from old versions of VUE that could do // a "category" search (as opposed to "category+keywords") -- // todo: what this actually do when it checked for the #none type? treatNoneSpecially = false; if (type == SEARCH_ONLY_LABELS) setBasic = true; else if (type == SEARCH_ONLY_KEYWORDS) textOnly = metadataOnly = true; else if (type == SEARCH_EVERYTHING) textOnly = everything = true; else//if(type == SEARCH_WITH_CATEGORIES) ; // default: all-false } /** note: everything bit is currently ignored -- we never index slides or slide content */ public void setEverything(boolean set) { everything = set; } public void setBasic(boolean set) { setBasic = set; } public void setNoneIsSpecial(boolean set) { treatNoneSpecially = set; } public void setMetadataOnly(boolean set) { metadataOnly = set; } public void setTextOnly(boolean set) { textOnly = set; } public void setResultAction(ResultOp ra) { this.resultAction = ra; } /** for taking an input directly from a JComboBox where type is unknown */ public void setResultAction(Object ra) { if (ra instanceof ResultOp) setResultAction((ResultOp)ra); else throw new IllegalArgumentException("not a ResultOp: " + Util.tags(ra)); } public ResultOp getResultAction() { return this.resultAction; } public String getName() { return VueResources.getString("searchgui.search"); } public static Object getGlobalResultsType() { return LastAction; } // public void loadKeywords(String searchString) { // tags = new ArrayList<String>(); // String[] parsedSpaces = searchString.split(" "); // for(int i=0;i<parsedSpaces.length;i++) { // tags.add(parsedSpaces[i]); // } // } //---------------------------------------------------------------------------------------- // End of the API //---------------------------------------------------------------------------------------- private static final String AS_ONE_QUERY = "as-one-query(logical-and)"; private static final String AS_MULTIPLE_QUERIES = "as-query-list(logical-or)"; /* * this has been boiled down, tho still needs a rewrite -- way to complex */ // Note: the only thing we take out of the VME's these days are the values, and possibly the keys private List<Query> makeQueryFromCriteria (final List<VueMetadataElement> terms, final String queryType) { Query query = new Query(); // note: most of the time, this won't even be used... final List<Query> queryList; if (queryType == AS_ONE_QUERY) queryList = null; else queryList = new ArrayList<Query>(); this.textToFind = new ArrayList<String>(); // NOTE SIDE EFFECT actualCriteriaAdded = false; if (DEBUG.SEARCH) { Log.debug("makeQueryFromCriteria: " + Util.tags(queryType) + "; terms=" + (terms.size() == 1 ? terms.get(0) : Util.tags(terms))); } for (VueMetadataElement criteria : terms) { if (DEBUG.SEARCH) Log.debug("processing criteria: " + criteria); // [DAN] query.addCriteria(criteria.getKey(),criteria.getValue()); // [SMF] final String[] statement = (String[])(criteria.getObject()); if (setBasic != true) { if (DEBUG.SEARCH) { Log.debug("query setBasic != true -- more than simple label-contains"); Log.debug("RDFIndex.VUE_ONTOLOGY+none=[" + RDFIndex.VUE_ONTOLOGY+"none]"); } if (this.treatNoneSpecially && criteria.getKey().equals(RDFIndex.VueTermOntologyNone)) { // Note that above is only place where treatNoneSpecially is tested if (DEBUG.SEARCH) Log.debug("using manual textToFind: #none is special & no term (query will be empty)"); // [DAN] query.addCriteria("*",criteria.getValue(),statement[2]); textToFind.add(criteria.getValue()); // The Query object won't even be used! } else if (this.textOnly) { // NOTE: this is the ONLY place "textOnly" is tested, and the result is redundant // to the case above -- FACTOR OUT. Note: this means that an EVERYTHING search // is the same as a CATEGORY+KEYWORDS search. if (DEBUG.SEARCH) Log.debug("using manual textToFind: textOnly=true (query will be empty)"); textToFind.add(criteria.getValue()); // The Query object won't even be used! } else { query.addCriteria(criteria.getKey(), criteria.getValue(), "CONTAINS"); // note above we're ignoring the Qualifier that exists in the VME object and // using "contains", which is appropriate, as 2nd term VME's appear to be // marked with STARTS_WITH as opposed to CONTAINS. Same applies to the // addCriteria below. // [DAN] query.addCriteria(criteria.getKey(),criteria.getValue(),statement[2]); // would be nice to be able to say // [DAN] query condition here -- could do as subclass of VueMetadataElement? getCondition()? then a search // [DAN] can be metadata too.. // [SMF] Now I see why statement[2] (VME object data) may have been ignored here -- it wasn't // always being properly set in MetadataSearchMainGUI -- not that we currently have any // GUI for this -- everything is always CONTAINS anyway (e.g., no STARTS_WITH). actualCriteriaAdded = true; } } else { query.addCriteria(RDFIndex.VUE_ONTOLOGY+Constants.LABEL, criteria.getValue(), "CONTAINS"); // [DAN] query.addCriteria(RDFIndex.VUE_ONTOLOGY+Constants.LABEL,criteria.getValue(),statement[2]); // see comments just above actualCriteriaAdded = true; } if (queryList != null) { // Instead of continuing to add criteria to the current query, add this // single-criteria query to the list, and start next iteration with a fresh query. // Note that the only time we appear to add multiple criteria to a single query // is when doing "Labels" searches with operator AND. if (actualCriteriaAdded) { queryList.add(query); query = new Query(); } } } if (DEBUG.SEARCH) { // Note that most of the time, all the queries are empty and won't even be used! if (queryList != null) { int i = 0; for (Query q : queryList) Log.debug("query " + (i++) + ": " + q.createSPARQLQuery()); } else { Log.debug("query: " + (query == null ? "<NO QUERY>" : query.createSPARQLQuery())); } } if (queryList == null) { return query == null ? null : Collections.singletonList(query); } else return queryList; } /** create one query with multiple criteria */ private Query createQuery_Logical_And(List<VueMetadataElement> terms) { return makeQueryFromCriteria(terms, AS_ONE_QUERY).get(0); } /** create one query for each criteria */ private List<Query> createQueries_Logical_Or(List<VueMetadataElement> terms) { return makeQueryFromCriteria(terms, AS_MULTIPLE_QUERIES); } private static final String INDEX_WITH_WAIT_CURSOR = "<index-with-wait-cursor>"; private static final String INDEX_NO_WAIT_CURSOR = "<index-no-wait-cursor>"; /** * @return an index over given search scope (e.g., the currently active map, or all open maps) */ private RDFIndex getIndexForScope(final ComboKey scope) { if (DEBUG.SEARCH) Log.debug("getIndexForScope " + Util.tags(scope)); if (scope == SEARCH_SCOPE_ALL_OPEN_MAPS) { if (DEBUG.SEARCH) Log.debug("indexing all open maps..."); this.waitCursorActivated = true; tufts.vue.gui.GUI.activateWaitCursor(); final RDFIndex globalIndex = new RDFIndex(); // TODO: do we really want to search amongst existing "Search Results" maps? for (LWMap map : VUE.getAllMaps()) { if (DEBUG.SEARCH || DEBUG.RDF) Log.debug("adding to global index: " + map); final RDFIndex mapIndex = getIndexForMap(map, !this.metadataOnly, INDEX_NO_WAIT_CURSOR); globalIndex.addMapIndex(mapIndex); } if (DEBUG.SEARCH || DEBUG.RDF) Log.debug("done indexing all maps."); return globalIndex; } else { // default SEARCH_SCOPE_CURRENT_MAP return getIndexForMap(VUE.getActiveMap(), !this.metadataOnly, INDEX_WITH_WAIT_CURSOR); } } /** * This will find a valid cached index for the given map, or create and populate a fresh * one. Note that this call can also have a crucial side effect: it will activate the global wait * cursor if indexing is begun, and this.waitCursorActivated must be checked later to see if it * should be cleared. */ private RDFIndex getIndexForMap(final LWMap map, boolean include_LWC_fields, String activateWait) { final CachedIndex cache = map.getClientData(CachedIndex.class); final RDFIndex index; if (cache != null && cache.isStillValid(include_LWC_fields)) { if (DEBUG.SEARCH) Log.debug("found valid cached index " + Util.tags(cache)); index = cache.index; } else { if (DEBUG.SEARCH || DEBUG.RDF) { if (cache != null) Log.debug("saw cached index " + Util.tags(cache) + "; but was invalid"); Log.debug("indexing " + map + "..."); } if (activateWait == INDEX_WITH_WAIT_CURSOR) { this.waitCursorActivated = true; tufts.vue.gui.GUI.activateWaitCursor(); } // Todo: can't we get rid of the METAONLY flag by modifying the search itself to ignore // the specific URI keys? index = new RDFIndex(); index.indexAdd(map, !include_LWC_fields, this.everything); // note: the "everything" is currently ignored during indexing -- e.g., we never index LWSlide content map.setClientData(CachedIndex.class, new CachedIndex(index, map, include_LWC_fields)); if (DEBUG.SEARCH || DEBUG.RDF) Log.debug(" indexed " + map + "."); } return index; } private static class CachedIndex { final RDFIndex index; final LWMap map; final long mapChangeStateAtIndex; final boolean indexHad_LWC_fields; CachedIndex(RDFIndex i, LWMap m, boolean didIndex_LWC_fields) { index = i; map = m; mapChangeStateAtIndex = map.getChangeState(); indexHad_LWC_fields = didIndex_LWC_fields; } boolean isStillValid(boolean want_LWC_fields) { return map.getChangeState() == mapChangeStateAtIndex && indexHad_LWC_fields == want_LWC_fields; } } private Collection<LWComponent> runSearch( final edu.tufts.vue.rdf.RDFIndex index, final List<VueMetadataElement> terms) // add actualCriteraAdded & crossTermOperator as explicit inputs { if (DEBUG.SEARCH) { Log.debug("runSearch: term(s): " + (terms.size() > 1 ? terms.size() : terms)); if (terms.size() > 1) Util.dump(terms); } // one of these two will be non-null, tho they will often be empty! final Query query; final List<Query> queryList; if (true /*searchType == TYPE_QUERY*/) { if (DEBUG.SEARCH) Log.debug("operator=" + Util.tags(crossTermOperator)); // The was designed was if it's AND, we create a single query, but if OR, // we create multiple queries. // NOTE THAT A FREQUENT RESULT OF THESE createQuery CALLS IS TO SIDE-EFFECT FILL // "textToFind", and RETURN AN EMPTY QUERY or EMPTY QUERY-LIST. if (crossTermOperator == Operator.AND) { query = createQuery_Logical_And(terms); queryList = null; } else { // if (crossTermOperator == SearchAction.OR) { // Dan had said: "todo: AND in first query" -- what'd he mean by this? // What it seems we could be missing is to handle the OR case via a single query... query = null; queryList = createQueries_Logical_Or(terms); } } // Although we'll get multiple URI hits for single nodes with multiple matching fields, the // current QuerySolution's only contain the *content* of what was matched, not the keyword, // and even if we had the keyword, we don't currently have a use case where we'd do // anything with it, so to simplify things we just make the results a HashSet. [ ED: We've // now added keywords to results, so we could examine them for interesting stats if we like, // e.g., a search for "mentor" (ClubZora test data set) might reveal that all hits happened // to occur on a field named "Role" ] final Collection<URI> results = new HashSet<URI>(); // TYPE_QUERY appears to be used in most (all?) cases, which works differently than // TYPE_FIELD. TYPE_FIELD, I *thought* I saw was used when we search just amongst // "labels", and probably other similar cases. The code paths are more different than they // ought to be. E.g., TYPE_FIELD will match the word "header" from the style on the master // slide -- I think that's an indexing issue -- so they can actually INDEX differently. // That is is being mediated by the "everything" flag. Okay -- it turns out TYPE_FIELD is // never used -- was not deployed in a live codepath in MetadataSearchMainGUI. // // SMF 2012-07 todo: could still refactor the hell out of what's left here. // // if (searchType == TYPE_FIELD) { // // Single search box case -- DOESN'T APPEAR TO BE DEPLOYED IN ANY ACTIVE VUE CODE -- SMF 2012 // if (DEBUG.Enabled) Log.info("TYPE_FIELD IN USE", new Throwable("HERE")); // for (String tag : tags) { // if (DEBUG.RDF || DEBUG.SEARCH) Log.debug("processing tag " + Util.tags(tag)); // results.addAll(index.searchAllResources(tag)); // } // } else if (searchType == TYPE_QUERY) { if (true) { //----------------------------------------------------------------------------- // THIS IS WHERE THE INDEX IS ACTUALLY ASKED TO DO SEARCHES: //----------------------------------------------------------------------------- if (actualCriteriaAdded) { // This code path appears only to be traveled when we NOT searching for "everything" if (textToFind.size() > 0) Log.error("CRITERIA + TEXT-TO-FIND! " + Util.tags(textToFind)); if (DEBUG.SEARCH) Log.debug("Query objects were used, operator=" + crossTermOperator); if (crossTermOperator == Operator.AND) { // In this case, the AND is built into the query via multiple SPARQL sub-statements: results.addAll(index.search(query)); } else if (crossTermOperator == Operator.OR) { // In this case, the OR is handled by running multiple single-statement queries: for (Query q : queryList) results.addAll(index.search(q)); } else { // should never happen: Log.error("UNHANDLED CROSS TERM OPERATOR " + Util.tags(crossTermOperator)); } } if (actualCriteriaAdded && textToFind.size() > 0) throw new Error("I THOUGHT ACTUAL QUERY & TEXT-TO-FIND WERE EXCLUSIVE CASES"); boolean firstTerm = true; for (String textInput : textToFind) { final String textTerm = textInput.trim(); if (DEBUG.SEARCH) Log.debug("textToFind has " + Util.tags(textTerm)); if (textTerm.length() > 0) { if (firstTerm || crossTermOperator == Operator.OR) results.addAll(index.searchAllValues(textTerm)); else if (crossTermOperator == Operator.AND) results.retainAll(index.searchAllValues(textTerm)); else Log.error("Unhandled operator: " + Util.tags(crossTermOperator)); } firstTerm = false; } } return index.decodeVueResults(results); } /** * Entry point for running the actual search. */ public void actionPerformed(ActionEvent ae) { if (DEBUG.SEARCH || DEBUG.RDF) { final Object s = ae.getSource(); Log.debug("AE: " + "srcTag=" + Util.tags(ae.getActionCommand()) + " src=" + (s instanceof java.awt.Component ? GUI.name(s) : Util.tags(s))); } final LWSelection selection = VUE.getSelection(); final LWMap activeMap = VUE.getActiveMap(); // selection.clear(); // let the selection be replaced in one fell swoop: less UI flickering waitCursorActivated = false; Collection<LWComponent> hits = null; try { // getIndex will have activated wait-cursor if new indexing took place hits = runSearch(getIndexForScope(this.searchScope), this.searchTerms); } catch (Throwable t) { Log.error("search error, src=(" + ae + ")", t); } finally { if (waitCursorActivated) tufts.vue.gui.GUI.clearWaitCursor(); } if (DEBUG.SEARCH) { Log.debug("results are in for scope " + Util.tags(searchScope) + "; willProcessAs: " + GUI.name(resultAction)); if (DEBUG.RDF) { // note: in may cases now, DEBUG.RDF means "DEBUG.SEARCH.EXTRA" Log.debug("=raw-results:"); Util.dump(hits); } else { Log.debug("raw-results: " + Util.tags(hits)); } } if (hits == null) { // Should only happen on exception: Log.warn("hits is null back from RDFIndex"); java.awt.Toolkit.getDefaultToolkit().beep(); return; } if (searchScope == SEARCH_SCOPE_ALL_OPEN_MAPS) { // Only one result-action exists for this case -- essentially (or is it exactly?): RA_COPY selection.clear(); displayAllMapsSearchResults(hits); } else if (resultAction == RA_COPY) { selection.clear(); produceSearchResultCopyMap(hits); } else { processResultsToMap(resultAction, hits, activeMap, selection); } } private void processResultsToMap( final ResultOp action, final Collection<LWComponent> hits, final LWMap map, final LWSelection selection) { revertSearchState(false); LastAction = resultAction; if (hits.isEmpty()) { selection.clear(); } else { final Collection<LWComponent> toSelect = targetNodesForAction(action, hits, map); if (DEBUG.SEARCH) { // note: in may cases now, DEBUG.RDF means "DEBUG.SEARCH.EXTRA" if (toSelect == null) { Log.debug("nothing returned for selection."); } else { Log.debug("to-select:" + Util.tags(toSelect)); //Util.dump(toSelect); } } if (action == RA_CLUSTER) { final Cluster2Layout layout = new Cluster2Layout(); layout.layout(selection); VUE.getUndoManager().mark(VueResources.getString("searchgui.cluster")); } else if (action == RA_LINK) { // now need to check toAdd //if (selection.count(LWNode.class) > 0) { if (false) { if (DEBUG.SEARCH) Log.debug("invokeLater " + LinkResultAction.class); // why invoke later? //GUI.invokeAfterAWT(new LinkResultAction(map, selection)); new LinkResultAction(map, selection).run(); } else { if (DEBUG.SEARCH) Log.debug("no LWNode's in selection: " + selection); java.awt.Toolkit.getDefaultToolkit().beep(); } } // Be sure to update the VUE selection in a SINGLE call -- not one element at a time, // which is needlessly slow / causes map updates on every add. if (toSelect != null) selection.setWithDescription(toSelect, "*** Search Results ***"); // TODO: something descriptive here else selection.clear(); //VueToolbarController.getController().selectionChanged(VUE.getSelection()); //VUE.getActiveViewer().requestFocus(); } map.notify(this, LWKey.Repaint); final MapViewer viewer = VUE.getActiveViewer(); if (viewer != null) { viewer.grabVueApplicationFocus("search", null); // This repaint should help in the case the active focal isn't a map / serves as backup viewer.repaint(); } } /** We must use PROPER (or ANY) to see node icons (EDITABLE leaves out node-icons), ANY would * include slides, which we don't want. */ private static final LWComponent.ChildKind SearchRelevant = LWComponent.ChildKind.PROPER; private static final int TARGETING_DENIED = Flag.FILTERED.bit | Flag.LOCKED.bit // | Flag.ICON.bit // handling separately // We shouldn't ever see any these, but just in case: | Flag.STYLE.bit // should never see unless index is bad (e.g., indexed ChildKind.ANY v.s. PROPER) | Flag.DATA_STYLE.bit // should never see unless index is bad | Flag.SLIDE_STYLE.bit // should never see unless index is bad | Flag.INTERNAL.bit // should never see | Flag.DELETED.bit // should never see | Flag.PRUNED.bit; // should never need (already hidden if this is set) /** * Take the given set of hits from the RDFIndex based search, and process them depending on the * the user-selected resulting action. Any returned collection of LWComponents are items to * actually be selected, which can vary from the LWComponents that were registered as hits. * (E.g., a hit on a node-icon will actually select it's parent node). * * At the moment, this method will also take aciton in the cases of RA_HIDE/RA_SHOW by * setting the appropriate FILTERED flags. */ private Collection<LWComponent> targetNodesForAction(final ResultOp action, final Collection<LWComponent> hits, final LWMap map) { if (DEBUG.SEARCH) Log.debug(Util.color(GUI.name(resultAction), Util.TERM_GREEN)); // THE PROBLEM WITH NODE ICONS: To review this ancient and regretful hack: When a tufts.vue.Resource is // set on a node that appears to be a reference to image content, we want to show that content right in // the node. To do this, we create an LWImage with an identical Resource, and add it as the first // child of that node. There are hacks abound through VUE to deal with this. These days, these // special LWImage's should also all have Flag.ICON set, a flag created for that purpose. Note that it // is of course possible to have an LWImage with a DIFFERENT resource than a node, added as a child. // This is especially wierd if the node had an image-icon-node, but it was deleted -- the resulting // LWImage will look exacly like a node-icon, but it won't be. // In any case, if we are going to be SELECTING, we need to turn any hit on a node-icon image to a hit // on the node. If we are going to be FILTERING, we need the reverse: to turn any filter on a node to // also filter the node-icon (or do we want it both ways then?). We need to handle the filtering case // in two places: when HIDING, we filter what we've hit, when SHOWING, we filter everything else. When // filtering everything else, we want all node-icons hit -- we can have that handled by requesting // ChildKind.PROPER descendents when we ask for everything else. // That said, there is something else we want: in cases of SELECT/LINK/CLUSTER, lets call them // TARGETING actions, we do not want to hit ANY nested content. So a single pass to add nodes for // their hit node icons, follwed by a pass to remove all nested content should work there. Also, in // the case of TARGETING actions, we only want to pick ChildKind.EDITABLE content (e.g., nothing on // hidden or locked layers). [Note that if we're were messing with the RDFindex stuff, we could have // such searches initially traverse only ChildKind.EDITABLE in the first place, but RDFindex simply // looks at all ChildKind.PROPER nodes). final boolean doTargeting = (action == RA_SELECT || action == RA_LINK || action == RA_CLUSTER); final boolean doFiltering = (action == RA_HIDE || action == RA_SHOW); if (doFiltering == doTargeting) throw new Error("impossible"); final Collection<LWComponent> targets = new HashSet<LWComponent>(hits.size()); if (doTargeting) { // TARGETING: remove disallowed targets, and retarget node-icons to their nodes for (LWComponent c : hits) { // First, leave out anything even remotely hidden or locked: if (c.isHidden() || c.hasAnyFlag(TARGETING_DENIED)) continue; final LWMap.Layer layer = c.getLayer(); if (layer == null || layer.isHidden() || layer.isLocked()) continue; // todo: what about being hidden via ancestors? if (c.hasFlag(Flag.ICON)) targets.add(c.getParent()); else targets.add(c); } // Now remove nested targets final List<LWComponent> nested = new ArrayList<LWComponent>(); for (LWComponent c : targets) { if (c.atTopLevel()) continue; for (LWComponent ancestor : c.getAncestors()) { if (targets.contains(ancestor)) { nested.add(c); break; } } } targets.removeAll(nested); return targets; } else { // FILTERING: add extra targets for (LWComponent c : hits) { targets.add(c); if (LWNode.isImageNode(c)) { // also filter the node-icon targets.add(c.getChild(0)); } else if (c instanceof LWGroup) { // If a search hit is on an actual LWGroup (e.g., notes hit), and we're // FILTERING that group, we ALSO want to filter all the children. Or, if we're // exclusively SHOWING that group, we want to make sure to show all the // children also. targets.addAll(c.getAllDescendents(SearchRelevant)); } } if (action == RA_HIDE) { hideComponents(map, targets); } else if (action == RA_SHOW) { // compute inverse of targets for this map: final Collection<LWComponent> allOnMap = map.getAllDescendents(SearchRelevant); final Collection<LWComponent> toHide = new ArrayList(32); for (LWComponent c : allOnMap) if (!targets.contains(c)) toHide.add(c); hideComponents(map, toHide); } else throw new Error(action.toString()); } return null; } private static void hideComponents(LWMap map, Collection<LWComponent> toHide) { FilteredBySearch = toHide; FilteredMap = map; for (LWComponent c : toHide) { // VUE-892 -- switch back to setFiltered from setHidden (needs change in LWImage to work for image nodes, but this // will handle child nodes/images correctly in non image nodes) c.setFiltered(true); } map.setClientData(RevertableState, Integer.valueOf(toHide.size())); } private static void revertPreviouslyHiddenToVisible(boolean repaint) { if (FilteredBySearch == null) { Log.warn("nothing was previously hidden"); return; } for (LWComponent c : FilteredBySearch) c.setFiltered(false); FilteredMap.setClientData(RevertableState, null); // removes property if (repaint) { // Note: this will NOT cause a viewer repaint if its current focal isn't the whole map FilteredMap.notify(SearchAction.class, LWKey.Repaint); } FilteredBySearch = null; FilteredMap = null; } private class LinkResultAction implements Runnable { final LWMap map; final LWSelection selection; // also incoming: searchTerms, crossTermOperator /** only meaningful if s.count(LWNode.class) > 0, otherwise nothing happens */ LinkResultAction(LWMap m, LWSelection s) { map = m; selection = s; } /** Create a new node, name it based on the search, and link the selected nodes to it. */ public void run() { if (DEBUG.SEARCH) Log.debug("LinkResultAction:run"); final StringBuilder name = new StringBuilder(); final LWNode centralNode = new LWNode("-central-node-"); boolean firstTerm = true; for (VueMetadataElement criteria : SearchAction.this.searchTerms) { final String key = criteria.getKey(); final String value = criteria.getValue(); if (!firstTerm) { name.append(' '); name.append(crossTermOperator.localized); name.append(' '); } else { firstTerm = false; } name.append(value); if (!key.equals(RDFIndex.VueTermOntologyNone)) { centralNode.addDataValue(key, value); } } final List<LWComponent> newComps = new ArrayList<LWComponent>(); final List<LWNode> selectedNodes = new ArrayList<LWNode>(); centralNode.setLabel(name.toString()); newComps.add(centralNode); // Create links from each node in selection (each LWNode hit by the search) // to the new central "search" node. for (LWNode hitNode : Util.typeFilter(selection, LWNode.class)) { LWLink newLink = new LWLink(hitNode, centralNode); selectedNodes.add(hitNode); newComps.add(newLink); } if (selectedNodes.size() > 0) { // wierd test, given knowns, but used ot exist for the doClusterAction call final Rectangle2D selectionBounds = selection.getBounds(); final UndoManager undoManager = map.getUndoManager(); final MapViewer activeViewer = VUE.getActiveViewer(); centralNode.setCenterAt(selectionBounds.getCenterX(), selectionBounds.getCenterY()); map.addChildren(newComps); if (undoManager != null) undoManager.mark(VueResources.getString("searchgui.link")); // @@.;tufts.vue.Actions.MakeCluster.doClusterAction(newNode, selectedNodes); // undoMgr.mark(VueResources.getString("menu.format.layout.makecluster")); if (activeViewer.getFocal() == map) { // we check activeViewer focal just in case it might have changed to another map, // or another component on this map, such as a group or a slide. activeViewer.scrollRectToVisible(selectionBounds.getBounds()); // activeViewer.scrollRectToVisible(selection.getBounds().getBounds()); // [???] must get bounds again after cluster action } selection.setWithDescription(newComps, "*** Link Search ****"); // tufts.vue.Actions.PushOut.act(); // undoMgr.mark(VueResources.local("menu.format.arrange.pushout")); } } } private void displayAllMapsSearchResults(final Collection<LWComponent> comps) { final LWMap searchResultMap = new LWMap("Search Result #" + ResultMapCount++); for (LWMap map : VUE.getAllMaps()) { if (DEBUG.SEARCH) Log.debug("processing " + map); //--------------------------------------------------------------------------------------------------- // The outer loop looks completely redundant here -- could just iterate comps checking for EDITABLE -- SMF //--------------------------------------------------------------------------------------------------- for (LWComponent next : map.getAllDescendents(LWComponent.ChildKind.EDITABLE)) { // TODO: what kind? if (comps.contains(next)) { // Todo: could use copy-context to include // links that exist between items in the set of hits. LWComponent duplicate = next.duplicate(); // why are we duplicating for this??? was this a bad cut/paste from the copy code? LWComponent parent = next.getParent(); if(parent !=null && !comps.contains(parent)) { if(parent instanceof LWNode) { duplicate.setLocation(parent.getLocation()); } if (LWNode.isImageNode(parent)) { if(!comps.contains(parent)) { LWComponent dup = parent.duplicate(); if(!(dup instanceof LWSlide) && !dup.hasFlag(LWComponent.Flag.SLIDE_STYLE) && (!(dup.hasAncestorOfType(LWSlide.class))) ) searchResultMap.add(dup); } } else { if(!(duplicate instanceof LWSlide) && !duplicate.hasFlag(LWComponent.Flag.SLIDE_STYLE) && (!(duplicate.hasAncestorOfType(LWSlide.class))) ) searchResultMap.add(duplicate); } /*if(next.hasFlag(LWComponent.Flag.SLIDE_STYLE)) { LWSlide slide = (LWSlide)next.getParentOfType(LWSlide.class); //searchResultMap.add(slide); searchResultMap.add(slide.getSourceNode()); }*/ } } } } VUE.displayMap(searchResultMap); } private void produceSearchResultCopyMap(final Collection<LWComponent> comps) { LWMap searchResultMap = new LWMap("Search Result " + ResultMapCount++); HashMap<LWComponent,LWComponent> duplicates = new HashMap<LWComponent,LWComponent>(); Iterator<LWComponent> components = VUE.getActiveMap().getAllDescendents(LWComponent.ChildKind.EDITABLE).iterator(); while(components.hasNext()) { LWComponent next = components.next(); if(comps.contains(next)) { LWComponent duplicate = next.duplicate(); duplicates.put(next,duplicate); LWComponent parent = next.getParent(); if(parent !=null && !comps.contains(parent)) { if(parent instanceof LWNode) { duplicate.setLocation(parent.getLocation()); } /*if(next instanceof LWLink) { LWLink link = (LWLink)next; LWComponent head = link.getHead(); if(head != null && comps.contains(head)) ((LWLink)duplicate).setHead(head); // OOPS needs to be // head's duplicate LWComponent tail = link.getTail(); if(tail != null && comps.contains(tail)) ((LWLink)duplicate).setTail(tail); // double OOPS }*/ // do we need this code any more? see // "raw image" search bug in jira // if we do, links may have to be handled // correctly for these nodes as well if(LWNode.isImageNode(parent)) { if(!comps.contains(parent)) { if(!(parent instanceof LWSlide) && !parent.hasFlag(LWComponent.Flag.SLIDE_STYLE) && (!parent.hasAncestorOfType(LWSlide.class))) searchResultMap.add(parent.duplicate()); } } else { if(!(duplicate instanceof LWSlide) && !duplicate.hasFlag(LWComponent.Flag.SLIDE_STYLE) && (!(duplicate.hasAncestorOfType(LWSlide.class)))) searchResultMap.add(duplicate); } } } } Iterator<LWComponent> components2 = VUE.getActiveMap().getAllDescendents(LWComponent.ChildKind.EDITABLE).iterator(); while(components2.hasNext()) { LWComponent next = components2.next(); if(next instanceof LWLink && duplicates.get(next) != null) { LWLink link = (LWLink)next; LWComponent head = link.getHead(); if(head != null && comps.contains(head)) ((LWLink)duplicates.get(next)).setHead(duplicates.get(head)); LWComponent tail = link.getTail(); if(tail != null && comps.contains(tail)) ((LWLink)duplicates.get(next)).setTail(duplicates.get(tail)); } } VUE.displayMap(searchResultMap); } // public void revertSelections() { // revertGlobalSearchSelection(); // } // /*public:deprecating*/ private void revertSelections_does_nothing(List<LWComponent> toBeReverted) // { // if (MARQUEE || toBeReverted == null) // return; // for (LWComponent c : toBeReverted) { // if (MARQUEE == false) { // // PROBLEM: only the LWSelection should be calling setSelected // // (further problem: given that, that API should have restricted this) // // Dan may have been trying to make use of just the selection halo's // // alone, tho that's quite confusing from a UX perspective. // c.setSelected(false); // } else { // // already done on click on VUE map // //VUE.getSelection().clear(); // } // } // } // /*public*/ private static void revertSelectionsFromMSGUI(Collection<LWComponent> toBeReverted) { // if (MARQUEE) { // VUE.getSelection().clear(); // VUE.getActiveViewer().repaint(); // return; // } // if (toBeReverted == null) // return; // final Iterator<LWComponent> it = toBeReverted.iterator(); // while (it.hasNext()) { // if(MARQUEE == false) { // it.next().setSelected(false); // } else { // /*Thread t = new Thread() { // public void run() { // //VUE.getSelection().clear(); // } // }; // try { // //SwingUtilities.invokeLater(t); // //t.start(); // } catch(Exception e) { // System.out.println("SearchAction - Exception trying to clear selection: " + e); // }*/ // } // } // } // TODO: should only need if from same map -- tie to an enabled Revert button private static void revertSearchState(boolean repaint) { if (LastAction == RA_HIDE || LastAction == RA_SHOW) revertPreviouslyHiddenToVisible(repaint); } public static void revertGlobalSearchSelection() { revertSearchState(true); } // TODO: should only need if from same map -- tie to an enabled Revert button public static void revertGlobalSearchSelectionFromMSGUI() { if (LastAction == RA_SELECT || LastAction == RA_CLUSTER || LastAction == RA_LINK) { VUE.getSelection().clear(); // todo: won't cause panner to repaint -- must not be listening to the selection? // revertSelectionsFromMSGUI(globalResults); } else if (LastAction == RA_HIDE || LastAction == RA_SHOW) revertPreviouslyHiddenToVisible(true); final MapViewer viewer = VUE.getActiveViewer(); LWMap map = null; LWComponent focal = null; if (viewer != null) { map = viewer.getMap(); focal = viewer.getFocal(); if (focal != null && focal != map) focal.notify(SearchAction.class, LWKey.Repaint); if (map != null) map.notify(SearchAction.class, LWKey.Repaint); viewer.repaint(); // shouldn't need this -- but just in case // Technically, what we really want to do is to issue a repaint through both to the // map and to the viewer FOCAL. (as we don't generate events for ever setFiltered // call, and don't want to) // Note: could also consider handling this via a UserActionCompleted. } } /** localized enum type for the kind of search (e.g., labels, everything, etc). */ public static final class /*enum*/ SearchType extends ComboKey { public static final Map<String,SearchType> KeyMap=new LinkedHashMap(), ValueMap=new HashMap(); private SearchType(String s) { super(s, KeyMap, ValueMap); } public static Object[] All() { if (KeyMap.isEmpty()) throw new InitError(SearchType.class); return KeyMap.values().toArray(); } } /** localized enum type for the desired result operation: e.g., select, show, hide, etc */ public static final class /*enum*/ ResultOp extends ComboKey { public static final Map<String,ResultOp> KeyMap=new LinkedHashMap(), ValueMap=new HashMap(); private ResultOp(String s) { super(s, KeyMap, ValueMap); } public static Object[] All() { if (KeyMap.isEmpty()) throw new InitError(ResultOp.class); return KeyMap.values().toArray(); } //public static final ResultOp NEW_OP = new ResultOp("my.new.op"); // this works -- would ensure calls to All would never be empty } }