You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tika.apache.org by ta...@apache.org on 2018/05/25 20:18:24 UTC

[tika] branch TIKA-2653 created (now bcf09e0)

This is an automated email from the ASF dual-hosted git repository.

tallison pushed a change to branch TIKA-2653
in repository https://gitbox.apache.org/repos/asf/tika.git.


      at bcf09e0  TIKA-2653 first, rough draft

This branch includes the following new commits:

     new bcf09e0  TIKA-2653 first, rough draft

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


-- 
To stop receiving notification emails like this one, please contact
tallison@apache.org.

[tika] 01/01: TIKA-2653 first, rough draft

Posted by ta...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

tallison pushed a commit to branch TIKA-2653
in repository https://gitbox.apache.org/repos/asf/tika.git

commit bcf09e04d7b232a3d5ac56bd858045e63e41205d
Author: tballison <ta...@mitre.org>
AuthorDate: Fri May 25 16:18:10 2018 -0400

    TIKA-2653 first, rough draft
---
 .../main/java/org/apache/tika/fork/ForkClient.java |  41 ++++++
 .../main/java/org/apache/tika/fork/ForkParser.java |  33 ++++-
 .../main/java/org/apache/tika/fork/ForkServer.java |  26 +++-
 .../java/org/apache/tika/parser/ParserFactory.java |  36 +++++
 .../apache/tika/parser/ParserFactoryFactory.java   |  53 +++++++
 .../java/org/apache/tika/utils/ProcessUtils.java   |  25 ++++
 .../java/org/apache/tika/utils/SystemUtils.java    |  61 ++++++++
 .../java/org/apache/tika/fork/ForkParserTest.java  |   8 ++
 .../apache/tika/fork/ForkParserTikaBinTest.java    | 153 +++++++++++++++++++++
 .../apache/tika/parser/mock/MockParserFactory.java |  66 +++++++++
 .../src/test/resources/test-documents/example.xml  |  47 +++++++
 .../src/test/resources/test-documents/testPST.pst  | Bin 271360 -> 271360 bytes
 .../test-documents/testPST_variousBodyTypes.pst    | Bin 271360 -> 271360 bytes
 13 files changed, 542 insertions(+), 7 deletions(-)

diff --git a/tika-core/src/main/java/org/apache/tika/fork/ForkClient.java b/tika-core/src/main/java/org/apache/tika/fork/ForkClient.java
index dd1d033..c9830ab 100644
--- a/tika-core/src/main/java/org/apache/tika/fork/ForkClient.java
+++ b/tika-core/src/main/java/org/apache/tika/fork/ForkClient.java
@@ -25,6 +25,7 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.NotSerializableException;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.jar.JarEntry;
@@ -33,6 +34,8 @@ import java.util.zip.ZipEntry;
 
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.io.IOUtils;
+import org.apache.tika.parser.ParserFactoryFactory;
+import org.apache.tika.utils.ProcessUtils;
 import org.xml.sax.ContentHandler;
 
 class ForkClient {
@@ -51,6 +54,44 @@ class ForkClient {
 
     private final InputStream error;
 
+    public ForkClient(Path tikaDir, ParserFactoryFactory parserFactoryFactory, List<String> java, long serverPulseMillis) throws IOException, TikaException {
+        jar = null;
+        loader = null;
+        boolean ok = false;
+        ProcessBuilder builder = new ProcessBuilder();
+        List<String> command = new ArrayList<>();
+        command.addAll(java);
+        command.add("-cp");
+        String dirString = tikaDir.toAbsolutePath().toString();
+        if (!dirString.endsWith("/")) {
+            dirString += "/*";
+        } else {
+            dirString += "/";
+        }
+        dirString = ProcessUtils.escapeCommandLine(dirString);
+        command.add(dirString);
+        command.add("org.apache.tika.fork.ForkServer");
+        command.add(Long.toString(serverPulseMillis));
+        builder.command(command);
+        try {
+            this.process = builder.start();
+
+            this.output = new DataOutputStream(process.getOutputStream());
+            this.input = new DataInputStream(process.getInputStream());
+            this.error = process.getErrorStream();
+
+            waitForStartBeacon();
+
+            sendObject(parserFactoryFactory, resources);
+
+            ok = true;
+        } finally {
+            if (!ok) {
+                close();
+            }
+        }
+    }
+
     public ForkClient(ClassLoader loader, Object object, List<String> java, long serverPulseMillis)
             throws IOException, TikaException {
         boolean ok = false;
diff --git a/tika-core/src/main/java/org/apache/tika/fork/ForkParser.java b/tika-core/src/main/java/org/apache/tika/fork/ForkParser.java
index c69b11c..580073a 100644
--- a/tika-core/src/main/java/org/apache/tika/fork/ForkParser.java
+++ b/tika-core/src/main/java/org/apache/tika/fork/ForkParser.java
@@ -18,6 +18,7 @@ package org.apache.tika.fork;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -33,6 +34,8 @@ import org.apache.tika.parser.AbstractParser;
 import org.apache.tika.parser.AutoDetectParser;
 import org.apache.tika.parser.ParseContext;
 import org.apache.tika.parser.Parser;
+import org.apache.tika.parser.ParserFactory;
+import org.apache.tika.parser.ParserFactoryFactory;
 import org.apache.tika.sax.TeeContentHandler;
 import org.xml.sax.ContentHandler;
 import org.xml.sax.SAXException;
@@ -42,10 +45,15 @@ public class ForkParser extends AbstractParser {
     /** Serial version UID */
     private static final long serialVersionUID = -4962742892274663950L;
 
+    //these are used by the legacy usage
     private final ClassLoader loader;
-
     private final Parser parser;
 
+    //these are used when the server builds a parser via a directory
+    //of jars, not via legacy bootstrap etc.
+    private final Path tikaBin;
+    private final ParserFactoryFactory parserFactoryFactory;
+
     /** Java command line */
     private List<String> java = Arrays.asList("java", "-Xmx32m");
 
@@ -58,6 +66,13 @@ public class ForkParser extends AbstractParser {
 
     private long serverPulseMillis = 5000;
 
+    public ForkParser(Path tikaBin, ParserFactoryFactory factoryFactory) {
+        loader = null;
+        parser = null;
+        this.tikaBin = tikaBin;
+        this.parserFactoryFactory = factoryFactory;
+    }
+
     /**
      * @param loader The ClassLoader to use 
      * @param parser the parser to delegate to. This one cannot be another ForkParser
@@ -66,6 +81,8 @@ public class ForkParser extends AbstractParser {
         if (parser instanceof ForkParser) {
             throw new IllegalArgumentException("The underlying parser of a ForkParser should not be a ForkParser, but a specific implementation.");
         }
+        this.tikaBin = null;
+        this.parserFactoryFactory = null;
         this.loader = loader;
         this.parser = parser;
     }
@@ -214,7 +231,7 @@ public class ForkParser extends AbstractParser {
 
             // Create a new process if there's room in the pool
             if (client == null && currentlyInUse < poolSize) {
-                client = new ForkClient(loader, parser, java, serverPulseMillis);
+                client = newClient();
             }
 
             // Ping the process, and get rid of it if it's inactive
@@ -237,6 +254,18 @@ public class ForkParser extends AbstractParser {
         }
     }
 
+    private ForkClient newClient() throws IOException, TikaException {
+        if (loader == null && parser == null && tikaBin != null && parserFactoryFactory != null) {
+            return new ForkClient(tikaBin, parserFactoryFactory, java, serverPulseMillis);
+        } else if (loader != null && parser != null && tikaBin == null && parserFactoryFactory == null) {
+           return new ForkClient(loader, parser, java, serverPulseMillis);
+        } else {
+            throw new IllegalStateException("Either a) loader and parser must be not null " +
+                    "or b) tikaBin and parserFactoryFactory must not be null");
+        }
+
+    }
+
     private synchronized void releaseClient(ForkClient client, boolean alive) {
         currentlyInUse--;
         if (currentlyInUse + pool.size() < poolSize && alive) {
diff --git a/tika-core/src/main/java/org/apache/tika/fork/ForkServer.java b/tika-core/src/main/java/org/apache/tika/fork/ForkServer.java
index 68d203d..364b94f 100644
--- a/tika-core/src/main/java/org/apache/tika/fork/ForkServer.java
+++ b/tika-core/src/main/java/org/apache/tika/fork/ForkServer.java
@@ -31,6 +31,8 @@ import java.util.zip.CheckedOutputStream;
 import java.util.zip.Checksum;
 
 import org.apache.tika.exception.TikaException;
+import org.apache.tika.parser.ParserFactory;
+import org.apache.tika.parser.ParserFactoryFactory;
 
 class ForkServer implements Runnable, Checksum {
 
@@ -117,11 +119,26 @@ class ForkServer implements Runnable, Checksum {
             output.writeByte(READY);
             output.flush();
 
-            ClassLoader loader = (ClassLoader) readObject(
+            final ClassLoader loader;
+            Object parser = null;
+            Object firstObject = readObject(
                     ForkServer.class.getClassLoader());
-            Thread.currentThread().setContextClassLoader(loader);
 
-            Object object = readObject(loader);
+            //legacy behavior
+            if (firstObject instanceof ClassLoader) {
+                loader = (ClassLoader) firstObject;
+                Thread.currentThread().setContextClassLoader(loader);
+                //parser from parent process
+                parser = readObject(loader);
+            } else if (firstObject instanceof ParserFactoryFactory) {
+                //the user has submitted a parser factory
+                loader = ForkServer.class.getClassLoader();
+                ParserFactory parserFactory = ((ParserFactoryFactory) firstObject).build();
+                parser = parserFactory.build();
+            } else {
+                throw new IllegalStateException("Was expecting ClassLoader or ParserFactoryFactory, not"+firstObject.getClass());
+            }
+
             while (true) {
                 int request = input.read();
                 if (request == -1) {
@@ -129,7 +146,7 @@ class ForkServer implements Runnable, Checksum {
                 } else if (request == PING) {
                     output.writeByte(PING);
                 } else if (request == CALL) {
-                    call(loader, object);
+                    call(loader, parser);
                 } else {
                     throw new IllegalStateException("Unexpected request");
                 }
@@ -187,7 +204,6 @@ class ForkServer implements Runnable, Checksum {
      * is expected to be preceded by a size integer, that is used for reading
      * the entire serialization into a memory before deserializing it.
      *
-     * @param input input stream from which the serialized object is read
      * @param loader class loader to be used for loading referenced classes
      * @throws IOException if the object could not be deserialized
      * @throws ClassNotFoundException if a referenced class is not found
diff --git a/tika-core/src/main/java/org/apache/tika/parser/ParserFactory.java b/tika-core/src/main/java/org/apache/tika/parser/ParserFactory.java
new file mode 100644
index 0000000..c8a92e7
--- /dev/null
+++ b/tika-core/src/main/java/org/apache/tika/parser/ParserFactory.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tika.parser;
+
+
+import org.apache.tika.exception.TikaException;
+
+import java.io.IOException;
+import java.util.Map;
+
+public abstract class ParserFactory {
+
+    final Map<String, String> args;
+
+    public ParserFactory(Map<String, String> args) {
+        this.args = args;
+    }
+
+    public abstract Parser build() throws IOException, TikaException;
+
+}
diff --git a/tika-core/src/main/java/org/apache/tika/parser/ParserFactoryFactory.java b/tika-core/src/main/java/org/apache/tika/parser/ParserFactoryFactory.java
new file mode 100644
index 0000000..e44ba80
--- /dev/null
+++ b/tika-core/src/main/java/org/apache/tika/parser/ParserFactoryFactory.java
@@ -0,0 +1,53 @@
+package org.apache.tika.parser;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.tika.exception.TikaException;
+
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.util.Map;
+
+/**
+ * Lightweight, easily serializable class that contains enough information
+ * to build a {@link ParserFactory}
+ */
+public class ParserFactoryFactory implements Serializable {
+
+    /** Serial version UID */
+    private static final long serialVersionUID = 4710974869988895410L;
+
+    private final String className;
+    private final Map<String, String> args;
+
+    public ParserFactoryFactory(String className, Map<String, String> args) {
+        this.className = className;
+        this.args = args;
+    }
+
+    public ParserFactory build() throws TikaException {
+        try {
+            Class<?> clazz = Class.forName(className);
+            Constructor<?> con = clazz.getConstructor(Map.class);
+            return (ParserFactory) con.newInstance(args);
+        } catch (ReflectiveOperationException|IllegalStateException e) {
+            throw new TikaException("Couldn't create factory", e);
+        }
+    }
+
+}
diff --git a/tika-core/src/main/java/org/apache/tika/utils/ProcessUtils.java b/tika-core/src/main/java/org/apache/tika/utils/ProcessUtils.java
new file mode 100644
index 0000000..588a00b
--- /dev/null
+++ b/tika-core/src/main/java/org/apache/tika/utils/ProcessUtils.java
@@ -0,0 +1,25 @@
+package org.apache.tika.utils;
+
+
+public class ProcessUtils {
+
+    /**
+     * This should correctly put double-quotes around an argument if
+     * ProcessBuilder doesn't seem to work (as it doesn't
+     * on paths with spaces on Windows)
+     *
+     * @param arg
+     * @return
+     */
+    public static String escapeCommandLine(String arg) {
+        if (arg == null) {
+            return arg;
+        }
+        //need to test for " " on windows, can't just add double quotes
+        //across platforms.
+        if (arg.contains(" ") && SystemUtils.IS_OS_WINDOWS) {
+            arg = "\"" + arg + "\"";
+        }
+        return arg;
+    }
+}
diff --git a/tika-core/src/main/java/org/apache/tika/utils/SystemUtils.java b/tika-core/src/main/java/org/apache/tika/utils/SystemUtils.java
new file mode 100644
index 0000000..889f62c
--- /dev/null
+++ b/tika-core/src/main/java/org/apache/tika/utils/SystemUtils.java
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.utils;
+
+/**
+ * Copied from commons-lang to avoid requiring the dependency
+ */
+public class SystemUtils {
+
+    private static final String OS_NAME_WINDOWS_PREFIX = "Windows";
+    public static final String OS_NAME = getSystemProperty("os.name");
+    public static final String OS_VERSION = getSystemProperty("os.version");
+    public static final boolean IS_OS_AIX = getOSMatchesName("AIX");
+    public static final boolean IS_OS_HP_UX = getOSMatchesName("HP-UX");
+    public static final boolean IS_OS_IRIX = getOSMatchesName("Irix");
+    public static final boolean IS_OS_LINUX = getOSMatchesName("Linux") || getOSMatchesName("LINUX");
+    public static final boolean IS_OS_MAC = getOSMatchesName("Mac");
+    public static final boolean IS_OS_MAC_OSX = getOSMatchesName("Mac OS X");
+    public static final boolean IS_OS_OS2 = getOSMatchesName("OS/2");
+    public static final boolean IS_OS_SOLARIS = getOSMatchesName("Solaris");
+    public static final boolean IS_OS_SUN_OS = getOSMatchesName("SunOS");
+    public static final boolean IS_OS_UNIX;
+    public static final boolean IS_OS_WINDOWS;
+
+
+    private static String getSystemProperty(String property) {
+        try {
+            return System.getProperty(property);
+        } catch (SecurityException var2) {
+            return null;
+        }
+    }
+
+    private static boolean getOSMatchesName(String osNamePrefix) {
+        return isOSNameMatch(OS_NAME, osNamePrefix);
+    }
+
+    static boolean isOSNameMatch(String osName, String osNamePrefix) {
+        return osName == null ? false : osName.startsWith(osNamePrefix);
+    }
+
+    static {
+        IS_OS_UNIX = IS_OS_AIX || IS_OS_HP_UX || IS_OS_IRIX || IS_OS_LINUX || IS_OS_MAC_OSX || IS_OS_SOLARIS || IS_OS_SUN_OS;
+        IS_OS_WINDOWS = getOSMatchesName(OS_NAME_WINDOWS_PREFIX);
+    }
+
+}
diff --git a/tika-core/src/test/java/org/apache/tika/fork/ForkParserTest.java b/tika-core/src/test/java/org/apache/tika/fork/ForkParserTest.java
index 01e08d9..ff9324a 100644
--- a/tika-core/src/test/java/org/apache/tika/fork/ForkParserTest.java
+++ b/tika-core/src/test/java/org/apache/tika/fork/ForkParserTest.java
@@ -26,6 +26,7 @@ import java.io.InputStream;
 import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Paths;
 import java.util.concurrent.Semaphore;
 
 import org.apache.tika.exception.TikaException;
@@ -235,4 +236,11 @@ public class ForkParserTest {
             parser.close();
         }
     }
+
+    @Test
+    public void testForkParserWithTikaBinPath() throws Exception {
+        ClassLoader loader = this.getClass().getClassLoader();
+
+        System.out.println("FORKPARSERTEST: " + Paths.get(".").toAbsolutePath() + " : "+System.getProperty("java.classpath"));
+    }
 }
diff --git a/tika-core/src/test/java/org/apache/tika/fork/ForkParserTikaBinTest.java b/tika-core/src/test/java/org/apache/tika/fork/ForkParserTikaBinTest.java
new file mode 100644
index 0000000..66f30b5
--- /dev/null
+++ b/tika-core/src/test/java/org/apache/tika/fork/ForkParserTikaBinTest.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.fork;
+
+import org.apache.tika.TikaTest;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.io.IOExceptionWithCause;
+import org.apache.tika.io.IOUtils;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.AbstractParser;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+import org.apache.tika.parser.ParserFactory;
+import org.apache.tika.parser.ParserFactoryFactory;
+import org.apache.tika.parser.mock.MockParser;
+import org.apache.tika.parser.mock.MockParserFactory;
+import org.apache.tika.sax.BodyContentHandler;
+import org.apache.tika.sax.ToHTMLContentHandler;
+import org.apache.tika.sax.ToXMLContentHandler;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Semaphore;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class ForkParserTikaBinTest extends TikaTest {
+    private static Path JAR_DIR;
+    private static final String JAR_FILE_NAME = "mock-tika-app.jar";
+    private static Path JAR_FILE;
+
+    @BeforeClass
+    public static void bootstrapJar() throws Exception {
+        JAR_DIR = Files.createTempDirectory("tika-fork-tikabin-");
+        JAR_FILE = JAR_DIR.resolve(JAR_FILE_NAME);
+        try (JarOutputStream jarOs = new JarOutputStream(Files.newOutputStream(JAR_FILE))) {
+            ClassLoader loader = ForkServer.class.getClassLoader();
+            for (Class<?> klass : getClasses("org.apache.tika")) {
+                String path = klass.getName().replace('.', '/') + ".class";
+                try (InputStream input = loader.getResourceAsStream(path)) {
+                    jarOs.putNextEntry(new JarEntry(path));
+                    IOUtils.copy(input, jarOs);
+                }
+            }
+        }
+    }
+
+
+    @AfterClass
+    public static void tearDown() throws Exception {
+        Files.delete(JAR_FILE);
+        Files.delete(JAR_DIR);
+    }
+
+    @Test
+    public void testHelloWorld() throws Exception {
+        ParserFactoryFactory pff = new ParserFactoryFactory("org.apache.tika.parser.mock.MockParserFactory",
+                Collections.EMPTY_MAP);
+        List<String> java = new ArrayList<>();
+        java.add("java");
+        ForkParser parser = new ForkParser(JAR_DIR, pff);
+        parser.setJavaCommand(java);
+        parser.setServerPulseMillis(10000);
+
+        ContentHandler contentHandler = new ToHTMLContentHandler();
+        Metadata m = new Metadata();
+        try (InputStream is = getClass().getResourceAsStream("/test-documents/example.xml")) {
+            parser.parse(is,contentHandler, m, new ParseContext());
+        } finally {
+            parser.close();
+        }
+        assertContains("hello world!", contentHandler.toString());
+        assertEquals("Nikolai Lobachevsky", m.get(TikaCoreProperties.CREATOR));
+    }
+
+    private static List<Class> getClasses(String packageName)
+            throws ClassNotFoundException, IOException {
+        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+        assert classLoader != null;
+        String path = packageName.replace('.', '/');
+        Enumeration<URL> resources = classLoader.getResources(path);
+        List<File> dirs = new ArrayList();
+        while (resources.hasMoreElements()) {
+            URL resource = resources.nextElement();
+            dirs.add(new File(resource.getFile().replaceAll("%20", " ")));
+        }
+        ArrayList classes = new ArrayList();
+        for (File directory : dirs) {
+            classes.addAll(findClasses(directory, packageName));
+        }
+        return classes;
+    }
+
+    private static List<Class> findClasses(File dir, String packageName) throws ClassNotFoundException {
+        List<Class> classes = new ArrayList();
+        if (!dir.exists()) {
+            return classes;
+        }
+        File[] files = dir.listFiles();
+        for (File file : files) {
+            if (file.isDirectory()) {
+                assert !file.getName().contains(".");
+                classes.addAll(findClasses(file, packageName + "." + file.getName()));
+            } else if (file.getName().endsWith(".class")) {
+                classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
+            }
+        }
+        return classes;
+    }
+}
diff --git a/tika-core/src/test/java/org/apache/tika/parser/mock/MockParserFactory.java b/tika-core/src/test/java/org/apache/tika/parser/mock/MockParserFactory.java
new file mode 100644
index 0000000..082cf61
--- /dev/null
+++ b/tika-core/src/test/java/org/apache/tika/parser/mock/MockParserFactory.java
@@ -0,0 +1,66 @@
+package org.apache.tika.parser.mock;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.extractor.ParsingEmbeddedDocumentExtractor;
+import org.apache.tika.io.IOExceptionWithCause;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.mime.MediaType;
+import org.apache.tika.parser.AbstractParser;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+import org.apache.tika.parser.ParserFactory;
+import org.apache.tika.sax.EmbeddedContentHandler;
+import org.apache.tika.sax.XHTMLContentHandler;
+import org.w3c.dom.Document;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.DocumentBuilder;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+
+public class MockParserFactory extends ParserFactory {
+
+    public MockParserFactory(Map<String, String> args) {
+        super(args);
+    }
+
+    @Override
+    public Parser build() throws IOException, TikaException {
+        return new MockParser();
+    }
+}
diff --git a/tika-core/src/test/resources/test-documents/example.xml b/tika-core/src/test/resources/test-documents/example.xml
new file mode 100644
index 0000000..46727ea
--- /dev/null
+++ b/tika-core/src/test/resources/test-documents/example.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+<mock>
+    <!-- this file offers all of the options as documentation
+    Parsing should stop at an IOException, of course
+    -->
+
+    <!-- action can be "add" or "set" -->
+    <metadata action="add" name="dc:creator">Nikolai Lobachevsky</metadata>
+    <!-- element is the name of the sax event to write, p=paragraph
+        if the element is not specified, the default is <p> -->
+    <write element="p">hello world!</write>
+    <!-- write something to System.out -->
+    <print_out>writing to System.out</print_out>
+    <!-- write something to System.err -->
+    <print_err>writing to System.err</print_err>
+    <!-- hang
+        millis: how many milliseconds to pause.  The actual hang time will probably
+            be a bit longer than the value specified.        heavy: whether or not the hang should do something computationally expensive.
+            If the value is false, this just does a Thread.sleep(millis).
+            This attribute is optional, with default of heavy=false.
+        pulse_millis: (required if "heavy" is true), how often to check to see
+            whether the thread was interrupted or that the total hang time exceeded the millis
+        interruptible: whether or not the parser will check to see if its thread
+            has been interrupted; this attribute is optional with default of true
+    -->
+
+</mock>
\ No newline at end of file
diff --git a/tika-parsers/src/test/resources/test-documents/testPST.pst b/tika-parsers/src/test/resources/test-documents/testPST.pst
index c10a928..dc4b673 100644
Binary files a/tika-parsers/src/test/resources/test-documents/testPST.pst and b/tika-parsers/src/test/resources/test-documents/testPST.pst differ
diff --git a/tika-parsers/src/test/resources/test-documents/testPST_variousBodyTypes.pst b/tika-parsers/src/test/resources/test-documents/testPST_variousBodyTypes.pst
index 2a9141a..8d939bc 100644
Binary files a/tika-parsers/src/test/resources/test-documents/testPST_variousBodyTypes.pst and b/tika-parsers/src/test/resources/test-documents/testPST_variousBodyTypes.pst differ

-- 
To stop receiving notification emails like this one, please contact
tallison@apache.org.