PluginServlet.java 18.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13
/**
 * $RCSfile$
 * $Revision$
 * $Date$
 *
 * Copyright (C) 2004 Jive Software. All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution.
 */

package org.jivesoftware.messenger.container;

14 15
import org.apache.jasper.JasperException;
import org.apache.jasper.JspC;
16 17 18
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
19
import org.jivesoftware.util.JiveGlobals;
20
import org.jivesoftware.util.Log;
21
import org.jivesoftware.util.StringUtils;
22

23 24 25
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
26 27 28
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
29 30 31 32 33 34

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
35
import java.util.HashMap;
36 37
import java.util.List;
import java.util.Map;
38 39 40
import java.util.concurrent.ConcurrentHashMap;

/**
41 42 43 44
 * The plugin servlet acts as a proxy for web requests (in the admin console)
 * to plugins. Since plugins can be dynamically loaded and live in a different place
 * than normal Jive Messenger admin console files, it's not possible to have them
 * added to the normal Jive Messenger admin console web app directory.<p>
45
 *
46 47 48 49
 * The servlet listens for requests in the form <tt>/plugins/[pluginName]/[JSP File]</tt>
 * (e.g. <tt>/plugins/foo/example.jsp</tt>). It also listens for image requests in the
 * the form <tt>/plugins/[pluginName]/images/*.png|gif</tt> (e.g.
 * <tt>/plugins/foo/images/example.gif</tt>).<p>
50
 *
51 52 53
 * JSP files must be compiled and available via the plugin's class loader. The mapping
 * between JSP name and servlet class files is defined in [pluginName]/web/web.xml.
 * Typically, this file is auto-generated by the JSP compiler when packaging the plugin.
54 55
 * Alternatively, if development mode is enabled for the plugin then the the JSP file
 * will be dynamically compiled using JSPC.
56 57 58 59 60
 *
 * @author Matt Tucker
 */
public class PluginServlet extends HttpServlet {

61
    private static Map<String, HttpServlet> servlets;
62
    private static File pluginDirectory;
63
    private static PluginManager pluginManager;
64
    private static ServletConfig servletConfig;
65

66
    static {
67 68
        servlets = new ConcurrentHashMap<String, HttpServlet>();
        pluginDirectory = new File(JiveGlobals.getHomeDirectory(), "plugins");
69 70
    }

71 72
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
73
        servletConfig = config;
74 75
    }

76
    public void service(HttpServletRequest request, HttpServletResponse response)
77 78
            throws ServletException, IOException
    {
79 80 81 82 83 84
        String pathInfo = request.getPathInfo();
        if (pathInfo == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        else {
Matt Tucker's avatar
Matt Tucker committed
85 86 87
            try {
                // Handle JSP requests.
                if (pathInfo.endsWith(".jsp")) {
88
                    if (handleDevJSP(pathInfo, request, response)) {
Derek DeMoro's avatar
Derek DeMoro committed
89 90
                        return;
                    }
91
                    handleJSP(pathInfo, request, response);
Matt Tucker's avatar
Matt Tucker committed
92 93 94 95
                    return;
                }
                // Handle image requests.
                else if (pathInfo.endsWith(".gif") || pathInfo.endsWith(".png")) {
96
                    handleImage(pathInfo, response);
Matt Tucker's avatar
Matt Tucker committed
97 98
                    return;
                }
Derek DeMoro's avatar
Derek DeMoro committed
99
                // Handle servlet requests.
100
                else if (getServlet(pathInfo) != null) {
101
                    handleServlet(pathInfo, request, response);
Derek DeMoro's avatar
Derek DeMoro committed
102
                }
Matt Tucker's avatar
Matt Tucker committed
103 104 105 106 107
                // Anything else results in a 404.
                else {
                    response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                    return;
                }
108
            }
Matt Tucker's avatar
Matt Tucker committed
109 110 111
            catch (Exception e) {
                Log.error(e);
                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
112 113 114 115 116
                return;
            }
        }
    }

Matt Tucker's avatar
Matt Tucker committed
117 118 119
    /**
     * Registers all JSP page servlets for a plugin.
     *
120
     * @param manager the plugin manager.
121 122 123
     * @param plugin  the plugin.
     * @param webXML  the web.xml file containing JSP page names to servlet class file
     *                mappings.
Matt Tucker's avatar
Matt Tucker committed
124
     */
125
    public static void registerServlets(PluginManager manager, Plugin plugin, File webXML) {
126
        pluginManager = manager;
127 128
        if (!webXML.exists()) {
            Log.error("Could not register plugin servlets, file " + webXML.getAbsolutePath() +
129
                    " does not exist.");
130 131 132 133
            return;
        }
        // Find the name of the plugin directory given that the webXML file
        // lives in plugins/[pluginName]/web/web.xml
134
        String pluginName = webXML.getParentFile().getParentFile().getParentFile().getName();
135
        try {
136 137 138
            // Make the reader non-validating so that it doesn't try to resolve external
            // DTD's. Trying to resolve external DTD's can break on some firewall configurations.
            SAXReader saxReader = new SAXReader(false);
139 140
            saxReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd",
                    false);
141 142 143
            Document doc = saxReader.read(webXML);
            // Find all <servlet> entries to discover name to class mapping.
            List classes = doc.selectNodes("//servlet");
144 145 146
            Map<String, Class> classMap = new HashMap<String, Class>();
            for (int i = 0; i < classes.size(); i++) {
                Element servletElement = (Element)classes.get(i);
147 148
                String name = servletElement.element("servlet-name").getTextTrim();
                String className = servletElement.element("servlet-class").getTextTrim();
149
                classMap.put(name, manager.loadClass(plugin, className));
150 151 152
            }
            // Find all <servelt-mapping> entries to discover name to URL mapping.
            List names = doc.selectNodes("//servlet-mapping");
153 154
            for (int i = 0; i < names.size(); i++) {
                Element nameElement = (Element)names.get(i);
155 156 157 158 159 160
                String name = nameElement.element("servlet-name").getTextTrim();
                String url = nameElement.element("url-pattern").getTextTrim();
                // Register the servlet for the URL.
                Class servletClass = classMap.get(name);
                Object instance = servletClass.newInstance();
                if (instance instanceof HttpServlet) {
161
                    // Initialize the servlet then add it to the map..
162 163
                    ((HttpServlet)instance).init(servletConfig);
                    servlets.put(pluginName + url, (HttpServlet)instance);
164
                }
165 166 167
                else {
                    Log.warn("Could not load " + (pluginName + url) + ": not a servlet.");
                }
168 169 170
            }
        }
        catch (Throwable e) {
Matt Tucker's avatar
Matt Tucker committed
171
            Log.error(e);
172 173 174
        }
    }

175 176 177 178
    /**
     * Unregisters all JSP page servlets for a plugin.
     *
     * @param webXML the web.xml file containing JSP page names to servlet class file
179
     *               mappings.
180 181 182 183
     */
    public static void unregisterServlets(File webXML) {
        if (!webXML.exists()) {
            Log.error("Could not unregister plugin servlets, file " + webXML.getAbsolutePath() +
184
                    " does not exist.");
185 186 187 188
            return;
        }
        // Find the name of the plugin directory given that the webXML file
        // lives in plugins/[pluginName]/web/web.xml
Matt Tucker's avatar
Matt Tucker committed
189
        String pluginName = webXML.getParentFile().getParentFile().getParentFile().getName();
190
        try {
191
            SAXReader saxReader = new SAXReader(false);
192 193
            saxReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd",
                    false);
194 195 196
            Document doc = saxReader.read(webXML);
            // Find all <servelt-mapping> entries to discover name to URL mapping.
            List names = doc.selectNodes("//servlet-mapping");
197 198
            for (int i = 0; i < names.size(); i++) {
                Element nameElement = (Element)names.get(i);
199 200 201 202 203 204 205 206 207 208 209 210 211
                String url = nameElement.element("url-pattern").getTextTrim();
                // Destroy the servlet than remove from servlets map.
                HttpServlet servlet = servlets.get(pluginName + url);
                servlet.destroy();
                servlets.remove(pluginName + url);
                servlet = null;
            }
        }
        catch (Throwable e) {
            Log.error(e);
        }
    }

212 213 214 215 216 217
    /**
     * Handles a request for a JSP page. It checks to see if a servlet is mapped
     * for the JSP URL. If one is found, request handling is passed to it. If no
     * servlet is found, a 404 error is returned.
     *
     * @param pathInfo the extra path info.
218
     * @param request the request object.
219
     * @param response the response object.
220 221
     * @throws ServletException if a servlet exception occurs while handling the request.
     * @throws IOException if an IOException occurs while handling the request.
222
     */
223
    private void handleJSP(String pathInfo, HttpServletRequest request,
224 225
            HttpServletResponse response) throws ServletException, IOException
    {
226 227
        // Strip the starting "/" from the path to find the JSP URL.
        String jspURL = pathInfo.substring(1);
228 229


230 231
        HttpServlet servlet = servlets.get(jspURL);
        if (servlet != null) {
232
            servlet.service(request, response);
233 234 235 236 237 238 239 240
            return;
        }
        else {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
    }

Derek DeMoro's avatar
Derek DeMoro committed
241
    /**
Matt Tucker's avatar
Matt Tucker committed
242 243
     * Handles a request for a Servlet. If one is found, request handling is passed to it.
     * If no servlet is found, a 404 error is returned.
Derek DeMoro's avatar
Derek DeMoro committed
244 245
     *
     * @param pathInfo the extra path info.
246
     * @param request  the request object.
Derek DeMoro's avatar
Derek DeMoro committed
247
     * @param response the response object.
Matt Tucker's avatar
Matt Tucker committed
248
     * @throws ServletException if a servlet exception occurs while handling the request.
249
     * @throws IOException      if an IOException occurs while handling the request.
Derek DeMoro's avatar
Derek DeMoro committed
250
     */
251 252
    private void handleServlet(String pathInfo, HttpServletRequest request,
                               HttpServletResponse response) throws ServletException, IOException {
Derek DeMoro's avatar
Derek DeMoro committed
253
        // Strip the starting "/" from the path to find the JSP URL.
254
        HttpServlet servlet = getServlet(pathInfo);
Derek DeMoro's avatar
Derek DeMoro committed
255
        if (servlet != null) {
256
            servlet.service(request, response);
Derek DeMoro's avatar
Derek DeMoro committed
257 258 259 260 261 262 263 264
            return;
        }
        else {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
    }

265 266 267 268 269 270 271 272
    /**
     * Returns the correct servlet with mapping checks.
     *
     * @param pathInfo the pathinfo to map to the servlet.
     * @return the mapped servlet, or null if no servlet was found.
     */
    private HttpServlet getServlet(String pathInfo) {
        pathInfo = pathInfo.substring(1).toLowerCase();
273

274 275 276 277 278 279
        HttpServlet servlet = servlets.get(pathInfo);
        if (servlet == null) {
            for (String key : servlets.keySet()) {
                int index = key.indexOf("/*");
                String searchkey = key;
                if (index != -1) {
280
                    searchkey = key.substring(0, index);
281 282 283 284 285 286 287 288 289 290
                }
                if (searchkey.startsWith(pathInfo) || pathInfo.startsWith(searchkey)) {
                    servlet = servlets.get(key);
                    break;
                }
            }
        }
        return servlet;
    }

291 292 293 294 295 296 297
    /**
     * Handles a request for an image.
     *
     * @param pathInfo the extra path info.
     * @param response the response object.
     * @throws IOException if an IOException occurs while handling the request.
     */
298
    private void handleImage(String pathInfo, HttpServletResponse response) throws IOException {
299
        String[] parts = pathInfo.split("/");
Matt Tucker's avatar
Matt Tucker committed
300 301 302 303 304
        // Image request must be in correct format.
        if (parts.length != 4) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
305 306
        File image = new File(pluginDirectory, parts[1] + File.separator + "web" +
                File.separator + "images" + File.separator + parts[3]);
307 308 309 310 311 312 313 314 315 316
        if (!image.exists()) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        else {
            // Content type will be GIF or PNG.
            String contentType = "image/gif";
            if (pathInfo.endsWith(".png")) {
                contentType = "image/png";
            }
317
            response.setHeader("Content-disposition", "filename=\"" + image + "\";");
318 319 320 321 322 323 324 325 326
            response.setContentType(contentType);
            // Write out the image to the user.
            InputStream in = null;
            ServletOutputStream out = null;
            try {
                in = new BufferedInputStream(new FileInputStream(image));
                out = response.getOutputStream();

                // Set the size of the file.
327
                response.setContentLength((int)image.length());
328

329 330 331
                // Use a 1K buffer.
                byte[] buf = new byte[1024];
                int len;
332
                while ((len = in.read(buf)) != -1) {
333
                    out.write(buf, 0, len);
334 335 336
                }
            }
            finally {
337 338 339 340 341 342 343 344 345 346
                try {
                    in.close();
                }
                catch (Exception ignored) {
                }
                try {
                    out.close();
                }
                catch (Exception ignored) {
                }
347 348 349
            }
        }
    }
350

351
    /**
352 353 354 355
     * Handles a request for a JSP page in development mode. If development mode is
     * not enabled, this method returns false so that normal JSP handling can be performed.
     * If development mode is enabled, this method tries to locate the JSP, compile
     * it using JSPC, and then return the output.
356 357 358 359
     *
     * @param pathInfo the extra path info.
     * @param request  the request object.
     * @param response the response object.
360
     * @return true if this page request was handled; false if the request was not handled.
361
     */
362 363 364
    private boolean handleDevJSP(String pathInfo, HttpServletRequest request,
            HttpServletResponse response)
    {
365 366 367 368 369
        String jspURL = pathInfo.substring(1);

        // Handle pre-existing pages and fail over to pre-compiled pages.
        int fileSeperator = jspURL.indexOf("/");
        if (fileSeperator != -1) {
370
            String pluginName = jspURL.substring(0, fileSeperator);
371
            Plugin plugin = pluginManager.getPlugin(pluginName);
372 373

            PluginDevEnvironment environment = pluginManager.getDevEnvironment(plugin);
374 375 376 377
            // If development mode not turned on for plugin, return false.
            if (environment == null) {
                return false;
            }
378
            File webDir = environment.getWebRoot();
379
            if (webDir == null || !webDir.exists()) {
380 381 382
                return false;
            }

383 384
            File pluginDirectory = pluginManager.getPluginDirectory(plugin);

385
            File compilationDir = new File(pluginDirectory, "classes");
Derek DeMoro's avatar
Derek DeMoro committed
386
            compilationDir.mkdirs();
387 388 389

            String jsp = jspURL.substring(fileSeperator + 1);

Derek DeMoro's avatar
Derek DeMoro committed
390 391
            int indexOfLastSlash = jsp.lastIndexOf("/");
            String relativeDir = "";
392 393
            if (indexOfLastSlash != -1) {
                relativeDir = jsp.substring(0, indexOfLastSlash);
Derek DeMoro's avatar
Derek DeMoro committed
394 395 396
                relativeDir = relativeDir.replaceAll("//", ".") + '.';
            }

397
            File jspFile = new File(webDir, jsp);
398 399 400
            String filename = jspFile.getName();
            int indexOfPeriod = filename.indexOf(".");
            if (indexOfPeriod != -1) {
401
                filename = "dev" + StringUtils.randomString(4);
402 403 404 405 406 407
            }

            JspC jspc = new JspC();
            if (!jspFile.exists()) {
                return false;
            }
408 409 410 411
            try {
                jspc.setJspFiles(jspFile.getCanonicalPath());
            }
            catch (IOException e) {
412
                Log.error(e);
413
            }
Derek DeMoro's avatar
Derek DeMoro committed
414
            jspc.setOutputDir(compilationDir.getAbsolutePath());
415 416 417 418 419 420 421 422
            jspc.setClassName(filename);
            jspc.setCompile(true);

            jspc.setClassPath(getClasspathForPlugin(plugin));
            try {
                jspc.execute();

                try {
423 424
                    Object servletInstance = pluginManager.loadClass(plugin, "org.apache.jsp." +
                            relativeDir + filename).newInstance();
425
                    HttpServlet servlet = (HttpServlet)servletInstance;
426
                    servlet.init(servletConfig);
427
                    servlet.service(request, response);
428 429 430
                    return true;
                }
                catch (Exception e) {
431
                    Log.error(e);
432 433 434 435
                }

            }
            catch (JasperException e) {
436
                Log.error(e);
437 438 439 440 441 442 443 444 445 446 447
            }
        }
        return false;
    }

    /**
     * Returns the classpath to use for the JSPC Compiler.
     *
     * @param plugin the plugin the jspc will handle.
     * @return the classpath needed to compile a single jsp in a plugin.
     */
Derek DeMoro's avatar
Derek DeMoro committed
448
    private static String getClasspathForPlugin(Plugin plugin) {
449 450 451 452
        StringBuilder builder = new StringBuilder();

        File pluginDirectory = pluginManager.getPluginDirectory(plugin);

453 454
        PluginDevEnvironment env = pluginManager.getDevEnvironment(plugin);

455
        // Load all jars from lib
456
        File libDirectory = new File(pluginDirectory, "lib");
457
        File[] libs = libDirectory.listFiles();
458
        for (int i = 0; i < libs.length; i++) {
459
            File libFile = libs[i];
Derek DeMoro's avatar
Derek DeMoro committed
460
            builder.append(libFile.getAbsolutePath() + ';');
461 462 463
        }

        File messengerRoot = pluginDirectory.getParentFile().getParentFile().getParentFile();
464
        File messengerLib = new File(messengerRoot, "target//lib");
465 466 467 468 469

        builder.append(messengerLib.getAbsolutePath() + "//servlet.jar;");
        builder.append(messengerLib.getAbsolutePath() + "//messenger.jar;");
        builder.append(messengerLib.getAbsolutePath() + "//jasper-compiler.jar;");
        builder.append(messengerLib.getAbsolutePath() + "//jasper-runtime.jar;");
470
        builder.append(env.getClassesDir().getAbsolutePath() + ";");
471 472 473

        return builder.toString();
    }
474
}