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; } }