X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;ds=sidebyside;f=bundles%2Forg.simantics.workbench.search.impl%2Fsrc%2Forg%2Fsimantics%2Fworkbench%2Fsearch%2Fimpl%2FSearchServiceImpl.java;fp=bundles%2Forg.simantics.workbench.search.impl%2Fsrc%2Forg%2Fsimantics%2Fworkbench%2Fsearch%2Fimpl%2FSearchServiceImpl.java;h=19c574547d9a5db7658d049878e61cfc90d3ac43;hb=38d133f2a39bab76deed5d047fbabee4479b5373;hp=0000000000000000000000000000000000000000;hpb=a9e5abf29200550168557ae2c7e0a6e2442f6c2b;p=simantics%2Fplatform.git diff --git a/bundles/org.simantics.workbench.search.impl/src/org/simantics/workbench/search/impl/SearchServiceImpl.java b/bundles/org.simantics.workbench.search.impl/src/org/simantics/workbench/search/impl/SearchServiceImpl.java new file mode 100644 index 000000000..19c574547 --- /dev/null +++ b/bundles/org.simantics.workbench.search.impl/src/org/simantics/workbench/search/impl/SearchServiceImpl.java @@ -0,0 +1,670 @@ +package org.simantics.workbench.search.impl; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.swt.SWT; +import org.eclipse.swt.browser.LocationEvent; +import org.eclipse.swt.browser.LocationListener; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IViewPart; +import org.eclipse.ui.IViewReference; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.simantics.ObjectIdentitySchedulingRule; +import org.simantics.Simantics; +import org.simantics.databoard.Bindings; +import org.simantics.db.ReadGraph; +import org.simantics.db.Resource; +import org.simantics.db.Session; +import org.simantics.db.exception.DatabaseException; +import org.simantics.db.layer0.adapter.Instances; +import org.simantics.db.layer0.genericrelation.Dependencies; +import org.simantics.db.layer0.request.ActiveModels; +import org.simantics.db.request.Read; +import org.simantics.db.service.SerialisationSupport; +import org.simantics.editors.Browser; +import org.simantics.editors.BrowserInput; +import org.simantics.layer0.Layer0; +import org.simantics.scl.runtime.function.Function; +import org.simantics.scl.runtime.function.Function5; +import org.simantics.ui.workbench.action.ChooseActionRequest; +import org.simantics.utils.FileUtils; +import org.simantics.utils.datastructures.MapList; +import org.simantics.utils.datastructures.Pair; +import org.simantics.utils.ui.ErrorLogger; +import org.simantics.utils.ui.ExceptionUtils; +import org.simantics.utils.ui.workbench.WorkbenchUtils; +import org.simantics.workbench.ontology.WorkbenchResource; +import org.simantics.workbench.search.ISearchService; +import org.simantics.workbench.search.QueryResult; +import org.simantics.workbench.search.SearchEngine; +import org.simantics.workbench.search.SearchQuery; +import org.simantics.workbench.search.SearchResult; +import org.simantics.workbench.search.Searching; + +import freemarker.template.TemplateException; + +/** + * @author Tuukka Lehtonen + * @author Marko Luukkainen + */ +public class SearchServiceImpl implements ISearchService{ + + private static final String SEARCH_TEMP_FILE_SUFFIX = "search.html"; + private static final String SEARCH_TEMP_DIR = "search"; + private static final String BROWSER_VIEW = BrowserView.ID; + private static final Integer MAX_RESULTS = 1000; + + @Override + public void performQuery(SearchQuery query, ResultBrowser browserType, boolean activateResultBrowser) { + try { + Set searchEngines = getSearchEngines(); + + // if query does not define used engines, use all available engines. + boolean containsEngines = false; + for (SearchEngine e : searchEngines) { + if (query.getSearchFlags().containsKey(e.getId())) { + containsEngines = true; + break; + } + } + if (!containsEngines) { + for (SearchEngine e : searchEngines) + if (e.isEnabledByDefault()) + query.setSearchFlag(e.getId(), "on"); + } + } catch (DatabaseException e) { + ExceptionUtils.logError(e); + } + switch (browserType) { + case EDITOR: + performEditorQuery(query); + break; + case VIEW: + performViewQuery(query, activateResultBrowser); + break; + } + + } + + private void performViewQuery(SearchQuery query, boolean activateResultBrowser) { + try { + browserView = (BrowserView) showLocalView( + BROWSER_VIEW, + activateResultBrowser ? IWorkbenchPage.VIEW_ACTIVATE : IWorkbenchPage.VIEW_CREATE); + if (browserView.getBrowser() != browserViewBrowser) { + browserViewBrowser = browserView.getBrowser(); + browserViewTemporaryFiles = new ArrayList(); + browserViewTemporaryFiles.add(getNewTemporaryFile()); + browserView.getBrowser().addLocationListener(browserViewLocationListener); + browserView.getBrowser().addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + browserViewBrowser = null; + } + }); + } + browserView.setPartProperty(BrowserView.LAST_QUERY_PROPERTY, query.getOriginalQuery()); + updateViewQuery(browserView, browserViewTemporaryFiles, query); + } catch (IOException e1) { + ErrorLogger.defaultLogError(e1); + } catch (PartInitException e1) { + ErrorLogger.defaultLogError(e1); + } + } + + private IViewPart showLocalView(String id, int mode) throws PartInitException { + final IWorkbenchWindow wb = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (wb == null) + return null; + + // Is view already created in the currently active workbench window? + IViewPart vp = findViewFromWindow(wb, id, false); + if (vp != null) { + if (mode == IWorkbenchPage.VIEW_CREATE) + return vp; + IWorkbenchPage page = vp.getViewSite().getPage(); + if (mode == IWorkbenchPage.VIEW_VISIBLE) { + page.bringToTop(vp); + } else if (mode == IWorkbenchPage.VIEW_ACTIVATE) { + page.activate(vp); + } + return vp; + } + + // Create the view on the active window's active page + IWorkbenchPage page = wb.getActivePage(); + if (page == null) { + IWorkbenchPage pages[] = wb.getPages(); + if (pages.length == 0) return null; + page = pages[0]; + } + return page.showView(id, null, mode); + } + + private static IViewPart findViewFromWindow(IWorkbenchWindow wb, String viewId, boolean restore) { + for (IWorkbenchPage page : wb.getPages()) { + IViewReference vr = page.findViewReference(viewId); + if (vr != null) { + IViewPart vp = vr.getView(restore); + if (vp != null) + return vp; + } + } + return null; + } + + /** + * There's always only one of these around. + */ + BrowserView browserView = null; + + org.eclipse.swt.browser.Browser browserViewBrowser = null; + + List browserViewTemporaryFiles = null; + + LocationListener browserViewLocationListener = new LocationListener() { + @Override + public void changing(LocationEvent event) { + //System.out.println("changing: " + event); + try { + URI newUri = new URI(event.location); + + // Handle resource links by opening an editor for them + if ("resource".equals(newUri.getScheme())) { + event.doit = false; + openResource(getShell(event), newUri.getSchemeSpecificPart()); + return; + } + } catch (URISyntaxException e) { + // This URI was not needed anyway, resource: URIs will always parse properly so let + // it go without logging. + } catch (DatabaseException e) { + ErrorLogger.defaultLogError(e); + } + } + + @Override + public void changed(LocationEvent event) { + // System.out.println("changed: " + event); + try { + URL newUrl = new URL(event.location); + String query = newUrl.getQuery(); + if (query != null) { + SearchQuery searchQuery = SearchQuery.decode(newUrl); + if (searchQuery.getOriginalQuery() != null) { + event.doit = updateViewQuery( + browserView, + browserViewTemporaryFiles, + searchQuery); + + } + } + } catch (IOException e) { + ErrorLogger.defaultLogError(e); + } + } + }; + + class ViewQueryJob extends Job { + + SearchQuery query; + + List temporaryFiles; + + BrowserView browserView; + + Display display; + + public ViewQueryJob(SearchQuery query, List temporaryFiles, BrowserView browserView) { + super("Search..."); + this.query = query; + this.temporaryFiles = temporaryFiles; + this.browserView = browserView; + this.display = browserView.getBrowser().getDisplay(); + } + + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + SubMonitor mon = SubMonitor.convert(monitor); + monitor.beginTask("Performing search '" + query + "'", 10); + monitor.subTask("Querying index"); + final List result = createResultPage(mon.newChild(8), query, MAX_RESULTS); + if (result == null) { + return Status.CANCEL_STATUS; + } + monitor.worked(8); + monitor.subTask("Generating results"); + updatePages(result, temporaryFiles); + monitor.worked(2); + updateBrowser(browserView, result.get(0)); + return Status.OK_STATUS; + } catch (DatabaseException e1) { + return new Status(IStatus.ERROR, Activator.PLUGIN_ID, + "Unexpected database problems during search result processing, see exception.", e1); + } catch (IOException e1) { + return new Status(IStatus.ERROR, Activator.PLUGIN_ID, + "Unexpected I/O problems during search result processing, see exception.", e1); + } finally { + monitor.done(); + } + } + + void updateBrowser(final BrowserView browserView, final QueryResult result) { + if (display.isDisposed()) + return; + display.asyncExec(new Runnable() { + @Override + public void run() { + if (browserView.isDisposed()) + return; + + try { + long t1 = System.currentTimeMillis(); + browserView.setUrl(temporaryFiles.get(0).toURI().toURL()); + //browserView.setContentDescription("'" + query + "' - " + result.getHitCount() + " matches in active model"); + long t2 = System.currentTimeMillis(); + System.out.println("Updating UI " + (t2-t1) + " ms"); + } catch (MalformedURLException e) { + ErrorLogger.defaultLogError(e); + } + } + }); + } + } + + protected boolean updateViewQuery(BrowserView browserView, List temporaryFiles, SearchQuery query) { + ViewQueryJob job = new ViewQueryJob(query, temporaryFiles, browserView); + job.setRule(new ObjectIdentitySchedulingRule(browserView)); + job.schedule(); + return true; + } + + protected Shell getShell(LocationEvent event) { + if (event.widget instanceof Control) { + Control c = (Control) event.widget; + if (c.isDisposed()) + return null; + return c.getShell(); + } + return null; + } + + /** + * @param query + */ + protected void performEditorQuery(final SearchQuery query) { + try { + final List temporaryFiles = new ArrayList(); + temporaryFiles.add(getNewTemporaryFile()); + final BrowserInput input = createBrowserInput(temporaryFiles.get(0), query); + updateInput(input, temporaryFiles.get(0), query); + + final Browser browser = (Browser) WorkbenchUtils.openEditor("org.simantics.editors.browser", input); + + browser.getBrowser().addLocationListener(new LocationListener() { + @Override + public void changing(LocationEvent event) { + // System.out.println("changing: " + event); + try { + // TODO : is there a better way to escape location? + URI newUri = new URI(event.location.replaceAll(" ", "%20")); + // Handle resource links by opening an editor for them + if ("resource".equals(newUri.getScheme())) { + event.doit = false; + openResource(getShell(event), newUri.getSchemeSpecificPart()); + return; + } + } catch (URISyntaxException e) { + ErrorLogger.defaultLogError(e); + } catch (DatabaseException e) { + ErrorLogger.defaultLogError(e); + } + } + + @Override + public void changed(LocationEvent event) { + if (query != null) { + // OpenEditor does not seem to pass any parameters (anything after '?' character) + SearchQuery searchQuery = query; + if (searchQuery.getOriginalQuery() != null) { + event.doit = updateQuery(browser, input, temporaryFiles, searchQuery); + } + } + } + }); + } catch (IOException e1) { + ErrorLogger.defaultLogError(e1); + } catch (PartInitException e1) { + ErrorLogger.defaultLogError(e1); + } + } + + protected void openResource(Shell shell, String link) throws DatabaseException { + try { + Session session = Simantics.getSession(); + final long id = Long.parseLong(link); + + Resource resource = session.syncRequest(new Read() { + @Override + public Resource perform(ReadGraph graph) throws DatabaseException { + SerialisationSupport ss = graph.getService(SerialisationSupport.class); + return ss.getResource(id); + } + }); + + ISelection input = new StructuredSelection(resource); + String perspectiveId = WorkbenchUtils.getCurrentPerspectiveId(); + + // Try the doubleClick-extensions + session.asyncRequest(new ChooseActionRequest(shell, input, perspectiveId, false, true)); + } catch (NumberFormatException e) { + return; + } + } + + /** + * @param query + */ + protected static boolean updateQuery(Browser browser, BrowserInput input, List temporaryFiles, final SearchQuery query) { + try { + String url = browser.getBrowser().getUrl(); + String inputUrl = input.getUrl().toString(); + if (url.equals(inputUrl)) + return false; + + List result = createResultPage(new NullProgressMonitor(), query, MAX_RESULTS); + updateInput(input, temporaryFiles.get(0), query); + updatePages(result,temporaryFiles); + // browser.getBrowser().setUrl(inputUrl); + browser.getBrowser().refresh(); + // temporaryFile.delete(); + for (File temporaryFile : temporaryFiles) + temporaryFile.deleteOnExit(); + return true; + } catch (DatabaseException e1) { + ErrorLogger.defaultLogError(e1); + } catch (IOException e1) { + ErrorLogger.defaultLogError(e1); + } + return false; + } + + private static File ensureTemporaryDirectoryExists() throws IOException { + return Simantics.getTemporaryDirectory(SEARCH_TEMP_DIR); + } + + /** + * @param html + * @return + * @throws IOException + */ + private static File getNewTemporaryFile() throws IOException { + return Simantics.getTempfile(SEARCH_TEMP_DIR, SEARCH_TEMP_FILE_SUFFIX); + } + + /** + * @param query + * @param html + * @return + * @throws IOException + */ + private static BrowserInput createBrowserInput(File file, SearchQuery query) throws IOException { + // new BrowserInput(file.toURI().toURL() + return new BrowserInput(SearchQuery.encode(file, query), query.getOriginalQuery(), false, false, SWT.NONE); + } + + private static void updatePages(List result, List temporaryFiles) throws IOException{ + ensureTemporaryDirectoryExists(); + for (int i = temporaryFiles.size(); i < result.size(); i++) { + temporaryFiles.add(getNewTemporaryFile()); + } + if (result.size() > 1) { + createPageControls(result,temporaryFiles); + } + if (result.size() > 0) { + for (int i = 0; i < result.size(); i++) { + updatePage(temporaryFiles.get(i), result.get(i).getHtml()); + } + } else { + updatePage(temporaryFiles.get(0), ""); + } + } + + /** + * @param html + * @return + * @throws IOException + */ + private static void updatePage(File file, String html) throws IOException { + long t1 = System.currentTimeMillis(); + FileUtils.writeFile(file, html.getBytes("UTF-8")); + long t2 = System.currentTimeMillis(); + System.out.println("Writing html page took " + (t2-t1) + " ms"); + } + + private static void createPageControls(List result, List temporaryFiles) throws IOException{ + if (result.size() == 1) + return; + for (int i = 0; i < result.size(); i++) { + boolean first = i == 0; + boolean last = i == result.size() -1; + QueryResult current = result.get(i); + String pageContrtol = "
"; + pageContrtol += "Page " + (i+1) + " of "+ + result.size() + " "; + if (first) { + pageContrtol += "First"; + pageContrtol += " Previous"; + pageContrtol += " Next"; + pageContrtol += " Last"; + } else if (last) { + pageContrtol += "First"; + pageContrtol += " Previous"; + pageContrtol += " Next"; + pageContrtol += " Last"; + } else { + pageContrtol += "First"; + pageContrtol += " Previous"; + pageContrtol += " Next"; + pageContrtol += " Last"; + } + pageContrtol += "

"; + + result.set(i, new QueryResult(current.getHeader(), pageContrtol+current.getContent()+pageContrtol,current.getFooter(),current.getHitCount())); + } + + } + + + + /** + * @param html + * @return + * @throws IOException + */ + private static void updateInput(BrowserInput input, File file, SearchQuery query) throws IOException { + URL url = SearchQuery.encode(file, query); + input.setUrl(url); + input.setName(query.getOriginalQuery()); + // System.out.println("update input: " + url + " - " + query + " - " + input); + } + + private static Set getSearchEngines() throws DatabaseException{ + return Simantics.getSession().syncRequest(new Read>() { + @Override + public Set perform(ReadGraph graph) + throws DatabaseException { + Resource project = Simantics.peekProjectResource(); + if (project == null) + return Collections.emptySet(); + return getSearchEngines(graph, project); + } + }); + } + + private static Set getSearchEngines(ReadGraph graph, Resource project) throws DatabaseException{ + Set searchEngines = new HashSet(); + Map> result = getSearchEnginesByModel(graph, project); + for (Set set : result.values()) + searchEngines.addAll(set); + return searchEngines; + } + + + private static Map> getSearchEnginesByModel(ReadGraph graph, Resource project) throws DatabaseException{ + WorkbenchResource WB = WorkbenchResource.getInstance(graph); + Layer0 L0 = Layer0.getInstance(graph); + Map> result = new HashMap>(); + Collection activeModels = graph.syncRequest(new ActiveModels(project)); + for (Resource model : activeModels) { + Set searchEngines = new HashSet(); + + List searchFunctions = new ArrayList(); + searchFunctions.addAll(graph.getObjects(model, WB.HasWorkbenchSearchFunction)); + if (searchFunctions.size() == 0) { + searchEngines.addAll(findSearchContributions(graph, model)); + } + if (searchFunctions.isEmpty() && searchEngines.isEmpty()) { + // TODO : backwards compatibility, to be removed. + searchFunctions.addAll(graph.getObjects(project, WB.HasWorkbenchSearchFunction)); + } + if (searchFunctions.isEmpty() && searchEngines.isEmpty()) + searchFunctions.add( WB.DependenciesSearchFunction); + + for (Resource searchFunction : searchFunctions) { + @SuppressWarnings("rawtypes") + Function f = graph.adapt(searchFunction, Function.class); + String id = graph.getURI(searchFunction); + String name = graph.getPossibleRelatedValue2(searchFunction, L0.HasLabel); + @SuppressWarnings("unchecked") + SearchEngine engine = new SearchEngine(id,name,(Function5)f, true); + { + engine.addSupportedParam(Dependencies.FIELD_NAME_SEARCH, "Name"); + engine.addSupportedParam(Dependencies.FIELD_TYPES_SEARCH, "Types"); + } + searchEngines.add(engine); + } + result.put(model, searchEngines); + } + return result; + } + + + /** + * @param subMonitor + * @param query + * @param maxResults + * @return + * @throws DatabaseException + */ + private static List createResultPage(final IProgressMonitor monitor, final SearchQuery query, final int maxResults) throws DatabaseException { + return Simantics.getSession().syncRequest(new Read>() { + @Override + public List perform(ReadGraph graph) throws DatabaseException { + Resource project = Simantics.peekProjectResource(); + if (project == null) + return null; + + Collection activeModels = graph.syncRequest(new ActiveModels(project)); + //if (activeModels.isEmpty()) + // return null; + long t1 = System.currentTimeMillis(); + Set searchEngines = new HashSet(); + MapList> searchResults = new MapList>(); + Map> searchEngineMap = getSearchEnginesByModel(graph, project); + + for (Resource model : activeModels) { + for (SearchEngine engine : searchEngineMap.get(model)) { + + if (query.getBooleanFlag(engine.getId())) { + SearchResult result = engine.getSearchFunction().apply(monitor, graph, model, query, maxResults); + if (!result.isEmpty()) { + searchResults.add(model, new Pair(engine, result)); + } + } + searchEngines.add(engine); + } +// searchResults.put(model, result); + } + long t2 = System.currentTimeMillis(); + System.out.println("Running search queries took " + (t2-t1) + " ms for query '" + query + "'"); + + try { + return Searching.generatePage(graph, searchEngines,query, maxResults, searchResults); + } catch (IOException e) { + Activator.getDefault().getLog().error("I/O problem while generating search result page.", e); + } catch (TemplateException e) { + Activator.getDefault().getLog().error("Template definition problem in search result page generation. Please inform the developers.", e); + } + return null; + } + }); + } + + private static Collection findSearchContributions(ReadGraph graph, Resource model) throws DatabaseException { + WorkbenchResource WB = WorkbenchResource.getInstance(graph); + Instances contributionFinder = graph.adapt(WB.SearchContribution, Instances.class); + Resource index = model; + List result = new ArrayList(); + for (Resource r : contributionFinder.find(graph, index)) { + SearchEngine engine = contributionToEngine(graph, r); + if (engine != null) + result.add(engine); + } + return result; + } + + private static SearchEngine contributionToEngine(ReadGraph graph, Resource searchContribution) throws DatabaseException { + Layer0 L0 = Layer0.getInstance(graph); + WorkbenchResource WB = WorkbenchResource.getInstance(graph); + + Resource searchFunction = graph.getPossibleObject(searchContribution, WB.hasSearchFunction); + if (searchFunction == null) + return null; + + @SuppressWarnings("rawtypes") + Function f = graph.adapt(searchFunction, Function.class); + String id = graph.getURI(searchFunction); + String name = graph.getPossibleRelatedValue2(searchFunction, L0.HasLabel); + + Boolean enabledByDefault = graph.getPossibleRelatedValue2(searchContribution, WB.SearchContribution_isEnabledByDefault, Bindings.BOOLEAN); + boolean enabled = !Boolean.FALSE.equals(enabledByDefault); + + @SuppressWarnings("unchecked") + SearchEngine engine = new SearchEngine(id,name,(Function5)f, enabled); + { + engine.addSupportedParam(Dependencies.FIELD_NAME_SEARCH, "Name"); + engine.addSupportedParam(Dependencies.FIELD_TYPES_SEARCH, "Types"); + } + return engine; + } + +}