Index tokenized lowercase versions of name and types for UI searches
[simantics/platform.git] / bundles / org.simantics.debug.ui / src / org / simantics / debug / ui / SearchResourceDialog.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2016 Association for Decentralized Information Management
3  * in Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *     Semantum Oy - index based searching and graph manipulation (#4255)
12  *******************************************************************************/
13 package org.simantics.debug.ui;
14
15 import java.util.ArrayList;
16 import java.util.Arrays;
17 import java.util.Collection;
18 import java.util.Collections;
19 import java.util.Comparator;
20 import java.util.HashSet;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Set;
24 import java.util.TreeSet;
25 import java.util.regex.Matcher;
26 import java.util.regex.Pattern;
27 import java.util.stream.Collectors;
28
29 import org.eclipse.core.runtime.CoreException;
30 import org.eclipse.core.runtime.IProgressMonitor;
31 import org.eclipse.core.runtime.IStatus;
32 import org.eclipse.core.runtime.Status;
33 import org.eclipse.jface.dialogs.IDialogConstants;
34 import org.eclipse.jface.dialogs.IDialogSettings;
35 import org.eclipse.jface.resource.JFaceResources;
36 import org.eclipse.jface.resource.LocalResourceManager;
37 import org.eclipse.jface.resource.ResourceManager;
38 import org.eclipse.jface.viewers.IStructuredSelection;
39 import org.eclipse.jface.viewers.LabelProvider;
40 import org.eclipse.jface.viewers.StructuredSelection;
41 import org.eclipse.swt.graphics.Image;
42 import org.eclipse.swt.widgets.Composite;
43 import org.eclipse.swt.widgets.Control;
44 import org.eclipse.swt.widgets.Display;
45 import org.eclipse.swt.widgets.Shell;
46 import org.eclipse.ui.IMemento;
47 import org.eclipse.ui.dialogs.FilteredItemsSelectionDialog;
48 import org.simantics.Simantics;
49 import org.simantics.db.ReadGraph;
50 import org.simantics.db.Resource;
51 import org.simantics.db.Session;
52 import org.simantics.db.common.primitiverequest.PossibleAdapter;
53 import org.simantics.db.common.procedure.adapter.TransientCacheListener;
54 import org.simantics.db.common.request.BinaryRead;
55 import org.simantics.db.common.request.ObjectsWithType;
56 import org.simantics.db.common.request.ReadRequest;
57 import org.simantics.db.common.request.UniqueRead;
58 import org.simantics.db.common.uri.UnescapedChildMapOfResource;
59 import org.simantics.db.common.utils.NameUtils;
60 import org.simantics.db.exception.DatabaseException;
61 import org.simantics.db.layer0.genericrelation.Dependencies;
62 import org.simantics.db.layer0.genericrelation.IndexQueries;
63 import org.simantics.db.layer0.migration.OntologiesFromLibrary;
64 import org.simantics.db.layer0.variable.Variables.Role;
65 import org.simantics.db.request.Read;
66 import org.simantics.db.service.SerialisationSupport;
67 import org.simantics.debug.ui.ResourceSearch.IResourceFilter;
68 import org.simantics.debug.ui.internal.Activator;
69 import org.simantics.debug.ui.internal.DebugUtils;
70 import org.simantics.layer0.Layer0;
71 import org.simantics.operation.Layer0X;
72 import org.simantics.scl.runtime.function.Function;
73 import org.simantics.ui.selection.ResourceWorkbenchSelectionElement;
74 import org.simantics.ui.workbench.dialogs.ResourceLabelProvider;
75 import org.simantics.utils.Container;
76 import org.simantics.utils.ui.BundleUtils;
77 import org.slf4j.Logger;
78 import org.slf4j.LoggerFactory;
79
80 /**
81  * TODO Add Debugger Composite as preview!
82  */
83 public class SearchResourceDialog extends FilteredItemsSelectionDialog {
84
85     private static final Logger LOGGER = LoggerFactory.getLogger(SearchResourceDialog.class);
86
87     /**
88      * The default maximum amount of Dependencies index hits to produce as results.
89      */
90     private static final int DEFAULT_MAX_INDEX_HITS = 1000;
91
92     private static final Pattern ID_PATTERN = Pattern.compile("\\$([0-9]+)"); //$NON-NLS-1$
93
94     private static final String SEARCH_RESOURCE_DIALOG = "SearchResourceDialog"; //$NON-NLS-1$
95
96     private static final int SHOW_IN_BROWSER_ID = IDialogConstants.CLIENT_ID + 1;
97
98     private static final String SHOW_IN_BROWSER_LABEL = Messages.SearchResourceDialog_ShowInBrowser;
99
100     private Session session;
101     @SuppressWarnings("unused")
102     private IStructuredSelection selection;
103     private ResourceManager resourceManager;
104     private IResourceFilter resourceFilter = ResourceSearch.FILTER_ALL;
105
106     LabelProvider detailsLabelProvider = new LabelProvider() {
107         @Override
108         public String getText(Object element) {
109             if (element == null)
110                 return "null"; //$NON-NLS-1$
111             // This may happen if multiple choice is enabled
112             if (element instanceof String)
113                 return (String) element;
114             @SuppressWarnings("unchecked")
115             Container<Resource> rc = (Container<Resource>) element;
116             final Resource r = rc.get();
117             try {
118                 return session.syncRequest(new Read<String>() {
119                     @Override
120                     public String perform(ReadGraph g) throws DatabaseException {
121                         String name = NameUtils.getSafeName(g, r);
122                         String uri = DebugUtils.getPossibleRootRelativePath(g, r);
123                         return
124                                 "[" + r.getResourceId() + "] - " //$NON-NLS-1$ //$NON-NLS-2$
125                                 + name
126                                 + (uri != null ? " - " : "") //$NON-NLS-1$ //$NON-NLS-2$
127                                 + (uri != null ? uri : ""); //$NON-NLS-1$
128                     }
129                 });
130             } catch (DatabaseException e) {
131                 Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, Messages.SearchResourceDialog_ActivatorResourceLabelProviderFailed, e));
132                 return ""; //$NON-NLS-1$
133             }
134         }
135     };
136
137     static class ElementLabelProvider extends ResourceLabelProvider {
138         public ElementLabelProvider(Display display) {
139             super(display);
140         }
141         @Override
142         public String getText(Object element) {
143             if (element==null)
144                 return "null"; //$NON-NLS-1$
145             return element.toString();
146         }
147         @Override
148         public Image getImage(Object element) {
149             if (element == null)
150                 return null;
151             @SuppressWarnings("unchecked")
152             Container<Resource> rc = (Container<Resource>) element;
153             final Resource r = rc.get();
154             return super.getImage(r);
155         }
156     };
157
158     ElementLabelProvider labelProvider;
159
160     public SearchResourceDialog(Session s, boolean multi, Shell shell, String title) {
161         this(s, multi, shell, title, null);
162     }
163
164     public SearchResourceDialog(Session s, boolean multi, Shell shell, String title, IStructuredSelection selection) {
165         super(shell, multi);
166         this.session = s;
167         this.selection = selection;
168         this.labelProvider = new ElementLabelProvider(shell.getDisplay());
169         setMessage(Messages.SearchResourceDialog_EnterNameResURIOrId);
170         setListLabelProvider(labelProvider);
171         setDetailsLabelProvider(detailsLabelProvider);
172         setTitle(title);
173         //setInitialPattern("*", FilteredItemsSelectionDialog.FULL_SELECTION);
174         setSelectionHistory(new ResourceSelectionHistory());
175         setSeparatorLabel(Messages.SearchResourceDialog_SeperatorLblPreviouslySelected);
176     }
177
178     @Override
179     protected void configureShell(Shell shell) {
180         this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), shell);
181         setImage((Image) resourceManager.get(BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/cog_blue.png"))); //$NON-NLS-1$
182         super.configureShell(shell);
183     }
184
185     @Override
186     protected void createButtonsForButtonBar(Composite parent) {
187         createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL,
188                 true);
189         createButton(parent, SHOW_IN_BROWSER_ID, SHOW_IN_BROWSER_LABEL,
190                 true);
191         createButton(parent, IDialogConstants.CANCEL_ID,
192                 IDialogConstants.CANCEL_LABEL, false);
193     }
194
195     @Override
196     protected void buttonPressed(int buttonId) {
197         if (buttonId == SHOW_IN_BROWSER_ID) {
198             okPressed();
199             LabeledResource lr = (LabeledResource) getFirstResult();
200             if (lr != null) {
201                 ShowInBrowser.defaultExecute(new StructuredSelection(new ResourceWorkbenchSelectionElement(lr.resource)));
202             }
203             return;
204         }
205         super.buttonPressed(buttonId);
206     }
207
208     class ResourceSelectionHistory extends FilteredItemsSelectionDialog.SelectionHistory {
209
210         @Override
211         protected Object restoreItemFromMemento(IMemento memento) {
212             String data = memento.getTextData();
213             try {
214                 SerialisationSupport support = Simantics.getSession().getService(SerialisationSupport.class);
215                 Resource r = support.getResource(Long.parseLong(data));
216                 if (r == null)
217                     return null;
218
219                 String name = session.syncRequest(new UniqueRead<String>() {
220                     @Override
221                     public String perform(ReadGraph g) throws DatabaseException {
222                         if (!resourceFilter.acceptResource(g, r))
223                             return null;
224                         try {
225                             try {
226                                 return DebugUtils.getSafeLabel(g, r);
227                             } catch (Exception ex) {
228                                 System.out.println("Exception thrown from restoreItemFromMemento"); //$NON-NLS-1$
229                             }
230                         } catch (Throwable t) {}
231                         return "" + r.getResourceId(); //$NON-NLS-1$
232                     }
233                 });
234                 if (name==null) return null;
235                 return new LabeledResource(name, r);
236             } catch (NumberFormatException | DatabaseException e) {
237                 LOGGER.info("Search memento restoration failed.", e); //$NON-NLS-1$
238                 return null;
239             }
240         }
241
242         @SuppressWarnings("unchecked")
243         @Override
244         protected void storeItemToMemento(Object item, IMemento memento) {
245             if(item instanceof Container) {
246                 try {
247                     SerialisationSupport support = Simantics.getSession().getService(SerialisationSupport.class);
248                     memento.putTextData(String.valueOf(support.getRandomAccessId(((Container<Resource>)item).get())));
249                 } catch (DatabaseException e) {
250                     e.printStackTrace();
251                 }
252             }
253         }
254     };
255
256     @Override
257     protected Control createExtendedContentArea(Composite parent) {
258         return null;
259     }
260
261     @Override
262     protected ItemsFilter createFilter() {
263         // NOTE: filter must be created here.
264         return new ItemsFilterWithSearchResults();
265     }
266
267     private class ItemsFilterWithSearchResults extends ItemsFilter {
268         private Set<Object> searchResults = new HashSet<Object>();
269
270         public ItemsFilterWithSearchResults() {
271             
272             final String pattern = getPattern();
273             final boolean findUris = pattern.trim().startsWith("http:/"); //$NON-NLS-1$
274             final long referencedResourceId = referencedResourceId(pattern);
275             final boolean findIds = referencedResourceId != 0;
276
277             searchResults.clear();
278             if (pattern.isEmpty()) return;
279             //progressMonitor.beginTask("Searching", IProgressMonitor.UNKNOWN);
280
281             try {
282                 session.syncRequest(new ReadRequest() {
283                     @Override
284                     public void run(ReadGraph graph) throws DatabaseException {
285                         // Find by ID first.
286                         if (findIds) {
287                             try {
288                                 Resource r = graph.getService(SerialisationSupport.class).getResource(referencedResourceId);
289                                 searchResults.add(new LabeledResource(DebugUtils.getSafeLabel(graph, r), r));
290                             } catch (DatabaseException e) {
291                                 // No resource for specified id.
292                             }
293                         }
294                         if (findUris) {
295                             String uri = pattern;
296                             if (uri.endsWith(Role.CHILD.getIdentifier())) {
297                                 uri = uri.substring(0, uri.length() - 1);
298                             }
299                             Resource r = graph.getPossibleResource(uri);
300                             if (r != null) {
301                                 searchResults.add(new LabeledResource(DebugUtils.getSafeURI(graph, r), r));
302
303                                 Map<String, Resource> children = graph.syncRequest(new UnescapedChildMapOfResource(r));
304                                 for (Resource child : children.values()) {
305                                         searchResults.add(new LabeledResource(DebugUtils.getSafeURI(graph, child), child));
306                                 }
307                             }
308                         } else {
309                             String[] terms = pattern.trim().split("\\s+"); //$NON-NLS-1$
310                             if (terms.length == 0) return;
311
312                             Resource project = Simantics.peekProjectResource();
313                             if (project != null) {
314                                 IResourceFilter rf = resourceFilter;
315                                 String filter = getFilterForResourceFilter(rf);
316                                 if (!filter.isEmpty())
317                                     filter += " AND "; //$NON-NLS-1$
318                                 
319                                 filter += Dependencies.FIELD_NAME_SEARCH + ":("; //$NON-NLS-1$
320                                 filter += Arrays.stream(terms).map(term -> "+" + IndexQueries.escape(term.toLowerCase(), false) + "*").collect(Collectors.joining(" ")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
321                                 filter +=")"; //$NON-NLS-1$
322
323                                 Layer0 L0 = Layer0.getInstance(graph);
324
325                                 Set<Resource> indexRoots = new HashSet<>();
326                                 indexRoots.addAll(graph.syncRequest(new ObjectsWithType(project, L0.ConsistsOf, L0.IndexRoot)));
327                                 indexRoots.addAll(graph.syncRequest(new OntologiesFromLibrary(graph.getRootLibrary())));
328                                 for (Resource indexRoot : indexRoots) {
329                                     Collection<Resource> hits = find(graph, indexRoot, filter);
330                                     for (Resource r : hits) {
331                                         if (rf != null && !rf.acceptResource(graph, r))
332                                             continue;
333                                         searchResults.add(new LabeledResource(DebugUtils.getSafeLabel(graph, r), r));
334                                     }
335                                 }
336                             }
337                         }
338                     }
339
340                     public Collection<Resource> find(ReadGraph graph, Resource index, String filter) throws DatabaseException {
341                         //TimeLogger.resetTimeAndLog("find(" + graph.getURI(index) + ", " + filter + ")");
342                         Collection<Resource> indexResult = graph.syncRequest(new QueryIndex(index, filter), TransientCacheListener.<Collection<Resource>>instance());
343                         //TimeLogger.log("found " + indexResult.size());
344                         return indexResult;
345                     }
346
347                 });
348             } catch (DatabaseException ex) {
349                 Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, ex.getMessage(), ex));
350             }
351
352         }
353
354         @Override
355         public boolean matchItem(Object item) {
356             return searchResults.contains(item);
357         }
358
359         @Override
360         public boolean isSubFilter(ItemsFilter filter) {
361             return false;
362         }
363         
364         @Override
365         public boolean isConsistentItem(Object item) {
366             return true;
367         }
368         
369         @Override
370         public boolean equalsFilter(ItemsFilter filter) {
371             return false;
372         }
373         
374         public void fillContentProvider(final AbstractContentProvider contentProvider) {
375                 for (Object item : searchResults) {
376                         contentProvider.add(item, this);
377                 }
378         }
379     }
380     
381     @Override
382     protected void fillContentProvider(final AbstractContentProvider contentProvider,
383             final ItemsFilter itemsFilter, final IProgressMonitor progressMonitor)
384                     throws CoreException {
385         ((ItemsFilterWithSearchResults) itemsFilter).fillContentProvider(contentProvider);
386         progressMonitor.done();
387     }
388
389     /**
390      * A (cacheable) query to optimize single index queries for immutable
391      * indexes such as ontologies.
392      */
393     static class QueryIndex extends BinaryRead<Resource, String, Collection<Resource>> {
394
395         public QueryIndex(Resource index, String filter) {
396             super(index, filter);
397         }
398
399         @Override
400         public Collection<Resource> perform(ReadGraph graph)
401                 throws DatabaseException {
402             Layer0X L0X = Layer0X.getInstance(graph);
403
404             @SuppressWarnings({ "unchecked", "rawtypes" })
405             Function dependencies = graph.syncRequest(new PossibleAdapter(L0X.DependencyResources, Function.class), TransientCacheListener.<Function>instance());
406             if (dependencies == null)
407                 return Collections.emptyList();
408
409             @SuppressWarnings("unchecked")
410             List<Resource> results = (List<Resource>) dependencies.apply(graph, parameter, parameter2, DEFAULT_MAX_INDEX_HITS);
411             if (results == null || results.isEmpty())
412                 return Collections.emptyList();
413
414             // TreeSet to keep the results in deterministic order and to prevent duplicates.
415             Set<Resource> resultSet = new TreeSet<>();
416             for (Resource res : results) {
417                 if (res != null && !resultSet.contains(res))
418                     resultSet.add(res);
419             }
420             return new ArrayList<Resource>(resultSet);
421         }
422
423     }
424
425     private long referencedResourceId(String pattern) {
426         Matcher m = ID_PATTERN.matcher(pattern);
427         if (m.matches()) {
428            String id = m.group(1);
429            try {
430                return Long.parseLong(id);
431            } catch (NumberFormatException nfe) {
432            }
433         }
434         return 0;
435     }
436
437     @Override
438     protected IDialogSettings getDialogSettings() {
439         IDialogSettings settings = Activator.getDefault().getDialogSettings()
440         .getSection(SEARCH_RESOURCE_DIALOG);
441         if (settings == null) {
442             settings = Activator.getDefault().getDialogSettings()
443             .addNewSection(SEARCH_RESOURCE_DIALOG);
444         }
445         return settings;
446     }
447
448     @SuppressWarnings("unchecked")
449     @Override
450     public String getElementName(Object item) {
451         return ((Container<Resource>)item).get().getResourceId()+""; //$NON-NLS-1$
452         //return item.toString();
453     }
454
455     @Override
456     protected Comparator<?> getItemsComparator() {
457         return (arg0, arg1) -> {
458             return arg0.toString().compareTo(arg1.toString());
459         };
460     }
461
462     @Override
463     protected IStatus validateItem(Object item) {
464         return Status.OK_STATUS;
465     }
466
467     public IResourceFilter getResourceFilter() {
468         return resourceFilter;
469     }
470
471     public void setResourceFilter(IResourceFilter resourceFilter) {
472         this.resourceFilter = resourceFilter;
473     }
474
475     private String getFilterForResourceFilter(IResourceFilter filter) {
476         if (filter == null || filter == ResourceSearch.FILTER_ALL)
477             return ""; //$NON-NLS-1$
478         if (filter == ResourceSearch.FILTER_RELATIONS)
479             return "Types:Relation";  //$NON-NLS-1$
480         if (filter == ResourceSearch.FILTER_TYPES)
481             return "Types:Type";  //$NON-NLS-1$
482         return ""; //$NON-NLS-1$
483     }
484
485 }