Skip to content

Commit 3f99b53

Browse files
authored
Merge pull request #737 from amvanbaren/bugfix/issue-653
https://open-vsx.org/admin/report API returning 500
2 parents f93932d + 60948ba commit 3f99b53

File tree

14 files changed

+375
-302
lines changed

14 files changed

+375
-302
lines changed

server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.stream.Collectors;
1616
import java.net.URI;
1717

18+
import com.fasterxml.jackson.databind.JsonNode;
1819
import com.google.common.base.Strings;
1920

2021
import org.eclipse.openvsx.LocalRegistryService;
@@ -42,6 +43,7 @@
4243

4344
@RestController
4445
public class AdminAPI {
46+
4547
@Autowired
4648
RepositoryService repositories;
4749

@@ -57,6 +59,41 @@ public class AdminAPI {
5759
@Autowired
5860
SearchUtilService search;
5961

62+
@GetMapping(
63+
path = "/admin/reports",
64+
produces = MediaType.APPLICATION_JSON_VALUE
65+
)
66+
public ResponseEntity<Map<String, List<String>>> getReports(
67+
@RequestParam("token") String tokenValue
68+
) {
69+
try {
70+
validateToken(tokenValue);
71+
return ResponseEntity.ok(admins.getReports());
72+
} catch (ErrorResultException exc) {
73+
return ResponseEntity.status(exc.getStatus()).build();
74+
}
75+
}
76+
77+
@PostMapping(
78+
path = "/admin/report/schedule",
79+
consumes = MediaType.APPLICATION_JSON_VALUE,
80+
produces = MediaType.APPLICATION_JSON_VALUE
81+
)
82+
public ResponseEntity<ResultJson> scheduleReport(
83+
@RequestParam String token,
84+
@RequestBody JsonNode json
85+
) {
86+
try {
87+
validateToken(token);
88+
var year = json.get("year").asInt();
89+
var month = json.get("month").asInt();
90+
admins.scheduleReport(year, month);
91+
return ResponseEntity.accepted().build();
92+
} catch (ErrorResultException exc) {
93+
return exc.toResponseEntity(ResultJson.class);
94+
}
95+
}
96+
6097
@GetMapping(
6198
path = "/admin/report",
6299
produces = MediaType.APPLICATION_JSON_VALUE
@@ -91,12 +128,15 @@ public ResponseEntity<String> getReportCsv(
91128
}
92129
}
93130

94-
private AdminStatistics getReport(String tokenValue, int year, int month) {
131+
private void validateToken(String tokenValue) {
95132
var accessToken = repositories.findAccessToken(tokenValue);
96133
if(accessToken == null || !accessToken.isActive() || accessToken.getUser() == null || !ROLE_ADMIN.equals(accessToken.getUser().getRole())) {
97134
throw new ErrorResultException("Invalid access token", HttpStatus.FORBIDDEN);
98135
}
136+
}
99137

138+
private AdminStatistics getReport(String tokenValue, int year, int month) {
139+
validateToken(tokenValue);
100140
return admins.getAdminStatistics(year, month);
101141
}
102142

server/src/main/java/org/eclipse/openvsx/admin/AdminService.java

Lines changed: 48 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,33 @@
1717
import org.eclipse.openvsx.eclipse.EclipseService;
1818
import org.eclipse.openvsx.entities.*;
1919
import org.eclipse.openvsx.json.*;
20+
import org.eclipse.openvsx.migration.HandlerJobRequest;
21+
import org.eclipse.openvsx.migration.MigrationRunner;
2022
import org.eclipse.openvsx.repositories.RepositoryService;
2123
import org.eclipse.openvsx.search.SearchUtilService;
2224
import org.eclipse.openvsx.storage.StorageUtilService;
2325
import org.eclipse.openvsx.util.*;
2426
import org.jobrunr.scheduling.JobRequestScheduler;
25-
import org.slf4j.Logger;
26-
import org.slf4j.LoggerFactory;
27+
import org.jobrunr.scheduling.cron.Cron;
2728
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.boot.context.event.ApplicationStartedEvent;
30+
import org.springframework.context.event.EventListener;
2831
import org.springframework.http.HttpStatus;
2932
import org.springframework.stereotype.Component;
30-
import org.springframework.util.StopWatch;
3133

3234
import javax.persistence.EntityManager;
3335
import javax.transaction.Transactional;
34-
import java.time.DateTimeException;
35-
import java.time.LocalDateTime;
36-
import java.time.temporal.ChronoUnit;
37-
import java.util.Comparator;
38-
import java.util.LinkedHashSet;
36+
import java.nio.charset.StandardCharsets;
37+
import java.time.Instant;
38+
import java.time.ZoneId;
39+
import java.util.*;
3940
import java.util.stream.Collectors;
4041

4142
import static org.eclipse.openvsx.entities.FileResource.*;
4243

4344
@Component
4445
public class AdminService {
4546

46-
private static final Logger LOGGER = LoggerFactory.getLogger(AdminService.class);
4747
@Autowired
4848
RepositoryService repositories;
4949

@@ -77,6 +77,12 @@ public class AdminService {
7777
@Autowired
7878
JobRequestScheduler scheduler;
7979

80+
@EventListener
81+
public void applicationStarted(ApplicationStartedEvent event) {
82+
var jobRequest = new HandlerJobRequest<>(MonthlyAdminStatisticsJobRequestHandler.class);
83+
scheduler.scheduleRecurrently("MonthlyAdminStatistics", Cron.monthly(1, 0, 3), ZoneId.of("UTC"), jobRequest);
84+
}
85+
8086
@Transactional(rollbackOn = ErrorResultException.class)
8187
public ResultJson deleteExtension(String namespaceName, String extensionName, UserData admin)
8288
throws ErrorResultException {
@@ -347,121 +353,49 @@ public void logAdminAction(UserData admin, ResultJson result) {
347353
}
348354
}
349355

350-
@Transactional
351356
public AdminStatistics getAdminStatistics(int year, int month) throws ErrorResultException {
357+
validateYearAndMonth(year, month);
358+
var statistics = repositories.findAdminStatisticsByYearAndMonth(year, month);
359+
if(statistics == null) {
360+
throw new NotFoundException();
361+
}
362+
363+
return statistics;
364+
}
365+
366+
public void scheduleReport(int year, int month) {
367+
validateYearAndMonth(year, month);
368+
if(repositories.findAdminStatisticsByYearAndMonth(year, month) != null) {
369+
throw new ErrorResultException("Report for " + year + "/" + month + " already exists");
370+
}
371+
372+
var jobIdText = "AdminStatistics::year=" + year + ",month=" + month;
373+
var jobId = UUID.nameUUIDFromBytes(jobIdText.getBytes(StandardCharsets.UTF_8));
374+
scheduler.enqueue(jobId, new AdminStatisticsJobRequest(year, month));
375+
}
376+
377+
private void validateYearAndMonth(int year, int month) {
352378
if(year < 0) {
353379
throw new ErrorResultException("Year can't be negative", HttpStatus.BAD_REQUEST);
354380
}
355381
if(month < 1 || month > 12) {
356382
throw new ErrorResultException("Month must be a value between 1 and 12", HttpStatus.BAD_REQUEST);
357383
}
358384

359-
var now = LocalDateTime.now();
360-
if(year > now.getYear() || (year == now.getYear() && month > now.getMonthValue())) {
385+
var now = TimeUtil.getCurrentUTC();
386+
if(year > now.getYear() || (year == now.getYear() && month >= now.getMonthValue())) {
361387
throw new ErrorResultException("Combination of year and month lies in the future", HttpStatus.BAD_REQUEST);
362388
}
389+
}
363390

364-
var statistics = repositories.findAdminStatisticsByYearAndMonth(year, month);
365-
if(statistics == null) {
366-
LocalDateTime startInclusive;
367-
try {
368-
startInclusive = LocalDateTime.of(year, month, 1, 0, 0);
369-
} catch(DateTimeException e) {
370-
throw new ErrorResultException("Invalid month or year", HttpStatus.BAD_REQUEST);
371-
}
372-
373-
var currentYearAndMonth = now.getYear() == year && now.getMonthValue() == month;
374-
var endExclusive = currentYearAndMonth
375-
? now.truncatedTo(ChronoUnit.MINUTES)
376-
: startInclusive.plusMonths(1);
377-
378-
LOGGER.info(">> ADMIN REPORT STATS");
379-
var stopwatch = new StopWatch();
380-
stopwatch.start("repositories.countActiveExtensions");
381-
var extensions = repositories.countActiveExtensions(endExclusive);
382-
stopwatch.stop();
383-
LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis());
384-
385-
stopwatch.start("repositories.downloadsBetween");
386-
var downloads = repositories.downloadsBetween(startInclusive, endExclusive);
387-
stopwatch.stop();
388-
LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis());
389-
390-
stopwatch.start("repositories.downloadsUntil");
391-
var downloadsTotal = repositories.downloadsUntil(endExclusive);
392-
stopwatch.stop();
393-
LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis());
394-
395-
stopwatch.start("repositories.countActiveExtensionPublishers");
396-
var publishers = repositories.countActiveExtensionPublishers(endExclusive);
397-
stopwatch.stop();
398-
LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis());
399-
400-
stopwatch.start("repositories.averageNumberOfActiveReviewsPerActiveExtension");
401-
var averageReviewsPerExtension = repositories.averageNumberOfActiveReviewsPerActiveExtension(endExclusive);
402-
stopwatch.stop();
403-
LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis());
404-
405-
stopwatch.start("repositories.countPublishersThatClaimedNamespaceOwnership");
406-
var namespaceOwners = repositories.countPublishersThatClaimedNamespaceOwnership(endExclusive);
407-
stopwatch.stop();
408-
LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis());
409-
410-
stopwatch.start("repositories.countActiveExtensionsGroupedByExtensionReviewRating");
411-
var extensionsByRating = repositories.countActiveExtensionsGroupedByExtensionReviewRating(endExclusive);
412-
stopwatch.stop();
413-
LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis());
414-
415-
stopwatch.start("repositories.countActiveExtensionPublishersGroupedByExtensionsPublished");
416-
var publishersByExtensionsPublished = repositories.countActiveExtensionPublishersGroupedByExtensionsPublished(endExclusive);
417-
stopwatch.stop();
418-
LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis());
419-
420-
var limit = 10;
421-
422-
stopwatch.start("repositories.topMostActivePublishingUsers");
423-
var topMostActivePublishingUsers = repositories.topMostActivePublishingUsers(endExclusive, limit);
424-
stopwatch.stop();
425-
LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis());
426-
427-
stopwatch.start("repositories.topNamespaceExtensions");
428-
var topNamespaceExtensions = repositories.topNamespaceExtensions(endExclusive, limit);
429-
stopwatch.stop();
430-
LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis());
431-
432-
stopwatch.start("repositories.topNamespaceExtensionVersions");
433-
var topNamespaceExtensionVersions = repositories.topNamespaceExtensionVersions(endExclusive, limit);
434-
stopwatch.stop();
435-
LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis());
436-
437-
stopwatch.start("repositories.topMostDownloadedExtensions");
438-
var topMostDownloadedExtensions = repositories.topMostDownloadedExtensions(endExclusive, limit);
439-
stopwatch.stop();
440-
LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis());
441-
LOGGER.info("<< ADMIN REPORT STATS");
442-
443-
statistics = new AdminStatistics();
444-
statistics.setYear(year);
445-
statistics.setMonth(month);
446-
statistics.setExtensions(extensions);
447-
statistics.setDownloads(downloads);
448-
statistics.setDownloadsTotal(downloadsTotal);
449-
statistics.setPublishers(publishers);
450-
statistics.setAverageReviewsPerExtension(averageReviewsPerExtension);
451-
statistics.setNamespaceOwners(namespaceOwners);
452-
statistics.setExtensionsByRating(extensionsByRating);
453-
statistics.setPublishersByExtensionsPublished(publishersByExtensionsPublished);
454-
statistics.setTopMostActivePublishingUsers(topMostActivePublishingUsers);
455-
statistics.setTopNamespaceExtensions(topNamespaceExtensions);
456-
statistics.setTopNamespaceExtensionVersions(topNamespaceExtensionVersions);
457-
statistics.setTopMostDownloadedExtensions(topMostDownloadedExtensions);
458-
459-
if(!currentYearAndMonth) {
460-
// archive statistics for quicker lookup next time
461-
entityManager.persist(statistics);
462-
}
463-
}
464-
465-
return statistics;
391+
public Map<String, List<String>> getReports() {
392+
return repositories.findAllAdminStatistics().stream()
393+
.sorted(Comparator.comparingInt(AdminStatistics::getYear).thenComparing(AdminStatistics::getMonth))
394+
.map(stat -> {
395+
var yearText = String.valueOf(stat.getYear());
396+
var monthText = String.valueOf(stat.getMonth());
397+
return new AbstractMap.SimpleEntry<>(yearText, monthText);
398+
})
399+
.collect(Collectors.groupingBy(Map.Entry::getKey, () -> new LinkedHashMap<>(), Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
466400
}
467401
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/** ******************************************************************************
2+
* Copyright (c) 2023 Precies. Software Ltd and others
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
* ****************************************************************************** */
10+
package org.eclipse.openvsx.admin;
11+
12+
import org.jobrunr.jobs.lambdas.JobRequest;
13+
import org.jobrunr.jobs.lambdas.JobRequestHandler;
14+
15+
public class AdminStatisticsJobRequest implements JobRequest {
16+
17+
private int year;
18+
private int month;
19+
20+
public AdminStatisticsJobRequest() {}
21+
22+
public AdminStatisticsJobRequest(int year, int month) {
23+
this.year = year;
24+
this.month = month;
25+
}
26+
27+
public int getYear() {
28+
return year;
29+
}
30+
31+
public void setYear(int year) {
32+
this.year = year;
33+
}
34+
35+
public int getMonth() {
36+
return month;
37+
}
38+
39+
public void setMonth(int month) {
40+
this.month = month;
41+
}
42+
43+
@Override
44+
public Class<? extends JobRequestHandler> getJobRequestHandler() {
45+
return AdminStatisticsJobRequestHandler.class;
46+
}
47+
}

0 commit comments

Comments
 (0)