001/*
002 * Copyright (c) 2004-2010, Kohsuke Kawaguchi
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
006 * that the following conditions are met:
007 *
008 *     * Redistributions of source code must retain the above copyright notice, this list of
009 *       conditions and the following disclaimer.
010 *     * Redistributions in binary form must reproduce the above copyright notice, this list of
011 *       conditions and the following disclaimer in the documentation and/or other materials
012 *       provided with the distribution.
013 *
014 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
015 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
016 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
017 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
018 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
019 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
020 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
021 * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
022 */
023
024package org.kohsuke.stapler.framework.adjunct;
025
026import org.kohsuke.stapler.*;
027
028import javax.servlet.ServletException;
029import javax.servlet.ServletContext;
030import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
031import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
032import java.io.IOException;
033import java.net.URL;
034import java.util.concurrent.ConcurrentHashMap;
035
036/**
037 * This application-scoped object that exposes djuncts to URL.
038 *
039 * <p>
040 * Adjuncts are packaging of JavaScript, CSS, and other static assets in jar files with dependency
041 * information between them. This allows JavaScript libraries and other static assets to be reused
042 * across different projects through Maven/Ivy.
043 *
044 * <p>
045 * To use {@link AdjunctManager} in your application, create one instance, and bind it to URL
046 * (like you do any other objects.) The most typical way of doing this is to define it as a
047 * field in your top-level object.
048 *
049 * <pre>
050 * public class MyApplication {
051 *     public final AdjunctManager adjuncts = new AdjunctManager(context,getClass().getClassLoader(),"/adjuncts");
052 * }
053 * </pre>
054 *
055 * <p>
056 * How you include an adjunct will depend on your template language, but for example in Jelly you do:
057 * <pre>{@code <st:adjunct includes="org.kohsuke.stapler.bootstrap"/>}</pre>
058 *
059 * Or from Groovy you do:
060 * <pre>{@code adjunct "org.kohsuke.stapler.bootstrap"}</pre>
061 *
062 * <p>
063 * ... and this produces a series of <tt>style</tt> and <tt>script</tt> tags that include all the
064 * necessary JavaScript, CSS, and their dependencies.
065 *
066 * <p>
067 * Internally, this class provides caching for {@link Adjunct}s.
068 *
069 * @author Kohsuke Kawaguchi
070 * @see Adjunct
071 */
072public class AdjunctManager {
073    private final ConcurrentHashMap<String, Adjunct> adjuncts = new ConcurrentHashMap<String,Adjunct>();
074
075    /**
076     * Map used as a set to remember which resources can be served.
077     */
078    private final ConcurrentHashMap<String,String> allowedResources = new ConcurrentHashMap<String,String>();
079
080    private final ClassLoader classLoader;
081
082    /**
083     * Absolute URL of the {@link AdjunctManager} in the calling application where it is bound to.
084     *
085     * <p>
086     * The path is treated relative from the context path of the application, and it
087     * needs to end without '/'. So it needs to be something like "foo/adjuncts" or
088     * just "adjuncts". Can be e.g. {@code adjuncts/uNiQuEhAsH} to improve caching behavior.
089     */
090    public final String rootURL;
091
092    /**
093     * Hint instructing adjuncts to load a debuggable non-minified version of the script,
094     * as opposed to the production version.
095     *
096     * This is only a hint, and so the semantics of it isn't very well defined. The intention
097     * is to assist JavaScript debugging.
098     */
099    public boolean debug = Boolean.getBoolean(AdjunctManager.class.getName()+".debug");
100
101    public final WebApp webApp;
102    private final long expiration;
103
104    @Deprecated
105    public AdjunctManager(ServletContext context,ClassLoader classLoader, String rootURL) {
106        this(context, classLoader, rootURL, /* one day */24L * 60 * 60 * 1000);
107    }
108
109    /**
110     * @param classLoader
111     *      ClassLoader to load adjuncts from.
112     * @param rootURL
113     *      See {@link #rootURL} for the meaning of this parameter.
114     * @param expiration milliseconds from service time until expiration, for {@link #doDynamic}
115     *                    (as in {@link StaplerResponse#serveFile(StaplerRequest, URL, long)});
116     *                    if {@link #rootURL} is unique per session then this can be very long;
117     *                    otherwise a day might be reasonable
118     */
119    public AdjunctManager(ServletContext context, ClassLoader classLoader, String rootURL, long expiration) {
120        this.classLoader = classLoader;
121        this.rootURL = rootURL;
122        this.webApp = WebApp.get(context);
123        this.expiration = expiration;
124        // register this globally
125        context.setAttribute(KEY,this);
126    }
127
128    public static AdjunctManager get(ServletContext context) {
129        return (AdjunctManager) context.getAttribute(KEY);
130    }
131
132    /**
133     * Obtains the adjunct.
134     *
135     * @return
136     *      always non-null.
137     * @throws IOException
138     *      if failed to locate {@link Adjunct}.
139     */
140    public Adjunct get(String name) throws IOException {
141        Adjunct a = adjuncts.get(name);
142        if(a!=null) return a;   // found it
143
144        synchronized (this) {
145            a = adjuncts.get(name);
146            if(a!=null) return a;   // one more check before we start loading
147            a = new Adjunct(this,name,classLoader);
148            adjuncts.put(name,a);
149            return a;
150        }
151    }
152
153    /**
154     * Serves resources in the class loader.
155     */
156    public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
157        String path = req.getRestOfPath();
158        if (path.charAt(0)=='/') path = path.substring(1);
159
160        if(!allowedResources.containsKey(path)) {
161            if(!allowResourceToBeServed(path)) {
162                rsp.sendError(SC_FORBIDDEN);
163                return;
164            }
165            // remember URLs that we can serve. but don't remember error ones, as it might be unbounded
166            allowedResources.put(path,path);
167        }
168
169        URL res = classLoader.getResource(path);
170        if(res==null) {
171            throw HttpResponses.error(SC_NOT_FOUND,new IllegalArgumentException("No such adjunct found: "+path));
172        } else {
173            long expires = MetaClass.NO_CACHE ? 0 : expiration;
174            rsp.serveFile(req,res,expires);
175        }
176    }
177
178    /**
179     * Controls whether the given resource can be served to browsers.
180     *
181     * <p>
182     * This method can be overridden by the sub classes to change the access control behavior.
183     *
184     * <p>
185     * {@link AdjunctManager} is capable of serving all the resources visible
186     * in the classloader by default. If the resource files need to be kept private,
187     * return false, which causes the request to fail with 401. 
188     *
189     * Otherwise return true, in which case the resource will be served.
190     */
191    protected boolean allowResourceToBeServed(String absolutePath) {
192        // does it have an adjunct directory marker?
193        int idx = absolutePath.lastIndexOf('/');
194        if (idx>0 && classLoader.getResource(absolutePath.substring(0,idx)+"/.adjunct")!=null)
195            return true;
196
197        // backward compatible behaviour
198        return absolutePath.endsWith(".gif")
199            || absolutePath.endsWith(".png")
200            || absolutePath.endsWith(".css")
201            || absolutePath.endsWith(".js");
202    }
203
204    /**
205     * Key in {@link ServletContext} to look up {@link AdjunctManager}.
206     */
207    private static final String KEY = AdjunctManager.class.getName();
208}