001: /*
002: * JBoss, Home of Professional Open Source.
003: * Copyright 2006, Red Hat Middleware LLC, and individual contributors
004: * as indicated by the @author tags. See the copyright.txt file in the
005: * distribution for a full listing of individual contributors.
006: *
007: * This is free software; you can redistribute it and/or modify it
008: * under the terms of the GNU Lesser General Public License as
009: * published by the Free Software Foundation; either version 2.1 of
010: * the License, or (at your option) any later version.
011: *
012: * This software is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this software; if not, write to the Free
019: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021: */
022: package org.jboss.web;
023:
024: import java.io.BufferedInputStream;
025: import java.io.BufferedReader;
026: import java.io.ByteArrayOutputStream;
027: import java.io.DataOutputStream;
028: import java.io.IOException;
029: import java.io.InputStream;
030: import java.io.InputStreamReader;
031: import java.net.InetAddress;
032: import java.net.MalformedURLException;
033: import java.net.ServerSocket;
034: import java.net.Socket;
035: import java.net.URL;
036: import java.util.Properties;
037:
038: import org.jboss.logging.Logger;
039: import org.jboss.util.threadpool.BasicThreadPool;
040: import org.jboss.util.threadpool.BasicThreadPoolMBean;
041:
042: import EDU.oswego.cs.dl.util.concurrent.ConcurrentReaderHashMap;
043:
044: /**
045: * A mini webserver that should be embedded in another application. It can
046: * server any file that is available from classloaders that are registered with
047: * it, including class-files.
048: *
049: * Its primary purpose is to simplify dynamic class-loading in RMI. Create an
050: * instance of it, register a classloader with your classes, start it, and
051: * you'll be able to let RMI-clients dynamically download classes from it.
052: *
053: * It is configured by calling any methods programmatically prior to startup.
054: * @author <a href="mailto:marc@jboss.org">Marc Fleury</a>
055: * @author <a href="mailto:Scott.Stark@org.jboss">Scott Stark</a>
056: * @authro <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
057: * @version $Revision: 62433 $
058: * @see WebClassLoader
059: */
060: public class WebServer implements Runnable {
061: // Constants -----------------------------------------------------
062:
063: // Attributes ----------------------------------------------------
064: private static Logger log = Logger.getLogger(WebServer.class);
065:
066: /**
067: * The port the web server listens on
068: */
069: private int port = 8083;
070:
071: /**
072: * The interface to bind to. This is useful for multi-homed hosts that want
073: * control over which interfaces accept connections.
074: */
075: private InetAddress bindAddress;
076:
077: /**
078: * The serverSocket listen queue depth
079: */
080: private int backlog = 50;
081:
082: /**
083: * The map of class loaders registered with the web server
084: */
085: private final ConcurrentReaderHashMap loaderMap = new ConcurrentReaderHashMap();
086:
087: /**
088: * The web server http listening socket
089: */
090: private ServerSocket server = null;
091:
092: /**
093: * A flag indicating if the server should attempt to download classes from
094: * thread context class loader when a request arrives that does not have a
095: * class loader key prefix.
096: */
097: private boolean downloadServerClasses = true;
098:
099: /**
100: * A flag indicating if the server should attempt to download resources,
101: * i.e. resource paths that don't end in .class
102: */
103: private boolean downloadResources = false;
104:
105: /**
106: * The class wide mapping of type suffixes(class, txt) to their mime type
107: * string used as the Content-Type header for the vended classes/resources
108: */
109: private static final Properties mimeTypes = new Properties();
110:
111: /**
112: * The thread pool used to manage listening threads
113: */
114: private BasicThreadPoolMBean threadPool;
115:
116: // Public --------------------------------------------------------
117:
118: /**
119: * Set the http listening port
120: */
121: public void setPort(int port) {
122: this .port = port;
123: }
124:
125: /**
126: * Get the http listening port
127: * @return the http listening port
128: */
129: public int getPort() {
130: return port;
131: }
132:
133: /**
134: * Set the http server bind address
135: * @param bindAddress
136: */
137: public void setBindAddress(InetAddress bindAddress) {
138: this .bindAddress = bindAddress;
139:
140: }
141:
142: /**
143: * Get the address the http server binds to
144: * @return the http bind address
145: */
146: public InetAddress getBindAddress() {
147: return bindAddress;
148: }
149:
150: /**
151: * Get the server sockets listen queue depth
152: * @return the listen queue depth
153: */
154: public int getBacklog() {
155: return backlog;
156: }
157:
158: /**
159: * Set the server sockets listen queue depth
160: */
161: public void setBacklog(int backlog) {
162: if (backlog <= 0)
163: backlog = 50;
164:
165: this .backlog = backlog;
166: }
167:
168: public boolean getDownloadServerClasses() {
169: return downloadServerClasses;
170: }
171:
172: public void setDownloadServerClasses(boolean flag) {
173: downloadServerClasses = flag;
174: }
175:
176: public boolean getDownloadResources() {
177: return downloadResources;
178: }
179:
180: public void setDownloadResources(boolean flag) {
181: downloadResources = flag;
182: }
183:
184: public BasicThreadPoolMBean getThreadPool() {
185: return threadPool;
186: }
187:
188: public void setThreadPool(BasicThreadPoolMBean threadPool) {
189: this .threadPool = threadPool;
190: }
191:
192: /**
193: * Augment the type suffix to mime type mappings
194: * @param extension - the type extension without a period(class, txt)
195: * @param type - the mime type string
196: */
197: public void addMimeType(String extension, String type) {
198: mimeTypes.put(extension, type);
199: }
200:
201: /**
202: * Start the web server on port and begin listening for requests.
203: */
204: public void start() throws Exception {
205: if (threadPool == null)
206: threadPool = new BasicThreadPool("ClassLoadingPool");
207:
208: try {
209: server = new ServerSocket(port, backlog, bindAddress);
210: log.debug("Started server: " + server);
211:
212: listen();
213: } catch (java.net.BindException be) {
214: throw new Exception("Port " + port + " already in use.", be);
215: } catch (IOException e) {
216: throw e;
217: }
218: }
219:
220: /**
221: * Close the web server listening socket
222: */
223: public void stop() {
224: try {
225: ServerSocket srv = server;
226: server = null;
227: srv.close();
228: } catch (Exception ignore) {
229: }
230: }
231:
232: /**
233: * Add a class loader to the web server map and return the URL that should be
234: * used as the annotated codebase for classes that are to be available via
235: * RMI dynamic classloading. The codebase URL is formed by taking the
236: * java.rmi.server.codebase system property and adding a subpath unique for
237: * the class loader instance.
238: * @param cl - the ClassLoader instance to begin serving download requests
239: * for
240: * @return the annotated codebase to use if java.rmi.server.codebase is set,
241: * null otherwise.
242: * @see #getClassLoaderKey(ClassLoader)
243: */
244: public URL addClassLoader(ClassLoader cl) {
245: String key = (cl instanceof WebClassLoader) ? ((WebClassLoader) cl)
246: .getKey()
247: : getClassLoaderKey(cl);
248: loaderMap.put(key, cl);
249: URL loaderURL = null;
250: String codebase = System
251: .getProperty("java.rmi.server.codebase");
252: if (codebase != null) {
253: if (codebase.endsWith("/") == false)
254: codebase += '/';
255: codebase += key;
256: codebase += '/';
257: try {
258: loaderURL = new URL(codebase);
259: } catch (MalformedURLException e) {
260: log.error("invalid url", e);
261: }
262: }
263: log.trace("Added ClassLoader: " + cl + " URL: " + loaderURL);
264: return loaderURL;
265: }
266:
267: /**
268: * Remove a class loader previously added via addClassLoader
269: * @param cl - the ClassLoader previously added via addClassLoader
270: */
271: public void removeClassLoader(ClassLoader cl) {
272: String key = getClassLoaderKey(cl);
273: loaderMap.remove(key);
274: }
275:
276: // Runnable implementation ---------------------------------------
277: /**
278: * Listen threads entry point. Here we accept a client connection and located
279: * requested classes/resources using the class loader specified in the http
280: * request.
281: */
282: public void run() {
283: // Return if the server has been stopped
284: if (server == null)
285: return;
286:
287: // Accept a connection
288: Socket socket = null;
289: try {
290: socket = server.accept();
291: } catch (IOException e) {
292: // If the server is not null meaning we were not stopped report the err
293: if (server != null)
294: log.error("Failed to accept connection", e);
295: return;
296: }
297:
298: // Create a new thread to accept the next connection
299: listen();
300:
301: try {
302: // Get the request socket output stream
303: DataOutputStream out = new DataOutputStream(socket
304: .getOutputStream());
305: try {
306: String httpCode = "200 OK";
307: // Get the requested item from the HTTP header
308: BufferedReader in = new BufferedReader(
309: new InputStreamReader(socket.getInputStream()));
310: String rawPath = getPath(in);
311:
312: // Parse the path into the class loader key and file path.
313: //
314: // The class loader key is a string whose format is
315: // "ClassName[oid]", where the oid substring may contain '/'
316: // chars. The expected form of the raw path is:
317: //
318: // "SomeClassName[some/object/id]/some/file/path"
319: //
320: // The class loader key is "SomeClassName[some/object/id]"
321: // and the file path is "some/file/path"
322:
323: int endOfKey = rawPath.indexOf(']');
324: String filePath = rawPath.substring(endOfKey + 2);
325: String loaderKey = rawPath.substring(0, endOfKey + 1);
326: log.trace("loaderKey = " + loaderKey);
327: log.trace("filePath = " + filePath);
328: ClassLoader loader = (ClassLoader) loaderMap
329: .get(loaderKey);
330: /* If we did not find a class loader check to see if the raw path
331: begins with className + '[' + class loader key + ']' by looking for
332: an '[' char. If it does not and downloadServerClasses is true use
333: the thread context class loader and set filePath to the rawPath
334: */
335: if (loader == null && rawPath.indexOf('[') < 0
336: && downloadServerClasses) {
337: filePath = rawPath;
338: log
339: .trace("No loader, reset filePath = "
340: + filePath);
341: loader = Thread.currentThread()
342: .getContextClassLoader();
343: }
344: log.trace("loader = " + loader);
345: byte[] bytes = {};
346: if (loader != null && filePath.endsWith(".class")) {
347: // A request for a class file
348: String className = filePath.substring(0,
349: filePath.length() - 6).replace('/', '.');
350: log.trace("loading className = " + className);
351: Class clazz = loader.loadClass(className);
352: URL clazzUrl = clazz.getProtectionDomain()
353: .getCodeSource().getLocation();
354: log.trace("clazzUrl = " + clazzUrl);
355: if (clazzUrl == null) {
356: // Does the WebClassLoader of clazz
357: // have the ability to obtain the bytecodes of clazz?
358: bytes = ((WebClassLoader) clazz
359: .getClassLoader()).getBytes(clazz);
360: if (bytes == null)
361: throw new Exception("Class not found: "
362: + className);
363: } else {
364: if (clazzUrl.getFile().endsWith("/") == false) {
365: clazzUrl = new URL("jar:" + clazzUrl + "!/"
366: + filePath);
367: }
368: // this is a hack for the AOP ClassProxyFactory
369: else if (clazzUrl.getFile().indexOf(
370: "/org_jboss_aop_proxy$") < 0) {
371: clazzUrl = new URL(clazzUrl, filePath);
372: }
373:
374: // Retrieve bytecodes
375: log.trace("new clazzUrl: " + clazzUrl);
376: bytes = getBytes(clazzUrl);
377: }
378: } else if (loader != null && filePath.length() > 0
379: && downloadServerClasses && downloadResources) {
380: // Try getting resource
381: log.trace("loading resource = " + filePath);
382: URL resourceURL = loader.getResource(filePath);
383: if (resourceURL == null)
384: httpCode = "404 Resource not found:" + filePath;
385: else {
386: // Retrieve bytes
387: log.trace("resourceURL = " + resourceURL);
388: bytes = getBytes(resourceURL);
389: }
390: } else {
391: httpCode = "404 Not Found";
392: }
393:
394: // Send bytecodes/resource data in response (assumes HTTP/1.0 or later)
395: try {
396: log.trace("HTTP code=" + httpCode
397: + ", Content-Length: " + bytes.length);
398: // The HTTP 1.0 header
399: out.writeBytes("HTTP/1.0 " + httpCode + "\r\n");
400: out.writeBytes("Content-Length: " + bytes.length
401: + "\r\n");
402: out.writeBytes("Content-Type: "
403: + getMimeType(filePath));
404: out.writeBytes("\r\n\r\n");
405: // The response body
406: out.write(bytes);
407: out.flush();
408: } catch (IOException ie) {
409: return;
410: }
411: } catch (Throwable e) {
412: try {
413: log.trace("HTTP code=404 " + e.getMessage());
414: // Write out error response
415: out.writeBytes("HTTP/1.0 400 " + e.getMessage()
416: + "\r\n");
417: out.writeBytes("Content-Type: text/html\r\n\r\n");
418: out.flush();
419: } catch (IOException ex) {
420: // Ignore
421: }
422: }
423: } catch (IOException ex) {
424: log.error("error writting response", ex);
425: } finally {
426: // Close the client request socket
427: try {
428: socket.close();
429: } catch (IOException e) {
430: }
431: }
432: }
433:
434: // Protected -----------------------------------------------------
435: /**
436: * Create the string key used as the key into the loaderMap.
437: * @return The class loader instance key.
438: */
439: protected String getClassLoaderKey(ClassLoader cl) {
440: String className = cl.getClass().getName();
441: int dot = className.lastIndexOf('.');
442: if (dot >= 0)
443: className = className.substring(dot + 1);
444: String key = className + '[' + cl.hashCode() + ']';
445: return key;
446: }
447:
448: protected void listen() {
449: threadPool.getInstance().run(this );
450: }
451:
452: /**
453: * @return the path portion of the HTTP request header.
454: */
455: protected String getPath(BufferedReader in) throws IOException {
456: String line = in.readLine();
457: log.trace("raw request=" + line);
458: // Find the request path by parsing the 'REQUEST_TYPE filePath HTTP_VERSION' string
459: int start = line.indexOf(' ') + 1;
460: int end = line.indexOf(' ', start + 1);
461: // The file minus the leading '/'
462: String filePath = line.substring(start + 1, end);
463: return filePath;
464: }
465:
466: /**
467: * Read the local class/resource contents into a byte array.
468: */
469: protected byte[] getBytes(URL url) throws IOException {
470: InputStream in = new BufferedInputStream(url.openStream());
471: log.debug("Retrieving " + url);
472: ByteArrayOutputStream out = new ByteArrayOutputStream();
473: byte[] tmp = new byte[1024];
474: int bytes;
475: while ((bytes = in.read(tmp)) != -1) {
476: out.write(tmp, 0, bytes);
477: }
478: in.close();
479: return out.toByteArray();
480: }
481:
482: /**
483: * Lookup the mime type for the suffix of the path argument.
484: * @return the mime-type string for path.
485: */
486: protected String getMimeType(String path) {
487: int dot = path.lastIndexOf(".");
488: String type = "text/html";
489: if (dot >= 0) {
490: // The suffix is the type extension without the '.'
491: String suffix = path.substring(dot + 1);
492: String mimeType = mimeTypes.getProperty(suffix);
493: if (mimeType != null)
494: type = mimeType;
495: }
496: return type;
497: }
498:
499: }
|