azkaban-aplcache

Details

diff --git a/src/java/azkaban/project/Project.java b/src/java/azkaban/project/Project.java
index 316b11b..e17720a 100644
--- a/src/java/azkaban/project/Project.java
+++ b/src/java/azkaban/project/Project.java
@@ -136,12 +136,9 @@ public class Project {
 		Map<String, Object> projectObject = (Map<String, Object>) object;
 		String name = (String) projectObject.get("name");
 		String description = (String) projectObject.get("description");
-		String lastModifiedUser = (String) projectObject
-				.get("lastModifiedUser");
-		long createTimestamp = coerceToLong(projectObject
-				.get("createTimestamp"));
-		long lastModifiedTimestamp = coerceToLong(projectObject
-				.get("lastModifiedTimestamp"));
+		String lastModifiedUser = (String) projectObject.get("lastModifiedUser");
+		long createTimestamp = coerceToLong(projectObject.get("createTimestamp"));
+		long lastModifiedTimestamp = coerceToLong(projectObject.get("lastModifiedTimestamp"));
 		String source = (String)projectObject.get("source");
 		
 		Project project = new Project(name);
@@ -191,20 +188,12 @@ public class Project {
 	public int hashCode() {
 		final int prime = 31;
 		int result = 1;
-		result = prime * result
-				+ (int) (createTimestamp ^ (createTimestamp >>> 32));
-		result = prime * result
-				+ ((description == null) ? 0 : description.hashCode());
-		result = prime
-				* result
-				+ (int) (lastModifiedTimestamp ^ (lastModifiedTimestamp >>> 32));
-		result = prime
-				* result
-				+ ((lastModifiedUser == null) ? 0 : lastModifiedUser.hashCode());
+		result = prime * result + (int) (createTimestamp ^ (createTimestamp >>> 32));
+		result = prime * result + ((description == null) ? 0 : description.hashCode());
+		result = prime  * result + (int) (lastModifiedTimestamp ^ (lastModifiedTimestamp >>> 32));
+		result = prime * result + ((lastModifiedUser == null) ? 0 : lastModifiedUser.hashCode());
 		result = prime * result + ((name == null) ? 0 : name.hashCode());
-		result = prime
-				* result
-				+ ((userToPermission == null) ? 0 : userToPermission.hashCode());
+		result = prime * result + ((userToPermission == null) ? 0 : userToPermission.hashCode());
 		return result;
 	}
 
@@ -216,14 +205,17 @@ public class Project {
 			return false;
 		if (getClass() != obj.getClass())
 			return false;
+
 		Project other = (Project) obj;
 		if (createTimestamp != other.createTimestamp)
 			return false;
+
 		if (description == null) {
 			if (other.description != null)
 				return false;
 		} else if (!description.equals(other.description))
 			return false;
+
 		if (lastModifiedTimestamp != other.lastModifiedTimestamp)
 			return false;
 		if (lastModifiedUser == null) {
diff --git a/src/java/azkaban/project/ProjectManager.java b/src/java/azkaban/project/ProjectManager.java
index d135d94..43f654c 100644
--- a/src/java/azkaban/project/ProjectManager.java
+++ b/src/java/azkaban/project/ProjectManager.java
@@ -7,20 +7,20 @@ import azkaban.user.User;
 import azkaban.utils.Props;
 
 public interface ProjectManager {
-    
-    public List<String> getProjectNames();
-    
-    public List<Project> getProjects(User user);
-    
-    public void commitProject(String name) throws ProjectManagerException;
-    
-    public Project getProject(String name, User user);
-    
-    public void uploadProject(String projectName, File projectDir, User uploader, boolean force) throws ProjectManagerException;
-    
-    public Project createProject(String projectName, String description, User creator) throws ProjectManagerException;
-    
-    public Project removeProject(String projectName, User user) throws ProjectManagerException;
-    
-    public Props getProperties(String projectName, String source, User user) throws ProjectManagerException;
+
+	public List<String> getProjectNames();
+
+	public List<Project> getProjects(User user);
+
+	public void commitProject(String name) throws ProjectManagerException;
+
+	public Project getProject(String name, User user);
+
+	public void uploadProject(String projectName, File projectDir, User uploader, boolean force) throws ProjectManagerException;
+
+	public Project createProject(String projectName, String description, User creator) throws ProjectManagerException;
+
+	public Project removeProject(String projectName, User user) throws ProjectManagerException;
+
+	public Props getProperties(String projectName, String source, User user) throws ProjectManagerException;
 }
\ No newline at end of file
diff --git a/src/java/azkaban/user/XmlUserManager.java b/src/java/azkaban/user/XmlUserManager.java
index 8e0b30e..21968a4 100644
--- a/src/java/azkaban/user/XmlUserManager.java
+++ b/src/java/azkaban/user/XmlUserManager.java
@@ -18,20 +18,19 @@ import org.xml.sax.SAXException;
 import azkaban.utils.Props;
 
 /**
- * Xml implementation of the UserManager.
- * Looks for the property user.manager.xml.file in the azkaban properties.
+ * Xml implementation of the UserManager. Looks for the property
+ * user.manager.xml.file in the azkaban properties.
  * 
- * The xml  to be in the following form:
- * <azkaban-users>
- *   	<user username="username" password="azkaban" roles="admin" groups="azkaban"/>
+ * The xml to be in the following form: <azkaban-users> <user
+ * username="username" password="azkaban" roles="admin" groups="azkaban"/>
  * </azkaban-users>
  * 
  * @author rpark
- *
+ * 
  */
 public class XmlUserManager implements UserManager {
-    private static final Logger logger = Logger.getLogger(XmlUserManager.class.getName());
-	
+	private static final Logger logger = Logger.getLogger(XmlUserManager.class.getName());
+
 	public static final String XML_FILE_PARAM = "user.manager.xml.file";
 	public static final String AZKABAN_USERS_TAG = "azkaban-users";
 	public static final String USER_TAG = "user";
@@ -43,10 +42,10 @@ public class XmlUserManager implements UserManager {
 	public static final String GROUPS_ATTR = "groups";
 
 	private String xmlPath;
-	
+
 	private HashMap<String, User> users;
 	private HashMap<String, String> userPassword;
-	
+
 	/**
 	 * The constructor.
 	 * 
@@ -54,41 +53,48 @@ public class XmlUserManager implements UserManager {
 	 */
 	public XmlUserManager(Props props) {
 		xmlPath = props.getString(XML_FILE_PARAM);
-		
+
 		parseXMLFile();
 	}
-	
+
 	private void parseXMLFile() {
 		File file = new File(xmlPath);
 		if (!file.exists()) {
-			throw new IllegalArgumentException("User xml file " + xmlPath + " doesn't exist.");
+			throw new IllegalArgumentException("User xml file " + xmlPath
+					+ " doesn't exist.");
 		}
-		
+
 		HashMap<String, User> users = new HashMap<String, User>();
 		HashMap<String, String> userPassword = new HashMap<String, String>();
-		
+
 		// Creating the document builder to parse xml.
-		DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
+		DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory
+				.newInstance();
 		DocumentBuilder builder = null;
 		try {
 			builder = docBuilderFactory.newDocumentBuilder();
 		} catch (ParserConfigurationException e) {
-			throw new IllegalArgumentException("Exception while parsing user xml. Document builder not created.", e);
+			throw new IllegalArgumentException(
+					"Exception while parsing user xml. Document builder not created.",
+					e);
 		}
-		
+
 		Document doc = null;
 		try {
 			doc = builder.parse(file);
 		} catch (SAXException e) {
-			throw new IllegalArgumentException("Exception while parsing " + xmlPath + ". Invalid XML.", e);
+			throw new IllegalArgumentException("Exception while parsing "
+					+ xmlPath + ". Invalid XML.", e);
 		} catch (IOException e) {
-			throw new IllegalArgumentException("Exception while parsing " + xmlPath + ". Error reading file.", e);
+			throw new IllegalArgumentException("Exception while parsing "
+					+ xmlPath + ". Error reading file.", e);
 		}
-		
-		// Only look at first item, because we should only be seeing azkaban-users tag.
+
+		// Only look at first item, because we should only be seeing
+		// azkaban-users tag.
 		NodeList tagList = doc.getChildNodes();
 		Node azkabanUsers = tagList.item(0);
-		
+
 		NodeList azkabanUsersList = azkabanUsers.getChildNodes();
 		for (int i = 0; i < azkabanUsersList.getLength(); ++i) {
 			Node node = azkabanUsersList.item(i);
@@ -98,25 +104,28 @@ public class XmlUserManager implements UserManager {
 				}
 			}
 		}
-		
+
 		// Synchronize the swap. Similarly, the gets are synchronized to this.
-		synchronized(this) {
+		synchronized (this) {
 			this.users = users;
 			this.userPassword = userPassword;
 		}
 	}
-	
+
 	private void parseUserTag(Node node, HashMap<String, User> users, HashMap<String, String> userPassword) {
 		NamedNodeMap userAttrMap = node.getAttributes();
 		Node userNameAttr = userAttrMap.getNamedItem(USERNAME_ATTR);
 		if (userNameAttr == null) {
-			throw new RuntimeException("Error loading user. The username doesn't exist");
+			throw new RuntimeException(
+					"Error loading user. The username doesn't exist");
 		}
 		Node passwordAttr = userAttrMap.getNamedItem(PASSWORD_ATTR);
 		if (passwordAttr == null) {
-			throw new RuntimeException("Error loading user. The password doesn't exist for " + passwordAttr);
+			throw new RuntimeException(
+					"Error loading user. The password doesn't exist for "
+							+ passwordAttr);
 		}
-		
+
 		// Add user to the user/password map
 		String username = userNameAttr.getNodeValue();
 		String password = passwordAttr.getNodeValue();
@@ -125,50 +134,52 @@ public class XmlUserManager implements UserManager {
 		User user = new User(userNameAttr.getNodeValue());
 		users.put(username, user);
 		logger.info("Loading user " + user.getUserId());
-		
+
 		Node roles = userAttrMap.getNamedItem(ROLES_ATTR);
 		if (roles != null) {
 			String value = roles.getNodeValue();
 			String[] roleSplit = value.split("\\s*,\\s*");
-			for (String role: roleSplit) {
+			for (String role : roleSplit) {
 				user.addRole(role);
 			}
 		}
-		
+
 		Node groups = userAttrMap.getNamedItem(GROUPS_ATTR);
 		if (groups != null) {
 			String value = groups.getNodeValue();
 			String[] groupSplit = value.split("\\s*,\\s*");
-			for (String group: groupSplit) {
+			for (String group : groupSplit) {
 				user.addGroup(group);
 			}
 		}
 	}
-	
+
 	@Override
 	public User getUser(String username, String password) throws UserManagerException {
 		if (username == null || username.trim().isEmpty()) {
 			throw new UserManagerException("Username is empty.");
-		}
-		else if (password == null || password.trim().isEmpty()) {
+		} else if (password == null || password.trim().isEmpty()) {
 			throw new UserManagerException("Password is empty.");
 		}
-		
-		// Minimize the synchronization of the get. Shouldn't matter if it doesn't exist. 
+
+		// Minimize the synchronization of the get. Shouldn't matter if it
+		// doesn't exist.
 		String foundPassword = null;
 		User user = null;
-		synchronized(this) {
+		synchronized (this) {
 			foundPassword = userPassword.get(username);
 			if (foundPassword != null) {
 				user = users.get(username);
 			}
 		}
-		
+
 		if (foundPassword == null || !foundPassword.equals(password)) {
 			throw new UserManagerException("Username/Password not found.");
 		}
-		// Once it gets to this point, no exception has been thrown. User shoudn't be
-		// null, but adding this check for if user and user/password hash tables go
+		// Once it gets to this point, no exception has been thrown. User
+		// shoudn't be
+		// null, but adding this check for if user and user/password hash tables
+		// go
 		// out of sync.
 		if (user == null) {
 			throw new UserManagerException("Internal error: User not found.");
diff --git a/src/java/azkaban/utils/JSONUtils.java b/src/java/azkaban/utils/JSONUtils.java
index b99a5a4..80ebde6 100644
--- a/src/java/azkaban/utils/JSONUtils.java
+++ b/src/java/azkaban/utils/JSONUtils.java
@@ -1,6 +1,5 @@
 package azkaban.utils;
 
-
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -19,11 +18,11 @@ public class JSONUtils {
 	 */
 	private JSONUtils() {
 	}
-	
+
 	public static String toJSON(Object obj) {
 		return toJSON(obj, false);
 	}
-	
+
 	public static String toJSON(Object obj, boolean prettyPrint) {
 		ObjectMapper mapper = new ObjectMapper();
 
@@ -37,28 +36,28 @@ public class JSONUtils {
 			throw new RuntimeException(e);
 		}
 	}
-	
+
 	public static Object parseJSONFromString(String json) throws IOException {
 		ObjectMapper mapper = new ObjectMapper();
-    	JsonFactory factory = new JsonFactory();
-    	JsonParser parser = factory.createJsonParser(json);
+		JsonFactory factory = new JsonFactory();
+		JsonParser parser = factory.createJsonParser(json);
 		JsonNode node = mapper.readTree(parser);
-		
+
 		return toObjectFromJSONNode(node);
 	}
-	
+
 	public static Object parseJSONFromFile(File file) throws IOException {
 		ObjectMapper mapper = new ObjectMapper();
-    	JsonFactory factory = new JsonFactory();
-    	JsonParser parser = factory.createJsonParser(file);
+		JsonFactory factory = new JsonFactory();
+		JsonParser parser = factory.createJsonParser(file);
 		JsonNode node = mapper.readTree(parser);
-		
+
 		return toObjectFromJSONNode(node);
 	}
-	
+
 	private static Object toObjectFromJSONNode(JsonNode node) {
 		if (node.isObject()) {
-			HashMap<String, Object> obj = new HashMap<String,Object>();
+			HashMap<String, Object> obj = new HashMap<String, Object>();
 			Iterator<String> iter = node.getFieldNames();
 			while (iter.hasNext()) {
 				String fieldName = iter.next();
@@ -66,10 +65,9 @@ public class JSONUtils {
 				Object subObj = toObjectFromJSONNode(subNode);
 				obj.put(fieldName, subObj);
 			}
-			
+
 			return obj;
-		}
-		else if (node.isArray()) {
+		} else if (node.isArray()) {
 			ArrayList<Object> array = new ArrayList<Object>();
 			Iterator<JsonNode> iter = node.getElements();
 			while (iter.hasNext()) {
@@ -78,29 +76,23 @@ public class JSONUtils {
 				array.add(subObject);
 			}
 			return array;
-		}
-		else if (node.isTextual()) {
+		} else if (node.isTextual()) {
 			return node.asText();
-		}
-		else if (node.isNumber()) {
+		} else if (node.isNumber()) {
 			if (node.isInt()) {
 				return node.asInt();
-			}
-			else if (node.isLong()) {
+			} else if (node.isLong()) {
 				return node.asLong();
-			}
-			else if (node.isDouble()) {
+			} else if (node.isDouble()) {
 				return node.asDouble();
-			}
-			else {
-				System.err.println("ERROR What is this!? " + node.getNumberType());
+			} else {
+				System.err.println("ERROR What is this!? "
+						+ node.getNumberType());
 				return null;
 			}
-		}
-		else if (node.isBoolean()) {
+		} else if (node.isBoolean()) {
 			return node.asBoolean();
-		}
-		else {
+		} else {
 			return null;
 		}
 	}
diff --git a/src/java/azkaban/utils/Pair.java b/src/java/azkaban/utils/Pair.java
index 867f845..0d85d02 100644
--- a/src/java/azkaban/utils/Pair.java
+++ b/src/java/azkaban/utils/Pair.java
@@ -3,16 +3,16 @@ package azkaban.utils;
 public class Pair<F, S> {
 	private final F first;
 	private final S second;
-	
+
 	public Pair(F first, S second) {
 		this.first = first;
 		this.second = second;
 	}
-	
+
 	public F getFirst() {
 		return first;
 	}
-	
+
 	public S getSecond() {
 		return second;
 	}
diff --git a/src/java/azkaban/utils/Props.java b/src/java/azkaban/utils/Props.java
index dad659b..6dd0a5e 100644
--- a/src/java/azkaban/utils/Props.java
+++ b/src/java/azkaban/utils/Props.java
@@ -42,931 +42,924 @@ import org.apache.log4j.Logger;
  * functions and Exception throwing. This class is not threadsafe.
  */
 public class Props {
-    private final Map<String, String> _current;
-    private Props _parent;
-    private String source = null;
-
-    /**
-     * Constructor for empty props with empty parent.
-     */
-    public Props() {
-        this(null);
-    }
-
-    /**
-     * Constructor for empty Props with parent override.
-     * 
-     * @param parent
-     */
-    public Props(Props parent) {
-        this._current = new HashMap<String, String>();
-        this._parent = parent;
-    }
-
-    /**
-     * Load props from a file.
-     * 
-     * @param parent
-     * @param file
-     * @throws IOException
-     */
-    public Props(Props parent, String filepath) throws IOException {
-        this(parent, new File(filepath));
-    }
-    
-    /**
-     * Load props from a file.
-     * 
-     * @param parent
-     * @param file
-     * @throws IOException
-     */
-    public Props(Props parent, File file) throws IOException {
-        this(parent);
-        setSource(file.getPath());
-
-        InputStream input = new BufferedInputStream(new FileInputStream(file));
-        try {
-            loadFrom(input);
-        }
-        catch (IOException e) {
-            input.close();
-            throw e;
-        }
-        input.close();
-    }
-    
-    /**
-     * Create props from property input streams
-     * 
-     * @param parent
-     * @param inputStreams
-     * @throws IOException
-     */
-    public Props(Props parent, InputStream inputStream) throws IOException {
-        this(parent);
-        loadFrom(inputStream);
-    }
-
-    /**
-     * 
-     * @param inputStream
-     * @throws IOException
-     */
-    private void loadFrom(InputStream inputStream) throws IOException {
-        Properties properties = new Properties();
-        properties.load(inputStream);
-        this.put(properties);
-    }
-
-    /**
-     * Create properties from maps of properties
-     * 
-     * @param parent
-     * @param props
-     */
-    public Props(Props parent, Map<String, String>... props) {
-        this(parent);
-        for (int i = props.length - 1; i >= 0; i--) {
-            this.putAll(props[i]);
-        }
-    }
-
-    /**
-     * Create properties from Properties objects
-     * 
-     * @param parent
-     * @param properties
-     */
-    public Props(Props parent, Properties... properties) {
-        this(parent);
-        for (int i = properties.length - 1; i >= 0; i--) {
-            this.put(properties[i]);
-        }
-    }
-
-    /**
-     * Create a Props object with the contents set to that of props.
-     * 
-     * @param parent
-     * @param props
-     */
-    public Props(Props parent, Props props) {
-        this(parent);
-        if (props != null) {
-            putAll(props);
-        }
-    }
-
-    /**
-     * Create a Props with a null parent from a list of key value pairing. i.e.
-     * [key1, value1, key2, value2 ...]
-     * 
-     * @param args
-     * @return
-     */
-    public static Props of(String... args) {
-        return of((Props) null, args);
-    }
-
-    /**
-     * Create a Props from a list of key value pairing. i.e. [key1, value1,
-     * key2, value2 ...]
-     * 
-     * @param args
-     * @return
-     */
-    @SuppressWarnings("unchecked")
-    public static Props of(Props parent, String... args) {
-        if (args.length % 2 != 0) {
-            throw new IllegalArgumentException("Must have an equal number of keys and values.");
-        }
-
-        Map<String, String> vals = new HashMap<String, String>(args.length / 2);
-
-        for (int i = 0; i < args.length; i += 2) {
-            vals.put(args[i], args[i + 1]);
-        }
-        return new Props(parent, vals);
-    }
-
-    /**
-     * Clear the current Props, but leaves the parent untouched.
-     */
-    public void clearLocal() {
-        _current.clear();
-    }
-
-    /**
-     * Check key in current Props then search in parent
-     * 
-     * @param k
-     * @return
-     */
-    public boolean containsKey(Object k) {
-        return _current.containsKey(k) || (_parent != null && _parent.containsKey(k));
-    }
-
-    /**
-     * Check value in current Props then search in parent
-     * 
-     * @param value
-     * @return
-     */
-    public boolean containsValue(Object value) {
-        return _current.containsValue(value) || (_parent != null && _parent.containsValue(value));
-    }
-
-    /**
-     * Return value if available in current Props otherwise return from parent
-     * 
-     * @param key
-     * @return
-     */
-    public String get(Object key) {
-        if (_current.containsKey(key)) {
-            return _current.get(key);
-        }
-        else if (_parent != null) {
-            return _parent.get(key);
-        }
-        else {
-            return null;
-        }
-    }
-
-    /**
-     * Get the key set from the current Props
-     * 
-     * @return
-     */
-    public Set<String> localKeySet() {
-        return _current.keySet();
-    }
-
-    /**
-     * Get parent Props
-     * 
-     * @return
-     */
-    public Props getParent() {
-        return _parent;
-    }
-
-    /**
-     * Put the given string value for the string key. This method performs any
-     * variable substitution in the value replacing any occurance of ${name}
-     * with the value of get("name").
-     * 
-     * @param key
-     *            The key to put the value to
-     * @param value
-     *            The value to do substitution on and store
-     * 
-     * @throws IllegalArgumentException
-     *             If the variable given for substitution is not a valid key in
-     *             this Props.
-     */
-    public String put(String key, String value) {
-        return _current.put(key, value);
-    }
-
-    /**
-     * Put the given Properties into the Props. This method performs any
-     * variable substitution in the value replacing any occurrence of ${name}
-     * with the value of get("name"). get() is called first on the Props and
-     * next on the Properties object.
-     * 
-     * @param properties
-     *            The properties to put
-     * 
-     * @throws IllegalArgumentException
-     *             If the variable given for substitution is not a valid key in
-     *             this Props.
-     */
-    public void put(Properties properties) {
-        for (String propName : properties.stringPropertyNames()) {
-            _current.put(propName, properties.getProperty(propName));
-        }
-    }
-
-    /**
-     * Put integer
-     * 
-     * @param key
-     * @param value
-     * @return
-     */
-    public String put(String key, Integer value) {
-        return _current.put(key, value.toString());
-    }
-
-    /**
-     * Put Long. Stores as String.
-     * 
-     * @param key
-     * @param value
-     * @return
-     */
-    public String put(String key, Long value) {
-        return _current.put(key, value.toString());
-    }
-
-    /**
-     * Put Double. Stores as String.
-     * 
-     * @param key
-     * @param value
-     * @return
-     */
-    public String put(String key, Double value) {
-        return _current.put(key, value.toString());
-    }
-
-    /**
-     * Put everything in the map into the props.
-     * 
-     * @param m
-     */
-    public void putAll(Map<? extends String, ? extends String> m) {
-        if (m == null) {
-            return;
-        }
-
-        for (Map.Entry<? extends String, ? extends String> entry : m.entrySet())
-        {
-            this.put(entry.getKey(), entry.getValue());
-        }
-    }
-
-    /**
-     * Put all properties in the props into the current props. Will handle null
-     * p.
-     * 
-     * @param p
-     */
-    public void putAll(Props p) {
-        if (p == null) {
-            return;
-        }
-
-        for (String key : p.getKeySet()) {
-            this.put(key, p.get(key));
-        }
-    }
-
-    /**
-     * Puts only the local props from p into the current properties
-     * 
-     * @param p
-     */
-    public void putLocal(Props p) {
-        for (String key : p.localKeySet()) {
-            this.put(key, p.get(key));
-        }
-    }
-
-    /**
-     * Remove only the local value of key s, and not the parents.
-     * 
-     * @param s
-     * @return
-     */
-    public String removeLocal(Object s) {
-        return _current.remove(s);
-    }
-
-    /**
-     * The number of unique keys defined by this Props and all parent Props
-     */
-    public int size() {
-        return getKeySet().size();
-    }
-
-    /**
-     * The number of unique keys defined by this Props (keys defined only in
-     * parent Props are not counted)
-     */
-    public int localSize() {
-        return _current.size();
-    }
-
-    /**
-     * Attempts to return the Class that corresponds to the Props value. If the
-     * class doesn't exit, an IllegalArgumentException will be thrown.
-     * 
-     * @param key
-     * @return
-     */
-    public Class<?> getClass(String key) {
-        try {
-            if (containsKey(key)) {
-                return Class.forName(get(key));
-            }
-            else {
-                throw new UndefinedPropertyException("Missing required property '" + key + "'");
-            }
-        }
-        catch (ClassNotFoundException e) {
-            throw new IllegalArgumentException(e);
-        }
-    }
-
-    /**
-     * Gets the class from the Props. If it doesn't exist, it will return the
-     * defaultClass
-     * 
-     * @param key
-     * @param c
-     * @return
-     */
-    public Class<?> getClass(String key, Class<?> defaultClass) {
-        if (containsKey(key)) {
-            return getClass(key);
-        }
-        else {
-            return defaultClass;
-        }
-    }
-
-    /**
-     * Gets the string from the Props. If it doesn't exist, it will return the
-     * defaultValue
-     * 
-     * @param key
-     * @param defaultValue
-     * @return
-     */
-    public String getString(String key, String defaultValue) {
-        if (containsKey(key)) {
-            return get(key);
-        }
-        else {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * Gets the string from the Props. If it doesn't exist, throw and
-     * UndefinedPropertiesException
-     * 
-     * @param key
-     * @param defaultValue
-     * @return
-     */
-    public String getString(String key) {
-        if (containsKey(key)) {
-            return get(key);
-        }
-        else {
-            throw new UndefinedPropertyException("Missing required property '" + key + "'");
-        }
-    }
-
-    /**
-     * Returns a list of strings with the comma as the separator of the value
-     * 
-     * @param key
-     * @return
-     */
-    public List<String> getStringList(String key) {
-        return getStringList(key, "\\s*,\\s*");
-    }
-
-    /**
-     * Returns a list of strings with the sep as the separator of the value
-     * 
-     * @param key
-     * @param sep
-     * @return
-     */
-    public List<String> getStringList(String key, String sep) {
-        String val = get(key);
-        if (val == null || val.trim().length() == 0) {
-            return Collections.emptyList();
-        }
-
-        if (containsKey(key)) {
-            return Arrays.asList(val.split(sep));
-        }
-        else {
-            throw new UndefinedPropertyException("Missing required property '" + key + "'");
-        }
-    }
-
-    /**
-     * Returns a list of strings with the comma as the separator of the value.
-     * If the value is null, it'll return the defaultValue.
-     * 
-     * @param key
-     * @return
-     */
-    public List<String> getStringList(String key, List<String> defaultValue) {
-        if (containsKey(key)) {
-            return getStringList(key);
-        }
-        else {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * Returns a list of strings with the sep as the separator of the value. If
-     * the value is null, it'll return the defaultValue.
-     * 
-     * @param key
-     * @return
-     */
-    public List<String> getStringList(String key, List<String> defaultValue, String sep) {
-        if (containsKey(key)) {
-            return getStringList(key, sep);
-        }
-        else {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * Returns true if the value equals "true". If the value is null, then the
-     * default value is returned.
-     * 
-     * @param key
-     * @param defaultValue
-     * @return
-     */
-    public boolean getBoolean(String key, boolean defaultValue) {
-        if (containsKey(key)) {
-            return "true".equalsIgnoreCase(get(key).trim());
-        }
-        else {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * Returns true if the value equals "true". If the value is null, then an
-     * UndefinedPropertyException is thrown.
-     * 
-     * @param key
-     * @return
-     */
-    public boolean getBoolean(String key) {
-        if (containsKey(key)) return "true".equalsIgnoreCase(get(key));
-        else throw new UndefinedPropertyException("Missing required property '" + key + "'");
-    }
-
-    /**
-     * Returns the long representation of the value. If the value is null, then
-     * the default value is returned. If the value isn't a long, then a parse
-     * exception will be thrown.
-     * 
-     * @param key
-     * @param defaultValue
-     * @return
-     */
-    public long getLong(String name, long defaultValue) {
-        if (containsKey(name)) {
-            return Long.parseLong(get(name));
-        }
-        else {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * Returns the long representation of the value. If the value is null, then
-     * a UndefinedPropertyException will be thrown. If the value isn't a long,
-     * then a parse exception will be thrown.
-     * 
-     * @param key
-     * @return
-     */
-    public long getLong(String name) {
-        if (containsKey(name)) {
-            return Long.parseLong(get(name));
-        }
-        else {
-            throw new UndefinedPropertyException("Missing required property '" + name + "'");
-        }
-    }
-
-    /**
-     * Returns the int representation of the value. If the value is null, then
-     * the default value is returned. If the value isn't a int, then a parse
-     * exception will be thrown.
-     * 
-     * @param key
-     * @param defaultValue
-     * @return
-     */
-    public int getInt(String name, int defaultValue) {
-        if (containsKey(name)) {
-            return Integer.parseInt(get(name).trim());
-        }
-        else {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * Returns the int representation of the value. If the value is null, then a
-     * UndefinedPropertyException will be thrown. If the value isn't a int, then
-     * a parse exception will be thrown.
-     * 
-     * @param key
-     * @return
-     */
-    public int getInt(String name) {
-        if (containsKey(name)) {
-            return Integer.parseInt(get(name).trim());
-        }
-        else {
-            throw new UndefinedPropertyException("Missing required property '" + name + "'");
-        }
-    }
-
-    /**
-     * Returns the double representation of the value. If the value is null,
-     * then the default value is returned. If the value isn't a double, then a
-     * parse exception will be thrown.
-     * 
-     * @param key
-     * @param defaultValue
-     * @return
-     */
-    public double getDouble(String name, double defaultValue) {
-        if (containsKey(name)) {
-            return Double.parseDouble(get(name).trim());
-        }
-        else {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * Returns the double representation of the value. If the value is null,
-     * then a UndefinedPropertyException will be thrown. If the value isn't a
-     * double, then a parse exception will be thrown.
-     * 
-     * @param key
-     * @return
-     */
-    public double getDouble(String name) {
-        if (containsKey(name)) {
-            return Double.parseDouble(get(name).trim());
-        }
-        else {
-            throw new UndefinedPropertyException("Missing required property '" + name + "'");
-        }
-    }
-
-    /**
-     * Returns the uri representation of the value. If the value is null, then
-     * the default value is returned. If the value isn't a uri, then a
-     * IllegalArgumentException will be thrown.
-     * 
-     * @param key
-     * @param defaultValue
-     * @return
-     */
-    public URI getUri(String name) {
-        if (containsKey(name)) {
-            try {
-                return new URI(get(name));
-            }
-            catch (URISyntaxException e) {
-                throw new IllegalArgumentException(e.getMessage());
-            }
-        }
-        else {
-            throw new UndefinedPropertyException("Missing required property '" + name + "'");
-        }
-    }
-
-    /**
-     * Returns the double representation of the value. If the value is null,
-     * then the default value is returned. If the value isn't a uri, then a
-     * IllegalArgumentException will be thrown.
-     * 
-     * @param key
-     * @param defaultValue
-     * @return
-     */
-    public URI getUri(String name, URI defaultValue) {
-        if (containsKey(name)) {
-            return getUri(name);
-        }
-        else {
-            return defaultValue;
-        }
-    }
-
-    public URI getUri(String name, String defaultValue) {
-        try {
-            return getUri(name, new URI(defaultValue));
-        }
-        catch (URISyntaxException e) {
-            throw new IllegalArgumentException(e.getMessage());
-        }
-    }
-
-    /**
-     * Store only those properties defined at this local level
-     * 
-     * @param file
-     *            The file to write to
-     * @throws IOException
-     *             If the file can't be found or there is an io error
-     */
-    public void storeLocal(File file) throws IOException {
-        BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
-        try {
-            storeLocal(out);
-        }
-        finally {
-            out.close();
-        }
-    }
-
-    /**
-     * Returns a copy of only the local values of this props
-     * 
-     * @return
-     */
-    @SuppressWarnings("unchecked")
-    public Props local() {
-        return new Props(null, _current);
-    }
-
-    /**
-     * Store only those properties defined at this local level
-     * 
-     * @param out
-     *            The output stream to write to
-     * @throws IOException
-     *             If the file can't be found or there is an io error
-     */
-    public void storeLocal(OutputStream out) throws IOException {
-        Properties p = new Properties();
-        for (String key : _current.keySet()) {
-            p.setProperty(key, get(key));
-        }
-        p.store(out, null);
-    }
-
-    /**
-     * Returns a java.util.Properties file populated with the stuff in here.
-     * 
-     * @return
-     */
-    public Properties toProperties() {
-        Properties p = new Properties();
-        for (String key : _current.keySet()) {
-            p.setProperty(key, get(key));
-        }
-
-        return p;
-    }
-
-    /**
-     * Store all properties, those local and also those in parent props
-     * 
-     * @param file
-     *            The file to store to
-     * @throws IOException
-     *             If there is an error writing
-     */
-    public void storeFlattened(File file) throws IOException {
-        BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
-        try {
-            storeFlattened(out);
-        }
-        finally {
-            out.close();
-        }
-    }
-
-    /**
-     * Store all properties, those local and also those in parent props
-     * 
-     * @param out
-     *            The stream to write to
-     * @throws IOException
-     *             If there is an error writing
-     */
-    public void storeFlattened(OutputStream out) throws IOException {
-        Properties p = new Properties();
-        for (Props curr = this; curr != null; curr = curr.getParent()) {
-            for (String key : curr.localKeySet()) {
-                if (!p.containsKey(key)) {
-                    p.setProperty(key, get(key));
-                }
-            }
-        }
-
-        p.store(out, null);
-    }
-
-    /**
-     * Get a map of all properties by string prefix
-     * 
-     * @param prefix
-     *            The string prefix
-     */
-    public Map<String, String> getMapByPrefix(String prefix) {
-        Map<String, String> values = new HashMap<String, String>();
-
-        if (_parent != null) {
-            for (Map.Entry<String, String> entry : _parent.getMapByPrefix(prefix).entrySet())
-            {
-                values.put(entry.getKey(), entry.getValue());
-            }
-        }
-
-        for (String key : this.localKeySet()) {
-            if (key.startsWith(prefix)) {
-                values.put(key.substring(prefix.length()), get(key));
-            }
-        }
-        return values;
-    }
-
-    /**
-     * Returns a set of all keys, including the parents
-     * @return
-     */
-    public Set<String> getKeySet() {
-        HashSet<String> keySet = new HashSet<String>();
-
-        keySet.addAll(localKeySet());
-
-        if (_parent != null) {
-            keySet.addAll(_parent.getKeySet());
-        }
-
-        return keySet;
-    }
-
-    /**
-     * Logs the property in the given logger
-     * 
-     * @param logger
-     * @param comment
-     */
-    public void logProperties(Logger logger, String comment) {
-        logger.info(comment);
-
-        for (String key : getKeySet()) {
-            logger.info("  key=" + key + " value=" + get(key));
-        }
-    }
-
-    /**
-     * Clones the Props p object and all of its parents.
-     * 
-     * @param p
-     * @return
-     */
-    public static Props clone(Props p) {
-        return copyNext(p);
-    }
-
-    /**
-     * 
-     * @param source
-     * @return
-     */
-    private static Props copyNext(Props source) {
-        Props priorNodeCopy = null;
-        if (source.getParent() != null) {
-            priorNodeCopy = copyNext(source.getParent());
-        }
-        Props dest = new Props(priorNodeCopy);
-        for (String key : source.localKeySet()) {
-            dest.put(key, source.get(key));
-        }
-
-        return dest;
-    }
-
-    /**
-     */
-    @Override
-    public boolean equals(Object o) {
-        if (o == this) {
-            return true;
-        }
-        else if (o == null) {
-            return false;
-        }
-        else if (o.getClass() != Props.class) {
-            return false;
-        }
-
-        Props p = (Props) o;
-        return _current.equals(p._current) && Utils.equals(this._parent, p._parent);
-    }
-
-    /**
-     * Returns true if the properties are equivalent, regardless of the hierarchy.
-     * 
-     * @param p
-     * @return
-     */
-    public boolean equalsProps(Props p) {
-        if (p == null) {
-            return false;
-        }
-
-        final Set<String> myKeySet = getKeySet();
-        for (String s : myKeySet) {
-            if (!get(s).equals(p.get(s))) {
-                return false;
-            }
-        }
-
-        return myKeySet.size() == p.getKeySet().size();
-    }
-
-    /**
-     * 
-     */
-    @Override
-    public int hashCode() {
-        int code = this._current.hashCode();
-        if (_parent != null) code += _parent.hashCode();
-        return code;
-    }
-
-    /**
-     * 
-     */
-    @Override
-    public String toString() {
-        StringBuilder builder = new StringBuilder("{");
-        for (Map.Entry<String, String> entry : this._current.entrySet()) {
-            builder.append(entry.getKey());
-            builder.append(": ");
-            builder.append(entry.getValue());
-            builder.append(", ");
-        }
-        if (_parent != null) {
-            builder.append(" parent = ");
-            builder.append(_parent.toString());
-        }
-        builder.append("}");
-        return builder.toString();
-    }
-
-    public String getSource() {
-        return source;
-    }
-
-    public void setSource(String source) {
-        this.source = source;
-    }
-
-    public void setParent(Props prop) {
-    	this._parent = prop;
-    }
+	private final Map<String, String> _current;
+	private Props _parent;
+	private String source = null;
+
+	/**
+	 * Constructor for empty props with empty parent.
+	 */
+	public Props() {
+		this(null);
+	}
+
+	/**
+	 * Constructor for empty Props with parent override.
+	 * 
+	 * @param parent
+	 */
+	public Props(Props parent) {
+		this._current = new HashMap<String, String>();
+		this._parent = parent;
+	}
+
+	/**
+	 * Load props from a file.
+	 * 
+	 * @param parent
+	 * @param file
+	 * @throws IOException
+	 */
+	public Props(Props parent, String filepath) throws IOException {
+		this(parent, new File(filepath));
+	}
+
+	/**
+	 * Load props from a file.
+	 * 
+	 * @param parent
+	 * @param file
+	 * @throws IOException
+	 */
+	public Props(Props parent, File file) throws IOException {
+		this(parent);
+		setSource(file.getPath());
+
+		InputStream input = new BufferedInputStream(new FileInputStream(file));
+		try {
+			loadFrom(input);
+		} catch (IOException e) {
+			input.close();
+			throw e;
+		}
+		input.close();
+	}
+
+	/**
+	 * Create props from property input streams
+	 * 
+	 * @param parent
+	 * @param inputStreams
+	 * @throws IOException
+	 */
+	public Props(Props parent, InputStream inputStream) throws IOException {
+		this(parent);
+		loadFrom(inputStream);
+	}
+
+	/**
+	 * 
+	 * @param inputStream
+	 * @throws IOException
+	 */
+	private void loadFrom(InputStream inputStream) throws IOException {
+		Properties properties = new Properties();
+		properties.load(inputStream);
+		this.put(properties);
+	}
+
+	/**
+	 * Create properties from maps of properties
+	 * 
+	 * @param parent
+	 * @param props
+	 */
+	public Props(Props parent, Map<String, String>... props) {
+		this(parent);
+		for (int i = props.length - 1; i >= 0; i--) {
+			this.putAll(props[i]);
+		}
+	}
+
+	/**
+	 * Create properties from Properties objects
+	 * 
+	 * @param parent
+	 * @param properties
+	 */
+	public Props(Props parent, Properties... properties) {
+		this(parent);
+		for (int i = properties.length - 1; i >= 0; i--) {
+			this.put(properties[i]);
+		}
+	}
+
+	/**
+	 * Create a Props object with the contents set to that of props.
+	 * 
+	 * @param parent
+	 * @param props
+	 */
+	public Props(Props parent, Props props) {
+		this(parent);
+		if (props != null) {
+			putAll(props);
+		}
+	}
+
+	/**
+	 * Create a Props with a null parent from a list of key value pairing. i.e.
+	 * [key1, value1, key2, value2 ...]
+	 * 
+	 * @param args
+	 * @return
+	 */
+	public static Props of(String... args) {
+		return of((Props) null, args);
+	}
+
+	/**
+	 * Create a Props from a list of key value pairing. i.e. [key1, value1,
+	 * key2, value2 ...]
+	 * 
+	 * @param args
+	 * @return
+	 */
+	@SuppressWarnings("unchecked")
+	public static Props of(Props parent, String... args) {
+		if (args.length % 2 != 0) {
+			throw new IllegalArgumentException(
+					"Must have an equal number of keys and values.");
+		}
+
+		Map<String, String> vals = new HashMap<String, String>(args.length / 2);
+
+		for (int i = 0; i < args.length; i += 2) {
+			vals.put(args[i], args[i + 1]);
+		}
+		return new Props(parent, vals);
+	}
+
+	/**
+	 * Clear the current Props, but leaves the parent untouched.
+	 */
+	public void clearLocal() {
+		_current.clear();
+	}
+
+	/**
+	 * Check key in current Props then search in parent
+	 * 
+	 * @param k
+	 * @return
+	 */
+	public boolean containsKey(Object k) {
+		return _current.containsKey(k)
+				|| (_parent != null && _parent.containsKey(k));
+	}
+
+	/**
+	 * Check value in current Props then search in parent
+	 * 
+	 * @param value
+	 * @return
+	 */
+	public boolean containsValue(Object value) {
+		return _current.containsValue(value)
+				|| (_parent != null && _parent.containsValue(value));
+	}
+
+	/**
+	 * Return value if available in current Props otherwise return from parent
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public String get(Object key) {
+		if (_current.containsKey(key)) {
+			return _current.get(key);
+		} else if (_parent != null) {
+			return _parent.get(key);
+		} else {
+			return null;
+		}
+	}
+
+	/**
+	 * Get the key set from the current Props
+	 * 
+	 * @return
+	 */
+	public Set<String> localKeySet() {
+		return _current.keySet();
+	}
+
+	/**
+	 * Get parent Props
+	 * 
+	 * @return
+	 */
+	public Props getParent() {
+		return _parent;
+	}
+
+	/**
+	 * Put the given string value for the string key. This method performs any
+	 * variable substitution in the value replacing any occurance of ${name}
+	 * with the value of get("name").
+	 * 
+	 * @param key
+	 *            The key to put the value to
+	 * @param value
+	 *            The value to do substitution on and store
+	 * 
+	 * @throws IllegalArgumentException
+	 *             If the variable given for substitution is not a valid key in
+	 *             this Props.
+	 */
+	public String put(String key, String value) {
+		return _current.put(key, value);
+	}
+
+	/**
+	 * Put the given Properties into the Props. This method performs any
+	 * variable substitution in the value replacing any occurrence of ${name}
+	 * with the value of get("name"). get() is called first on the Props and
+	 * next on the Properties object.
+	 * 
+	 * @param properties
+	 *            The properties to put
+	 * 
+	 * @throws IllegalArgumentException
+	 *             If the variable given for substitution is not a valid key in
+	 *             this Props.
+	 */
+	public void put(Properties properties) {
+		for (String propName : properties.stringPropertyNames()) {
+			_current.put(propName, properties.getProperty(propName));
+		}
+	}
+
+	/**
+	 * Put integer
+	 * 
+	 * @param key
+	 * @param value
+	 * @return
+	 */
+	public String put(String key, Integer value) {
+		return _current.put(key, value.toString());
+	}
+
+	/**
+	 * Put Long. Stores as String.
+	 * 
+	 * @param key
+	 * @param value
+	 * @return
+	 */
+	public String put(String key, Long value) {
+		return _current.put(key, value.toString());
+	}
+
+	/**
+	 * Put Double. Stores as String.
+	 * 
+	 * @param key
+	 * @param value
+	 * @return
+	 */
+	public String put(String key, Double value) {
+		return _current.put(key, value.toString());
+	}
+
+	/**
+	 * Put everything in the map into the props.
+	 * 
+	 * @param m
+	 */
+	public void putAll(Map<? extends String, ? extends String> m) {
+		if (m == null) {
+			return;
+		}
+
+		for (Map.Entry<? extends String, ? extends String> entry : m.entrySet()) {
+			this.put(entry.getKey(), entry.getValue());
+		}
+	}
+
+	/**
+	 * Put all properties in the props into the current props. Will handle null
+	 * p.
+	 * 
+	 * @param p
+	 */
+	public void putAll(Props p) {
+		if (p == null) {
+			return;
+		}
+
+		for (String key : p.getKeySet()) {
+			this.put(key, p.get(key));
+		}
+	}
+
+	/**
+	 * Puts only the local props from p into the current properties
+	 * 
+	 * @param p
+	 */
+	public void putLocal(Props p) {
+		for (String key : p.localKeySet()) {
+			this.put(key, p.get(key));
+		}
+	}
+
+	/**
+	 * Remove only the local value of key s, and not the parents.
+	 * 
+	 * @param s
+	 * @return
+	 */
+	public String removeLocal(Object s) {
+		return _current.remove(s);
+	}
+
+	/**
+	 * The number of unique keys defined by this Props and all parent Props
+	 */
+	public int size() {
+		return getKeySet().size();
+	}
+
+	/**
+	 * The number of unique keys defined by this Props (keys defined only in
+	 * parent Props are not counted)
+	 */
+	public int localSize() {
+		return _current.size();
+	}
+
+	/**
+	 * Attempts to return the Class that corresponds to the Props value. If the
+	 * class doesn't exit, an IllegalArgumentException will be thrown.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public Class<?> getClass(String key) {
+		try {
+			if (containsKey(key)) {
+				return Class.forName(get(key));
+			} else {
+				throw new UndefinedPropertyException(
+						"Missing required property '" + key + "'");
+			}
+		} catch (ClassNotFoundException e) {
+			throw new IllegalArgumentException(e);
+		}
+	}
+
+	/**
+	 * Gets the class from the Props. If it doesn't exist, it will return the
+	 * defaultClass
+	 * 
+	 * @param key
+	 * @param c
+	 * @return
+	 */
+	public Class<?> getClass(String key, Class<?> defaultClass) {
+		if (containsKey(key)) {
+			return getClass(key);
+		} else {
+			return defaultClass;
+		}
+	}
+
+	/**
+	 * Gets the string from the Props. If it doesn't exist, it will return the
+	 * defaultValue
+	 * 
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public String getString(String key, String defaultValue) {
+		if (containsKey(key)) {
+			return get(key);
+		} else {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * Gets the string from the Props. If it doesn't exist, throw and
+	 * UndefinedPropertiesException
+	 * 
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public String getString(String key) {
+		if (containsKey(key)) {
+			return get(key);
+		} else {
+			throw new UndefinedPropertyException("Missing required property '"
+					+ key + "'");
+		}
+	}
+
+	/**
+	 * Returns a list of strings with the comma as the separator of the value
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public List<String> getStringList(String key) {
+		return getStringList(key, "\\s*,\\s*");
+	}
+
+	/**
+	 * Returns a list of strings with the sep as the separator of the value
+	 * 
+	 * @param key
+	 * @param sep
+	 * @return
+	 */
+	public List<String> getStringList(String key, String sep) {
+		String val = get(key);
+		if (val == null || val.trim().length() == 0) {
+			return Collections.emptyList();
+		}
+
+		if (containsKey(key)) {
+			return Arrays.asList(val.split(sep));
+		} else {
+			throw new UndefinedPropertyException("Missing required property '"
+					+ key + "'");
+		}
+	}
+
+	/**
+	 * Returns a list of strings with the comma as the separator of the value.
+	 * If the value is null, it'll return the defaultValue.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public List<String> getStringList(String key, List<String> defaultValue) {
+		if (containsKey(key)) {
+			return getStringList(key);
+		} else {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * Returns a list of strings with the sep as the separator of the value. If
+	 * the value is null, it'll return the defaultValue.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public List<String> getStringList(String key, List<String> defaultValue,
+			String sep) {
+		if (containsKey(key)) {
+			return getStringList(key, sep);
+		} else {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * Returns true if the value equals "true". If the value is null, then the
+	 * default value is returned.
+	 * 
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public boolean getBoolean(String key, boolean defaultValue) {
+		if (containsKey(key)) {
+			return "true".equalsIgnoreCase(get(key).trim());
+		} else {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * Returns true if the value equals "true". If the value is null, then an
+	 * UndefinedPropertyException is thrown.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public boolean getBoolean(String key) {
+		if (containsKey(key))
+			return "true".equalsIgnoreCase(get(key));
+		else
+			throw new UndefinedPropertyException("Missing required property '"
+					+ key + "'");
+	}
+
+	/**
+	 * Returns the long representation of the value. If the value is null, then
+	 * the default value is returned. If the value isn't a long, then a parse
+	 * exception will be thrown.
+	 * 
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public long getLong(String name, long defaultValue) {
+		if (containsKey(name)) {
+			return Long.parseLong(get(name));
+		} else {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * Returns the long representation of the value. If the value is null, then
+	 * a UndefinedPropertyException will be thrown. If the value isn't a long,
+	 * then a parse exception will be thrown.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public long getLong(String name) {
+		if (containsKey(name)) {
+			return Long.parseLong(get(name));
+		} else {
+			throw new UndefinedPropertyException("Missing required property '"
+					+ name + "'");
+		}
+	}
+
+	/**
+	 * Returns the int representation of the value. If the value is null, then
+	 * the default value is returned. If the value isn't a int, then a parse
+	 * exception will be thrown.
+	 * 
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public int getInt(String name, int defaultValue) {
+		if (containsKey(name)) {
+			return Integer.parseInt(get(name).trim());
+		} else {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * Returns the int representation of the value. If the value is null, then a
+	 * UndefinedPropertyException will be thrown. If the value isn't a int, then
+	 * a parse exception will be thrown.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public int getInt(String name) {
+		if (containsKey(name)) {
+			return Integer.parseInt(get(name).trim());
+		} else {
+			throw new UndefinedPropertyException("Missing required property '"
+					+ name + "'");
+		}
+	}
+
+	/**
+	 * Returns the double representation of the value. If the value is null,
+	 * then the default value is returned. If the value isn't a double, then a
+	 * parse exception will be thrown.
+	 * 
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public double getDouble(String name, double defaultValue) {
+		if (containsKey(name)) {
+			return Double.parseDouble(get(name).trim());
+		} else {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * Returns the double representation of the value. If the value is null,
+	 * then a UndefinedPropertyException will be thrown. If the value isn't a
+	 * double, then a parse exception will be thrown.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public double getDouble(String name) {
+		if (containsKey(name)) {
+			return Double.parseDouble(get(name).trim());
+		} else {
+			throw new UndefinedPropertyException("Missing required property '"
+					+ name + "'");
+		}
+	}
+
+	/**
+	 * Returns the uri representation of the value. If the value is null, then
+	 * the default value is returned. If the value isn't a uri, then a
+	 * IllegalArgumentException will be thrown.
+	 * 
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public URI getUri(String name) {
+		if (containsKey(name)) {
+			try {
+				return new URI(get(name));
+			} catch (URISyntaxException e) {
+				throw new IllegalArgumentException(e.getMessage());
+			}
+		} else {
+			throw new UndefinedPropertyException("Missing required property '"
+					+ name + "'");
+		}
+	}
+
+	/**
+	 * Returns the double representation of the value. If the value is null,
+	 * then the default value is returned. If the value isn't a uri, then a
+	 * IllegalArgumentException will be thrown.
+	 * 
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public URI getUri(String name, URI defaultValue) {
+		if (containsKey(name)) {
+			return getUri(name);
+		} else {
+			return defaultValue;
+		}
+	}
+
+	public URI getUri(String name, String defaultValue) {
+		try {
+			return getUri(name, new URI(defaultValue));
+		} catch (URISyntaxException e) {
+			throw new IllegalArgumentException(e.getMessage());
+		}
+	}
+
+	/**
+	 * Store only those properties defined at this local level
+	 * 
+	 * @param file
+	 *            The file to write to
+	 * @throws IOException
+	 *             If the file can't be found or there is an io error
+	 */
+	public void storeLocal(File file) throws IOException {
+		BufferedOutputStream out = new BufferedOutputStream(
+				new FileOutputStream(file));
+		try {
+			storeLocal(out);
+		} finally {
+			out.close();
+		}
+	}
+
+	/**
+	 * Returns a copy of only the local values of this props
+	 * 
+	 * @return
+	 */
+	@SuppressWarnings("unchecked")
+	public Props local() {
+		return new Props(null, _current);
+	}
+
+	/**
+	 * Store only those properties defined at this local level
+	 * 
+	 * @param out
+	 *            The output stream to write to
+	 * @throws IOException
+	 *             If the file can't be found or there is an io error
+	 */
+	public void storeLocal(OutputStream out) throws IOException {
+		Properties p = new Properties();
+		for (String key : _current.keySet()) {
+			p.setProperty(key, get(key));
+		}
+		p.store(out, null);
+	}
+
+	/**
+	 * Returns a java.util.Properties file populated with the stuff in here.
+	 * 
+	 * @return
+	 */
+	public Properties toProperties() {
+		Properties p = new Properties();
+		for (String key : _current.keySet()) {
+			p.setProperty(key, get(key));
+		}
+
+		return p;
+	}
+
+	/**
+	 * Store all properties, those local and also those in parent props
+	 * 
+	 * @param file
+	 *            The file to store to
+	 * @throws IOException
+	 *             If there is an error writing
+	 */
+	public void storeFlattened(File file) throws IOException {
+		BufferedOutputStream out = new BufferedOutputStream(
+				new FileOutputStream(file));
+		try {
+			storeFlattened(out);
+		} finally {
+			out.close();
+		}
+	}
+
+	/**
+	 * Store all properties, those local and also those in parent props
+	 * 
+	 * @param out
+	 *            The stream to write to
+	 * @throws IOException
+	 *             If there is an error writing
+	 */
+	public void storeFlattened(OutputStream out) throws IOException {
+		Properties p = new Properties();
+		for (Props curr = this; curr != null; curr = curr.getParent()) {
+			for (String key : curr.localKeySet()) {
+				if (!p.containsKey(key)) {
+					p.setProperty(key, get(key));
+				}
+			}
+		}
+
+		p.store(out, null);
+	}
+
+	/**
+	 * Get a map of all properties by string prefix
+	 * 
+	 * @param prefix
+	 *            The string prefix
+	 */
+	public Map<String, String> getMapByPrefix(String prefix) {
+		Map<String, String> values = new HashMap<String, String>();
+
+		if (_parent != null) {
+			for (Map.Entry<String, String> entry : _parent.getMapByPrefix(
+					prefix).entrySet()) {
+				values.put(entry.getKey(), entry.getValue());
+			}
+		}
+
+		for (String key : this.localKeySet()) {
+			if (key.startsWith(prefix)) {
+				values.put(key.substring(prefix.length()), get(key));
+			}
+		}
+		return values;
+	}
+
+	/**
+	 * Returns a set of all keys, including the parents
+	 * 
+	 * @return
+	 */
+	public Set<String> getKeySet() {
+		HashSet<String> keySet = new HashSet<String>();
+
+		keySet.addAll(localKeySet());
+
+		if (_parent != null) {
+			keySet.addAll(_parent.getKeySet());
+		}
+
+		return keySet;
+	}
+
+	/**
+	 * Logs the property in the given logger
+	 * 
+	 * @param logger
+	 * @param comment
+	 */
+	public void logProperties(Logger logger, String comment) {
+		logger.info(comment);
+
+		for (String key : getKeySet()) {
+			logger.info("  key=" + key + " value=" + get(key));
+		}
+	}
+
+	/**
+	 * Clones the Props p object and all of its parents.
+	 * 
+	 * @param p
+	 * @return
+	 */
+	public static Props clone(Props p) {
+		return copyNext(p);
+	}
+
+	/**
+	 * 
+	 * @param source
+	 * @return
+	 */
+	private static Props copyNext(Props source) {
+		Props priorNodeCopy = null;
+		if (source.getParent() != null) {
+			priorNodeCopy = copyNext(source.getParent());
+		}
+		Props dest = new Props(priorNodeCopy);
+		for (String key : source.localKeySet()) {
+			dest.put(key, source.get(key));
+		}
+
+		return dest;
+	}
+
+	/**
+     */
+	@Override
+	public boolean equals(Object o) {
+		if (o == this) {
+			return true;
+		} else if (o == null) {
+			return false;
+		} else if (o.getClass() != Props.class) {
+			return false;
+		}
+
+		Props p = (Props) o;
+		return _current.equals(p._current)
+				&& Utils.equals(this._parent, p._parent);
+	}
+
+	/**
+	 * Returns true if the properties are equivalent, regardless of the
+	 * hierarchy.
+	 * 
+	 * @param p
+	 * @return
+	 */
+	public boolean equalsProps(Props p) {
+		if (p == null) {
+			return false;
+		}
+
+		final Set<String> myKeySet = getKeySet();
+		for (String s : myKeySet) {
+			if (!get(s).equals(p.get(s))) {
+				return false;
+			}
+		}
+
+		return myKeySet.size() == p.getKeySet().size();
+	}
+
+	/**
+     * 
+     */
+	@Override
+	public int hashCode() {
+		int code = this._current.hashCode();
+		if (_parent != null)
+			code += _parent.hashCode();
+		return code;
+	}
+
+	/**
+     * 
+     */
+	@Override
+	public String toString() {
+		StringBuilder builder = new StringBuilder("{");
+		for (Map.Entry<String, String> entry : this._current.entrySet()) {
+			builder.append(entry.getKey());
+			builder.append(": ");
+			builder.append(entry.getValue());
+			builder.append(", ");
+		}
+		if (_parent != null) {
+			builder.append(" parent = ");
+			builder.append(_parent.toString());
+		}
+		builder.append("}");
+		return builder.toString();
+	}
+
+	public String getSource() {
+		return source;
+	}
+
+	public void setSource(String source) {
+		this.source = source;
+	}
+
+	public void setParent(Props prop) {
+		this._parent = prop;
+	}
 }
diff --git a/src/java/azkaban/utils/UndefinedPropertyException.java b/src/java/azkaban/utils/UndefinedPropertyException.java
index 758f4b1..a63aabb 100644
--- a/src/java/azkaban/utils/UndefinedPropertyException.java
+++ b/src/java/azkaban/utils/UndefinedPropertyException.java
@@ -17,14 +17,14 @@
 package azkaban.utils;
 
 /**
- * Indicates that a required property is missing from the Props 
+ * Indicates that a required property is missing from the Props
  */
 public class UndefinedPropertyException extends RuntimeException {
 
-    private static final long serialVersionUID = 1;
+	private static final long serialVersionUID = 1;
 
-    public UndefinedPropertyException(String message) {
-        super(message);
-    }
+	public UndefinedPropertyException(String message) {
+		super(message);
+	}
 
 }
diff --git a/src/java/azkaban/utils/Utils.java b/src/java/azkaban/utils/Utils.java
index 5d95084..c00951a 100644
--- a/src/java/azkaban/utils/Utils.java
+++ b/src/java/azkaban/utils/Utils.java
@@ -37,135 +37,141 @@ import org.apache.commons.io.IOUtils;
  * A util helper class full of static methods that are commonly used.
  */
 public class Utils {
-    public static final Random RANDOM = new Random();
-	
-    /**
-     * Private constructor.
-     */
-    private Utils() {
-    }
-
-    /**
-     * Equivalent to Object.equals except that it handles nulls. If a and b are
-     * both null, true is returned.
-     * 
-     * @param a
-     * @param b
-     * @return
-     */
-    public static boolean equals(Object a, Object b) {
-        if (a == null || b == null) {
-            return a == b;
-        }
-
-        return a.equals(b);
-    }
-
-    /**
-     * Return the object if it is non-null, otherwise throw an exception
-     * 
-     * @param <T>
-     *            The type of the object
-     * @param t
-     *            The object
-     * @return The object if it is not null
-     * @throws IllegalArgumentException
-     *             if the object is null
-     */
-    public static <T> T nonNull(T t) {
-        if (t == null) {
-            throw new IllegalArgumentException("Null value not allowed.");
-        }
-        else {
-            return t;
-        }
-    }
-    
-    /**
-     * Print the message and then exit with the given exit code
-     * 
-     * @param message The message to print
-     * @param exitCode The exit code
-     */
-    public static void croak(String message, int exitCode) {
-        System.err.println(message);
-        System.exit(exitCode);
-    }
-    
-    public static File createTempDir() {
-        return createTempDir(new File(System.getProperty("java.io.tmpdir")));
-    }
-
-    public static File createTempDir(File parent) {
-        File temp = new File(parent, Integer.toString(Math.abs(RANDOM.nextInt()) % 100000000));
-        temp.delete();
-        temp.mkdir();
-        temp.deleteOnExit();
-        return temp;
-    }
-    
-    public static void zip(File input, File output) throws IOException {
-        FileOutputStream out = new FileOutputStream(output);
-        ZipOutputStream zOut = new ZipOutputStream(out);
-        zipFile("", input, zOut);
-        zOut.close();
-    }
-
-    private static void zipFile(String path, File input, ZipOutputStream zOut) throws IOException {
-        if(input.isDirectory()) {
-            File[] files = input.listFiles();
-            if(files != null) {
-                for(File f: files) {
-                    String childPath = path + input.getName() + (f.isDirectory() ? "/" : "");
-                    zipFile(childPath, f, zOut);
-                }
-            }
-        } else {
-            String childPath = path + (path.length() > 0 ? "/" : "") + input.getName();
-            ZipEntry entry = new ZipEntry(childPath);
-            zOut.putNextEntry(entry);
-            InputStream fileInputStream = new BufferedInputStream(new FileInputStream(input));
-            IOUtils.copy(fileInputStream, zOut);
-            fileInputStream.close();
-        }
-    }
-
-    public static void unzip(ZipFile source, File dest) throws IOException {
-        Enumeration<?> entries = source.entries();
-        while(entries.hasMoreElements()) {
-            ZipEntry entry = (ZipEntry) entries.nextElement();
-            File newFile = new File(dest, entry.getName());
-            if(entry.isDirectory()) {
-                newFile.mkdirs();
-            } else {
-                newFile.getParentFile().mkdirs();
-                InputStream src = source.getInputStream(entry);
-                OutputStream output = new BufferedOutputStream(new FileOutputStream(newFile));
-                IOUtils.copy(src, output);
-                src.close();
-                output.close();
-            }
-        }
-    }
-
-    public static String flattenToString(Collection<?> collection, String delimiter) {
-    	StringBuffer buffer = new StringBuffer();
-    	for (Object obj: collection) {
-    		buffer.append(obj.toString());
-    		buffer.append(',');
-    	}
-    	
-    	if (buffer.length() > 0) {
-    		buffer.setLength(buffer.length() - 1);
-    	}
-    	return buffer.toString();
-    }
-    
-    public static Double convertToDouble(Object obj) {
-    	if (obj instanceof String) {
-    		return Double.parseDouble((String)obj);
-    	}
-    	
-    	return (Double)obj;
-    }
+	public static final Random RANDOM = new Random();
+
+	/**
+	 * Private constructor.
+	 */
+	private Utils() {
+	}
+
+	/**
+	 * Equivalent to Object.equals except that it handles nulls. If a and b are
+	 * both null, true is returned.
+	 * 
+	 * @param a
+	 * @param b
+	 * @return
+	 */
+	public static boolean equals(Object a, Object b) {
+		if (a == null || b == null) {
+			return a == b;
+		}
+
+		return a.equals(b);
+	}
+
+	/**
+	 * Return the object if it is non-null, otherwise throw an exception
+	 * 
+	 * @param <T>
+	 *            The type of the object
+	 * @param t
+	 *            The object
+	 * @return The object if it is not null
+	 * @throws IllegalArgumentException
+	 *             if the object is null
+	 */
+	public static <T> T nonNull(T t) {
+		if (t == null) {
+			throw new IllegalArgumentException("Null value not allowed.");
+		} else {
+			return t;
+		}
+	}
+
+	/**
+	 * Print the message and then exit with the given exit code
+	 * 
+	 * @param message
+	 *            The message to print
+	 * @param exitCode
+	 *            The exit code
+	 */
+	public static void croak(String message, int exitCode) {
+		System.err.println(message);
+		System.exit(exitCode);
+	}
+
+	public static File createTempDir() {
+		return createTempDir(new File(System.getProperty("java.io.tmpdir")));
+	}
+
+	public static File createTempDir(File parent) {
+		File temp = new File(parent,
+				Integer.toString(Math.abs(RANDOM.nextInt()) % 100000000));
+		temp.delete();
+		temp.mkdir();
+		temp.deleteOnExit();
+		return temp;
+	}
+
+	public static void zip(File input, File output) throws IOException {
+		FileOutputStream out = new FileOutputStream(output);
+		ZipOutputStream zOut = new ZipOutputStream(out);
+		zipFile("", input, zOut);
+		zOut.close();
+	}
+
+	private static void zipFile(String path, File input, ZipOutputStream zOut) throws IOException {
+		if (input.isDirectory()) {
+			File[] files = input.listFiles();
+			if (files != null) {
+				for (File f : files) {
+					String childPath = path + input.getName()
+							+ (f.isDirectory() ? "/" : "");
+					zipFile(childPath, f, zOut);
+				}
+			}
+		} else {
+			String childPath = path + (path.length() > 0 ? "/" : "")
+					+ input.getName();
+			ZipEntry entry = new ZipEntry(childPath);
+			zOut.putNextEntry(entry);
+			InputStream fileInputStream = new BufferedInputStream(
+					new FileInputStream(input));
+			IOUtils.copy(fileInputStream, zOut);
+			fileInputStream.close();
+		}
+	}
+
+	public static void unzip(ZipFile source, File dest) throws IOException {
+		Enumeration<?> entries = source.entries();
+		while (entries.hasMoreElements()) {
+			ZipEntry entry = (ZipEntry) entries.nextElement();
+			File newFile = new File(dest, entry.getName());
+			if (entry.isDirectory()) {
+				newFile.mkdirs();
+			} else {
+				newFile.getParentFile().mkdirs();
+				InputStream src = source.getInputStream(entry);
+				OutputStream output = new BufferedOutputStream(
+						new FileOutputStream(newFile));
+				IOUtils.copy(src, output);
+				src.close();
+				output.close();
+			}
+		}
+	}
+
+	public static String flattenToString(Collection<?> collection, String delimiter) {
+		StringBuffer buffer = new StringBuffer();
+		for (Object obj : collection) {
+			buffer.append(obj.toString());
+			buffer.append(',');
+		}
+
+		if (buffer.length() > 0) {
+			buffer.setLength(buffer.length() - 1);
+		}
+		return buffer.toString();
+	}
+
+	public static Double convertToDouble(Object obj) {
+		if (obj instanceof String) {
+			return Double.parseDouble((String) obj);
+		}
+
+		return (Double) obj;
+	}
 }
\ No newline at end of file
diff --git a/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java b/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
index c57fd8b..139b1e8 100644
--- a/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
+++ b/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
@@ -43,279 +43,305 @@ import azkaban.webapp.session.Session;
  * Base Servlet for pages
  */
 public abstract class AbstractAzkabanServlet extends HttpServlet {
-    private static final DateTimeFormatter ZONE_FORMATTER = DateTimeFormat.forPattern("z");
-    private static final String AZKABAN_SUCCESS_MESSAGE = "azkaban.success.message";
-    private static final String AZKABAN_FAILURE_MESSAGE = "azkaban.failure.message";
-    
-    private static final long serialVersionUID = -1;
-    public static final String DEFAULT_LOG_URL_PREFIX = "predefined_log_url_prefix";
-    public static final String LOG_URL_PREFIX = "log_url_prefix";
-    public static final String HTML_TYPE = "text/html";
-    public static final String XML_MIME_TYPE = "application/xhtml+xml";
-    public static final String JSON_MIME_TYPE = "application/json";
-    
-    private AzkabanWebServer application;
-    private String name;
-    private String label;
-    private String color;
-    
-    /**
-     * To retrieve the application for the servlet
-     * @return
-     */
-    public AzkabanWebServer getApplication() {
-        return application;
-    }
-
-    @Override
-    public void init(ServletConfig config) throws ServletException {
-        application = (AzkabanWebServer) config.getServletContext().getAttribute(
-                AzkabanServletContextListener.AZKABAN_SERVLET_CONTEXT_KEY);
-
-        if (application == null) {
-            throw new IllegalStateException("No batch application is defined in the servlet context!");
-        }
-        
-        Props props = application.getAzkabanProps();
-        name = props.getString("azkaban.name", "");
-        label = props.getString("azkaban.label", "");
-        color = props.getString("azkaban.color", "#FF0000");
-    }
-
-    /**
-     * Checks for the existance of the parameter in the request
-     * 
-     * @param request
-     * @param param
-     * @return
-     */
-    public boolean hasParam(HttpServletRequest request, String param) {
-        return request.getParameter(param) != null;
-    }
-
-    /**
-     * Retrieves the param from the http servlet request. Will throw an exception if not
-     * found
-     * 
-     * @param request
-     * @param name
-     * @return
-     * @throws ServletException
-     */
-    public String getParam(HttpServletRequest request, String name) throws ServletException {
-        String p = request.getParameter(name);
-        if (p == null) throw new ServletException("Missing required parameter '" + name + "'.");
-        else return p;
-    }
-
-    /**
-     * Returns the param and parses it into an int. Will throw an exception if not found, or
-     * a parse error if the type is incorrect.
-     * 
-     * @param request
-     * @param name
-     * @return
-     * @throws ServletException
-     */
-    public int getIntParam(HttpServletRequest request, String name) throws ServletException {
-        String p = getParam(request, name);
-        return Integer.parseInt(p);
-    }
-
-    /**
-     * Returns the session value of the request.
-     * 
-     * @param request
-     * @param key
-     * @param value
-     */
-    protected void setSessionValue(HttpServletRequest request, String key, Object value) {
-        request.getSession(true).setAttribute(key, value);
-    }
-
-    /**
-     * Adds a session value to the request
-     * 
-     * @param request
-     * @param key
-     * @param value
-     */
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    protected void addSessionValue(HttpServletRequest request, String key, Object value) {
-        List l = (List) request.getSession(true).getAttribute(key);
-        if (l == null) l = new ArrayList();
-        l.add(value);
-        request.getSession(true).setAttribute(key, l);
-    }
-
-    /**
-     * Sets an error message in azkaban.failure.message in the cookie. This will be used by the
-     * web client javascript to somehow display the message
-     * 
-     * @param response
-     * @param errorMsg
-     */
-    protected void setErrorMessageInCookie(HttpServletResponse response, String errorMsg) {
-        Cookie cookie = new Cookie(AZKABAN_FAILURE_MESSAGE, errorMsg);
-        response.addCookie(cookie);
-    }
-
-    /**
-     * Sets a message in azkaban.success.message in the cookie. This will be used by the
-     * web client javascript to somehow display the message
-     * 
-     * @param response
-     * @param errorMsg
-     */
-    protected void setSuccessMessageInCookie(HttpServletResponse response, String message) {
-        Cookie cookie = new Cookie(AZKABAN_SUCCESS_MESSAGE, message);
-        response.addCookie(cookie);
-    }
-    
-    /**
-     * Retrieves a success message from a cookie. azkaban.success.message
-     * @param request
-     * @return
-     */
-    protected String getSuccessMessageFromCookie(HttpServletRequest request) {
-        Cookie cookie = getCookieByName(request, AZKABAN_SUCCESS_MESSAGE);
-
-        if (cookie == null) {
-            return null;
-        }    
-        return cookie.getValue();
-    }
-    
-    /**
-     * Retrieves a success message from a cookie. azkaban.failure.message
-     * @param request
-     * @return
-     */
-    protected String getErrorMessageFromCookie(HttpServletRequest request) {
-        Cookie cookie = getCookieByName(request, AZKABAN_FAILURE_MESSAGE);
-        if (cookie == null) {
-            return null;
-        }
-
-        return cookie.getValue();
-    }
-    
-    /**
-     * Retrieves a cookie by name. Potential issue in performance if a lot of cookie
-     * variables are used.
-     * 
-     * @param request
-     * @return
-     */
-    protected Cookie getCookieByName(HttpServletRequest request, String name) {
-        Cookie[] cookies = request.getCookies();
-        if (cookies != null) {
-	        for(Cookie cookie : cookies) {
-	            if (name.equals(cookie.getName())) {
-	                return cookie;
-	            }
-	        }
-        }
-
-        return null;
-    }
-
-    /**
-     * Creates a new velocity page to use. With session.
-     * 
-     * @param req
-     * @param resp
-     * @param template
-     * @return
-     */
-    protected Page newPage(HttpServletRequest req, HttpServletResponse resp, Session session, String template) {
-        Page page = new Page(req, resp, application.getVelocityEngine(), template);
-        page.add("azkaban_name", name);
-        page.add("azkaban_label", label);
-        page.add("azkaban_color", color);
-        page.add("timezone", ZONE_FORMATTER.print(System.currentTimeMillis()));
-        page.add("currentTime",(new DateTime()).getMillis());
-        if (session != null && session.getUser() != null) {
-        	page.add("user_id", session.getUser().getUserId());
-        }
-        page.add("context", req.getContextPath());
-        
-        String errorMsg = getErrorMessageFromCookie(req);
-        page.add("error_message", errorMsg == null || errorMsg.isEmpty()? "null": errorMsg);
-        setErrorMessageInCookie(resp, null);
-        
-        String successMsg = getSuccessMessageFromCookie(req);
-        page.add("success_message", successMsg == null || successMsg.isEmpty()? "null":  successMsg);
-        setSuccessMessageInCookie(resp, null);
-
-        
-        return page;
-    }
-
-    /**
-     * Creates a new velocity page to use.
-     * 
-     * @param req
-     * @param resp
-     * @param template
-     * @return
-     */
-    protected Page newPage(HttpServletRequest req, HttpServletResponse resp, String template) {
-        Page page = new Page(req, resp, application.getVelocityEngine(), template);
-        page.add("azkaban_name", name);
-        page.add("azkaban_label", label);
-        page.add("azkaban_color", color);
-        page.add("timezone", ZONE_FORMATTER.print(System.currentTimeMillis()));
-        page.add("currentTime",(new DateTime()).getMillis());
-        page.add("context", req.getContextPath());
-        return page;
-    }
-
-    /**
-     * Writes json out to the stream.
-     * 
-     * @param resp
-     * @param obj
-     * @throws IOException
-     */
-    protected void writeJSON(HttpServletResponse resp, Object obj) throws IOException {
-        resp.setContentType(JSON_MIME_TYPE);
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.writeValue(resp.getOutputStream(), obj);
-    }
-    
-    /**
-     * Retrieve the Azkaban application
-     * @param config
-     * @return
-     */
-    public static AzkabanWebServer getApp(ServletConfig config) {
-        AzkabanWebServer app = (AzkabanWebServer) config
-                .getServletContext()
-                .getAttribute(
-                        AzkabanServletContextListener.AZKABAN_SERVLET_CONTEXT_KEY);
-
-        if (app == null) {
-            throw new IllegalStateException("No batch application is defined in the servlet context!");
-        }
-        else {
-            return app;
-        }
-    }
-    
-    public static String createJsonResponse(String status, String message, String action, Map<String, Object> params) {
-    	HashMap<String,Object> response = new HashMap<String,Object>();
-    	response.put("status", status);
-    	if (message != null) {
-    		response.put("message", message);
-    	}
-    	if (action != null) {
-    		response.put("action", action);
-    	}
-    	if (params != null) {
-    		response.putAll(params);
-    	}
-    	
-    	return JSONUtils.toJSON(response);
-    }
+	private static final DateTimeFormatter ZONE_FORMATTER = DateTimeFormat
+			.forPattern("z");
+	private static final String AZKABAN_SUCCESS_MESSAGE = "azkaban.success.message";
+	private static final String AZKABAN_FAILURE_MESSAGE = "azkaban.failure.message";
+
+	private static final long serialVersionUID = -1;
+	public static final String DEFAULT_LOG_URL_PREFIX = "predefined_log_url_prefix";
+	public static final String LOG_URL_PREFIX = "log_url_prefix";
+	public static final String HTML_TYPE = "text/html";
+	public static final String XML_MIME_TYPE = "application/xhtml+xml";
+	public static final String JSON_MIME_TYPE = "application/json";
+
+	private AzkabanWebServer application;
+	private String name;
+	private String label;
+	private String color;
+
+	/**
+	 * To retrieve the application for the servlet
+	 * 
+	 * @return
+	 */
+	public AzkabanWebServer getApplication() {
+		return application;
+	}
+
+	@Override
+	public void init(ServletConfig config) throws ServletException {
+		application = (AzkabanWebServer) config
+				.getServletContext()
+				.getAttribute(
+						AzkabanServletContextListener.AZKABAN_SERVLET_CONTEXT_KEY);
+
+		if (application == null) {
+			throw new IllegalStateException(
+					"No batch application is defined in the servlet context!");
+		}
+
+		Props props = application.getAzkabanProps();
+		name = props.getString("azkaban.name", "");
+		label = props.getString("azkaban.label", "");
+		color = props.getString("azkaban.color", "#FF0000");
+	}
+
+	/**
+	 * Checks for the existance of the parameter in the request
+	 * 
+	 * @param request
+	 * @param param
+	 * @return
+	 */
+	public boolean hasParam(HttpServletRequest request, String param) {
+		return request.getParameter(param) != null;
+	}
+
+	/**
+	 * Retrieves the param from the http servlet request. Will throw an
+	 * exception if not found
+	 * 
+	 * @param request
+	 * @param name
+	 * @return
+	 * @throws ServletException
+	 */
+	public String getParam(HttpServletRequest request, String name)
+			throws ServletException {
+		String p = request.getParameter(name);
+		if (p == null)
+			throw new ServletException("Missing required parameter '" + name
+					+ "'.");
+		else
+			return p;
+	}
+
+	/**
+	 * Returns the param and parses it into an int. Will throw an exception if
+	 * not found, or a parse error if the type is incorrect.
+	 * 
+	 * @param request
+	 * @param name
+	 * @return
+	 * @throws ServletException
+	 */
+	public int getIntParam(HttpServletRequest request, String name)
+			throws ServletException {
+		String p = getParam(request, name);
+		return Integer.parseInt(p);
+	}
+
+	/**
+	 * Returns the session value of the request.
+	 * 
+	 * @param request
+	 * @param key
+	 * @param value
+	 */
+	protected void setSessionValue(HttpServletRequest request, String key,
+			Object value) {
+		request.getSession(true).setAttribute(key, value);
+	}
+
+	/**
+	 * Adds a session value to the request
+	 * 
+	 * @param request
+	 * @param key
+	 * @param value
+	 */
+	@SuppressWarnings({ "unchecked", "rawtypes" })
+	protected void addSessionValue(HttpServletRequest request, String key,
+			Object value) {
+		List l = (List) request.getSession(true).getAttribute(key);
+		if (l == null)
+			l = new ArrayList();
+		l.add(value);
+		request.getSession(true).setAttribute(key, l);
+	}
+
+	/**
+	 * Sets an error message in azkaban.failure.message in the cookie. This will
+	 * be used by the web client javascript to somehow display the message
+	 * 
+	 * @param response
+	 * @param errorMsg
+	 */
+	protected void setErrorMessageInCookie(HttpServletResponse response,
+			String errorMsg) {
+		Cookie cookie = new Cookie(AZKABAN_FAILURE_MESSAGE, errorMsg);
+		response.addCookie(cookie);
+	}
+
+	/**
+	 * Sets a message in azkaban.success.message in the cookie. This will be
+	 * used by the web client javascript to somehow display the message
+	 * 
+	 * @param response
+	 * @param errorMsg
+	 */
+	protected void setSuccessMessageInCookie(HttpServletResponse response,
+			String message) {
+		Cookie cookie = new Cookie(AZKABAN_SUCCESS_MESSAGE, message);
+		response.addCookie(cookie);
+	}
+
+	/**
+	 * Retrieves a success message from a cookie. azkaban.success.message
+	 * 
+	 * @param request
+	 * @return
+	 */
+	protected String getSuccessMessageFromCookie(HttpServletRequest request) {
+		Cookie cookie = getCookieByName(request, AZKABAN_SUCCESS_MESSAGE);
+
+		if (cookie == null) {
+			return null;
+		}
+		return cookie.getValue();
+	}
+
+	/**
+	 * Retrieves a success message from a cookie. azkaban.failure.message
+	 * 
+	 * @param request
+	 * @return
+	 */
+	protected String getErrorMessageFromCookie(HttpServletRequest request) {
+		Cookie cookie = getCookieByName(request, AZKABAN_FAILURE_MESSAGE);
+		if (cookie == null) {
+			return null;
+		}
+
+		return cookie.getValue();
+	}
+
+	/**
+	 * Retrieves a cookie by name. Potential issue in performance if a lot of
+	 * cookie variables are used.
+	 * 
+	 * @param request
+	 * @return
+	 */
+	protected Cookie getCookieByName(HttpServletRequest request, String name) {
+		Cookie[] cookies = request.getCookies();
+		if (cookies != null) {
+			for (Cookie cookie : cookies) {
+				if (name.equals(cookie.getName())) {
+					return cookie;
+				}
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Creates a new velocity page to use. With session.
+	 * 
+	 * @param req
+	 * @param resp
+	 * @param template
+	 * @return
+	 */
+	protected Page newPage(HttpServletRequest req, HttpServletResponse resp,
+			Session session, String template) {
+		Page page = new Page(req, resp, application.getVelocityEngine(),
+				template);
+		page.add("azkaban_name", name);
+		page.add("azkaban_label", label);
+		page.add("azkaban_color", color);
+		page.add("timezone", ZONE_FORMATTER.print(System.currentTimeMillis()));
+		page.add("currentTime", (new DateTime()).getMillis());
+		if (session != null && session.getUser() != null) {
+			page.add("user_id", session.getUser().getUserId());
+		}
+		page.add("context", req.getContextPath());
+
+		String errorMsg = getErrorMessageFromCookie(req);
+		page.add("error_message",
+				errorMsg == null || errorMsg.isEmpty() ? "null" : errorMsg);
+		setErrorMessageInCookie(resp, null);
+
+		String successMsg = getSuccessMessageFromCookie(req);
+		page.add("success_message",
+				successMsg == null || successMsg.isEmpty() ? "null"
+						: successMsg);
+		setSuccessMessageInCookie(resp, null);
+
+		return page;
+	}
+
+	/**
+	 * Creates a new velocity page to use.
+	 * 
+	 * @param req
+	 * @param resp
+	 * @param template
+	 * @return
+	 */
+	protected Page newPage(HttpServletRequest req, HttpServletResponse resp,
+			String template) {
+		Page page = new Page(req, resp, application.getVelocityEngine(),
+				template);
+		page.add("azkaban_name", name);
+		page.add("azkaban_label", label);
+		page.add("azkaban_color", color);
+		page.add("timezone", ZONE_FORMATTER.print(System.currentTimeMillis()));
+		page.add("currentTime", (new DateTime()).getMillis());
+		page.add("context", req.getContextPath());
+		return page;
+	}
+
+	/**
+	 * Writes json out to the stream.
+	 * 
+	 * @param resp
+	 * @param obj
+	 * @throws IOException
+	 */
+	protected void writeJSON(HttpServletResponse resp, Object obj)
+			throws IOException {
+		resp.setContentType(JSON_MIME_TYPE);
+		ObjectMapper mapper = new ObjectMapper();
+		mapper.writeValue(resp.getOutputStream(), obj);
+	}
+
+	/**
+	 * Retrieve the Azkaban application
+	 * 
+	 * @param config
+	 * @return
+	 */
+	public static AzkabanWebServer getApp(ServletConfig config) {
+		AzkabanWebServer app = (AzkabanWebServer) config
+				.getServletContext()
+				.getAttribute(
+						AzkabanServletContextListener.AZKABAN_SERVLET_CONTEXT_KEY);
+
+		if (app == null) {
+			throw new IllegalStateException(
+					"No batch application is defined in the servlet context!");
+		} else {
+			return app;
+		}
+	}
+
+	public static String createJsonResponse(String status, String message,
+			String action, Map<String, Object> params) {
+		HashMap<String, Object> response = new HashMap<String, Object>();
+		response.put("status", status);
+		if (message != null) {
+			response.put("message", message);
+		}
+		if (action != null) {
+			response.put("action", action);
+		}
+		if (params != null) {
+			response.putAll(params);
+		}
+
+		return JSONUtils.toJSON(response);
+	}
 }
diff --git a/src/java/azkaban/webapp/servlet/AzkabanServletContextListener.java b/src/java/azkaban/webapp/servlet/AzkabanServletContextListener.java
index e1e99f7..f4a755b 100644
--- a/src/java/azkaban/webapp/servlet/AzkabanServletContextListener.java
+++ b/src/java/azkaban/webapp/servlet/AzkabanServletContextListener.java
@@ -25,23 +25,24 @@ import azkaban.webapp.AzkabanWebServer;
  * A ServletContextListener that loads the batch application
  */
 public class AzkabanServletContextListener implements ServletContextListener {
-    public static final String AZKABAN_SERVLET_CONTEXT_KEY = "azkaban_app";
-
-    private AzkabanWebServer app;
-
-    /**
-     * Delete the app
-     */
-    public void contextDestroyed(ServletContextEvent event) {
-        this.app = null;
-    }
-
-    /**
-     * Load the app
-     */
-    public void contextInitialized(ServletContextEvent event) {
-        this.app = new AzkabanWebServer();
-
-        event.getServletContext().setAttribute(AZKABAN_SERVLET_CONTEXT_KEY, this.app);
-    }
+	public static final String AZKABAN_SERVLET_CONTEXT_KEY = "azkaban_app";
+
+	private AzkabanWebServer app;
+
+	/**
+	 * Delete the app
+	 */
+	public void contextDestroyed(ServletContextEvent event) {
+		this.app = null;
+	}
+
+	/**
+	 * Load the app
+	 */
+	public void contextInitialized(ServletContextEvent event) {
+		this.app = new AzkabanWebServer();
+
+		event.getServletContext().setAttribute(AZKABAN_SERVLET_CONTEXT_KEY,
+				this.app);
+	}
 }
diff --git a/src/java/azkaban/webapp/servlet/IndexServlet.java b/src/java/azkaban/webapp/servlet/IndexServlet.java
index 0af2f2f..91ab4ba 100644
--- a/src/java/azkaban/webapp/servlet/IndexServlet.java
+++ b/src/java/azkaban/webapp/servlet/IndexServlet.java
@@ -34,33 +34,34 @@ import azkaban.webapp.session.Session;
  * The main page
  */
 public class IndexServlet extends LoginAbstractAzkabanServlet {
-    //private static final Logger logger = Logger.getLogger(IndexServlet.class.getName());
+	// private static final Logger logger =
+	// Logger.getLogger(IndexServlet.class.getName());
 
-    private static final long serialVersionUID = -1;
+	private static final long serialVersionUID = -1;
 
-    @Override
-    protected void handleGet(HttpServletRequest req, HttpServletResponse resp, Session session) throws ServletException,
-            IOException {
-    	User user = session.getUser();
-    	
-    	ProjectManager manager = this.getApplication().getProjectManager();
-    	List<Project> projects = manager.getProjects(user);
-        Page page = newPage(req, resp, session, "azkaban/webapp/servlet/velocity/index.vm");
-        page.add("projects", projects);
-        page.render();
-    }
+	@Override
+	protected void handleGet(HttpServletRequest req, HttpServletResponse resp,
+			Session session) throws ServletException, IOException {
+		User user = session.getUser();
 
-    @Override
-    protected void handlePost(HttpServletRequest req, HttpServletResponse resp, Session session)
-            throws ServletException, IOException {
-        if(hasParam(req, "action")) {
-        	String action = getParam(req, "action");
-        	if (action.equals("create")) {
-        		
-        	}
-        }
-        else {
-            resp.sendRedirect(req.getContextPath());
-        }
-    }
+		ProjectManager manager = this.getApplication().getProjectManager();
+		List<Project> projects = manager.getProjects(user);
+		Page page = newPage(req, resp, session,
+				"azkaban/webapp/servlet/velocity/index.vm");
+		page.add("projects", projects);
+		page.render();
+	}
+
+	@Override
+	protected void handlePost(HttpServletRequest req, HttpServletResponse resp,
+			Session session) throws ServletException, IOException {
+		if (hasParam(req, "action")) {
+			String action = getParam(req, "action");
+			if (action.equals("create")) {
+
+			}
+		} else {
+			resp.sendRedirect(req.getContextPath());
+		}
+	}
 }
diff --git a/src/java/azkaban/webapp/servlet/LoginAbstractAzkabanServlet.java b/src/java/azkaban/webapp/servlet/LoginAbstractAzkabanServlet.java
index 76ec115..f568ca3 100644
--- a/src/java/azkaban/webapp/servlet/LoginAbstractAzkabanServlet.java
+++ b/src/java/azkaban/webapp/servlet/LoginAbstractAzkabanServlet.java
@@ -21,81 +21,82 @@ import azkaban.webapp.session.Session;
  * verified.
  */
 public abstract class LoginAbstractAzkabanServlet extends
-        AbstractAzkabanServlet {
-
-    private static final long serialVersionUID = 1L;
-
-    private static final Logger logger = Logger
-            .getLogger(LoginAbstractAzkabanServlet.class.getName());
-    private static final String SESSION_ID_NAME = "azkaban.session.id";
-
-    @Override
-    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
-            throws ServletException, IOException {
-
-        // Set session id
-        Session session = getSessionFromRequest(req);
-        if (hasParam(req, "logout")) {
-            resp.sendRedirect(req.getContextPath());
-            if (session != null) {
-                getApplication().getSessionCache().removeSession(
-                        session.getSessionId());
-            }
-            return;
-        }
-
-        if (session != null) {
-            logger.info("Found session " + session.getUser());
-            handleGet(req, resp, session);
-        } else {
-            handleLogin(req, resp);
-        }
-    }
-
-    private Session getSessionFromRequest(HttpServletRequest req) {
-        Cookie cookie = getCookieByName(req, SESSION_ID_NAME);
-        String sessionId = null;
-
-        if (cookie != null) {
-            sessionId = cookie.getValue();
-            logger.info("Session id " + sessionId);
-        }
-        if (sessionId == null) {
-            return null;
-        } else {
-            return getApplication().getSessionCache().getSession(sessionId);
-        }
-    }
-
-    private void handleLogin(HttpServletRequest req, HttpServletResponse resp)
-            throws ServletException, IOException {
-        handleLogin(req, resp, null);
-    }
-
-    private void handleLogin(HttpServletRequest req, HttpServletResponse resp,
-            String errorMsg) throws ServletException, IOException {
-
-        Page page = newPage(req, resp, "azkaban/webapp/servlet/velocity/login.vm");
-        if (errorMsg != null) {
-            page.add("errorMsg", errorMsg);
-        }
-
-        page.render();
-    }
-
-    @Override
-    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
-            throws ServletException, IOException {
-        if (hasParam(req, "action")) {
-            String action = getParam(req, "action");
-            if (action.equals("login")) {
-                if (hasParam(req, "username") && hasParam(req, "password")) {
-                    String username = getParam(req, "username");
-                    String password = getParam(req, "password");
-
-                    UserManager manager = getApplication().getUserManager();
-
-                    User user = null;
+		AbstractAzkabanServlet {
+
+	private static final long serialVersionUID = 1L;
+
+	private static final Logger logger = Logger
+			.getLogger(LoginAbstractAzkabanServlet.class.getName());
+	private static final String SESSION_ID_NAME = "azkaban.session.id";
+
+	@Override
+	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+			throws ServletException, IOException {
+
+		// Set session id
+		Session session = getSessionFromRequest(req);
+		if (hasParam(req, "logout")) {
+			resp.sendRedirect(req.getContextPath());
+			if (session != null) {
+				getApplication().getSessionCache().removeSession(
+						session.getSessionId());
+			}
+			return;
+		}
+
+		if (session != null) {
+			logger.info("Found session " + session.getUser());
+			handleGet(req, resp, session);
+		} else {
+			handleLogin(req, resp);
+		}
+	}
+
+	private Session getSessionFromRequest(HttpServletRequest req) {
+		Cookie cookie = getCookieByName(req, SESSION_ID_NAME);
+		String sessionId = null;
+
+		if (cookie != null) {
+			sessionId = cookie.getValue();
+			logger.info("Session id " + sessionId);
+		}
+		if (sessionId == null) {
+			return null;
+		} else {
+			return getApplication().getSessionCache().getSession(sessionId);
+		}
+	}
+
+	private void handleLogin(HttpServletRequest req, HttpServletResponse resp)
+			throws ServletException, IOException {
+		handleLogin(req, resp, null);
+	}
+
+	private void handleLogin(HttpServletRequest req, HttpServletResponse resp,
+			String errorMsg) throws ServletException, IOException {
+
+		Page page = newPage(req, resp,
+				"azkaban/webapp/servlet/velocity/login.vm");
+		if (errorMsg != null) {
+			page.add("errorMsg", errorMsg);
+		}
+
+		page.render();
+	}
+
+	@Override
+	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+			throws ServletException, IOException {
+		if (hasParam(req, "action")) {
+			String action = getParam(req, "action");
+			if (action.equals("login")) {
+				if (hasParam(req, "username") && hasParam(req, "password")) {
+					String username = getParam(req, "username");
+					String password = getParam(req, "password");
+
+					UserManager manager = getApplication().getUserManager();
+
+					User user = null;
 					try {
 						user = manager.getUser(username, password);
 					} catch (UserManagerException e) {
@@ -103,89 +104,94 @@ public abstract class LoginAbstractAzkabanServlet extends
 						return;
 					}
 
-                    String randomUID = UUID.randomUUID().toString();
-                    Session session = new Session(randomUID, user);
-                    resp.addCookie(new Cookie(SESSION_ID_NAME, randomUID));
-                    getApplication().getSessionCache().addSession(session);
-                    handleGet(req, resp, session);
-                } else {
-                	if (isAjaxCall(req)) {
-                		String response = createJsonResponse("error", "Incorrect Login.", "login", null);
-                		writeResponse(resp, response);
-                	}
-                	else {
-                		handleLogin(req, resp, "Enter username and password");
-                	}
-                }
-            } else {
-                Session session = getSessionFromRequest(req);
-                if (session == null) {
-                	if (isAjaxCall(req)) {
-                		String response = createJsonResponse("error", "Invalid Session. Need to re-login", "login", null);
-                		writeResponse(resp, response);
-                	}
-                	else {
-                		handleLogin(req, resp, "Enter username and password");
-                	}
-                } else {
-                	handlePost(req, resp, session);
-                }
-            }
-        } else {
-            Session session = getSessionFromRequest(req);
-            if (session == null) {
-            	if (isAjaxCall(req)) {
-            		String response = createJsonResponse("error", "Invalid Session. Need to re-login", "login", null);
-            		writeResponse(resp, response);
-            	}
-            	else {
-            		handleLogin(req, resp, "Enter username and password");
-            	}
-            } else {
-            	handlePost(req, resp, session);
-            }
-        }
-    }
-    
-    protected void writeResponse(HttpServletResponse resp, String response) throws IOException {
-    	Writer writer = resp.getWriter();
-    	writer.append(response);
-    	writer.flush();
-    }
-    
-    protected boolean isAjaxCall(HttpServletRequest req) throws ServletException {
-    	String value = req.getHeader("X-Requested-With");
-    	if (value != null) {
-    		logger.info("has X-Requested-With " + value);
-     		return value.equals("XMLHttpRequest");
-    	}
-    	
-    	return false;
-    }
-    
-    /** 
-     * The get request is handed off to the implementor after the user is logged in.
-     * 
-     * @param req
-     * @param resp
-     * @param session
-     * @throws ServletException
-     * @throws IOException
-     */
-    protected abstract void handleGet(HttpServletRequest req,
-            HttpServletResponse resp, Session session) throws ServletException,
-            IOException;
-
-    /**
-     * The post request is handed off to the implementor after the user is logged in.
-     * 
-     * @param req
-     * @param resp
-     * @param session
-     * @throws ServletException
-     * @throws IOException
-     */
-    protected abstract void handlePost(HttpServletRequest req,
-            HttpServletResponse resp, Session session) throws ServletException,
-            IOException;
+					String randomUID = UUID.randomUUID().toString();
+					Session session = new Session(randomUID, user);
+					resp.addCookie(new Cookie(SESSION_ID_NAME, randomUID));
+					getApplication().getSessionCache().addSession(session);
+					handleGet(req, resp, session);
+				} else {
+					if (isAjaxCall(req)) {
+						String response = createJsonResponse("error",
+								"Incorrect Login.", "login", null);
+						writeResponse(resp, response);
+					} else {
+						handleLogin(req, resp, "Enter username and password");
+					}
+				}
+			} else {
+				Session session = getSessionFromRequest(req);
+				if (session == null) {
+					if (isAjaxCall(req)) {
+						String response = createJsonResponse("error",
+								"Invalid Session. Need to re-login", "login",
+								null);
+						writeResponse(resp, response);
+					} else {
+						handleLogin(req, resp, "Enter username and password");
+					}
+				} else {
+					handlePost(req, resp, session);
+				}
+			}
+		} else {
+			Session session = getSessionFromRequest(req);
+			if (session == null) {
+				if (isAjaxCall(req)) {
+					String response = createJsonResponse("error",
+							"Invalid Session. Need to re-login", "login", null);
+					writeResponse(resp, response);
+				} else {
+					handleLogin(req, resp, "Enter username and password");
+				}
+			} else {
+				handlePost(req, resp, session);
+			}
+		}
+	}
+
+	protected void writeResponse(HttpServletResponse resp, String response)
+			throws IOException {
+		Writer writer = resp.getWriter();
+		writer.append(response);
+		writer.flush();
+	}
+
+	protected boolean isAjaxCall(HttpServletRequest req)
+			throws ServletException {
+		String value = req.getHeader("X-Requested-With");
+		if (value != null) {
+			logger.info("has X-Requested-With " + value);
+			return value.equals("XMLHttpRequest");
+		}
+
+		return false;
+	}
+
+	/**
+	 * The get request is handed off to the implementor after the user is logged
+	 * in.
+	 * 
+	 * @param req
+	 * @param resp
+	 * @param session
+	 * @throws ServletException
+	 * @throws IOException
+	 */
+	protected abstract void handleGet(HttpServletRequest req,
+			HttpServletResponse resp, Session session) throws ServletException,
+			IOException;
+
+	/**
+	 * The post request is handed off to the implementor after the user is
+	 * logged in.
+	 * 
+	 * @param req
+	 * @param resp
+	 * @param session
+	 * @throws ServletException
+	 * @throws IOException
+	 */
+	protected abstract void handlePost(HttpServletRequest req,
+			HttpServletResponse resp, Session session) throws ServletException,
+			IOException;
 }
\ No newline at end of file
diff --git a/src/java/azkaban/webapp/servlet/MultipartParser.java b/src/java/azkaban/webapp/servlet/MultipartParser.java
index 66fb88f..234f986 100644
--- a/src/java/azkaban/webapp/servlet/MultipartParser.java
+++ b/src/java/azkaban/webapp/servlet/MultipartParser.java
@@ -31,32 +31,32 @@ import org.apache.commons.fileupload.servlet.ServletFileUpload;
 
 public class MultipartParser {
 
-    private DiskFileItemFactory _uploadItemFactory;
-
-    public MultipartParser(int spillToDiskSize) {
-        _uploadItemFactory = new DiskFileItemFactory();
-        _uploadItemFactory.setSizeThreshold(spillToDiskSize);
-    }
-
-    @SuppressWarnings("unchecked")
-	public Map<String, Object> parseMultipart(HttpServletRequest request) throws IOException,
-            ServletException {
-        ServletFileUpload upload = new ServletFileUpload(_uploadItemFactory);
-        List<FileItem> items = null;
-        try {
-            items = upload.parseRequest(request);
-        } catch(FileUploadException e) {
-            throw new ServletException(e);
-        }
-
-        Map<String, Object> params = new HashMap<String, Object>();
-        for(FileItem item: items) {
-            if(item.isFormField())
-                params.put(item.getFieldName(), item.getString());
-            else
-                params.put(item.getFieldName(), item);
-        }
-        return params;
-    }
+	private DiskFileItemFactory _uploadItemFactory;
+
+	public MultipartParser(int spillToDiskSize) {
+		_uploadItemFactory = new DiskFileItemFactory();
+		_uploadItemFactory.setSizeThreshold(spillToDiskSize);
+	}
+
+	@SuppressWarnings("unchecked")
+	public Map<String, Object> parseMultipart(HttpServletRequest request)
+			throws IOException, ServletException {
+		ServletFileUpload upload = new ServletFileUpload(_uploadItemFactory);
+		List<FileItem> items = null;
+		try {
+			items = upload.parseRequest(request);
+		} catch (FileUploadException e) {
+			throw new ServletException(e);
+		}
+
+		Map<String, Object> params = new HashMap<String, Object>();
+		for (FileItem item : items) {
+			if (item.isFormField())
+				params.put(item.getFieldName(), item.getString());
+			else
+				params.put(item.getFieldName(), item);
+		}
+		return params;
+	}
 
 }
diff --git a/src/java/azkaban/webapp/servlet/Page.java b/src/java/azkaban/webapp/servlet/Page.java
index 3f0029a..1c52bab 100644
--- a/src/java/azkaban/webapp/servlet/Page.java
+++ b/src/java/azkaban/webapp/servlet/Page.java
@@ -28,58 +28,57 @@ import azkaban.utils.Utils;
  * A page to display
  */
 public class Page {
-    private static final String DEFAULT_MIME_TYPE = "text/html";
-    @SuppressWarnings("unused")
+	private static final String DEFAULT_MIME_TYPE = "text/html";
+	@SuppressWarnings("unused")
 	private final HttpServletRequest request;
-    private final HttpServletResponse response;
-    private final VelocityEngine engine;
-    private final VelocityContext context;
-    private final String template;
-    private String mimeType = DEFAULT_MIME_TYPE;
-    private VelocityUtils utils = new VelocityUtils();
-    
-    /**
-     * Creates a page and sets up the velocity engine to render
-     * 
-     * @param request
-     * @param response
-     * @param engine
-     * @param template
-     */
-    public Page(HttpServletRequest request,
-                HttpServletResponse response,
-                VelocityEngine engine,
-                String template) {
-        this.request = Utils.nonNull(request);
-        this.response = Utils.nonNull(response);
-        this.engine = Utils.nonNull(engine);
-        this.template = Utils.nonNull(template);
-        this.context = new VelocityContext();
-        this.context.put("utils", utils);
-        this.context.put("session", request.getSession(true));
-        this.context.put("context", request.getContextPath());
-    }
+	private final HttpServletResponse response;
+	private final VelocityEngine engine;
+	private final VelocityContext context;
+	private final String template;
+	private String mimeType = DEFAULT_MIME_TYPE;
+	private VelocityUtils utils = new VelocityUtils();
 
-    /**
-     * Renders the page in UTF-8
-     */
-    public void render() {
-        try {
-            response.setContentType(mimeType);
-            engine.mergeTemplate(template, "UTF-8", context, response.getWriter());
-        } catch(Exception e) {
-            throw new PageRenderException(e);
-        }
-    }
+	/**
+	 * Creates a page and sets up the velocity engine to render
+	 * 
+	 * @param request
+	 * @param response
+	 * @param engine
+	 * @param template
+	 */
+	public Page(HttpServletRequest request, HttpServletResponse response,
+			VelocityEngine engine, String template) {
+		this.request = Utils.nonNull(request);
+		this.response = Utils.nonNull(response);
+		this.engine = Utils.nonNull(engine);
+		this.template = Utils.nonNull(template);
+		this.context = new VelocityContext();
+		this.context.put("utils", utils);
+		this.context.put("session", request.getSession(true));
+		this.context.put("context", request.getContextPath());
+	}
 
-    /**
-     * Adds variables to the velocity context.
-     */
-    public void add(String name, Object value) {
-        context.put(name, value);
-    }
-    
-    public void setMimeType(String type) {
-        mimeType = type;
-    }
+	/**
+	 * Renders the page in UTF-8
+	 */
+	public void render() {
+		try {
+			response.setContentType(mimeType);
+			engine.mergeTemplate(template, "UTF-8", context,
+					response.getWriter());
+		} catch (Exception e) {
+			throw new PageRenderException(e);
+		}
+	}
+
+	/**
+	 * Adds variables to the velocity context.
+	 */
+	public void add(String name, Object value) {
+		context.put(name, value);
+	}
+
+	public void setMimeType(String type) {
+		mimeType = type;
+	}
 }
diff --git a/src/java/azkaban/webapp/servlet/PageRenderException.java b/src/java/azkaban/webapp/servlet/PageRenderException.java
index 26a4c06..dd63b5f 100644
--- a/src/java/azkaban/webapp/servlet/PageRenderException.java
+++ b/src/java/azkaban/webapp/servlet/PageRenderException.java
@@ -20,9 +20,9 @@ package azkaban.webapp.servlet;
  * Thrown if there is an error rendering the page
  */
 public class PageRenderException extends RuntimeException {
-    private static final long serialVersionUID = -1;
+	private static final long serialVersionUID = -1;
 
-    public PageRenderException(Throwable cause) {
-        super(cause);
-    }
+	public PageRenderException(Throwable cause) {
+		super(cause);
+	}
 }
diff --git a/src/java/azkaban/webapp/servlet/VelocityUtils.java b/src/java/azkaban/webapp/servlet/VelocityUtils.java
index 5430988..00825e4 100644
--- a/src/java/azkaban/webapp/servlet/VelocityUtils.java
+++ b/src/java/azkaban/webapp/servlet/VelocityUtils.java
@@ -5,21 +5,21 @@ import org.joda.time.format.DateTimeFormat;
 import org.joda.time.format.DateTimeFormatter;
 
 public class VelocityUtils {
-    public String formatDate(long timestamp) {
-        return formatDate(timestamp, "yyyy-MM-dd HH:mm:ss");
-    }
-	
-    public String formatDate(DateTime date) {
-        return formatDate(date, "yyyy-MM-dd HH:mm:ss");
-    }
-    
-    public String formatDate(long timestamp, String format) {
-        DateTimeFormatter f = DateTimeFormat.forPattern(format);
-        return f.print(timestamp);
-    }
-    
-    public String formatDate(DateTime date, String format) {
-        DateTimeFormatter f = DateTimeFormat.forPattern(format);
-        return f.print(date);
-    }
+	public String formatDate(long timestamp) {
+		return formatDate(timestamp, "yyyy-MM-dd HH:mm:ss");
+	}
+
+	public String formatDate(DateTime date) {
+		return formatDate(date, "yyyy-MM-dd HH:mm:ss");
+	}
+
+	public String formatDate(long timestamp, String format) {
+		DateTimeFormatter f = DateTimeFormat.forPattern(format);
+		return f.print(timestamp);
+	}
+
+	public String formatDate(DateTime date, String format) {
+		DateTimeFormatter f = DateTimeFormat.forPattern(format);
+		return f.print(date);
+	}
 }
\ No newline at end of file