/* * OddTruth.java * * Copyright (c) 2007 Shawn Vincent. All Rights Reserved. * Hereby donated to the public domain. */ package com.svincent.util; import java.io.*; import com.svincent.util.Cloptus.*; /** * A simple and powerful Java template mechanism. * * This is a simple Java source preprocessor that allows pleasant and * convenient generation of HTML (and other textual output). * * OddTruth defines these simple rules: * * 1. Java code goes right through: the file starts in a Java context. * * This means an OddTruth program is a Java program: you can do * anything you want. * * 2. Hash (#) at the beginning of a line denotes template text: * it is wrapped in 'out.print (...)' * * This is an unobtrusive syntax for outputting HTML and the like. * * 3. In template text, '{...}' is a Java expression: it is wrapped in * 'out.oddPrint(...)' * * You often want to embed the results of expressions. * * 4. In template text, '{.ID(...,...)}' is a method call on 'out': it * becomes 'out.ID(...,...)' * * This lets you do different escaping mechanisms in a clean, * powerful way. * * 5. In template text, Java backslash escape sequences are allowed. * * Often you want to output weird characters (i.e. high Unicode * characters) - this lets you do it using a syntax you're * familiar with, along with providing a natural means to escape * magic characters (like '{'). * * 6. Hash-quote (#") at the beginning of a line generates a String, * instead of printing to out. Strings on subsequent lines are * automatically concatenated. * * Sometimes it's convenient to store the results of a template * in a String -- this provides a natural mechanism for doing * just that. * * An additional rule might be nice: Some kind of multi-line context * (maybe ### ... ###) -- kindof a pain to add right now -- maybe some * day. * * XXX feature to add: better locus information for OddTruth error messages. * * Example program: * * class MyHomePage { * public void viewPage (HtmlWriter out) * { * # * # * #

Welcome to my Home page!

* # * #

Hi: it's currently {new Date()}, and I like you lots.

* # * #

mail me!

* # * # * } * public String getEmailAddress () { ... } * } * * Also see RunOddTruth: an integration of OddTruth with BeanShell so * that you can run OddTruth programs directly! **/ public class OddTruth { File[] inputFiles; /** * **/ public static void main (String[] args) { try { new OddTruth ().run (args); } catch (IOException ex) { ex.printStackTrace (); } } /** * **/ public void run (String[] args) throws IOException { // --- process arguments. OptSet opts = new OptSet (); FileOpt inOpt = new FileOpt (opts, "in") .description ("List of files to process") .positional (true) .list (true) .required (true); ParsedArgs parsedArgs = opts.run (args); if (parsedArgs == null) return; inputFiles = inOpt.getArray (parsedArgs); System.out.println ("Odd Truth compiling " +inputFiles.length +" source file" +(inputFiles.length == 1 ? "" : "s")); preprocessJavaFiles (); } /** * **/ public void preprocessJavaFiles () throws IOException { for (File f : inputFiles) preprocessJavaFile (f); } /** * **/ public static void preprocessJavaFile (File f) throws IOException { String path = f.getPath (); // XXX hack. Don't process Emacs temp files if (path.endsWith ("~") || path.endsWith ("#")) return; int extensionIndex = path.lastIndexOf ('.'); String strippedExtension; if (extensionIndex == -1) strippedExtension = path; else strippedExtension = path.substring (0, extensionIndex); String newPath = strippedExtension + ".java"; Reader in = new FileReader (f); PrintWriter out = new PrintWriter (new FileWriter (new File (newPath)), true); preprocessJavaFile (in, out); } // /** // * // **/ // public static String preprocessJavaFile (String file) // { // String[] lines = file.split ("\n"); // StringBuilder out = new StringBuilder (); // for (String line:lines) // { // out.append (preprocessJavaLine (line)); // out.append ("\n"); // } // return out.toString (); // } /** * **/ public static void preprocessJavaFile (Reader _in, PrintWriter out) throws IOException { BufferedReader in = new BufferedReader (_in); boolean prevLineWasTemplate = false; String line = in.readLine (); while (line != null) { // --- only lines starting with '#' are touched. if (line.trim ().startsWith ("#")) { out.println (preprocessTemplateTextLine(line,prevLineWasTemplate)); prevLineWasTemplate = true; } // --- other lines are just Java: no preprocessing. else { out.println (line); prevLineWasTemplate = false; } line = in.readLine (); } out.flush (); } /** * **/ protected static String preprocessTemplateTextLine (String line, boolean prevLineWasTemplate) { StringBuilder out = new StringBuilder (); int grapesIndex = line.indexOf ('#'); if (grapesIndex == -1) throw new OddTruthException ("Template text line must start with '#' character."); int pos = grapesIndex+1; boolean outputString = line.length () > pos && line.charAt (pos) == '"'; if (outputString) pos++; // skip open quote. // --- output preceeding whitespace (so resulting Java code looks nicer) out.append (line.substring (0, grapesIndex)); if (outputString) { // --- concatenate subsequent string lines together. if (prevLineWasTemplate) out.append ('+'); out.append ('"'); } else out.append ("{"); int squiggleIndex = findNextOpenSquiggle (line, pos); while (squiggleIndex != -1) { // --- if the squiggle doesn't start the next section, we've // --- got template text. Output it. if (squiggleIndex > pos) { String templateText = line.substring (pos, squiggleIndex); // --- parse Java string escapes. templateText = unquoteJavaStringLiteral (templateText); if (outputString) out.append (quoteJavaStringLiteral (templateText)); else out.append ("out.print (\"" +quoteJavaStringLiteral (templateText) +"\"); "); } // --- now, 'squiggleIndex' is at the start of the Java // --- expression. Find the end. int endSquiggleIndex = findClosingSquiggle (line, squiggleIndex); String embeddedJava = line.substring (squiggleIndex, endSquiggleIndex+1); out.append (processEmbeddedJava (embeddedJava, outputString)); pos = endSquiggleIndex+1; squiggleIndex = findNextOpenSquiggle (line, pos); } if (pos < line.length ()) { // --- Extract the template text at the end of the line. String templateText = line.substring (pos); // --- parse Java string escapes. templateText = unquoteJavaStringLiteral (templateText); if (outputString) out.append (quoteJavaStringLiteral (templateText)+"\\n"); else out.append ("out.println (\"" +quoteJavaStringLiteral (templateText) +"\"); "); } else { if (outputString) out.append ("\\n"); else out.append ("out.println ();"); } if (outputString) out.append ('"'); else out.append ("}"); return out.toString (); } protected static int findNextOpenSquiggle (String line, int pos) { int openSquiggle = line.indexOf ('{', pos); // --- skip escaped squiggles. while (openSquiggle != -1 && openSquiggle > 0 && line.charAt (openSquiggle-1) == '\\') openSquiggle = line.indexOf ('{', openSquiggle+1); return openSquiggle; } /** * **/ protected static String processEmbeddedJava (String embeddedJava, boolean outputString) { if (!embeddedJava.startsWith ("{") || !embeddedJava.endsWith ("}")) throw new OddTruthException ("Embedded Java must be surrounded by squiggles."); embeddedJava = embeddedJava.substring (1, embeddedJava.length () - 1); if (outputString) return embeddedJava; else if (embeddedJava.startsWith (".")) return "out"+embeddedJava+"; "; // XXX error checking here? else return "out.oddPrint ("+embeddedJava+"); "; //XXX error checking here? } /** * **/ protected static int findClosingSquiggle (String line, int openingSquiggle) { boolean inStringLiteral = false; boolean inCharLiteral = false; for (int i=openingSquiggle+1; i 64) { // make a char array, so we don't need to call charAt a zillion times. char[] srcChars = src.toCharArray (); for (int i=0; i= len) out.append (c); else { i++; c = src.charAt (i); switch (c) { case 'b': out.append ('\b'); break; case 't': out.append ('\t'); break; case 'n': out.append ('\n'); break; case 'f': out.append ('\f'); break; case 'r': out.append ('\r'); break; case '"': out.append ('"'); break; case '\'': out.append ('\''); break; case '\\': out.append ('\\'); break; case 'u': { String hexCode = src.substring (i+1, i+5); i+=4; try { out.append ((char)Integer.parseInt (hexCode, 16)); } catch (NumberFormatException ex) { throw (IllegalArgumentException) new IllegalArgumentException ("Bad unicode escape: "+hexCode).initCause (ex); } break; } default: out.append (c); } } break; default: out.append (c); break; } } return out.toString (); } /** * Given a character, quote it as a Java string literal. If the * character doesn't require any quoting return null. This rather * bizzare behavour allows for the efficent implementation of some * other algorithms on top of this. (it avoids a string creation * per character). Use quoteCharLiteral if you don't want this * funny null behaviour.

* * Quoting is performed first as per JLS, section 3.10.6: "Escape * Sequences for Character and String Literals". This quotes most * of the normal characters such as \b and \n, etc.

* * Secondly, any non-printable-ASCII character (including the * control values in low ASCII, as well as any non-ASCII Unicode * character) is quoted using Unicode escapes, as per JLS, section * 3.3: "Unicode Escapes".

**/ public static String lookupQuotedForm (char c, boolean stringConstant) { switch (c) { case '\b': return "\\b"; case '\t': return "\\t"; case '\n': return "\\n"; case '\f': return "\\f"; case '\r': return "\\r"; case '"': if (stringConstant) return "\\\""; else return null; case '\'': if (!stringConstant) return "\\'"; else return null; case '\\': return "\\\\"; default: if (!(c >= ' ' && c <= '~')) { return "\\u" + to4DigitHex (c); } else return null; } } public static String to4DigitHex (int n) { String hexString = Integer.toHexString (n); return ("0000".substring (0, 4 - hexString.length ())) + hexString; } public static String quoteHtml (String html) { if (html == null) return null; StringBuffer out = new StringBuffer (); int len = html.length (); for (int i=0; i': out.append (">"); break; case '"': out.append ("""); break; default: if (c != '\'' && !(Character.isISOControl (c) && !Character.isWhitespace (c)) && c < 0x7f) out.append (c); else { String namedEntity = null; // XXX if (namedEntity != null) { out.append (namedEntity); } else { out.append ('&'); out.append ('#'); out.append ('x'); out.append (to4DigitHex (c)); out.append (';'); } } } } return out.toString (); } // ------------------------------------------------------------------------- // ------------------------------------------------------------------------- // ------------------------------------------------------------------------- /** * **/ public static class OddWriter extends PrintWriter { public OddWriter (Writer out) { super (out); } public OddWriter (Writer out, boolean autoFlush) { super (out, autoFlush); } public OddWriter (OutputStream out) { super (out); } public OddWriter (OutputStream out, boolean autoFlush) { super (out, autoFlush); } public void oddPrint (Object v) { print (v); } /* Alternate quoting methods */ public void html (String s) { print (OddTruth.quoteHtml (s)); } // public void htmlAttr (String s) // { print (OddTruth.quoteHtmlAttrValue (s)); } // public void htmlUri (String s) // { print (OddTruth.quoteHtmlUriAttrValue (s)); } // public void xml (String s) // { print (OddTruth.quoteXmlText (s)); } // public void xmlAttr (String s) // { print (OddTruth.quoteXmlAttrValue (s)); } public void raw (String s) { super.print (s); } public void javaStringLiteral (String s) { print (OddTruth.quoteJavaStringLiteral (s)); } } /** * **/ public static class OddHtmlWriter extends OddWriter { public OddHtmlWriter (Writer out) { super (out); } public OddHtmlWriter (Writer out, boolean autoFlush) { super (out, autoFlush); } public OddHtmlWriter (OutputStream out) { super (out); } public OddHtmlWriter (OutputStream out, boolean autoFlush) { super (out, autoFlush); } public void oddPrint (Object v) { printQuoted (v); } public void printQuoted (Object v) { print (OddTruth.quoteHtml (String.valueOf (v))); } } // ------------------------------------------------------------------------- // ------------------------------------------------------------------------- // ------------------------------------------------------------------------- /** * **/ public static class OddTruthException extends RuntimeException { public OddTruthException () { super (); } public OddTruthException (String msg) { super (msg); } public OddTruthException (String msg, Throwable ex) { super (msg); initCause (ex); } } }