xref: /OpenGrok/opengrok-web/src/main/java/org/opengrok/web/api/v1/controller/SearchController.java (revision c6f0939b1c668e9f8e1e276424439c3106b3a029)
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