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 ([email protected]) + */ +@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 ([email protected]) + */ +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 ([email protected]) + */ +@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 ([email protected]) + */ +@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 ([email protected]) + */ +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 ([email protected]) + * @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 ([email protected]) + * @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 ([email protected]) + */ +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 ([email protected]) + * @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; + +}
