diff --git a/comixed-library/src/main/java/org/comixedproject/model/tasks/TaskAuditLogEntry.java b/comixed-library/src/main/java/org/comixedproject/model/tasks/TaskAuditLogEntry.java index 0eac3bcdc..9772f89f7 100644 --- a/comixed-library/src/main/java/org/comixedproject/model/tasks/TaskAuditLogEntry.java +++ b/comixed-library/src/main/java/org/comixedproject/model/tasks/TaskAuditLogEntry.java @@ -18,11 +18,15 @@ package org.comixedproject.model.tasks; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; import java.util.Date; import java.util.Objects; import javax.persistence.*; import lombok.Getter; import lombok.Setter; +import org.comixed.views.View; /** * TaskAuditLogEntry represents a single entry in the task audit log table. @@ -41,21 +45,31 @@ public class TaskAuditLogEntry { @Column(name = "start_time", nullable = false, updatable = false) @Getter @Setter + @JsonProperty("startTime") + @JsonFormat(shape = JsonFormat.Shape.NUMBER) + @JsonView(View.TaskAuditLogEntryList.class) private Date startTime = new Date(); @Column(name = "end_time", nullable = false, updatable = false) @Getter @Setter + @JsonProperty("endTime") + @JsonFormat(shape = JsonFormat.Shape.NUMBER) + @JsonView(View.TaskAuditLogEntryList.class) private Date endTime = new Date(); @Column(name = "successful", nullable = false, updatable = false) @Getter @Setter + @JsonProperty("successful") + @JsonView(View.TaskAuditLogEntryList.class) private Boolean successful; @Column(name = "description", nullable = false, updatable = false, length = 2048) @Getter @Setter + @JsonProperty("description") + @JsonView(View.TaskAuditLogEntryList.class) private String description; @Override diff --git a/comixed-library/src/main/java/org/comixedproject/views/View.java b/comixed-library/src/main/java/org/comixedproject/views/View.java index 93b2545e3..63aed93ba 100644 --- a/comixed-library/src/main/java/org/comixedproject/views/View.java +++ b/comixed-library/src/main/java/org/comixedproject/views/View.java @@ -62,4 +62,7 @@ public interface LibraryUpdate {} /** Used when viewing the list of plugins. */ public interface PluginList {} + + /** Uses when viewing the list of task audit log entries. */ + public interface TaskAuditLogEntryList {} } diff --git a/comixed-rest-api/src/main/java/org/comixed/controller/ComiXedControllerException.java b/comixed-rest-api/src/main/java/org/comixed/controller/ComiXedControllerException.java new file mode 100644 index 000000000..6e4f56770 --- /dev/null +++ b/comixed-rest-api/src/main/java/org/comixed/controller/ComiXedControllerException.java @@ -0,0 +1,31 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixed.controller; + +/** + * ComiXedControllerException is throw when an error occurs during the processing of a + * REST API request. + * + * @author Darryl L. Pierce + */ +public class ComiXedControllerException extends Exception { + public ComiXedControllerException(final String message, final Exception cause) { + super(message, cause); + } +} diff --git a/comixed-rest-api/src/main/java/org/comixed/controller/tasks/TaskController.java b/comixed-rest-api/src/main/java/org/comixed/controller/tasks/TaskController.java new file mode 100644 index 000000000..296252773 --- /dev/null +++ b/comixed-rest-api/src/main/java/org/comixed/controller/tasks/TaskController.java @@ -0,0 +1,71 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixed.controller.tasks; + +import com.fasterxml.jackson.annotation.JsonView; +import java.util.Date; +import java.util.List; +import lombok.extern.log4j.Log4j2; +import org.comixed.controller.ComiXedControllerException; +import org.comixed.model.tasks.TaskAuditLogEntry; +import org.comixed.repositories.tasks.TaskAuditLogRepository; +import org.comixed.service.ComiXedServiceException; +import org.comixed.service.task.TaskService; +import org.comixed.views.View; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * TaskController provides REST APIs for interacting with the tasks system. + * + * @author Darryl L. Pierce + */ +@RestController +@RequestMapping(value = "/api/tasks") +@Log4j2 +public class TaskController { + @Autowired private TaskAuditLogRepository taskAuditLogRepository; + @Autowired private TaskService taskService; + + /** + * Retrieve the list of log entries after the cutoff time. + * + * @param cutoff the cutoff + * @return the log entries + */ + @GetMapping(value = "/entries/{cutoff}", produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasRole('ADMIN')") + @JsonView(View.TaskAuditLogEntryList.class) + public List getAllAfterDate(@PathVariable("cutoff") final Long timestamp) + throws ComiXedControllerException { + final Date cutoff = new Date(timestamp); + log.debug("Getting all task audit log entries after: {}", cutoff); + + try { + return this.taskService.getAuditLogEntriesAfter(cutoff); + } catch (ComiXedServiceException error) { + throw new ComiXedControllerException("unable to get task audit log entries", error); + } + } +} diff --git a/comixed-rest-api/src/main/java/org/comixedproject/controller/scraping/ComicVineScraperController.java b/comixed-rest-api/src/main/java/org/comixedproject/controller/scraping/ComicVineScraperController.java index ef66013b5..5db281d0f 100644 --- a/comixed-rest-api/src/main/java/org/comixedproject/controller/scraping/ComicVineScraperController.java +++ b/comixed-rest-api/src/main/java/org/comixedproject/controller/scraping/ComicVineScraperController.java @@ -55,7 +55,7 @@ public class ComicVineScraperController { * @param volume the volume id * @param request the request body * @return the issue - * @throws RESTException if an error occurs + * @throws ComiXedControllerException if an error occurs */ @PostMapping( value = "/volumes/{volume}/issues", @@ -64,7 +64,7 @@ public class ComicVineScraperController { public ScrapingIssue queryForIssue( @PathVariable("volume") final Integer volume, @RequestBody() final GetScrapingIssueRequest request) - throws RESTException { + throws ComiXedControllerException { String issue = request.getIssueNumber(); boolean skipCache = request.isSkipCache(); String apiKey = request.getApiKey(); @@ -75,7 +75,7 @@ public ScrapingIssue queryForIssue( try { return this.scrapingAdaptor.getIssue(apiKey, volume, issue, skipCache); } catch (ScrapingException error) { - throw new RESTException("Failed to get single scraping issue", error); + throw new ComiXedControllerException("Failed to get single scraping issue", error); } } @@ -84,14 +84,14 @@ public ScrapingIssue queryForIssue( * * @param request the reqwuest body * @return the list of volumes - * @throws RESTException if an error occurs + * @throws ComiXedControllerException if an error occurs */ @PostMapping( value = "/volumes", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public List queryForVolumes(@RequestBody() final GetVolumesRequest request) - throws RESTException { + throws ComiXedControllerException { String apiKey = request.getApiKey(); boolean skipCache = request.getSkipCache(); String series = request.getSeries(); @@ -105,7 +105,7 @@ public List queryForVolumes(@RequestBody() final GetVolumesReque return result; } catch (ScrapingException error) { - throw new RESTException("Failed to get list of volumes", error); + throw new ComiXedControllerException("Failed to get list of volumes", error); } } @@ -116,7 +116,7 @@ public List queryForVolumes(@RequestBody() final GetVolumesReque * @param issueId the issue id * @param request the request body * @return the scraped and updaed {@link Comic} - * @throws RESTException if an error occurs + * @throws ComiXedControllerException if an error occurs */ @PostMapping( value = "/comics/{comicId}/issue/{issueId}", @@ -127,7 +127,7 @@ public Comic scrapeAndSaveComicDetails( @PathVariable("comicId") final Long comicId, @PathVariable("issueId") final String issueId, @RequestBody() final ComicScrapeRequest request) - throws RESTException { + throws ComiXedControllerException { boolean skipCache = request.getSkipCache(); String apiKey = request.getApiKey(); @@ -138,7 +138,7 @@ public Comic scrapeAndSaveComicDetails( try { comic = this.comicService.getComic(comicId); } catch (ComicException error) { - throw new RESTException("Failed to load comic", error); + throw new ComiXedControllerException("Failed to load comic", error); } try { @@ -150,7 +150,7 @@ public Comic scrapeAndSaveComicDetails( return comic; } catch (ScrapingException error) { - throw new RESTException("Failed to scrape comic", error); + throw new ComiXedControllerException("Failed to scrape comic", error); } } } diff --git a/comixed-rest-api/src/test/java/org/comixed/controller/tasks/TaskControllerTest.java b/comixed-rest-api/src/test/java/org/comixed/controller/tasks/TaskControllerTest.java new file mode 100644 index 000000000..9e288f65b --- /dev/null +++ b/comixed-rest-api/src/test/java/org/comixed/controller/tasks/TaskControllerTest.java @@ -0,0 +1,61 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixed.controller.tasks; + +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertSame; + +import java.util.Date; +import java.util.List; +import org.comixed.controller.ComiXedControllerException; +import org.comixed.model.tasks.TaskAuditLogEntry; +import org.comixed.service.ComiXedServiceException; +import org.comixed.service.task.TaskService; +import org.comixed.service.user.UserService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class TaskControllerTest { + private static final Date TEST_LAST_UPDATED_DATE = new Date(); + private static final String TEST_USER_EMAIL = "user@domain.tld"; + + @InjectMocks private TaskController taskController; + @Mock private UserService userService; + @Mock private TaskService taskService; + @Mock private List auditLogEntries; + + @Test + public void testGetAllEntries() throws ComiXedServiceException, ComiXedControllerException { + Mockito.when(taskService.getAuditLogEntriesAfter(Mockito.any(Date.class))) + .thenReturn(auditLogEntries); + + final List result = + taskController.getAllAfterDate(TEST_LAST_UPDATED_DATE.getTime()); + + assertNotNull(result); + assertSame(auditLogEntries, result); + + Mockito.verify(taskService, Mockito.times(1)).getAuditLogEntriesAfter(TEST_LAST_UPDATED_DATE); + } +} diff --git a/comixed-rest-api/src/test/java/org/comixedproject/controller/scraping/ComicVineScraperControllerTest.java b/comixed-rest-api/src/test/java/org/comixedproject/controller/scraping/ComicVineScraperControllerTest.java index 15b830af3..4b0f0b6ea 100644 --- a/comixed-rest-api/src/test/java/org/comixedproject/controller/scraping/ComicVineScraperControllerTest.java +++ b/comixed-rest-api/src/test/java/org/comixedproject/controller/scraping/ComicVineScraperControllerTest.java @@ -59,8 +59,9 @@ public class ComicVineScraperControllerTest { @Mock private ComicService comicService; @Mock private Comic comic; - @Test(expected = RESTException.class) - public void testQueryForVolumesAdaptorRaisesException() throws ScrapingException, RESTException { + @Test(expected = ComiXedControllerException.class) + public void testQueryForVolumesAdaptorRaisesException() + throws ScrapingException, ComiXedControllerException { Mockito.when( scrapingAdaptor.getVolumes( Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean())) @@ -75,7 +76,7 @@ public void testQueryForVolumesAdaptorRaisesException() throws ScrapingException } @Test - public void testQueryForVolumes() throws RESTException, ScrapingException { + public void testQueryForVolumes() throws ComiXedControllerException, ScrapingException { Mockito.when( scrapingAdaptor.getVolumes( Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean())) @@ -91,7 +92,7 @@ public void testQueryForVolumes() throws RESTException, ScrapingException { } @Test - public void testQueryForVolumesSkipCache() throws ScrapingException, RESTException { + public void testQueryForVolumesSkipCache() throws ScrapingException, ComiXedControllerException { Mockito.when( scrapingAdaptor.getVolumes( Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean())) @@ -106,8 +107,9 @@ public void testQueryForVolumesSkipCache() throws ScrapingException, RESTExcepti .getVolumes(TEST_API_KEY, TEST_SERIES_NAME, true); } - @Test(expected = RESTException.class) - public void testQueryForIssueAdaptorRaisesException() throws ScrapingException, RESTException { + @Test(expected = ComiXedControllerException.class) + public void testQueryForIssueAdaptorRaisesException() + throws ScrapingException, ComiXedControllerException { Mockito.when( scrapingAdaptor.getIssue( Mockito.anyString(), Mockito.anyInt(), Mockito.anyString(), Mockito.anyBoolean())) @@ -124,7 +126,7 @@ public void testQueryForIssueAdaptorRaisesException() throws ScrapingException, } @Test - public void testQueryForIssue() throws ScrapingException, RESTException { + public void testQueryForIssue() throws ScrapingException, ComiXedControllerException { Mockito.when( scrapingAdaptor.getIssue( Mockito.anyString(), Mockito.anyInt(), Mockito.anyString(), Mockito.anyBoolean())) @@ -142,8 +144,9 @@ public void testQueryForIssue() throws ScrapingException, RESTException { .getIssue(TEST_API_KEY, TEST_VOLUME, TEST_ISSUE_NUMBER, TEST_SKIP_CACHE); } - @Test(expected = RESTException.class) - public void testScrapeAndSaveComicDetailsNoSuchComic() throws ComicException, RESTException { + @Test(expected = ComiXedControllerException.class) + public void testScrapeAndSaveComicDetailsNoSuchComic() + throws ComicException, ComiXedControllerException { Mockito.when(comicService.getComic(Mockito.anyLong())).thenThrow(ComicException.class); try { @@ -154,9 +157,9 @@ public void testScrapeAndSaveComicDetailsNoSuchComic() throws ComicException, RE } } - @Test(expected = RESTException.class) + @Test(expected = ComiXedControllerException.class) public void testScrapeAndSaveComicDetailsScrapingAdaptorRaisesException() - throws ComicException, ScrapingException, RESTException { + throws ComicException, ScrapingException, ComiXedControllerException { Mockito.when(comicService.getComic(Mockito.anyLong())).thenReturn(comic); Mockito.doThrow(ScrapingException.class) .when(scrapingAdaptor) @@ -177,7 +180,7 @@ public void testScrapeAndSaveComicDetailsScrapingAdaptorRaisesException() @Test public void testScrapeAndSaveComicDetails() - throws ComicException, ScrapingException, RESTException { + throws ComicException, ScrapingException, ComiXedControllerException { Mockito.when(comicService.getComic(Mockito.anyLong())).thenReturn(comic); Comic result = diff --git a/comixed-services/src/main/java/org/comixed/service/ComiXedServiceException.java b/comixed-services/src/main/java/org/comixed/service/ComiXedServiceException.java new file mode 100644 index 000000000..fb3e6e3f3 --- /dev/null +++ b/comixed-services/src/main/java/org/comixed/service/ComiXedServiceException.java @@ -0,0 +1,30 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixed.service; + +/** + * ComiXedServiceException is thrown when a service-level exception occurs. + * + * @author Darryl L. Pierce + */ +public class ComiXedServiceException extends Exception { + public ComiXedServiceException(final String message, final InterruptedException cause) { + super(message, cause); + } +} diff --git a/comixed-services/src/main/java/org/comixedproject/service/task/TaskService.java b/comixed-services/src/main/java/org/comixedproject/service/task/TaskService.java index 7454236cb..2bb45c92a 100644 --- a/comixed-services/src/main/java/org/comixedproject/service/task/TaskService.java +++ b/comixed-services/src/main/java/org/comixedproject/service/task/TaskService.java @@ -18,6 +18,8 @@ package org.comixedproject.service.task; +import java.util.Date; +import java.util.List; import lombok.extern.log4j.Log4j2; import org.comixedproject.model.tasks.TaskType; import org.comixedproject.repositories.tasks.TaskRepository; @@ -32,7 +34,10 @@ @Service @Log4j2 public class TaskService { + private static final Object SEMAPHORE = new Object(); + @Autowired private TaskRepository taskRepository; + @Autowired private TaskAuditLogRepository taskAuditLogRepository; /** * Returns the current number of records with the given task type. @@ -45,4 +50,39 @@ public int getTaskCount(final TaskType taskType) { log.debug("Found {} instance{} of {}", result, result == 1 ? "" : "s", taskType); return result; } + + /** + * Return all log entries after the cutoff timestamp. + * + * @param cutoff the cutoff + * @return the log entries + * @throws ComiXedServiceException if an error occurs + */ + public List getAuditLogEntriesAfter(final Date cutoff) + throws ComiXedServiceException { + log.debug("Finding task audit log entries aftrr date: {}", cutoff); + + List result = null; + boolean done = false; + long started = System.currentTimeMillis(); + while (!done) { + result = this.taskAuditLogRepository.findAllByStartTimeGreaterThan(cutoff); + + if (result.isEmpty()) { + log.debug("Waiting for task audit log entries"); + synchronized (SEMAPHORE) { + try { + SEMAPHORE.wait(1000L); + } catch (InterruptedException error) { + log.debug("Interrupted while getting task audit log entries", error); + Thread.currentThread().interrupt(); + } + } + } + + done = !result.isEmpty() || (System.currentTimeMillis() - started > 60000); + } + + return result; + } } diff --git a/comixed-services/src/test/java/org/comixedproject/service/task/TaskServiceTest.java b/comixed-services/src/test/java/org/comixedproject/service/task/TaskServiceTest.java index ec53a119c..2a543f062 100644 --- a/comixed-services/src/test/java/org/comixedproject/service/task/TaskServiceTest.java +++ b/comixed-services/src/test/java/org/comixedproject/service/task/TaskServiceTest.java @@ -18,7 +18,7 @@ package org.comixedproject.service.task; -import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.*; import org.comixedproject.model.tasks.TaskType; import org.comixedproject.repositories.tasks.TaskRepository; @@ -32,17 +32,35 @@ @RunWith(MockitoJUnitRunner.class) public class TaskServiceTest { private static final int TEST_TASK_COUNT = 279; + private static final Date TEST_AUDIT_LOG_CUTOFF_DATE = new Date(); - @InjectMocks private TaskService service; + @InjectMocks private TaskService taskService; @Mock private TaskRepository taskRepository; + @Mock private TaskAuditLogRepository taskAuditLogRepository; + @Mock private List auditLogEntryList; @Test public void testGetTaskCount() { Mockito.when(taskRepository.getTaskCount(Mockito.any(TaskType.class))) .thenReturn(TEST_TASK_COUNT); - final int result = service.getTaskCount(TaskType.ADD_COMIC); + final int result = taskService.getTaskCount(TaskType.ADD_COMIC); assertEquals(TEST_TASK_COUNT, result); } + + @Test + public void testGetAuditLogEntriesAfter() throws ComiXedServiceException { + Mockito.when(taskAuditLogRepository.findAllByStartTimeGreaterThan(Mockito.any(Date.class))) + .thenReturn(auditLogEntryList); + + final List result = + taskService.getAuditLogEntriesAfter(TEST_AUDIT_LOG_CUTOFF_DATE); + + assertNotNull(result); + assertSame(auditLogEntryList, result); + + Mockito.verify(taskAuditLogRepository, Mockito.times(1)) + .findAllByStartTimeGreaterThan(TEST_AUDIT_LOG_CUTOFF_DATE); + } }