/*
 * Copyright 2007 Hippo
 *
 * Licensed 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 nl.hippo.cms.background;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import nl.hippo.cocoon.repository.RepositoryConfiguration;
import nl.hippo.cocoon.repository.RepositoryManager;
import nl.hippo.cocoon.webdav.WebDAVHelper;

import org.apache.avalon.framework.service.ServiceManager;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.webdav.lib.methods.MkcolMethod;
import org.apache.webdav.lib.methods.PropFindMethod;
import org.apache.webdav.lib.methods.SearchMethod;
import org.jdom.Content;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Text;
import org.jdom.output.XMLOutputter;

/**
 * @author <a href="mailto:d.dam@hippo.nl">Dennis Dam</a>
 *
 * @version $Id: BrokenLinkCheckTask.java 8299 2007-09-24 16:03:40Z ddam $
 */
public class BrokenLinkCheckTask implements Runnable, Serializable {

    public static final String URL_SEPARATOR_CHARS = " ";
    public static final String HTTP_ERROR_PREFIX = "message.http-error.";
    public static final String BROKENLINK_I18N_PREFIX = "cms.brokenlinks.";
    public static final String I18N_INTERNAL_LINK_INVALID = BROKENLINK_I18N_PREFIX+"internal-link-invalid";
    
    public static final String I18N_NS_URI = "http://apache.org/cocoon/i18n/2.1";

    private static final HttpClient repoHttpClient = new HttpClient(new MultiThreadedHttpConnectionManager());
    private static final HttpClient generalHttpClient = new HttpClient(new MultiThreadedHttpConnectionManager());
    private static final String DEFAULT_CONTENT_PATH = "/content";
    private static final String CONFIGURATION_FOLDER = "/configuration";
    private static final String BROKENLINKS_FOLDER = CONFIGURATION_FOLDER + "/brokenlinks";
    private static final String BROKEN_LINKS_DOCUMENT_PATH = BROKENLINKS_FOLDER + "/brokenlinks.xml";
    private static final String WEBDAV = "webdav:";
    private static final String HTTP = "http:";
    private static final String PROTOCOL_SEPARATOR = "://";
    private static final String DEFAULT_PROTOCOL_PREFIX = "http"+PROTOCOL_SEPARATOR; 
    private static final String HIPPO_NS = "http://hippo.nl/cms/1.0";
    private static final String DAV_NS = "DAV:";
    private static final String LINKS_PROP = "links";

    private static final long serialVersionUID = -1L;

    private final Log logger = LogFactory.getLog(BrokenLinkCheckTask.class);

    private String repositoryName;
    private String username;
    private String password;
    private String contentPath;
    private String absoluteContentPath;
    private String brokenLinksDocPath;
    private ServiceManager manager;
    private RepositoryConfiguration repoConfig;
    private HttpState httpState = null;

    public BrokenLinkCheckTask(ServiceManager manager, String activeRepositoryName, String systemuserName,
            String systemuserPassword, String contentPath) {
        this.manager = manager;
        this.repositoryName = activeRepositoryName;
        this.username = systemuserName;
        this.password = systemuserPassword;
        this.contentPath = contentPath == null ? DEFAULT_CONTENT_PATH : contentPath;
    }

    private void refreshConnection() {
        httpState = new HttpState();
        httpState.setAuthenticationPreemptive(true);

        UsernamePasswordCredentials credentials = new UsernamePasswordCredentials();
        credentials.setUserName(username);
        credentials.setPassword(password);
        RepositoryManager repoMan = null;
        repoConfig = null;
        try {
            repoMan = (RepositoryManager) this.manager.lookup(RepositoryManager.ROLE);
            repoConfig = repoMan.getRepositoryConfiguration(repositoryName);
        } catch (Exception e) {
            logger.error(e);
        } finally {
            if (repoMan != null) {
                this.manager.release(repoMan);
            }
        }
        httpState.setCredentials(null, repoConfig.getHost(), credentials);
        try {
            WebDAVHelper.login(repoConfig.getRoot() + repoConfig.getUsersPath() + '/' + username, httpState);
        } catch (IOException e) {
            logger.error("Unable to login with systemuser:", e);
        }

        this.absoluteContentPath = getAbsoluteUri(this.contentPath);
        this.brokenLinksDocPath = getAbsoluteUri(BROKEN_LINKS_DOCUMENT_PATH);
    }

    private static final String normalizeUri(String uri) {
        if (uri.startsWith(WEBDAV)) {
            return HTTP + uri.substring(WEBDAV.length());
        }
        return uri;
    }

    private String getAbsoluteUri(String relativeUri) {
        return normalizeUri(repoConfig.getFiles() + relativeUri);
    }

    /**
     *  Finds all documents in the repository with a non-empty 'links' attribute, and returns a mapping of
     *  document url to the document's 'links' property value, e.g.:
     *  "/content/foo/bar.xml"  -->  "http://www.hippo.nl http://www.brokenwebsite.nu"
     * 
     * @return a map of relative document urls (String) to the value of the 'link' property of that document (String) 
     * @throws IOException
     */
    private Map getUrlsWithLinks() throws IOException {
        InputStream daslStream = getClass().getResourceAsStream("resources/findLinks.xml");
        BufferedReader reader = new BufferedReader(new InputStreamReader(daslStream, "UTF-8"));
        StringBuffer dasl = new StringBuffer();
        String str = null;

        while ((str = reader.readLine()) != null) {
            dasl.append(str + "\n");
        }
        SearchMethod search = new SearchMethod(normalizeUri(this.absoluteContentPath), dasl.toString());
        Map result = new HashMap();
        try {
            int propfindResult = repoHttpClient.executeMethod(search.getHostConfiguration(), search, httpState);
            if (propfindResult == 207) {
                Enumeration responseUrls = search.getAllResponseURLs();
                while (responseUrls.hasMoreElements()) {
                    String responseUrl = (String) responseUrls.nextElement();
                    Enumeration responseProperties = search.getResponseProperties(responseUrl);
                    while (responseProperties.hasMoreElements()) {
                        org.apache.webdav.lib.Property responseProperty = (org.apache.webdav.lib.Property) responseProperties
                                .nextElement();
                        if (responseProperty != null && responseProperty.getNamespaceURI().equals(HIPPO_NS)
                                && responseProperty.getLocalName().equals(LINKS_PROP)
                                && responseProperty.getPropertyAsString().length() > 0) {
                            String relativePath = StringUtils.substringAfter(responseUrl, repoConfig.getFilesPath());
                            result.put(relativePath, responseProperty.getPropertyAsString());
                        }
                    }
                }
            }
        } finally {
            search.releaseConnection();
        }
        return result;
    }

    /**
     * Check for broken links. Expects as input a mapping of relative document uri's to 'link' property values. 
     * A link property value contains a list of space-separated urls.
     * <p>Only external links (i.e. those not beginning with a forward slash) will be checked.
     * HTTP links are verified by doing a HEAD request and checking that the result code is less
     * than 300.</p>
     * <p>During processing a placeholder file will be placed in the repository.</p>
     * 
     * @param repository URL of the placeholder file.
     * @param username Username for connecting to the repository.
     * @param password Password for connecting to the repository.
     * @param data Input data.
     * @return A map whose keys are the page URLs and whose values are Lists. Each item of the list
     * is an array of two strings; first string is the link URL, second one is a message detailing
     * why the link is considered broken.
     * @throws IOException
     */
    private Map checkLinks(Map urlToPropValueMapping) throws IOException {
        Map brokenLinks = new HashMap();
        for (Iterator iter = urlToPropValueMapping.keySet().iterator(); iter.hasNext();) {
            String relDocUri = (String) iter.next();
            String links = (String) urlToPropValueMapping.get(relDocUri);
            StringTokenizer st = new StringTokenizer(links, URL_SEPARATOR_CHARS);
            while (st.hasMoreTokens()) {
                String link = st.nextToken();
                Content errorMessage = null;
                if (link.charAt(0) == '/') {
                    errorMessage = checkInternalLink(link);
                } else {
                    errorMessage = checkExternalLink(link);
                }
                if (errorMessage != null) {
                    List brokenLinksForThisDoc = (List) brokenLinks.get(relDocUri);
                    if (brokenLinksForThisDoc == null) {
                        brokenLinksForThisDoc = new LinkedList();
                        brokenLinks.put(relDocUri, brokenLinksForThisDoc);
                    }
                    brokenLinksForThisDoc.add(new Object[] { link, errorMessage });
                }
            }
        }
        return brokenLinks;
    }

    private Content checkInternalLink(String link) {
        try {
            String val = WebDAVHelper.propfindAsString(getAbsoluteUri(link), DAV_NS , "getcontenttype", httpState);
            if (val == null) {
                Element el = new Element("text", "i18n", I18N_NS_URI);
                el.addContent(I18N_INTERNAL_LINK_INVALID);
                return el;
            }
            return null;
        } catch (Exception e) {
            return new Text(e.toString());
        } 
    }

    private String normalizeExternalUri(String uri){
        int protIndex = uri.indexOf(PROTOCOL_SEPARATOR);
        if (protIndex <= 0){
            return DEFAULT_PROTOCOL_PREFIX+uri;
        } else {
            return uri;
        }
    }
    
    private Content checkExternalLink(String link) {
        generalHttpClient.setConnectionTimeout(30000);
        HeadMethod head = null;
        try {
        	
			if (normalizeExternalUri(link).equals("http://")) {
				Element el = new Element("text", "i18n", I18N_NS_URI);
				el.addContent(HTTP_ERROR_PREFIX + new Integer(400).toString());
				return el;
			} 
            head = new HeadMethod(normalizeExternalUri(link));
            head.setFollowRedirects(true);
            try {
                int resultCode = generalHttpClient.executeMethod(head);
                if (resultCode >= 300) {
                    Element el = new Element("text", "i18n", I18N_NS_URI);
                    el.addContent(HTTP_ERROR_PREFIX + new Integer(head.getStatusCode()).toString());
                    return el;
                }
            } catch (Exception e) {
                return new Text(e.toString());
            }
            return null;
        } finally {
            if (head != null) {
                head.releaseConnection();
            }
        }
    }

    private boolean ensureFolderExists(String relativePath) {
        String absPath = getAbsoluteUri(relativePath);
        PropFindMethod method = new PropFindMethod(absPath);
        try {
            int result = repoHttpClient.executeMethod(method.getHostConfiguration(), method, httpState);
            if (result >= 200 && result < 300) {
                return true;
            } else {
                MkcolMethod mkColMethod = new MkcolMethod(absPath);
                result = repoHttpClient.executeMethod(mkColMethod.getHostConfiguration(), mkColMethod, httpState);
                return (result >= 200 && result < 300);
            }
        } catch (IOException e) {
            logger.error("Error while creating folder " + absPath);
        }

        return false;
    }

    private boolean ensureFolderExists() {
        return (ensureFolderExists(CONFIGURATION_FOLDER) && ensureFolderExists(BROKENLINKS_FOLDER));
    }

    /**
     * Save the results of link checking to a repository file.
     * 
     * @param repository URL of the output file.
     * @param username Username for connecting to the repository.
     * @param password Password for connecting to the repository.
     * @param pages Broken links data.
     * @throws IOException
     */
    public void updateBrokenLinksDocument(Map pages) throws IOException {
        ensureFolderExists();
        Element root = new Element("broken-links");
        root.setAttribute("date", new Date().toString());
        for (Iterator it = pages.keySet().iterator(); it.hasNext();) {
            String page = (String) it.next();
            Element pageEl = new Element("page");
            pageEl.setAttribute("url", page);
            Collection links = (Collection) pages.get(page);
            for (Iterator it2 = links.iterator(); it2.hasNext();) {
                Object[] link = (Object[]) it2.next();
                Element linkEl = new Element("link");
                linkEl.setAttribute("url", (String) link[0]);
                linkEl.addContent((Content) link[1]);
                pageEl.addContent(linkEl);
            }
            root.addContent(pageEl);
        }
        Document doc = new Document(root);
        XMLOutputter outputter = new XMLOutputter();
        StringWriter sw = new StringWriter();
        outputter.output(doc, sw);
        WebDAVHelper.put(brokenLinksDocPath, new ByteArrayInputStream(sw.toString().getBytes()), httpState);
    }

    public void run() {
        try {
            refreshConnection();
            updateBrokenLinksDocument(checkLinks(getUrlsWithLinks()));
        } catch (IOException e) {
            logger.error(e);
        }
    }

}
