1717import org .eclipse .openvsx .eclipse .EclipseService ;
1818import org .eclipse .openvsx .entities .*;
1919import org .eclipse .openvsx .json .*;
20+ import org .eclipse .openvsx .migration .HandlerJobRequest ;
21+ import org .eclipse .openvsx .migration .MigrationRunner ;
2022import org .eclipse .openvsx .repositories .RepositoryService ;
2123import org .eclipse .openvsx .search .SearchUtilService ;
2224import org .eclipse .openvsx .storage .StorageUtilService ;
2325import org .eclipse .openvsx .util .*;
2426import org .jobrunr .scheduling .JobRequestScheduler ;
25- import org .slf4j .Logger ;
26- import org .slf4j .LoggerFactory ;
27+ import org .jobrunr .scheduling .cron .Cron ;
2728import org .springframework .beans .factory .annotation .Autowired ;
29+ import org .springframework .boot .context .event .ApplicationStartedEvent ;
30+ import org .springframework .context .event .EventListener ;
2831import org .springframework .http .HttpStatus ;
2932import org .springframework .stereotype .Component ;
30- import org .springframework .util .StopWatch ;
3133
3234import javax .persistence .EntityManager ;
3335import 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 .*;
3940import java .util .stream .Collectors ;
4041
4142import static org .eclipse .openvsx .entities .FileResource .*;
4243
4344@ Component
4445public 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}
0 commit comments