001: package pygmy.core;
002:
003: import java.io.*;
004: import java.util.*;
005: import java.util.logging.Logger;
006: import java.util.logging.Level;
007: import java.util.logging.LogManager;
008: import java.lang.reflect.Constructor;
009: import java.lang.reflect.InvocationTargetException;
010:
011: /**
012: * <p>
013: * Server is the core of the system. A server glues together {@link Handler}s
014: * and {@link EndPoint}s. {@link EndPoint}s are responsible for reading the
015: * {@link HttpRequest} from a source and sending the HttpResponse over that source.
016: * {@link EndPoint}s then sends the request to the {@link Handler} by calling the post()
017: * method on the server to send the request to this server's {@link Handler}s. {@link Handler}s
018: * process the {@link HttpRequest} and produce an appropriate {@link HttpResponse}.
019: * </p><p>
020: * The server contains the configuration for the entire server. What are the expected values
021: * in the configuration is mainly controlled by what handlers and endpoints are configured.
022: * Depending on which handlers and endpoints have been enabled, the configuration will vary.
023: * The only two parameters are required: <i>handler</i> and <i><handler's name>.class</i>.
024: * Here is an example configuration:
025: * </p><pre>
026: * <div class="code">
027: * handler=my\ handler
028: * my\ handler.class=pygmy.handlers.DefaultChainHandler
029: * my\ handler.chain=handler1, handler2
030: * my\ handler.url-prefix=/
031: *
032: * handler1.class=pygmy.handlers.FileHandler
033: * handler1.root=C:\temp
034: * handler1.url-prefix=/home-directory
035: *
036: * handler2.class=pygmy.handlers.ResourceHandler
037: * handler2.url-prefix=/jar
038: * handler2.resourceMount=/html
039: * handler2.default=index.html
040: * </pre>
041: * </div
042: * <p>
043: * In the above configuration, <i>handler</i> property is the name of first handler.
044: * The name is used to find all the other properties for that particular handler.
045: * The .class property is used to tell the Server the name of the class to instantiate.
046: * The two other properties, .chain and .url-prefix, are particular to the
047: * {@link pygmy.handlers.DefaultChainHandler}.
048: * </p><p>
049: * Server's only have <b>one</b> {@link Handler}. However, the power of
050: * {@link Handler}s is the ability to have more than one. The
051: * {@link pygmy.handlers.DefaultChainHandler} provides the ability to create a
052: * chain of multiple handlers. See {@link pygmy.handlers.DefaultChainHandler} for
053: * information on configuring it.
054: * </p><p>
055: * Server also contains a set of {@link pygmy.core.EndPoint}s. When the server initializes
056: * itself it looks in the configuration for the <i>endpoints</i> parameter. The <i>endpoints</i>
057: * parameter contains a space seperated list of the names of the endpoints this server will
058: * create. For each name in the list it will look for a config parameter
059: * <i><name of endpoint>.class</i> in the configuration. It will instantiate the classname
060: * using the no-argument constructor and add it to the set of endpoints in the server.
061: * </p><p>
062: * If the server does not find the <i>endpoints</i> parameter, then it will create a default EndPoint
063: * of type {@link pygmy.core.ServerSocketEndPoint} named http. Here is an example of using the <i>endpoints</i>
064: * parameter:
065: * </p>
066: * <div class="code">
067: * <pre>
068: * endpoints=endpoint1 endpoint2
069: * handler=handler1
070: *
071: * endpoint1.class=my.package.MyEndPoint
072: * endpoint1.param1=foo
073: * endpoint1.param2=bar
074: * endpoint2.class=my.package.AnotherEndPoint
075: * endpoint2.param1=foo
076: * endpoint2.param2=bar
077: * endpoint2.param3=baz
078: * ...
079: * </pre>
080: * </div>
081: * <p>
082: * Server class looks for the following properties in the configuration:
083: * </p>
084: * <table class="inner">
085: * <tr class="header"><td>Parameter Name</td><td>Default Value</td><td>Required</td></tr>
086: * <tr class="row"><td>handler</td><td>None</td><td>Yes</td></tr>
087: * <tr class="altrow"><td>endpoints</td><td>http</td><td>No</td></tr>
088: * <tr class="row"><td><handler name>.class</td><td>None</td><td>Yes</td></tr>
089: * <tr class="altrow"><td><endpoint name>.class</td><td>None</td><td>iff endpoints param is defined</td></tr>
090: * <tr class="row"><td>threadpool.size</td><td>5</td><td>No</td></tr>
091: * </table>
092: */
093: public class Server implements Runnable {
094:
095: private static final Logger log = Logger.getLogger(Server.class
096: .getName());
097:
098: Properties config = new ChainableProperties();
099: HashMap endpoints = new HashMap();
100: Handler handler = null;
101: ResponseListener responseListener = null;
102: ThreadPool threadPool;
103:
104: private static final String CLAZZ = ".class";
105:
106: /**
107: * This creates a server using the given filename as the configuration for this server. The configuration file
108: * should follow format of normal {@link java.util.Properties} file.
109: *
110: * @param filename the path to a file to use as the configuration of this server.
111: * @throws IOException
112: */
113: public Server(String filename) throws IOException {
114: InputStream is = null;
115: try {
116: is = new BufferedInputStream(new FileInputStream(filename));
117: config.load(is);
118: } finally {
119: if (is != null) {
120: is.close();
121: }
122: }
123: }
124:
125: /**
126: * This creates a server using the given configuration.
127: *
128: * @param config the configuration to use for this server.
129: */
130: public Server(Properties config) {
131: this .config = config;
132: }
133:
134: /**
135: * This creates a server from commandline arguments.
136: *
137: * @param args an array of config parameters, the format of the list is a '-' followed by either 'config' or some
138: * name of a parameter (i.e. http.port), and a space and a value for that property. All -config will load a file
139: * either from the filesystem or the class path if the file doesn't exist on the filesystem.
140: */
141: public Server(String[] args) throws IOException {
142: config = new ChainableProperties();
143: processArguments(args, config);
144: }
145:
146: /**
147: * This method adds an {@link EndPoint} to this server. It will be initialized once the {@link #start} method is called.
148: * @param name The name of this EndPoint instance.
149: * @param endpoint The instance of the endpoint to add.
150: */
151: public void addEndPoint(String name, EndPoint endpoint) {
152: endpoints.put(name, endpoint);
153: }
154:
155: // todo delete this method. it isn't used.
156: // /**
157: // * This sets the handler instance for the server. The handlername is used to pass to the
158: // * {@link Handler#initialize} method.
159: // *
160: // * @param handlerName the name that should be assigned to this handler.
161: // * @param handler the instance of the handler used by this server to service requests.
162: // */
163: // public void setHandler( String handlerName, Handler handler ) {
164: // this.rootHandlersName = handlerName;
165: // this.handler = handler;
166: // }
167:
168: /**
169: * This puts a configuration property into the server's configuration.
170: *
171: * @param key The unique key to store the value under.
172: * @param value The value of the key.
173: */
174: public void putProperty(Object key, Object value) {
175: config.put(key, value);
176: }
177:
178: /**
179: * Returns the property stored under the key.
180: * @param key the configuration key to look up.
181: * @return the value stored in the configuration at this key.
182: */
183: public String getProperty(String key) {
184: return config.getProperty(key);
185: }
186:
187: /**
188: * Returns the property stored under the key. If there isn't a property called key, then it returns the defaultValue.
189: * @param key the configuration key to look up.
190: * @param defaultValue the defaultValue returned if nothing is found under the key.
191: * @return the value stored in the configuration at this key.
192: */
193: public String getProperty(String key, String defaultValue) {
194: return config.getProperty(key, defaultValue);
195: }
196:
197: /**
198: * Returns true iff the key is in the configuration
199: * @param key the name of the key.
200: * @return true if and only if the key is contained in the configuration. False otherwise.
201: */
202: public boolean hasProperty(String key) {
203: return config.containsKey(key);
204: }
205:
206: /**
207: * This returns the object stored under the given key.
208: *
209: * @param key the key to look up the stored object.
210: * @return the object stored at the given key.
211: */
212: public Object get(Object key) {
213: return config.get(key);
214: }
215:
216: /**
217: * Returns the configuration for the server.
218: * @return the configuration for the server.
219: */
220: public Properties getConfig() {
221: return config;
222: }
223:
224: public Object getRegisteredComponent(Class clazz) {
225: return config.get(clazz);
226: }
227:
228: /**
229: * This is called when a program wants to register a shared component for Handlers to have access to. The objects's
230: * class will be the defining key for identitying the object. Registering more than one instance will not
231: * be supported. If you're program must differ between two instances, then register a manager and allow the
232: * Handler's to interact with that to differentiate the individual instances.
233: *
234: * @param object the object the user wants to make available to Handler instances.
235: */
236: public void registerComponent(Object object) {
237: config.put(object.getClass(), object);
238: }
239:
240: /**
241: * This method is called to start the web server. It will initialize the server's Handler and all the EndPoints
242: * then call the {@link EndPoint#start} on each EndPoint. This method will return after the above steps are
243: * done.
244: */
245: public void start() {
246: Runtime.getRuntime().addShutdownHook(
247: new Thread(this , "PygmyShutdown"));
248: initializeThreads();
249: initializeHandler();
250: if (handler == null) {
251: return;
252: }
253:
254: constructEndPoints();
255:
256: for (Iterator i = endpoints.values().iterator(); i.hasNext();) {
257: EndPoint currentEndPoint = (EndPoint) i.next();
258: currentEndPoint.start();
259: }
260: }
261:
262: private void initializeThreads() {
263: try {
264: threadPool = new ThreadPool(Integer.parseInt(config
265: .getProperty("threadpool.size", "5")));
266: } catch (NumberFormatException e) {
267: log
268: .warning("threadpool.size was not a number using default of 5");
269: threadPool = new ThreadPool(5);
270: }
271: }
272:
273: protected void initializeHandler() {
274: if (handler == null) {
275: handler = (Handler) constructPygmyObject(getProperty("handler"));
276: }
277: handler.initialize(getProperty("handler"), this );
278: }
279:
280: /**
281: * This is the method used to construct pygmy objects. Given the object name it appends .class onto the end and
282: * looks for the classname in the server's configuration. It then analyzes the class's constructor parameters for
283: * objects that it depends on. Then it looks those objects up by class in the registered object pool. Finally, it
284: * calls the constructor using reflection passing any registered objects as arguments. It returns
285: * the newly constructed object or null if there was a problem.
286: *
287: * @param objectName the name of the object defined in the server's configuration.
288: * @return the newly constructed object, or null there was a problem instantiated the object.
289: */
290: public Object constructPygmyObject(String objectName) {
291: Object theObject = null;
292: String objectClassname = getProperty(objectName + CLAZZ);
293: try {
294: if (objectClassname == null)
295: throw new ClassNotFoundException(objectName + CLAZZ
296: + " configuration property not found.");
297: Class handlerClass = Class.forName(objectClassname);
298: Constructor[] constructors = handlerClass.getConstructors();
299: Class[] paramClass = constructors[0].getParameterTypes();
300: Object[] params = new Object[paramClass.length];
301: for (int i = 0; i < paramClass.length; i++) {
302: if (paramClass[i].equals(Server.class)) {
303: params[i] = this ;
304: } else if (paramClass[i].equals(String.class)) {
305: params[i] = objectName;
306: } else {
307: params[i] = getRegisteredComponent(paramClass[i]);
308: }
309: }
310: theObject = constructors[0].newInstance(params);
311: log.config("Pygmy object constructed. object=" + objectName
312: + " class=" + objectClassname);
313: } catch (IllegalAccessException e) {
314: log
315: .log(
316: Level.SEVERE,
317: "Could not access constructor. Make sure it has the constructor is public. Service not started. class="
318: + objectClassname, e);
319: } catch (InstantiationException e) {
320: log.log(Level.SEVERE,
321: "Could not instantiate object. Service not started. class="
322: + objectClassname, e);
323: } catch (ClassNotFoundException e) {
324: log.log(Level.SEVERE,
325: "Could not find class. Service not started. class="
326: + objectClassname, e);
327: } catch (InvocationTargetException e) {
328: log
329: .log(
330: Level.SEVERE,
331: "Could not instantiate object because constructor threw an exception. Service not started. class="
332: + objectClassname, e);
333: log.log(Level.SEVERE, "Cause:", e.getTargetException());
334: }
335: return theObject;
336: }
337:
338: private void constructEndPoints() {
339: String val = getProperty("endpoints");
340: if (val != null) {
341: StringTokenizer tokenizer = new StringTokenizer(val);
342: while (tokenizer.hasMoreTokens()) {
343: String endPointName = tokenizer.nextToken();
344: try {
345: EndPoint endPoint = (EndPoint) constructPygmyObject(endPointName);
346: endPoint.initialize(endPointName, this );
347: addEndPoint(endPointName, endPoint);
348: } catch (IOException e) {
349: log.log(Level.SEVERE, endPointName
350: + " was not initialized properly.", e);
351: }
352: }
353: } else {
354: log.log(Level.SEVERE, "No endpoints defined.");
355: }
356: }
357:
358: /**
359: * This is called when the server is shutdown thread is called.
360: */
361: public void run() {
362: shutdown();
363: synchronized (this ) {
364: this .notify();
365: }
366: }
367:
368: /**
369: * This method will shutdown the Handler, and call {@link EndPoint#shutdown} on each EndPoint.
370: */
371: public void shutdown() {
372: log.info("Starting shutdown.");
373: try {
374: threadPool.shutdown();
375: if (handler != null) {
376: log.info("Shutting down handlers.");
377: handler.shutdown(this );
378: }
379: Collection values = endpoints.values();
380: if (values != null) {
381: for (Iterator i = values.iterator(); i.hasNext();) {
382: EndPoint currentEndPoint = (EndPoint) i.next();
383: log.info("Shutting down endpoint "
384: + currentEndPoint.getName());
385: currentEndPoint.shutdown(this );
386: }
387: }
388: } finally {
389: log.info("Shutdown complete.");
390: }
391: }
392:
393: /**
394: * This method is used to post a {@link HttpRequest} to the server's handler. It will create a HttpResponse
395: * for the EndPoint to send to the client.
396: * @param request
397: * @return A HttpResponse that corresponds to the HttpRequest being handled.
398: * @throws IOException
399: */
400: public boolean post(Request request, Response response)
401: throws IOException {
402: return handler.handle(request, response);
403: }
404:
405: /**
406: * This method posts a Runnable onto the Server's task queue. The server's {@link ThreadPool} will service the
407: * runnable once a thread is freed up.
408: * @param runnable An instance of Runnable that the user wishes to run on the server's {@link ThreadPool}.
409: */
410: public void post(Runnable runnable) {
411: threadPool.execute(runnable);
412: }
413:
414: /**
415: * Returns the instance of the ResponseListener for this Server.
416: *
417: * @return the ResponseListener for this Server, or null if there is none.
418: */
419: public ResponseListener getResponseListeners() {
420: return responseListener;
421: }
422:
423: /**
424: * This sets the ResponseListener for entire server. All replys being sent to any client will
425: * be notified to this instance.
426: *
427: * @param listener the instance of a ResponseListener to use for this Server.
428: */
429: public void setResponseListener(ResponseListener listener) {
430: this .responseListener = listener;
431: }
432:
433: public static void main(String[] args) throws IOException {
434: Server server = new Server(args);
435: try {
436: server.start();
437: System.out
438: .println("Server started. Press <Ctrl-C> to stop.");
439: synchronized (server) {
440: server.wait();
441: }
442: } catch (InterruptedException e) {
443: log.log(Level.INFO, "Server Interupted.");
444: }
445: }
446:
447: protected void processArguments(String[] args, Properties props)
448: throws IOException {
449: for (int i = 0; i < args.length; i += 2) {
450: if (args[i].equalsIgnoreCase("-config")) {
451: if (i + 1 < args.length) {
452: loadConfiguration(args[i + 1], props);
453: } else {
454: throw new IOException(
455: "-config parameter must be followed by a config file.");
456: }
457: } else if (args[i].startsWith("-")) {
458: props.setProperty(args[i].substring(1), args[i + 1]);
459: }
460: }
461: setDefaultProperties(props);
462: setupLogging(props);
463: }
464:
465: private void setupLogging(Properties props) throws IOException {
466: ByteArrayOutputStream stream = new ByteArrayOutputStream();
467: props.store(stream, "");
468: ByteArrayInputStream bais = new ByteArrayInputStream(stream
469: .toByteArray());
470: LogManager.getLogManager().readConfiguration(bais);
471: }
472:
473: private static void setDefaultProperties(Properties props) {
474: setDefaultProperty(props, "mime.html", "text/html");
475: setDefaultProperty(props, "mime.zip",
476: "application/x-zip-compressed");
477: setDefaultProperty(props, "mime.gif", "image/gif");
478: setDefaultProperty(props, "mime.jpeg", "image/jpeg");
479: setDefaultProperty(props, "mime.jpg", "image/jpeg");
480: setDefaultProperty(props, "mime.css", "text/css");
481: setDefaultProperty(props, "http.port", "80");
482: setDefaultProperty(props, "handler", "chain");
483: setDefaultProperty(props, "chain.class",
484: "pygmy.handlers.DefaultChainHandler");
485: setDefaultProperty(props, "chain.chain", "root");
486: // sets a default endpoint for http
487: setDefaultProperty(props, "endpoints", "http");
488: setDefaultProperty(props, "http.class",
489: "pygmy.core.ServerSocketEndPoint");
490: // these are properties read by the ResourceHandler named 'root'
491: setDefaultProperty(props, "root.class",
492: "pygmy.handlers.ResourceHandler");
493: setDefaultProperty(props, "root.urlPrefix", "/");
494: setDefaultProperty(props, "root.resourceMount", "/doc");
495: }
496:
497: private static void setDefaultProperty(Properties props,
498: String key, String value) {
499: if (props.getProperty(key) == null) {
500: props.setProperty(key, value);
501: }
502: }
503:
504: protected void loadConfiguration(String config, Properties props)
505: throws IOException {
506: InputStream is = openInputStream(config);
507: props.load(is);
508: is.close();
509: }
510:
511: private InputStream openInputStream(String config)
512: throws FileNotFoundException {
513: InputStream is;
514: try {
515: is = new FileInputStream(config);
516: } catch (FileNotFoundException e) {
517: is = Server.class.getResourceAsStream("/" + config);
518: if (is == null)
519: throw e;
520: }
521: return is;
522: }
523: }
|