Skip to content

Commit 8b88e70

Browse files
committed
Changed comic file selection to be stored server-side [#2484]
1 parent 091b818 commit 8b88e70

File tree

27 files changed

+568
-305
lines changed

27 files changed

+568
-305
lines changed

comixed-model/src/main/java/org/comixedproject/model/comicfiles/ComicFile.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.fasterxml.jackson.annotation.JsonProperty;
2222
import com.fasterxml.jackson.annotation.JsonView;
2323
import lombok.Getter;
24+
import lombok.Setter;
2425
import org.apache.commons.io.FilenameUtils;
2526
import org.comixedproject.views.View;
2627

@@ -52,6 +53,12 @@ public class ComicFile {
5253
@Getter
5354
private long size;
5455

56+
@JsonProperty("selected")
57+
@JsonView(View.ComicFileList.class)
58+
@Getter
59+
@Setter
60+
private boolean selected = false;
61+
5562
public ComicFile(final String filename, final long size) {
5663
this.filename = filename.replace("\\", "/");
5764
this.baseFilename = FilenameUtils.getName(this.filename);

comixed-model/src/main/java/org/comixedproject/model/comicfiles/ComicFileGroup.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.ArrayList;
2424
import java.util.List;
2525
import lombok.Getter;
26+
import lombok.NoArgsConstructor;
2627
import lombok.NonNull;
2728
import lombok.RequiredArgsConstructor;
2829
import org.comixedproject.views.View;
@@ -34,6 +35,7 @@
3435
* @author Darryl L. Pierce
3536
*/
3637
@RequiredArgsConstructor
38+
@NoArgsConstructor
3739
public class ComicFileGroup {
3840
@JsonProperty("directory")
3941
@JsonView(View.ComicFileList.class)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* ComiXed - A digital comic book library management application.
3+
* Copyright (C) 2025, The ComiXed Project
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses>
17+
*/
18+
19+
package org.comixedproject.model.net.comicfiles;
20+
21+
import com.fasterxml.jackson.annotation.JsonProperty;
22+
import lombok.AllArgsConstructor;
23+
import lombok.Getter;
24+
import lombok.NoArgsConstructor;
25+
26+
/**
27+
* <code>ToggleComicFileSelectionsRequest</code> contains the request body when toggling comic file
28+
* selections.
29+
*
30+
* @author Darryl L. Pierce
31+
*/
32+
@NoArgsConstructor
33+
@AllArgsConstructor
34+
public class ToggleComicFileSelectionsRequest {
35+
@JsonProperty("filename")
36+
@Getter
37+
private String filename;
38+
39+
@JsonProperty("selected")
40+
@Getter
41+
private boolean selected;
42+
}

comixed-rest/src/main/java/org/comixedproject/rest/comicfiles/ComicFileController.java

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import com.fasterxml.jackson.annotation.JsonView;
2222
import com.fasterxml.jackson.core.JsonProcessingException;
23+
import com.fasterxml.jackson.core.type.TypeReference;
2324
import com.fasterxml.jackson.databind.ObjectMapper;
2425
import io.micrometer.core.annotation.Timed;
2526
import jakarta.servlet.http.HttpSession;
@@ -69,20 +70,17 @@ public class ComicFileController {
6970
@GetMapping(value = "/api/files/session", produces = MediaType.APPLICATION_JSON_VALUE)
7071
@Timed(value = "comixed.comic-file.load-comic-files-session")
7172
@JsonView(View.ComicFileList.class)
72-
public LoadComicFilesResponse loadComicFilesFromSession(final HttpSession session) {
73+
public LoadComicFilesResponse loadComicFilesFromSession(final HttpSession session)
74+
throws JsonProcessingException {
7375
log.info("Loading comic files from user session");
74-
final Object encodedComicFiles = session.getAttribute(COMIC_FILES);
75-
if (Objects.isNull(encodedComicFiles)) {
76-
log.debug("No comic files found in session");
76+
if (session.getAttribute(COMIC_FILES) == null) {
7777
return null;
7878
}
79-
try {
80-
return new LoadComicFilesResponse(
81-
this.objectMapper.readValue(encodedComicFiles.toString(), List.class));
82-
} catch (JsonProcessingException error) {
83-
log.error("Failed to parse comic files from session", error);
79+
final List<ComicFileGroup> comicFiles = this.doLoadComicFileSelections(session);
80+
if (Objects.isNull(comicFiles)) {
8481
return null;
8582
}
83+
return new LoadComicFilesResponse(comicFiles);
8684
}
8785

8886
/**
@@ -152,6 +150,40 @@ public byte[] getImportFileCover(@RequestParam("filename") String filename) {
152150
return result;
153151
}
154152

153+
/**
154+
* Toggles comic file selections.
155+
*
156+
* @param session the session
157+
* @param request the request
158+
* @return the comic file groups
159+
* @throws JsonProcessingException if an error occurs updating the session
160+
*/
161+
@PostMapping(
162+
value = "/api/files/import/selections",
163+
produces = MediaType.APPLICATION_JSON_VALUE,
164+
consumes = MediaType.APPLICATION_JSON_VALUE)
165+
@PreAuthorize("hasRole('ADMIN')")
166+
@Timed(value = "comixed.comic-file.selection-toggle")
167+
@JsonView(View.ComicFileList.class)
168+
public LoadComicFilesResponse toggleComicFileSelections(
169+
final HttpSession session, @RequestBody final ToggleComicFileSelectionsRequest request)
170+
throws JsonProcessingException {
171+
final List<ComicFileGroup> comicFiles = this.doLoadComicFileSelections(session);
172+
if (Objects.nonNull(comicFiles)) {
173+
final String filename = request.getFilename();
174+
final boolean selected = request.isSelected();
175+
176+
log.info("Toggling comic files selections: filename={}, selected={}", filename, selected);
177+
this.comicFileService.toggleComicFileSelections(comicFiles, filename, selected);
178+
log.debug("Updating comic files in session");
179+
session.setAttribute(COMIC_FILES, this.objectMapper.writeValueAsString(comicFiles));
180+
return new LoadComicFilesResponse(comicFiles);
181+
} else {
182+
log.info("No comic files to toggle...");
183+
return null;
184+
}
185+
}
186+
155187
/**
156188
* Begins the process of enqueueing comic files for import.
157189
*
@@ -202,4 +234,15 @@ public FilenameMetadataResponse scrapeFilename(
202234
return new FilenameMetadataResponse(
203235
info.isFound(), info.getSeries(), info.getVolume(), info.getIssueNumber());
204236
}
237+
238+
private List<ComicFileGroup> doLoadComicFileSelections(final HttpSession session)
239+
throws JsonProcessingException {
240+
final Object encodedComicFiles = session.getAttribute(COMIC_FILES);
241+
if (Objects.isNull(encodedComicFiles)) {
242+
log.debug("No comic files found in session");
243+
return null;
244+
}
245+
return this.objectMapper.readValue(
246+
encodedComicFiles.toString(), new TypeReference<List<ComicFileGroup>>() {});
247+
}
205248
}

comixed-rest/src/test/java/org/comixedproject/rest/comicfiles/ComicFileControllerTest.java

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,23 @@
2222
import static org.junit.Assert.*;
2323

2424
import com.fasterxml.jackson.core.JsonProcessingException;
25+
import com.fasterxml.jackson.core.type.TypeReference;
2526
import com.fasterxml.jackson.databind.ObjectMapper;
2627
import jakarta.servlet.http.HttpSession;
2728
import java.io.IOException;
2829
import java.util.List;
2930
import java.util.Random;
3031
import org.apache.commons.lang.math.RandomUtils;
3132
import org.comixedproject.adaptors.AdaptorException;
33+
import org.comixedproject.model.comicfiles.ComicFile;
3234
import org.comixedproject.model.comicfiles.ComicFileGroup;
3335
import org.comixedproject.model.metadata.FilenameMetadata;
3436
import org.comixedproject.model.net.comicfiles.*;
3537
import org.comixedproject.service.comicfiles.ComicFileService;
3638
import org.comixedproject.service.metadata.FilenameScrapingRuleService;
3739
import org.junit.jupiter.api.Test;
3840
import org.junit.jupiter.api.extension.ExtendWith;
39-
import org.mockito.InjectMocks;
40-
import org.mockito.Mock;
41-
import org.mockito.Mockito;
41+
import org.mockito.*;
4242
import org.mockito.junit.jupiter.MockitoExtension;
4343
import org.springframework.batch.core.JobParametersInvalidException;
4444
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
@@ -60,6 +60,7 @@ class ComicFileControllerTest {
6060
private static final Boolean TEST_SKIP_METADATA = RandomUtils.nextBoolean();
6161
private static final Boolean TEST_SKIP_BLOCKING_PAGES = RandomUtils.nextBoolean();
6262
private static final String TEST_ENCODED_COMIC_FILES = "The encoded comic file list";
63+
private static final boolean TEST_SELECTED = RandomUtils.nextBoolean();
6364

6465
@InjectMocks private ComicFileController controller;
6566
@Mock private ComicFileService comicFileService;
@@ -70,19 +71,25 @@ class ComicFileControllerTest {
7071
@Mock private FilenameMetadata filenameMetadata;
7172
@Mock private HttpSession session;
7273

74+
@Captor private ArgumentCaptor<TypeReference> typeReferenceArgumentCaptor;
75+
7376
@Test
7477
void loadComicFilesFromSession() throws JsonProcessingException {
7578
Mockito.when(session.getAttribute(COMIC_FILES)).thenReturn(TEST_ENCODED_COMIC_FILES);
76-
Mockito.when(objectMapper.readValue(Mockito.anyString(), Mockito.any(Class.class)))
79+
Mockito.when(objectMapper.readValue(Mockito.anyString(), typeReferenceArgumentCaptor.capture()))
7780
.thenReturn(comicFileGroupList);
7881

7982
final LoadComicFilesResponse result = controller.loadComicFilesFromSession(session);
8083

8184
assertNotNull(result);
8285
assertSame(comicFileGroupList, result.getGroups());
8386

84-
Mockito.verify(session, Mockito.times(1)).getAttribute(COMIC_FILES);
85-
Mockito.verify(objectMapper, Mockito.times(1)).readValue(TEST_ENCODED_COMIC_FILES, List.class);
87+
final TypeReference<ComicFile> typeReference = typeReferenceArgumentCaptor.getValue();
88+
assertNotNull(typeReference);
89+
90+
Mockito.verify(session, Mockito.times(2)).getAttribute(COMIC_FILES);
91+
Mockito.verify(objectMapper, Mockito.times(1))
92+
.readValue(TEST_ENCODED_COMIC_FILES, typeReference);
8693
}
8794

8895
@Test
@@ -101,15 +108,23 @@ void loadComicFilesFromSession_nothingStored() throws JsonProcessingException {
101108
@Test
102109
void loadComicFilesFromSession_parsingError() throws JsonProcessingException {
103110
Mockito.when(session.getAttribute(COMIC_FILES)).thenReturn(TEST_ENCODED_COMIC_FILES);
104-
Mockito.when(objectMapper.readValue(Mockito.anyString(), Mockito.any(Class.class)))
111+
Mockito.when(objectMapper.readValue(Mockito.anyString(), typeReferenceArgumentCaptor.capture()))
105112
.thenThrow(JsonProcessingException.class);
106113

107-
final LoadComicFilesResponse result = controller.loadComicFilesFromSession(session);
114+
assertThrows(
115+
JsonProcessingException.class,
116+
() -> {
117+
final LoadComicFilesResponse result = controller.loadComicFilesFromSession(session);
108118

109-
assertNull(result);
119+
assertNull(result);
110120

111-
Mockito.verify(session, Mockito.times(1)).getAttribute(COMIC_FILES);
112-
Mockito.verify(objectMapper, Mockito.times(1)).readValue(TEST_ENCODED_COMIC_FILES, List.class);
121+
final TypeReference<ComicFile> typeReference = typeReferenceArgumentCaptor.getValue();
122+
assertNotNull(typeReference);
123+
124+
Mockito.verify(session, Mockito.times(1)).getAttribute(COMIC_FILES);
125+
Mockito.verify(objectMapper, Mockito.times(1))
126+
.readValue(TEST_ENCODED_COMIC_FILES, typeReference);
127+
});
113128
}
114129

115130
@Test
@@ -137,6 +152,30 @@ void getImportFileCover() throws AdaptorException {
137152
Mockito.verify(comicFileService, Mockito.times(1)).getImportFileCover(COMIC_ARCHIVE);
138153
}
139154

155+
@Test
156+
void toggleComicFileSelections() throws JsonProcessingException {
157+
Mockito.when(session.getAttribute(COMIC_FILES)).thenReturn(TEST_ENCODED_COMIC_FILES);
158+
Mockito.when(objectMapper.readValue(Mockito.anyString(), typeReferenceArgumentCaptor.capture()))
159+
.thenReturn(comicFileGroupList);
160+
161+
Mockito.doNothing()
162+
.when(comicFileService)
163+
.toggleComicFileSelections(Mockito.anyList(), Mockito.anyString(), Mockito.anyBoolean());
164+
165+
final LoadComicFilesResponse result =
166+
controller.toggleComicFileSelections(
167+
session, new ToggleComicFileSelectionsRequest("", TEST_SELECTED));
168+
169+
assertNotNull(result);
170+
assertSame(comicFileGroupList, result.getGroups());
171+
172+
final TypeReference typeReference = typeReferenceArgumentCaptor.getValue();
173+
assertNotNull(typeReference);
174+
175+
Mockito.verify(comicFileService, Mockito.times(1))
176+
.toggleComicFileSelections(comicFileGroupList, "", TEST_SELECTED);
177+
}
178+
140179
@Test
141180
void loadComicFiles_noLimit() throws IOException {
142181
Mockito.when(comicFileService.getAllComicsUnder(Mockito.anyString(), Mockito.anyInt()))

comixed-services/src/main/java/org/comixedproject/service/comicfiles/ComicFileService.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Optional;
2626
import lombok.extern.log4j.Log4j2;
2727
import org.apache.commons.io.FilenameUtils;
28+
import org.apache.commons.lang3.StringUtils;
2829
import org.comixedproject.adaptors.AdaptorException;
2930
import org.comixedproject.adaptors.comicbooks.ComicBookAdaptor;
3031
import org.comixedproject.adaptors.comicbooks.ComicFileAdaptor;
@@ -187,4 +188,27 @@ public void importComicFiles(final List<String> filenames) {
187188
log.debug("Initiating processing");
188189
this.applicationEventPublisher.publishEvent(LoadComicBooksEvent.instance);
189190
}
191+
192+
/**
193+
* Toggles the selected state for comic files.
194+
*
195+
* @param groups the comic file groups
196+
* @param filename the filename
197+
* @param selected the selected state
198+
*/
199+
public void toggleComicFileSelections(
200+
final List<ComicFileGroup> groups, final String filename, final boolean selected) {
201+
if (StringUtils.isBlank(filename)) {
202+
log.debug("Toggling all comic files: selected={}", selected);
203+
groups.forEach(group -> group.getFiles().forEach(file -> file.setSelected(selected)));
204+
} else {
205+
log.debug("Toggling file: {} selected={}", filename, selected);
206+
groups.forEach(
207+
group ->
208+
group.getFiles().stream()
209+
.filter(file -> file.getFilename().equals(filename))
210+
.findFirst()
211+
.ifPresent(file -> file.setSelected(selected)));
212+
}
213+
}
190214
}

0 commit comments

Comments
 (0)