/*
 * 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.
 */

/* $Id: FOURIResolver.java 1296483 2012-03-02 21:34:30Z gadams $ */

package org.apache.fop.apps;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;

import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;
import javax.xml.transform.stream.StreamSource;

import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.xmlgraphics.util.io.Base64EncodeStream;
import org.apache.xmlgraphics.util.uri.CommonURIResolver;

/**
 * Provides FOP specific URI resolution. This is the default URIResolver
 * {@link FOUserAgent} will use unless overridden.
 *
 * @see javax.xml.transform.URIResolver
 */
public class FOURIResolver implements javax.xml.transform.URIResolver {

    // log
    private Log log = LogFactory.getLog("FOP");

    /** Common URIResolver */
    private CommonURIResolver commonURIResolver = new CommonURIResolver();

    /** A user settable URI Resolver */
    private URIResolver uriResolver = null;

    /** true if exceptions are to be thrown if the URIs cannot be resolved. */
    private boolean throwExceptions = false;

    /**
     * Checks if the given base URL is acceptable. It also normalizes the URL.
     * @param base the base URL to check
     * @return the normalized URL
     * @throws MalformedURLException if there's a problem with a file URL
     */
    public String checkBaseURL(String base) throws MalformedURLException {
        // replace back slash with forward slash to ensure windows file:/// URLS are supported
        base = base.replace('\\', '/');
        if (!base.endsWith("/")) {
            // The behavior described by RFC 3986 regarding resolution of relative
            // references may be misleading for normal users:
            // file://path/to/resources + myResource.res -> file://path/to/myResource.res
            // file://path/to/resources/ + myResource.res -> file://path/to/resources/myResource.res
            // We assume that even when the ending slash is missing, users have the second
            // example in mind
            base += "/";
        }
        File dir = new File(base);
        if (dir.isDirectory()) {
            return dir.toURI().toASCIIString();
        } else {
            URI baseURI;
            try {
                baseURI = new URI(base);
                String scheme = baseURI.getScheme();
                boolean directoryExists = true;
                if ("file".equals(scheme)) {
                    dir = FileUtils.toFile(baseURI.toURL());
                    directoryExists = dir.isDirectory();
                }
                if (scheme == null || !directoryExists) {
                    String message = "base " + base + " is not a valid directory";
                    if (throwExceptions) {
                        throw new MalformedURLException(message);
                    }
                    log.error(message);
                }
                return baseURI.toASCIIString();
            } catch (URISyntaxException e) {
                //TODO not ideal: our base URLs are actually base URIs.
                throw new MalformedURLException(e.getMessage());
            }
        }
    }

    /**
     * Default constructor
     */
    public FOURIResolver() {
        this(false);
    }

    /**
     * Additional constructor
     *
     * @param throwExceptions
     *            true if exceptions are to be thrown if the URIs cannot be
     *            resolved.
     */
    public FOURIResolver(boolean throwExceptions) {
        this.throwExceptions = throwExceptions;
    }

    /**
     * Handles resolve exceptions appropriately.
     *
     * @param e
     *            the exception
     * @param errorStr
     *            error string
     * @param strict
     *            strict user config
     */
    private void handleException(Exception e, String errorStr, boolean strict)
            throws TransformerException {
        if (strict) {
            throw new TransformerException(errorStr, e);
        }
        log.error(e.getMessage());
    }

    /**
     * Called by the processor through {@link FOUserAgent} when it encounters an
     * uri in an external-graphic element. (see also
     * {@link javax.xml.transform.URIResolver#resolve(String, String)} This
     * resolver will allow URLs without a scheme, i.e. it assumes 'file:' as the
     * default scheme. It also allows relative URLs with scheme, e.g.
     * file:../../abc.jpg which is not strictly RFC compliant as long as the
     * scheme is the same as the scheme of the base URL. If the base URL is null
     * a 'file:' URL referencing the current directory is used as the base URL.
     * If the method is successful it will return a Source of type
     * {@link javax.xml.transform.stream.StreamSource} with its SystemID set to
     * the resolved URL used to open the underlying InputStream.
     *
     * @param href
     *            An href attribute, which may be relative or absolute.
     * @param base
     *            The base URI against which the first argument will be made
     *            absolute if the absolute URI is required.
     * @return A {@link javax.xml.transform.Source} object, or null if the href
     *         cannot be resolved.
     * @throws javax.xml.transform.TransformerException
     *             Never thrown by this implementation.
     * @see javax.xml.transform.URIResolver#resolve(String, String)
     */
    public Source resolve(String href, String base) throws TransformerException {
        Source source = null;

        // data URLs can be quite long so evaluate early and don't try to build a File
        // (can lead to problems)
        source = commonURIResolver.resolve(href, base);

        // Custom uri resolution
        if (source == null && uriResolver != null) {
            source = uriResolver.resolve(href, base);
        }

        // Fallback to default resolution mechanism
        if (source == null) {
            URL absoluteURL = null;
            int hashPos = href.indexOf('#');
            String fileURL;
            String fragment;
            if (hashPos >= 0) {
                fileURL = href.substring(0, hashPos);
                fragment = href.substring(hashPos);
            } else {
                fileURL = href;
                fragment = null;
            }
            File file = new File(fileURL);
            if (file.canRead() && file.isFile()) {
                try {
                    if (fragment != null) {
                        absoluteURL = new URL(file.toURI().toURL().toExternalForm() + fragment);
                    } else {
                        absoluteURL = file.toURI().toURL();
                    }
                } catch (MalformedURLException mfue) {
                    handleException(mfue, "Could not convert filename '" + href
                            + "' to URL", throwExceptions);
                }
            } else {
                // no base provided
                if (base == null) {
                    // We don't have a valid file protocol based URL
                    try {
                        absoluteURL = new URL(href);
                    } catch (MalformedURLException mue) {
                        try {
                            // the above failed, we give it another go in case
                            // the href contains only a path then file: is
                            // assumed
                            absoluteURL = new URL("file:" + href);
                        } catch (MalformedURLException mfue) {
                            handleException(mfue, "Error with URL '" + href
                                    + "'", throwExceptions);
                        }
                    }

                    // try and resolve from context of base
                } else {
                    URL baseURL = null;
                    try {
                        baseURL = new URL(base);
                    } catch (MalformedURLException mfue) {
                        handleException(mfue, "Error with base URL '" + base
                                + "'", throwExceptions);
                    }

                    /*
                     * This piece of code is based on the following statement in
                     * RFC2396 section 5.2:
                     *
                     * 3) If the scheme component is defined, indicating that
                     * the reference starts with a scheme name, then the
                     * reference is interpreted as an absolute URI and we are
                     * done. Otherwise, the reference URI's scheme is inherited
                     * from the base URI's scheme component.
                     *
                     * Due to a loophole in prior specifications [RFC1630], some
                     * parsers allow the scheme name to be present in a relative
                     * URI if it is the same as the base URI scheme.
                     * Unfortunately, this can conflict with the correct parsing
                     * of non-hierarchical URI. For backwards compatibility, an
                     * implementation may work around such references by
                     * removing the scheme if it matches that of the base URI
                     * and the scheme is known to always use the <hier_part>
                     * syntax.
                     *
                     * The URL class does not implement this work around, so we
                     * do.
                     */
                    assert (baseURL != null);
                    String scheme = baseURL.getProtocol() + ":";
                    if (href.startsWith(scheme) && "file:".equals(scheme)) {
                        href = href.substring(scheme.length());
                        int colonPos = href.indexOf(':');
                        int slashPos = href.indexOf('/');
                        if (slashPos >= 0 && colonPos >= 0
                                && colonPos < slashPos) {
                            href = "/" + href; // Absolute file URL doesn't
                            // have a leading slash
                        }
                    }
                    try {
                        absoluteURL = new URL(baseURL, href);
                    } catch (MalformedURLException mfue) {
                        handleException(mfue, "Error with URL; base '" + base
                                + "' " + "href '" + href + "'", throwExceptions);
                    }
                }
            }

            if (absoluteURL != null) {
                String effURL = absoluteURL.toExternalForm();
                try {
                    URLConnection connection = absoluteURL.openConnection();
                    connection.setAllowUserInteraction(false);
                    connection.setDoInput(true);
                    updateURLConnection(connection, href);
                    connection.connect();
                    return new StreamSource(connection.getInputStream(), effURL);
                } catch (FileNotFoundException fnfe) {
                    // Note: This is on "debug" level since the caller is
                    // supposed to handle this
                    log.debug("File not found: " + effURL);
                } catch (java.io.IOException ioe) {
                    log.error("Error with opening URL '" + effURL + "': "
                            + ioe.getMessage());
                }
            }
        }
        return source;
    }

    /**
     * This method allows you to set special values on a URLConnection just
     * before the connect() method is called. Subclass FOURIResolver and
     * override this method to do things like adding the user name and password
     * for HTTP basic authentication.
     *
     * @param connection
     *            the URLConnection instance
     * @param href
     *            the original URI
     */
    protected void updateURLConnection(URLConnection connection, String href) {
        // nop
    }

    /**
     * This is a convenience method for users who want to override
     * updateURLConnection for HTTP basic authentication. Simply call it using
     * the right username and password.
     *
     * @param connection
     *            the URLConnection to set up for HTTP basic authentication
     * @param username
     *            the username
     * @param password
     *            the password
     */
    protected void applyHttpBasicAuthentication(URLConnection connection,
            String username, String password) {
        String combined = username + ":" + password;
        try {
            ByteArrayOutputStream baout = new ByteArrayOutputStream(combined
                    .length() * 2);
            Base64EncodeStream base64 = new Base64EncodeStream(baout);
            // TODO Not sure what charset/encoding can be used with basic
            // authentication
            base64.write(combined.getBytes("UTF-8"));
            base64.close();
            connection.setRequestProperty("Authorization", "Basic "
                    + new String(baout.toByteArray(), "UTF-8"));
        } catch (IOException e) {
            // won't happen. We're operating in-memory.
            throw new RuntimeException(
                    "Error during base64 encodation of username/password");
        }
    }

    /**
     * Sets the custom URI Resolver. It is used for resolving factory-level URIs like
     * hyphenation patterns and as backup for URI resolution performed during a
     * rendering run.
     *
     * @param resolver
     *            the new URI resolver
     */
    public void setCustomURIResolver(URIResolver resolver) {
        this.uriResolver = resolver;
    }

    /**
     * Returns the custom URI Resolver.
     *
     * @return the URI Resolver or null, if none is set
     */
    public URIResolver getCustomURIResolver() {
        return this.uriResolver;
    }

    /**
     * @param throwExceptions
     *            Whether or not to throw exceptions on resolution error
     */
    public void setThrowExceptions(boolean throwExceptions) {
        this.throwExceptions = throwExceptions;
    }
}
