XHTMLBalancingHandler.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.sax;

import java.util.ArrayDeque;
import java.util.Deque;

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

/**
 * SAX decorator that tracks open elements so a parser can recover well-formed
 * XHTML when an exception interrupts the SAX stream mid-element.
 * <p>
 * The decorator is a thin passthrough on the happy path: it pushes and pops an
 * internal stack on {@code startElement}/{@code endElement} and otherwise forwards
 * every event to the wrapped handler unchanged. It deliberately does NOT mask
 * bad event sequences (mismatched or excess endElement, duplicate attributes,
 * etc.) -- those remain visible to {@link StrictXHTMLValidator} so parser bugs
 * still surface as test failures.
 * <p>
 * The unhappy path -- a per-part SAX parser throwing mid-element after emitting
 * one or more start tags -- is handled via {@link #drainOpenElements()}, which
 * emits a matching {@code endElement} (with the original uri/localName/qName)
 * for every element still on the stack, in reverse open order. The wrapped
 * handler is left in a well-formed state with no dangling elements from the
 * failed sub-parse.
 * <p>
 * Typical use wraps the handler that receives events from an inner SAX parser,
 * inside the catch arm that swallows the inner parser's exception:
 * <pre>{@code
 * XHTMLBalancingHandler balancer = new XHTMLBalancingHandler(contentHandler);
 * try {
 *     XMLReaderUtils.parseSAX(stream, new EmbeddedContentHandler(balancer), context);
 * } catch (SAXException e) {
 *     balancer.drainOpenElements();
 *     // ... log and continue ...
 * }
 * }</pre>
 * This handler does not touch {@code startDocument}/{@code endDocument}; the
 * caller still owns the document lifecycle.
 */
public class XHTMLBalancingHandler extends ContentHandlerDecorator {

    private final Deque<QName> openElements = new ArrayDeque<>();

    public XHTMLBalancingHandler(ContentHandler handler) {
        super(handler);
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attrs)
            throws SAXException {
        openElements.push(new QName(uri, localName, qName));
        super.startElement(uri, localName, qName, attrs);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        // Pop best-effort: an unbalanced endElement (e.g., emitted after the
        // matching startElement was swallowed) still forwards downstream so a
        // wrapping StrictXHTMLValidator sees the violation.
        if (!openElements.isEmpty()) {
            openElements.pop();
        }
        super.endElement(uri, localName, qName);
    }

    /**
     * Emits a matching {@code endElement} for every element still on the open
     * stack, in reverse open order. After this call the stack is empty.
     * <p>
     * Intended for the catch arm of a caller that swallowed a
     * {@link SAXException} from an inner SAX parser: the inner parser may have
     * left one or more elements open mid-stream, and downstream serialization
     * needs matching closers before any further events.
     * <p>
     * Does NOT emit {@code endDocument} -- document lifecycle stays with the
     * caller.
     */
    public void drainOpenElements() throws SAXException {
        while (!openElements.isEmpty()) {
            QName q = openElements.pop();
            super.endElement(q.uri, q.localName, q.qName);
        }
    }

    /**
     * Number of elements currently open through this handler. Exposed for
     * tests and for callers that want to know whether
     * {@link #drainOpenElements()} would emit anything.
     */
    public int openElementCount() {
        return openElements.size();
    }

    private static final class QName {
        final String uri;
        final String localName;
        final String qName;

        QName(String uri, String localName, String qName) {
            this.uri = uri == null ? "" : uri;
            this.localName = localName == null ? "" : localName;
            this.qName = qName == null ? "" : qName;
        }
    }
}