You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2016/08/09 19:53:46 UTC

[13/51] [partial] incubator-juneau git commit: Rename project directories.

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/utils/PojoRest.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/utils/PojoRest.java b/juneau-core/src/main/java/org/apache/juneau/utils/PojoRest.java
new file mode 100644
index 0000000..05f7d40
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/utils/PojoRest.java
@@ -0,0 +1,847 @@
+/***************************************************************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations under the License.
+ ***************************************************************************************************************************/
+package org.apache.juneau.utils;
+
+import static java.net.HttpURLConnection.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Provides the ability to perform standard REST operations (GET, PUT, POST, DELETE) against
+ * nodes in a POJO model.  Nodes in the POJO model are addressed using URLs.
+ * <p>
+ * 	A POJO model is defined as a tree model where nodes consist of consisting of the following:
+ * 	<ul class='spaced-list'>
+ * 		<li>{@link Map Maps} and Java beans representing JSON objects.
+ * 		<li>{@link Collection Collections} and arrays representing JSON arrays.
+ * 		<li>Java beans.
+ * 	</ul>
+ * <p>
+ * 	Leaves of the tree can be any type of object.
+ * <p>
+ * 	Use {@link #get(String) get()} to retrieve an element from a JSON tree.<br>
+ * 	Use {@link #put(String,Object) put()} to create (or overwrite) an element in a JSON tree.<br>
+ * 	Use {@link #post(String,Object) post()} to add an element to a list in a JSON tree.<br>
+ * 	Use {@link #delete(String) delete()} to remove an element from a JSON tree.<br>
+ * <p>
+ * 	Leading slashes in URLs are ignored.  So <js>"/xxx/yyy/zzz"</js> and <js>"xxx/yyy/zzz"</js> are considered identical.
+ *
+ * <h6 class='topic'>Examples</h6>
+ * <p class='bcode'>
+ * 	<jc>// Construct an unstructured POJO model</jc>
+ * 	ObjectMap m = <jk>new</jk> ObjectMap(<js>""</js>
+ * 		+ <js>"{"</js>
+ * 		+ <js>"	name:'John Smith', "</js>
+ * 		+ <js>"	address:{ "</js>
+ * 		+ <js>"		streetAddress:'21 2nd Street', "</js>
+ * 		+ <js>"		city:'New York', "</js>
+ * 		+ <js>"		state:'NY', "</js>
+ * 		+ <js>"		postalCode:10021 "</js>
+ * 		+ <js>"	}, "</js>
+ * 		+ <js>"	phoneNumbers:[ "</js>
+ * 		+ <js>"		'212 555-1111', "</js>
+ * 		+ <js>"		'212 555-2222' "</js>
+ * 		+ <js>"	], "</js>
+ * 		+ <js>"	additionalInfo:null, "</js>
+ * 		+ <js>"	remote:false, "</js>
+ * 		+ <js>"	height:62.4, "</js>
+ * 		+ <js>"	'fico score':' &gt; 640' "</js>
+ * 		+ <js>"} "</js>
+ * 	);
+ *
+ * 	<jc>// Wrap Map inside a PojoRest object</jc>
+ * 	PojoRest johnSmith = <jk>new</jk> PojoRest(m);
+ *
+ * 	<jc>// Get a simple value at the top level</jc>
+ * 	<jc>// "John Smith"</jc>
+ * 	String name = johnSmith.getString(<js>"name"</js>);
+ *
+ * 	<jc>// Change a simple value at the top level</jc>
+ * 	johnSmith.put(<js>"name"</js>, <js>"The late John Smith"</js>);
+ *
+ * 	<jc>// Get a simple value at a deep level</jc>
+ * 	<jc>// "21 2nd Street"</jc>
+ * 	String streetAddress = johnSmith.getString(<js>"address/streetAddress"</js>);
+ *
+ * 	<jc>// Set a simple value at a deep level</jc>
+ * 	johnSmith.put(<js>"address/streetAddress"</js>, <js>"101 Cemetery Way"</js>);
+ *
+ * 	<jc>// Get entries in a list</jc>
+ * 	<jc>// "212 555-1111"</jc>
+ * 	String firstPhoneNumber = johnSmith.getString(<js>"phoneNumbers/0"</js>);
+ *
+ * 	<jc>// Add entries to a list</jc>
+ * 	johnSmith.post(<js>"phoneNumbers"</js>, <js>"212 555-3333"</js>);
+ *
+ * 	<jc>// Delete entries from a model</jc>
+ * 	johnSmith.delete(<js>"fico score"</js>);
+ *
+ * 	<jc>// Add entirely new structures to the tree</jc>
+ * 	ObjectMap medicalInfo = new ObjectMap(<js>""</js>
+ * 		+ <js>"{"</js>
+ * 		+ <js>"	currentStatus: 'deceased',"</js>
+ * 		+ <js>"	health: 'non-existent',"</js>
+ * 		+ <js>"	creditWorthiness: 'not good'"</js>
+ * 		+ <js>"}"</js>
+ * 	);
+ * 	johnSmith.put(<js>"additionalInfo/medicalInfo"</js>, medicalInfo);
+ * <p>
+ * 	In the special case of collections/arrays of maps/beans, a special XPath-like selector notation
+ * 	can be used in lieu of index numbers on GET requests to return a map/bean with a specified attribute value.<br>
+ * 	The syntax is {@code @attr=val}, where attr is the attribute name on the child map, and val is the matching value.
+ *
+ * <h6 class='topic'>Examples</h6>
+ * <p class='bcode'>
+ * 	<jc>// Get map/bean with name attribute value of 'foo' from a list of items</jc>
+ * 	Map m = pojoRest.getMap(<js>"/items/@name=foo"</js>);
+ * </p>
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+@SuppressWarnings({"unchecked","rawtypes"})
+public final class PojoRest {
+
+	/** The list of possible request types. */
+	private static final int GET=1, PUT=2, POST=3, DELETE=4;
+
+	private ReaderParser parser = JsonParser.DEFAULT;
+	private final BeanContext bc;
+
+	/** If true, the root cannot be overwritten */
+	private boolean rootLocked = false;
+
+	/** The root of the model. */
+	private JsonNode root;
+
+	/**
+	 * Create a new instance of a REST interface over the specified object.
+	 * <p>
+	 * 	Uses {@link BeanContext#DEFAULT} for working with Java beans.
+	 *
+	 * @param o The object to be wrapped.
+	 */
+	public PojoRest(Object o) {
+		this(o, null);
+	}
+
+	/**
+	 * Create a new instance of a REST interface over the specified object.
+	 * <p>
+	 * 	The parser is used as the bean context.
+	 *
+	 * @param o The object to be wrapped.
+	 * @param parser The parser to use for parsing arguments and converting objects to the correct data type.
+	 */
+	public PojoRest(Object o, ReaderParser parser) {
+		if (parser == null)
+			parser = JsonParser.DEFAULT;
+		this.parser = parser;
+		this.bc = parser.getBeanContext();
+		this.root = new JsonNode(null, null, o, bc.object());
+	}
+
+	/**
+	 * Call this method to prevent the root object from being overwritten on put("", xxx); calls.
+	 *
+	 * @return This object (for method chaining).
+	 */
+	public PojoRest setRootLocked() {
+		this.rootLocked = true;
+		return this;
+	}
+
+	/**
+	 * The root object that was passed into the constructor of this method.
+	 *
+	 * @return The root object.
+	 */
+	public Object getRootObject() {
+		return root.o;
+	}
+
+	/**
+	 * Retrieves the element addressed by the URL.
+	 *
+	 * @param url The URL of the element to retrieve.
+	 * 		If null or blank, returns the root.
+	 * @return The addressed element, or null if that element does not exist in the tree.
+	 */
+	public Object get(String url) {
+		return get(url, null);
+	}
+
+	/**
+	 * Retrieves the element addressed by the URL.
+	 *
+	 * @param url The URL of the element to retrieve.
+	 * 		If null or blank, returns the root.
+	 * @param defVal The default value if the map doesn't contain the specified mapping.
+	 * @return The addressed element, or null if that element does not exist in the tree.
+	 */
+	public Object get(String url, Object defVal) {
+		Object o = service(GET, url, null);
+		return o == null ? defVal : o;
+	}
+
+	/**
+	 * Retrieves the element addressed by the URL as the specified object type.
+	 * <p>
+	 * Will convert object to the specified type per {@link BeanContext#convertToType(Object, ClassMeta)}.
+	 *
+	 * @param type The specified object type.
+	 * @param url The URL of the element to retrieve.
+	 * 		If null or blank, returns the root.
+	 * @param <T> The specified object type.
+	 *
+	 * @return The addressed element, or null if that element does not exist in the tree.
+	 */
+	public <T> T get(Class<T> type, String url) {
+		return get(type, url, null);
+	}
+
+	/**
+	 * Retrieves the element addressed by the URL as the specified object type.
+	 * <p>
+	 * Will convert object to the specified type per {@link BeanContext#convertToType(Object, ClassMeta)}.
+	 *
+	 * @param type The specified object type.
+	 * @param url The URL of the element to retrieve.
+	 * 		If null or blank, returns the root.
+	 * @param def The default value if addressed item does not exist.
+	 * @param <T> The specified object type.
+	 *
+	 * @return The addressed element, or null if that element does not exist in the tree.
+	 */
+	public <T> T get(Class<T> type, String url, T def) {
+		Object o = service(GET, url, null);
+		if (o == null)
+			return def;
+		return bc.convertToType(o, type);
+	}
+
+	/**
+	 * Returns the specified entry value converted to a {@link String}.
+	 * <p>
+	 * 	Shortcut for <code>get(String.<jk>class</jk>, key)</code>.
+	 *
+	 * @param url The key.
+	 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+	 */
+	public String getString(String url) {
+		return get(String.class, url);
+	}
+
+	/**
+	 * Returns the specified entry value converted to a {@link String}.
+	 * <p>
+	 * 	Shortcut for <code>get(String.<jk>class</jk>, key, defVal)</code>.
+	 *
+	 * @param url The key.
+	 * @param defVal The default value if the map doesn't contain the specified mapping.
+	 * @return The converted value, or the default value if the map contains no mapping for this key.
+	 */
+	public String getString(String url, String defVal) {
+		return get(String.class, url, defVal);
+	}
+
+	/**
+	 * Returns the specified entry value converted to an {@link Integer}.
+	 * <p>
+	 * 	Shortcut for <code>get(Integer.<jk>class</jk>, key)</code>.
+	 *
+	 * @param url The key.
+	 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+	 * @throws InvalidDataConversionException If value cannot be converted.
+	 */
+	public Integer getInt(String url) {
+		return get(Integer.class, url);
+	}
+
+	/**
+	 * Returns the specified entry value converted to an {@link Integer}.
+	 * <p>
+	 * 	Shortcut for <code>get(Integer.<jk>class</jk>, key, defVal)</code>.
+	 *
+	 * @param url The key.
+	 * @param defVal The default value if the map doesn't contain the specified mapping.
+	 * @return The converted value, or the default value if the map contains no mapping for this key.
+	 * @throws InvalidDataConversionException If value cannot be converted.
+	 */
+	public Integer getInt(String url, Integer defVal) {
+		return get(Integer.class, url, defVal);
+	}
+
+	/**
+	 * Returns the specified entry value converted to a {@link Long}.
+	 * <p>
+	 * 	Shortcut for <code>get(Long.<jk>class</jk>, key)</code>.
+	 *
+	 * @param url The key.
+	 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+	 * @throws InvalidDataConversionException If value cannot be converted.
+	 */
+	public Long getLong(String url) {
+		return get(Long.class, url);
+	}
+
+	/**
+	 * Returns the specified entry value converted to a {@link Long}.
+	 * <p>
+	 * 	Shortcut for <code>get(Long.<jk>class</jk>, key, defVal)</code>.
+	 *
+	 * @param url The key.
+	 * @param defVal The default value if the map doesn't contain the specified mapping.
+	 * @return The converted value, or the default value if the map contains no mapping for this key.
+	 * @throws InvalidDataConversionException If value cannot be converted.
+	 */
+	public Long getLong(String url, Long defVal) {
+		return get(Long.class, url, defVal);
+	}
+
+	/**
+	 * Returns the specified entry value converted to a {@link Boolean}.
+	 * <p>
+	 * 	Shortcut for <code>get(Boolean.<jk>class</jk>, key)</code>.
+	 *
+	 * @param url The key.
+	 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+	 * @throws InvalidDataConversionException If value cannot be converted.
+	 */
+	public Boolean getBoolean(String url) {
+		return get(Boolean.class, url);
+	}
+
+	/**
+	 * Returns the specified entry value converted to a {@link Boolean}.
+	 * <p>
+	 * 	Shortcut for <code>get(Boolean.<jk>class</jk>, key, defVal)</code>.
+	 *
+	 * @param url The key.
+	 * @param defVal The default value if the map doesn't contain the specified mapping.
+	 * @return The converted value, or the default value if the map contains no mapping for this key.
+	 * @throws InvalidDataConversionException If value cannot be converted.
+	 */
+	public Boolean getBoolean(String url, Boolean defVal) {
+		return get(Boolean.class, url, defVal);
+	}
+
+	/**
+	 * Returns the specified entry value converted to a {@link Map}.
+	 * <p>
+	 * 	Shortcut for <code>get(Map.<jk>class</jk>, key)</code>.
+	 *
+	 * @param url The key.
+	 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+	 * @throws InvalidDataConversionException If value cannot be converted.
+	 */
+	public Map<?,?> getMap(String url) {
+		return get(Map.class, url);
+	}
+
+	/**
+	 * Returns the specified entry value converted to a {@link Map}.
+	 * <p>
+	 * 	Shortcut for <code>get(Map.<jk>class</jk>, key, defVal)</code>.
+	 *
+	 * @param url The key.
+	 * @param defVal The default value if the map doesn't contain the specified mapping.
+	 * @return The converted value, or the default value if the map contains no mapping for this key.
+	 * @throws InvalidDataConversionException If value cannot be converted.
+	 */
+	public Map<?,?> getMap(String url, Map<?,?> defVal) {
+		return get(Map.class, url, defVal);
+	}
+
+	/**
+	 * Returns the specified entry value converted to a {@link List}.
+	 * <p>
+	 * 	Shortcut for <code>get(List.<jk>class</jk>, key)</code>.
+	 *
+	 * @param url The key.
+	 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+	 * @throws InvalidDataConversionException If value cannot be converted.
+	 */
+	public List<?> getList(String url) {
+		return get(List.class, url);
+	}
+
+	/**
+	 * Returns the specified entry value converted to a {@link List}.
+	 * <p>
+	 * 	Shortcut for <code>get(List.<jk>class</jk>, key, defVal)</code>.
+	 *
+	 * @param url The key.
+	 * @param defVal The default value if the map doesn't contain the specified mapping.
+	 * @return The converted value, or the default value if the map contains no mapping for this key.
+	 * @throws InvalidDataConversionException If value cannot be converted.
+	 */
+	public List<?> getList(String url, List<?> defVal) {
+		return get(List.class, url, defVal);
+	}
+
+	/**
+	 * Returns the specified entry value converted to a {@link Map}.
+	 * <p>
+	 * 	Shortcut for <code>get(ObjectMap.<jk>class</jk>, key)</code>.
+	 *
+	 * @param url The key.
+	 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+	 * @throws InvalidDataConversionException If value cannot be converted.
+	 */
+	public ObjectMap getObjectMap(String url) {
+		return get(ObjectMap.class, url);
+	}
+
+	/**
+	 * Returns the specified entry value converted to a {@link ObjectMap}.
+	 * <p>
+	 * 	Shortcut for <code>get(ObjectMap.<jk>class</jk>, key, defVal)</code>.
+	 *
+	 * @param url The key.
+	 * @param defVal The default value if the map doesn't contain the specified mapping.
+	 * @return The converted value, or the default value if the map contains no mapping for this key.
+	 * @throws InvalidDataConversionException If value cannot be converted.
+	 */
+	public ObjectMap getObjectMap(String url, ObjectMap defVal) {
+		return get(ObjectMap.class, url, defVal);
+	}
+
+	/**
+	 * Returns the specified entry value converted to a {@link ObjectList}.
+	 * <p>
+	 * 	Shortcut for <code>get(ObjectList.<jk>class</jk>, key)</code>.
+	 *
+	 * @param url The key.
+	 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+	 * @throws InvalidDataConversionException If value cannot be converted.
+	 */
+	public ObjectList getObjectList(String url) {
+		return get(ObjectList.class, url);
+	}
+
+	/**
+	 * Returns the specified entry value converted to a {@link ObjectList}.
+	 * <p>
+	 * 	Shortcut for <code>get(ObjectList.<jk>class</jk>, key, defVal)</code>.
+	 *
+	 * @param url The key.
+	 * @param defVal The default value if the map doesn't contain the specified mapping.
+	 * @return The converted value, or the default value if the map contains no mapping for this key.
+	 * @throws InvalidDataConversionException If value cannot be converted.
+	 */
+	public ObjectList getObjectList(String url, ObjectList defVal) {
+		return get(ObjectList.class, url, defVal);
+	}
+
+	/**
+	 * Executes the specified method with the specified parameters on the specified object.
+	 *
+	 * @param url The URL of the element to retrieve.
+	 * @param method The method signature.
+	 * 	<p>
+	 * 		Can be any of the following formats:
+	 * 	</p>
+	 * 	<ul class='spaced-list'>
+	 * 		<li>Method name only.  e.g. <js>"myMethod"</js>.
+	 * 		<li>Method name with class names.  e.g. <js>"myMethod(String,int)"</js>.
+	 * 		<li>Method name with fully-qualified class names.  e.g. <js>"myMethod(java.util.String,int)"</js>.
+	 * 	</ul>
+	 * 	<p>
+	 * 		As a rule, use the simplest format needed to uniquely resolve a method.
+	 * 	</p>
+	 * @param args The arguments to pass as parameters to the method.<br>
+	 * 	These will automatically be converted to the appropriate object type if possible.<br>
+	 * 	This must be an array, like a JSON array.
+	 * @return The returned object from the method call.
+	 * @throws IllegalAccessException If the <code>Constructor</code> object enforces Java language access control and the underlying constructor is inaccessible.
+	 * @throws IllegalArgumentException If one of the following occurs:
+	 * 	<ul class='spaced-list'>
+	 * 		<li>The number of actual and formal parameters differ.
+	 * 		<li>An unwrapping conversion for primitive arguments fails.
+	 * 		<li>A parameter value cannot be converted to the corresponding formal parameter type by a method invocation conversion.
+	 * 		<li>The constructor pertains to an enum type.
+	 * 	</ul>
+	 * @throws InvocationTargetException If the underlying constructor throws an exception.
+	 * @throws ParseException If the input contains a syntax error or is malformed.
+	 * @throws NoSuchMethodException
+	 * @throws IOException
+	 */
+	public Object invokeMethod(String url, String method, String args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException, ParseException, NoSuchMethodException, IOException {
+		return new PojoIntrospector(get(url), parser).invokeMethod(method, args);
+	}
+
+	/**
+	 * Returns the list of available methods that can be passed to the {@link #invokeMethod(String, String, String)} for the object
+	 * 	addressed by the specified URL.
+	 *
+	 * @param url The URL.
+	 * @return The list of methods.
+	 */
+	public Collection<String> getPublicMethods(String url) {
+		Object o = get(url);
+		if (o == null)
+			return null;
+		return bc.getClassMeta(o.getClass()).getPublicMethods().keySet();
+	}
+
+	/**
+	 * Returns the class type of the object at the specified URL.
+	 *
+	 * @param url The URL.
+	 * @return The class type.
+	 */
+	public ClassMeta getClassMeta(String url) {
+		JsonNode n = getNode(normalizeUrl(url), root);
+		if (n == null)
+			return null;
+		return n.cm;
+	}
+
+	/**
+	 * Sets/replaces the element addressed by the URL.
+	 * <p>
+	 * 	This method expands the POJO model as necessary to create the new element.
+	 *
+	 * @param url The URL of the element to create.
+	 * 		If <jk>null</jk> or blank, the root itself is replaced with the specified value.
+	 * @param val The value being set.  Value can be of any type.
+	 * @return The previously addressed element, or <jk>null</jk> the element did not previously exist.
+	 */
+	public Object put(String url, Object val) {
+		return service(PUT, url, val);
+	}
+
+	/**
+	 * Adds a value to a list element in a POJO model.
+	 * <p>
+	 * 	The URL is the address of the list being added to.
+	 * <p>
+	 * 	If the list does not already exist, it will be created.
+	 * <p>
+	 * 	This method expands the POJO model as necessary to create the new element.
+	 * <p>
+	 * 	Note:  You can only post to three types of nodes:
+	 * 	<ul class='spaced-list'>
+	 * 		<li>{@link List Lists}
+	 * 		<li>{@link Map Maps} containing integers as keys (i.e sparse arrays)
+	 * 		<li>arrays
+	 * 	</ul>
+	 *
+	 * @param url The URL of the element being added to.
+	 * 		If null or blank, the root itself (assuming it's one of the types specified above) is added to.
+	 * @param val The value being added.
+	 * @return The URL of the element that was added.
+	 */
+	public String post(String url, Object val) {
+		return (String)service(POST, url, val);
+	}
+
+	/**
+	 * Remove an element from a POJO model.
+	 * <p>
+	 * qIf the element does not exist, no action is taken.
+	 *
+	 * @param url The URL of the element being deleted.
+	 * 		If <jk>null</jk> or blank, the root itself is deleted.
+	 * @return The removed element, or null if that element does not exist.
+	 */
+	public Object delete(String url) {
+		return service(DELETE, url, null);
+	}
+
+	@Override /* Object */
+	public String toString() {
+		return String.valueOf(root.o);
+	}
+
+	/** Handle nulls and strip off leading '/' char. */
+	private String normalizeUrl(String url) {
+
+		// Interpret nulls and blanks the same (i.e. as addressing the root itself)
+		if (url == null)
+			url = "";
+
+		// Strip off leading slash if present.
+		if (url.length() > 0 && url.charAt(0) == '/')
+			url = url.substring(1);
+
+		return url;
+	}
+
+
+	/*
+	 * Workhorse method.
+	 */
+	private Object service(int method, String url, Object val) throws PojoRestException {
+
+		url = normalizeUrl(url);
+
+		if (method == GET) {
+			JsonNode p = getNode(url, root);
+			return p == null ? null : p.o;
+		}
+
+		// Get the url of the parent and the property name of the addressed object.
+		int i = url.lastIndexOf('/');
+		String parentUrl = (i == -1 ? null : url.substring(0, i));
+		String childKey = (i == -1 ? url : url.substring(i + 1));
+
+		if (method == PUT) {
+			if (url.length() == 0) {
+				if (rootLocked)
+					throw new PojoRestException(HTTP_FORBIDDEN, "Cannot overwrite root object");
+				Object o = root.o;
+				root = new JsonNode(null, null, val, bc.object());
+				return o;
+			}
+			JsonNode n = (parentUrl == null ? root : getNode(parentUrl, root));
+			if (n == null)
+				throw new PojoRestException(HTTP_NOT_FOUND, "Node at URL ''{0}'' not found.", parentUrl);
+			ClassMeta cm = n.cm;
+			Object o = n.o;
+			if (cm.isMap())
+				return ((Map)o).put(childKey, convert(val, cm.getValueType()));
+			if (cm.isCollection() && o instanceof List)
+				return ((List)o).set(parseInt(childKey), convert(val, cm.getElementType()));
+			if (cm.isArray()) {
+				o = setArrayEntry(n.o, parseInt(childKey), val, cm.getElementType());
+				ClassMeta pct = n.parent.cm;
+				Object po = n.parent.o;
+				if (pct.isMap()) {
+					((Map)po).put(n.keyName, o);
+					return url;
+				}
+				if (pct.isBean()) {
+					BeanMap m = bc.forBean(po);
+					m.put(n.keyName, o);
+					return url;
+				}
+				throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform PUT on ''{0}'' with parent node type ''{1}''", url, pct);
+			}
+			if (cm.isBean())
+				return bc.forBean(o).put(childKey, val);
+			throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform PUT on ''{0}'' whose parent is of type ''{1}''", url, cm);
+		}
+
+		if (method == POST) {
+			// Handle POST to root special
+			if (url.length() == 0) {
+				ClassMeta cm = root.cm;
+				Object o = root.o;
+				if (cm.isCollection()) {
+					Collection c = (Collection)o;
+					c.add(convert(val, cm.getElementType()));
+					return (c instanceof List ? url + "/" + (c.size()-1) : null);
+				}
+				if (cm.isArray()) {
+					Object[] o2 = addArrayEntry(o, val, cm.getElementType());
+					root = new JsonNode(null, null, o2, null);
+					return url + "/" + (o2.length-1);
+				}
+				throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' of type ''{1}''", url, cm);
+			}
+			JsonNode n = getNode(url, root);
+			if (n == null)
+				throw new PojoRestException(HTTP_NOT_FOUND, "Node at URL ''{0}'' not found.", url);
+			ClassMeta cm = n.cm;
+			Object o = n.o;
+			if (cm.isArray()) {
+				Object[] o2 = addArrayEntry(o, val, cm.getElementType());
+				ClassMeta pct = n.parent.cm;
+				Object po = n.parent.o;
+				if (pct.isMap()) {
+					((Map)po).put(childKey, o2);
+					return url + "/" + (o2.length-1);
+				}
+				if (pct.isBean()) {
+					BeanMap m = bc.forBean(po);
+					m.put(childKey, o2);
+					return url + "/" + (o2.length-1);
+				}
+				throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' with parent node type ''{1}''", url, pct);
+			}
+			if (cm.isCollection()) {
+				Collection c = (Collection)o;
+				c.add(convert(val, cm.getElementType()));
+				return (c instanceof List ? url + "/" + (c.size()-1) : null);
+			}
+			throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' of type ''{1}''", url, cm);
+		}
+
+		if (method == DELETE) {
+			if (url.length() == 0) {
+				if (rootLocked)
+					throw new PojoRestException(HTTP_FORBIDDEN, "Cannot overwrite root object");
+				Object o = root.o;
+				root = new JsonNode(null, null, null, bc.object());
+				return o;
+			}
+			JsonNode n = (parentUrl == null ? root : getNode(parentUrl, root));
+			ClassMeta cm = n.cm;
+			Object o = n.o;
+			if (cm.isMap())
+				return ((Map)o).remove(childKey);
+			if (cm.isCollection() && o instanceof List)
+				return ((List)o).remove(parseInt(childKey));
+			if (cm.isArray()) {
+				int index = parseInt(childKey);
+				Object old = ((Object[])o)[index];
+				Object[] o2 = removeArrayEntry(o, index);
+				ClassMeta pct = n.parent.cm;
+				Object po = n.parent.o;
+				if (pct.isMap()) {
+					((Map)po).put(n.keyName, o2);
+					return old;
+				}
+				if (pct.isBean()) {
+					BeanMap m = bc.forBean(po);
+					m.put(n.keyName, o2);
+					return old;
+				}
+				throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' with parent node type ''{1}''", url, pct);
+			}
+			if (cm.isBean())
+				return bc.forBean(o).put(childKey, null);
+			throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform PUT on ''{0}'' whose parent is of type ''{1}''", url, cm);
+		}
+
+		return null;	// Never gets here.
+	}
+
+	private Object[] setArrayEntry(Object o, int index, Object val, ClassMeta componentType) {
+		Object[] a = (Object[])o;
+		if (a.length <= index) {
+			// Expand out the array.
+			Object[] a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), index+1);
+			System.arraycopy(a, 0, a2, 0, a.length);
+			a = a2;
+		}
+		a[index] = convert(val, componentType);
+		return a;
+	}
+
+	private Object[] addArrayEntry(Object o, Object val, ClassMeta componentType) {
+		Object[] a = (Object[])o;
+		// Expand out the array.
+		Object[] a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), a.length+1);
+		System.arraycopy(a, 0, a2, 0, a.length);
+		a2[a.length] = convert(val, componentType);
+		return a2;
+	}
+
+	private Object[] removeArrayEntry(Object o, int index) {
+		Object[] a = (Object[])o;
+		// Shrink the array.
+		Object[] a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), a.length-1);
+		System.arraycopy(a, 0, a2, 0, index);
+		System.arraycopy(a, index+1, a2, index, a.length-index-1);
+		return a2;
+	}
+
+	class JsonNode {
+		Object o;
+		ClassMeta cm;
+		JsonNode parent;
+		String keyName;
+
+		JsonNode(JsonNode parent, String keyName, Object o, ClassMeta cm) {
+			this.o = o;
+			this.keyName = keyName;
+			this.parent = parent;
+			if (cm == null || cm.isObject()) {
+				if (o == null)
+					cm = bc.object();
+				else
+					cm = bc.getClassMetaForObject(o);
+			}
+			this.cm = cm;
+		}
+	}
+
+	JsonNode getNode(String url, JsonNode n) {
+		if (url == null || url.isEmpty())
+			return n;
+		int i = url.indexOf('/');
+		String parentKey, childUrl = null;
+		if (i == -1) {
+			parentKey = url;
+		} else {
+			parentKey = url.substring(0, i);
+			childUrl = url.substring(i + 1);
+		}
+
+		Object o = n.o;
+		Object o2 = null;
+		ClassMeta cm = n.cm;
+		ClassMeta ct2 = null;
+		if (o == null)
+			return null;
+		if (cm.isMap()) {
+			o2 = ((Map)o).get(parentKey);
+			ct2 = cm.getValueType();
+		} else if (cm.isCollection() && o instanceof List) {
+			int key = parseInt(parentKey);
+			List l = ((List)o);
+			if (l.size() <= key)
+				return null;
+			o2 = l.get(key);
+			ct2 = cm.getElementType();
+		} else if (cm.isArray()) {
+			int key = parseInt(parentKey);
+			Object[] a = ((Object[])o);
+			if (a.length <= key)
+				return null;
+			o2 = a[key];
+			ct2 = cm.getElementType();
+		} else if (cm.isBean()) {
+			BeanMap m = bc.forBean(o);
+			o2 = m.get(parentKey);
+			BeanPropertyMeta pMeta = m.getPropertyMeta(parentKey);
+			if (pMeta == null)
+				throw new PojoRestException(HTTP_BAD_REQUEST,
+					"Unknown property ''{0}'' encountered while trying to parse into class ''{1}''",
+					parentKey, m.getClassMeta()
+				);
+			ct2 = pMeta.getClassMeta();
+		}
+
+		if (childUrl == null)
+			return new JsonNode(n, parentKey, o2, ct2);
+
+		return getNode(childUrl, new JsonNode(n, parentKey, o2, ct2));
+	}
+
+	private Object convert(Object in, ClassMeta cm) {
+		if (cm == null)
+			return in;
+		if (cm.isBean() && in instanceof Map)
+			return bc.convertToType(in, cm);
+		return in;
+	}
+
+	private int parseInt(String key) {
+		try {
+			return Integer.parseInt(key);
+		} catch (NumberFormatException e) {
+			throw new PojoRestException(HTTP_BAD_REQUEST,
+				"Cannot address an item in an array with a non-integer key ''{0}''", key
+			);
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/utils/PojoRestException.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/utils/PojoRestException.java b/juneau-core/src/main/java/org/apache/juneau/utils/PojoRestException.java
new file mode 100644
index 0000000..aeced12
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/utils/PojoRestException.java
@@ -0,0 +1,60 @@
+/***************************************************************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations under the License.
+ ***************************************************************************************************************************/
+package org.apache.juneau.utils;
+
+import java.net.*;
+import java.text.*;
+
+/**
+ * Generic exception thrown from the {@link PojoRest} class.
+ * <p>
+ * 	Typically, this is a user-error, such as trying to address a non-existent node in the tree.
+ * <p>
+ * 	The status code is an HTTP-equivalent code.  It will be one of the following:
+ * <ul class='spaced-list'>
+ * 	<li>{@link HttpURLConnection#HTTP_BAD_REQUEST HTTP_BAD_REQUEST} - Attempting to do something impossible.
+ * 	<li>{@link HttpURLConnection#HTTP_NOT_FOUND HTTP_NOT_FOUND} - Attempting to access a non-existent node in the tree.
+ * 	<li>{@link HttpURLConnection#HTTP_FORBIDDEN HTTP_FORBIDDEN} - Attempting to overwrite the root object.
+ * </ul>
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+public final class PojoRestException extends RuntimeException {
+
+	private static final long serialVersionUID = 1L;
+
+	private int status;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param status The HTTP-equivalent status code.
+	 * @param message The detailed message.
+	 * @param args Optional message arguments.
+	 */
+	public PojoRestException(int status, String message, Object...args) {
+		super(args.length == 0 ? message : MessageFormat.format(message, args));
+		this.status = status;
+	}
+
+	/**
+	 * The HTTP-equivalent status code.
+	 * <p>
+	 * 	See above for details.
+	 *
+	 * @return The HTTP-equivalent status code.
+	 */
+	public int getStatus() {
+		return status;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/utils/ProcBuilder.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/utils/ProcBuilder.java b/juneau-core/src/main/java/org/apache/juneau/utils/ProcBuilder.java
new file mode 100644
index 0000000..aa3ca4a
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/utils/ProcBuilder.java
@@ -0,0 +1,387 @@
+/***************************************************************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations under the License.
+ ***************************************************************************************************************************/
+package org.apache.juneau.utils;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+import java.util.logging.*;
+
+import org.apache.juneau.internal.*;
+import org.apache.juneau.utils.IOPipe.*;
+
+/**
+ * Utility class for running operating system processes.
+ * <p>
+ * Similar to {@link java.lang.ProcessBuilder} but with additional features.
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+@SuppressWarnings("hiding")
+public class ProcBuilder {
+
+	private java.lang.ProcessBuilder pb = new java.lang.ProcessBuilder();
+	private TeeWriter outWriters = new TeeWriter(), logWriters = new TeeWriter();
+	private LineProcessor lp;
+	private Process p;
+	private int maxExitStatus = 0;
+	private boolean byLines;
+	private String divider = "--------------------------------------------------------------------------------";
+
+	/**
+	 * Creates a process builder with the specified arguments.
+	 * Equivalent to calling <code>ProcessBuilder.create().command(args);</code>
+	 *
+	 * @param args The command-line arguments.
+	 * @return A new process builder.
+	 */
+	public static ProcBuilder create(Object...args) {
+		return new ProcBuilder().command(args);
+	}
+
+	/**
+	 * Creates an empty process builder.
+	 *
+	 * @return A new process builder.
+	 */
+	public static ProcBuilder create() {
+		return new ProcBuilder().command();
+	}
+
+	/**
+	 * Command arguments.
+	 * Arguments can be collections or arrays and will be automatically expanded.
+	 *
+	 * @param args The command-line arguments.
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder command(Object...args) {
+		return commandIf(ANY, args);
+	}
+
+	/**
+	 * Command arguments if the specified matcher matches.
+	 * Can be used for specifying os-specific commands.
+	 * Example:
+	 * <p class='bcode'>
+	 * 	ProcessBuilder pb = ProcessBuilder
+	 * 		.create()
+	 * 		.commandIf(<jsf>WINDOWS</jsf>, <js>"cmd /c dir"</js>)
+	 * 		.commandIf(<jsf>UNIX</jsf>, <js>"bash -c ls"</js>)
+	 * 		.merge()
+	 * 		.execute();
+	 * </p>
+	 *
+	 * @param m The matcher.
+	 * @param args The command line arguments if matcher matches.
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder commandIf(Matcher m, Object...args) {
+		if (m.matches())
+			pb.command(toList(args));
+		return this;
+	}
+
+	/**
+	 * Append to the command arguments.
+	 * Arguments can be collections or arrays and will be automatically expanded.
+	 *
+	 * @param args The command-line arguments.
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder append(Object...args) {
+		return appendIf(ANY, args);
+	}
+
+	/**
+	 * Append to the command arguments if the specified matcher matches.
+	 * Arguments can be collections or arrays and will be automatically expanded.
+	 *
+	 * @param m The matcher.
+	 * @param args The command line arguments if matcher matches.
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder appendIf(Matcher m, Object...args) {
+		if (m.matches())
+			pb.command().addAll(toList(args));
+		return this;
+	}
+
+	/**
+	 * Merge STDOUT and STDERR into a single stream.
+	 *
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder merge() {
+		pb.redirectErrorStream(true);
+		return this;
+	}
+
+	/**
+	 * Use by-lines mode.
+	 * Flushes output after every line of input.
+	 *
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder byLines() {
+		this.byLines = true;
+		return this;
+	}
+
+	/**
+	 * Pipe output to the specified writer.
+	 * The method can be called multiple times to write to multiple writers.
+	 *
+	 * @param w The writer to pipe to.
+	 * @param close Close the writer afterwards.
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder pipeTo(Writer w, boolean close) {
+		this.outWriters.add(w, close);
+		return this;
+	}
+
+	/**
+	 * Pipe output to the specified writer, but don't close the writer.
+	 *
+	 * @param w The writer to pipe to.
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder pipeTo(Writer w) {
+		return pipeTo(w, false);
+	}
+
+	/**
+	 * Pipe output to the specified writer, including the command and return code.
+	 * The method can be called multiple times to write to multiple writers.
+	 *
+	 * @param w The writer to pipe to.
+	 * @param close Close the writer afterwards.
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder logTo(Writer w, boolean close) {
+		this.logWriters.add(w, close);
+		this.outWriters.add(w, close);
+		return this;
+	}
+
+	/**
+	 * Pipe output to the specified writer, including the command and return code.
+	 * The method can be called multiple times to write to multiple writers.
+	 * Don't close the writer afterwards.
+	 *
+	 * @param w The writer to pipe to.
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder logTo(Writer w) {
+		return logTo(w, false);
+	}
+
+	/**
+	 * Pipe output to the specified writer, including the command and return code.
+	 * The method can be called multiple times to write to multiple writers.
+	 *
+	 * @param level The log level.
+	 * @param logger The logger to log to.
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder logTo(final Level level, final Logger logger) {
+		if (logger.isLoggable(level)) {
+			logTo(new StringWriter() {
+				private boolean isClosed;  // Prevents messages from being written twice.
+				@Override /* Writer */
+				public void close() {
+					if (! isClosed)
+						logger.log(level, this.toString());
+					isClosed = true;
+				}
+			}, true);
+		}
+		return this;
+	}
+
+	/**
+	 * Line processor to use to process/convert lines of output returned by the process.
+	 *
+	 * @param lp The new line processor.
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder lp(LineProcessor lp) {
+		this.lp = lp;
+		return this;
+	}
+
+	/**
+	 * Append the specified environment variables to the process.
+	 *
+	 * @param env The new set of environment variables.
+	 * @return This object (for method chaining).
+	 */
+	@SuppressWarnings({"rawtypes", "unchecked"})
+	public ProcBuilder env(Map env) {
+		if (env != null)
+			for (Map.Entry e : (Set<Map.Entry>)env.entrySet())
+				environment(e.getKey().toString(), e.getValue() == null ? null : e.getValue().toString());
+		return this;
+	}
+
+	/**
+	 * Append the specified environment variable.
+	 *
+	 * @param key The environment variable name.
+	 * @param val The environment variable value.
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder environment(String key, String val) {
+		pb.environment().put(key, val);
+		return this;
+	}
+
+	/**
+	 * Sets the directory where the command will be executed.
+	 *
+	 * @param directory The directory.
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder directory(File directory) {
+		pb.directory(directory);
+		return this;
+	}
+
+	/**
+	 * Sets the maximum allowed return code on the process call.
+	 * If the return code exceeds this value, an IOException is returned on the {@link #run()} command.
+	 * The default value is '0'.
+	 *
+	 * @param maxExitStatus The maximum exit status.
+	 * @return This object (for method chaining).
+	 */
+	public ProcBuilder maxExitStatus(int maxExitStatus) {
+		this.maxExitStatus = maxExitStatus;
+		return this;
+	}
+
+	/**
+	 * Run this command and pipes the output to the specified writer or output stream.
+	 *
+	 * @return The exit code from the process.
+	 * @throws IOException
+	 * @throws InterruptedException
+	 */
+	public int run() throws IOException, InterruptedException {
+		if (pb.command().size() == 0)
+			throw new IOException("No command specified in ProcBuilder.");
+		try {
+			logWriters.append(divider).append('\n').flush();
+			logWriters.append(StringUtils.join(pb.command(), " ")).append('\n').flush();
+			p = pb.start();
+			IOPipe.create(p.getInputStream(), outWriters).lineProcessor(lp).byLines(byLines).run();
+			int rc = p.waitFor();
+			logWriters.append("Exit: ").append(String.valueOf(p.exitValue())).append('\n').flush();
+			if (rc > maxExitStatus)
+				throw new IOException("Return code "+rc+" from command " + StringUtils.join(pb.command(), " "));
+			return rc;
+		} finally {
+			close();
+		}
+	}
+
+	/**
+	 * Run this command and returns the output as a simple string.
+	 *
+	 * @return The output from the command.
+	 * @throws IOException
+	 * @throws InterruptedException
+	 */
+	public String getOutput() throws IOException, InterruptedException {
+		StringWriter sw = new StringWriter();
+		pipeTo(sw).run();
+		return sw.toString();
+	}
+
+	/**
+	 * Returns the output from this process as a {@link Scanner}.
+	 *
+	 * @return The output from the process as a Scanner object.
+	 * @throws IOException
+	 * @throws InterruptedException
+	 */
+	public Scanner getScanner() throws IOException, InterruptedException {
+		StringWriter sw = new StringWriter();
+		pipeTo(sw, true);
+		run();
+		return new Scanner(sw.toString());
+	}
+
+	/**
+	 * Destroys the underlying process.
+	 * This method is only needed if the {@link #getScanner()} method was used.
+	 */
+	private void close() {
+		IOUtils.closeQuietly(logWriters, outWriters);
+		if (p != null)
+			p.destroy();
+	}
+
+	/**
+	 * Specifies interface for defining OS-specific commands.
+	 */
+	public abstract static class Matcher {
+		abstract boolean matches();
+	}
+
+	private static String OS = System.getProperty("os.name").toLowerCase();
+
+	/** Operating system matcher: Any operating system. */
+	public final static Matcher ANY = new Matcher() {
+		@Override boolean matches() {
+			return true;
+		}
+	};
+
+	/** Operating system matcher: Any Windows system. */
+	public final static Matcher WINDOWS = new Matcher() {
+		@Override boolean matches() {
+			return OS.indexOf("win") >= 0;
+		}
+	};
+
+	/** Operating system matcher: Any Mac system. */
+	public final static Matcher MAC = new Matcher() {
+		@Override boolean matches() {
+			return OS.indexOf("mac") >= 0;
+		}
+	};
+
+	/** Operating system matcher: Any Unix or Linux system. */
+	public final static Matcher UNIX = new Matcher() {
+		@Override boolean matches() {
+			return OS.indexOf("nix") >= 0 || OS.indexOf("nux") >= 0 || OS.indexOf("aix") > 0;
+		}
+	};
+
+	private static List<String> toList(Object...args) {
+		List<String> l = new LinkedList<String>();
+		for (Object o : args) {
+			if (o.getClass().isArray())
+				for (int i = 0; i < Array.getLength(o); i++)
+					l.add(Array.get(o, i).toString());
+			else if (o instanceof Collection)
+				for (Object o2 : (Collection<?>)o)
+					l.add(o2.toString());
+			else
+				l.add(o.toString());
+		}
+		return l;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/utils/ZipFileList.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/utils/ZipFileList.java b/juneau-core/src/main/java/org/apache/juneau/utils/ZipFileList.java
new file mode 100644
index 0000000..84eb222
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/utils/ZipFileList.java
@@ -0,0 +1,140 @@
+/***************************************************************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations under the License.
+ ***************************************************************************************************************************/
+package org.apache.juneau.utils;
+
+import java.io.*;
+import java.util.*;
+import java.util.zip.*;
+
+/**
+ * Utility class for representing the contents of a zip file as a list of entries
+ * whose contents don't resolve until serialize time.
+ * <p>
+ * Generally associated with <code>RestServlets</code> using the <code>responseHandlers</code>
+ * 	annotation so that REST methods can easily create ZIP file responses by simply returning instances
+ * 	of this class.
+ */
+@SuppressWarnings("serial")
+public class ZipFileList extends LinkedList<ZipFileList.ZipFileEntry> {
+
+	/**
+	 * The name of the zip file.
+	 */
+	public final String fileName;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param fileName The file name of the zip file to create.
+	 */
+	public ZipFileList(String fileName) {
+		this.fileName = fileName;
+	}
+
+	/**
+	 * Add an entry to this list.
+	 *
+	 * @param e The zip file entry.
+	 * @return This object (for method chaining).
+	 */
+	public ZipFileList append(ZipFileEntry e) {
+		add(e);
+		return this;
+	}
+
+	/**
+	 * Interface for ZipFileList entries.
+	 */
+	public static interface ZipFileEntry {
+		/**
+		 * Write this entry to the specified output stream.
+		 *
+		 * @param zos The output stream to write to.
+		 * @throws IOException
+		 */
+		void write(ZipOutputStream zos) throws IOException;
+	}
+
+	/**
+	 * ZipFileList entry for File entry types.
+	 */
+	public static class FileEntry implements ZipFileEntry {
+
+		/** The root file to base the entry paths on. */
+		protected File root;
+
+		/** The file being zipped. */
+		protected File file;
+
+		/**
+		 * Constructor.
+		 *
+		 * @param root The root file that represents the base path.
+		 * @param file The file to add to the zip file.
+		 */
+		public FileEntry(File root, File file) {
+			this.root = root;
+			this.file = file;
+		}
+
+		/**
+		 * Constructor.
+		 *
+		 * @param file The file to add to the zip file.
+		 */
+		public FileEntry(File file) {
+			this.file = file;
+			this.root = (file.isDirectory() ? file : file.getParentFile());
+		}
+
+		@Override /* ZipFileEntry */
+		public void write(ZipOutputStream zos) throws IOException {
+			addFile(zos, file);
+		}
+
+		/**
+		 * Subclasses can override this method to customize which files get added to a zip file.
+		 *
+		 * @param f The file being added to the zip file.
+		 * @return Always returns <jk>true</jk>.
+		 */
+		public boolean doAdd(File f) {
+			return true;
+		}
+
+		/**
+		 * Adds the specified file to the specified output stream.
+		 *
+		 * @param zos The output stream.
+		 * @param f The file to add.
+		 * @throws IOException
+		 */
+		protected void addFile(ZipOutputStream zos, File f) throws IOException {
+			if (doAdd(f)) {
+				if (f.isDirectory()) {
+					File[] fileList = f.listFiles();
+					if (fileList == null)
+						throw new IOException(f.toString());
+					for (File fc : fileList)
+						addFile(zos, fc);
+				} else if (f.canRead()) {
+					String path = f.getAbsolutePath().substring(root.getAbsolutePath().length() + 1).replace('\\', '/');
+					ZipEntry e = new ZipEntry(path);
+					e.setSize(f.length());
+					zos.putNextEntry(e);
+					IOPipe.create(new FileInputStream(f), zos).run();
+				}
+			}
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/utils/package.html
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/utils/package.html b/juneau-core/src/main/java/org/apache/juneau/utils/package.html
new file mode 100644
index 0000000..c3d6a65
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/utils/package.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<!--
+/***************************************************************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *  
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *  
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations under the License.
+ *
+ ***************************************************************************************************************************/
+ -->
+<html>
+<head>
+	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+	<style type="text/css">
+		/* For viewing in Page Designer */
+		@IMPORT url("../../../../../../javadoc.css");
+
+		/* For viewing in REST interface */
+		@IMPORT url("../htdocs/javadoc.css");
+		body { 
+			margin: 20px; 
+		}	
+	</style>
+	<script>
+		/* Replace all @code and @link tags. */	
+		window.onload = function() {
+			document.body.innerHTML = document.body.innerHTML.replace(/\{\@code ([^\}]+)\}/g, '<code>$1</code>');
+			document.body.innerHTML = document.body.innerHTML.replace(/\{\@link (([^\}]+)\.)?([^\.\}]+)\}/g, '<code>$3</code>');
+		}
+	</script>
+</head>
+<body>
+<p>Utility classes</p>
+
+<script>
+	function toggle(x) {
+		var div = x.nextSibling;
+		while (div != null && div.nodeType != 1)
+			div = div.nextSibling;
+		if (div != null) {
+			var d = div.style.display;
+			if (d == 'block' || d == '') {
+				div.style.display = 'none';
+				x.className += " closed";
+			} else {
+				div.style.display = 'block';
+				x.className = x.className.replace(/(?:^|\s)closed(?!\S)/g , '' );
+			}
+		}
+	}
+</script>
+
+</body>
+</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/xml/Namespace.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/xml/Namespace.java b/juneau-core/src/main/java/org/apache/juneau/xml/Namespace.java
new file mode 100644
index 0000000..6b361ce
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/xml/Namespace.java
@@ -0,0 +1,89 @@
+/***************************************************************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations under the License.
+ ***************************************************************************************************************************/
+package org.apache.juneau.xml;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * Represents a simple namespace mapping between a simple name and URI.
+ * <p>
+ * 	In general, the simple name will be used as the XML prefix mapping unless
+ * 	there are conflicts or prefix remappings in the serializer.
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+@Bean(sort=true)
+public final class Namespace implements Comparable<Namespace> {
+	final String name, uri;
+	private final int hashCode;
+
+	/**
+	 * Constructor.
+	 * <p>
+	 * 	Use this constructor when the long name and short name are the same value.
+	 *
+	 * @param name The long and short name of this schema.
+	 * @param uri The URI of this schema.
+	 */
+	@BeanConstructor(properties={"name","uri"})
+	public Namespace(String name, String uri) {
+		this.name = name;
+		this.uri = uri;
+		this.hashCode = (name == null ? 0 : name.hashCode()) + uri.hashCode();
+	}
+
+	/**
+	 * Returns the namespace name.
+	 *
+	 * @return The namespace name.
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * Returns the namespace URI.
+	 *
+	 * @return The namespace URI.
+	 */
+	public String getUri() {
+		return uri;
+	}
+
+	@Override /* Comparable */
+	public int compareTo(Namespace o) {
+		int i = name.compareTo(o.name);
+		if (i == 0)
+			i = uri.compareTo(o.uri);
+		return i;
+	}
+
+	/**
+	 * For performance reasons, equality is always based on identity, since
+	 * the {@link NamespaceFactory} class ensures no duplicate name+uri pairs.
+	 */
+	@Override /* Object */
+	public boolean equals(Object o) {
+		return this == o;
+	}
+
+	@Override /* Object */
+	public int hashCode() {
+		return hashCode;
+	}
+
+	@Override /* Object */
+	public String toString() {
+		return "{name:'"+name+"',uri:'"+uri+"'}";
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/xml/NamespaceFactory.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/xml/NamespaceFactory.java b/juneau-core/src/main/java/org/apache/juneau/xml/NamespaceFactory.java
new file mode 100644
index 0000000..ba89619
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/xml/NamespaceFactory.java
@@ -0,0 +1,130 @@
+/***************************************************************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations under the License.
+ ***************************************************************************************************************************/
+package org.apache.juneau.xml;
+
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Factory class for getting unique instances of {@link Namespace} objects.
+ * <p>
+ * 	For performance reasons, {@link Namespace} objects are stored in {@link IdentityList IdentityLists}.
+ * 	For this to work property, namespaces with the same name and URI must only be represented by a single
+ * 	{@link Namespace} instance.
+ * 	This factory class ensures this identity uniqueness.
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+public final class NamespaceFactory {
+
+	private static ConcurrentHashMap<String,Namespace> cache = new ConcurrentHashMap<String,Namespace>();
+
+	/**
+	 * Get the {@link Namespace} with the specified name and URI, and create a new one
+	 * 	if this is the first time it's been encountered.
+	 *
+	 * @param name The namespace name.  See {@link Namespace#getName()}.
+	 * @param uri The namespace URI.  See {@link Namespace#getUri()}.
+	 * @return The namespace object.
+	 */
+	public static Namespace get(String name, String uri) {
+		String key = name + "+" + uri;
+		Namespace n = cache.get(key);
+		if (n == null) {
+			n = new Namespace(name, uri);
+			Namespace n2 = cache.putIfAbsent(key, n);
+			return (n2 == null ? n : n2);
+		}
+		return n;
+	}
+
+	/**
+	 * Converts the specified object into a {@link Namespace} object.
+	 * <p>
+	 * 	Can be any of following types:
+	 * <ul class='spaced-list'>
+	 * 	<li>A {@link Namespace} object
+	 * 	<li>A JSON string containing a single key/value pair indicating the name/URI mapping.
+	 * 	<li>A <code>Map</code> containing a single key/value pair indicating the name/URI mapping.
+	 * </ul>
+	 *
+	 * @param o The input.
+	 * @return The namespace object, or <jk>null</jk> if the input was <jk>null</jk> or an empty JSON object.
+	 */
+	@SuppressWarnings("rawtypes")
+	public static Namespace parseNamespace(Object o) {
+		if (o == null)
+			return null;
+		if (o instanceof Namespace)
+			return (Namespace)o;
+		try {
+			Map<?,?> m = (o instanceof Map ? (Map)o : new ObjectMap(o.toString()));
+			if (m.size() == 0)
+				return null;
+			if (m.size() > 1)
+				throw new RuntimeException("Too many namespaces specified.  Only one allowed. '"+o+"'");
+			Map.Entry<?,?> e = m.entrySet().iterator().next();
+			return get(e.getKey().toString(), e.getValue().toString());
+		} catch (ParseException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	/**
+	 * Converts the specified object into an array of {@link Namespace} object.
+	 * <p>
+	 * 	Can be any of following types:
+	 * <ul class='spaced-list'>
+	 * 	<li>A {@link Namespace} array
+	 * 	<li>A JSON string with key/value pairs indicating name/URI pairs.
+	 * 	<li>A <code>Map</code> with key/value pairs indicating name/URI pairs.
+	 * 	<li>A <code>Collection</code> containing any of object that can be passed to {@link #parseNamespace(Object)}.
+	 * </ul>
+	 *
+	 * @param o The input.
+	 * @return The namespace objects, or <jk>null</jk> if the input was <jk>null</jk> or an empty JSON object.
+	 */
+	@SuppressWarnings("rawtypes")
+	public static Namespace[] parseNamespaces(Object o) {
+		try {
+			if (o instanceof Namespace[])
+				return (Namespace[])o;
+
+			if (o instanceof CharSequence)
+				o = new ObjectMap(o.toString());
+
+			Namespace[] n;
+			int i = 0;
+			if (o instanceof Collection) {
+				Collection c = (Collection)o;
+				n = new Namespace[c.size()];
+				for (Object o2 : c)
+					n[i++] = parseNamespace(o2);
+			} else if (o instanceof Map) {
+				Map<?,?> m = (Map<?,?>)o;
+				n = new Namespace[m.size()];
+				for (Map.Entry e : m.entrySet())
+					n[i++] = get(e.getKey().toString(), e.getValue().toString());
+			} else {
+				throw new RuntimeException("Invalid type passed to NamespaceFactory.listFromObject: '"+o+"'");
+			}
+			return n;
+		} catch (ParseException e) {
+			throw new RuntimeException(e);
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/xml/XmlBeanMeta.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/xml/XmlBeanMeta.java b/juneau-core/src/main/java/org/apache/juneau/xml/XmlBeanMeta.java
new file mode 100644
index 0000000..d6063f7
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/xml/XmlBeanMeta.java
@@ -0,0 +1,129 @@
+/***************************************************************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations under the License.
+ ***************************************************************************************************************************/
+package org.apache.juneau.xml;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.xml.annotation.*;
+
+/**
+ * Metadata on beans specific to the XML serializers and parsers pulled from the {@link Xml @Xml} annotation on the class.
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ * @param <T> The bean class type.
+ */
+public class XmlBeanMeta<T> {
+
+	// XML related fields
+	private final Map<String,BeanPropertyMeta<T>> xmlAttrs;                        // Map of bean properties that are represented as XML attributes.
+	private final BeanPropertyMeta<T> xmlContent;                                  // Bean property that is represented as XML content within the bean element.
+	private final XmlContentHandler<T> xmlContentHandler;                          // Class used to convert bean to XML content.
+	private final Map<String,BeanPropertyMeta<T>> childElementProperties;          // Properties defined with @Xml.childName annotation.
+	private final BeanMeta<T> beanMeta;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param beanMeta The metadata on the bean that this metadata applies to.
+	 * @param pNames Only look at these property names.  If <jk>null</jk>, apply to all bean properties.
+	 */
+	public XmlBeanMeta(BeanMeta<T> beanMeta, String[] pNames) {
+		this.beanMeta = beanMeta;
+		Class<T> c = beanMeta.getClassMeta().getInnerClass();
+
+		Map<String,BeanPropertyMeta<T>> tXmlAttrs = new LinkedHashMap<String,BeanPropertyMeta<T>>();
+		BeanPropertyMeta<T> tXmlContent = null;
+		XmlContentHandler<T> tXmlContentHandler = null;
+		Map<String,BeanPropertyMeta<T>> tChildElementProperties = new LinkedHashMap<String,BeanPropertyMeta<T>>();
+
+		for (BeanPropertyMeta<T> p : beanMeta.getPropertyMetas(pNames)) {
+			XmlFormat xf = p.getXmlMeta().getXmlFormat();
+			if (xf == XmlFormat.ATTR)
+				tXmlAttrs.put(p.getName(), p);
+			else if (xf == XmlFormat.CONTENT) {
+				if (tXmlContent != null)
+					throw new BeanRuntimeException(c, "Multiple instances of CONTENT properties defined on class.  Only one property can be designated as such.");
+				tXmlContent = p;
+				tXmlContentHandler = p.getXmlMeta().getXmlContentHandler();
+			}
+			// Look for any properties that are collections with @Xml.childName specified.
+			String n = p.getXmlMeta().getChildName();
+			if (n != null) {
+				if (tChildElementProperties.containsKey(n))
+					throw new BeanRuntimeException(c, "Multiple properties found with the name ''{0}''.", n);
+				tChildElementProperties.put(n, p);
+			}
+		}
+
+		xmlAttrs = Collections.unmodifiableMap(tXmlAttrs);
+		xmlContent = tXmlContent;
+		xmlContentHandler = tXmlContentHandler;
+		childElementProperties = (tChildElementProperties.isEmpty() ? null : Collections.unmodifiableMap(tChildElementProperties));
+	}
+
+	/**
+	 * Returns the list of properties annotated with an {@link Xml#format()} of {@link XmlFormat#ATTR}.
+	 * In other words, the list of properties that should be rendered as XML attributes instead of child elements.
+	 *
+	 * @return Metadata on the XML attribute properties of the bean.
+	 */
+	protected Map<String,BeanPropertyMeta<T>> getXmlAttrProperties() {
+		return xmlAttrs;
+	}
+
+	/**
+	 * Returns the bean property annotated with an {@link Xml#format()} value of {@link XmlFormat#CONTENT}
+	 *
+	 * @return The bean property, or <jk>null</jk> if annotation is not specified.
+	 */
+	protected BeanPropertyMeta<T> getXmlContentProperty() {
+		return xmlContent;
+	}
+
+	/**
+	 * Return the XML content handler for this bean.
+	 *
+	 * @return The XML content handler for this bean, or <jk>null</jk> if no content handler is defined.
+	 */
+	protected XmlContentHandler<T> getXmlContentHandler() {
+		return xmlContentHandler;
+	}
+
+	/**
+	 * Returns the child element properties for this bean.
+	 * See {@link Xml#childName()}
+	 *
+	 * @return The child element properties for this bean, or <jk>null</jk> if no child element properties are defined.
+	 */
+	protected Map<String,BeanPropertyMeta<T>> getChildElementProperties() {
+		return childElementProperties;
+	}
+
+	/**
+	 * Returns bean property meta with the specified name.
+	 * This is identical to calling {@link BeanMeta#getPropertyMeta(String)} except it first retrieves
+	 * 	the bean property meta based on the child name (e.g. a property whose name is "people", but whose child name is "person").
+	 *
+	 * @param fieldName The bean property name.
+	 * @return The property metadata.
+	 */
+	protected BeanPropertyMeta<T> getPropertyMeta(String fieldName) {
+		if (childElementProperties != null) {
+			BeanPropertyMeta<T> bpm = childElementProperties.get(fieldName);
+			if (bpm != null)
+				return bpm;
+		}
+		return beanMeta.getPropertyMeta(fieldName);
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/xml/XmlBeanPropertyMeta.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/xml/XmlBeanPropertyMeta.java b/juneau-core/src/main/java/org/apache/juneau/xml/XmlBeanPropertyMeta.java
new file mode 100644
index 0000000..c4c5e67
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/xml/XmlBeanPropertyMeta.java
@@ -0,0 +1,163 @@
+/***************************************************************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations under the License.
+ ***************************************************************************************************************************/
+package org.apache.juneau.xml;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.xml.annotation.*;
+
+/**
+ * Metadata on bean properties specific to the XML serializers and parsers pulled from the {@link Xml @Xml} annotation on the bean property.
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ * @param <T> The bean class.
+ */
+public class XmlBeanPropertyMeta<T> {
+
+	private Namespace namespace = null;
+	private XmlFormat xmlFormat = XmlFormat.NORMAL;
+	private XmlContentHandler<T> xmlContentHandler = null;
+	private String childName;
+	private final BeanPropertyMeta<T> beanPropertyMeta;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param beanPropertyMeta The metadata of the bean property of this additional metadata.
+	 */
+	public XmlBeanPropertyMeta(BeanPropertyMeta<T> beanPropertyMeta) {
+		this.beanPropertyMeta = beanPropertyMeta;
+
+		if (beanPropertyMeta.getField() != null)
+			findXmlInfo(beanPropertyMeta.getField().getAnnotation(Xml.class));
+		if (beanPropertyMeta.getGetter() != null)
+			findXmlInfo(beanPropertyMeta.getGetter().getAnnotation(Xml.class));
+		if (beanPropertyMeta.getSetter() != null)
+			findXmlInfo(beanPropertyMeta.getSetter().getAnnotation(Xml.class));
+
+		if (namespace == null)
+			namespace = beanPropertyMeta.getBeanMeta().getClassMeta().getXmlMeta().getNamespace();
+
+		if (beanPropertyMeta.isBeanUri() && xmlFormat != XmlFormat.ELEMENT)
+			xmlFormat = XmlFormat.ATTR;
+	}
+
+	/**
+	 * Returns the XML namespace associated with this bean property.
+	 * <p>
+	 * 	Namespace is determined in the following order:
+	 * <ol>
+	 * 	<li>{@link Xml#prefix()} annotation defined on bean property field.
+	 * 	<li>{@link Xml#prefix()} annotation defined on bean getter.
+	 * 	<li>{@link Xml#prefix()} annotation defined on bean setter.
+	 * 	<li>{@link Xml#prefix()} annotation defined on bean.
+	 * 	<li>{@link Xml#prefix()} annotation defined on bean package.
+	 * 	<li>{@link Xml#prefix()} annotation defined on bean superclasses.
+	 * 	<li>{@link Xml#prefix()} annotation defined on bean superclass packages.
+	 * 	<li>{@link Xml#prefix()} annotation defined on bean interfaces.
+	 * 	<li>{@link Xml#prefix()} annotation defined on bean interface packages.
+	 * </ol>
+	 *
+	 * @return The namespace associated with this bean property, or <jk>null</jk> if no namespace is
+	 * 	associated with it.
+	 */
+	public Namespace getNamespace() {
+		return namespace;
+	}
+
+	/**
+	 * Returns the XML format of this property from the {@link Xml#format} annotation on this bean property.
+	 *
+	 * @return The XML format, or {@link XmlFormat#NORMAL} if annotation not specified.
+	 */
+	protected XmlFormat getXmlFormat() {
+		return xmlFormat;
+	}
+
+	/**
+	 * Returns the XML content handler of this property from the {@link Xml#contentHandler} annotation on this bean property.
+	 *
+	 * @return The XML content handler, or <jk>null</jk> if annotation not specified.
+	 */
+	protected XmlContentHandler<T> getXmlContentHandler() {
+		return xmlContentHandler;
+	}
+
+	/**
+	 * Returns the child element of this property from the {@link Xml#childName} annotation on this bean property.
+	 *
+	 * @return The child element, or <jk>null</jk> if annotation not specified.
+	 */
+	protected String getChildName() {
+		return childName;
+	}
+
+	/**
+	 * Returns the bean property metadata that this metadata belongs to.
+	 *
+	 * @return The bean property metadata.  Never <jk>null</jk>.
+	 */
+	protected BeanPropertyMeta<T> getBeanPropertyMeta() {
+		return beanPropertyMeta;
+	}
+
+	@SuppressWarnings("unchecked")
+	private void findXmlInfo(Xml xml) {
+		if (xml == null)
+			return;
+		ClassMeta<?> cmProperty = beanPropertyMeta.getClassMeta();
+		ClassMeta<?> cmBean = beanPropertyMeta.getBeanMeta().getClassMeta();
+		String name = beanPropertyMeta.getName();
+		if (! xml.name().isEmpty())
+			throw new BeanRuntimeException(cmBean.getInnerClass(), "Annotation error on property ''{0}''.  Found @Xml.name annotation can only be specified on types.", name);
+
+		List<Xml> xmls = beanPropertyMeta.findAnnotations(Xml.class);
+		List<XmlSchema> schemas = beanPropertyMeta.findAnnotations(XmlSchema.class);
+		namespace = XmlUtils.findNamespace(xmls, schemas);
+
+		if (xmlFormat == XmlFormat.NORMAL)
+			xmlFormat = xml.format();
+
+		boolean isCollection = cmProperty.isCollection() || cmProperty.isArray();
+
+		String cen = xml.childName();
+		if ((! cen.isEmpty()) && (! isCollection))
+			throw new BeanRuntimeException(cmProperty.getInnerClass(), "Annotation error on property ''{0}''.  @Xml.childName can only be specified on collections and arrays.", name);
+
+		if (xmlFormat == XmlFormat.COLLAPSED) {
+			if (isCollection) {
+				if (cen.isEmpty())
+					cen = cmProperty.getXmlMeta().getChildName();
+				if (cen == null || cen.isEmpty())
+					cen = cmProperty.getElementType().getXmlMeta().getElementName();
+				if (cen == null || cen.isEmpty())
+					cen = name;
+			} else {
+				throw new BeanRuntimeException(cmBean.getInnerClass(), "Annotation error on property ''{0}''.  @Xml.format=COLLAPSED can only be specified on collections and arrays.", name);
+			}
+			if (cen.isEmpty() && isCollection)
+				cen = cmProperty.getXmlMeta().getElementName();
+		}
+
+		try {
+			if (xmlFormat == XmlFormat.CONTENT && xml.contentHandler() != XmlContentHandler.NULL.class)
+				xmlContentHandler = (XmlContentHandler<T>) xml.contentHandler().newInstance();
+		} catch (Exception e) {
+			throw new BeanRuntimeException(cmBean.getInnerClass(), "Could not instantiate content handler ''{0}''", xml.contentHandler().getName()).initCause(e);
+		}
+
+		if (! cen.isEmpty())
+			childName = cen;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/xml/XmlClassMeta.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/xml/XmlClassMeta.java b/juneau-core/src/main/java/org/apache/juneau/xml/XmlClassMeta.java
new file mode 100644
index 0000000..1e23d9e
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/xml/XmlClassMeta.java
@@ -0,0 +1,118 @@
+/***************************************************************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations under the License.
+ ***************************************************************************************************************************/
+package org.apache.juneau.xml;
+
+import java.util.*;
+
+import org.apache.juneau.internal.*;
+import org.apache.juneau.xml.annotation.*;
+
+
+/**
+ * Metadata on classes specific to the XML serializers and parsers pulled from the {@link Xml @Xml} annotation on the class.
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+public class XmlClassMeta {
+
+	private final Namespace namespace;
+	private final Xml xml;
+	private final XmlFormat format;
+	private final String elementName;
+	private final String childName;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param c The class that this annotation is defined on.
+	 */
+	public XmlClassMeta(Class<?> c) {
+		this.namespace = findNamespace(c);
+		this.xml =  ReflectionUtils.getAnnotation(Xml.class, c);
+		if (xml != null) {
+			this.format = xml.format();
+			this.elementName = StringUtils.nullIfEmpty(xml.name());
+			this.childName = StringUtils.nullIfEmpty(xml.childName());
+
+		} else {
+			this.format = XmlFormat.NORMAL;
+			this.elementName = null;
+			this.childName = null;
+		}
+	}
+
+	/**
+	 * Returns the {@link Xml} annotation defined on the class.
+	 *
+	 * @return The value of the {@link Xml} annotation defined on the class, or <jk>null</jk> if annotation is not specified.
+	 */
+	protected Xml getAnnotation() {
+		return xml;
+	}
+
+	/**
+	 * Returns the {@link Xml#format()} annotation defined on the class.
+	 *
+	 * @return The value of the {@link Xml#format()} annotation, or {@link XmlFormat#NORMAL} if not specified.
+	 */
+	protected XmlFormat getFormat() {
+		return format;
+	}
+
+	/**
+	 * Returns the {@link Xml#name()} annotation defined on the class.
+	 *
+	 * @return The value of the {@link Xml#name()} annotation, or <jk>null</jk> if not specified.
+	 */
+	protected String getElementName() {
+		return elementName;
+	}
+
+	/**
+	 * Returns the {@link Xml#childName()} annotation defined on the class.
+	 *
+	 * @return The value of the {@link Xml#childName()} annotation, or <jk>null</jk> if not specified.
+	 */
+	protected String getChildName() {
+		return childName;
+	}
+
+	/**
+	 * Returns the XML namespace associated with this class.
+	 * <p>
+	 * 	Namespace is determined in the following order:
+	 * <ol>
+	 * 	<li>{@link Xml#prefix()} annotation defined on class.
+	 * 	<li>{@link Xml#prefix()} annotation defined on package.
+	 * 	<li>{@link Xml#prefix()} annotation defined on superclasses.
+	 * 	<li>{@link Xml#prefix()} annotation defined on superclass packages.
+	 * 	<li>{@link Xml#prefix()} annotation defined on interfaces.
+	 * 	<li>{@link Xml#prefix()} annotation defined on interface packages.
+	 * </ol>
+	 *
+	 * @return The namespace associated with this class, or <jk>null</jk> if no namespace is
+	 * 	associated with it.
+	 */
+	protected Namespace getNamespace() {
+		return namespace;
+	}
+
+	private Namespace findNamespace(Class<?> c) {
+		if (c == null)
+			return null;
+
+		List<Xml> xmls = ReflectionUtils.findAnnotations(Xml.class, c);
+		List<XmlSchema> schemas = ReflectionUtils.findAnnotations(XmlSchema.class, c);
+		return XmlUtils.findNamespace(xmls, schemas);
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/xml/XmlContentHandler.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/xml/XmlContentHandler.java b/juneau-core/src/main/java/org/apache/juneau/xml/XmlContentHandler.java
new file mode 100644
index 0000000..377be5d
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/xml/XmlContentHandler.java
@@ -0,0 +1,139 @@
+/***************************************************************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations under the License.
+ ***************************************************************************************************************************/
+package org.apache.juneau.xml;
+
+import javax.xml.stream.*;
+
+import org.apache.juneau.dto.atom.*;
+import org.apache.juneau.xml.annotation.*;
+
+/**
+ * Customization class that allows a bean (or parts of a bean) to be serialized as XML text or mixed content.
+ * <p>
+ * 	For example, the ATOM specification allows text elements (e.g. title, subtitle...)
+ * 		to be either plain text or XML depending on the value of a <xa>type</xa> attribute.
+ * 	The behavior of text escaping thus depends on that attribute.
+ *
+ * <p class='bcode'>
+ * 	<xt>&lt;feed</xt> <xa>xmlns</xa>=<xs>"http://www.w3.org/2005/Atom"</xs><xt>&gt;</xt>
+ * 		<xt>&lt;title</xt> <xa>type</xa>=<xs>"html"</xs><xt>&gt;</xt>
+ * 			&amp;lt;p&amp;gt;&amp;lt;i&amp;gt;This is the title&amp;lt;/i&amp;gt;&amp;lt;/p&amp;gt;
+ * 		<xt>&lt;/title&gt;</xt>
+ * 		<xt>&lt;title</xt> <xa>type</xa>=<xs>"xhtml"</xs><xt>&gt;</xt>
+ * 			<xt>&lt;div</xt> <xa>xmlns</xa>=<xs>"http://www.w3.org/1999/xhtml"</xs><xt>&gt;</xt>
+ * 				<xt>&lt;p&gt;&lt;i&gt;</xt>This is the subtitle<xt>&lt;/i&gt;&lt;/p&gt;</xt>
+ * 			<xt>&lt;/div&gt;</xt>
+ * 		<xt>&lt;/title&gt;</xt>
+ * 	<xt>&lt;/feed&gt;</xt>
+ * </p>
+ *
+ * <p>
+ * 	The ATOM {@link Text} class (the implementation for both the <xt>&lt;title&gt;</xt> and <xt>&lt;subtitle&gt;</xt>
+ * 		tags shown above) then associates a content handler through the {@link Xml#contentHandler()} annotation
+ * 		on the bean property containing the text, like so...
+ *
+ * <p class='bcode'>
+ * 	<ja>@Xml</ja>(format=<jsf>ATTR</jsf>)
+ * 	<jk>public</jk> String getType() {
+ * 		<jk>return</jk> <jf>type</jf>;
+ * 	}
+ *
+ * 	<ja>@Xml</ja>(format=<jsf>CONTENT</jsf>, contentHandler=TextContentHandler.<jk>class</jk>)
+ * 	<jk>public</jk> String getText() {
+ * 		<jk>return</jk> <jf>text</jf>;
+ * 	}
+ *
+ * 	<jk>public void</jk> setText(String text) {
+ * 		<jk>this</jk>.<jf>text</jf> = text;
+ * 	}
+ * </p>
+ *
+ * <p>
+ * 	The content handler that transforms the output is shown below...
+ *
+ * <p class='bcode'>
+ * 	<jk>public static class</jk> TextContentHandler <jk>implements</jk> XmlContentHandler&lt;Text&gt; {
+ *
+ * 		<ja>@Override</ja>
+ * 		<jk>public void</jk> parse(XMLStreamReader r, Text text) <jk>throws</jk> Exception {
+ * 			String type = text.<jf>type</jf>;
+ * 			<jk>if</jk> (type != <jk>null</jk> && type.equals(<js>"xhtml"</js>))
+ * 				text.<jf>text</jf> = <jsm>decode</jsm>(readXmlContents(r).trim());
+ * 			<jk>else</jk>
+ * 				text.<jf>text</jf> = <jsm>decode</jsm>(r.getElementText().trim());
+ * 		}
+ *
+ * 		<ja>@Override</ja>
+ * 		<jk>public void</jk> serialize(XmlSerializerWriter w, Text text) <jk>throws</jk> Exception {
+ * 			String type = text.<jf>type</jf>;
+ * 			String content = text.<jf>text</jf>;
+ * 			<jk>if</jk> (type != <jk>null</jk> && type.equals(<js>"xhtml"</js>))
+ * 				w.encodeTextInvalidChars(content);
+ * 			<jk>else</jk>
+ * 				w.encodeText(content);
+ * 		}
+ * 	}
+ * </p>
+ *
+ * <h6 class='topic'>Notes</h6>
+ * <ul class='spaced-list'>
+ * 	<li>The {@link Xml#contentHandler()} annotation can only be specified on a bean class, or a bean property
+ * 		of format {@link XmlFormat#CONTENT}.
+ * </ul>
+ *
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ * @param <T> The class type of the bean
+ */
+public interface XmlContentHandler<T> {
+
+	/**
+	 * Represents <jk>null</jk> on the {@link Xml#contentHandler()} annotation.
+	 */
+	public static interface NULL extends XmlContentHandler<Object> {}
+
+	/**
+	 * Reads XML element content the specified reader and sets the appropriate value on the specified bean.
+	 * <p>
+	 * 	When this method is called, the attributes have already been parsed and set on the bean.
+	 * 	Therefore, if the content handling is different based on some XML attribute (e.g.
+	 * 		<code><xa>type</xa>=<xs>"text/xml"</xs></code> vs <code><xa>type</xa>=<xs>"text/plain"</xs></code>)
+	 * 		then that attribute value can be obtained via the set bean property.
+	 *
+	 * @param r The XML stream reader.
+	 * 	When called, the reader is positioned on the element containing the text to read.
+	 * 	For example, calling <code>r.getElementText()</code> can be called immediately
+	 * 	to return the element text if the element contains only characters and whitespace.
+	 * 	However typically, the stream is going to contain XML elements that need to
+	 * 	be handled special (otherwise you wouldn't need to use an <code>XmlContentHandler</code>
+	 * 	to begin with).
+	 * @param bean The bean where the parsed contents are going to be placed.
+	 * 	Subclasses determine how the content maps to values in the bean.
+	 * 	However, typically the contents map to a single property on the bean.
+	 * @throws Exception If any problem occurs.  Causes parse to fail.
+	 */
+	public void parse(XMLStreamReader r, T bean) throws Exception;
+
+	/**
+	 * Writes XML element content from values in the specified bean.
+	 *
+	 * @param w The XML output writer.
+	 * 	When called, the XML element/attributes and
+	 * 		whitespace/indentation (if enabled) have already been written to the stream.
+	 * 	Subclasses must simply write the contents of the element.
+	 * @param bean The bean whose values will be converted to XML content.
+	 * @throws Exception If any problems occur.  Causes serialize to fail.
+	 */
+	public void serialize(XmlWriter w, T bean) throws Exception;
+
+}