package jenkins.model;

import hudson.Extension;
import hudson.model.UnprotectedRootAction;
import hudson.util.TimeUnit2;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Enumeration;

/**
 * Serves files located in the {@code /assets} classpath directory via the Jenkins core ClassLoader.
 * e.g. the URL {@code /assets/jquery-detached/jsmodules/jquery2.js} will load {@code jquery-detached/jsmodules/jquery2.js}
 * resource from the classpath below {@code /assets}.
 *
 * @author Kohsuke Kawaguchi
 *
 * @since 2.0
 */
@Extension @Symbol("assetManager")
public class AssetManager implements UnprotectedRootAction {

    // not shown in the UI
    @Override
    public String getIconFileName() {
        return null;
    }

    @Override
    public String getDisplayName() {
        return null;
    }

    @Override
    public String getUrlName() {
        return "assets";
    }

    /**
     * Exposes assets in the core classloader over HTTP.
     */
    public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
        String path = req.getRestOfPath();
        URL resource = findResource(path);

        if (resource == null) {
            rsp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // Stapler routes requests like the "/static/.../foo/bar/zot" to be treated like "/foo/bar/zot"
        // and this is used to serve long expiration header, by using Jenkins.VERSION_HASH as "..."
        // to create unique URLs. Recognize that and set a long expiration header.
        String requestPath = req.getRequestURI().substring(req.getContextPath().length());
        boolean staticLink = requestPath.startsWith("/static/");
        long expires = staticLink ? TimeUnit2.DAYS.toMillis(365) : -1;

        // use serveLocalizedFile to support automatic locale selection
        rsp.serveLocalizedFile(req, resource, expires);
    }

    /**
     * Locates the asset from the classloader.
     *
     * <p>
     * To allow plugins to bring its own assets without worrying about colliding with the assets in core,
     * look for child classloader first. But to support plugins that get split, if the child classloader
     * doesn't find it, fall back to the parent classloader.
     */
    private URL findResource(String path) throws IOException {
        try {
            if (path.contains("..")) // crude avoidance of directory traversal attack
                throw new IllegalArgumentException(path);

            String name;
            if (path.charAt(0) == '/') {
                name = "assets" + path;
            } else {
                name = "assets/" + path;
            }

            ClassLoader cl = Jenkins.class.getClassLoader();
            URL url = (URL) $findResource.invoke(cl, name);
            if (url==null) {
                // pick the last one, which is the one closest to the leaf of the classloader tree.
                Enumeration<URL> e = cl.getResources(name);
                while (e.hasMoreElements()) {
                    url = e.nextElement();
                }
            }
            return url;
        } catch (InvocationTargetException|IllegalAccessException e) {
            throw new Error(e);
        }
    }

    private static final Method $findResource = init();

    private static Method init() {
        try {
            Method m = ClassLoader.class.getDeclaredMethod("findResource", String.class);
            m.setAccessible(true);
            return m;
        } catch (NoSuchMethodException e) {
            throw (Error)new NoSuchMethodError().initCause(e);
        }
    }
}
