|
Re: Are Tests First Class Clients?
|
Posted: Jan 26, 2005 9:34 AM
|
|
Here's the code:
package com.artima.nextgen.webmvc;
import org.apache.velocity.context.Context;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.runtime.RuntimeSingleton;
import org.suiterunner.Suite;
import org.suiterunner.TestFailedException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.*;
import com.artima.website.ArtimaConstants;
/**
* Contains a template name and context map, with which merges can be executed.
* The merges can be executed in two ways: the <code>merge</code> and
* <code>toString</code> methods. The <code>merge</code> method writes
* the merge output to a passed <code>Writer</code>. The <code>toString</code>
* method writes returns the output of the merge as a <code>String</code>.
* <p/>
* <p/>
* This class can be placed inside a context map passed to a different Merger,
* because its <code>toString</code> method does a merge and returns the result
* as a <code>String</code>. This allows controllers to build documents as trees
* of <code>Merger</code>s.
*/
public class Merger {
// I pass a Map to the constructors, not a Velocity context, because I want
// to do a defensive copy of the contents of the map. Since I am
// going to do a defensive copy anyway, there's no need to have
// controllers married to the velocity Context interface at all.
// The reason I want to do a defensive copy is because people could
// add in to the context passed in a copy of this Merger, which would
// result in a cycle in the tree. That would cause the JVM to run out
// of stack space when attempting a merge. Not a good thing. By doing it
// this way, I don't even need to check for cycles here in the constructor,
// because they are by definition impossible. You can't add this Merger to
// the contextMap passed to this Merger's constructor before this Merger's
// constructor has completed. Such a feat would require time travel. - bv 11/9/2004
//
private String templateName;
private Context context;
/**
* Construct a new <code>Merger</code>. The passed contest map
* may contain other <code>Merger</code>s.
*
* @param templateName the name of the template
* @param contextMap a context map
* @throws NullPointerException if passed <code>templateName</code> or <code>contextMap</code>
* is <code>null</code>
*/
public Merger(String templateName, Map contextMap) {
initialize(templateName, contextMap);
}
private void initialize(String templateName, Map contextMap) {
if (templateName == null || contextMap == null) {
throw new NullPointerException();
}
this.templateName = templateName;
context = new VelocityContext();
for (Iterator it = contextMap.keySet().iterator(); it.hasNext();) {
String key = (String) it.next();
Object value = contextMap.get(key);
context.put(key, value);
}
}
/**
* Construct a new <code>Merger</code> with passed resource bundle and context
* map. The resource bundle must contain the template name under the key
* <code>"templateName"</code>. The passed contest map
* may contain other <code>Merger</code>s.
*
* @param bundle a <code>ResourceBundle</code> containing the name of the
* template under the key "templateName".
* @param contextMap a context map
* @throws NullPointerException if passed <code>bundle</code> or <code>contextMap</code>
* is <code>null</code>.
* @throws java.util.MissingResourceException
* if passed <code>bundle</code> does not
* contain a value for the key <code>"templateName"</code>.
* @throws IllegalArgumentException if the passed <code>bundle</code> contains a value
* for the
*/
public Merger(ResourceBundle bundle, Map contextMap) {
if (bundle == null || contextMap == null) {
throw new NullPointerException();
}
Object o = bundle.getObject(ArtimaConstants.TEMPLATE_NAME);
String templateName = null;
try {
templateName = (String) o;
}
catch (ClassCastException e) {
IllegalArgumentException iae = new IllegalArgumentException("Value in bundle for templateName should be a String but is a: "
+ o.getClass().getName());
iae.initCause(e);
throw iae;
}
initialize(templateName, contextMap);
}
/**
* Merge the template and context contained in this <code>Merger</code>,
* writing the output to the passed <code>Writer</code>.
*
* @param writer the Writer to which to write the merge output
*/
void merge(Writer writer) {
try {
Template tmpl = RuntimeSingleton.getTemplate(templateName);
tmpl.merge(context, writer);
}
catch (Exception e) {
throw new RuntimeException("Missing template: " + templateName, e);
}
}
/**
* Merges the template and context contained in this <code>Merger</code>,
* returning the result as a <code>String</code>. This
* <code>toString()</code> implementation enables Merger's to be embedded in
* Contexts.
*
* @return the result of the merge as a String
*/
public String toString() {
StringWriter writer = new StringWriter();
merge(writer);
return writer.toString();
}
public boolean equals(Object o) {
boolean eq = false;
if (o != null) {
if (o.getClass() == Merger.class) {
Merger m = (Merger) o;
if (m.templateName.equals(templateName)) {
if (areContextsEqual(m.context, context)) {
eq = true;
}
}
}
}
return eq;
}
private static boolean areContextsEqual(Context a, Context b) {
if (a == null || b == null) {
throw new NullPointerException();
}
boolean eq = true;
Object[] aKeys = a.getKeys();
Object[] bKeys = b.getKeys();
if (aKeys.length == bKeys.length) {
for (int i = 0; i < aKeys.length; i++) {
String aKey = (String) aKeys[i];
if (b.containsKey(aKey)) {
if (!(a.get(aKey).equals(b.get(aKey)))) {
eq = false;
break;
}
}
else {
eq = false;
break;
}
}
}
else {
eq = false;
}
return eq;
}
// For unit tests. Eventually, I'd like to make this private.
public Map getContext() {
Object[] keys = context.getKeys();
Map map = new HashMap();
for (int i = 0; i < keys.length; i++) {
String key = (String) keys[i];
map.put(key, context.get(key));
}
return map;
}
// For unit tests. Delete after making sure Frank didn't use this in the mean time.
public String getTemplateName() {
return templateName;
}
/**
* Verify the context contained in this <code>Merger</code> is as expected. This method is
* intended to be used with unit tests.
*
* @param expected the expected context map.
* @throws org.suiterunner.TestFailedException
* if the context map is not as expected
* @throws NullPointerException if the passed <code>expected</code> <code>Map</code>
* is <code>null</code>
*/
public void verifyContext(Map expected) {
if (expected == null) {
throw new NullPointerException("Expected context Map cannot be null.");
}
verifyMapsEqual(expected, getContext());
}
/**
* Verify the template name is as expected. This method is intended to be used with unit tests.
*
* @param expected the expected template name.
* @throws org.suiterunner.TestFailedException
* if the redirect URL is not as expected
* @throws NullPointerException if the passed <code>expected</code> <code>String</code>
* is <code>null</code>
*/
public void verifyTemplateName(String expected) {
if (expected == null) {
throw new NullPointerException("Expected templateName cannot be null.");
}
Suite.verify(templateName.equals(expected), "templateName should have been: "
+ expected + ", but was: " + templateName);
}
private static void verifyMapsEqual(Map expected, Map actual) {
if (expected == null || actual == null) {
throw new NullPointerException();
}
Set expectedKeys = expected.keySet();
Set actualKeys = actual.keySet();
if (expectedKeys.equals(actualKeys)) {
for (Iterator it = expectedKeys.iterator(); it.hasNext();) {
String key = (String) it.next();
Object expectedVal = expected.get(key);
Object actualVal = actual.get(key);
if (!expectedVal.getClass().isArray()) {
if (!expectedVal.equals(actualVal)) {
String msg = printMapsToString(expectedKeys, actualKeys, "Context key mismatch.");
throw new TestFailedException(msg);
}
}
else {
Object[] expectedArr = (Object[]) expectedVal;
Object[] actualArr = (Object[]) actualVal;
if (expectedArr.length == actualArr.length) {
for (int i = 0; i < expectedArr.length; i++) {
Object expectedEle = expectedArr[i];
Object actualEle = actualArr[i];
if (!expectedEle.equals(actualEle)) {
String msg = printMapsToString(expectedKeys, actualKeys, "Array elements not equal.");
throw new TestFailedException(msg);
}
}
}
else {
String msg = printMapsToString(expectedKeys, actualKeys, "Context key mismatch.");
throw new TestFailedException("Arrays are different lengths.");
}
}
}
}
else {
String msg = printMapsToString(expectedKeys, actualKeys, "Context key mismatch.");
throw new TestFailedException(msg);
}
}
private static String printMapsToString(Set expectedKeys, Set actualKeys, String message) {
StringBuffer buf = new StringBuffer();
buf.append(message);
buf.append(" ");
buf.append("expectedKeys.size() is ");
buf.append(expectedKeys.size());
buf.append(", actualKeys.size() is ");
buf.append(actualKeys.size());
buf.append(". expectedKeys.toString() is ");
buf.append(expectedKeys.toString());
buf.append(". actualKeys.toString() is ");
buf.append(actualKeys.toString());
buf.append(".");
return buf.toString();
}
String printToString(String msg) {
if (msg == null) {
throw new NullPointerException();
}
StringBuffer buf = new StringBuffer();
buf.append(msg);
buf.append(" ");
buf.append(printMergerToString(this));
return buf.toString();
}
private static String printMergerToString(Merger merger) {
StringBuffer buf = new StringBuffer();
buf.append("(");
Map map = merger.getContext();
for (Iterator it = map.keySet().iterator(); it.hasNext();) {
Object key = (Object) it.next();
Object val = map.get(key);
buf.append(key.toString());
buf.append(":");
if (val instanceof Merger) {
String s = printMergerToString((Merger) val);
buf.append(s);
}
else {
String s = val.toString();
buf.append(s);
buf.append(" ");
}
}
buf.append(")");
return buf.toString();
}
}
Critique away...
|
|