1 /* 2 * CDDL HEADER START 3 * 4 * The contents of this file are subject to the terms of the 5 * Common Development and Distribution License (the "License"). 6 * You may not use this file except in compliance with the License. 7 * 8 * See LICENSE.txt included in this distribution for the specific 9 * language governing permissions and limitations under the License. 10 * 11 * When distributing Covered Code, include this CDDL HEADER in each 12 * file and include the License file at LICENSE.txt. 13 * If applicable, add the following below this CDDL HEADER, with the 14 * fields enclosed by brackets "[]" replaced with your own identifying 15 * information: Portions Copyright [yyyy] [name of copyright owner] 16 * 17 * CDDL HEADER END 18 */ 19 20 /* 21 * Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved. 22 * Portions Copyright (c) 2020, Chris Fraire <cfraire@me.com>. 23 */ 24 package org.opengrok.web.api.v1.controller; 25 26 import jakarta.inject.Inject; 27 import jakarta.servlet.http.HttpServletRequest; 28 import jakarta.ws.rs.DefaultValue; 29 import jakarta.ws.rs.GET; 30 import jakarta.ws.rs.Path; 31 import jakarta.ws.rs.Produces; 32 import jakarta.ws.rs.QueryParam; 33 import jakarta.ws.rs.WebApplicationException; 34 import jakarta.ws.rs.core.Context; 35 import jakarta.ws.rs.core.MediaType; 36 import jakarta.ws.rs.core.Response; 37 import org.apache.lucene.search.Query; 38 import org.opengrok.indexer.configuration.Project; 39 import org.opengrok.indexer.search.Hit; 40 import org.opengrok.indexer.search.SearchEngine; 41 import org.opengrok.indexer.web.QueryParameters; 42 import org.opengrok.web.PageConfig; 43 import org.opengrok.web.api.v1.filter.CorsEnable; 44 import org.opengrok.web.api.v1.suggester.provider.service.SuggesterService; 45 46 import java.time.Duration; 47 import java.time.Instant; 48 import java.util.ArrayList; 49 import java.util.Collections; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Set; 53 import java.util.stream.Collectors; 54 55 @Path(SearchController.PATH) 56 public class SearchController { 57 58 public static final String PATH = "search"; 59 60 private static final int MAX_RESULTS = 1000; 61 62 @Inject 63 private SuggesterService suggester; 64 65 @GET 66 @CorsEnable 67 @Produces(MediaType.APPLICATION_JSON) search( @ontext final HttpServletRequest req, @QueryParam(QueryParameters.FULL_SEARCH_PARAM) final String full, @QueryParam("def") final String def, @QueryParam("symbol") final String symbol, @QueryParam(QueryParameters.PATH_SEARCH_PARAM) final String path, @QueryParam(QueryParameters.HIST_SEARCH_PARAM) final String hist, @QueryParam(QueryParameters.TYPE_SEARCH_PARAM) final String type, @QueryParam("projects") final List<String> projects, @QueryParam("maxresults") @DefaultValue(MAX_RESULTS + "") final int maxResults, @QueryParam(QueryParameters.START_PARAM) @DefaultValue(0 + "") final int startDocIndex )68 public SearchResult search( 69 @Context final HttpServletRequest req, 70 @QueryParam(QueryParameters.FULL_SEARCH_PARAM) final String full, 71 @QueryParam("def") final String def, // Nearly QueryParameters.DEFS_SEARCH_PARAM 72 @QueryParam("symbol") final String symbol, // Akin to QueryBuilder.REFS_SEARCH_PARAM 73 @QueryParam(QueryParameters.PATH_SEARCH_PARAM) final String path, 74 @QueryParam(QueryParameters.HIST_SEARCH_PARAM) final String hist, 75 @QueryParam(QueryParameters.TYPE_SEARCH_PARAM) final String type, 76 @QueryParam("projects") final List<String> projects, 77 @QueryParam("maxresults") // Akin to QueryParameters.COUNT_PARAM 78 @DefaultValue(MAX_RESULTS + "") final int maxResults, 79 @QueryParam(QueryParameters.START_PARAM) @DefaultValue(0 + "") final int startDocIndex 80 ) { 81 try (SearchEngineWrapper engine = new SearchEngineWrapper(full, def, symbol, path, hist, type)) { 82 83 if (!engine.isValid()) { 84 throw new WebApplicationException("Invalid request", Response.Status.BAD_REQUEST); 85 } 86 87 Instant startTime = Instant.now(); 88 89 suggester.onSearch(projects, engine.getQuery()); 90 91 Map<String, List<SearchHit>> hits = engine.search(req, projects, startDocIndex, maxResults) 92 .stream() 93 .collect(Collectors.groupingBy(Hit::getPath, 94 Collectors.mapping(h -> new SearchHit(h.getLine(), h.getLineno()), Collectors.toList()))); 95 96 long duration = Duration.between(startTime, Instant.now()).toMillis(); 97 98 int endDocument = startDocIndex + hits.size() - 1; 99 100 return new SearchResult(duration, engine.numResults, hits, startDocIndex, endDocument); 101 } 102 } 103 104 private static class SearchEngineWrapper implements AutoCloseable { 105 106 private final SearchEngine engine = new SearchEngine(); 107 108 private int numResults; 109 SearchEngineWrapper( final String full, final String def, final String symbol, final String path, final String hist, final String type )110 private SearchEngineWrapper( 111 final String full, 112 final String def, 113 final String symbol, 114 final String path, 115 final String hist, 116 final String type 117 ) { 118 engine.setFreetext(full); 119 engine.setDefinition(def); 120 engine.setSymbol(symbol); 121 engine.setFile(path); 122 engine.setHistory(hist); 123 engine.setType(type); 124 } 125 search( final HttpServletRequest req, final List<String> projects, final int startDocIndex, final int maxResults )126 public List<Hit> search( 127 final HttpServletRequest req, 128 final List<String> projects, 129 final int startDocIndex, 130 final int maxResults 131 ) { 132 Set<Project> allProjects = PageConfig.get(req).getProjectHelper().getAllProjects(); 133 if (projects == null || projects.isEmpty()) { 134 numResults = engine.search(new ArrayList<>(allProjects)); 135 } else { 136 numResults = engine.search(allProjects.stream() 137 .filter(p -> projects.contains(p.getName())) 138 .collect(Collectors.toList())); 139 } 140 141 if (startDocIndex > numResults) { 142 return Collections.emptyList(); 143 } 144 145 int resultSize = numResults - startDocIndex; 146 if (resultSize > maxResults) { 147 resultSize = maxResults; 148 } 149 150 List<Hit> results = new ArrayList<>(); 151 engine.results(startDocIndex, startDocIndex + resultSize, results); 152 153 return results; 154 } 155 isValid()156 private boolean isValid() { 157 return engine.isValidQuery(); 158 } 159 getQuery()160 private Query getQuery() { 161 return engine.getQueryObject(); 162 } 163 164 @Override close()165 public void close() { 166 engine.destroy(); 167 } 168 } 169 170 private static class SearchResult { 171 172 private final long time; 173 174 private final int resultCount; 175 176 private final int startDocument; 177 178 private final int endDocument; 179 180 private final Map<String, List<SearchHit>> results; 181 SearchResult( final long time, final int resultCount, final Map<String, List<SearchHit>> results, final int startDocument, final int endDocument )182 private SearchResult( 183 final long time, 184 final int resultCount, 185 final Map<String, List<SearchHit>> results, 186 final int startDocument, 187 final int endDocument 188 ) { 189 this.time = time; 190 this.resultCount = resultCount; 191 this.results = results; 192 this.startDocument = startDocument; 193 this.endDocument = endDocument; 194 } 195 getTime()196 public long getTime() { 197 return time; 198 } 199 getResultCount()200 public int getResultCount() { 201 return resultCount; 202 } 203 getResults()204 public Map<String, List<SearchHit>> getResults() { 205 return results; 206 } 207 getStartDocument()208 public int getStartDocument() { 209 return startDocument; 210 } 211 getEndDocument()212 public int getEndDocument() { 213 return endDocument; 214 } 215 } 216 217 private static class SearchHit { 218 219 private final String line; 220 221 private final String lineNumber; 222 SearchHit(final String line, final String lineNumber)223 private SearchHit(final String line, final String lineNumber) { 224 this.line = line; 225 this.lineNumber = lineNumber; 226 } 227 getLine()228 public String getLine() { 229 return line; 230 } 231 getLineNumber()232 public String getLineNumber() { 233 return lineNumber; 234 } 235 } 236 237 } 238