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);
+ }
}