001: /*
002: HttpdBase4J: An embeddable Java web server framework that supports HTTP, HTTPS,
003: templated content and serving content from inside a jar or archive.
004: Copyright (C) 2007 Donald Munro
005:
006: This library is free software; you can redistribute it and/or
007: modify it under the terms of the GNU Lesser General Public
008: License as published by the Free Software Foundation; either
009: version 2.1 of the License, or (at your option) any later version.
010:
011: This library is distributed in the hope that it will be useful,
012: but WITHOUT ANY WARRANTY; without even the implied warranty of
013: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: Lesser General Public License for more details.
015:
016: You should have received a copy of the GNU Lesser General Public
017: License along with this library; if not,see http://www.gnu.org/licenses/lgpl.txt
018: */
019:
020: package net.homeip.donaldm.httpdbase4j;
021:
022: import java.io.BufferedInputStream;
023: import java.io.ByteArrayInputStream;
024: import java.io.File;
025: import java.io.IOException;
026: import java.io.InputStream;
027: import java.util.Collections;
028: import java.util.HashMap;
029: import java.util.Map;
030:
031: import org.antlr.stringtemplate.StringTemplate;
032: import org.antlr.stringtemplate.StringTemplateGroup;
033:
034: import com.sun.net.httpserver.HttpExchange;
035: import java.io.BufferedOutputStream;
036: import java.io.ByteArrayOutputStream;
037: import java.io.FileOutputStream;
038: import java.util.regex.Matcher;
039: import java.util.regex.Pattern;
040: import java.util.zip.DeflaterOutputStream;
041: import java.util.zip.GZIPOutputStream;
042:
043: /**
044: * Provides a base handler class for templating based on the StringTemplate
045: * library (http://www.stringtemplate.org/)
046: * The Templates (which must have an extension of '.st') may be
047: * processed in three ways:
048: * <ul>
049: * <li>
050: * <p>
051: * The user may specify a template processing class that implements the
052: * Templatable interface. The Templatable.processTemplate method of that class
053: * will then be called for each template.
054: * </p>
055: * </li>
056: * <li>
057: * <p>
058: * If a template processing class is not specified in the constructor then
059: * the StringTemplateHandler class uses itself as a template processing
060: * class. The default Templatable handler method loads a class from
061: * the Java package in m_templateJavaPackage (which must be specified in the
062: * constructor) with the same name as the template. This class must have an
063: * empty public constructor and must implement the Templatable interface.
064: * The class name (excluding the extension) must either be exactly the same
065: * as the template (ie have the same case too) or the first letter can be
066: * uppercase. For example if the the template is listpersons.st then
067: * listpersons.class or Listpersons.class will be found.
068: * </p>
069: * </li>
070: * <li>
071: * <p>
072: * The user can overide the StringTemplateHTTPD class and provide his own
073: * processTemplate method
074: * </p>
075: * </li>
076: * </ul>
077: * The StringTemplate library (@see http://www.stringtemplate.org) requires
078: * stringtemplate.jar and antlr-2.7.7.jar be in the classpath.
079: * @see FileStringTemplateHandler
080: * @see ArchiveStringTemplateHandler
081: * @author Donald Munro
082: */
083: abstract public class StringTemplateHandler implements HttpHandleable,
084: Postable, Templatable
085: //========================================================
086: {
087: /**
088: * Template group for the home directory (also handles sub-dirs of the
089: * home dir */
090: protected StringTemplateGroup m_templateGroup = null;
091:
092: /**
093: * Template processing and generation class.
094: */
095: protected Templatable m_templateProcessor = null;
096:
097: /**
098: * The package name for per-template processing and generation Java classes.
099: **/
100: protected String m_templateJavaPackage = "";
101:
102: protected Map<Long, Object> m_resultMap = Collections
103: .synchronizedMap(new HashMap<Long, Object>());
104:
105: protected Map<Long, File> m_postMap = Collections
106: .synchronizedMap(new HashMap<Long, File>());
107:
108: protected Httpd m_httpd = null;
109:
110: private static Pattern m_htmlPattern = Pattern.compile(
111: ".*<.*\\s*html\\s*([^>]*)\\s*>.*", Pattern.DOTALL
112: | Pattern.CASE_INSENSITIVE);
113:
114: private static Pattern m_xmlPattern = Pattern.compile(
115: ".*<.*\\s*\\?xml\\s*([^\\?>]*)\\s*\\?>.*", Pattern.DOTALL
116: | Pattern.CASE_INSENSITIVE);
117:
118: protected boolean m_isCacheable = false;
119:
120: /**
121: * Constructor with template package.
122: * @param httpd The Httpd instance
123: * @param templateJavaPackage The Java package where template processing
124: * classes are located.
125: * @throws java.io.IOException Throws an IO exception if the homeDir
126: * directory is not accessible.
127: */
128: public StringTemplateHandler(Httpd httpd, String templateJavaPackage)
129: throws IOException
130: //------------------------------------------------------------------
131: {
132: if (templateJavaPackage == null)
133: throw new IllegalArgumentException(
134: "Template package is null");
135: m_httpd = httpd;
136: m_templateJavaPackage = templateJavaPackage;
137: m_templateProcessor = this ;
138: m_httpd.addDefaultFile("index.st");
139: }
140:
141: /**
142: * Constructor with a template processor class that implements
143: * the Templatable interface.
144: * @param httpd The Httpd instance
145: * @param templateProcessor A Java class that implements the Templatable
146: * interface.
147: * @throws java.io.IOException Throws an IO exception if the homeDir
148: * directory not accessible.
149: * @throws java.lang.IllegalArgumentException Throws an
150: * IllegalArgumentException exception if templateProcessor is null
151: */
152: public StringTemplateHandler(Httpd httpd,
153: Templatable templateProcessor) throws IOException,
154: IllegalArgumentException
155: //------------------------------------------------------------------
156: {
157: if (templateProcessor == null)
158: throw new IllegalArgumentException(
159: "Template processor is null");
160: m_httpd = httpd;
161: m_templateProcessor = templateProcessor;
162: m_httpd.addDefaultFile("index.st");
163: }
164:
165: abstract protected StringTemplate getTemplate(Request request);
166:
167: abstract protected Templatable getTemplateInstance(
168: String templateName);
169:
170: /**
171: * @param b true to enable debug mode (Disables use of template cache)
172: */
173: public void setDebug(boolean b)
174: //-----------------------------
175: {
176: if (b)
177: m_templateGroup.setRefreshInterval(0);
178: else
179: m_templateGroup.setRefreshInterval(Integer.MAX_VALUE);
180: }
181:
182: /**
183: * By default the StringTemplateHandler derived classes return m_isCacheable
184: * which defaults to false to disallow cacheing for template based resources.
185: * Use this method to enable/disable caching for this handler.
186: * @param isCacheable true to enable cahceing for this handler.
187: */
188: public void setCacheable(boolean isCacheable) {
189: m_isCacheable = isCacheable;
190: }
191:
192: /**
193: * @return true if cacheing is enabled for StringTemplate resources, else
194: * false
195: */
196: public boolean getCacheable() {
197: return m_isCacheable;
198: }
199:
200: public HttpResponse onServeHeaders(long id, HttpExchange ex,
201: Request request)
202: //---------------------------------------------------------------------------
203: {
204: StringTemplate template = getTemplate(request);
205: if (template == null)
206: return null;
207:
208: HttpResponse r = new HttpResponse(ex, Http.HTTP_OK);
209: /* Content-Length 0 == chunked
210: *r.addHeader("Content-Length", "0");
211: *or */
212: StringBuffer mimeType = new StringBuffer();
213: String s = m_templateProcessor.templateString(template,
214: request, mimeType);
215: if (s == null)
216: return null;
217:
218: if (mimeType.length() > 0)
219: r.addHeader("Content-Type", mimeType.toString());
220: else {
221: Matcher matcher = m_htmlPattern.matcher(s);
222: if (matcher.matches())
223: r.addHeader("Content-Type", Http.MIME_HTML);
224: else {
225: matcher = m_xmlPattern.matcher(s);
226: if (matcher.matches())
227: r.addHeader("Content-Type", Http.MIME_XML);
228: else
229: r.addHeader("Content-Type", Http.MIME_PLAINTEXT);
230: }
231: }
232: ByteArrayOutputStream baos = null;
233: if (request.m_encoding != null) {
234: if (request.m_cacheFile != null)
235: request.m_cacheFile.delete();
236: BufferedInputStream bis = null;
237: BufferedOutputStream bos = null, boss = null;
238: baos = new ByteArrayOutputStream();
239: byte[] buffer = new byte[4096];
240:
241: try {
242: bis = new BufferedInputStream(new ByteArrayInputStream(
243: s.getBytes()));
244: if (request.m_encoding.compareTo("gzip") == 0) {
245: if (request.m_cacheFile != null)
246: bos = new BufferedOutputStream(
247: new GZIPOutputStream(
248: new FileOutputStream(
249: request.m_cacheFile)));
250: boss = new BufferedOutputStream(
251: new GZIPOutputStream(baos));
252: }
253: if (request.m_encoding.compareTo("deflate") == 0) {
254: if (request.m_cacheFile != null)
255: bos = new BufferedOutputStream(
256: new DeflaterOutputStream(
257: new FileOutputStream(
258: request.m_cacheFile)));
259: boss = new BufferedOutputStream(
260: new DeflaterOutputStream(baos));
261: }
262: while (true) {
263: int cb = bis.read(buffer);
264: if (cb == -1)
265: break;
266: if (bos != null)
267: bos.write(buffer, 0, cb);
268: boss.write(buffer, 0, cb);
269: }
270: boss.close();
271: boss = null;
272: } catch (Exception e) {
273: Httpd.Log(Httpd.LogLevel.ERROR,
274: "Compressing StringTemplate output", e);
275: request.m_cacheFile = null;
276: request.m_encoding = null;
277: if (baos != null)
278: try {
279: baos.close();
280: } catch (Exception ee) {
281: }
282: baos = null;
283: } finally {
284: if (bis != null)
285: try {
286: bis.close();
287: } catch (Exception e) {
288: }
289: if (bos != null)
290: try {
291: bos.close();
292: } catch (Exception e) {
293: }
294: if (boss != null)
295: try {
296: boss.close();
297: } catch (Exception e) {
298: }
299: }
300: }
301:
302: if (baos != null) {
303: r
304: .addHeader("Content-Length", Integer.toString(baos
305: .size()));
306: m_resultMap.put(id, baos);
307: } else {
308: r.addHeader("Content-Length", Integer.toString(s.length()));
309: m_resultMap.put(id, s);
310: }
311: return r;
312: }
313:
314: /**
315: * @inheritDoc
316: */
317: public InputStream onServeBody(long id, HttpExchange ex,
318: Request request)
319: //---------------------------------------------------------------------------
320: {
321: try {
322: Object o = m_resultMap.get(id);
323: if (o != null) {
324: if (o instanceof String) {
325: String s = (String) o;
326: return new BufferedInputStream(
327: new ByteArrayInputStream(s.getBytes()));
328: } else {
329: if (o instanceof ByteArrayOutputStream) {
330: ByteArrayOutputStream baos = (ByteArrayOutputStream) o;
331: return new BufferedInputStream(
332: new ByteArrayInputStream(baos
333: .toByteArray()));
334: }
335: }
336: }
337: } catch (Exception e) {
338: Httpd.Log(Httpd.LogLevel.ERROR,
339: "Error creating input stream of "
340: + "template output string", e);
341: return null;
342: } finally {
343: m_resultMap.remove(id);
344: }
345: return null;
346: }
347:
348: /**
349: * The method processes templates and returns the file generated from the
350: * template. The default behaviour when this class is the template processor
351: * is to load a class from m_templateJavaPackage with the same name as the
352: * template. This class must have an empty public constructor and must
353: * implement the Templatable interface.
354: * Overiding classes can modify this behaviour to change the way templates
355: * content is generated.
356: * @param template The template
357: * @param request The Request instance.
358: * @return The file generated from the template. */
359: public File templateFile(StringTemplate template, Request request,
360: StringBuffer mimeType, File dir)
361: //----------------------------------------------------------------------
362: {
363: Templatable instance = getTemplateInstance(template.getName());
364: if (instance == null)
365: return null;
366: return instance.templateFile(template, request, mimeType, dir);
367: }
368:
369: /**
370: * The method processes templates and returns the file generated from the
371: * template. The default behaviour when this class is the template processor
372: * is to load a class from m_templateJavaPackage with the same name as the
373: * template. This class must have an empty public constructor and must
374: * implement the Templatable interface.
375: * Overiding classes can modify this behaviour to change the way templates
376: * content is generated.
377: * @param template The template
378: * @param request The Request instance.
379: * @return The String generated from the template. */
380: public String templateString(StringTemplate template,
381: Request request, StringBuffer mimeType)
382: //-------------------------------------------------------------------------
383: {
384: Templatable instance = getTemplateInstance(template.getName());
385: if (instance == null)
386: return null;
387: return instance.templateString(template, request, mimeType);
388: }
389:
390: /**
391: * The method processes templates and returns the file generated from the
392: * template. The default behaviour when this class is the template processor
393: * is to load a class from m_templateJavaPackage with the same name as the
394: * template. This class must have an empty public constructor and must
395: * implement the Templatable interface.
396: * Overiding classes can modify this behaviour to change the way templates
397: * content is generated.
398: * <b>Note</b> With the current implementation only templateString (and
399: * templateFile for POST processing) is used so this method need not be
400: * implemented, although if the implementation changes for example to use
401: * chunked encoding and not to generate a string in onServeHeaders, but
402: * instead to generate a stream in onServeBody then templateStream would be
403: * required.
404: * @param template The template
405: * @param request The Request instance.
406: * @return The InputStream generated from the template. */
407: public InputStream templateStream(StringTemplate template,
408: Request request, StringBuffer mimeType)
409: //-------------------------------------------------------------------------
410: {
411: Templatable instance = getTemplateInstance(template.getName());
412: if (instance == null)
413: return null;
414: return instance.templateStream(template, request, mimeType);
415: }
416:
417: public boolean onPreServe(long id, HttpExchange ex, Request request)
418: //------------------------------------------------------------------
419: {
420: return true;
421: }
422:
423: public void onPostServe(long id, HttpExchange ex, Request request,
424: boolean isOK) {
425: }
426:
427: public Request onFileNotFound(long id, HttpExchange ex,
428: Request request) {
429: return null;
430: }
431:
432: public Object onHandlePost(long id, HttpExchange ex,
433: Request request, HttpResponse response, File dir,
434: Object... extraParameters)
435: //----------------------------------------------------------------------
436: {
437: StringTemplate template = getTemplate(request);
438: if (template == null)
439: return null;
440: Templatable instance = null;
441: if (m_templateProcessor == this ) // Get class of same name as template
442: instance = getTemplateInstance(template.getName());
443: else
444: // Use supplied (single class) template processor
445: instance = m_templateProcessor;
446:
447: // In either case check if the class supports Postable
448: if (!(instance instanceof Postable))
449: return null;
450:
451: // If it does then call onHandlePost
452: Postable postProcessor = (Postable) instance;
453: return postProcessor.onHandlePost(id, ex, request, response,
454: dir, template);
455:
456: }
457:
458: public String onListDirectory(Request request) {
459: return m_httpd.onListDirectory(request);
460: }
461:
462: protected Class _instantiateTemplateClass(String className)
463: //-------------------------------------------------
464: {
465: Class C = null;
466: try {
467: C = Class.forName(className);
468: } catch (ClassNotFoundException e) {
469: C = null;
470: } catch (NoClassDefFoundError e) {
471: C = null;
472: } catch (Exception e) {
473: C = null;
474: Httpd.Log(Httpd.LogLevel.ERROR, "Exception instantiating "
475: + className, e);
476: }
477: return C;
478: }
479:
480: }
|