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':' > 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><feed</xt> <xa>xmlns</xa>=<xs>"http://www.w3.org/2005/Atom"</xs><xt>></xt>
+ * <xt><title</xt> <xa>type</xa>=<xs>"html"</xs><xt>></xt>
+ * &lt;p&gt;&lt;i&gt;This is the title&lt;/i&gt;&lt;/p&gt;
+ * <xt></title></xt>
+ * <xt><title</xt> <xa>type</xa>=<xs>"xhtml"</xs><xt>></xt>
+ * <xt><div</xt> <xa>xmlns</xa>=<xs>"http://www.w3.org/1999/xhtml"</xs><xt>></xt>
+ * <xt><p><i></xt>This is the subtitle<xt></i></p></xt>
+ * <xt></div></xt>
+ * <xt></title></xt>
+ * <xt></feed></xt>
+ * </p>
+ *
+ * <p>
+ * The ATOM {@link Text} class (the implementation for both the <xt><title></xt> and <xt><subtitle></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<Text> {
+ *
+ * <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;
+
+}