mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-13 11:05:03 +00:00
added unit tests
This commit is contained in:
parent
1490b29ec0
commit
f2468d26c9
@ -473,6 +473,7 @@ spotless {
|
||||
target sourceSets.main.allJava
|
||||
target project(':common').sourceSets.main.allJava
|
||||
target project(':proprietary').sourceSets.main.allJava
|
||||
target project(':stirling-pdf').sourceSets.main.allJava
|
||||
|
||||
googleJavaFormat("1.27.0").aosp().reorderImports(false)
|
||||
|
||||
@ -574,4 +575,4 @@ tasks.named('build') {
|
||||
doFirst {
|
||||
println "Delegating to :stirling-pdf:bootJar"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import io.github.pixee.security.SystemCommand;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@ -10,29 +8,22 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Properties;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
import org.springframework.core.Ordered;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@Lazy
|
||||
@ -257,7 +248,7 @@ public class AppConfig {
|
||||
return applicationProperties.getSystem().getDatasource();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Bean(name = "runningProOrHigher")
|
||||
@Profile("default")
|
||||
public boolean runningProOrHigher() {
|
||||
@ -275,14 +266,14 @@ public class AppConfig {
|
||||
public boolean googleDriveEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Bean(name = "license")
|
||||
@Profile("default")
|
||||
public String licenseType() {
|
||||
return "NORMAL";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Bean(name = "disablePixel")
|
||||
public boolean disablePixel() {
|
||||
return Boolean.parseBoolean(env.getProperty("DISABLE_PIXEL", "false"));
|
||||
|
@ -0,0 +1,83 @@
|
||||
package stirling.software.proprietary.security.service;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TeamServiceTest {
|
||||
|
||||
@Mock
|
||||
private TeamRepository teamRepository;
|
||||
|
||||
@InjectMocks
|
||||
private TeamService teamService;
|
||||
|
||||
@Test
|
||||
void getDefaultTeam() {
|
||||
var team = new Team();
|
||||
team.setName("Marleyans");
|
||||
|
||||
when(teamRepository.findByName(TeamService.DEFAULT_TEAM_NAME))
|
||||
.thenReturn(Optional.of(team));
|
||||
|
||||
Team result = teamService.getOrCreateDefaultTeam();
|
||||
|
||||
assertEquals(team, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createDefaultTeam_whenRepositoryIsEmpty() {
|
||||
String teamName = "Default";
|
||||
var defaultTeam = new Team();
|
||||
defaultTeam.setId(1L);
|
||||
defaultTeam.setName(teamName);
|
||||
|
||||
when(teamRepository.findByName(teamName))
|
||||
.thenReturn(Optional.empty());
|
||||
when(teamRepository.save(any(Team.class))).thenReturn(defaultTeam);
|
||||
|
||||
Team result = teamService.getOrCreateDefaultTeam();
|
||||
|
||||
assertEquals(TeamService.DEFAULT_TEAM_NAME, result.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getInternalTeam() {
|
||||
var team = new Team();
|
||||
team.setName("Eldians");
|
||||
|
||||
when(teamRepository.findByName(TeamService.INTERNAL_TEAM_NAME))
|
||||
.thenReturn(Optional.of(team));
|
||||
|
||||
Team result = teamService.getOrCreateInternalTeam();
|
||||
|
||||
assertEquals(team, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createInternalTeam_whenRepositoryIsEmpty() {
|
||||
String teamName = "Internal";
|
||||
Team internalTeam = new Team();
|
||||
internalTeam.setId(2L);
|
||||
internalTeam.setName(teamName);
|
||||
|
||||
when(teamRepository.findByName(teamName))
|
||||
.thenReturn(Optional.empty());
|
||||
when(teamRepository.save(any(Team.class))).thenReturn(internalTeam);
|
||||
when(teamRepository.findByName(TeamService.INTERNAL_TEAM_NAME))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
Team result = teamService.getOrCreateInternalTeam();
|
||||
|
||||
assertEquals(internalTeam, result);
|
||||
}
|
||||
}
|
@ -0,0 +1,317 @@
|
||||
package stirling.software.proprietary.security.service;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.database.repository.AuthorityRepository;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserServiceTest {
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Mock
|
||||
private TeamRepository teamRepository;
|
||||
|
||||
@Mock
|
||||
private AuthorityRepository authorityRepository;
|
||||
|
||||
@Mock
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Mock
|
||||
private MessageSource messageSource;
|
||||
|
||||
@Mock
|
||||
private SessionPersistentRegistry sessionPersistentRegistry;
|
||||
|
||||
@Mock
|
||||
private DatabaseServiceInterface databaseService;
|
||||
|
||||
@Mock
|
||||
private ApplicationProperties.Security.OAUTH2 oauth2Properties;
|
||||
|
||||
@InjectMocks
|
||||
private UserService userService;
|
||||
|
||||
private Team mockTeam;
|
||||
private User mockUser;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockTeam = new Team();
|
||||
mockTeam.setId(1L);
|
||||
mockTeam.setName("Test Team");
|
||||
|
||||
mockUser = new User();
|
||||
mockUser.setId(1L);
|
||||
mockUser.setUsername("testuser");
|
||||
mockUser.setEnabled(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveUser_WithUsernameAndAuthenticationType_Success() throws Exception {
|
||||
// Given
|
||||
String username = "testuser";
|
||||
AuthenticationType authType = AuthenticationType.WEB;
|
||||
|
||||
when(teamRepository.findByName("Default")).thenReturn(Optional.of(mockTeam));
|
||||
when(userRepository.save(any(User.class))).thenReturn(mockUser);
|
||||
doNothing().when(databaseService).exportDatabase();
|
||||
|
||||
// When
|
||||
userService.saveUser(username, authType);
|
||||
|
||||
// Then
|
||||
verify(userRepository).save(any(User.class));
|
||||
verify(databaseService).exportDatabase();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveUser_WithUsernamePasswordAndTeamId_Success() throws Exception {
|
||||
// Given
|
||||
String username = "testuser";
|
||||
String password = "password123";
|
||||
Long teamId = 1L;
|
||||
String encodedPassword = "encodedPassword123";
|
||||
|
||||
when(passwordEncoder.encode(password)).thenReturn(encodedPassword);
|
||||
when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam));
|
||||
when(userRepository.save(any(User.class))).thenReturn(mockUser);
|
||||
doNothing().when(databaseService).exportDatabase();
|
||||
|
||||
// When
|
||||
User result = userService.saveUser(username, password, teamId);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
verify(passwordEncoder).encode(password);
|
||||
verify(teamRepository).findById(teamId);
|
||||
verify(userRepository).save(any(User.class));
|
||||
verify(databaseService).exportDatabase();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveUser_WithTeamAndRole_Success() throws Exception {
|
||||
// Given
|
||||
String username = "testuser";
|
||||
String password = "password123";
|
||||
String role = Role.ADMIN.getRoleId();
|
||||
boolean firstLogin = true;
|
||||
String encodedPassword = "encodedPassword123";
|
||||
|
||||
when(passwordEncoder.encode(password)).thenReturn(encodedPassword);
|
||||
when(userRepository.save(any(User.class))).thenReturn(mockUser);
|
||||
doNothing().when(databaseService).exportDatabase();
|
||||
|
||||
// When
|
||||
User result = userService.saveUser(username, password, mockTeam, role, firstLogin);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
verify(passwordEncoder).encode(password);
|
||||
verify(userRepository).save(any(User.class));
|
||||
verify(databaseService).exportDatabase();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveUser_WithInvalidUsername_ThrowsException() throws Exception {
|
||||
// Given
|
||||
String invalidUsername = "ab"; // Too short (less than 3 characters)
|
||||
AuthenticationType authType = AuthenticationType.WEB;
|
||||
|
||||
// When & Then
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> userService.saveUser(invalidUsername, authType)
|
||||
);
|
||||
|
||||
verify(userRepository, never()).save(any(User.class));
|
||||
verify(databaseService, never()).exportDatabase();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveUser_WithNullPassword_Success() throws Exception {
|
||||
// Given
|
||||
String username = "testuser";
|
||||
Long teamId = 1L;
|
||||
|
||||
when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam));
|
||||
when(userRepository.save(any(User.class))).thenReturn(mockUser);
|
||||
doNothing().when(databaseService).exportDatabase();
|
||||
|
||||
// When
|
||||
User result = userService.saveUser(username, null, teamId);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
verify(passwordEncoder, never()).encode(anyString());
|
||||
verify(userRepository).save(any(User.class));
|
||||
verify(databaseService).exportDatabase();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveUser_WithEmptyPassword_Success() throws Exception {
|
||||
// Given
|
||||
String username = "testuser";
|
||||
String emptyPassword = "";
|
||||
Long teamId = 1L;
|
||||
|
||||
when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam));
|
||||
when(userRepository.save(any(User.class))).thenReturn(mockUser);
|
||||
doNothing().when(databaseService).exportDatabase();
|
||||
|
||||
// When
|
||||
User result = userService.saveUser(username, emptyPassword, teamId);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
verify(passwordEncoder, never()).encode(anyString());
|
||||
verify(userRepository).save(any(User.class));
|
||||
verify(databaseService).exportDatabase();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveUser_WithValidEmail_Success() throws Exception {
|
||||
// Given
|
||||
String emailUsername = "test@example.com";
|
||||
AuthenticationType authType = AuthenticationType.SSO;
|
||||
|
||||
when(teamRepository.findByName("Default")).thenReturn(Optional.of(mockTeam));
|
||||
when(userRepository.save(any(User.class))).thenReturn(mockUser);
|
||||
doNothing().when(databaseService).exportDatabase();
|
||||
|
||||
// When
|
||||
userService.saveUser(emailUsername, authType);
|
||||
|
||||
// Then
|
||||
verify(userRepository).save(any(User.class));
|
||||
verify(databaseService).exportDatabase();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveUser_WithReservedUsername_ThrowsException() throws Exception {
|
||||
// Given
|
||||
String reservedUsername = "all_users";
|
||||
AuthenticationType authType = AuthenticationType.WEB;
|
||||
|
||||
// When & Then
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> userService.saveUser(reservedUsername, authType)
|
||||
);
|
||||
|
||||
verify(userRepository, never()).save(any(User.class));
|
||||
verify(databaseService, never()).exportDatabase();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveUser_WithAnonymousUser_ThrowsException() throws Exception {
|
||||
// Given
|
||||
String anonymousUsername = "anonymoususer";
|
||||
AuthenticationType authType = AuthenticationType.WEB;
|
||||
|
||||
// When & Then
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> userService.saveUser(anonymousUsername, authType)
|
||||
);
|
||||
|
||||
verify(userRepository, never()).save(any(User.class));
|
||||
verify(databaseService, never()).exportDatabase();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveUser_DatabaseExportThrowsException_StillSavesUser() throws Exception {
|
||||
// Given
|
||||
String username = "testuser";
|
||||
String password = "password123";
|
||||
Long teamId = 1L;
|
||||
String encodedPassword = "encodedPassword123";
|
||||
|
||||
when(passwordEncoder.encode(password)).thenReturn(encodedPassword);
|
||||
when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam));
|
||||
when(userRepository.save(any(User.class))).thenReturn(mockUser);
|
||||
doThrow(new SQLException("Database export failed")).when(databaseService).exportDatabase();
|
||||
|
||||
// When & Then
|
||||
assertThrows(SQLException.class, () -> userService.saveUser(username, password, teamId));
|
||||
|
||||
// Verify user was still saved before the exception
|
||||
verify(userRepository).save(any(User.class));
|
||||
verify(databaseService).exportDatabase();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveUser_WithFirstLoginFlag_Success() throws Exception {
|
||||
// Given
|
||||
String username = "testuser";
|
||||
String password = "password123";
|
||||
Long teamId = 1L;
|
||||
boolean firstLogin = true;
|
||||
boolean enabled = false;
|
||||
String encodedPassword = "encodedPassword123";
|
||||
|
||||
when(passwordEncoder.encode(password)).thenReturn(encodedPassword);
|
||||
when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam));
|
||||
when(userRepository.save(any(User.class))).thenReturn(mockUser);
|
||||
doNothing().when(databaseService).exportDatabase();
|
||||
|
||||
// When
|
||||
userService.saveUser(username, password, teamId, firstLogin, enabled);
|
||||
|
||||
// Then
|
||||
verify(passwordEncoder).encode(password);
|
||||
verify(userRepository).save(any(User.class));
|
||||
verify(databaseService).exportDatabase();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveUser_WithCustomRole_Success() throws Exception {
|
||||
// Given
|
||||
String username = "testuser";
|
||||
String password = "password123";
|
||||
Long teamId = 1L;
|
||||
String customRole = Role.LIMITED_API_USER.getRoleId();
|
||||
String encodedPassword = "encodedPassword123";
|
||||
|
||||
when(passwordEncoder.encode(password)).thenReturn(encodedPassword);
|
||||
when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam));
|
||||
when(userRepository.save(any(User.class))).thenReturn(mockUser);
|
||||
doNothing().when(databaseService).exportDatabase();
|
||||
|
||||
// When
|
||||
userService.saveUser(username, password, teamId, customRole);
|
||||
|
||||
// Then
|
||||
verify(passwordEncoder).encode(password);
|
||||
verify(userRepository).save(any(User.class));
|
||||
verify(databaseService).exportDatabase();
|
||||
}
|
||||
|
||||
}
|
@ -13,8 +13,6 @@ import java.util.Properties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@ -210,7 +208,7 @@ public class SPDFApplication {
|
||||
if (arg.startsWith("--spring.profiles.active=")) {
|
||||
String[] provided = arg.substring(arg.indexOf('=') + 1).split(",");
|
||||
if (provided.length > 0) {
|
||||
log.info("#######0000000000000###############################");
|
||||
log.info("#######0000000000000###############################");
|
||||
return provided;
|
||||
}
|
||||
}
|
||||
@ -218,14 +216,16 @@ public class SPDFApplication {
|
||||
}
|
||||
log.info("######################################");
|
||||
// 2. Detect if SecurityConfiguration is present on classpath
|
||||
if (isClassPresent("stirling.software.proprietary.security.configuration.SecurityConfiguration")) {
|
||||
log.info("security");
|
||||
return new String[] { "security" };
|
||||
if (isClassPresent(
|
||||
"stirling.software.proprietary.security.configuration.SecurityConfiguration")) {
|
||||
log.info("security");
|
||||
return new String[] {"security"};
|
||||
} else {
|
||||
log.info("default");
|
||||
return new String[] { "default" };
|
||||
log.info("default");
|
||||
return new String[] {"default"};
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isClassPresent(String className) {
|
||||
try {
|
||||
Class.forName(className, false, SPDFApplication.class.getClassLoader());
|
||||
|
@ -80,4 +80,4 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||
HttpServletResponse response,
|
||||
Object handler,
|
||||
Exception ex) {}
|
||||
}
|
||||
}
|
||||
|
@ -209,4 +209,4 @@ public class EndpointInspector implements ApplicationListener<ContextRefreshedEv
|
||||
}
|
||||
logger.info("=== END: All discovered GET endpoints ===");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,4 +49,4 @@ public class EndpointInterceptor implements HandlerInterceptor {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -155,4 +155,4 @@ public class ExternalAppDepConfig {
|
||||
}
|
||||
endpointConfiguration.logDisabledEndpointsSummary();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,9 +25,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
// Handler for external static resources
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations(
|
||||
"file:" + InstallationPathConfig.getStaticPath(),
|
||||
"classpath:/static/"
|
||||
);
|
||||
"file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
|
||||
registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
|
||||
registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/");
|
||||
// .setCachePeriod(0); // Optional: disable caching
|
||||
|
@ -49,18 +49,18 @@ public class EditTableOfContentsController {
|
||||
summary = "Extract PDF Bookmarks",
|
||||
description = "Extracts bookmarks/table of contents from a PDF document as JSON.")
|
||||
@ResponseBody
|
||||
public List<Map<String, Object>> extractBookmarks(@RequestParam("file") MultipartFile file)
|
||||
public List<Map<String, Object>> extractBookmarks(@RequestParam("file") MultipartFile file)
|
||||
throws Exception {
|
||||
PDDocument document = null;
|
||||
try {
|
||||
document = pdfDocumentFactory.load(file);
|
||||
PDDocumentOutline outline = document.getDocumentCatalog().getDocumentOutline();
|
||||
|
||||
|
||||
if (outline == null) {
|
||||
log.info("No outline/bookmarks found in PDF");
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
|
||||
return extractBookmarkItems(document, outline);
|
||||
} finally {
|
||||
if (document != null) {
|
||||
@ -68,18 +68,19 @@ public class EditTableOfContentsController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> extractBookmarkItems(PDDocument document, PDDocumentOutline outline) throws Exception {
|
||||
|
||||
private List<Map<String, Object>> extractBookmarkItems(
|
||||
PDDocument document, PDDocumentOutline outline) throws Exception {
|
||||
List<Map<String, Object>> bookmarks = new ArrayList<>();
|
||||
PDOutlineItem current = outline.getFirstChild();
|
||||
|
||||
|
||||
while (current != null) {
|
||||
Map<String, Object> bookmark = new HashMap<>();
|
||||
|
||||
|
||||
// Get bookmark title
|
||||
String title = current.getTitle();
|
||||
bookmark.put("title", title);
|
||||
|
||||
|
||||
// Get page number (1-based for UI purposes)
|
||||
PDPage page = current.findDestinationPage(document);
|
||||
if (page != null) {
|
||||
@ -88,39 +89,40 @@ public class EditTableOfContentsController {
|
||||
} else {
|
||||
bookmark.put("pageNumber", 1);
|
||||
}
|
||||
|
||||
|
||||
// Process children if any
|
||||
PDOutlineItem child = current.getFirstChild();
|
||||
if (child != null) {
|
||||
List<Map<String, Object>> children = new ArrayList<>();
|
||||
PDOutlineNode parent = current;
|
||||
|
||||
|
||||
while (child != null) {
|
||||
// Recursively process child items
|
||||
Map<String, Object> childBookmark = processChild(document, child);
|
||||
children.add(childBookmark);
|
||||
child = child.getNextSibling();
|
||||
}
|
||||
|
||||
|
||||
bookmark.put("children", children);
|
||||
} else {
|
||||
bookmark.put("children", new ArrayList<>());
|
||||
}
|
||||
|
||||
|
||||
bookmarks.add(bookmark);
|
||||
current = current.getNextSibling();
|
||||
}
|
||||
|
||||
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
private Map<String, Object> processChild(PDDocument document, PDOutlineItem item) throws Exception {
|
||||
|
||||
private Map<String, Object> processChild(PDDocument document, PDOutlineItem item)
|
||||
throws Exception {
|
||||
Map<String, Object> bookmark = new HashMap<>();
|
||||
|
||||
|
||||
// Get bookmark title
|
||||
String title = item.getTitle();
|
||||
bookmark.put("title", title);
|
||||
|
||||
|
||||
// Get page number (1-based for UI purposes)
|
||||
PDPage page = item.findDestinationPage(document);
|
||||
if (page != null) {
|
||||
@ -129,92 +131,94 @@ public class EditTableOfContentsController {
|
||||
} else {
|
||||
bookmark.put("pageNumber", 1);
|
||||
}
|
||||
|
||||
|
||||
// Process children if any
|
||||
PDOutlineItem child = item.getFirstChild();
|
||||
if (child != null) {
|
||||
List<Map<String, Object>> children = new ArrayList<>();
|
||||
|
||||
|
||||
while (child != null) {
|
||||
// Recursively process child items
|
||||
Map<String, Object> childBookmark = processChild(document, child);
|
||||
children.add(childBookmark);
|
||||
child = child.getNextSibling();
|
||||
}
|
||||
|
||||
|
||||
bookmark.put("children", children);
|
||||
} else {
|
||||
bookmark.put("children", new ArrayList<>());
|
||||
}
|
||||
|
||||
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
|
||||
@PostMapping(value = "/edit-table-of-contents", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Edit Table of Contents",
|
||||
description = "Add or edit bookmarks/table of contents in a PDF document.")
|
||||
public ResponseEntity<byte[]> editTableOfContents(@ModelAttribute EditTableOfContentsRequest request)
|
||||
throws Exception {
|
||||
public ResponseEntity<byte[]> editTableOfContents(
|
||||
@ModelAttribute EditTableOfContentsRequest request) throws Exception {
|
||||
MultipartFile file = request.getFileInput();
|
||||
PDDocument document = null;
|
||||
|
||||
try {
|
||||
document = pdfDocumentFactory.load(file);
|
||||
|
||||
|
||||
// Parse the bookmark data from JSON
|
||||
List<BookmarkItem> bookmarks = objectMapper.readValue(
|
||||
request.getBookmarkData(),
|
||||
new TypeReference<List<BookmarkItem>>() {});
|
||||
|
||||
List<BookmarkItem> bookmarks =
|
||||
objectMapper.readValue(
|
||||
request.getBookmarkData(), new TypeReference<List<BookmarkItem>>() {});
|
||||
|
||||
// Create a new document outline
|
||||
PDDocumentOutline outline = new PDDocumentOutline();
|
||||
document.getDocumentCatalog().setDocumentOutline(outline);
|
||||
|
||||
|
||||
// Add bookmarks to the outline
|
||||
addBookmarksToOutline(document, outline, bookmarks);
|
||||
|
||||
|
||||
// Save the document to a byte array
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
|
||||
|
||||
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
baos.toByteArray(), filename + "_with_toc.pdf", MediaType.APPLICATION_PDF);
|
||||
|
||||
|
||||
} finally {
|
||||
if (document != null) {
|
||||
document.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addBookmarksToOutline(PDDocument document, PDDocumentOutline outline, List<BookmarkItem> bookmarks) {
|
||||
|
||||
private void addBookmarksToOutline(
|
||||
PDDocument document, PDDocumentOutline outline, List<BookmarkItem> bookmarks) {
|
||||
for (BookmarkItem bookmark : bookmarks) {
|
||||
PDOutlineItem item = createOutlineItem(document, bookmark);
|
||||
outline.addLast(item);
|
||||
|
||||
|
||||
if (bookmark.getChildren() != null && !bookmark.getChildren().isEmpty()) {
|
||||
addChildBookmarks(document, item, bookmark.getChildren());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addChildBookmarks(PDDocument document, PDOutlineItem parent, List<BookmarkItem> children) {
|
||||
|
||||
private void addChildBookmarks(
|
||||
PDDocument document, PDOutlineItem parent, List<BookmarkItem> children) {
|
||||
for (BookmarkItem child : children) {
|
||||
PDOutlineItem item = createOutlineItem(document, child);
|
||||
parent.addLast(item);
|
||||
|
||||
|
||||
if (child.getChildren() != null && !child.getChildren().isEmpty()) {
|
||||
addChildBookmarks(document, item, child.getChildren());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private PDOutlineItem createOutlineItem(PDDocument document, BookmarkItem bookmark) {
|
||||
PDOutlineItem item = new PDOutlineItem();
|
||||
item.setTitle(bookmark.getTitle());
|
||||
|
||||
|
||||
// Get the target page - adjust for 0-indexed pages in PDFBox
|
||||
int pageIndex = bookmark.getPageNumber() - 1;
|
||||
if (pageIndex < 0) {
|
||||
@ -222,41 +226,41 @@ public class EditTableOfContentsController {
|
||||
} else if (pageIndex >= document.getNumberOfPages()) {
|
||||
pageIndex = document.getNumberOfPages() - 1;
|
||||
}
|
||||
|
||||
|
||||
PDPage page = document.getPage(pageIndex);
|
||||
item.setDestination(page);
|
||||
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
// Inner class to represent bookmarks in JSON
|
||||
public static class BookmarkItem {
|
||||
private String title;
|
||||
private int pageNumber;
|
||||
private List<BookmarkItem> children = new ArrayList<>();
|
||||
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
|
||||
public int getPageNumber() {
|
||||
return pageNumber;
|
||||
}
|
||||
|
||||
|
||||
public void setPageNumber(int pageNumber) {
|
||||
this.pageNumber = pageNumber;
|
||||
}
|
||||
|
||||
|
||||
public List<BookmarkItem> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
|
||||
public void setChildren(List<BookmarkItem> children) {
|
||||
this.children = children;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,9 +117,9 @@ public class MergeController {
|
||||
// Create the document outline
|
||||
PDDocumentOutline outline = new PDDocumentOutline();
|
||||
mergedDocument.getDocumentCatalog().setDocumentOutline(outline);
|
||||
|
||||
|
||||
int pageIndex = 0; // Current page index in the merged document
|
||||
|
||||
|
||||
// Iterate through the original files
|
||||
for (MultipartFile file : files) {
|
||||
// Get the filename without extension to use as bookmark title
|
||||
@ -128,20 +128,20 @@ public class MergeController {
|
||||
if (title != null && title.contains(".")) {
|
||||
title = title.substring(0, title.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
|
||||
// Create an outline item for this file
|
||||
PDOutlineItem item = new PDOutlineItem();
|
||||
item.setTitle(title);
|
||||
|
||||
|
||||
// Set the destination to the first page of this file in the merged document
|
||||
if (pageIndex < mergedDocument.getNumberOfPages()) {
|
||||
PDPage page = mergedDocument.getPage(pageIndex);
|
||||
item.setDestination(page);
|
||||
}
|
||||
|
||||
|
||||
// Add the item to the outline
|
||||
outline.addLast(item);
|
||||
|
||||
|
||||
// Increment page index for the next file
|
||||
try (PDDocument doc = pdfDocumentFactory.load(file)) {
|
||||
pageIndex += doc.getNumberOfPages();
|
||||
@ -212,7 +212,7 @@ public class MergeController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add table of contents if generateToc is true
|
||||
if (generateToc && files.length > 0) {
|
||||
addTableOfContents(mergedDocument, files);
|
||||
@ -245,4 +245,4 @@ public class MergeController {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,4 +50,4 @@ public class SettingsController {
|
||||
public ResponseEntity<Map<String, Boolean>> getDisabledEndpoints() {
|
||||
return ResponseEntity.ok(endpointConfiguration.getEndpointStatuses());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,9 +27,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
|
@ -31,11 +31,11 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
||||
import stirling.software.common.model.PdfMetadata;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.service.PdfMetadataService;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
|
@ -31,9 +31,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
|
@ -16,8 +16,10 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@ -39,9 +41,9 @@ public class ConvertEmlToPDF {
|
||||
summary = "Convert EML to PDF",
|
||||
description =
|
||||
"This endpoint converts EML (email) files to PDF format with extensive"
|
||||
+ " customization options. Features include font settings, image constraints, display modes, attachment handling,"
|
||||
+ " and HTML debug output. Input: EML file, Output: PDF"
|
||||
+ " or HTML file. Type: SISO")
|
||||
+ " customization options. Features include font settings, image constraints, display modes, attachment handling,"
|
||||
+ " and HTML debug output. Input: EML file, Output: PDF"
|
||||
+ " or HTML file. Type: SISO")
|
||||
public ResponseEntity<byte[]> convertEmlToPdf(@ModelAttribute EmlToPdfRequest request) {
|
||||
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
@ -94,7 +96,8 @@ public class ConvertEmlToPDF {
|
||||
try {
|
||||
byte[] pdfBytes =
|
||||
EmlToPdf.convertEmlToPdf(
|
||||
runtimePathConfig.getWeasyPrintPath(), // Use configured WeasyPrint path
|
||||
runtimePathConfig
|
||||
.getWeasyPrintPath(), // Use configured WeasyPrint path
|
||||
request,
|
||||
fileBytes,
|
||||
originalFilename,
|
||||
@ -119,12 +122,20 @@ public class ConvertEmlToPDF {
|
||||
.body("Conversion was interrupted".getBytes(StandardCharsets.UTF_8));
|
||||
} catch (IllegalArgumentException e) {
|
||||
String errorMessage = buildErrorMessage(e, originalFilename);
|
||||
log.error("EML to PDF conversion failed for {}: {}", originalFilename, errorMessage, e);
|
||||
log.error(
|
||||
"EML to PDF conversion failed for {}: {}",
|
||||
originalFilename,
|
||||
errorMessage,
|
||||
e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(errorMessage.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (RuntimeException e) {
|
||||
String errorMessage = buildErrorMessage(e, originalFilename);
|
||||
log.error("EML to PDF conversion failed for {}: {}", originalFilename, errorMessage, e);
|
||||
log.error(
|
||||
"EML to PDF conversion failed for {}: {}",
|
||||
originalFilename,
|
||||
errorMessage,
|
||||
e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(errorMessage.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
@ -23,9 +23,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.model.api.GeneralFile;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.api.GeneralFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.FileToPdf;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
@ -875,4 +875,4 @@ public class CompressController {
|
||||
}
|
||||
return Math.min(9, currentLevel + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,8 +36,8 @@ import stirling.software.SPDF.model.PipelineConfig;
|
||||
import stirling.software.SPDF.model.PipelineOperation;
|
||||
import stirling.software.SPDF.model.PipelineResult;
|
||||
import stirling.software.SPDF.service.ApiDocService;
|
||||
import stirling.software.common.service.PostHogService;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.service.PostHogService;
|
||||
import stirling.software.common.util.FileMonitor;
|
||||
|
||||
@Service
|
||||
|
@ -184,7 +184,8 @@ public class RedactController {
|
||||
String pageNumbersInput = request.getPageNumbers();
|
||||
String[] parsedPageNumbers =
|
||||
pageNumbersInput != null ? pageNumbersInput.split(",") : new String[0];
|
||||
List<Integer> pageNumbers = GeneralUtils.parsePageList(parsedPageNumbers, pagesCount, false);
|
||||
List<Integer> pageNumbers =
|
||||
GeneralUtils.parsePageList(parsedPageNumbers, pagesCount, false);
|
||||
Collections.sort(pageNumbers);
|
||||
return pageNumbers;
|
||||
}
|
||||
|
@ -128,4 +128,4 @@ public class ConverterWebController {
|
||||
model.addAttribute("currentPage", "eml-to-pdf");
|
||||
return "convert/eml-to-pdf";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -136,7 +136,7 @@ public class GeneralWebController {
|
||||
model.addAttribute("currentPage", "edit-table-of-contents");
|
||||
return "edit-table-of-contents";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/multi-tool")
|
||||
@Hidden
|
||||
public String multiToolForm(Model model) {
|
||||
@ -350,4 +350,4 @@ public class GeneralWebController {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,8 +22,8 @@ import io.swagger.v3.oas.annotations.Hidden;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.Dependency;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@ -48,9 +48,7 @@ public class HomeWebController {
|
||||
InputStream is = resource.getInputStream();
|
||||
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
Map<String, List<Dependency>> data =
|
||||
mapper.readValue(json, new TypeReference<>() {
|
||||
});
|
||||
Map<String, List<Dependency>> data = mapper.readValue(json, new TypeReference<>() {});
|
||||
model.addAttribute("dependencies", data.get("dependencies"));
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
|
@ -4,15 +4,21 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class EditTableOfContentsRequest extends PDFFile {
|
||||
|
||||
@Schema(description = "Bookmark structure in JSON format", example = "[{\"title\":\"Chapter 1\",\"pageNumber\":1,\"children\":[{\"title\":\"Section 1.1\",\"pageNumber\":2}]}]")
|
||||
|
||||
@Schema(
|
||||
description = "Bookmark structure in JSON format",
|
||||
example =
|
||||
"[{\"title\":\"Chapter 1\",\"pageNumber\":1,\"children\":[{\"title\":\"Section 1.1\",\"pageNumber\":2}]}]")
|
||||
private String bookmarkData;
|
||||
|
||||
@Schema(description = "Whether to replace existing bookmarks or append to them", example = "true")
|
||||
|
||||
@Schema(
|
||||
description = "Whether to replace existing bookmarks or append to them",
|
||||
example = "true")
|
||||
private Boolean replaceExisting;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
|
||||
@Data
|
||||
|
@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
|
||||
@Data
|
||||
|
@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
|
||||
@Data
|
||||
@ -11,31 +12,31 @@ import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
public class ConvertToImageRequest extends PDFWithPageNums {
|
||||
|
||||
@Schema(
|
||||
description = "The output image format",
|
||||
defaultValue = "png",
|
||||
allowableValues = {"png", "jpeg", "jpg", "gif", "webp"},
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "The output image format",
|
||||
defaultValue = "png",
|
||||
allowableValues = {"png", "jpeg", "jpg", "gif", "webp"},
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String imageFormat;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"Choose between a single image containing all pages or separate images for each"
|
||||
+ " page",
|
||||
defaultValue = "multiple",
|
||||
allowableValues = {"single", "multiple"},
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description =
|
||||
"Choose between a single image containing all pages or separate images for each"
|
||||
+ " page",
|
||||
defaultValue = "multiple",
|
||||
allowableValues = {"single", "multiple"},
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String singleOrMultiple;
|
||||
|
||||
@Schema(
|
||||
description = "The color type of the output image(s)",
|
||||
defaultValue = "color",
|
||||
allowableValues = {"color", "greyscale", "blackwhite"},
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "The color type of the output image(s)",
|
||||
defaultValue = "color",
|
||||
allowableValues = {"color", "greyscale", "blackwhite"},
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String colorType;
|
||||
|
||||
@Schema(
|
||||
description = "The DPI (dots per inch) for the output image(s)",
|
||||
defaultValue = "300",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "The DPI (dots per inch) for the output image(s)",
|
||||
defaultValue = "300",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Integer dpi;
|
||||
}
|
||||
|
@ -12,8 +12,8 @@ import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
public class ContainsTextRequest extends PDFWithPageNums {
|
||||
|
||||
@Schema(
|
||||
description = "The text to check for",
|
||||
defaultValue = "text",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "The text to check for",
|
||||
defaultValue = "text",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String text;
|
||||
}
|
||||
|
@ -12,8 +12,8 @@ import stirling.software.SPDF.model.api.PDFComparison;
|
||||
public class FileSizeRequest extends PDFComparison {
|
||||
|
||||
@Schema(
|
||||
description = "Size of the file in bytes",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
defaultValue = "0")
|
||||
description = "Size of the file in bytes",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
defaultValue = "0")
|
||||
private long fileSize;
|
||||
}
|
||||
|
@ -12,8 +12,8 @@ import stirling.software.SPDF.model.api.PDFComparison;
|
||||
public class PageRotationRequest extends PDFComparison {
|
||||
|
||||
@Schema(
|
||||
description = "Rotation in degrees",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
defaultValue = "0")
|
||||
description = "Rotation in degrees",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
defaultValue = "0")
|
||||
private int rotation;
|
||||
}
|
||||
|
@ -12,9 +12,9 @@ import stirling.software.SPDF.model.api.PDFComparison;
|
||||
public class PageSizeRequest extends PDFComparison {
|
||||
|
||||
@Schema(
|
||||
description = "Standard Page Size",
|
||||
allowableValues = {"A0", "A1", "A2", "A3", "A4", "A5", "A6", "LETTER", "LEGAL"},
|
||||
defaultValue = "A4",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "Standard Page Size",
|
||||
allowableValues = {"A0", "A1", "A2", "A3", "A4", "A5", "A6", "LETTER", "LEGAL"},
|
||||
defaultValue = "A4",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String standardPageSize;
|
||||
}
|
||||
|
@ -32,11 +32,11 @@ public class MergePdfsRequest extends MultiplePDFFiles {
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
defaultValue = "true")
|
||||
private Boolean removeCertSign;
|
||||
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
description =
|
||||
"Flag indicating whether to generate a table of contents for the merged PDF. If true, a table of contents will be created using the input filenames as chapter names.",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
||||
defaultValue = "false")
|
||||
private boolean generateToc = false;
|
||||
}
|
||||
}
|
||||
|
@ -14,33 +14,33 @@ import stirling.software.common.model.api.PDFFile;
|
||||
public class OverlayPdfsRequest extends PDFFile {
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"An array of PDF files to be used as overlays on the base PDF. The order in"
|
||||
+ " these files is applied based on the selected mode.",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description =
|
||||
"An array of PDF files to be used as overlays on the base PDF. The order in"
|
||||
+ " these files is applied based on the selected mode.",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private MultipartFile[] overlayFiles;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"The mode of overlaying: 'SequentialOverlay' for sequential application,"
|
||||
+ " 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay'"
|
||||
+ " for fixed repetition based on provided counts",
|
||||
allowableValues = {"SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay"},
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description =
|
||||
"The mode of overlaying: 'SequentialOverlay' for sequential application,"
|
||||
+ " 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay'"
|
||||
+ " for fixed repetition based on provided counts",
|
||||
allowableValues = {"SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay"},
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String overlayMode;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"An array of integers specifying the number of times each corresponding overlay"
|
||||
+ " file should be applied in the 'FixedRepeatOverlay' mode. This should"
|
||||
+ " match the length of the overlayFiles array.",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
description =
|
||||
"An array of integers specifying the number of times each corresponding overlay"
|
||||
+ " file should be applied in the 'FixedRepeatOverlay' mode. This should"
|
||||
+ " match the length of the overlayFiles array.",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private int[] counts;
|
||||
|
||||
@Schema(
|
||||
description = "Overlay position 0 is Foregound, 1 is Background",
|
||||
allowableValues = {"0", "1"},
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
type = "number")
|
||||
description = "Overlay position 0 is Foregound, 1 is Background",
|
||||
allowableValues = {"0", "1"},
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
type = "number")
|
||||
private int overlayPosition;
|
||||
}
|
||||
|
@ -14,9 +14,9 @@ import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
public class AddStampRequest extends PDFWithPageNums {
|
||||
|
||||
@Schema(
|
||||
description = "The stamp type (text or image)",
|
||||
allowableValues = {"text", "image"},
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "The stamp type (text or image)",
|
||||
allowableValues = {"text", "image"},
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String stampType;
|
||||
|
||||
@Schema(description = "The stamp text", defaultValue = "Stirling Software")
|
||||
@ -26,60 +26,60 @@ public class AddStampRequest extends PDFWithPageNums {
|
||||
private MultipartFile stampImage;
|
||||
|
||||
@Schema(
|
||||
description = "The selected alphabet of the stamp text",
|
||||
allowableValues = {"roman", "arabic", "japanese", "korean", "chinese"},
|
||||
defaultValue = "roman")
|
||||
description = "The selected alphabet of the stamp text",
|
||||
allowableValues = {"roman", "arabic", "japanese", "korean", "chinese"},
|
||||
defaultValue = "roman")
|
||||
private String alphabet = "roman";
|
||||
|
||||
@Schema(
|
||||
description = "The font size of the stamp text and image",
|
||||
defaultValue = "30",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "The font size of the stamp text and image",
|
||||
defaultValue = "30",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private float fontSize;
|
||||
|
||||
@Schema(
|
||||
description = "The rotation of the stamp in degrees",
|
||||
defaultValue = "0",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "The rotation of the stamp in degrees",
|
||||
defaultValue = "0",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private float rotation;
|
||||
|
||||
@Schema(
|
||||
description = "The opacity of the stamp (0.0 - 1.0)",
|
||||
defaultValue = "0.5",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "The opacity of the stamp (0.0 - 1.0)",
|
||||
defaultValue = "0.5",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private float opacity;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"Position for stamp placement based on a 1-9 grid (1: bottom-left, 2: bottom-center,"
|
||||
+ " 3: bottom-right, 4: middle-left, 5: middle-center, 6: middle-right,"
|
||||
+ " 7: top-left, 8: top-center, 9: top-right)",
|
||||
allowableValues = {"1", "2", "3", "4", "5", "6", "7", "8", "9"},
|
||||
defaultValue = "5",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description =
|
||||
"Position for stamp placement based on a 1-9 grid (1: bottom-left, 2: bottom-center,"
|
||||
+ " 3: bottom-right, 4: middle-left, 5: middle-center, 6: middle-right,"
|
||||
+ " 7: top-left, 8: top-center, 9: top-right)",
|
||||
allowableValues = {"1", "2", "3", "4", "5", "6", "7", "8", "9"},
|
||||
defaultValue = "5",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private int position;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"Override X coordinate for stamp placement. If set, it will override the"
|
||||
+ " position-based calculation. Negative value means no override.",
|
||||
defaultValue = "-1",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description =
|
||||
"Override X coordinate for stamp placement. If set, it will override the"
|
||||
+ " position-based calculation. Negative value means no override.",
|
||||
defaultValue = "-1",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private float overrideX; // Default to -1 indicating no override
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"Override Y coordinate for stamp placement. If set, it will override the"
|
||||
+ " position-based calculation. Negative value means no override.",
|
||||
defaultValue = "-1",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description =
|
||||
"Override Y coordinate for stamp placement. If set, it will override the"
|
||||
+ " position-based calculation. Negative value means no override.",
|
||||
defaultValue = "-1",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private float overrideY; // Default to -1 indicating no override
|
||||
|
||||
@Schema(
|
||||
description = "Specifies the margin size for the stamp.",
|
||||
allowableValues = {"small", "medium", "large", "x-large"},
|
||||
defaultValue = "medium",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "Specifies the margin size for the stamp.",
|
||||
allowableValues = {"small", "medium", "large", "x-large"},
|
||||
defaultValue = "medium",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String customMargin;
|
||||
|
||||
@Schema(description = "The color of the stamp text", defaultValue = "#d3d3d3")
|
||||
|
@ -14,71 +14,71 @@ import stirling.software.common.model.api.PDFFile;
|
||||
public class MetadataRequest extends PDFFile {
|
||||
|
||||
@Schema(
|
||||
description = "Delete all metadata if set to true",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "Delete all metadata if set to true",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean deleteAll;
|
||||
|
||||
@Schema(
|
||||
description = "The author of the document",
|
||||
defaultValue = "author",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
description = "The author of the document",
|
||||
defaultValue = "author",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private String author;
|
||||
|
||||
@Schema(
|
||||
description = "The creation date of the document (format: yyyy/MM/dd HH:mm:ss)",
|
||||
pattern = "yyyy/MM/dd HH:mm:ss",
|
||||
defaultValue = "2023/10/01 12:00:00",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
description = "The creation date of the document (format: yyyy/MM/dd HH:mm:ss)",
|
||||
pattern = "yyyy/MM/dd HH:mm:ss",
|
||||
defaultValue = "2023/10/01 12:00:00",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private String creationDate;
|
||||
|
||||
@Schema(
|
||||
description = "The creator of the document",
|
||||
defaultValue = "creator",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
description = "The creator of the document",
|
||||
defaultValue = "creator",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private String creator;
|
||||
|
||||
@Schema(
|
||||
description = "The keywords for the document",
|
||||
defaultValue = "keywords",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
description = "The keywords for the document",
|
||||
defaultValue = "keywords",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private String keywords;
|
||||
|
||||
@Schema(
|
||||
description = "The modification date of the document (format: yyyy/MM/dd HH:mm:ss)",
|
||||
pattern = "yyyy/MM/dd HH:mm:ss",
|
||||
defaultValue = "2023/10/01 12:00:00",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
description = "The modification date of the document (format: yyyy/MM/dd HH:mm:ss)",
|
||||
pattern = "yyyy/MM/dd HH:mm:ss",
|
||||
defaultValue = "2023/10/01 12:00:00",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private String modificationDate;
|
||||
|
||||
@Schema(
|
||||
description = "The producer of the document",
|
||||
defaultValue = "producer",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
description = "The producer of the document",
|
||||
defaultValue = "producer",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private String producer;
|
||||
|
||||
@Schema(
|
||||
description = "The subject of the document",
|
||||
defaultValue = "subject",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
description = "The subject of the document",
|
||||
defaultValue = "subject",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private String subject;
|
||||
|
||||
@Schema(
|
||||
description = "The title of the document",
|
||||
defaultValue = "title",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
description = "The title of the document",
|
||||
defaultValue = "title",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private String title;
|
||||
|
||||
@Schema(
|
||||
description = "The trapped status of the document",
|
||||
defaultValue = "False",
|
||||
allowableValues = {"True", "False", "Unknown"},
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
description = "The trapped status of the document",
|
||||
defaultValue = "False",
|
||||
allowableValues = {"True", "False", "Unknown"},
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private String trapped;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"Map list of key and value of custom parameters. Note these must start with"
|
||||
+ " customKey and customValue if they are non-standard")
|
||||
description =
|
||||
"Map list of key and value of custom parameters. Note these must start with"
|
||||
+ " customKey and customValue if they are non-standard")
|
||||
private Map<String, String> allRequestParams;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
|
||||
@Data
|
||||
|
@ -12,24 +12,24 @@ import stirling.software.common.model.api.PDFFile;
|
||||
public class AddPasswordRequest extends PDFFile {
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"The owner password to be added to the PDF file (Restricts what can be done"
|
||||
+ " with the document once it is opened)",
|
||||
format = "password")
|
||||
description =
|
||||
"The owner password to be added to the PDF file (Restricts what can be done"
|
||||
+ " with the document once it is opened)",
|
||||
format = "password")
|
||||
private String ownerPassword;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"The password to be added to the PDF file (Restricts the opening of the"
|
||||
+ " document itself.)",
|
||||
format = "password")
|
||||
description =
|
||||
"The password to be added to the PDF file (Restricts the opening of the"
|
||||
+ " document itself.)",
|
||||
format = "password")
|
||||
private String password;
|
||||
|
||||
@Schema(
|
||||
description = "The length of the encryption key",
|
||||
allowableValues = {"40", "128", "256"},
|
||||
defaultValue = "256",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "The length of the encryption key",
|
||||
allowableValues = {"40", "128", "256"},
|
||||
defaultValue = "256",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private int keyLength = 256;
|
||||
|
||||
@Schema(description = "Whether document assembly is prevented", defaultValue = "false")
|
||||
@ -39,8 +39,8 @@ public class AddPasswordRequest extends PDFFile {
|
||||
private Boolean preventExtractContent;
|
||||
|
||||
@Schema(
|
||||
description = "Whether content extraction for accessibility is prevented",
|
||||
defaultValue = "false")
|
||||
description = "Whether content extraction for accessibility is prevented",
|
||||
defaultValue = "false")
|
||||
private Boolean preventExtractForAccessibility;
|
||||
|
||||
@Schema(description = "Whether form filling is prevented", defaultValue = "false")
|
||||
@ -50,8 +50,8 @@ public class AddPasswordRequest extends PDFFile {
|
||||
private Boolean preventModify;
|
||||
|
||||
@Schema(
|
||||
description = "Whether modification of annotations is prevented",
|
||||
defaultValue = "false")
|
||||
description = "Whether modification of annotations is prevented",
|
||||
defaultValue = "false")
|
||||
private Boolean preventModifyAnnotations;
|
||||
|
||||
@Schema(description = "Whether printing of the document is prevented", defaultValue = "false")
|
||||
|
@ -14,19 +14,19 @@ import stirling.software.common.model.api.security.RedactionArea;
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ManualRedactPdfRequest extends PDFWithPageNums {
|
||||
@Schema(
|
||||
description = "A list of areas that should be redacted",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "A list of areas that should be redacted",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<RedactionArea> redactions;
|
||||
|
||||
@Schema(
|
||||
description = "Convert the redacted PDF to an image",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "Convert the redacted PDF to an image",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean convertPDFToImage;
|
||||
|
||||
@Schema(
|
||||
description = "The color used to fully redact certain pages",
|
||||
defaultValue = "#000000",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "The color used to fully redact certain pages",
|
||||
defaultValue = "#000000",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String pageRedactionColor;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
|
||||
@Data
|
||||
@ -11,38 +12,38 @@ import stirling.software.common.model.api.PDFFile;
|
||||
public class RedactPdfRequest extends PDFFile {
|
||||
|
||||
@Schema(
|
||||
description = "List of text to redact from the PDF",
|
||||
defaultValue = "text,text2",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "List of text to redact from the PDF",
|
||||
defaultValue = "text,text2",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String listOfText;
|
||||
|
||||
@Schema(
|
||||
description = "Whether to use regex for the listOfText",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "Whether to use regex for the listOfText",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean useRegex;
|
||||
|
||||
@Schema(
|
||||
description = "Whether to use whole word search",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "Whether to use whole word search",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean wholeWordSearch;
|
||||
|
||||
@Schema(
|
||||
description = "The color for redaction",
|
||||
defaultValue = "#000000",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "The color for redaction",
|
||||
defaultValue = "#000000",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String redactColor;
|
||||
|
||||
@Schema(
|
||||
description = "Custom padding for redaction",
|
||||
type = "number",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "Custom padding for redaction",
|
||||
type = "number",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private float customPadding;
|
||||
|
||||
@Schema(
|
||||
description = "Convert the redacted PDF to an image",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "Convert the redacted PDF to an image",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean convertPDFToImage;
|
||||
}
|
||||
|
@ -12,38 +12,38 @@ import stirling.software.common.model.api.PDFFile;
|
||||
public class SanitizePdfRequest extends PDFFile {
|
||||
|
||||
@Schema(
|
||||
description = "Remove JavaScript actions from the PDF",
|
||||
defaultValue = "true",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "Remove JavaScript actions from the PDF",
|
||||
defaultValue = "true",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean removeJavaScript;
|
||||
|
||||
@Schema(
|
||||
description = "Remove embedded files from the PDF",
|
||||
defaultValue = "true",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "Remove embedded files from the PDF",
|
||||
defaultValue = "true",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean removeEmbeddedFiles;
|
||||
|
||||
@Schema(
|
||||
description = "Remove XMP metadata from the PDF",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "Remove XMP metadata from the PDF",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean removeXMPMetadata;
|
||||
|
||||
@Schema(
|
||||
description = "Remove document info metadata from the PDF",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "Remove document info metadata from the PDF",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean removeMetadata;
|
||||
|
||||
@Schema(
|
||||
description = "Remove links from the PDF",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "Remove links from the PDF",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean removeLinks;
|
||||
|
||||
@Schema(
|
||||
description = "Remove fonts from the PDF",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
description = "Remove fonts from the PDF",
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean removeFonts;
|
||||
}
|
||||
|
@ -14,8 +14,8 @@ import io.micrometer.core.instrument.search.Search;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.service.PostHogService;
|
||||
import stirling.software.SPDF.config.EndpointInspector;
|
||||
import stirling.software.common.service.PostHogService;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
|
@ -0,0 +1,382 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import stirling.software.SPDF.controller.api.EditTableOfContentsController.BookmarkItem;
|
||||
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class EditTableOfContentsControllerTest {
|
||||
|
||||
@Mock
|
||||
private CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Mock
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@InjectMocks
|
||||
private EditTableOfContentsController editTableOfContentsController;
|
||||
|
||||
private MockMultipartFile mockFile;
|
||||
private PDDocument mockDocument;
|
||||
private PDDocumentCatalog mockCatalog;
|
||||
private PDPageTree mockPages;
|
||||
private PDPage mockPage1;
|
||||
private PDPage mockPage2;
|
||||
private PDDocumentOutline mockOutline;
|
||||
private PDOutlineItem mockOutlineItem;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockFile = new MockMultipartFile("file", "test.pdf", "application/pdf", "PDF content".getBytes());
|
||||
mockDocument = mock(PDDocument.class);
|
||||
mockCatalog = mock(PDDocumentCatalog.class);
|
||||
mockPages = mock(PDPageTree.class);
|
||||
mockPage1 = mock(PDPage.class);
|
||||
mockPage2 = mock(PDPage.class);
|
||||
mockOutline = mock(PDDocumentOutline.class);
|
||||
mockOutlineItem = mock(PDOutlineItem.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractBookmarks_WithExistingBookmarks_Success() throws Exception {
|
||||
// Given
|
||||
when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument);
|
||||
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockCatalog.getDocumentOutline()).thenReturn(mockOutline);
|
||||
when(mockOutline.getFirstChild()).thenReturn(mockOutlineItem);
|
||||
|
||||
when(mockOutlineItem.getTitle()).thenReturn("Chapter 1");
|
||||
when(mockOutlineItem.findDestinationPage(mockDocument)).thenReturn(mockPage1);
|
||||
when(mockDocument.getPages()).thenReturn(mockPages);
|
||||
when(mockPages.indexOf(mockPage1)).thenReturn(0);
|
||||
when(mockOutlineItem.getFirstChild()).thenReturn(null);
|
||||
when(mockOutlineItem.getNextSibling()).thenReturn(null);
|
||||
|
||||
// When
|
||||
List<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
|
||||
Map<String, Object> bookmark = result.get(0);
|
||||
assertEquals("Chapter 1", bookmark.get("title"));
|
||||
assertEquals(1, bookmark.get("pageNumber")); // 1-based
|
||||
assertInstanceOf(List.class, bookmark.get("children"));
|
||||
|
||||
verify(mockDocument).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractBookmarks_NoOutline_ReturnsEmptyList() throws Exception {
|
||||
// Given
|
||||
when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument);
|
||||
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockCatalog.getDocumentOutline()).thenReturn(null);
|
||||
|
||||
// When
|
||||
List<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
verify(mockDocument).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractBookmarks_WithNestedBookmarks_Success() throws Exception {
|
||||
// Given
|
||||
PDOutlineItem childItem = mock(PDOutlineItem.class);
|
||||
|
||||
when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument);
|
||||
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockCatalog.getDocumentOutline()).thenReturn(mockOutline);
|
||||
when(mockOutline.getFirstChild()).thenReturn(mockOutlineItem);
|
||||
|
||||
// Parent bookmark
|
||||
when(mockOutlineItem.getTitle()).thenReturn("Chapter 1");
|
||||
when(mockOutlineItem.findDestinationPage(mockDocument)).thenReturn(mockPage1);
|
||||
when(mockDocument.getPages()).thenReturn(mockPages);
|
||||
when(mockPages.indexOf(mockPage1)).thenReturn(0);
|
||||
when(mockOutlineItem.getFirstChild()).thenReturn(childItem);
|
||||
when(mockOutlineItem.getNextSibling()).thenReturn(null);
|
||||
|
||||
// Child bookmark
|
||||
when(childItem.getTitle()).thenReturn("Section 1.1");
|
||||
when(childItem.findDestinationPage(mockDocument)).thenReturn(mockPage2);
|
||||
when(mockPages.indexOf(mockPage2)).thenReturn(1);
|
||||
when(childItem.getFirstChild()).thenReturn(null);
|
||||
when(childItem.getNextSibling()).thenReturn(null);
|
||||
|
||||
// When
|
||||
List<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
|
||||
Map<String, Object> parentBookmark = result.get(0);
|
||||
assertEquals("Chapter 1", parentBookmark.get("title"));
|
||||
assertEquals(1, parentBookmark.get("pageNumber"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> children = (List<Map<String, Object>>) parentBookmark.get("children");
|
||||
assertEquals(1, children.size());
|
||||
|
||||
Map<String, Object> childBookmark = children.get(0);
|
||||
assertEquals("Section 1.1", childBookmark.get("title"));
|
||||
assertEquals(2, childBookmark.get("pageNumber"));
|
||||
|
||||
verify(mockDocument).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractBookmarks_PageNotFound_UsesPageOne() throws Exception {
|
||||
// Given
|
||||
when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument);
|
||||
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockCatalog.getDocumentOutline()).thenReturn(mockOutline);
|
||||
when(mockOutline.getFirstChild()).thenReturn(mockOutlineItem);
|
||||
|
||||
when(mockOutlineItem.getTitle()).thenReturn("Chapter 1");
|
||||
when(mockOutlineItem.findDestinationPage(mockDocument)).thenReturn(null); // Page not found
|
||||
when(mockOutlineItem.getFirstChild()).thenReturn(null);
|
||||
when(mockOutlineItem.getNextSibling()).thenReturn(null);
|
||||
|
||||
// When
|
||||
List<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
|
||||
Map<String, Object> bookmark = result.get(0);
|
||||
assertEquals("Chapter 1", bookmark.get("title"));
|
||||
assertEquals(1, bookmark.get("pageNumber")); // Default to page 1
|
||||
|
||||
verify(mockDocument).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEditTableOfContents_Success() throws Exception {
|
||||
// Given
|
||||
EditTableOfContentsRequest request = new EditTableOfContentsRequest();
|
||||
request.setFileInput(mockFile);
|
||||
request.setBookmarkData("[{\"title\":\"Chapter 1\",\"pageNumber\":1,\"children\":[]}]");
|
||||
request.setReplaceExisting(true);
|
||||
|
||||
List<BookmarkItem> bookmarks = new ArrayList<>();
|
||||
BookmarkItem bookmark = new BookmarkItem();
|
||||
bookmark.setTitle("Chapter 1");
|
||||
bookmark.setPageNumber(1);
|
||||
bookmark.setChildren(new ArrayList<>());
|
||||
bookmarks.add(bookmark);
|
||||
|
||||
when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument);
|
||||
when(objectMapper.readValue(eq(request.getBookmarkData()), any(TypeReference.class))).thenReturn(bookmarks);
|
||||
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockDocument.getNumberOfPages()).thenReturn(5);
|
||||
when(mockDocument.getPage(0)).thenReturn(mockPage1);
|
||||
|
||||
// Mock saving behavior
|
||||
doAnswer(invocation -> {
|
||||
ByteArrayOutputStream baos = invocation.getArgument(0);
|
||||
baos.write("mocked pdf content".getBytes());
|
||||
return null;
|
||||
}).when(mockDocument).save(any(ByteArrayOutputStream.class));
|
||||
|
||||
// When
|
||||
ResponseEntity<byte[]> result = editTableOfContentsController.editTableOfContents(request);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertNotNull(result.getBody());
|
||||
|
||||
ArgumentCaptor<PDDocumentOutline> outlineCaptor = ArgumentCaptor.forClass(PDDocumentOutline.class);
|
||||
verify(mockCatalog).setDocumentOutline(outlineCaptor.capture());
|
||||
|
||||
PDDocumentOutline capturedOutline = outlineCaptor.getValue();
|
||||
assertNotNull(capturedOutline);
|
||||
|
||||
verify(mockDocument).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEditTableOfContents_WithNestedBookmarks_Success() throws Exception {
|
||||
// Given
|
||||
EditTableOfContentsRequest request = new EditTableOfContentsRequest();
|
||||
request.setFileInput(mockFile);
|
||||
|
||||
String bookmarkJson = "[{\"title\":\"Chapter 1\",\"pageNumber\":1,\"children\":[{\"title\":\"Section 1.1\",\"pageNumber\":2,\"children\":[]}]}]";
|
||||
request.setBookmarkData(bookmarkJson);
|
||||
|
||||
List<BookmarkItem> bookmarks = new ArrayList<>();
|
||||
BookmarkItem parentBookmark = new BookmarkItem();
|
||||
parentBookmark.setTitle("Chapter 1");
|
||||
parentBookmark.setPageNumber(1);
|
||||
|
||||
BookmarkItem childBookmark = new BookmarkItem();
|
||||
childBookmark.setTitle("Section 1.1");
|
||||
childBookmark.setPageNumber(2);
|
||||
childBookmark.setChildren(new ArrayList<>());
|
||||
|
||||
List<BookmarkItem> children = new ArrayList<>();
|
||||
children.add(childBookmark);
|
||||
parentBookmark.setChildren(children);
|
||||
bookmarks.add(parentBookmark);
|
||||
|
||||
when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument);
|
||||
when(objectMapper.readValue(eq(bookmarkJson), any(TypeReference.class))).thenReturn(bookmarks);
|
||||
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockDocument.getNumberOfPages()).thenReturn(5);
|
||||
when(mockDocument.getPage(0)).thenReturn(mockPage1);
|
||||
when(mockDocument.getPage(1)).thenReturn(mockPage2);
|
||||
|
||||
doAnswer(invocation -> {
|
||||
ByteArrayOutputStream baos = invocation.getArgument(0);
|
||||
baos.write("mocked pdf content".getBytes());
|
||||
return null;
|
||||
}).when(mockDocument).save(any(ByteArrayOutputStream.class));
|
||||
|
||||
// When
|
||||
ResponseEntity<byte[]> result = editTableOfContentsController.editTableOfContents(request);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
verify(mockCatalog).setDocumentOutline(any(PDDocumentOutline.class));
|
||||
verify(mockDocument).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEditTableOfContents_PageNumberBounds_ClampsValues() throws Exception {
|
||||
// Given
|
||||
EditTableOfContentsRequest request = new EditTableOfContentsRequest();
|
||||
request.setFileInput(mockFile);
|
||||
request.setBookmarkData("[{\"title\":\"Chapter 1\",\"pageNumber\":-5,\"children\":[]},{\"title\":\"Chapter 2\",\"pageNumber\":100,\"children\":[]}]");
|
||||
|
||||
List<BookmarkItem> bookmarks = new ArrayList<>();
|
||||
|
||||
BookmarkItem bookmark1 = new BookmarkItem();
|
||||
bookmark1.setTitle("Chapter 1");
|
||||
bookmark1.setPageNumber(-5); // Negative page number
|
||||
bookmark1.setChildren(new ArrayList<>());
|
||||
|
||||
BookmarkItem bookmark2 = new BookmarkItem();
|
||||
bookmark2.setTitle("Chapter 2");
|
||||
bookmark2.setPageNumber(100); // Page number exceeds document pages
|
||||
bookmark2.setChildren(new ArrayList<>());
|
||||
|
||||
bookmarks.add(bookmark1);
|
||||
bookmarks.add(bookmark2);
|
||||
|
||||
when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument);
|
||||
when(objectMapper.readValue(eq(request.getBookmarkData()), any(TypeReference.class))).thenReturn(bookmarks);
|
||||
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockDocument.getNumberOfPages()).thenReturn(5);
|
||||
when(mockDocument.getPage(0)).thenReturn(mockPage1); // For negative page number
|
||||
when(mockDocument.getPage(4)).thenReturn(mockPage2); // For page number exceeding bounds
|
||||
|
||||
doAnswer(invocation -> {
|
||||
ByteArrayOutputStream baos = invocation.getArgument(0);
|
||||
baos.write("mocked pdf content".getBytes());
|
||||
return null;
|
||||
}).when(mockDocument).save(any(ByteArrayOutputStream.class));
|
||||
|
||||
// When
|
||||
ResponseEntity<byte[]> result = editTableOfContentsController.editTableOfContents(request);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
verify(mockDocument).getPage(0); // Clamped to first page
|
||||
verify(mockDocument).getPage(4); // Clamped to last page
|
||||
verify(mockDocument).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateOutlineItem_ValidPageNumber_Success() throws Exception {
|
||||
// Given
|
||||
BookmarkItem bookmark = new BookmarkItem();
|
||||
bookmark.setTitle("Test Chapter");
|
||||
bookmark.setPageNumber(3);
|
||||
|
||||
when(mockDocument.getNumberOfPages()).thenReturn(5);
|
||||
when(mockDocument.getPage(2)).thenReturn(mockPage1); // 0-indexed
|
||||
|
||||
// When
|
||||
Method createOutlineItemMethod = EditTableOfContentsController.class.getDeclaredMethod("createOutlineItem", PDDocument.class, BookmarkItem.class);
|
||||
createOutlineItemMethod.setAccessible(true);
|
||||
PDOutlineItem result = (PDOutlineItem) createOutlineItemMethod.invoke(editTableOfContentsController, mockDocument, bookmark);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
verify(mockDocument).getPage(2);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testBookmarkItem_GettersAndSetters() {
|
||||
// Given
|
||||
BookmarkItem bookmark = new BookmarkItem();
|
||||
List<BookmarkItem> children = new ArrayList<>();
|
||||
|
||||
// When
|
||||
bookmark.setTitle("Test Title");
|
||||
bookmark.setPageNumber(5);
|
||||
bookmark.setChildren(children);
|
||||
|
||||
// Then
|
||||
assertEquals("Test Title", bookmark.getTitle());
|
||||
assertEquals(5, bookmark.getPageNumber());
|
||||
assertEquals(children, bookmark.getChildren());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEditTableOfContents_IOExceptionDuringLoad_ThrowsException() throws Exception {
|
||||
// Given
|
||||
EditTableOfContentsRequest request = new EditTableOfContentsRequest();
|
||||
request.setFileInput(mockFile);
|
||||
|
||||
when(pdfDocumentFactory.load(mockFile)).thenThrow(new RuntimeException("Failed to load PDF"));
|
||||
|
||||
// When & Then
|
||||
assertThrows(RuntimeException.class, () -> editTableOfContentsController.editTableOfContents(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractBookmarks_IOExceptionDuringLoad_ThrowsException() throws Exception {
|
||||
// Given
|
||||
when(pdfDocumentFactory.load(mockFile)).thenThrow(new RuntimeException("Failed to load PDF"));
|
||||
|
||||
// When & Then
|
||||
assertThrows(RuntimeException.class, () -> editTableOfContentsController.extractBookmarks(mockFile));
|
||||
}
|
||||
}
|
@ -0,0 +1,279 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MergeControllerTest {
|
||||
|
||||
@Mock
|
||||
private CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@InjectMocks
|
||||
private MergeController mergeController;
|
||||
|
||||
private MockMultipartFile mockFile1;
|
||||
private MockMultipartFile mockFile2;
|
||||
private MockMultipartFile mockFile3;
|
||||
private PDDocument mockDocument;
|
||||
private PDDocument mockMergedDocument;
|
||||
private PDDocumentCatalog mockCatalog;
|
||||
private PDPageTree mockPages;
|
||||
private PDPage mockPage1;
|
||||
private PDPage mockPage2;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockFile1 = new MockMultipartFile("file1", "document1.pdf", "application/pdf", "PDF content 1".getBytes());
|
||||
mockFile2 = new MockMultipartFile("file2", "document2.pdf", "application/pdf", "PDF content 2".getBytes());
|
||||
mockFile3 = new MockMultipartFile("file3", "chapter3.pdf", "application/pdf", "PDF content 3".getBytes());
|
||||
|
||||
mockDocument = mock(PDDocument.class);
|
||||
mockMergedDocument = mock(PDDocument.class);
|
||||
mockCatalog = mock(PDDocumentCatalog.class);
|
||||
mockPages = mock(PDPageTree.class);
|
||||
mockPage1 = mock(PDPage.class);
|
||||
mockPage2 = mock(PDPage.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddTableOfContents_WithMultipleFiles_Success() throws Exception {
|
||||
// Given
|
||||
MultipartFile[] files = {mockFile1, mockFile2, mockFile3};
|
||||
|
||||
// Mock the merged document setup
|
||||
when(mockMergedDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockMergedDocument.getNumberOfPages()).thenReturn(6);
|
||||
when(mockMergedDocument.getPage(0)).thenReturn(mockPage1);
|
||||
when(mockMergedDocument.getPage(2)).thenReturn(mockPage2);
|
||||
when(mockMergedDocument.getPage(4)).thenReturn(mockPage1);
|
||||
|
||||
// Mock individual document loading for page count
|
||||
PDDocument doc1 = mock(PDDocument.class);
|
||||
PDDocument doc2 = mock(PDDocument.class);
|
||||
PDDocument doc3 = mock(PDDocument.class);
|
||||
|
||||
when(pdfDocumentFactory.load(mockFile1)).thenReturn(doc1);
|
||||
when(pdfDocumentFactory.load(mockFile2)).thenReturn(doc2);
|
||||
when(pdfDocumentFactory.load(mockFile3)).thenReturn(doc3);
|
||||
|
||||
when(doc1.getNumberOfPages()).thenReturn(2);
|
||||
when(doc2.getNumberOfPages()).thenReturn(2);
|
||||
when(doc3.getNumberOfPages()).thenReturn(2);
|
||||
|
||||
// When
|
||||
Method addTableOfContentsMethod = MergeController.class.getDeclaredMethod("addTableOfContents", PDDocument.class, MultipartFile[].class);
|
||||
addTableOfContentsMethod.setAccessible(true);
|
||||
addTableOfContentsMethod.invoke(mergeController, mockMergedDocument, files);
|
||||
|
||||
// Then
|
||||
ArgumentCaptor<PDDocumentOutline> outlineCaptor = ArgumentCaptor.forClass(PDDocumentOutline.class);
|
||||
verify(mockCatalog).setDocumentOutline(outlineCaptor.capture());
|
||||
|
||||
PDDocumentOutline capturedOutline = outlineCaptor.getValue();
|
||||
assertNotNull(capturedOutline);
|
||||
|
||||
// Verify that documents were loaded for page count
|
||||
verify(pdfDocumentFactory).load(mockFile1);
|
||||
verify(pdfDocumentFactory).load(mockFile2);
|
||||
verify(pdfDocumentFactory).load(mockFile3);
|
||||
|
||||
// Verify document closing
|
||||
verify(doc1).close();
|
||||
verify(doc2).close();
|
||||
verify(doc3).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddTableOfContents_WithSingleFile_Success() throws Exception {
|
||||
// Given
|
||||
MultipartFile[] files = {mockFile1};
|
||||
|
||||
when(mockMergedDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockMergedDocument.getNumberOfPages()).thenReturn(3);
|
||||
when(mockMergedDocument.getPage(0)).thenReturn(mockPage1);
|
||||
|
||||
PDDocument doc1 = mock(PDDocument.class);
|
||||
when(pdfDocumentFactory.load(mockFile1)).thenReturn(doc1);
|
||||
when(doc1.getNumberOfPages()).thenReturn(3);
|
||||
|
||||
// When
|
||||
Method addTableOfContentsMethod = MergeController.class.getDeclaredMethod("addTableOfContents", PDDocument.class, MultipartFile[].class);
|
||||
addTableOfContentsMethod.setAccessible(true);
|
||||
addTableOfContentsMethod.invoke(mergeController, mockMergedDocument, files);
|
||||
|
||||
// Then
|
||||
verify(mockCatalog).setDocumentOutline(any(PDDocumentOutline.class));
|
||||
verify(pdfDocumentFactory).load(mockFile1);
|
||||
verify(doc1).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddTableOfContents_WithEmptyArray_Success() throws Exception {
|
||||
// Given
|
||||
MultipartFile[] files = {};
|
||||
when(mockMergedDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
|
||||
// When
|
||||
Method addTableOfContentsMethod = MergeController.class.getDeclaredMethod("addTableOfContents", PDDocument.class, MultipartFile[].class);
|
||||
addTableOfContentsMethod.setAccessible(true);
|
||||
addTableOfContentsMethod.invoke(mergeController, mockMergedDocument, files);
|
||||
|
||||
// Then
|
||||
verify(mockMergedDocument).getDocumentCatalog();
|
||||
verify(mockCatalog).setDocumentOutline(any(PDDocumentOutline.class));
|
||||
verifyNoInteractions(pdfDocumentFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddTableOfContents_WithIOException_HandlesGracefully() throws Exception {
|
||||
// Given
|
||||
MultipartFile[] files = {mockFile1, mockFile2};
|
||||
|
||||
when(mockMergedDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockMergedDocument.getNumberOfPages()).thenReturn(4);
|
||||
when(mockMergedDocument.getPage(anyInt())).thenReturn(mockPage1); // Use anyInt() to avoid stubbing conflicts
|
||||
|
||||
// First document loads successfully
|
||||
PDDocument doc1 = mock(PDDocument.class);
|
||||
when(pdfDocumentFactory.load(mockFile1)).thenReturn(doc1);
|
||||
when(doc1.getNumberOfPages()).thenReturn(2);
|
||||
|
||||
// Second document throws IOException
|
||||
when(pdfDocumentFactory.load(mockFile2)).thenThrow(new IOException("Failed to load document"));
|
||||
|
||||
// When
|
||||
Method addTableOfContentsMethod = MergeController.class.getDeclaredMethod("addTableOfContents", PDDocument.class, MultipartFile[].class);
|
||||
addTableOfContentsMethod.setAccessible(true);
|
||||
|
||||
// Should not throw exception
|
||||
assertDoesNotThrow(() ->
|
||||
addTableOfContentsMethod.invoke(mergeController, mockMergedDocument, files)
|
||||
);
|
||||
|
||||
// Then
|
||||
verify(mockCatalog).setDocumentOutline(any(PDDocumentOutline.class));
|
||||
verify(pdfDocumentFactory).load(mockFile1);
|
||||
verify(pdfDocumentFactory).load(mockFile2);
|
||||
verify(doc1).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddTableOfContents_FilenameWithoutExtension_UsesFullName() throws Exception {
|
||||
// Given
|
||||
MockMultipartFile fileWithoutExtension = new MockMultipartFile("file", "document_no_ext", "application/pdf", "PDF content".getBytes());
|
||||
MultipartFile[] files = {fileWithoutExtension};
|
||||
|
||||
when(mockMergedDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockMergedDocument.getNumberOfPages()).thenReturn(1);
|
||||
when(mockMergedDocument.getPage(0)).thenReturn(mockPage1);
|
||||
|
||||
PDDocument doc = mock(PDDocument.class);
|
||||
when(pdfDocumentFactory.load(fileWithoutExtension)).thenReturn(doc);
|
||||
when(doc.getNumberOfPages()).thenReturn(1);
|
||||
|
||||
// When
|
||||
Method addTableOfContentsMethod = MergeController.class.getDeclaredMethod("addTableOfContents", PDDocument.class, MultipartFile[].class);
|
||||
addTableOfContentsMethod.setAccessible(true);
|
||||
addTableOfContentsMethod.invoke(mergeController, mockMergedDocument, files);
|
||||
|
||||
// Then
|
||||
verify(mockCatalog).setDocumentOutline(any(PDDocumentOutline.class));
|
||||
verify(doc).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddTableOfContents_PageIndexExceedsDocumentPages_HandlesGracefully() throws Exception {
|
||||
// Given
|
||||
MultipartFile[] files = {mockFile1};
|
||||
|
||||
when(mockMergedDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockMergedDocument.getNumberOfPages()).thenReturn(0); // No pages in merged document
|
||||
|
||||
PDDocument doc1 = mock(PDDocument.class);
|
||||
when(pdfDocumentFactory.load(mockFile1)).thenReturn(doc1);
|
||||
when(doc1.getNumberOfPages()).thenReturn(3);
|
||||
|
||||
// When
|
||||
Method addTableOfContentsMethod = MergeController.class.getDeclaredMethod("addTableOfContents", PDDocument.class, MultipartFile[].class);
|
||||
addTableOfContentsMethod.setAccessible(true);
|
||||
|
||||
// Should not throw exception
|
||||
assertDoesNotThrow(() ->
|
||||
addTableOfContentsMethod.invoke(mergeController, mockMergedDocument, files)
|
||||
);
|
||||
|
||||
// Then
|
||||
verify(mockCatalog).setDocumentOutline(any(PDDocumentOutline.class));
|
||||
verify(mockMergedDocument, never()).getPage(anyInt());
|
||||
verify(doc1).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMergeDocuments_Success() throws IOException {
|
||||
// Given
|
||||
PDDocument doc1 = mock(PDDocument.class);
|
||||
PDDocument doc2 = mock(PDDocument.class);
|
||||
List<PDDocument> documents = Arrays.asList(doc1, doc2);
|
||||
|
||||
PDPageTree pages1 = mock(PDPageTree.class);
|
||||
PDPageTree pages2 = mock(PDPageTree.class);
|
||||
PDPage page1 = mock(PDPage.class);
|
||||
PDPage page2 = mock(PDPage.class);
|
||||
|
||||
when(pdfDocumentFactory.createNewDocument()).thenReturn(mockMergedDocument);
|
||||
when(doc1.getPages()).thenReturn(pages1);
|
||||
when(doc2.getPages()).thenReturn(pages2);
|
||||
when(pages1.iterator()).thenReturn(Arrays.asList(page1).iterator());
|
||||
when(pages2.iterator()).thenReturn(Arrays.asList(page2).iterator());
|
||||
|
||||
// When
|
||||
PDDocument result = mergeController.mergeDocuments(documents);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(mockMergedDocument, result);
|
||||
verify(mockMergedDocument).addPage(page1);
|
||||
verify(mockMergedDocument).addPage(page2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMergeDocuments_EmptyList_ReturnsEmptyDocument() throws IOException {
|
||||
// Given
|
||||
List<PDDocument> documents = Arrays.asList();
|
||||
|
||||
when(pdfDocumentFactory.createNewDocument()).thenReturn(mockMergedDocument);
|
||||
|
||||
// When
|
||||
PDDocument result = mergeController.mergeDocuments(documents);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(mockMergedDocument, result);
|
||||
verify(mockMergedDocument, never()).addPage(any(PDPage.class));
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user