/*
* Created on 12 gen 2016
* Copyright 2015 by Andrea Vacondio (andrea.vacondio@gmail.com).
* This file is part of Sejda.
*
* Sejda is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Sejda is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Sejda. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sejda.core.service;
import static java.util.Objects.nonNull;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.sejda.core.Sejda;
import org.sejda.core.notification.context.GlobalNotificationContext;
import org.sejda.io.SeekableSource;
import org.sejda.io.SeekableSources;
import org.sejda.model.SejdaFileExtensions;
import org.sejda.model.notification.EventListener;
import org.sejda.model.notification.event.TaskExecutionFailedEvent;
import org.sejda.model.notification.event.TaskExecutionWarningEvent;
import org.sejda.model.output.DirectoryTaskOutput;
import org.sejda.model.output.FileOrDirectoryTaskOutput;
import org.sejda.model.output.FileTaskOutput;
import org.sejda.model.parameter.base.MultipleOutputTaskParameters;
import org.sejda.model.parameter.base.SingleOrMultipleOutputTaskParameters;
import org.sejda.model.parameter.base.SingleOutputTaskParameters;
import org.sejda.model.pdf.PdfVersion;
import org.sejda.sambox.input.PDFParser;
import org.sejda.sambox.pdmodel.PDDocument;
import org.sejda.sambox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
import org.sejda.sambox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import org.sejda.sambox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode;
/**
* Context to be used in tests
*
* @author Andrea Vacondio
*
*/
public class TaskTestContext implements Closeable {
private ByteArrayOutputStream streamOutput;
private File fileOutput;
private PDDocument outputDocument;
/**
* Initialize the given params with a {@link FileTaskOutput}
*
* @param params
* @return
*/
public TaskTestContext pdfOutputTo(SingleOutputTaskParameters params) throws IOException {
this.fileOutput = File.createTempFile("SejdaTest", ".pdf");
this.fileOutput.deleteOnExit();
params.setOutput(new FileTaskOutput(fileOutput));
return this;
}
/**
* Initialize the given params with a {@link FileTaskOutput} on a file with the given extension
*
* @param params
* @param extension
* @return
* @throws IOException
*/
public TaskTestContext fileOutputTo(SingleOutputTaskParameters params, String extension) throws IOException {
this.fileOutput = File.createTempFile("SejdaTest", extension);
this.fileOutput.deleteOnExit();
params.setOutput(new FileTaskOutput(fileOutput));
return this;
}
/**
* Initialize the given params with a {@link DirectoryTaskOutput}
*
* @param params
* @return
* @throws IOException
*/
public TaskTestContext directoryOutputTo(MultipleOutputTaskParameters params) throws IOException {
this.fileOutput = Files.createTempDirectory("SejdaTest").toFile();
this.fileOutput.deleteOnExit();
params.setOutput(new DirectoryTaskOutput(fileOutput));
return this;
}
public TaskTestContext directoryOutputTo(SingleOrMultipleOutputTaskParameters params) throws IOException {
this.fileOutput = Files.createTempDirectory("SejdaTest").toFile();
this.fileOutput.deleteOnExit();
params.setOutput(new FileOrDirectoryTaskOutput(fileOutput));
return this;
}
/**
* asserts the creator has been set to the info dictionary
*
* @return
*/
public TaskTestContext assertCreator() {
requirePDDocument();
assertEquals(Sejda.CREATOR, outputDocument.getDocumentInformation().getCreator());
return this;
}
/**
* asserts the PDF version of the output is the given one
*
* @return
*/
public TaskTestContext assertVersion(PdfVersion version) {
requirePDDocument();
assertEquals("Wrong output PDF version", version.getVersionString(), outputDocument.getVersion());
return this;
}
/**
* asserts the output document has that number of pages
*
* @return
*/
public TaskTestContext assertPages(int expected) {
requirePDDocument();
assertEquals("Wrong number of pages", expected, outputDocument.getNumberOfPages());
return this;
}
/**
* asserts the output document with the given filename exists and has that number of pages. This assert will work only for multiple output task.
*
* @return
* @throws IOException
* @see this{@link #assertPages(int)}
*/
public TaskTestContext assertPages(String filename, int expected) throws IOException {
assertOutputContainsFilenames(filename);
try (PDDocument doc = PDFParser.parse(SeekableSources.seekableSourceFrom(new File(fileOutput, filename)))) {
assertEquals(expected, doc.getNumberOfPages());
}
return this;
}
/**
* @param hasOutline
*/
public TaskTestContext assertHasOutline(boolean hasOutline) {
requirePDDocument();
if (hasOutline) {
assertNotNull(outputDocument.getDocumentCatalog().getDocumentOutline());
} else {
assertNull(outputDocument.getDocumentCatalog().getDocumentOutline());
}
return this;
}
/**
* assert that the document has an acroform with some field in it
*
* @param hasForms
*/
public TaskTestContext assertHasAcroforms(boolean hasForms) {
requirePDDocument();
if (hasForms) {
assertNotNull(outputDocument.getDocumentCatalog().getAcroForm());
assertTrue(outputDocument.getDocumentCatalog().getAcroForm().getFields().size() > 0);
} else {
assertNull(outputDocument.getDocumentCatalog().getAcroForm());
}
return this;
}
/**
* assert the document outline contains an item with the given title
*
* @param string
* @return
*/
public TaskTestContext assertOutlineContains(String title) {
requirePDDocument();
PDDocumentOutline outline = outputDocument.getDocumentCatalog().getDocumentOutline();
assertNotNull(outline);
if (!findOutlineItem(title, outline)) {
fail("Unable to find outline node with title: " + title);
}
return this;
}
/**
* assert the document outline doesn't contain an item with the given title
*
* @param string
* @return
*/
public TaskTestContext assertOutlineDoesntContain(String title) {
requirePDDocument();
PDDocumentOutline outline = outputDocument.getDocumentCatalog().getDocumentOutline();
if (nonNull(outline)) {
if (findOutlineItem(title, outline)) {
fail("Found outline node with title: " + title);
}
}
return this;
}
private boolean findOutlineItem(String title, PDOutlineNode node) {
boolean found = false;
if (node.hasChildren()) {
for (PDOutlineItem current : node.children()) {
found = findOutlineItem(title, current);
if (found) {
return true;
}
}
}
if (node instanceof PDOutlineItem && title.equals(((PDOutlineItem) node).getTitle())) {
return true;
}
return false;
}
/**
* asserts that a multiple output task has generated the given number of output files
*
* @return
* @throws IOException
*/
public TaskTestContext assertOutputSize(int size) throws IOException {
if (size == 0) {
return assertEmptyMultipleOutput();
}
requireMultipleOutputs();
String[] files = fileOutput.list();
assertEquals("An unexpected number of output files has been created: " + StringUtils.join(files, ","), size,
files.length);
assertEquals("Some output file is hidden", size, Files.list(fileOutput.toPath()).filter(p -> {
if (IS_OS_WINDOWS) {
try {
return !(Boolean) Files.getAttribute(p, "dos:hidden");
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
return true;
}).count());
return this;
}
/**
* asserts that a multiple output task has generated no output
*
* @return
*/
public TaskTestContext assertEmptyMultipleOutput() {
assertNotNull(fileOutput);
assertTrue("Expected an output directory", fileOutput.isDirectory());
assertEquals("Found output files while expecting none", 0,
fileOutput.listFiles((d, n) -> !n.endsWith(".tmp")).length);
return this;
}
/**
* asserts that a multiple output task has generated the given file names
*
* @param filenames
* @return
*/
public TaskTestContext assertOutputContainsFilenames(String... filenames) {
requireMultipleOutputs();
Set<String> outputFiles = Arrays.stream(fileOutput.listFiles()).map(File::getName).collect(Collectors.toSet());
Arrays.stream(filenames)
.forEach(f -> assertTrue(f + " missing but expected. Files were: " + StringUtils.join(outputFiles),
outputFiles.contains(f)));
return this;
}
/**
* Applies the given consumer to every generated output
*
* @param consumer
* @return
* @throws IOException
*/
public TaskTestContext forEachPdfOutput(Consumer<PDDocument> consumer) throws IOException {
if (nonNull(outputDocument)) {
requirePDDocument();
consumer.accept(outputDocument);
} else if (nonNull(fileOutput) && fileOutput.isDirectory()) {
requireMultipleOutputs();
for (File current : fileOutput.listFiles()) {
try (PDDocument doc = PDFParser.parse(SeekableSources.seekableSourceFrom(current))) {
consumer.accept(doc);
}
}
} else {
fail("No output to apply to");
}
return this;
}
/**
* Applies the given consumer to generated single output PDF document
*
* @param consumer
* @return
* @throws IOException
*/
public TaskTestContext forPdfOutput(Consumer<PDDocument> consumer) {
requirePDDocument();
consumer.accept(outputDocument);
return this;
}
/**
* Applies the given consumer to generated output PDF document with the given name
*
* @param consumer
* @return
* @throws IOException
*/
public TaskTestContext forPdfOutput(String filename, Consumer<PDDocument> consumer) throws IOException {
requireMultipleOutputs();
assertTrue("Not a PDF output",
isNotEmpty(filename) && filename.toLowerCase().endsWith(SejdaFileExtensions.PDF_EXTENSION));
try (PDDocument doc = PDFParser.parse(SeekableSources.seekableSourceFrom(new File(fileOutput, filename)))) {
consumer.accept(doc);
}
return this;
}
/**
* Applies the given consumer to every generated output
*
* @param consumer
* @return
* @throws IOException
*/
public TaskTestContext forEachRawOutput(Consumer<Path> consumer) throws IOException {
requireMultipleOutputs();
Files.list(fileOutput.toPath()).forEach(consumer);
return this;
}
/**
* Applies the given consumer to a single generated output
*
* @param consumer
* @return
* @throws IOException
*/
public TaskTestContext forRawOutput(Consumer<Path> consumer) {
assertNotNull(fileOutput);
consumer.accept(fileOutput.toPath());
return this;
}
private void requireMultipleOutputs() {
assertNotNull(fileOutput);
assertTrue("Expected an output directory", fileOutput.isDirectory());
assertTrue("No output has been created", fileOutput.listFiles().length > 0);
}
private void requirePDDocument() {
assertNotNull(
"No output document, make sure to call TaskTestContext::assertTaskCompleted before any other assert method",
outputDocument);
}
/**
* Asserts that the task has completed and generated some output. If a single output task, then the output is paresed and a {@link PDDocument} is returned
*
* @return
* @throws IOException
*/
public PDDocument assertTaskCompleted() throws IOException {
return this.assertTaskCompleted(null);
}
/**
* Asserts that the task has completed and generated some output. If the task generated a single output, then the output is parsed and a {@link PDDocument} is returned
*
* @param password
* @return
* @throws IOException
*/
public PDDocument assertTaskCompleted(String password) throws IOException {
if (nonNull(fileOutput)) {
if (fileOutput.isDirectory()) {
File[] files = fileOutput.listFiles();
assertTrue("No output has been created", files.length > 0);
if (files.length == 1) {
initOutputFromSource(files[0], password);
}
} else {
initOutputFromSource(fileOutput, password);
}
} else if (nonNull(streamOutput)) {
org.sejda.util.IOUtils.close(streamOutput);
initOutputFromSource(SeekableSources.inMemorySeekableSourceFrom(streamOutput.toByteArray()), password);
}
return outputDocument;
}
Throwable taskFailureCause = null;
List<String> taskWarnings = new ArrayList<>();
private EventListener<TaskExecutionWarningEvent> warningsListener = new EventListener<TaskExecutionWarningEvent>() {
@Override
public void onEvent(TaskExecutionWarningEvent event) {
taskWarnings.add(event.getWarning());
}
};
private EventListener<TaskExecutionFailedEvent> failureListener = new EventListener<TaskExecutionFailedEvent>() {
@Override
public void onEvent(TaskExecutionFailedEvent event) {
taskFailureCause = event.getFailingCause();
}
};
public void expectTaskWillFail() {
taskFailureCause = null;
GlobalNotificationContext.getContext().removeListener(failureListener);
GlobalNotificationContext.getContext().addListener(failureListener);
}
public void assertTaskFailed(String message) {
assertNotNull(taskFailureCause);
assertThat(taskFailureCause.getMessage(), startsWith(message));
}
public void expectTaskWillProduceWarnings() {
taskWarnings = new ArrayList<>();
GlobalNotificationContext.getContext().removeListener(warningsListener);
GlobalNotificationContext.getContext().addListener(warningsListener);
}
public void assertTaskWarning(String message) {
assertThat(taskWarnings, hasItem(message));
}
public void assertNoTaskWarnings() {
assertEquals("Expected no warnings but got: " + StringUtils.join(taskWarnings, ","), taskWarnings.size(), 0);
}
private void initOutputFromSource(File source, String password) throws IOException {
if (source.getName().toLowerCase().endsWith(SejdaFileExtensions.PDF_EXTENSION)) {
this.outputDocument = PDFParser.parse(SeekableSources.seekableSourceFrom(source), password);
assertNotNull(outputDocument);
}
}
private void initOutputFromSource(SeekableSource source, String password) throws IOException {
this.outputDocument = PDFParser.parse(source, password);
assertNotNull(outputDocument);
}
@Override
public void close() throws IOException {
IOUtils.closeQuietly(streamOutput);
this.streamOutput = null;
IOUtils.closeQuietly(outputDocument);
this.outputDocument = null;
if (nonNull(fileOutput)) {
if (fileOutput.isDirectory()) {
Files.walkFileTree(fileOutput.toPath(), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
Files.deleteIfExists(fileOutput.toPath());
}
this.fileOutput = null;
}
public File getFileOutput() {
return fileOutput;
}
}