/*******************************************************************************
 * 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.html.parser.BlancoHtmlContentSerializer;
import blanco.html.parser.BlancoHtmlParser;
import blanco.html.parser.BlancoHtmlParserFactory;

/**
 * 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();
            encodingParser.parse(argHtmlInput);
            String current = new String(argHtmlInput, encodingParser
                    .getEncoding());
            current = normalizeByTagSoup(current);
            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。
     * @return 正常化された出力HTML。
     * @throws IOException
     *             入出力例外が発生した場合。
     * @throws SAXException
     *             SAX例外が発生した場合。
     */
    private static String normalizeByTagSoup(final String argHtmlInput)
            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用)
        serializer.setOutputProperty(XMLWriter.DOCTYPE_PUBLIC,
                "-//W3C//DTD HTML 4.01 Transitional//EN");

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

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

        // 出力を (xhtmlではなく) html にセットします。
        serializer.setOutputProperty(XMLWriter.METHOD, "html");

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

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

        // 出力先の文字エンコーディングを指定します。
        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();
    }
}
