• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

oracle / opengrok / #3670

01 Nov 2023 10:10AM UTC coverage: 74.437% (-0.7%) from 75.16%
#3670

push

web-flow
Fix Sonar codesmell issues (#4460)

Signed-off-by: Gino Augustine <ginoaugustine@gmail.com>

308 of 308 new or added lines in 27 files covered. (100.0%)

43623 of 58604 relevant lines covered (74.44%)

0.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.27
/opengrok-web/src/main/java/org/opengrok/web/api/v1/controller/SuggesterController.java
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.validation.Valid;
28
import jakarta.validation.constraints.Min;
29
import jakarta.validation.constraints.NotBlank;
30
import jakarta.ws.rs.BeanParam;
31
import jakarta.ws.rs.Consumes;
32
import jakarta.ws.rs.DefaultValue;
33
import jakarta.ws.rs.GET;
34
import jakarta.ws.rs.POST;
35
import jakarta.ws.rs.PUT;
36
import jakarta.ws.rs.Path;
37
import jakarta.ws.rs.PathParam;
38
import jakarta.ws.rs.Produces;
39
import jakarta.ws.rs.QueryParam;
40
import jakarta.ws.rs.WebApplicationException;
41
import jakarta.ws.rs.core.MediaType;
42
import jakarta.ws.rs.core.Response;
43
import org.apache.lucene.index.Term;
44
import org.apache.lucene.queryparser.classic.ParseException;
45
import org.apache.lucene.search.Query;
46
import org.apache.lucene.util.BytesRef;
47
import org.opengrok.indexer.web.Laundromat;
48
import org.opengrok.suggest.LookupResultItem;
49
import org.opengrok.suggest.Suggester.Suggestions;
50
import org.opengrok.suggest.SuggesterUtils;
51
import org.opengrok.indexer.configuration.RuntimeEnvironment;
52
import org.opengrok.indexer.configuration.SuggesterConfig;
53
import org.opengrok.indexer.logger.LoggerFactory;
54
import org.opengrok.indexer.search.QueryBuilder;
55
import org.opengrok.indexer.web.Util;
56
import org.opengrok.web.api.v1.filter.CorsEnable;
57
import org.opengrok.web.api.v1.filter.IncomingFilter;
58
import org.opengrok.web.api.v1.suggester.model.SuggesterData;
59
import org.opengrok.web.api.v1.suggester.model.SuggesterQueryData;
60
import org.opengrok.web.api.v1.suggester.parser.SuggesterQueryDataParser;
61
import org.opengrok.web.api.v1.suggester.provider.filter.Authorized;
62
import org.opengrok.web.api.v1.suggester.provider.filter.Suggester;
63
import org.opengrok.web.api.v1.suggester.provider.service.SuggesterService;
64

65
import java.net.MalformedURLException;
66
import java.net.URL;
67
import java.time.Duration;
68
import java.time.Instant;
69
import java.util.AbstractMap.SimpleEntry;
70
import java.util.List;
71
import java.util.Map.Entry;
72
import java.util.Objects;
73
import java.util.Optional;
74
import java.util.concurrent.CompletableFuture;
75
import java.util.logging.Level;
76
import java.util.logging.Logger;
77
import java.util.stream.Collectors;
78

79
/**
80
 * Endpoint for suggester related REST queries.
81
 */
82
@Path(SuggesterController.PATH)
83
@Suggester
84
public final class SuggesterController {
85

86
    public static final String PATH = "suggest";
87

88
    private static final int POPULARITY_DEFAULT_PAGE_SIZE = 100;
89

90
    private static final Logger logger = LoggerFactory.getLogger(SuggesterController.class);
1✔
91

92
    private final RuntimeEnvironment env = RuntimeEnvironment.getInstance();
1✔
93

94
    private final SuggesterService suggester;
95

96
    @Inject
97
    public SuggesterController(SuggesterService suggester) {
1✔
98
        this.suggester = suggester;
1✔
99
    }
1✔
100

101
    /**
102
     * Returns suggestions based on the search criteria specified in {@code data}.
103
     * @param data suggester form data
104
     * @return list of suggestions and other related information
105
     * @throws ParseException if the Lucene query created from {@code data} could not be parsed
106
     */
107
    @GET
108
    @Authorized
109
    @CorsEnable
110
    @Produces(MediaType.APPLICATION_JSON)
111
    public Result getSuggestions(@Valid @BeanParam final SuggesterQueryData data) throws ParseException {
112
        Instant start = Instant.now();
1✔
113

114
        SuggesterData suggesterData = SuggesterQueryDataParser.parse(data);
1✔
115
        if (suggesterData.getSuggesterQuery() == null) {
1✔
116
            throw new ParseException("Could not determine suggester query");
×
117
        }
118

119
        SuggesterConfig config = env.getSuggesterConfig();
1✔
120

121
        modifyDataBasedOnConfiguration(suggesterData, config);
1✔
122

123
        if (!satisfiesConfiguration(suggesterData, config)) {
1✔
124
            logger.log(Level.FINER, "Suggester request with data {0} does not satisfy configuration settings", data);
1✔
125
            throw new WebApplicationException(Response.Status.NOT_FOUND);
1✔
126
        }
127

128
        Suggestions suggestions = suggester.getSuggestions(
1✔
129
                suggesterData.getProjects(), suggesterData.getSuggesterQuery(), suggesterData.getQuery());
1✔
130

131
        Instant end = Instant.now();
1✔
132

133
        long timeInMs = Duration.between(start, end).toMillis();
1✔
134

135
        return new Result(suggestions.getItems(), suggesterData.getIdentifier(),
1✔
136
                suggesterData.getSuggesterQueryFieldText(), timeInMs, suggestions.isPartialResult());
1✔
137
    }
138

139
    private void modifyDataBasedOnConfiguration(final SuggesterData data, final SuggesterConfig config) {
140
        if (config.getAllowedProjects() != null) {
1✔
141
            data.getProjects().removeIf(project -> !config.getAllowedProjects().contains(project));
1✔
142
        }
143
    }
1✔
144

145
    private boolean satisfiesConfiguration(final SuggesterData data, final SuggesterConfig config) {
146
        if (config.getMinChars() > data.getSuggesterQuery().length()) {
1✔
147
            return false;
1✔
148
        }
149

150
        if (config.getMaxProjects() < data.getProjects().size()) {
1✔
151
            return false;
1✔
152
        }
153

154
        if (config.getAllowedFields() != null && !config.getAllowedFields().contains(data.getSuggesterQuery().getField())) {
1✔
155
            return false;
1✔
156
        }
157

158
        return config.isAllowComplexQueries() || !SuggesterUtils.isComplexQuery(data.getQuery(), data.getSuggesterQuery());
1✔
159
    }
160

161
    /**
162
     * Returns the suggester configuration {@link SuggesterConfig}.
163
     * Because of the {@link IncomingFilter}, the
164
     * {@link org.opengrok.web.api.v1.controller.ConfigurationController} cannot be accessed from the
165
     * web page by the remote user. To resolve the problem, this method exposes this functionality.
166
     * @return suggester configuration
167
     */
168
    @GET
169
    @Path("/config")
170
    @CorsEnable
171
    @Produces(MediaType.APPLICATION_JSON)
172
    public SuggesterConfig getConfig() {
173
        return env.getSuggesterConfig();
1✔
174
    }
175

176
    @PUT
177
    @Path("/rebuild")
178
    public void rebuild() {
179
        CompletableFuture.runAsync(suggester::rebuild);
1✔
180
    }
1✔
181

182
    @PUT
183
    @Path("/rebuild/{project}")
184
    public void rebuild(@PathParam("project") final String project) {
185
        CompletableFuture.runAsync(() -> suggester.rebuild(project));
1✔
186
    }
1✔
187

188
    /**
189
     * Initializes the search data used by suggester to perform most popular completion. The passed {@code urls} are
190
     * decomposed into single terms which search counts are then increased by 1.
191
     * @param urls list of URLs in JSON format, e.g.
192
     * {@code ["http://demo.opengrok.org/search?project=opengrok&full=test"]}
193
     */
194
    @POST
195
    @Path("/init/queries")
196
    @Consumes(MediaType.APPLICATION_JSON)
197
    public void addSearchCountsQueries(final List<String> urls) {
198
        for (String urlStr : urls) {
1✔
199
            try {
200
                var url = new URL(urlStr);
1✔
201
                var params = Util.getQueryParams(url);
1✔
202

203
                var projects = params.get("project");
1✔
204

205
                for (String field : QueryBuilder.getSearchFields()) {
1✔
206

207
                    List<String> fieldQueryText = params.get(field);
1✔
208
                    if (Objects.nonNull(fieldQueryText) && !fieldQueryText.isEmpty()) {
1✔
209
                        if (fieldQueryText.size() > 2) {
1✔
210
                            logger.log(Level.WARNING, "Bad format, ignoring {0}", urlStr);
×
211
                        } else {
212
                            getQuery(field, fieldQueryText.get(0))
1✔
213
                                    .ifPresent(q -> suggester.onSearch(projects, q));
1✔
214

215
                        }
216
                    }
217

218
                }
1✔
219
            } catch (MalformedURLException e) {
×
220
                logger.log(Level.WARNING, e, () -> "Could not add search counts for " + urlStr);
×
221
            }
1✔
222
        }
1✔
223
    }
1✔
224

225
    private Optional<Query> getQuery(final String field, final String value) {
226
        QueryBuilder builder = new QueryBuilder();
1✔
227

228
        switch (field) {
1✔
229
            case QueryBuilder.FULL:
230
                builder.setFreetext(value);
1✔
231
                break;
1✔
232
            case QueryBuilder.DEFS:
233
                builder.setDefs(value);
×
234
                break;
×
235
            case QueryBuilder.REFS:
236
                builder.setRefs(value);
×
237
                break;
×
238
            case QueryBuilder.PATH:
239
                builder.setPath(value);
×
240
                break;
×
241
            case QueryBuilder.HIST:
242
                builder.setHist(value);
×
243
                break;
×
244
            case QueryBuilder.TYPE:
245
                builder.setType(value);
×
246
                break;
×
247
            default:
248
                return Optional.empty();
×
249
        }
250
        try {
251
            return Optional.of(builder.build());
1✔
252
        } catch (ParseException e) {
×
253
            logger.log(Level.FINE, "Bad request", e);
×
254
            return Optional.empty();
×
255
        }
256
    }
257

258
    /**
259
     * Initializes the search data used by suggester to perform most popular completion.
260
     * @param termIncrements data by which to initialize the search data
261
     */
262
    @POST
263
    @Path("/init/raw")
264
    @Consumes(MediaType.APPLICATION_JSON)
265
    public void addSearchCountsRaw(@Valid final List<TermIncrementData> termIncrements) {
266
        for (TermIncrementData termIncrement : termIncrements) {
1✔
267
            suggester.increaseSearchCount(termIncrement.project,
1✔
268
                    new Term(termIncrement.field, termIncrement.token), termIncrement.increment);
269
        }
1✔
270
    }
1✔
271

272
    /**
273
     * Returns the searched terms sorted according to their popularity.
274
     * @param project project for which to return the data
275
     * @param field field for which to return the data
276
     * @param page which page of data to retrieve
277
     * @param pageSize number of results to return
278
     * @param all return all pages
279
     * @return list of terms with their popularity
280
     */
281
    @GET
282
    @Path("/popularity/{project}")
283
    @Produces(MediaType.APPLICATION_JSON)
284
    public List<Entry<String, Integer>> getPopularityDataPaged(
285
            @PathParam("project") String project,
286
            @QueryParam("field") @DefaultValue(QueryBuilder.FULL) String field,
287
            @QueryParam("page") @DefaultValue("" + 0) final int page,
288
            @QueryParam("pageSize") @DefaultValue("" + POPULARITY_DEFAULT_PAGE_SIZE) final int pageSize,
289
            @QueryParam("all") final boolean all
290
    ) {
291
        if (!QueryBuilder.isSearchField(field)) {
1✔
292
            throw new WebApplicationException("field is invalid", Response.Status.BAD_REQUEST);
×
293
        }
294
        // Avoid classification as a taint bug.
295
        project = Laundromat.launderInput(project);
1✔
296
        field = Laundromat.launderInput(field);
1✔
297

298
        List<Entry<BytesRef, Integer>> data;
299
        if (all) {
1✔
300
            data = suggester.getPopularityData(project, field, 0, Integer.MAX_VALUE);
1✔
301
        } else {
302
            data = suggester.getPopularityData(project, field, page, pageSize);
1✔
303
        }
304
        return data.stream()
1✔
305
                .map(e -> new SimpleEntry<>(e.getKey().utf8ToString(), e.getValue()))
1✔
306
                .collect(Collectors.toList());
1✔
307
    }
308

309
    private static class Result {
310

311
        private long time;
312

313
        private List<LookupResultItem> suggestions;
314

315
        private String identifier;
316

317
        private String queryText;
318

319
        private boolean partialResult;
320

321
        Result(
322
                final List<LookupResultItem> suggestions,
323
                final String identifier,
324
                final String queryText,
325
                final long time,
326
                final boolean partialResult
327
        ) {
1✔
328
            this.suggestions = suggestions;
1✔
329
            this.identifier = identifier;
1✔
330
            this.queryText = queryText;
1✔
331
            this.time = time;
1✔
332
            this.partialResult = partialResult;
1✔
333
        }
1✔
334

335
        public long getTime() {
336
            return time;
1✔
337
        }
338

339
        public void setTime(long time) {
340
            this.time = time;
×
341
        }
×
342

343
        public List<LookupResultItem> getSuggestions() {
344
            return suggestions;
1✔
345
        }
346

347
        public void setSuggestions(List<LookupResultItem> suggestions) {
348
            this.suggestions = suggestions;
×
349
        }
×
350

351
        public String getIdentifier() {
352
            return identifier;
1✔
353
        }
354

355
        public void setIdentifier(String identifier) {
356
            this.identifier = identifier;
×
357
        }
×
358

359
        public String getQueryText() {
360
            return queryText;
1✔
361
        }
362

363
        public void setQueryText(String queryText) {
364
            this.queryText = queryText;
×
365
        }
×
366

367
        public boolean isPartialResult() {
368
            return partialResult;
1✔
369
        }
370

371
        public void setPartialResult(boolean partialResult) {
372
            this.partialResult = partialResult;
×
373
        }
×
374
    }
375

376
    private static class TermIncrementData {
377

378
        private String project;
379

380
        @NotBlank(message = "Field cannot be blank")
381
        private String field;
382

383
        @NotBlank(message = "Token cannot be blank")
384
        private String token;
385

386
        @Min(message = "Increment must be positive", value = 0)
387
        private int increment;
388

389
        public String getProject() {
390
            return project;
×
391
        }
392

393
        public void setProject(String project) {
394
            this.project = project;
1✔
395
        }
1✔
396

397
        public String getField() {
398
            return field;
×
399
        }
400

401
        public void setField(String field) {
402
            this.field = field;
1✔
403
        }
1✔
404

405
        public String getToken() {
406
            return token;
×
407
        }
408

409
        public void setToken(String token) {
410
            this.token = token;
1✔
411
        }
1✔
412

413
        public int getIncrement() {
414
            return increment;
×
415
        }
416

417
        public void setIncrement(int increment) {
418
            this.increment = increment;
1✔
419
        }
1✔
420
    }
421

422
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc