001package org.kohsuke.stapler.jelly.jruby;
002
003import org.apache.commons.jelly.JellyTagException;
004import org.apache.commons.jelly.Script;
005import org.jruby.RubyClass;
006import org.jruby.RubyModule;
007import org.jruby.RubyObject;
008import org.jruby.embed.LocalContextScope;
009import org.jruby.embed.ScriptingContainer;
010import org.kohsuke.MetaInfServices;
011import org.kohsuke.stapler.Dispatcher;
012import org.kohsuke.stapler.Facet;
013import org.kohsuke.stapler.MetaClass;
014import org.kohsuke.stapler.RequestImpl;
015import org.kohsuke.stapler.ResponseImpl;
016import org.kohsuke.stapler.TearOffSupport;
017import org.kohsuke.stapler.WebApp;
018import org.kohsuke.stapler.jelly.JellyCompatibleFacet;
019import org.kohsuke.stapler.jelly.JellyFacet;
020import org.kohsuke.stapler.jelly.jruby.erb.ERbClassTearOff;
021import org.kohsuke.stapler.lang.Klass;
022
023import javax.servlet.RequestDispatcher;
024import javax.servlet.ServletException;
025import java.io.IOException;
026import java.net.URL;
027import java.util.ArrayList;
028import java.util.Collection;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.concurrent.CopyOnWriteArrayList;
033import java.util.logging.Level;
034
035import static java.util.logging.Level.FINE;
036
037/**
038 * {@link Facet} that adds Ruby-based view technologies.
039 *
040 * @author Kohsuke Kawaguchi
041 * @author Hiroshi Nakamura
042 */
043@MetaInfServices(Facet.class)
044public class JRubyFacet extends Facet implements JellyCompatibleFacet {
045    /*package*/ final List<RubyTemplateLanguage> languages = new CopyOnWriteArrayList<RubyTemplateLanguage>();
046
047    /**
048     * There are all kinds of downsides in doing this, but for the time being we just use one scripting container.
049     */
050    private final ScriptingContainer container;
051
052    private final RubyKlassNavigator navigator;
053
054    /**
055     * {@link RubyTemplateContainer}s keyed by their {@linkplain RubyTemplateLanguage#getScriptExtension() extensions}.
056     * (since {@link #container} is a singleton per {@link JRubyFacet}, this is also just one map.
057     */
058    private final Map<String,RubyTemplateContainer> templateContainers = new HashMap<String, RubyTemplateContainer>();
059
060    private final Collection<Class<? extends AbstractRubyTearOff>> tearOffTypes = new CopyOnWriteArrayList<Class<? extends AbstractRubyTearOff>>();
061
062    public JRubyFacet() {
063        // TODO: is this too early? Shall we allow registrations later?
064        languages.addAll(Facet.discoverExtensions(RubyTemplateLanguage.class,
065                Thread.currentThread().getContextClassLoader(), getClass().getClassLoader()));
066
067        container = new ScriptingContainer(LocalContextScope.SINGLETHREAD); // we don't want any funny multiplexing from ScriptingContainer.
068        container.runScriptlet("require 'org/kohsuke/stapler/jelly/jruby/JRubyJellyScriptImpl'");
069
070        navigator = new RubyKlassNavigator(container.getProvider().getRuntime(), getClass().getClassLoader());
071
072        for (RubyTemplateLanguage l : languages) {
073            templateContainers.put(l.getScriptExtension(),l.createContainer(container));
074            tearOffTypes.add(l.getTearOffClass());
075        }
076    }
077
078    private RubyTemplateContainer selectTemplateContainer(String path) {
079        int idx = path.lastIndexOf('.');
080        if (idx >= 0) {
081            RubyTemplateContainer t = templateContainers.get(path.substring(idx));
082            if (t!=null)    return t;
083        }
084        throw new IllegalArgumentException("Unrecognized file extension: "+path);
085    }
086
087    public Script parseScript(URL template) throws IOException {
088        return selectTemplateContainer(template.getPath()).parseScript(template);
089    }
090
091    @Override
092    public Klass<RubyModule> getKlass(Object o) {
093        if (o instanceof RubyObject)
094            return makeKlass(((RubyObject) o).getMetaClass());
095        return null;
096    }
097
098    private Klass<RubyModule> makeKlass(RubyModule o) {
099        return new Klass<RubyModule>(o,navigator);
100    }
101
102    public synchronized MetaClass getClassInfo(RubyClass r) {
103        return WebApp.getCurrent().getMetaClass(makeKlass(r));
104    }
105
106    private boolean isRuby(MetaClass mc) {
107        return mc.klass.clazz instanceof RubyModule;
108    }
109
110    public void buildViewDispatchers(final MetaClass owner, List<Dispatcher> dispatchers) {
111        for (final Class<? extends AbstractRubyTearOff> t : getClassTearOffTypes()) {
112            dispatchers.add(new ScriptInvokingDispatcher() {
113                final AbstractRubyTearOff tearOff = owner.loadTearOff(t);
114                @Override
115                public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException {
116                    String next = req.tokens.peek();
117                    if(next==null) return false;
118
119                    // only match the end of the URL
120                    if (req.tokens.countRemainingTokens()>1)    return false;
121                    // and avoid serving both "foo" and "foo/" as relative URL semantics are drastically different
122                    if (req.getRequestURI().endsWith("/"))      return false;
123                    
124                    if (!isBasename(next)) {
125                        // potentially an attempt to make a folder traversal
126                        return false;
127                    }
128    
129                    Script script = tearOff.findScript(next);
130    
131                    if (script == null) {
132                        // no script found
133                        return false;
134                    }
135                    
136                    return invokeScript(req, rsp, node, next, script);
137                }
138            });
139        }
140    }
141
142    @Override
143    public void buildFallbackDispatchers(MetaClass owner, List<Dispatcher> dispatchers) {
144        if (isRuby(owner)) {
145            dispatchers.add(new RackDispatcher());
146        }
147    }
148
149    public Collection<Class<? extends AbstractRubyTearOff>> getClassTearOffTypes() {
150        return tearOffTypes;
151    }
152
153    public Collection<String> getScriptExtensions() {
154        List<String> r = new ArrayList<String>();
155        for (RubyTemplateLanguage l : languages)
156            r.add(l.getScriptExtension());
157        return r;
158    }
159
160
161    public RequestDispatcher createRequestDispatcher(RequestImpl request, Klass<?> type, Object it, String viewName) throws IOException {
162        TearOffSupport mc = request.stapler.getWebApp().getMetaClass(type);
163        return mc.loadTearOff(ERbClassTearOff.class).createDispatcher(it,viewName);
164    }
165
166    private ScriptDispatcher makeIndexDispatcher(MetaClass mc) throws IOException {
167        for (Class<? extends AbstractRubyTearOff> t : getClassTearOffTypes()) {
168            final AbstractRubyTearOff rt = mc.loadTearOff(t);
169            final Script script = rt.findScript("index");
170            if(script!=null)
171                return new ScriptDispatcher(rt, script);
172        }
173        return null;
174    }
175
176    @Override
177    public void buildIndexDispatchers(MetaClass mc, List<Dispatcher> dispatchers) {
178        try {
179            ScriptDispatcher d = makeIndexDispatcher(mc);
180            if (d!=null)
181                dispatchers.add(d);
182        } catch (IOException e) {
183            LOGGER.log(Level.WARNING, "Failed to parse Ruby index view for "+mc, e);
184        }
185    }
186
187    public boolean handleIndexRequest(RequestImpl req, ResponseImpl rsp, Object node, MetaClass mc) throws IOException, ServletException {
188        ScriptDispatcher d = makeIndexDispatcher(mc);
189        return d!=null && d.dispatch(req,rsp,node);
190    }
191
192    private static class ScriptDispatcher extends Dispatcher {
193        private final AbstractRubyTearOff rt;
194        private final Script script;
195
196        public ScriptDispatcher(AbstractRubyTearOff rt, Script script) {
197            this.rt = rt;
198            this.script = script;
199        }
200
201        @Override
202        public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException {
203            try {
204                if (req.tokens.hasMore())
205                    return false;
206
207                if(LOGGER.isLoggable(FINE))
208                    LOGGER.fine("Invoking index"+ rt.getDefaultScriptExtension()+" on " + node);
209
210                WebApp.getCurrent().getFacet(JellyFacet.class).scriptInvoker.invokeScript(req, rsp, script, node);
211                return true;
212            } catch (JellyTagException e) {
213                throw new ServletException(e);
214            }
215        }
216
217        @Override
218        public String toString() {
219            return "index"+ rt.getDefaultScriptExtension()+" for url=/";
220        }
221    }
222}
223