/******************************************************************************* * Copyright (c) 2014, 2016 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.orion.internal.server.search; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.core.filesystem.IFileStore; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.orion.internal.server.servlets.Activator; import org.eclipse.orion.server.core.OrionConfiguration; import org.eclipse.orion.server.core.metastore.ProjectInfo; import org.eclipse.orion.server.core.metastore.UserInfo; import org.eclipse.orion.server.core.metastore.WorkspaceInfo; import org.eclipse.orion.server.core.users.UserConstants; import org.eclipse.orion.server.servlets.OrionServlet; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Servlet for performing searches against files in the workspace. * * @author Aidan Redpath * @author Anthony Hunter */ public class SearchServlet extends OrionServlet { private static final String FIELD_NAMES = "Name,NameLower,Length,Directory,LastModified,Location,Path,RegEx,CaseSensitive,WholeWord,Exclude"; //$NON-NLS-1$ private static final List<String> FIELD_LIST = Arrays.asList(FIELD_NAMES.split(",")); //$NON-NLS-1$ private static final long serialVersionUID = 1L; private Logger logger = LoggerFactory.getLogger("org.eclipse.orion.server.config"); //$NON-NLS-1$ private void addAllProjectsToScope(WorkspaceInfo workspaceInfo, SearchOptions options) throws CoreException { List<String> projectnames = workspaceInfo.getProjectNames(); for (String projectName : projectnames) { ProjectInfo projectInfo = OrionConfiguration.getMetaStore().readProject(workspaceInfo.getUniqueId(), projectName); if (projectInfo == null) { logger.error("Unexpected missing project with name " + projectName + " in workspace " + workspaceInfo.getUniqueId()); continue; } SearchScope scope = new SearchScope(projectInfo.getProjectStore(), workspaceInfo, projectInfo); options.getScopes().add(scope); } } private SearchOptions buildSearchOptions(HttpServletRequest req, HttpServletResponse resp) throws SearchException, ServletException { SearchOptions options = new SearchOptions(); String queryString = getEncodedParameter(req, "q"); if (queryString == null) return null; if (queryString.length() > 0) { //divide into search terms delimited by space or plus ('+') character List<String> terms = new ArrayList<String>(Arrays.asList(queryString.split("[\\s\\+]+"))); //$NON-NLS-1$ while (!terms.isEmpty()) { String term = terms.remove(0); if (term.length() == 0) continue; if (isSearchField(term)) { if (term.startsWith("NameLower:")) { //$NON-NLS-1$ //decode the search term, we do not want to decode the location try { term = URLDecoder.decode(term, "UTF-8"); //$NON-NLS-1$ } catch (UnsupportedEncodingException e) { //try with encoded term } options.setIsFilenamePatternCaseSensitive(false); options.setFilenamePattern(term.substring(10)); } else if (term.startsWith("Location:")) { //$NON-NLS-1${ int ctxLength = req.getContextPath().length(); if((ctxLength + 9) > term.length()){ handleException(resp, "Invalid search term: " + term, null, HttpServletResponse.SC_BAD_REQUEST); return null; } String location = term.substring(9 + ctxLength); try { location = URLDecoder.decode(location, "UTF-8"); //$NON-NLS-1$ } catch (UnsupportedEncodingException e) { //try with encoded term } options.setLocation(location); continue; } else if (term.startsWith("Name:")) { //$NON-NLS-1$ try { term = URLDecoder.decode(term, "UTF-8"); //$NON-NLS-1$ } catch (UnsupportedEncodingException e) { //try with encoded term } options.setIsFilenamePatternCaseSensitive(true); options.setFilenamePattern(term.substring(5)); } else if (term.startsWith("RegEx:")) { options.setRegEx(true); } else if (term.startsWith("CaseSensitive:")) { options.setIsSearchTermCaseSensitive(true); } else if (term.startsWith("WholeWord:")) { options.setIsSearchWholeWord(true); } else if(term.startsWith("Exclude:")) { String exclude = term.substring("Exclude:".length()); String[] items = exclude.split(","); for(String item : items) { try { options.setExcluded(URLDecoder.decode(item, "UTF-8")); } catch(UnsupportedEncodingException usee) { //ignore, bad term } } } } else if(term.indexOf(":") > -1) { //unknown search term, ignore continue; } else { //decode the term string now try { term = URLDecoder.decode(term, "UTF-8"); //$NON-NLS-1$ } catch (UnsupportedEncodingException e) { //try with encoded term } options.setSearchTerm(term); options.setFileSearch(true); } } } String login = req.getRemoteUser(); options.setUsername(login); setScopes(req, resp, options); return options; } /** * Convert the list of file results to JSON format for return to the client. * @param contextPath the context path of the server that is added to the location for each result. * @param files The list of files matching the search query. * @param options The search options. * @return the file results in JSON format. */ private JSONObject convertListToJson(String contextPath, List<SearchResult> files, SearchOptions options) { JSONObject resultsJSON = new JSONObject(); JSONObject responseJSON = new JSONObject(); try { resultsJSON.put("start", 0); JSONArray docs = new JSONArray(); int found = 0; if(files != null) { found = files.size(); for (SearchResult file : files) { docs.put(file.toJSON(contextPath)); } } resultsJSON.put("numFound", found); resultsJSON.put("docs", docs); // Add to parent JSON JSONObject responseHeader = new JSONObject(); responseHeader.put("status", 0); //responseHeader.put("QTime", 77); JSONObject params = new JSONObject(); params.put("wt", "json"); params.put("fl", FIELD_NAMES); JSONArray fq = new JSONArray(); if (options.getDefaultLocation() != null) { fq.put("Location:" + options.getDefaultLocation()); } else if (options.getLocation() != null) { fq.put("Location:" + options.getLocation()); } else { throw new RuntimeException("Scope or DefaultScope is missing"); } if (options.getUsername() != null) { fq.put("UserName:" + options.getUsername()); } else { throw new RuntimeException("UserName is missing"); } params.put("fq", fq); params.put("rows", "10000"); params.put("start", "0"); params.put("sort", "Path asc"); responseHeader.put("params", params); responseJSON.put("responseHeader", responseHeader); responseJSON.put("response", resultsJSON); } catch (JSONException e) { logger.error("SearchServlet.convertListToJson: " + e.getLocalizedMessage(), e); } catch (CoreException e) { logger.error("SearchServlet.convertListToJson: " + e.getLocalizedMessage(), e); } catch (URISyntaxException e) { logger.error("SearchServlet.convertListToJson: " + e.getLocalizedMessage(), e); } return responseJSON; } public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { if (SearchJob.isSearchJobRunning(req.getRemoteUser())) { resp.sendError(HttpServletResponse.SC_CONFLICT, "A search task is already running for " + req.getRemoteUser() + ", try again later."); return; } SearchOptions options = buildSearchOptions(req, resp); if(options == null) { return; } SearchJob searchJob = new SearchJob(options); searchJob.schedule(); searchJob.join(); if (!searchJob.getResult().isOK()) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, searchJob.getResult().getMessage()); } List<SearchResult> files = searchJob.getSearchResults(); writeResponse(req, resp, files, options); } catch (SearchException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); } catch (InterruptedException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); } } /** * Returns a request parameter in encoded form. Returns <code>null</code> * if no such parameter is defined or has an empty value. */ private String getEncodedParameter(HttpServletRequest req, String key) { String query = req.getQueryString(); for (String param : query.split("&")) { //$NON-NLS-1$ String[] pair = param.split("=", 2); //$NON-NLS-1$ if (pair.length == 2 && key.equals(pair[0])) return pair[1]; } return null; } /** * Returns whether the search term is against a particular field rather than the default field * (search on name, location, etc). */ private boolean isSearchField(String term) { for (String field : FIELD_LIST) { if (term.startsWith(field + ":")) //$NON-NLS-1$ return true; } return false; } /** * Sets the default scopes to the location of each project. * @param req The request from the servlet. * @param res The response to the servlet. * @throws SearchException Thrown if there is an error reading a file. */ private void setDefaultScopes(HttpServletRequest req, HttpServletResponse resp, SearchOptions options) throws SearchException { String login = req.getRemoteUser(); try { UserInfo userInfo = OrionConfiguration.getMetaStore().readUserByProperty(UserConstants.USER_NAME, login, false, false); List<String> workspaceIds = userInfo.getWorkspaceIds(); for (String workspaceId : workspaceIds) { WorkspaceInfo workspaceInfo = OrionConfiguration.getMetaStore().readWorkspace(workspaceId); options.setDefaultLocation("/file/" + workspaceId); addAllProjectsToScope(workspaceInfo, options); } } catch (CoreException e) { throw (new SearchException(e)); } } private boolean setScopeFromRequest(HttpServletRequest req, HttpServletResponse resp, SearchOptions options) { try { String pathInfo = options.getLocation(); if (pathInfo != null && (pathInfo.startsWith(Activator.LOCATION_FILE_SERVLET))) { pathInfo = pathInfo.substring(Activator.LOCATION_FILE_SERVLET.length()); } else if (pathInfo != null && (pathInfo.startsWith(Activator.LOCATION_WORKSPACE_SERVLET))) { pathInfo = pathInfo.substring(Activator.LOCATION_WORKSPACE_SERVLET.length()); } if (pathInfo != null && pathInfo.endsWith("*")) { pathInfo = pathInfo.substring(0, pathInfo.length() - 1); } IPath path = pathInfo == null ? Path.ROOT : new Path(pathInfo); // prevent path canonicalization hacks if (pathInfo != null && !pathInfo.equals(path.toString())) { return false; } // don't allow anyone to mess with metadata if (path.segmentCount() > 0 && ".metadata".equals(path.segment(0))) { //$NON-NLS-1$ return false; } // Must have a path if (path.segmentCount() == 0) { return false; } WorkspaceInfo workspaceInfo = OrionConfiguration.getMetaStore().readWorkspace(path.segment(0)); if (workspaceInfo == null || workspaceInfo.getUniqueId() == null) { return false; } if (path.segmentCount() == 1) { // Bug 415700: handle path format /workspaceId if (workspaceInfo != null && workspaceInfo.getUniqueId() != null) { addAllProjectsToScope(workspaceInfo, options); return true; } return false; } //path format is /workspaceId/projectName/[suffix] ProjectInfo projectInfo = OrionConfiguration.getMetaStore().readProject(workspaceInfo.getUniqueId(), path.segment(1)); if (projectInfo != null) { IFileStore projectStore = projectInfo.getProjectStore(); IFileStore scopeStore = projectStore.getFileStore(path.removeFirstSegments(2)); SearchScope scope = new SearchScope(scopeStore, workspaceInfo, projectInfo); options.getScopes().add(scope); return true; } // Bug 415700: handle path format /workspaceId/[file] if (path.segmentCount() == 2) { IFileStore workspaceStore = OrionConfiguration.getMetaStore().getWorkspaceContentLocation(workspaceInfo.getUniqueId()); IFileStore scopeStore = workspaceStore.getChild(path.segment(1)); SearchScope scope = new SearchScope(scopeStore, workspaceInfo, null); options.getScopes().add(scope); return true; } return false; } catch (CoreException e) { logger.error("FileGrepper.setScopeFromRequest: " + e.getLocalizedMessage(), e); return false; } } /** * Set the scope of the search to the user home if the scope was not given. * @param req The HTTP request to the servlet. * @param resp The HTTP response from the servlet. * @throws SearchException */ private void setScopes(HttpServletRequest req, HttpServletResponse resp, SearchOptions options) throws SearchException { if (!setScopeFromRequest(req, resp, options)) { setDefaultScopes(req, resp, options); } } private void writeResponse(HttpServletRequest req, HttpServletResponse resp, List<SearchResult> files, SearchOptions options) throws IOException { try { JSONObject json = convertListToJson(req.getContextPath(), files, options); writeJSONResponse(req, resp, json); } catch (IllegalStateException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); } } }