Commits

Stephen McKamey committed fdc1aed

- adding auto-emitting of ambient data before the very first script block

Comments (0)

Files changed (10)

src/org/duelengine/duel/DuelContext.java

 	private final Appendable output;
 	private final ClientIDStrategy clientID;
 	private boolean encodeNonASCII = true;
+	private boolean globalDataPending;
 	private SparseMap globalData;
 
 	public DuelContext(Appendable output) {
 		this.encodeNonASCII = value;
 	}
 
-	Object getGlobals() {
-		return this.globalData;
-	}
-
 	public Object getGlobalData(String ident) {
 		if (ident == null) {
 			throw new NullPointerException("ident");
 			this.globalData = new SparseMap();
 		}
 
+		this.globalDataPending = true;
 		this.globalData.putSparse(ident, value);
 	}
 
 		return true;
 	}
 
+	SparseMap getGlobalData() {
+		return this.globalData;
+	}
+
+	boolean isGlobalDataPending() {
+		return this.globalDataPending;
+	}
+
+	void setGlobalDataPending(boolean value) {
+		this.globalDataPending = value;
+	}
+
 	@Override
 	public Appendable append(CharSequence csq)
 		throws IOException {

src/org/duelengine/duel/DuelView.java

 import java.util.*;
 
 /**
- * The skeletal implementation of DUEL view runtime
+ * The skeletal implementation of DUEL view runtime.
+ * Inherently thread-safe as contains no instance data.
  */
 public abstract class DuelView {
 
 		}
 	}
 
+	protected void writeGlobalData(DuelContext output, DataEncoder encoder, boolean needsTags)
+		throws IOException {
+
+		if (!output.isGlobalDataPending()) {
+			return;
+		}
+
+		if (needsTags) {
+			formatter.writeOpenElementBeginTag(output, "script");
+			formatter.writeAttribute(output, "type", "text/javascript");
+			formatter.writeCloseElementBeginTag(output);
+		}
+		encoder.writeVars(output, output.getGlobalData());
+		if (needsTags) {
+			formatter.writeElementEndTag(output, "script");
+		}
+		output.setGlobalDataPending(false);
+	}
+	
 	/**
 	 * A work-around for dynamic post-inc/dec operators
 	 * @param value

src/org/duelengine/duel/codedom/CodeConditionStatement.java

 package org.duelengine.duel.codedom;
 
-import java.util.Arrays;
-
 public class CodeConditionStatement extends CodeStatement {
 
 	private CodeExpression condition;
-	private final CodeStatementCollection trueStatements = new CodeStatementCollection();
-	private final CodeStatementCollection falseStatements = new CodeStatementCollection();
+	private final CodeStatementCollection trueStatements;
+	private final CodeStatementCollection falseStatements;
 
 	public CodeConditionStatement() {
+		this(null);
 	}
 
 	public CodeConditionStatement(CodeExpression condition, CodeStatement... trueStatements) {
 	}
 
 	public CodeConditionStatement(CodeExpression condition, CodeStatement[] trueStatements, CodeStatement[] falseStatements) {
+		this.trueStatements = new CodeStatementCollection(this);
+		this.falseStatements = new CodeStatementCollection(this);
+
 		this.condition = condition;
 
 		if (trueStatements != null) {

src/org/duelengine/duel/codedom/CodeIterationStatement.java

 	private CodeStatement initStatement;
 	private CodeExpression testExpression;
 	private CodeStatement incrementStatement;
-	private final CodeStatementCollection statements = new CodeStatementCollection();
+	private final CodeStatementCollection statements;
 
 	public CodeIterationStatement() {
+		this(null, null, null);
 	}
 
 	public CodeIterationStatement(CodeStatement initStatement, CodeExpression testExpression, CodeStatement incrementStatement, CodeStatement... statements) {
 		this.initStatement = initStatement;
 		this.testExpression = testExpression;
 		this.incrementStatement = incrementStatement;
+		this.statements = new CodeStatementCollection(this);
 		if (statements != null) {
 			this.statements.addAll(statements);
 		}

src/org/duelengine/duel/codedom/CodeMethod.java

  */
 public class CodeMethod extends CodeMember {
 
-	private Class<?> returnType = Void.class;
+	private Class<?> returnType;
 	private final List<CodeParameterDeclarationExpression> parameters = new ArrayList<CodeParameterDeclarationExpression>();
-	private final CodeStatementCollection statements = new CodeStatementCollection();
+	private final CodeStatementCollection statements;
 	private final List<Class<?>> exceptions = new ArrayList<Class<?>>();
 	private boolean override;
 
 	public CodeMethod() {
+		this(AccessModifierType.DEFAULT, null, null, null);
 	}
 
 	public CodeMethod(AccessModifierType access, Class<?> returnType, String methodName,
 
 		super(access, methodName);
 
-		if (returnType != null) {
-			this.returnType = returnType;
-		}
+		this.setReturnType(returnType);
+
 		if (parameters != null) {
 			this.parameters.addAll(Arrays.asList(parameters));
 		}
+
+		this.statements = new CodeStatementCollection(this);
 		if (statements != null) {
 			this.statements.addAll(statements);
 		}

src/org/duelengine/duel/codedom/CodeStatementBlock.java

 package org.duelengine.duel.codedom;
 
 /**
- * Only used internally to pass around a sequence of statements as a CodeObject
+ * Used internally to pass around a sequence of statements as a CodeObject
  */
 public class CodeStatementBlock extends CodeObject {
 
-	private final CodeStatementCollection statements = new CodeStatementCollection();
+	private final CodeStatementCollection statements;
 
 	public CodeStatementBlock() {
-	}
-
-	public CodeStatementBlock(CodeStatement[] statements) {
-		this.statements.addAll(statements);
+		this.statements = new CodeStatementCollection(this);
 	}
 
 	public CodeStatementCollection getStatements() {

src/org/duelengine/duel/codedom/CodeStatementCollection.java

 @SuppressWarnings("serial")
 public class CodeStatementCollection extends ArrayList<CodeStatement> implements IdentifierScope {
 
+	private final CodeObject owner;
 	private Map<String, String> identMap;
 	private int nextID;
 
-	public CodeStatementCollection() {
+	public CodeStatementCollection(CodeObject owner) {
+		this.owner = owner;
 	}
 
-	public CodeStatementCollection(CodeStatement[] statements) {
-		this(Arrays.asList(statements));
-	}
-
-	public CodeStatementCollection(Iterable<CodeStatement> statements) {
-		if (statements != null) {
-			for (CodeStatement statement : statements) {
-				this.add(statement);
-			}
-		}
+	public CodeObject getOwner() {
+		return owner;
 	}
 
 	public boolean addAll(CodeStatementBlock block) {

src/org/duelengine/duel/codegen/CodeDOMBuilder.java

 
 import java.io.IOException;
 import java.util.*;
-
 import org.duelengine.duel.*;
 import org.duelengine.duel.ast.*;
 import org.duelengine.duel.codedom.*;
 	private final StringBuilder buffer;
 	private final Stack<CodeStatementCollection> scopeStack = new Stack<CodeStatementCollection>();
 	private CodeTypeDeclaration viewType;
-	private TagMode tagMode = TagMode.None;
+	private TagMode tagMode;
+	private boolean globalsEmitted;
 
 	public CodeDOMBuilder() {
 		this(null);
 			String name = fullName.substring(lastDot+1);
 			String ns = (lastDot > 0) ? fullName.substring(0, lastDot) : null;
 
+			this.scopeStack.clear();
+			this.tagMode = TagMode.None;
+			this.globalsEmitted = false;
 			this.viewType = CodeDOMUtility.createViewType(ns, name);
 
 			CodeMethod method = this.buildRenderMethod(viewNode.getChildren()).withOverride();
 		CodeExpression dataExpr;
 		DuelNode callData = node.getAttribute(CALLCommandNode.DATA);
 		if (callData instanceof CodeBlockNode) {
-			dataExpr = this.translateExpression((CodeBlockNode)callData);
+			dataExpr = this.translateExpression((CodeBlockNode)callData, false);
 		} else {
 			dataExpr = new CodeVariableReferenceExpression(Object.class, "data");
 		}
 		CodeExpression indexExpr;
 		DuelNode callIndex = node.getAttribute(CALLCommandNode.INDEX);
 		if (callIndex instanceof CodeBlockNode) {
-			indexExpr = this.translateExpression((CodeBlockNode)callIndex);
+			indexExpr = this.translateExpression((CodeBlockNode)callIndex, false);
 		} else {
 			indexExpr = new CodeVariableReferenceExpression(int.class, "index");
 		}
 		CodeExpression countExpr;
 		DuelNode callCount = node.getAttribute(CALLCommandNode.COUNT);
 		if (callCount instanceof CodeBlockNode) {
-			countExpr = this.translateExpression((CodeBlockNode)callCount);
+			countExpr = this.translateExpression((CodeBlockNode)callCount, false);
 		} else {
 			countExpr = new CodeVariableReferenceExpression(int.class, "count");
 		}
 		CodeExpression keyExpr;
 		DuelNode callKey = node.getAttribute(CALLCommandNode.KEY);
 		if (callKey instanceof CodeBlockNode) {
-			keyExpr = this.translateExpression((CodeBlockNode)callKey);
+			keyExpr = this.translateExpression((CodeBlockNode)callKey, false);
 		} else {
 			keyExpr = new CodeVariableReferenceExpression(String.class, "key");
 		}
 		CodeExpression dataExpr;
 		DuelNode loopCount = node.getAttribute(FORCommandNode.COUNT);
 		if (loopCount instanceof CodeBlockNode) {
-			CodeExpression countExpr = this.translateExpression((CodeBlockNode)loopCount);
+			CodeExpression countExpr = this.translateExpression((CodeBlockNode)loopCount, false);
 
 			DuelNode loopData = node.getAttribute(FORCommandNode.DATA);
 			if (loopData instanceof CodeBlockNode) {
-				dataExpr = this.translateExpression((CodeBlockNode)loopData);
+				dataExpr = this.translateExpression((CodeBlockNode)loopData, false);
 			} else {
 				dataExpr = new CodeVariableReferenceExpression(Object.class, "data");
 			}
 		} else {
 			DuelNode loopObj = node.getAttribute(FORCommandNode.IN);
 			if (loopObj instanceof CodeBlockNode) {
-				CodeExpression objExpr = this.translateExpression((CodeBlockNode)loopObj);
+				CodeExpression objExpr = this.translateExpression((CodeBlockNode)loopObj, false);
 				this.buildIterationObject(scope, objExpr, innerBind);
 
 			} else {
 					throw new InvalidNodeException("FOR loop missing arguments", loopArray);
 				}
 
-				CodeExpression arrayExpr = this.translateExpression((CodeBlockNode)loopArray);
+				CodeExpression arrayExpr = this.translateExpression((CodeBlockNode)loopArray, false);
 				this.buildIterationArray(scope, arrayExpr, innerBind);
 			}
 		}
 		CodeConditionStatement condition = new CodeConditionStatement();
 		scope.add(condition);
 
-		condition.setCondition(this.translateExpression(testNode));
+		condition.setCondition(this.translateExpression(testNode, false));
 
 		if (node.hasChildren()) {
 			this.scopeStack.push(condition.getTrueStatements());
 		return condition.getFalseStatements();
 	}
 
-	private CodeExpression translateExpression(CodeBlockNode node) {
+	private CodeExpression translateExpression(CodeBlockNode node, boolean isWrite) {
 
 		try {
 			// convert from JavaScript source to CodeDOM
 					new CodeVariableReferenceExpression(String.class, "key"));
 			}
 
-			if (members.size() > 0 &&
+			if (isWrite && members.size() > 0 &&
 				members.get(0).getUserData(ScriptTranslator.EXTERNAL_IDENTS) instanceof Object[]) {
 
 				Object[] idents = (Object[])members.get(0).getUserData(ScriptTranslator.EXTERNAL_IDENTS);
 		this.formatter.writeOpenElementBeginTag(this.buffer, "script");
 		this.formatter.writeAttribute(this.buffer, "type", "text/javascript");
 		this.formatter.writeCloseElementBeginTag(this.buffer);
+		this.ensureGlobalsEmitted(false);
 
 		// emit patch function call which serializes attributes into object
 		this.buffer.append("duel.write(");
 		throws IOException {
 
 		String tagName = element.getTagName();
+
+		if ("script".equalsIgnoreCase(tagName)) {
+			this.ensureGlobalsEmitted(true);
+		}
+		
 		this.formatter.writeOpenElementBeginTag(this.buffer, tagName);
 
 		int argSize = 0;
 			TagMode prevMode = this.tagMode;
 			if ("script".equalsIgnoreCase(tagName) || "style".equalsIgnoreCase(tagName)) {
 				this.tagMode = TagMode.SuspendMode;
+
 			} else if ("pre".equalsIgnoreCase(tagName)) {
 				this.tagMode = TagMode.PreMode;
 			}
+
 			try {
 				for (DuelNode child : element.getChildren()) {
 					if (this.settings.getNormalizeWhitespace() &&
 		this.formatter.writeOpenElementBeginTag(this.buffer, "script");
 		this.formatter.writeAttribute(this.buffer, "type", "text/javascript");
 		this.formatter.writeCloseElementBeginTag(this.buffer);
+		this.ensureGlobalsEmitted(false);
 
 		// emit patch function call which serializes attributes into object
 		this.buffer.append("duel.attr(");
 		CodeVariableDeclarationStatement idVar = this.emitClientID();
 		this.formatter.writeCloseAttribute(this.buffer);
 		this.formatter.writeCloseElementBeginTag(this.buffer);
+		this.ensureGlobalsEmitted(false);
 
 		// emit patch function call which serializes attributes into object
 		this.buffer.append("duel.replace(");
 			block = new ExpressionNode(block.getValue(), block.getIndex(), block.getLine(), block.getColumn());
 		}
 
-		CodeExpression codeExpr = this.translateExpression(block);
+		CodeExpression codeExpr = this.translateExpression(block, true);
 		if (codeExpr == null) {
 			return null;
 		}
 			CodeDOMUtility.emitExpression(codeExpr);
 	}
 
+	private void ensureGlobalsEmitted(boolean needsTags) {
+		if (this.globalsEmitted) {
+			return;
+		}
+
+		// emit check just inside first script block
+		this.flushBuffer();
+
+		CodeField encoder = this.ensureEncoder();
+		this.scopeStack.peek().add(
+			new CodeMethodInvokeExpression(
+				Void.class,
+				new CodeThisReferenceExpression(),
+				"writeGlobalData",
+				new CodeVariableReferenceExpression(DuelContext.class, "output"),
+				new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), encoder),
+				new CodePrimitiveExpression(needsTags)));
+
+		// check scope chain for any condition or iteration blocks that
+		// might prevent this from being emitted. if found do not mark.
+		// there is a runtime check as well which will prevent multiple.
+		for (CodeStatementCollection scope : this.scopeStack) {
+
+			// TODO: this is too naive. doesn't work with hybrid methods
+			if (!(scope.getOwner() instanceof CodeMethod)) {
+				return;
+			}
+		}
+
+		this.globalsEmitted = true;
+	}
+
 	private CodeMethod ensureInitMethod() {
 		for (CodeMember member : this.viewType.getMembers()) {
 			if (member instanceof CodeMethod &&

src/org/duelengine/duel/codegen/ServerCodeGen.java

 		throws IOException {
 
 		output.append("if (");
-		this.writeExpression(output, statement.getCondition(), ParensSetting.SUPPRESS);
+		this.writeExpression(output, CodeDOMUtility.ensureBoolean(statement.getCondition()), ParensSetting.SUPPRESS);
 		output.append(") {");
 		this.depth++;
 
 
 		this.writeStatement(output, statement.getInitStatement(), true);
 		output.append("; ");
-		this.writeExpression(output, statement.getTestExpression());
+		this.writeExpression(output, CodeDOMUtility.ensureBoolean(statement.getTestExpression()));
 		output.append("; ");
 		this.writeStatement(output, statement.getIncrementStatement(), true);
 

test/org/duelengine/duel/codegen/CodeDOMBuilderTests.java

 						Void.class,
 						new CodeVariableReferenceExpression(DuelContext.class, "output"),
 						"append",
-						new CodePrimitiveExpression("<div foo=\"&amp;&lt;&gt;&quot;\">&amp;&lt;&gt;\"<script type=\"text/javascript\">&<>\"</script>&amp;&lt;&gt;\"</div>")))
-				).withOverride().withThrows(IOException.class)
+						new CodePrimitiveExpression("<div foo=\"&amp;&lt;&gt;&quot;\">&amp;&lt;&gt;\""))),
+				new CodeExpressionStatement(new CodeMethodInvokeExpression(
+					Void.class,
+					new CodeThisReferenceExpression(),
+					"writeGlobalData",
+					new CodeVariableReferenceExpression(DuelContext.class, "output"),
+					new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), DataEncoder.class, "encoder_2"),
+					new CodePrimitiveExpression(true))),
+				new CodeExpressionStatement(new CodeMethodInvokeExpression(
+					Void.class,
+					new CodeVariableReferenceExpression(DuelContext.class, "output"),
+					"append",
+					new CodePrimitiveExpression("<script type=\"text/javascript\">&<>\"</script>&amp;&lt;&gt;\"</div>")))
+				).withOverride().withThrows(IOException.class),
+			new CodeField(
+				AccessModifierType.PRIVATE,
+				DataEncoder.class,
+				"encoder_2"),
+			new CodeMethod(
+				AccessModifierType.PROTECTED,
+				Void.class,
+				"init",
+				null,
+				new CodeExpressionStatement(new CodeBinaryOperatorExpression(
+					CodeBinaryOperatorType.ASSIGN,
+					new CodeFieldReferenceExpression(
+						new CodeThisReferenceExpression(),
+						DataEncoder.class,
+						"encoder_2"),
+					new CodeObjectCreateExpression(
+						DataEncoder.class.getSimpleName(),
+						new CodePrimitiveExpression("\n"),
+						new CodePrimitiveExpression("\t"))))).withOverride()
 			);
 
 		CodeTypeDeclaration actual = new CodeDOMBuilder().buildView(input);
 					Void.class,
 					new CodeVariableReferenceExpression(DuelContext.class, "output"),
 					"append",
-					new CodePrimitiveExpression("\">Lorem ipsum.</p><script type=\"text/javascript\">duel.attr("))),
+					new CodePrimitiveExpression("\">Lorem ipsum.</p><script type=\"text/javascript\">"))),
+				new CodeExpressionStatement(new CodeMethodInvokeExpression(
+					Void.class,
+					new CodeThisReferenceExpression(),
+					"writeGlobalData",
+					new CodeVariableReferenceExpression(DuelContext.class, "output"),
+					new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), DataEncoder.class, "encoder_5"),
+					new CodePrimitiveExpression(false))),
+				new CodeExpressionStatement(new CodeMethodInvokeExpression(
+					Void.class,
+					new CodeVariableReferenceExpression(DuelContext.class, "output"),
+					"append",
+					new CodePrimitiveExpression("duel.attr("))),
 				new CodeExpressionStatement(new CodeMethodInvokeExpression(
 					Void.class,
 					new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), DataEncoder.class, "encoder_5"),
 					Void.class,
 					new CodeVariableReferenceExpression(DuelContext.class, "output"),
 					"append",
-					new CodePrimitiveExpression("\">duel.replace("))),
+					new CodePrimitiveExpression("\">"))),
+				new CodeExpressionStatement(new CodeMethodInvokeExpression(
+					Void.class,
+					new CodeThisReferenceExpression(),
+					"writeGlobalData",
+					new CodeVariableReferenceExpression(DuelContext.class, "output"),
+					new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), DataEncoder.class, "encoder_3"),
+					new CodePrimitiveExpression(false))),
+				new CodeExpressionStatement(new CodeMethodInvokeExpression(
+					Void.class,
+					new CodeVariableReferenceExpression(DuelContext.class, "output"),
+					"append",
+					new CodePrimitiveExpression("duel.replace("))),
 				new CodeExpressionStatement(new CodeMethodInvokeExpression(
 					Void.class,
 					new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), DataEncoder.class, "encoder_3"),
 					Void.class,
 					new CodeVariableReferenceExpression(DuelContext.class, "output"),
 					"append",
-					new CodePrimitiveExpression("<script type=\"text/javascript\">duel.write(function() { return (foo.bar); });</script>"))),
+					new CodePrimitiveExpression("<script type=\"text/javascript\">"))),
+				new CodeExpressionStatement(new CodeMethodInvokeExpression(
+					Void.class,
+					new CodeThisReferenceExpression(),
+					"writeGlobalData",
+					new CodeVariableReferenceExpression(DuelContext.class, "output"),
+					new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), DataEncoder.class, "encoder_4"),
+					new CodePrimitiveExpression(false))),
+				new CodeExpressionStatement(new CodeMethodInvokeExpression(
+					Void.class,
+					new CodeVariableReferenceExpression(DuelContext.class, "output"),
+					"append",
+					new CodePrimitiveExpression("duel.write(function() { return (foo.bar); });</script>"))),
 				new CodeMethodReturnStatement(CodePrimitiveExpression.NULL)
-			).withThrows(IOException.class)
+			).withThrows(IOException.class),
+			new CodeField(
+				AccessModifierType.PRIVATE,
+				DataEncoder.class,
+				"encoder_4"),
+			new CodeMethod(
+				AccessModifierType.PROTECTED,
+				Void.class,
+				"init",
+				null,
+				new CodeExpressionStatement(new CodeBinaryOperatorExpression(
+					CodeBinaryOperatorType.ASSIGN,
+					new CodeFieldReferenceExpression(
+						new CodeThisReferenceExpression(),
+						DataEncoder.class,
+						"encoder_4"),
+					new CodeObjectCreateExpression(
+						DataEncoder.class.getSimpleName(),
+						new CodePrimitiveExpression("\n"),
+						new CodePrimitiveExpression("\t"))))).withOverride()
 		);
 
 		CodeTypeDeclaration actual = new CodeDOMBuilder().buildView(input);
 					Void.class,
 					new CodeVariableReferenceExpression(DuelContext.class, "output"),
 					"append",
-					new CodePrimitiveExpression("<script type=\"text/javascript\">duel.write(function(data) { foo.bar = (baz+data); }, "))),
+					new CodePrimitiveExpression("<script type=\"text/javascript\">"))),
+				new CodeExpressionStatement(new CodeMethodInvokeExpression(
+					Void.class,
+					new CodeThisReferenceExpression(),
+					"writeGlobalData",
+					new CodeVariableReferenceExpression(DuelContext.class, "output"),
+					new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), DataEncoder.class, "encoder_4"),
+					new CodePrimitiveExpression(false))),
+				new CodeExpressionStatement(new CodeMethodInvokeExpression(
+					Void.class,
+					new CodeVariableReferenceExpression(DuelContext.class, "output"),
+					"append",
+					new CodePrimitiveExpression("duel.write(function(data) { foo.bar = (baz+data); }, "))),
 				new CodeExpressionStatement(new CodeMethodInvokeExpression(
 					Void.class,
 					new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), DataEncoder.class, "encoder_4"),
 		);
 
 		CodeTypeDeclaration actual = new CodeDOMBuilder().buildView(input);
-System.out.println(expected);
-System.err.println(actual);
 		assertEquals(expected, actual);
 	}
 }