/*******************************************************************************
 * Copyright (c) 2008 IGA Tosiki, NTT DATA BUSINESS BRAINS Corp.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    IGA Tosiki (NTT DATA BUSINESS BRAINS Corp.) - initial API and implementation
 *******************************************************************************/
/*
 * blanco Framework
 * Copyright (C) 2008 NTT DATA BUSINESS BRAINS CORPORATION
 * 
 * This library 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.
 */
package blanco.html.normalizer.util;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;

import org.ccil.cowan.tagsoup.HTMLSchema;
import org.ccil.cowan.tagsoup.Parser;
import org.ccil.cowan.tagsoup.XMLWriter;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import blanco.commons.util.BlancoStringUtil;
import blanco.html.parser.BlancoHtmlContentSerializer;
import blanco.html.parser.BlancoHtmlParser;
import blanco.html.parser.BlancoHtmlParserFactory;
import blanco.html.parser.helper.BlancoHtmlNullContentHandler;
import blanco.xml.bind.valueobject.BlancoXmlDtd;

/**
 * HTML正常化のためのユーティリティクラス。
 * 
 * @author IGA Tosiki
 */
public class BlancoHtmlNormalizerUtil {
    /**
     * 与えられた HTML を正常化します。
     * 
     * @param argHtmlInput
     *            入力HTML。
     * @return 正常化後の HTML。
     * @throws IOException
     *             入出力例外が発生した場合。
     */
    public static byte[] normalize(final byte[] argHtmlInput)
            throws IOException {
        try {
            final BlancoHtmlParser encodingParser = BlancoHtmlParserFactory
                    .getInstance();
            final BlancoXmlDtd dtd = new BlancoXmlDtd();
            class MyDtdHandler extends BlancoHtmlNullContentHandler {
                @Override
                public void startDTD(String name, String publicId,
                        String systemId) throws SAXException {
                    dtd.setName(name);
                    dtd.setPublicId(publicId);
                    dtd.setSystemId(systemId);
                }
            }
            encodingParser.setLexicalHandler(new MyDtdHandler());
            encodingParser.parse(argHtmlInput);
            String current = new String(argHtmlInput, encodingParser
                    .getEncoding());
            current = normalizeByTagSoup(current, dtd);
            current = normalizeNumericCharacterReference(current
                    .getBytes(encodingParser.getEncoding()), encodingParser
                    .getEncoding());

            return current.getBytes(encodingParser.getEncoding());
        } catch (SAXException e) {
            throw new IOException("SAXException例外発生: " + e.toString());
        }
    }

    /**
     * TagSoup を利用して HTMLを正常化します。
     * 
     * オプションタグの補足が特に有益。
     * 
     * @param argHtmlInput
     *            正常化させたい入力HTML。
     * @param dtd
     *            参考となる DTD。
     * @return 正常化された出力HTML。
     * @throws IOException
     *             入出力例外が発生した場合。
     * @throws SAXException
     *             SAX例外が発生した場合。
     */
    private static String normalizeByTagSoup(final String argHtmlInput,
            final BlancoXmlDtd dtd) throws IOException, SAXException {
        final XMLReader parser = new Parser();

        final HTMLSchema schema = new HTMLSchema();
        parser.setProperty(Parser.schemaProperty, schema);

        final StringWriter output = new StringWriter();

        final XMLWriter serializer = new XMLWriter(output);
        parser.setContentHandler(serializer);

        // TODO 確認した時点では、これが期待通りには機能せず。
        // parser.setDTDHandler(serializer);
        // 仕方が無いので、以下の記述により DOCTYPE を強制的に出力させる。(html用)

        boolean isXHTML = false;

        if (BlancoStringUtil.null2Blank(dtd.getPublicId()).length() == 0) {
            serializer.setOutputProperty(XMLWriter.DOCTYPE_PUBLIC,
                    "-//W3C//DTD HTML 4.01 Transitional//EN");
        } else {
            serializer.setOutputProperty(XMLWriter.DOCTYPE_PUBLIC, dtd
                    .getPublicId());
            if (dtd.getPublicId().toUpperCase().indexOf("XHTML") >= 0) {
                isXHTML = true;
            }
        }

        if (isXHTML == false) {
            // <html> に名前空間をあらわす属性が付かないようにする。
            parser.setFeature(Parser.namespacesFeature, false);
        }

        final InputSource input = new InputSource();
        input.setCharacterStream(new StringReader(argHtmlInput));

        if (isXHTML == false) {
            // 出力を html にセットします。
            serializer.setOutputProperty(XMLWriter.METHOD, "html");
        } else {
            // 出力を xhtml にセットします。(TagSoup の宣言には、これは存在せず)
            serializer.setOutputProperty(XMLWriter.METHOD, "xhtml");
        }

        // XML宣言の出力を抑制します。
        serializer.setOutputProperty(XMLWriter.OMIT_XML_DECLARATION, "yes");

        // 属性へのデフォルト付与を抑制させます。
        parser.setFeature(Parser.defaultAttributesFeature, false);

        // 出力先の文字エンコーディングを指定します。
        // ※ここでは「Windows-31J」を指定しています。UTF-8 では、「&nbsp;」が適切に処理できません。
        serializer.setOutputProperty(XMLWriter.ENCODING, "Windows-31J");

        // 知らない名前の要素について寛大に処理します。(jsp対策において必要と想定)
        parser.setFeature(
                "http://www.ccil.org/~cowan/tagsoup/features/ignore-bogons",
                false);

        // コメントをそのまま出力します。
        parser.setProperty(Parser.lexicalHandlerProperty, serializer);

        // パースを実施。
        parser.parse(input);

        return output.toString();
    }

    /**
     * 数値文字参照をデコードします。
     * 
     * @param argHtmlInput
     *            入力HTML。
     * @param encoding
     *            文字エンコーディング。
     * @return 出力HTML。
     * @throws IOException
     *             入出力例外が発生した場合。
     * @throws SAXException
     */
    private static String normalizeNumericCharacterReference(
            final byte[] argHtmlInput, final String encoding)
            throws IOException, SAXException {
        final StringWriter strWriter = new StringWriter();
        BlancoHtmlContentSerializer serializer = new BlancoHtmlContentSerializer();
        serializer.setWriter(new BufferedWriter(strWriter));

        final BlancoHtmlParser parser = BlancoHtmlParserFactory.getInstance();
        parser.setHandler(serializer);
        parser.setLexicalHandler(serializer);
        parser.setDTDHandler(serializer);
        parser.setEncoding(encoding);

        parser.parse(argHtmlInput);

        return strWriter.toString();
    }
}
