All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.kohsuke.stapler.jelly.jruby.JRubyFacet Maven / Gradle / Ivy

There is a newer version: 1.263
Show newest version
package org.kohsuke.stapler.jelly.jruby;

import org.apache.commons.jelly.JellyTagException;
import org.apache.commons.jelly.Script;
import org.jruby.RubyClass;
import org.jruby.RubyModule;
import org.jruby.RubyObject;
import org.jruby.embed.LocalContextScope;
import org.jruby.embed.ScriptingContainer;
import org.kohsuke.MetaInfServices;
import org.kohsuke.stapler.Dispatcher;
import org.kohsuke.stapler.Facet;
import org.kohsuke.stapler.MetaClass;
import org.kohsuke.stapler.RequestImpl;
import org.kohsuke.stapler.ResponseImpl;
import org.kohsuke.stapler.TearOffSupport;
import org.kohsuke.stapler.WebApp;
import org.kohsuke.stapler.jelly.JellyCompatibleFacet;
import org.kohsuke.stapler.jelly.JellyFacet;
import org.kohsuke.stapler.jelly.jruby.erb.ERbClassTearOff;
import org.kohsuke.stapler.lang.Klass;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;

import static java.util.logging.Level.FINE;

/**
 * {@link Facet} that adds Ruby-based view technologies.
 *
 * @author Kohsuke Kawaguchi
 * @author Hiroshi Nakamura
 */
@MetaInfServices(Facet.class)
public class JRubyFacet extends Facet implements JellyCompatibleFacet {
    /*package*/ final List languages = new CopyOnWriteArrayList();

    /**
     * There are all kinds of downsides in doing this, but for the time being we just use one scripting container.
     */
    private final ScriptingContainer container;

    private final RubyKlassNavigator navigator;

    /**
     * {@link RubyTemplateContainer}s keyed by their {@linkplain RubyTemplateLanguage#getScriptExtension() extensions}.
     * (since {@link #container} is a singleton per {@link JRubyFacet}, this is also just one map.
     */
    private final Map templateContainers = new HashMap();

    private final Collection> tearOffTypes = new CopyOnWriteArrayList>();

    public JRubyFacet() {
        // TODO: is this too early? Shall we allow registrations later?
        languages.addAll(Facet.discoverExtensions(RubyTemplateLanguage.class,
                Thread.currentThread().getContextClassLoader(), getClass().getClassLoader()));

        container = new ScriptingContainer(LocalContextScope.SINGLETHREAD); // we don't want any funny multiplexing from ScriptingContainer.
        container.runScriptlet("require 'org/kohsuke/stapler/jelly/jruby/JRubyJellyScriptImpl'");

        navigator = new RubyKlassNavigator(container.getProvider().getRuntime(), getClass().getClassLoader());

        for (RubyTemplateLanguage l : languages) {
            templateContainers.put(l.getScriptExtension(),l.createContainer(container));
            tearOffTypes.add(l.getTearOffClass());
        }
    }

    private RubyTemplateContainer selectTemplateContainer(String path) {
        int idx = path.lastIndexOf('.');
        if (idx >= 0) {
            RubyTemplateContainer t = templateContainers.get(path.substring(idx));
            if (t!=null)    return t;
        }
        throw new IllegalArgumentException("Unrecognized file extension: "+path);
    }

    public Script parseScript(URL template) throws IOException {
        return selectTemplateContainer(template.getPath()).parseScript(template);
    }

    @Override
    public Klass getKlass(Object o) {
        if (o instanceof RubyObject)
            return makeKlass(((RubyObject) o).getMetaClass());
        return null;
    }

    private Klass makeKlass(RubyModule o) {
        return new Klass(o,navigator);
    }

    public synchronized MetaClass getClassInfo(RubyClass r) {
        return WebApp.getCurrent().getMetaClass(makeKlass(r));
    }

    private boolean isRuby(MetaClass mc) {
        return mc.klass.clazz instanceof RubyModule;
    }

    public void buildViewDispatchers(final MetaClass owner, List dispatchers) {
        for (final Class t : getClassTearOffTypes()) {
            dispatchers.add(new ScriptInvokingDispatcher() {
                final AbstractRubyTearOff tearOff = owner.loadTearOff(t);
                @Override
                public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException {
                    String next = req.tokens.peek();
                    if(next==null) return false;

                    // only match the end of the URL
                    if (req.tokens.countRemainingTokens()>1)    return false;
                    // and avoid serving both "foo" and "foo/" as relative URL semantics are drastically different
                    if (req.getRequestURI().endsWith("/"))      return false;
                    
                    if (!isBasename(next)) {
                        // potentially an attempt to make a folder traversal
                        return false;
                    }
    
                    Script script = tearOff.findScript(next);
    
                    if (script == null) {
                        // no script found
                        return false;
                    }
                    
                    return invokeScript(req, rsp, node, next, script);
                }
            });
        }
    }

    @Override
    public void buildFallbackDispatchers(MetaClass owner, List dispatchers) {
        if (isRuby(owner)) {
            dispatchers.add(new RackDispatcher());
        }
    }

    public Collection> getClassTearOffTypes() {
        return tearOffTypes;
    }

    public Collection getScriptExtensions() {
        List r = new ArrayList();
        for (RubyTemplateLanguage l : languages)
            r.add(l.getScriptExtension());
        return r;
    }


    public RequestDispatcher createRequestDispatcher(RequestImpl request, Klass type, Object it, String viewName) throws IOException {
        TearOffSupport mc = request.stapler.getWebApp().getMetaClass(type);
        return mc.loadTearOff(ERbClassTearOff.class).createDispatcher(it,viewName);
    }

    private ScriptDispatcher makeIndexDispatcher(MetaClass mc) throws IOException {
        for (Class t : getClassTearOffTypes()) {
            final AbstractRubyTearOff rt = mc.loadTearOff(t);
            final Script script = rt.findScript("index");
            if(script!=null)
                return new ScriptDispatcher(rt, script);
        }
        return null;
    }

    @Override
    public void buildIndexDispatchers(MetaClass mc, List dispatchers) {
        try {
            ScriptDispatcher d = makeIndexDispatcher(mc);
            if (d!=null)
                dispatchers.add(d);
        } catch (IOException e) {
            LOGGER.log(Level.WARNING, "Failed to parse Ruby index view for "+mc, e);
        }
    }

    public boolean handleIndexRequest(RequestImpl req, ResponseImpl rsp, Object node, MetaClass mc) throws IOException, ServletException {
        ScriptDispatcher d = makeIndexDispatcher(mc);
        return d!=null && d.dispatch(req,rsp,node);
    }

    private static class ScriptDispatcher extends Dispatcher {
        private final AbstractRubyTearOff rt;
        private final Script script;

        public ScriptDispatcher(AbstractRubyTearOff rt, Script script) {
            this.rt = rt;
            this.script = script;
        }

        @Override
        public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException {
            try {
                if (req.tokens.hasMore())
                    return false;

                if(LOGGER.isLoggable(FINE))
                    LOGGER.fine("Invoking index"+ rt.getDefaultScriptExtension()+" on " + node);

                WebApp.getCurrent().getFacet(JellyFacet.class).scriptInvoker.invokeScript(req, rsp, script, node);
                return true;
            } catch (JellyTagException e) {
                throw new ServletException(e);
            }
        }

        @Override
        public String toString() {
            return "index"+ rt.getDefaultScriptExtension()+" for url=/";
        }
    }
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy