Skip to content
Snippets Groups Projects
Commit 9d3ff13e authored by Marius Dumitru Florea's avatar Marius Dumitru Florea
Browse files

XWIKI-19270: Add support for performing the PDF export using a browser running...

XWIKI-19270: Add support for performing the PDF export using a browser running in a Docker container
* More tests
parent 0a0087ef
No related branches found
No related tags found
No related merge requests found
Showing
with 509 additions and 10 deletions
......@@ -36,6 +36,7 @@
<xwiki.extension.name>PDF Export Default API Implementation</xwiki.extension.name>
<dockerJava.version>3.2.13</dockerJava.version>
<checkstyle.suppressions.location>${basedir}/src/checkstyle/checkstyle-suppressions.xml</checkstyle.suppressions.location>
<xwiki.jacoco.instructionRatio>0.34</xwiki.jacoco.instructionRatio>
</properties>
<dependencies>
<dependency>
......
......@@ -35,14 +35,13 @@
*/
public class PrintToPDFInputStream extends InputStream
{
/**
* Read chunks of 1MB.
*/
private static final int BUFFER_SIZE = 1 << 20;
private final IO io;
private final String stream;
private IO io;
private final Runnable closeCallback;
private String stream;
private final int bufferSize;
private boolean finished;
......@@ -50,7 +49,17 @@ public class PrintToPDFInputStream extends InputStream
private byte[] buffer = new byte[] {};
private Runnable closeCallback;
/**
* Creates a new instance for reading the specified PDF stream.
*
* @param io the service used to read the PDF data
* @param stream a handle of the stream that holds the PDF data
*/
public PrintToPDFInputStream(IO io, String stream)
{
this(io, stream, () -> {
});
}
/**
* Creates a new instance for reading the specified PDF stream.
......@@ -60,10 +69,25 @@ public class PrintToPDFInputStream extends InputStream
* @param closeCallback the code to execute when this input stream is closed
*/
public PrintToPDFInputStream(IO io, String stream, Runnable closeCallback)
{
// Read chunks of 1MB.
this(io, stream, closeCallback, 1 << 20);
}
/**
* Creates a new instance for reading the specified PDF stream.
*
* @param io the service used to read the PDF data
* @param stream a handle of the stream that holds the PDF data
* @param closeCallback the code to execute when this input stream is closed
* @param bufferSize the maximum number of bytes to read at once from the specified stream
*/
public PrintToPDFInputStream(IO io, String stream, Runnable closeCallback, int bufferSize)
{
this.io = io;
this.stream = stream;
this.closeCallback = closeCallback;
this.bufferSize = bufferSize;
}
@Override
......@@ -89,7 +113,7 @@ private byte[] readBuffer()
return new byte[] {};
}
Read read = this.io.read(this.stream, null, BUFFER_SIZE);
Read read = this.io.read(this.stream, null, this.bufferSize);
this.finished = read.getEof() == Boolean.TRUE;
if (read.getBase64Encoded() == Boolean.TRUE) {
return Base64.getDecoder().decode(read.getData());
......
......@@ -87,9 +87,13 @@ public class DefaultPDFExportJobRequestFactory implements PDFExportJobRequestFac
@Override
public PDFExportJobRequest createRequest() throws Exception
{
PDFExportJobRequest request = new PDFExportJobRequest();
String suffix = new Date().getTime() + "-" + ThreadLocalRandom.current().nextInt(100, 1000);
return createRequest(suffix);
}
protected PDFExportJobRequest createRequest(String suffix) throws Exception
{
PDFExportJobRequest request = new PDFExportJobRequest();
request.setId(EXPORT, "pdf", suffix);
setRightsProperties(request);
......
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.export.pdf.internal.chrome;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import org.jodconverter.core.util.IOUtils;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.xwiki.test.junit5.mockito.ComponentTest;
import com.github.kklisura.cdt.protocol.commands.IO;
import com.github.kklisura.cdt.protocol.types.io.Read;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link PrintToPDFInputStream}.
*
* @version $Id$
*/
@ComponentTest
class PrintToPDFInputStreamTest
{
@Mock
private IO io;
@Mock
private Runnable closeCallback;
@Test
void read() throws IOException
{
assertRead("The quick brown fox jumps over the laz\u00FF dog.", 5, false);
}
@Test
void readBase64Encoded() throws IOException
{
assertRead("The quick brown fox jumps over the laz\u00FF dog.", 8, true);
}
private void assertRead(String content, int bufferSize, boolean useBase64Encoding) throws IOException
{
String streamHandle = "test";
setUpStream(streamHandle, content, bufferSize, useBase64Encoding);
PrintToPDFInputStream inputStream =
new PrintToPDFInputStream(this.io, streamHandle, this.closeCallback, bufferSize);
assertEquals(content, IOUtils.toString(inputStream, StandardCharsets.UTF_8));
verify(this.closeCallback).run();
}
private void setUpStream(String streamHandle, String content, int bufferSize, boolean useBase64Encoding)
{
List<Read> reads = getReads(content, bufferSize, useBase64Encoding);
if (reads.size() > 1) {
when(this.io.read(streamHandle, null, bufferSize)).thenReturn(reads.get(0),
reads.subList(1, reads.size()).toArray(new Read[] {}));
} else if (reads.size() == 1) {
when(this.io.read(streamHandle, null, bufferSize)).thenReturn(reads.get(0));
}
}
private List<Read> getReads(String content, int bufferSize, boolean useBase64Encoding)
{
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
if (useBase64Encoding) {
bytes = Base64.getEncoder().encode(bytes);
}
int count = bytes.length / bufferSize;
if (bytes.length % bufferSize != 0) {
count++;
}
List<Read> reads = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
Read read = new Read();
read.setBase64Encoded(useBase64Encoding);
read.setEof(i == count - 1);
int offset = i * bufferSize;
read.setData(
new String(bytes, offset, Math.min(bufferSize, bytes.length - offset), StandardCharsets.UTF_8));
reads.add(read);
}
return reads;
}
}
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.export.pdf.internal.docker;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import javax.servlet.http.Cookie;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.xwiki.export.pdf.PDFExportConfiguration;
import org.xwiki.export.pdf.internal.chrome.ChromeManager;
import org.xwiki.export.pdf.internal.job.PDFExportContextStore;
import org.xwiki.export.pdf.job.PDFExportJobRequest;
import org.xwiki.test.annotation.BeforeComponent;
import org.xwiki.test.junit5.mockito.ComponentTest;
import org.xwiki.test.junit5.mockito.InjectMockComponents;
import org.xwiki.test.junit5.mockito.MockComponent;
import com.github.kklisura.cdt.protocol.types.network.CookieParam;
import com.github.kklisura.cdt.services.ChromeDevToolsService;
import com.xpn.xwiki.internal.context.XWikiContextContextStore;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link DockerPDFPrinter}.
*
* @version $Id$
*/
@ComponentTest
class DockerPDFPrinterTest
{
@InjectMockComponents
private DockerPDFPrinter dockerPDFPrinter;
@MockComponent
private PDFExportConfiguration configuration;
@MockComponent
private ChromeManager chromeManager;
@MockComponent
private ContainerManager containerManager;
@Mock
ChromeDevToolsService tabDevToolsService;
private PDFExportJobRequest request = new PDFExportJobRequest();
private String containerId = "8f55a905efec";
@BeforeComponent
void configure()
{
this.request.setContext(new HashMap<>());
when(this.configuration.getChromeDockerContainerName()).thenReturn("test-pdf-printer");
when(this.configuration.getChromeDockerImage()).thenReturn("test/chrome:latest");
when(this.configuration.getChromeDockerHostName()).thenReturn("docker");
when(this.configuration.getChromeRemoteDebuggingPort()).thenReturn(1234);
}
@Test
void printWithoutPreviewURL()
{
try {
this.dockerPDFPrinter.print(this.request);
fail();
} catch (IOException e) {
assertEquals("Print preview URL missing.", e.getMessage());
}
}
@Test
void printWithUnsupportedProtocol() throws Exception
{
this.request.getContext().put(XWikiContextContextStore.PROP_REQUEST_URL, new URL("file://some/file.txt"));
try {
this.dockerPDFPrinter.print(this.request);
fail();
} catch (IOException e) {
assertEquals("Unsupported protocol [file].", e.getMessage());
}
}
@Test
void print() throws Exception
{
URL printPreviewURL = new URL("http://localhost:8080/xwiki/bin/export/Some/Page");
URL dockerPrintPreviewURL = new URL("http://docker:8080/xwiki/bin/export/Some/Page");
this.request.getContext().put(XWikiContextContextStore.PROP_REQUEST_URL, printPreviewURL);
Cookie[] cookies = new Cookie[] {};
this.request.getContext().put(PDFExportContextStore.ENTRY_COOKIES, cookies);
CookieParam cookieParam = new CookieParam();
List<CookieParam> cookieParams = Collections.singletonList(cookieParam);
when(this.chromeManager.toCookieParams(cookies)).thenReturn(cookieParams);
when(this.chromeManager.createIncognitoTab()).thenReturn(this.tabDevToolsService);
InputStream pdfInputStream = mock(InputStream.class);
when(this.chromeManager.printToPDF(same(this.tabDevToolsService), any(Runnable.class)))
.then(new Answer<InputStream>()
{
@Override
public InputStream answer(InvocationOnMock invocation) throws Throwable
{
try {
return pdfInputStream;
} finally {
invocation.getArgument(1, Runnable.class).run();
}
}
});
assertSame(pdfInputStream, this.dockerPDFPrinter.print(this.request));
verify(this.chromeManager).setCookies(this.tabDevToolsService, cookieParams);
assertEquals(dockerPrintPreviewURL.toString(), cookieParam.getUrl());
verify(this.chromeManager).navigate(this.tabDevToolsService, dockerPrintPreviewURL);
verify(this.chromeManager).setBaseURL(this.tabDevToolsService, printPreviewURL);
verify(this.chromeManager).closeIncognitoTab(this.tabDevToolsService);
}
@BeforeComponent("initializeAndDispose")
void beforeInitializeAndDispose()
{
when(this.containerManager.maybeReuseContainerByName(this.configuration.getChromeDockerContainerName()))
.thenReturn(null);
when(this.containerManager.isLocalImagePresent(this.configuration.getChromeDockerImage())).thenReturn(false);
when(this.containerManager.createContainer(this.configuration.getChromeDockerImage(),
this.configuration.getChromeDockerContainerName(), this.configuration.getChromeRemoteDebuggingPort(),
Arrays.asList("--no-sandbox", "--remote-debugging-address=0.0.0.0",
"--remote-debugging-port=" + this.configuration.getChromeRemoteDebuggingPort())))
.thenReturn(this.containerId);
}
@Test
void initializeAndDispose() throws Exception
{
verify(this.containerManager).pullImage(this.configuration.getChromeDockerImage());
verify(this.containerManager).startContainer(this.containerId);
this.dockerPDFPrinter.dispose();
verify(this.containerManager).stopContainer(this.containerId);
}
@BeforeComponent("initializeWithExistingContainer")
void beforeInitializeWithExistingContainer()
{
when(this.containerManager.maybeReuseContainerByName(this.configuration.getChromeDockerContainerName()))
.thenReturn(this.containerId);
}
@Test
void initializeWithExistingContainer() throws Exception
{
verify(this.containerManager, never()).startContainer(any(String.class));
}
}
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.export.pdf.internal.job;
import java.io.Serializable;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Named;
import javax.inject.Provider;
import org.junit.jupiter.api.Test;
import org.xwiki.context.concurrent.ContextStoreManager;
import org.xwiki.export.pdf.job.PDFExportJobRequest;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.model.reference.EntityReferenceSerializer;
import org.xwiki.test.junit5.mockito.ComponentTest;
import org.xwiki.test.junit5.mockito.InjectMockComponents;
import org.xwiki.test.junit5.mockito.MockComponent;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.internal.export.DocumentSelectionResolver;
import com.xpn.xwiki.web.XWikiRequest;
import com.xpn.xwiki.web.XWikiURLFactory;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link DefaultPDFExportJobRequestFactory}.
*
* @version $Id$
*/
@ComponentTest
class DefaultPDFExportJobRequestFactoryTest
{
@InjectMockComponents
private DefaultPDFExportJobRequestFactory requestFactory;
@MockComponent
private Provider<XWikiContext> xcontextProvider;
@MockComponent
private DocumentSelectionResolver documentSelectionResolver;
@MockComponent
@Named("current")
private DocumentReferenceResolver<String> currentDocumentReferenceResolver;
@MockComponent
@Named("local")
private EntityReferenceSerializer<String> localStringEntityReferenceSerializer;
@MockComponent
private ContextStoreManager contextStoreManager;
@Test
void createRequest() throws Exception
{
// Setup
DocumentReference aliceReference = new DocumentReference("test", "Users", "Alice");
DocumentReference bobReference = new DocumentReference("test", "Users", "Bob");
XWikiContext xcontext = mock(XWikiContext.class);
when(xcontext.getUserReference()).thenReturn(aliceReference);
when(xcontext.getAuthorReference()).thenReturn(bobReference);
XWikiRequest httpRequest = mock(XWikiRequest.class);
when(xcontext.getRequest()).thenReturn(httpRequest);
when(httpRequest.get("pdfQueryString")).thenReturn("color=red");
when(httpRequest.get("pdfHash")).thenReturn("foo");
XWikiDocument currentDocument = mock(XWikiDocument.class);
when(currentDocument.getDocumentReference()).thenReturn(new DocumentReference("test", "Some", "Page"));
when(xcontext.getDoc()).thenReturn(currentDocument);
when(this.localStringEntityReferenceSerializer
.serialize(currentDocument.getDocumentReference().getLastSpaceReference())).thenReturn("Some");
XWikiURLFactory urlFactory = mock(XWikiURLFactory.class);
when(xcontext.getURLFactory()).thenReturn(urlFactory);
URL printPreviewURL = new URL("http://localhost:8080/xwiki/bin/export/Some/Page?key=value#hash");
String queryString = "format=html-print&xpage=get&outputSyntax=plain&async=true&"
+ "sheet=XWiki.PDFExport.Sheet&jobId=export%2Fpdf%2Ftest&color=red";
when(urlFactory.createExternalURL("Some", "Page", "export", queryString, "foo", "test", xcontext))
.thenReturn(printPreviewURL);
when(this.xcontextProvider.get()).thenReturn(xcontext);
List<String> supportedContextEntries = Arrays.asList("one", "two");
when(this.contextStoreManager.getSupportedEntries()).thenReturn(supportedContextEntries);
Map<String, Serializable> pdfExportContext = new HashMap<>();
when(this.contextStoreManager.save(supportedContextEntries)).thenReturn(pdfExportContext);
List<DocumentReference> selectedDocuments = Arrays.asList(new DocumentReference("test", "First", "Page"),
new DocumentReference("test", "Second", "Page"));
when(this.documentSelectionResolver.getSelectedDocuments()).thenReturn(selectedDocuments);
when(httpRequest.get("pdftemplate")).thenReturn("Some.Template");
DocumentReference templateReference = new DocumentReference("test", "Some", "Template");
when(this.currentDocumentReferenceResolver.resolve("Some.Template")).thenReturn(templateReference);
when(httpRequest.get("pdfcover")).thenReturn("1");
when(httpRequest.get("pdftoc")).thenReturn("0");
when(httpRequest.get("pdfheader")).thenReturn("false");
// Execution
PDFExportJobRequest request = this.requestFactory.createRequest("test");
// Checks
assertEquals(3, request.getId().size());
assertEquals(Arrays.asList("export", "pdf"), request.getId().subList(0, 2));
assertTrue(request.isCheckRights());
assertEquals(aliceReference, request.getUserReference());
assertEquals(bobReference, request.getAuthorReference());
assertEquals("export", request.getContext().get("action"));
@SuppressWarnings("unchecked")
Map<String, String[]> requestParameters =
(Map<String, String[]>) request.getContext().get("request.parameters");
assertEquals(Collections.singleton("key"), requestParameters.keySet());
assertEquals(Collections.singletonList("value"), Arrays.asList(requestParameters.get("key")));
assertEquals(printPreviewURL, request.getContext().get("request.url"));
assertEquals(selectedDocuments, request.getDocuments());
assertEquals(templateReference, request.getTemplate());
assertTrue(request.isWithCover());
assertFalse(request.isWithToc());
assertTrue(request.isWithHeader());
assertTrue(request.isWithFooter());
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment