![JAR search and dependency download from the Maven repository](/logo.png)
org.apache.juneau.objecttools.ObjectRest Maven / Gradle / Ivy
// ***************************************************************************************************************************
// * 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.objecttools;
import static java.net.HttpURLConnection.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import org.apache.juneau.*;
import org.apache.juneau.collections.*;
import org.apache.juneau.json.*;
import org.apache.juneau.parser.*;
/**
* POJO REST API.
*
*
* 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.
*
*
* A POJO model is defined as a tree model where nodes consist of consisting of the following:
*
* -
* {@link Map Maps} and Java beans representing JSON objects.
*
-
* {@link Collection Collections} and arrays representing JSON arrays.
*
-
* Java beans.
*
*
*
* Leaves of the tree can be any type of object.
*
*
* Use {@link #get(String) get()} to retrieve an element from a JSON tree.
*
Use {@link #put(String,Object) put()} to create (or overwrite) an element in a JSON tree.
*
Use {@link #post(String,Object) post()} to add an element to a list in a JSON tree.
*
Use {@link #delete(String) delete()} to remove an element from a JSON tree.
*
*
* Leading slashes in URLs are ignored.
* So "/xxx/yyy/zzz" and "xxx/yyy/zzz" are considered identical.
*
*
Example:
*
* // Construct an unstructured POJO model
* JsonMap map = JsonMap.ofJson (""
* + "{"
* + " name:'John Smith', "
* + " address:{ "
* + " streetAddress:'21 2nd Street', "
* + " city:'New York', "
* + " state:'NY', "
* + " postalCode:10021 "
* + " }, "
* + " phoneNumbers:[ "
* + " '212 555-1111', "
* + " '212 555-2222' "
* + " ], "
* + " additionalInfo:null, "
* + " remote:false, "
* + " height:62.4, "
* + " 'fico score':' > 640' "
* + "} "
* );
*
* // Wrap Map inside an ObjectRest object
* ObjectRest johnSmith = ObjectRest.create (map );
*
* // Get a simple value at the top level
* // "John Smith"
* String name = johnSmith .getString("name" );
*
* // Change a simple value at the top level
* johnSmith .put("name" , "The late John Smith" );
*
* // Get a simple value at a deep level
* // "21 2nd Street"
* String streetAddress = johnSmith .getString("address/streetAddress" );
*
* // Set a simple value at a deep level
* johnSmith .put("address/streetAddress" , "101 Cemetery Way" );
*
* // Get entries in a list
* // "212 555-1111"
* String firstPhoneNumber = johnSmith .getString("phoneNumbers/0" );
*
* // Add entries to a list
* johnSmith .post("phoneNumbers" , "212 555-3333" );
*
* // Delete entries from a model
* johnSmith .delete("fico score" );
*
* // Add entirely new structures to the tree
* JsonMap medicalInfo = JsonMap.ofJson (""
* + "{"
* + " currentStatus: 'deceased',"
* + " health: 'non-existent',"
* + " creditWorthiness: 'not good'"
* + "}"
* );
* johnSmith .put("additionalInfo/medicalInfo" , medicalInfo );
*
*
*
* 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.
*
The syntax is {@code @attr=val}, where attr is the attribute name on the child map, and val is the matching value.
*
*
Example:
*
* // Get map/bean with name attribute value of 'foo' from a list of items
* Map map = objectRest .getMap("/items/@name=foo" );
*
*
* See Also:
*
*/
@SuppressWarnings({"unchecked","rawtypes"})
public final class ObjectRest {
//-----------------------------------------------------------------------------------------------------------------
// Static
//-----------------------------------------------------------------------------------------------------------------
/** The list of possible request types. */
private static final int GET=1, PUT=2, POST=3, DELETE=4;
/**
* Static creator.
* @param o The object being wrapped.
* @return A new {@link ObjectRest} object.
*/
public static ObjectRest create(Object o) {
return new ObjectRest(o);
}
/**
* Static creator.
* @param o The object being wrapped.
* @param parser The parser to use for parsing arguments and converting objects to the correct data type.
* @return A new {@link ObjectRest} object.
*/
public static ObjectRest create(Object o, ReaderParser parser) {
return new ObjectRest(o, parser);
}
//-----------------------------------------------------------------------------------------------------------------
// Instance
//-----------------------------------------------------------------------------------------------------------------
private ReaderParser parser = JsonParser.DEFAULT;
final BeanSession session;
/** 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.
*
*
* Uses {@link BeanContext#DEFAULT} for working with Java beans.
*
* @param o The object to be wrapped.
*/
public ObjectRest(Object o) {
this(o, null);
}
/**
* Create a new instance of a REST interface over the specified object.
*
*
* 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 ObjectRest(Object o, ReaderParser parser) {
this.session = parser == null ? BeanContext.DEFAULT_SESSION : parser.getBeanContext().getSession();
if (parser == null)
parser = JsonParser.DEFAULT;
this.parser = parser;
this.root = new JsonNode(null, null, o, session.object());
}
/**
* Call this method to prevent the root object from being overwritten on put("", xxx); calls.
*
* @return This object.
*/
public ObjectRest 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 getWithDefault(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 getWithDefault(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.
*
*
* Will convert object to the specified type per {@link BeanSession#convertToType(Object, Class)}.
*
*
Examples:
*
* ObjectRest objectRest = new ObjectRest(object );
*
* // Value converted to a string.
* String string = objectRest .get("path/to/string" , String.class );
*
* // Value converted to a bean.
* MyBean bean = objectRest .get("path/to/bean" , MyBean.class );
*
* // Value converted to a bean array.
* MyBean[] beanArray = objectRest .get("path/to/beanarray" , MyBean[].class );
*
* // Value converted to a linked-list of objects.
* List list = objectRest .get("path/to/list" , LinkedList.class );
*
* // Value converted to a map of object keys/values.
* Map map = objectRest .get("path/to/map" , TreeMap.class );
*
*
* @param url
* The URL of the element to retrieve.
* If null or blank, returns the root.
* @param type The specified object type.
*
* @param The specified object type.
* @return The addressed element, or null if that element does not exist in the tree.
*/
public T get(String url, Class type) {
return getWithDefault(url, null, type);
}
/**
* Retrieves the element addressed by the URL as the specified object type.
*
*
* Will convert object to the specified type per {@link BeanSession#convertToType(Object, Class)}.
*
*
* The type can be a simple type (e.g. beans, strings, numbers) or parameterized type (collections/maps).
*
*
Examples:
*
* ObjectRest objectRest = new ObjectRest(object );
*
* // Value converted to a linked-list of strings.
* List<String> list1 = objectRest .get("path/to/list1" , LinkedList.class , String.class );
*
* // Value converted to a linked-list of beans.
* List<MyBean> list2 = objectRest .get("path/to/list2" , LinkedList.class , MyBean.class );
*
* // Value converted to a linked-list of linked-lists of strings.
* List<List<String>> list3 = objectRest .get("path/to/list3" , LinkedList.class , LinkedList.class , String.class );
*
* // Value converted to a map of string keys/values.
* Map<String,String> map1 = objectRest .get("path/to/map1" , TreeMap.class , String.class , String.class );
*
* // Value converted to a map containing string keys and values of lists containing beans.
* Map<String,List<MyBean>> map2 = objectRest .get("path/to/map2" , TreeMap.class , String.class , List.class , MyBean.class );
*
*
*
* Collection classes are assumed to be followed by zero or one objects indicating the element type.
*
*
* Map classes are assumed to be followed by zero or two meta objects indicating the key and value types.
*
*
* The array can be arbitrarily long to indicate arbitrarily complex data structures.
*
*
Notes:
* -
* Use the {@link #get(String, Class)} method instead if you don't need a parameterized map/collection.
*
*
* @param url
* The URL of the element to retrieve.
* If null or blank, returns the root.
* @param type The specified object type.
* @param args The specified object parameter types.
*
* @param The specified object type.
* @return The addressed element, or null if that element does not exist in the tree.
*/
public T get(String url, Type type, Type...args) {
return getWithDefault(url, null, type, args);
}
/**
* Same as {@link #get(String, Class)} but returns a default value if the addressed element is null or non-existent.
*
* @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 type The specified object type.
*
* @param The specified object type.
* @return The addressed element, or null if that element does not exist in the tree.
*/
public T getWithDefault(String url, T def, Class type) {
Object o = service(GET, url, null);
if (o == null)
return def;
return session.convertToType(o, type);
}
/**
* Same as {@link #get(String,Type,Type[])} but returns a default value if the addressed element is null or non-existent.
*
* @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 type The specified object type.
* @param args The specified object parameter types.
*
* @param The specified object type.
* @return The addressed element, or null if that element does not exist in the tree.
*/
public T getWithDefault(String url, T def, Type type, Type...args) {
Object o = service(GET, url, null);
if (o == null)
return def;
return session.convertToType(o, type, args);
}
/**
* Returns the specified entry value converted to a {@link String}.
*
*
* Shortcut for get(String.class , key)
.
*
* @param url The key.
* @return The converted value, or null if the map contains no mapping for this key.
*/
public String getString(String url) {
return get(url, String.class);
}
/**
* Returns the specified entry value converted to a {@link String}.
*
*
* Shortcut for get(String.class , key, defVal)
.
*
* @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 getWithDefault(url, defVal, String.class);
}
/**
* Returns the specified entry value converted to an {@link Integer}.
*
*
* Shortcut for get(Integer.class , key)
.
*
* @param url The key.
* @return The converted value, or null if the map contains no mapping for this key.
* @throws InvalidDataConversionException If value cannot be converted.
*/
public Integer getInt(String url) {
return get(url, Integer.class);
}
/**
* Returns the specified entry value converted to an {@link Integer}.
*
*
* Shortcut for get(Integer.class , key, defVal)
.
*
* @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 getWithDefault(url, defVal, Integer.class);
}
/**
* Returns the specified entry value converted to a {@link Long}.
*
*
* Shortcut for get(Long.class , key)
.
*
* @param url The key.
* @return The converted value, or null if the map contains no mapping for this key.
* @throws InvalidDataConversionException If value cannot be converted.
*/
public Long getLong(String url) {
return get(url, Long.class);
}
/**
* Returns the specified entry value converted to a {@link Long}.
*
*
* Shortcut for get(Long.class , key, defVal)
.
*
* @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 getWithDefault(url, defVal, Long.class);
}
/**
* Returns the specified entry value converted to a {@link Boolean}.
*
*
* Shortcut for get(Boolean.class , key)
.
*
* @param url The key.
* @return The converted value, or null if the map contains no mapping for this key.
* @throws InvalidDataConversionException If value cannot be converted.
*/
public Boolean getBoolean(String url) {
return get(url, Boolean.class);
}
/**
* Returns the specified entry value converted to a {@link Boolean}.
*
*
* Shortcut for get(Boolean.class , key, defVal)
.
*
* @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 getWithDefault(url, defVal, Boolean.class);
}
/**
* Returns the specified entry value converted to a {@link Map}.
*
*
* Shortcut for get(Map.class , key)
.
*
* @param url The key.
* @return The converted value, or null if the map contains no mapping for this key.
* @throws InvalidDataConversionException If value cannot be converted.
*/
public Map,?> getMap(String url) {
return get(url, Map.class);
}
/**
* Returns the specified entry value converted to a {@link Map}.
*
*
* Shortcut for get(Map.class , key, defVal)
.
*
* @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 getWithDefault(url, defVal, Map.class);
}
/**
* Returns the specified entry value converted to a {@link List}.
*
*
* Shortcut for get(List.class , key)
.
*
* @param url The key.
* @return The converted value, or null if the map contains no mapping for this key.
* @throws InvalidDataConversionException If value cannot be converted.
*/
public List> getList(String url) {
return get(url, List.class);
}
/**
* Returns the specified entry value converted to a {@link List}.
*
*
* Shortcut for get(List.class , key, defVal)
.
*
* @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 getWithDefault(url, defVal, List.class);
}
/**
* Returns the specified entry value converted to a {@link Map}.
*
*
* Shortcut for get(JsonMap.class , key)
.
*
* @param url The key.
* @return The converted value, or null if the map contains no mapping for this key.
* @throws InvalidDataConversionException If value cannot be converted.
*/
public JsonMap getJsonMap(String url) {
return get(url, JsonMap.class);
}
/**
* Returns the specified entry value converted to a {@link JsonMap}.
*
*
* Shortcut for get(JsonMap.class , key, defVal)
.
*
* @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 JsonMap getJsonMap(String url, JsonMap defVal) {
return getWithDefault(url, defVal, JsonMap.class);
}
/**
* Returns the specified entry value converted to a {@link JsonList}.
*
*
* Shortcut for get(JsonList.class , key)
.
*
* @param url The key.
* @return The converted value, or null if the map contains no mapping for this key.
* @throws InvalidDataConversionException If value cannot be converted.
*/
public JsonList getJsonList(String url) {
return get(url, JsonList.class);
}
/**
* Returns the specified entry value converted to a {@link JsonList}.
*
*
* Shortcut for get(JsonList.class , key, defVal)
.
*
* @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 JsonList getJsonList(String url, JsonList defVal) {
return getWithDefault(url, defVal, JsonList.class);
}
/**
* 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.
*
* Can be any of the following formats:
*
* -
* Method name only. e.g.
"myMethod" .
* -
* Method name with class names. e.g.
"myMethod(String,int)" .
* -
* Method name with fully-qualified class names. e.g.
"myMethod(java.util.String,int)" .
*
*
* As a rule, use the simplest format needed to uniquely resolve a method.
* @param args
* The arguments to pass as parameters to the method.
* These will automatically be converted to the appropriate object type if possible.
* This must be an array, like a JSON array.
* @return The returned object from the method call.
* @throws ExecutableException Exception occurred on invoked constructor/method/field.
* @throws ParseException Malformed input encountered.
* @throws IOException Thrown by underlying stream.
*/
public Object invokeMethod(String url, String method, String args) throws ExecutableException, ParseException, IOException {
try {
return new ObjectIntrospector(get(url), parser).invokeMethod(method, args);
} catch (NoSuchMethodException | IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
throw new ExecutableException(e);
}
}
/**
* 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 getPublicMethods(String url) {
Object o = get(url);
if (o == null)
return null;
return session.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.
*
*
* This method expands the POJO model as necessary to create the new element.
*
* @param url
* The URL of the element to create.
* If null 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 null 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.
*
*
* The URL is the address of the list being added to.
*
*
* If the list does not already exist, it will be created.
*
*
* This method expands the POJO model as necessary to create the new element.
*
*
Notes:
* -
* You can only post to three types of nodes:
*
* - {@link List Lists}
*
- {@link Map Maps} containing integers as keys (i.e sparse arrays)
*
- arrays
*
*
*
* @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.
*
*
* If the element does not exist, no action is taken.
*
* @param url
* The URL of the element being deleted.
* If null 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 static 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 ObjectRestException {
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.isEmpty()) {
if (rootLocked)
throw new ObjectRestException(HTTP_FORBIDDEN, "Cannot overwrite root object");
Object o = root.o;
root = new JsonNode(null, null, val, session.object());
return o;
}
JsonNode n = (parentUrl == null ? root : getNode(parentUrl, root));
if (n == null)
throw new ObjectRestException(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 = session.toBeanMap(po);
m.put(n.keyName, o);
return url;
}
throw new ObjectRestException(HTTP_BAD_REQUEST, "Cannot perform PUT on ''{0}'' with parent node type ''{1}''", url, pct);
}
if (cm.isBean())
return session.toBeanMap(o).put(childKey, val);
throw new ObjectRestException(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.isEmpty()) {
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 ObjectRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' of type ''{1}''", url, cm);
}
JsonNode n = getNode(url, root);
if (n == null)
throw new ObjectRestException(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 = session.toBeanMap(po);
m.put(childKey, o2);
return url + "/" + (o2.length-1);
}
throw new ObjectRestException(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 ObjectRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' of type ''{1}''", url, cm);
}
if (method == DELETE) {
if (url.isEmpty()) {
if (rootLocked)
throw new ObjectRestException(HTTP_FORBIDDEN, "Cannot overwrite root object");
Object o = root.o;
root = new JsonNode(null, null, null, session.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 = session.toBeanMap(po);
m.put(n.keyName, o2);
return old;
}
throw new ObjectRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' with parent node type ''{1}''", url, pct);
}
if (cm.isBean())
return session.toBeanMap(o).put(childKey, null);
throw new ObjectRestException(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 static 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 = session.object();
else
cm = session.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 = session.toBeanMap(o);
o2 = m.get(parentKey);
BeanPropertyMeta pMeta = m.getPropertyMeta(parentKey);
if (pMeta == null)
throw new ObjectRestException(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 session.convertToType(in, cm);
return in;
}
private static int parseInt(String key) {
try {
return Integer.parseInt(key);
} catch (NumberFormatException e) {
throw new ObjectRestException(HTTP_BAD_REQUEST,
"Cannot address an item in an array with a non-integer key ''{0}''", key
);
}
}
}