XWPFStylesShim.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.microsoft.ooxml.xwpf;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import org.apache.poi.openxml4j.opc.PackagePart;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import org.apache.tika.exception.TikaException;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.microsoft.ooxml.OOXMLWordAndPowerPointTextHandler;
import org.apache.tika.utils.XMLReaderUtils;

/**
 * Scrapes from styles.xml each style's name, its basedOn parent and whether
 * its own run properties set bold or italics.
 */
public class XWPFStylesShim {

    /**
     * Empty singleton to be used when there is no style info
     */
    public static final XWPFStylesShim EMPTY_STYLES = new EmptyXWPFStyles();

    // bounds basedOn chains, including cyclic ones
    private static final int MAX_BASED_ON_DEPTH = 32;
    // Word itself caps style names at 253 characters
    private static final int MAX_STYLE_NAME_LENGTH = 255;

    private final Map<String, StyleInfo> styles = new HashMap<>();

    private XWPFStylesShim() {

    }

    public XWPFStylesShim(PackagePart part, ParseContext parseContext)
            throws IOException, TikaException, SAXException {

        try (InputStream is = part.getInputStream()) {
            onDocumentLoad(parseContext, is);
        }
    }

    private void onDocumentLoad(ParseContext parseContext, InputStream stream)
            throws TikaException, IOException, SAXException {
        XMLReaderUtils
                .parseSAX(stream, new StylesStripper(), parseContext);
    }

    /**
     * @param styleId
     * @return style's name or null if styleId is null or can't be found
     */
    public String getStyleName(String styleId) {
        if (styleId == null) {
            return null;
        }
        StyleInfo info = styles.get(styleId);
        return info == null ? null : info.name;
    }

    /**
     * @return whether the style, or the nearest style it is based on that says
     * either way, sets bold; false if none does
     */
    public boolean isBold(String styleId) {
        StyleInfo info = styles.get(styleId);
        for (int i = 0; info != null && i < MAX_BASED_ON_DEPTH; i++) {
            if (info.bold != null) {
                return info.bold;
            }
            info = info.basedOn == null ? null : styles.get(info.basedOn);
        }
        return false;
    }

    /**
     * @return whether the style, or the nearest style it is based on that says
     * either way, sets italics; false if none does
     */
    public boolean isItalics(String styleId) {
        StyleInfo info = styles.get(styleId);
        for (int i = 0; info != null && i < MAX_BASED_ON_DEPTH; i++) {
            if (info.italics != null) {
                return info.italics;
            }
            info = info.basedOn == null ? null : styles.get(info.basedOn);
        }
        return false;
    }

    private static class StyleInfo {
        String name;
        String basedOn;
        Boolean bold;
        Boolean italics;
    }

    private static class EmptyXWPFStyles extends XWPFStylesShim {

        @Override
        public String getStyleName(String styleId) {
            return null;
        }

        @Override
        public boolean isBold(String styleId) {
            return false;
        }

        @Override
        public boolean isItalics(String styleId) {
            return false;
        }
    }

    private class StylesStripper extends DefaultHandler {

        StyleInfo current = null;
        // depth below the current <w:style>; its own rPr is at 1, toggles at 2
        int depth = 0;
        boolean inStyleRPr = false;

        @Override
        public void startElement(String uri, String localName, String qName, Attributes atts)
                throws SAXException {
            if (current != null) {
                depth++;
            }
            if (uri != null && !OOXMLWordAndPowerPointTextHandler.W_NS.equals(uri)) {
                return;
            }
            if ("style".equals(localName)) {
                String styleId =
                        atts.getValue(OOXMLWordAndPowerPointTextHandler.W_NS, "styleId");
                if (styleId != null) {
                    current = new StyleInfo();
                    styles.put(styleId, current);
                    depth = 0;
                }
            } else if (current == null) {
                return;
            } else if (depth == 1 && "name".equals(localName)) {
                String name = getVal(atts);
                current.name = name != null && name.length() > MAX_STYLE_NAME_LENGTH
                        ? name.substring(0, MAX_STYLE_NAME_LENGTH) : name;
            } else if (depth == 1 && "basedOn".equals(localName)) {
                current.basedOn = getVal(atts);
            } else if (depth == 1 && "rPr".equals(localName)) {
                inStyleRPr = true;
            } else if (depth == 2 && inStyleRPr && "b".equals(localName)) {
                current.bold = getOnOff(atts);
            } else if (depth == 2 && inStyleRPr && "i".equals(localName)) {
                current.italics = getOnOff(atts);
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if (current == null) {
                return;
            }
            if (depth == 0) {
                current = null;
                return;
            }
            if (depth == 1) {
                inStyleRPr = false;
            }
            depth--;
        }

        private String getVal(Attributes atts) {
            return atts.getValue(OOXMLWordAndPowerPointTextHandler.W_NS, "val");
        }

        private boolean getOnOff(Attributes atts) {
            String v = getVal(atts);
            return v == null || !("0".equals(v) || "false".equals(v) || "off".equals(v));
        }
    }

}