Skip to content

Commit

Permalink
Added grouping comic files when retrieved for import [#41]
Browse files Browse the repository at this point in the history
  • Loading branch information
mcpierce committed Oct 27, 2021
1 parent c521304 commit fa8e8b8
Show file tree
Hide file tree
Showing 21 changed files with 274 additions and 70 deletions.
@@ -0,0 +1,48 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2021, 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 <http://www.gnu.org/licenses>
*/

package org.comixedproject.model.comicfiles;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonView;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.comixedproject.views.View;

/**
* <code>ComicFileGroup</code> holds a list of {@link ComicFile} instances that exist in the same
* directory.
*
* @author Darryl L. Pierce
*/
@RequiredArgsConstructor
public class ComicFileGroup {
@JsonProperty("directory")
@JsonView(View.ComicFileList.class)
@NonNull
@Getter
private String directory;

@JsonProperty("files")
@JsonView(View.ComicFileList.class)
@Getter
private List<ComicFile> files = new ArrayList<>();
}
Expand Up @@ -25,14 +25,14 @@
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.comixedproject.model.comicfiles.ComicFile;
import org.comixedproject.model.comicfiles.ComicFileGroup;
import org.comixedproject.views.View;

@AllArgsConstructor
public class LoadComicFilesResponse {
@JsonProperty("files")
@JsonProperty("groups")
@Getter
@Setter
@JsonView(View.ComicFileList.class)
private List<ComicFile> files = new ArrayList<>();
private List<ComicFileGroup> groups = new ArrayList<>();
}
Expand Up @@ -24,7 +24,7 @@
import java.util.List;
import java.util.Random;
import org.comixedproject.adaptors.AdaptorException;
import org.comixedproject.model.comicfiles.ComicFile;
import org.comixedproject.model.comicfiles.ComicFileGroup;
import org.comixedproject.model.net.GetAllComicsUnderRequest;
import org.comixedproject.model.net.ImportComicFilesRequest;
import org.comixedproject.model.net.comicfiles.FilenameMetadataRequest;
Expand Down Expand Up @@ -66,7 +66,7 @@ public class ComicFileControllerTest {
@Mock private FilenameScrapingRuleService filenameScrapingRuleService;
@Mock private Job addComicsToLibraryJob;
@Mock private JobLauncher jobLauncher;
@Mock private List<ComicFile> comicFileList;
@Mock private List<ComicFileGroup> comicFileGroupList;
@Mock private List<String> filenameList;
@Mock private JobExecution jobExecution;
@Mock private FilenameMetadata filenameMetadata;
Expand Down Expand Up @@ -101,13 +101,13 @@ public void testGetImportFileCover() throws AdaptorException {
@Test
public void testGetAllComicsUnderNoLimit() throws IOException {
Mockito.when(comicFileService.getAllComicsUnder(Mockito.anyString(), Mockito.anyInt()))
.thenReturn(comicFileList);
.thenReturn(comicFileGroupList);

final LoadComicFilesResponse response =
controller.loadComicFiles(new GetAllComicsUnderRequest(TEST_DIRECTORY, TEST_NO_LIMIT));

assertNotNull(response);
assertSame(comicFileList, response.getFiles());
assertSame(comicFileGroupList, response.getGroups());

Mockito.verify(comicFileService, Mockito.times(1))
.getAllComicsUnder(TEST_DIRECTORY, TEST_NO_LIMIT);
Expand All @@ -116,13 +116,13 @@ public void testGetAllComicsUnderNoLimit() throws IOException {
@Test
public void testGetAllComicsUnder() throws IOException {
Mockito.when(comicFileService.getAllComicsUnder(Mockito.anyString(), Mockito.anyInt()))
.thenReturn(comicFileList);
.thenReturn(comicFileGroupList);

final LoadComicFilesResponse response =
controller.loadComicFiles(new GetAllComicsUnderRequest(TEST_DIRECTORY, TEST_LIMIT));

assertNotNull(response);
assertSame(comicFileList, response.getFiles());
assertSame(comicFileGroupList, response.getGroups());

Mockito.verify(comicFileService, Mockito.times(1))
.getAllComicsUnder(TEST_DIRECTORY, TEST_LIMIT);
Expand Down
Expand Up @@ -22,15 +22,18 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.FilenameUtils;
import org.comixedproject.adaptors.AdaptorException;
import org.comixedproject.adaptors.comicbooks.ComicBookAdaptor;
import org.comixedproject.adaptors.comicbooks.ComicFileAdaptor;
import org.comixedproject.model.comicbooks.Comic;
import org.comixedproject.model.comicfiles.ComicFile;
import org.comixedproject.model.comicfiles.ComicFileDescriptor;
import org.comixedproject.repositories.comicbooks.ComicRepository;
import org.comixedproject.model.comicfiles.ComicFileGroup;
import org.comixedproject.repositories.comicfiles.ComicFileDescriptorRepository;
import org.comixedproject.service.comicbooks.ComicService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -45,7 +48,7 @@
@Log4j2
public class ComicFileService {
@Autowired private ComicBookAdaptor comicBookAdaptor;
@Autowired private ComicRepository comicRepository;
@Autowired private ComicService comicService;
@Autowired private ComicFileDescriptorRepository comicFileDescriptorRepository;
@Autowired private ComicFileAdaptor comicFileAdaptor;

Expand All @@ -54,12 +57,22 @@ public byte[] getImportFileCover(final String comicArchive) throws AdaptorExcept
return this.comicBookAdaptor.loadCover(comicArchive);
}

public List<ComicFile> getAllComicsUnder(final String rootDirectory, final Integer maximum)
/**
* Retrieves up to a maximum number of comic files below a given root directory, groups by
* absolute directory. Returns only files that have a comic extension and which do not already
* appear in the database.
*
* @param rootDirectory the root directory
* @param maximum the maximum number of files
* @return the comic files
* @throws IOException if an error occurs
*/
public List<ComicFileGroup> getAllComicsUnder(final String rootDirectory, final int maximum)
throws IOException {
log.debug("Getting comics below root: {}", rootDirectory);
log.debug("Getting {} comics below root: {}", maximum == 0 ? "all" : maximum, rootDirectory);

final File rootFile = new File(rootDirectory);
final List<ComicFile> result = new ArrayList<>();
final List<ComicFileGroup> result = new ArrayList<>();

if (rootFile.exists()) {
if (rootFile.isDirectory()) {
Expand All @@ -75,24 +88,44 @@ public List<ComicFile> getAllComicsUnder(final String rootDirectory, final Integ
}

private void loadFilesUnder(
final List<ComicFile> files, final File directory, final Integer maximum) throws IOException {
log.debug("Loading files in directory: {}", directory);
final List<ComicFileGroup> entries, final File directory, final int maximum)
throws IOException {
log.trace("Loading files in directory: {}", directory);
if (directory.listFiles() != null) {
for (File file : directory.listFiles()) {
if (maximum > 0 && files.size() == maximum) {
log.debug("Loading maximum comics: {}", maximum);
return;
if (!entries.isEmpty()) {
final int total =
entries.stream()
.map(comicFileGroup -> comicFileGroup.getFiles().size())
.reduce((runningTotal, entry) -> runningTotal += entry)
.get();
if (maximum > 0 && total == maximum) {
log.trace("Finished loading comics");
return;
}
}
if (file.isDirectory()) {
this.loadFilesUnder(files, file, maximum);
this.loadFilesUnder(entries, file, maximum);
} else {
if (canBeImported(file)) {
final String filePath = file.getCanonicalPath();
final long fileSize = file.length();

log.debug("Adding file: {} ({} bytes)", file.getAbsolutePath(), file.length());

files.add(new ComicFile(filePath, fileSize));
final String parentPath = FilenameUtils.getPath(filePath);
final Optional<ComicFileGroup> entry =
entries.stream()
.filter(comicFileGroup -> comicFileGroup.getDirectory().equals(parentPath))
.findFirst();
ComicFileGroup group = null;
if (entry.isPresent()) {
group = entry.get();
} else {
log.trace("Creating new grouping");
group = new ComicFileGroup(parentPath);
entries.add(group);
}
log.trace("Adding comic file");
group.getFiles().add(new ComicFile(filePath, fileSize));
}
}
}
Expand All @@ -103,7 +136,8 @@ private boolean canBeImported(final File file) throws IOException {
boolean isComic = this.comicFileAdaptor.isComicFile(file);

final String filePath = file.getCanonicalPath();
final Comic comic = this.comicRepository.findByFilename(filePath);
log.trace("Checking if comic file is already in the database");
final Comic comic = this.comicService.findByFilename(filePath);

return isComic && (comic == null);
}
Expand Down
Expand Up @@ -28,10 +28,10 @@
import org.comixedproject.adaptors.comicbooks.ComicBookAdaptor;
import org.comixedproject.adaptors.comicbooks.ComicFileAdaptor;
import org.comixedproject.model.comicbooks.Comic;
import org.comixedproject.model.comicfiles.ComicFile;
import org.comixedproject.model.comicfiles.ComicFileDescriptor;
import org.comixedproject.repositories.comicbooks.ComicRepository;
import org.comixedproject.model.comicfiles.ComicFileGroup;
import org.comixedproject.repositories.comicfiles.ComicFileDescriptorRepository;
import org.comixedproject.service.comicbooks.ComicService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
Expand All @@ -48,17 +48,18 @@ public class ComicFileServiceTest {
private static final String TEST_ROOT_DIRECTORY = "src/test/resources";
private static final String TEST_COMIC_ARCHIVE =
TEST_ROOT_DIRECTORY + "/" + TEST_ARCHIVE_FILENAME;
private static final Integer TEST_LIMIT = 2;
private static final Integer TEST_NO_LIMIT = -1;
private static final int TEST_LIMIT = 2;
private static final int TEST_NO_LIMIT = -1;

@InjectMocks private ComicFileService service;
@Mock private ComicBookAdaptor comicBookAdaptor;
@Mock private ComicFileAdaptor comicFileAdaptor;
@Mock private ComicRepository comicRepository;
@Mock private ComicService comicService;
@Mock private ComicFileDescriptorRepository comicFileDescriptorRepository;
@Mock private Comic comic;
@Mock private ComicFileDescriptor savedComicFileDescriptor;
@Mock private List<ComicFileDescriptor> comicFileDescriptors;
@Mock private ComicFileDescriptor comicFileDescriptor;

@Test
public void testGetImportFileCoverWithNoCover() throws AdaptorException {
Expand All @@ -83,7 +84,7 @@ public void testGetImportFileCover() throws AdaptorException {

@Test
public void testGetAllComicsUnderInvalidDirectory() throws IOException {
final List<ComicFile> result =
final List<ComicFileGroup> result =
service.getAllComicsUnder(TEST_ROOT_DIRECTORY + "/nonexistent", TEST_LIMIT);

assertNotNull(result);
Expand All @@ -92,7 +93,7 @@ public void testGetAllComicsUnderInvalidDirectory() throws IOException {

@Test
public void testGetAllComicsUnderWithFileSupplied() throws IOException {
final List<ComicFile> result = service.getAllComicsUnder(TEST_COMIC_ARCHIVE, TEST_LIMIT);
final List<ComicFileGroup> result = service.getAllComicsUnder(TEST_COMIC_ARCHIVE, TEST_LIMIT);

assertNotNull(result);
assertTrue(result.isEmpty());
Expand All @@ -101,37 +102,45 @@ public void testGetAllComicsUnderWithFileSupplied() throws IOException {
@Test
public void testGetAllComicsAlreadyImported() throws IOException {
Mockito.when(comicFileAdaptor.isComicFile(Mockito.any(File.class))).thenReturn(true);
Mockito.when(comicRepository.findByFilename(Mockito.anyString())).thenReturn(comic);
Mockito.when(comicService.findByFilename(Mockito.anyString())).thenReturn(comic);

final List<ComicFile> result = service.getAllComicsUnder(TEST_ROOT_DIRECTORY, TEST_LIMIT);
final List<ComicFileGroup> result = service.getAllComicsUnder(TEST_ROOT_DIRECTORY, TEST_LIMIT);

assertNotNull(result);
assertTrue(result.isEmpty());

Mockito.verify(comicRepository, Mockito.times(1))
Mockito.verify(comicService, Mockito.times(1))
.findByFilename(new File(TEST_COMIC_ARCHIVE).getCanonicalPath());
}

@Test
public void testGetAllComicsUnderWithLimit() throws IOException {
Mockito.when(comicFileAdaptor.isComicFile(Mockito.any(File.class))).thenReturn(true);

final List<ComicFile> result = service.getAllComicsUnder(TEST_ROOT_DIRECTORY, TEST_LIMIT);
final List<ComicFileGroup> result = service.getAllComicsUnder(TEST_ROOT_DIRECTORY, TEST_LIMIT);

assertNotNull(result);
assertFalse(result.isEmpty());
assertEquals(TEST_LIMIT.intValue(), result.size());
assertEquals(
TEST_LIMIT,
result.stream()
.map(comicFileGroup -> comicFileGroup.getFiles().size())
.reduce((sum, size) -> sum += size)
.get()
.intValue());
}

@Test
public void testGetAllComicsUnder() throws IOException {
Mockito.when(comicFileAdaptor.isComicFile(Mockito.any(File.class))).thenCallRealMethod();

final List<ComicFile> result = service.getAllComicsUnder(TEST_ROOT_DIRECTORY, TEST_NO_LIMIT);
final List<ComicFileGroup> result =
service.getAllComicsUnder(TEST_ROOT_DIRECTORY, TEST_NO_LIMIT);

assertNotNull(result);
assertFalse(result.isEmpty());
assertEquals(3, result.size());
assertEquals(1, result.size());
assertEquals(3, result.get(0).getFiles().size());
}

@Test
Expand Down Expand Up @@ -163,4 +172,11 @@ public void testFindComicFileDescriptors() {

Mockito.verify(comicFileDescriptorRepository, Mockito.times(1)).findAll();
}

@Test
public void testDeleteComicFileDescriptor() {
service.deleteComicFileDescriptor(comicFileDescriptor);

Mockito.verify(comicFileDescriptorRepository, Mockito.times(1)).delete(comicFileDescriptor);
}
}
Expand Up @@ -18,6 +18,7 @@

import { createAction, props } from '@ngrx/store';
import { ComicFile } from '@app/comic-files/models/comic-file';
import { ComicFileGroup } from '@app/comic-files/models/comic-file-group';

export const loadComicFiles = createAction(
'[Comic File List] Load comics in a file system',
Expand All @@ -26,7 +27,7 @@ export const loadComicFiles = createAction(

export const comicFilesLoaded = createAction(
'[Comic File List] Loaded comics in a file system',
props<{ files: ComicFile[] }>()
props<{ groups: ComicFileGroup[] }>()
);

export const loadComicFilesFailed = createAction(
Expand Down

0 comments on commit fa8e8b8

Please sign in to comment.