/*
 * $Id: VKCLinkFilter.java,v 1.5 2005/03/24 12:57:05 marbobo Exp $
 *
 * Copyright (c) Siemens AG 2008 
 *           (c) Universität Paderborn 2008 
 * Licensed under EUPL 1.1. 
 *
 * $Log: VKCLinkFilter.java,v $
 * Revision 1.5  2005/03/24 12:57:05  marbobo
 * + Unnötige Anweisungen entfernt
 *
 * Revision 1.4  2005/03/21 09:18:27  lawern
 * *** empty log message ***
 *
 * Revision 1.3  2005/03/18 09:27:34  lawern
 * w3c.dom.Document wird jetzt von Tidy erzeugt.
 *
 * Revision 1.2  2005/03/17 16:36:22  lawern
 * Verhindern, dass UTF-Zeichen konvertiert werden.
 *
 * Revision 1.1  2005/03/16 12:44:07  lawern
 * Eigener LinkFilter und Erweiterung der LinkRules.
 *
 */
package vkc.util.webcopy;

import net.matuschek.http.HttpDoc;

import net.matuschek.spider.docfilter.DocumentFilter;
import net.matuschek.spider.docfilter.FilterException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.w3c.tidy.Tidy;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import java.net.MalformedURLException;
import java.net.URL;

import java.util.StringTokenizer;


/**
 * Localizer tries to replace absolute links by relative links
 * and should allow offline browsing.
 *
 * It uses JTidy to parse the file.
 *
 * @author Daniel Matuschek
 * @version $Revision: 1.5 $
 */
public class VKCLinkFilter implements DocumentFilter {
    /** processing enabled ? */
    protected boolean enabled = true;

    /**
     * This method processes the file and will replace
     * absolute links by relative.
     *
     * @return the old document, if the ContentType is not
     * text/html, a new (localized) document otherwise.
     */
    public HttpDoc process(HttpDoc input) throws FilterException {
        if (input == null) {
            return null;
        }

        if (!input.isHTML()) {
            return input;
        }

        if (!enabled) {
            return input;
        }

        Tidy tidy = new Tidy();

        //verhindern dass der Inhalt umkodiert wird
        tidy.setRawOut(true);
        tidy.setQuoteAmpersand(false);
        tidy.setQuoteMarks(false);
        tidy.setQuoteNbsp(false);

        //alte JTidy-Versionen
        tidy.setCharEncoding(org.w3c.tidy.Configuration.RAW);

        //bei neueren JTidy-Versionen:
        //tidy.setInputEncoding("ISO-8859-1");
        //tidy.setOutputEncoding("ISO-8859-1");
        //Document doc = input.getDOM();
        Document doc = tidy.parseDOM(new ByteArrayInputStream(
                    input.getContent()), null);
        rewriteDOM(doc, input.getURL());

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        tidy.pprint(doc, bos);

        try {
            bos.flush();
            bos.close();
        } catch (IOException e) {
        }

        input.setContent(bos.toByteArray());

        return input;
    }

    /**
     * Enable processing, the will parse the document and try to
     * replace absolute by relative links.
     */
    public void enable() {
        this.enabled = true;
    }

    /**
     * Disable processing, the filter will not change the document
     * content.
     */
    public void disable() {
        this.enabled = false;
    }

    /**
     * Is the link processing enabled ?
     *
     * @return true, if the filter processes links, false otherwise
     */
    public boolean isEnabled() {
        return this.enabled;
    }

    /**
     * Rewrite this DOM with relative URLs. Will process the whole DOM
     *
     * @param node root node of the DOM to modify
     * @param url base URL of teh document itself (for relative addressing)
     */
    private void rewriteDOM(Node node, URL url) throws FilterException {
        // this should not happen !
        if (node == null) {
            throw new FilterException("Got a null node");
        }

        // ELEMENT ?
        if (node instanceof Element) {
            String name = node.getNodeName();

            if (name.equals("a") || name.equals("area")) {
                globalizeAttrib(node, "href", url);
            } else if (name.equals("img") || name.equals("frame") ||
                    name.equals("input") || name.equals("script")) {
                localizeAttrib(node, "src", url);
            } else if (name.equals("link")) {
                localizeAttrib(node, "href", url);
            } else if (name.equals("td") || name.equals("tr")) {
                localizeAttrib(node, "background", url);
            }
        }

        // recursive travel through all childs
        NodeList childs = node.getChildNodes();

        for (int i = 0; i < childs.getLength(); i++) {
            rewriteDOM(childs.item(i), url);
        }
    }

    /**
     * Globalize a given attribute for a Element. <br />
     *
     * @param node an element node that should be globalized
     * @param attribute name of the attribute that should be globalized
     * @param context an URL that is the context for relative
     * addressing (base address)
     */
    private void globalizeAttrib(Node node, String attribute, URL context) {
        Element el = (Element) node;
        String oldValue = el.getAttribute(attribute);

        // only globalize if the attribute exists
        // only globalize if it is another file
        if (!oldValue.equals("")) {
            String newValue = globalizeURL(oldValue, context);

            if (!equalURL(newValue, context.toString())) {
                el.setAttribute(attribute, newValue);
            }
        }
    }

    /**
     * Globalize a given URL.
     *
     * @param urlStr a String containing a URL, can be relative
     * (e.g. ../index.html) or absolute ("http://myserver/")
     * @param context an URL that a the context URL for relative URLs
     *
     * @return a String containing an URL that will be absolute to the given
     * context
     */
    private String globalizeURL(String urlStr, URL context) {
        URL url;

        try {
            url = new URL(context, urlStr);
        } catch (MalformedURLException e) {
            return urlStr;
        }

        return url.toString();
    }

    /**
     * Localize a given attribute for a Element. <br />
     * Thanks to Paul Tan for the feedback
     *
     * @param node an element node that should be localized
     * @param attribute name of the attribute that should be localized
     * @param context an URL that is the context for relative
     * addressing (base address)
     */
    private void localizeAttrib(Node node, String attribute, URL context) {
        Element el = (Element) node;
        String oldValue = el.getAttribute(attribute);

        // only localize if the attribute exists
        // only localize if the file is in another directory
        if (!oldValue.equals("") && (oldValue.indexOf("/") != -1)) {
            String newValue = localizeURL(oldValue, context);
            el.setAttribute(attribute, newValue);
        } // end of if ()
    }

    /**
     * Localize a given URL.
     *
     * Thanks to Paul Tan and Laurent Salinas for the feedback.
     *
     * @param urlStr a String containing a URL, can be relative
     * (e.g. ../index.html) or absolute ("http://myserver/")
     * @param context an URL that a the context URL for relative URLs
     *
     * @return a String containing an URL that will be relative to the given
     * context if both URLs are on the same host, otherwise it will simply
     * return urlStr
     */
    private String localizeURL(String urlStr, URL context) {
        URL url;

        try {
            url = new URL(context, urlStr);
        } catch (MalformedURLException e) {
            return urlStr;
        }

        // only localize "http:" links
        if (!url.getProtocol().equalsIgnoreCase("http")) {
            return urlStr;
        }

        // only localize if new URL is on the same host !
        if ((context != null) &&
                (context.getHost().equalsIgnoreCase(url.getHost()))) {
            String ref = url.getRef();
            String path = url.getPath();

            // Already relative
            // this should only happen if the context
            // is null
            if (path.startsWith("../")) {
                return urlStr;
            }

            // URL references
            if ((ref != null) && (!ref.equals(""))) {
                path = path + "#" + ref;
            }

            // implied index.html
            if ((path.length() > 0) &&
                    ((path.charAt(path.length() - 1)) == '/')) {
                path = path + "index.html";
            }

            return localizePath(url.getPath(), context.getPath());
        } 

        return urlStr;
    }

    /**
     * Localize a given path. Very dumb, but it works ;-)
     *
     * @param path path to localize
     * @param context reference path
     * @return a path that is given relative
     *
     * Example: <br />
     * path="/images/test.gif" <br />
     * context="/test/index.html"<br />
     * result="../images/test.gif"
     */
    private String localizePath(String path, String context) {
        StringTokenizer st = new StringTokenizer(context, "/");
        int depth = st.countTokens();

        if (!context.endsWith("/")) {
            depth--;
        }

        StringBuffer sb = new StringBuffer();

        if (depth > 0) {
            for (int i = 0; i < depth; i++) {
                sb.append("/..");
            }

            sb.deleteCharAt(0);
        } else {
            if (path.startsWith("/")) {
                // delete first character (absolute path);
                path = path.substring(1);
            }
        }

        sb.append(path);

        return sb.toString();
    }

    /*
     * Checks if the two Urls refers to the same wep page.
     */
    private boolean equalURL(String url1, String url2) {
        int url1End = url1.lastIndexOf('#');
        int url2End = url2.lastIndexOf('#');
        String head1;
        String head2;

        if (url1End != -1) {
            head1 = url1.substring(0, url1End);
        } else {
            head1 = url1;
        }

        if (url2End != -1) {
            head2 = url2.substring(0, url2End);
        } else {
            head2 = url2;
        }

        try {
            URL headUrl1 = new URL(head1);
            URL headUrl2 = new URL(head2);

            return headUrl1.equals(headUrl2);
        } catch (MalformedURLException e) {
            return false;
        }
    }
}
