ParseHooks.java

/*
 * 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.hook;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.tika.config.TransientParseState;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.mime.MediaType;
import org.apache.tika.parser.ParseContext;

/**
 * The hooks a parse runs, in config order. Pinning and offering never fail a document: a
 * problem is a warning on that document's metadata.
 *
 * @since Apache Tika 4.1
 */
public final class ParseHooks implements TransientParseState {

    private static final Logger LOG = LoggerFactory.getLogger(ParseHooks.class);

    /** Marks a top-level parse in progress; tracks the documents being parsed, outermost last. */
    public static final class Run implements TransientParseState {
        private final Deque<Metadata> open = new ArrayDeque<>();

        /** Enters a document; returns the one whose parser embedded it, null at the top. */
        public Metadata enter(Metadata metadata) {
            Metadata parent = open.peek();
            open.push(metadata);
            return parent;
        }

        public void exit() {
            open.pop();
        }

        /** The document whose parser is running; null between documents. */
        public Metadata current() {
            return open.peek();
        }

        /** The document the current one is embedded in; null at the top. */
        public Metadata parent() {
            Iterator<Metadata> it = open.iterator();
            if (!it.hasNext()) {
                return null;
            }
            it.next();
            return it.hasNext() ? it.next() : null;
        }
    }

    private final List<ParseHook> hooks;

    public ParseHooks(List<ParseHook> hooks) {
        this.hooks = List.copyOf(hooks);
    }

    public List<ParseHook> getHooks() {
        return hooks;
    }

    public boolean isEmpty() {
        return hooks.isEmpty();
    }

    public void start(Metadata root, ParseContext context) throws TikaException {
        for (ParseHook hook : hooks) {
            hook.start(root, context);
        }
    }

    /** The document's bytes as a file when a hook wants them, else null. */
    public Path pin(MediaType type, Metadata metadata, TikaInputStream tis, ParseContext context)
            throws TikaException {
        boolean wanted = false;
        for (ParseHook hook : hooks) {
            wanted |= hook.wants(type, metadata, context);
        }
        if (!wanted) {
            return null;
        }
        try {
            return tis.getPath();
        } catch (IOException e) {
            metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
                    "parse hook: could not pin bytes: " + e.getMessage());
            return null;
        }
    }

    public void offer(MediaType type, Metadata metadata, Metadata parent, Path bytes,
                      ParseContext context) {
        for (ParseHook hook : hooks) {
            try {
                hook.offer(type, metadata, parent, bytes, context);
            } catch (IOException | TikaException | RuntimeException e) {
                LOG.warn("parse hook {} failed on {}", hook.getClass().getName(), type, e);
                metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
                        "parse hook " + hook.getClass().getSimpleName() + ": " + e.getMessage());
            }
        }
    }

    /** Whether any hook wants the current document's pages rendered as images of this type. */
    public boolean wantsPages(MediaType renderType, ParseContext context) throws TikaException {
        Run run = context.get(Run.class);
        Metadata document = run == null ? null : run.current();
        if (document == null) {
            return false;
        }
        for (ParseHook hook : hooks) {
            if (hook.wantsPages(renderType, document, context)) {
                return true;
            }
        }
        return false;
    }

    /** A rendered page of the current document, from its own parser; never fails it. */
    public void offerPage(MediaType type, int page, Path bytes, ParseContext context) {
        Run run = context.get(Run.class);
        Metadata document = run == null ? null : run.current();
        if (document == null) {
            return;
        }
        Metadata parent = run.parent();
        for (ParseHook hook : hooks) {
            try {
                hook.offerPage(type, document, parent, page, bytes, context);
            } catch (IOException | TikaException | RuntimeException e) {
                LOG.warn("parse hook {} failed on page {}", hook.getClass().getName(), page, e);
                document.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
                        "parse hook " + hook.getClass().getSimpleName() + " on page " + page
                                + ": " + e.getMessage());
            }
        }
    }

    public void end(Metadata root, boolean failed, ParseContext context) {
        for (ParseHook hook : hooks) {
            try {
                hook.end(root, failed, context);
            } catch (RuntimeException e) {
                LOG.warn("parse hook {} failed at end", hook.getClass().getName(), e);
                root.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
                        "parse hook " + hook.getClass().getSimpleName() + ": " + e.getMessage());
            }
        }
    }
}