001: /* *****************************************************************************
002: * ScriptCompiler.java
003: * ****************************************************************************/
004:
005: /* J_LZ_COPYRIGHT_BEGIN *******************************************************
006: * Copyright 2001-2006 Laszlo Systems, Inc. All Rights Reserved. *
007: * Use is subject to license terms. *
008: * J_LZ_COPYRIGHT_END *********************************************************/
009:
010: package org.openlaszlo.sc;
011:
012: import java.io.*;
013: import java.util.*;
014: import org.openlaszlo.server.LPS;
015: import org.openlaszlo.utils.ChainedException;
016: import org.openlaszlo.iv.flash.api.*;
017: import org.openlaszlo.iv.flash.api.action.*;
018: import org.openlaszlo.iv.flash.util.*;
019: import org.apache.log4j.*;
020: import org.openlaszlo.cache.Cache;
021: import org.openlaszlo.utils.FileUtils;
022: import org.openlaszlo.compiler.CompilationError;
023:
024: /** Utility class for compiling scripts, translating Java objects
025: * (maps, lists, and strings) to source expressions for the
026: * corresponding JavaScript object.
027: *
028: * @author Oliver Steele
029: */
030: public class ScriptCompiler extends Cache {
031: private static Logger mLogger = Logger
032: .getLogger(ScriptCompiler.class);
033:
034: static public final String SCRIPT_CACHE_NAME = "scache";
035:
036: /** Map(String properties+script, byte[] bytes), or null if the
037: * cache hasn't been initialized, has been cleared, or the cache
038: * size is zero. */
039: // TODO: [2002-11-28 ows] use org.apache.commons.util.BufferCache?
040: // TODO: [2002-11-28 ows] wrap in Collections.synchronizedMap?
041: private static ScriptCompiler mScriptCache = null;
042:
043: public ScriptCompiler(String name, File cacheDirectory,
044: Properties props) throws IOException {
045: super (name, cacheDirectory, props);
046: }
047:
048: public static ScriptCompiler getScriptCompilerCache() {
049: return mScriptCache;
050: }
051:
052: public static synchronized ScriptCompiler initScriptCompilerCache(
053: File cacheDirectory, Properties initprops)
054: throws IOException {
055: if (mScriptCache != null) {
056: return mScriptCache;
057: }
058: mScriptCache = new ScriptCompiler(SCRIPT_CACHE_NAME,
059: cacheDirectory, initprops);
060: return mScriptCache;
061: }
062:
063: /**
064: * Compiles the ActionScript in script to a new movie in the swf
065: * file named by outfile.
066: *
067: * @param script a <code>String</code> value
068: * @param outfile a <code>File</code> value
069: */
070: public static void compile(String script, File outfile,
071: int swfversion) throws IOException {
072: FileOutputStream ostream = new FileOutputStream(outfile);
073: compile(script, ostream, swfversion);
074: ostream.close();
075: }
076:
077: /**
078: * Compile the ActionScript in script to a movie, that's written
079: * to output.
080: *
081: * @param script a <code>String</code> value
082: * @param ostream an <code>OutputStream</code> value
083: */
084: public static void compile(String script, OutputStream ostream,
085: int swfversion) throws IOException {
086: byte[] action = compileToByteArray(script, new Properties());
087: writeScriptToStream(action, ostream, swfversion);
088: }
089:
090: /*
091: // TODO: [2004-01-07 hqm] This cache clearing method has the
092: following bug now; if the in memory ScriptCache has not been
093: created yet when clearCache is called, then the disk cache won't
094: get cleared. We need to make sure mScriptCache is initialized at
095: server startup.]
096: */
097: public static boolean clearCacheStatic() {
098: if (mScriptCache != null) {
099: return mScriptCache.clearCache();
100: }
101: return false;
102: }
103:
104: private static byte[] _compileToByteArray(String script,
105: Properties properties) {
106: org.openlaszlo.sc.Compiler compiler = new org.openlaszlo.sc.Compiler();
107: compiler.setProperties(properties);
108: try {
109: return compiler.compile(script);
110: } catch (org.openlaszlo.sc.parser.TokenMgrError e) {
111: // The error message isn't helpful, and has the wrong
112: // source location in it, so ignore it.
113: // TODO: [2003-01-09 ows] Fix the error message.
114: throw new CompilerException(
115: /* (non-Javadoc)
116: * @i18n.test
117: * @org-mes="Lexical error. The source location is for the element that contains the erroneous script. The error may come from an unterminated comment."
118: */
119: org.openlaszlo.i18n.LaszloMessages.getMessage(
120: ScriptCompiler.class.getName(), "051018-119"));
121: }
122: }
123:
124: /**
125: * @return a cache key for the given properties
126: */
127: static String sortedPropertiesList(Properties props) {
128: TreeSet sorted = new TreeSet();
129: for (java.util.Enumeration e = props.propertyNames(); e
130: .hasMoreElements();) {
131: String key = (String) e.nextElement();
132:
133: String value = props.getProperty(key);
134: StringBuffer buf = new StringBuffer();
135: buf.append(key);
136: buf.append(' ');
137: buf.append(value);
138:
139: sorted.add(buf.toString());
140: }
141: StringBuffer buf = new StringBuffer();
142: for (java.util.Iterator e = sorted.iterator(); e.hasNext();) {
143: String str = (String) e.next();
144: buf.append(str);
145: }
146: String propstring = buf.toString();
147: return propstring;
148: }
149:
150: /** Compiles the specified script into bytecode
151: *
152: * @param script a script
153: * @return an array containing the bytecode
154: */
155: public static byte[] compileToByteArray(String script,
156: Properties properties) {
157:
158: if (mLogger.getLevel() == Level.ALL) {
159: System.out.println(script);
160: }
161: // We only want to keep off the properties that affect the
162: // compilation. Currently, "filename" is the only property
163: // that tends to change and that the script compiler ignores,
164: // so make a copy of properties that neutralizes that.
165: properties = (Properties) properties.clone();
166: properties.setProperty("filename", "");
167: // The key is a string representation of the arguments:
168: // properties, and the script.
169: StringWriter writer = new StringWriter();
170: writer.write(sortedPropertiesList(properties));
171: writer.getBuffer().append(script);
172: String key = writer.toString();
173: // Check the cache. clearCache may clear the cache at any
174: // time, so use a copy of it so that it doesn't change state
175: // between a test that it's null and a method call on it.
176: ScriptCompiler cache = mScriptCache;
177: Item item = null;
178: byte[] code = null;
179: try {
180: if (mScriptCache == null) {
181: return _compileToByteArray(script, properties);
182: } else {
183: synchronized (mScriptCache) {
184: item = mScriptCache.findItem(key, null, false);
185: }
186: }
187:
188: if (item.getInfo().getSize() != 0) {
189: code = item.getRawByteArray();
190: } else {
191: code = _compileToByteArray(script, properties);
192: // Another thread might already have set this since we
193: // called get. That's okay --- it's the same value.
194: synchronized (mScriptCache) {
195: item.update(new ByteArrayInputStream(code), null);
196: item.updateInfo();
197: item.markClean();
198: }
199: }
200:
201: mScriptCache.updateCache(item);
202:
203: return (byte[]) code;
204: } catch (IOException e) {
205: throw new CompilationError(e,
206: "IOException in compilation/script-cache");
207: }
208: }
209:
210: /**
211: * @param action actionscript byte codes
212: * @param ostream outputstream to write SWF
213: */
214: public static void writeScriptToStream(byte[] action,
215: OutputStream ostream, int swfversion) throws IOException {
216: FlashFile file = FlashFile.newFlashFile();
217: Script s = new Script(1);
218: file.setMainScript(s);
219: file.setVersion(swfversion);
220: Program program = new Program(action, 0, action.length);
221: Frame frame = s.newFrame();
222: frame.addFlashObject(new DoAction(program));
223: InputStream input;
224: try {
225: input = file.generate().getInputStream();
226: } catch (IVException e) {
227: throw new ChainedException(e);
228: }
229:
230: byte[] buffer = new byte[1024];
231: int b = 0;
232: while ((b = input.read(buffer)) > 0) {
233: ostream.write(buffer, 0, b);
234: }
235: }
236:
237: /** Writes a LaszloScript expression that evaluates to a
238: * LaszloScript representation of the object.
239: *
240: * @param elt an element
241: * @param writer a writer
242: * @throws java.io.IOException if an error occurs
243: */
244: public static void writeObject(Object object, java.io.Writer writer)
245: throws java.io.IOException {
246: if (object instanceof Map) {
247: writeMap((Map) object, writer);
248: } else if (object instanceof List) {
249: writeList((List) object, writer);
250: } else {
251: writer.write(object.toString());
252: }
253: }
254:
255: /** Writes a LaszloScript object literal whose properties are the
256: * keys of the map and whose property values are the LaszloScript
257: * representations of the map's values.
258: *
259: * The elements of the map are strings that represent JavaScript
260: * expressions, not values. That is, the value "foo" will compile
261: * to a reference to the variable named foo; "'foo'" or "\"foo\""
262: * is necessary to enter a string in the map.
263: *
264: * @param map String -> Object
265: * @param writer a writer
266: * @return a string
267: */
268: private static void writeMap(Map map, java.io.Writer writer)
269: throws java.io.IOException {
270: writer.write("{");
271: // Sort the keys, so that regression tests aren't sensitive to
272: // the undefined order of iterating a (non-TreeMap) Map.
273: SortedMap smap = new TreeMap(map);
274: for (Iterator iter = smap.entrySet().iterator(); iter.hasNext();) {
275: Map.Entry entry = (Map.Entry) iter.next();
276: String key = (String) entry.getKey();
277: Object value = entry.getValue();
278: if (!isIdentifier(key))
279: key = quote(key);
280: writer.write(key + ": ");
281: writeObject(value, writer);
282: if (iter.hasNext()) {
283: writer.write(", ");
284: }
285: }
286: writer.write("}");
287: }
288:
289: /** Writes a LaszloScript array literal that evaluates to a
290: * LaszloScript array whose elements are LaszloScript
291: * representations of the arguments elements.
292: *
293: * The elements of the list are strings that represent JavaScript
294: * expressions, not values. That is, the value "foo" will compile
295: * to a reference to the variable named foo; "'foo'" or "\"foo\""
296: * is necessary to enter a string in the array.
297: *
298: * @param list a list
299: * @param writer a writer
300: * @return a string
301: */
302: private static void writeList(List list, java.io.Writer writer)
303: throws java.io.IOException {
304: writer.write("[");
305: for (java.util.Iterator iter = list.iterator(); iter.hasNext();) {
306: writeObject(iter.next(), writer);
307: if (iter.hasNext()) {
308: writer.write(", ");
309: }
310: }
311: writer.write("]");
312: }
313:
314: /** Returns true iff the string is a valid JavaScript identifier. */
315: public static boolean isIdentifier(String s) {
316: if (s.length() == 0)
317: return false;
318: if (!Character.isJavaIdentifierStart(s.charAt(0)))
319: return false;
320: for (int i = 1; i < s.length(); i++)
321: if (!Character.isJavaIdentifierPart(s.charAt(i)))
322: return false;
323: s = s.intern();
324: String[] keywords = { "break", "continue", "delete", "else",
325: "for", "function", "if", "in", "instanceof", "new",
326: "return", "this", "typeof", "var", "void", "while",
327: "with", "case", "catch", "class", "const", "debugger",
328: "default", "do", "enum", "export", "extends",
329: "finally", "import", "super", "switch", "throw", "try" };
330: for (int i = 0; i < keywords.length; i++) {
331: if (s == keywords[i])
332: return false;
333: }
334: return true;
335: }
336:
337: /** Enclose the specified string in double-quotes, and character-quote
338: * any characters that need it.
339: * @param s a string
340: * @return a quoted string
341: */
342: public static String quote(String s) {
343: try {
344: final char CHAR_ESCAPE = '\\';
345: java.io.StringReader reader = new java.io.StringReader(s);
346: java.io.StringWriter writer = new java.io.StringWriter();
347: int i;
348: int n = 0;
349: char quote = '\"';
350: // Minimize escaping of quotes
351: if (s.indexOf('\'') >= 0 || s.indexOf('\"') >= 0) {
352: while ((i = reader.read()) != -1) {
353: char c = (char) i;
354: switch (c) {
355: case '\'':
356: n--;
357: break;
358: case '\"':
359: n++;
360: break;
361: }
362: }
363: reader.reset();
364: quote = n > 0 ? '\'' : '\"';
365: }
366: writer.write(quote);
367: while ((i = reader.read()) != -1) {
368: char c = (char) i;
369: switch (c) {
370: case '\n':
371: writer.write("\\n");
372: break;
373: case '\r':
374: writer.write("\\r");
375: break;
376: case '\b':
377: writer.write("\\b");
378: break;
379: case '\t':
380: writer.write("\\t");
381: break;
382: case '\u000B':
383: writer.write("\\v");
384: break;
385: case '\f':
386: writer.write("\\f");
387: break;
388: case '\\':
389: writer.write(CHAR_ESCAPE);
390: writer.write(c);
391: break;
392: case '\'':
393: case '\"':
394: if (c == quote) {
395: writer.write(CHAR_ESCAPE);
396: }
397: writer.write(c);
398: break;
399: default:
400: if (i == 0) {
401: // ECMAScript NUL is a special case
402: writer.write(CHAR_ESCAPE);
403: writer.write('0');
404: } else if (i < 32 || (i >= 128 && i <= 0xff)) {
405: // ECMAScript string literal hex unicode escape sequence
406: writer.write(CHAR_ESCAPE);
407: writer.write('x');
408: // Format as \ xXX two-digit zero padded hex string
409: writer.write(hexchar((c >> 4) & 0x0F));
410: writer.write(hexchar(c & 0x0F));
411: } else if (i > 0xff) {
412: // ECMAScript string literal hex unicode escape sequence
413: writer.write(CHAR_ESCAPE);
414: writer.write('u');
415: // Format as \ uXXXX four-digit zero padded hex string
416: writer.write(hexchar((c >> 12) & 0x0F));
417: writer.write(hexchar((c >> 8) & 0x0F));
418: writer.write(hexchar((c >> 4) & 0x0F));
419: writer.write(hexchar(c & 0x0F));
420: } else {
421: writer.write(c);
422: }
423: }
424: }
425: writer.write(quote);
426: return writer.toString();
427: } catch (java.io.IOException e) {
428: throw new ChainedException(e);
429: }
430: }
431:
432: static char hexchar(int c) {
433: char hexchars[] = { '0', '1', '2', '3', '4', '5', '6', '7',
434: '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
435: return (hexchars[c & 0x0F]);
436: }
437: }
|