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.jelly;
025
026import org.apache.commons.jelly.JellyContext;
027import org.kohsuke.stapler.Stapler;
028import org.kohsuke.stapler.StaplerRequest;
029import org.kohsuke.stapler.StaplerResponse;
030import org.apache.commons.jelly.Script;
031import org.apache.commons.jelly.JellyTagException;
032import org.apache.commons.jelly.XMLOutput;
033import org.apache.commons.jelly.XMLOutputFactory;
034import org.apache.commons.jelly.impl.TagScript;
035
036import javax.annotation.Nonnull;
037import javax.servlet.ServletContext;
038import java.io.BufferedOutputStream;
039import java.io.IOException;
040import java.io.OutputStream;
041import java.io.Writer;
042import java.util.Enumeration;
043
044/**
045 * Standard implementation of {@link ScriptInvoker}.
046 * 
047 * @author Kohsuke Kawaguchi
048 */
049public class DefaultScriptInvoker implements ScriptInvoker, XMLOutputFactory {
050    public void invokeScript(StaplerRequest req, StaplerResponse rsp, Script script, Object it) throws IOException, JellyTagException {
051        XMLOutput xmlOutput = createXMLOutput(req, rsp, script, it);
052
053        invokeScript(req,rsp,script,it,xmlOutput);
054        
055        xmlOutput.flush();
056        xmlOutput.close();
057    }
058
059    public void invokeScript(StaplerRequest req, StaplerResponse rsp, Script script, Object it, XMLOutput out) throws IOException, JellyTagException {
060        JellyContext context = createContext(req,rsp,script,it);
061        exportVariables(req, rsp, script, it, context);
062
063        script.run(context,out);
064    }
065
066    protected XMLOutput createXMLOutput(StaplerRequest req, StaplerResponse rsp, Script script, Object it) throws IOException {
067        // TODO: make XMLOutput auto-close OutputStream to avoid leak
068        String ct = rsp.getContentType();
069        XMLOutput output;
070        if (ct != null && !ct.startsWith("text/html")) {
071            output = XMLOutput.createXMLOutput(createOutputStream(req, rsp, script, it));
072        } else {
073            output = HTMLWriterOutput.create(createOutputStream(req, rsp, script, it));
074
075        }
076        return output;
077    }
078
079    private boolean doCompression(Script script) {
080        if (COMPRESS_BY_DEFAULT)    return true;
081        if (script instanceof TagScript) {
082            TagScript ts = (TagScript) script;
083            if(ts.getLocalName().equals("compress"))
084                return true;
085        }
086        return false;
087    }
088
089    private interface OutputStreamSupplier {
090        @Nonnull OutputStream get() throws IOException;
091    }
092
093    private class LazyOutputStreamSupplier implements OutputStreamSupplier {
094        private final OutputStreamSupplier supplier;
095        private volatile OutputStream out;
096
097        private LazyOutputStreamSupplier(OutputStreamSupplier supplier) {
098            this.supplier = supplier;
099        }
100
101        @Override
102        @Nonnull
103        public OutputStream get() throws IOException {
104            if (out == null) {
105                synchronized (this) {
106                    if (out == null) {
107                        out = supplier.get();
108                    }
109                }
110            }
111            return out;
112        }
113    }
114
115    protected OutputStream createOutputStream(StaplerRequest req, StaplerResponse rsp, Script script, Object it) throws IOException {
116        OutputStreamSupplier out = new LazyOutputStreamSupplier(() -> {
117            req.getWebApp().getDispatchValidator().requireDispatchAllowed(req, rsp);
118            return doCompression(script) ? rsp.getCompressedOutputStream(req) : new BufferedOutputStream(rsp.getOutputStream());
119        });
120        return new OutputStream() {
121            @Override
122            public void write(int b) throws IOException {
123                out.get().write(b);
124            }
125
126            @Override
127            public void write(byte[] b) throws IOException {
128                out.get().write(b);
129            }
130
131            @Override
132            public void write(byte[] b, int off, int len) throws IOException {
133                out.get().write(b, off, len);
134            }
135
136            @Override
137            public void flush() throws IOException {
138                // flushing ServletOutputStream causes Tomcat to
139                // send out headers, making it impossible to set contentType from the script.
140                // so don't let Jelly flush.
141            }
142
143            @Override
144            public void close() throws IOException {
145                out.get().close();
146            }
147        };
148    }
149
150    protected void exportVariables(StaplerRequest req, StaplerResponse rsp, Script script, Object it, JellyContext context) {
151        Enumeration en = req.getAttributeNames();
152        // expose request attributes, just like JSP
153        while (en.hasMoreElements()) {
154            String name = (String) en.nextElement();
155            context.setVariable(name,req.getAttribute(name));
156        }
157
158        context.setVariable("request",req);
159        context.setVariable("response",rsp);
160        context.setVariable("it",it);
161        ServletContext servletContext = req.getServletContext();
162        context.setVariable("servletContext",servletContext);
163        context.setVariable("app",servletContext.getAttribute("app"));
164        // property bag to store request scope variables
165        context.setVariable("requestScope",context.getVariables());
166        // this variable is needed to make "jelly:fmt" taglib work correctly
167        context.setVariable("org.apache.commons.jelly.tags.fmt.locale",req.getLocale());
168    }
169
170    protected JellyContext createContext(final StaplerRequest req, StaplerResponse rsp, Script script, Object it) {
171        CustomJellyContext context = new CustomJellyContext();
172        // let Jelly see the whole classes
173        context.setClassLoader(req.getWebApp().getClassLoader());
174        // so TagScript.getBodyText() will use HTMLWriterOutput
175        context.setVariable(XMLOutputFactory.class.getName(), this);
176        return context;
177    }
178
179    public XMLOutput createXMLOutput(Writer writer, boolean escapeText) {
180        StaplerResponse rsp = Stapler.getCurrentResponse();
181        String ct = rsp!=null ? rsp.getContentType() : "?";
182        if (ct != null && !ct.startsWith("text/html"))
183            return XMLOutput.createXMLOutput(writer, escapeText);
184        return HTMLWriterOutput.create(writer, escapeText);
185    }
186
187    /**
188     * Whether gzip compression of the dynamic content is enabled by default or not.
189     *
190     * <p>
191     * For non-trivial web applications, where the performance matters, it is normally a good trade-off to spend
192     * a bit of CPU cycles to compress data. This is because:
193     *
194     * <ul>
195     * <li>CPU is already 1 or 2 order of magnitude faster than RAM and network.
196     * <li>CPU is getting faster than any other components, such as RAM and network.
197     * <li>Because of the TCP window slow start, on a large latency network, compression makes difference in
198     *     the order of 100ms to 1sec to the completion of a request by saving multiple roundtrips.
199     * </ul>
200     *
201     * Stuff rendered by Jelly is predominantly text, so the compression would work well.
202     *
203     * @see <a href="http://www.slideshare.net/guest22d4179/latency-trumps-all">Latency Trumps All</a>
204     */
205    public static boolean COMPRESS_BY_DEFAULT = Boolean.parseBoolean(System.getProperty(DefaultScriptInvoker.class.getName()+".compress","true"));
206}