001    /*
002     * $Id: AbstractHttpServlet.java 4032 2006-08-30 07:18:49Z mguillem $
003     * 
004     * Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
005     * 
006     * Redistribution and use of this software and associated documentation
007     * ("Software"), with or without modification, are permitted provided that the
008     * following conditions are met:
009     * 
010     * 1. Redistributions of source code must retain copyright statements and
011     * notices. Redistributions must also contain a copy of this document.
012     * 
013     * 2. Redistributions in binary form must reproduce the above copyright notice,
014     * this list of conditions and the following disclaimer in the documentation
015     * and/or other materials provided with the distribution.
016     * 
017     * 3. The name "groovy" must not be used to endorse or promote products derived
018     * from this Software without prior written permission of The Codehaus. For
019     * written permission, please contact info@codehaus.org.
020     * 
021     * 4. Products derived from this Software may not be called "groovy" nor may
022     * "groovy" appear in their names without prior written permission of The
023     * Codehaus. "groovy" is a registered trademark of The Codehaus.
024     * 
025     * 5. Due credit should be given to The Codehaus - http://groovy.codehaus.org/
026     * 
027     * THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS ``AS IS'' AND ANY
028     * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
029     * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
030     * DISCLAIMED. IN NO EVENT SHALL THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR
031     * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
032     * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
033     * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
034     * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
035     * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
036     * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
037     *  
038     */
039    package groovy.servlet;
040    
041    import groovy.lang.MetaClass;
042    import groovy.util.ResourceConnector;
043    import groovy.util.ResourceException;
044    
045    import java.io.File;
046    import java.io.IOException;
047    import java.net.URL;
048    import java.net.URLConnection;
049    import java.util.regex.Matcher;
050    import java.util.regex.Pattern;
051    
052    import javax.servlet.ServletConfig;
053    import javax.servlet.ServletContext;
054    import javax.servlet.ServletException;
055    import javax.servlet.http.HttpServlet;
056    import javax.servlet.http.HttpServletRequest;
057    
058    /**
059     * A common ground dealing with the HTTP servlet API wrinkles.
060     * 
061     * <h4>Resource name mangling (pattern replacement)</h4>
062     * 
063     * <p> 
064     * Also implements Groovy's {@link groovy.util.ResourceConnector} in dynamic
065     * manner. It allows to modifiy the resource name that is searched for with a
066     * <i>replace all</i> operation. See {@link java.util.regex.Pattern} and
067     * {@link java.util.regex.Matcher} for details.
068     * The servlet init parameter names are:
069     * <pre>
070     * resource.name.regex = empty - defaults to null
071     * resource.name.replacement = empty - defaults to null
072     * resource.name.replace.all = true (default) | false means replaceFirst()
073     * </pre>
074     * Note: If you specify a regex, you have to specify a replacement string too!
075     * Otherwise an exception gets raised.
076     *
077     * <h4>Logging and bug-hunting options</h4>
078     *
079     * <p> 
080     * This implementation provides a verbosity flag switching log statements.
081     * The servlet init parameter name is:
082     * <pre>
083     * verbose = false(default) | true
084     * </pre>
085     * 
086     * <p> 
087     * In order to support class-loading-troubles-debugging with Tomcat 4 or
088     * higher, you can log the class loader responsible for loading some classes.
089     * See <a href="http://jira.codehaus.org/browse/GROOVY-861">GROOVY-861</a> for details.
090     * The servlet init parameter name is:
091     * <pre>
092     * log.GROOVY861 = false(default) | true
093     * </pre>
094     * 
095     * <p> 
096     * If you experience class-loading-troubles with Tomcat 4 (or higher) or any
097     * other servlet container using custom class loader setups, you can fallback
098     * to use (slower) reflection in Groovy's MetaClass implementation. Please
099     * contact the dev team with your problem! Thanks.
100     * The servlet init parameter name is:
101     * <pre>
102     * reflection = false(default) | true
103     * </pre>
104     * 
105     *
106     * @author Christian Stein
107     */
108    public abstract class AbstractHttpServlet extends HttpServlet implements ResourceConnector {
109    
110        /**
111         * Content type of the HTTP response.
112         */
113        public static final String CONTENT_TYPE_TEXT_HTML = "text/html";
114    
115        /**
116         * Servlet API include key name: path_info
117         */
118        public static final String INC_PATH_INFO = "javax.servlet.include.path_info";
119    
120        /* *** Not used, yet. See comments in getScriptUri(HttpServletRequest). ***
121         * Servlet API include key name: request_uri
122         */
123        public static final String INC_REQUEST_URI = "javax.servlet.include.request_uri";
124    
125        /**
126         * Servlet API include key name: servlet_path
127         */
128        public static final String INC_SERVLET_PATH = "javax.servlet.include.servlet_path";
129    
130        /**
131         * Servlet (or the web application) context.
132         */
133        protected ServletContext servletContext;
134    
135        /**
136         * <b>Null</b> or compiled pattern matcher read from "resource.name.regex"
137         *  and used in {@link AbstractHttpServlet#getResourceConnection(String)}.
138         */
139        protected Matcher resourceNameMatcher;
140    
141        /**
142         * The replacement used by the resource name matcher.
143         */
144        protected String resourceNameReplacement;
145    
146        /**
147         * The replace method to use on the matcher.
148         * <pre>
149         * true - replaceAll(resourceNameReplacement); (default)
150         * false - replaceFirst(resourceNameReplacement);
151         * </pre>
152         */
153        protected boolean resourceNameReplaceAll;
154    
155        /**
156         * Controls almost all log output.
157         */
158        protected boolean verbose;
159    
160        /**
161         * Mirrors the static value of the reflection flag in MetaClass.
162         * See {@link AbstractHttpServlet#logGROOVY861}
163         */
164        protected boolean reflection;
165    
166        /**
167         * Debug flag logging the class the class loader of the request.
168         */
169        private boolean logGROOVY861;
170    
171        /**
172         * Initializes all fields with default values.
173         */
174        public AbstractHttpServlet() {
175            this.servletContext = null;
176            this.resourceNameMatcher = null;
177            this.resourceNameReplacement = null;
178            this.resourceNameReplaceAll = true;
179            this.verbose = false;
180            this.reflection = false;
181            this.logGROOVY861 = false;
182        }
183    
184        /**
185         * Interface method for ResourceContainer. This is used by the GroovyScriptEngine.
186         */
187        public URLConnection getResourceConnection(String name) throws ResourceException {
188            /*
189             * First, mangle resource name with the compiled pattern.
190             */
191            Matcher matcher = resourceNameMatcher;
192            if (matcher != null) {
193                matcher.reset(name);
194                String replaced;
195                if (resourceNameReplaceAll) {
196                    replaced = resourceNameMatcher.replaceAll(resourceNameReplacement);
197                } else {
198                    replaced = resourceNameMatcher.replaceFirst(resourceNameReplacement);
199                }
200                if (!name.equals(replaced)) {
201                    if (verbose) {
202                        log("Replaced resource name \"" + name + "\" with \"" + replaced + "\".");
203                    }
204                    name = replaced;
205                }
206            }
207    
208            /*
209             * Try to locate the resource and return an opened connection to it.
210             */
211            try {
212                URL url = servletContext.getResource("/" + name);
213                if (url == null) {
214                    url = servletContext.getResource("/WEB-INF/groovy/" + name);
215                }
216                if (url == null) {
217                    throw new ResourceException("Resource \"" + name + "\" not found!");
218                }
219                return url.openConnection();
220            } catch (IOException e) {
221                throw new ResourceException("Problems getting resource named \"" + name + "\"!", e);
222            }
223        }
224    
225        /**
226         * Returns the include-aware uri of the script or template file.
227         * 
228         * @param request
229         *  the http request to analyze
230         * @return the include-aware uri either parsed from request attributes or
231         *  hints provided by the servlet container
232         */
233        protected String getScriptUri(HttpServletRequest request) {
234            /*
235             * Log some debug information for http://jira.codehaus.org/browse/GROOVY-861
236             */
237            if (logGROOVY861) {
238                log("Logging request class and its class loader:");
239                log(" c = request.getClass() :\"" + request.getClass() + "\"");
240                log(" l = c.getClassLoader() :\"" + request.getClass().getClassLoader() + "\"");
241                log(" l.getClass()           :\"" + request.getClass().getClassLoader().getClass() + "\"");
242                /*
243                 * Keep logging, if we're verbose. Else turn it off.
244                 */
245                logGROOVY861 = verbose;
246            }
247    
248            //
249            // NOTE: This piece of code is heavily inspired by Apaches Jasper2!
250            // 
251            // http://cvs.apache.org/viewcvs.cgi/jakarta-tomcat-jasper/jasper2/ \
252            //        src/share/org/apache/jasper/servlet/JspServlet.java?view=markup
253            //
254            // Why doesn't it use request.getRequestURI() or INC_REQUEST_URI?
255            //
256    
257            String uri = null;
258            String info = null;
259    
260            //
261            // Check to see if the requested script/template source file has been the
262            // target of a RequestDispatcher.include().
263            //
264            uri = (String) request.getAttribute(INC_SERVLET_PATH);
265            if (uri != null) {
266                //
267                // Requested script/template file has been target of 
268                // RequestDispatcher.include(). Its path is assembled from the relevant
269                // javax.servlet.include.* request attributes and returned!
270                //
271                info = (String) request.getAttribute(INC_PATH_INFO);
272                if (info != null) {
273                    uri += info;
274                }
275                return uri;
276            }
277    
278            //
279            // Requested script/template file has not been the target of a 
280            // RequestDispatcher.include(). Reconstruct its path from the request's
281            // getServletPath() and getPathInfo() results.
282            //
283            uri = request.getServletPath();
284            info = request.getPathInfo();
285            if (info != null) {
286                uri += info;
287            }
288    
289            /*
290             * TODO : Enable auto ".groovy" extension replacing here!
291             * http://cvs.groovy.codehaus.org/viewrep/groovy/groovy/groovy-core/src/main/groovy/servlet/GroovyServlet.java?r=1.10#l259 
292             */
293    
294            return uri;
295        }
296    
297        /**
298         * Parses the http request for the real script or template source file.
299         * @param request the http request to analyze
300         * @return a file object using an absolute file path name
301         */
302        protected File getScriptUriAsFile(HttpServletRequest request) {
303            String uri = getScriptUri(request);
304            String real = servletContext.getRealPath(uri);
305            File file = new File(real).getAbsoluteFile();
306            return file;
307        }
308    
309        /**
310         * Overrides the generic init method to set some debug flags.
311         * 
312         * @param config
313         *  the servlet coniguration provided by the container
314         * @throws ServletException if init() method defined in super class 
315         *  javax.servlet.GenericServlet throws it
316         */
317        public void init(ServletConfig config) throws ServletException {
318            /*
319             * Never forget super.init()!
320             */
321            super.init(config);
322    
323            /*
324             * Grab the servlet context.
325             */
326            this.servletContext = config.getServletContext();
327    
328            /*
329             * Get verbosity hint.
330             */
331            String value = config.getInitParameter("verbose");
332            if (value != null) {
333                this.verbose = Boolean.valueOf(value).booleanValue();
334            }
335    
336            /*
337             * And now the real init work...
338             */
339            if (verbose) {
340                log("Parsing init parameters...");
341            }
342    
343            String regex = config.getInitParameter("resource.name.regex");
344            if (regex != null) {
345                String replacement = config.getInitParameter("resource.name.replacement");
346                if (replacement == null) {
347                    Exception npex = new NullPointerException("resource.name.replacement");
348                    String message = "Init-param 'resource.name.replacement' not specified!";
349                    log(message, npex);
350                    throw new ServletException(message, npex);
351                }
352                int flags = 0; // TODO : Parse pattern compile flags.
353                this.resourceNameMatcher = Pattern.compile(regex, flags).matcher("");
354                this.resourceNameReplacement = replacement;
355                String all = config.getInitParameter("resource.name.replace.all");
356                if (all != null) {
357                    this.resourceNameReplaceAll = Boolean.valueOf(all).booleanValue();
358                }
359            }
360    
361            value = config.getInitParameter("reflection");
362            if (value != null) {
363                this.reflection = Boolean.valueOf(value).booleanValue();
364                MetaClass.setUseReflection(reflection);
365            }
366    
367            value = config.getInitParameter("logGROOVY861");
368            if (value != null) {
369                this.logGROOVY861 = Boolean.valueOf(value).booleanValue();
370                // nothing else to do here
371            }
372    
373            /*
374             * If verbose, log the parameter values.
375             */
376            if (verbose) {
377                log("(Abstract) init done. Listing some parameter name/value pairs:");
378                log("verbose = " + verbose); // this *is* verbose! ;)
379                log("reflection = " + reflection);
380                log("logGROOVY861 = " + logGROOVY861);
381                if (resourceNameMatcher != null) {
382                    log("resource.name.regex = " + resourceNameMatcher.pattern().pattern());
383                }
384                else {
385                    log("resource.name.regex = null");
386                }
387                log("resource.name.replacement = " + resourceNameReplacement);
388            }
389        }
390    }