/* (c) 2017 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.rest;
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.CollectionModel;
import freemarker.ext.beans.MapModel;
import freemarker.template.SimpleHash;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import org.geoserver.ows.util.ClassProperties;
import org.geoserver.ows.util.OwsUtils;
import org.geotools.util.logging.Logging;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Wraps the object being serialized in a {@link SimpleHash} template model.
* <p>
* The method {@link #wrapInternal(Map, SimpleHash, Object)} may be overridden to customize
* the returned model.
* </p>
*/
public class ObjectToMapWrapper<T> extends BeansWrapper {
private static final Logger LOGGER = Logging.getLogger("org.geoserver.rest");
/**
* The class of object being serialized.
*/
Class<T> clazz;
Collection<Class> classesToExpand;
/**
* Constructs an ObjectToMapWrapper for the provided clazz.
* @param clazz
*/
public ObjectToMapWrapper(Class<T> clazz ) {
this(clazz, Collections.EMPTY_LIST);
}
/**
* Constructs an ObjectToMapWrapper for the provided clazz.
* Any child properties that match classesToExpand will be unwrapped to a map
* @param clazz
* @param classesToExpand
*/
public ObjectToMapWrapper(Class<T> clazz, Collection<Class> classesToExpand) {
this.clazz = clazz;
this.classesToExpand = classesToExpand;
}
/**
* Constructs a {@link SimpleHash} representing the passed object.
*
* If the object is already a SimpleHash, it is returned.
* If the object is a {@link Collection}, with contents matching {@link #clazz}, a SimpleHash with a single entry
* is returned:
*
* "values", containing the collection, as a {@link CollectionModel}
*
* If the object is an {@link Object}, that matches {@link #clazz}, a SimpleHash with a two entries is returned:
*
* "properties", containing a {@link MapModel} representing the object. Map entries are populated using reflection
* to get the property names and values. See {@link OwsUtils#get(Object, String)} for more details. If any values
* have a class assignable to any class included in {@link #classesToExpand}, those values are likewise extracted
* into a map. Otherwise the toString method of that object is called.
*
* If none of the above give a result, {@link BeansWrapper#wrap(Object)} is returned.
*
* @param object Object to wrap
* @return A {@link SimpleHash} representing the passed object.
* @throws TemplateModelException
*/
@SuppressWarnings("unchecked")
@Override
public TemplateModel wrap(Object object) throws TemplateModelException {
if ( object instanceof SimpleHash) {
return (SimpleHash) object;
}
if ( object instanceof Collection) {
Collection c = (Collection) object;
if (c.isEmpty() || clazz.isAssignableFrom(c.iterator().next().getClass())) {
SimpleHash hash = new SimpleHash();
hash.put("values", new CollectionModel(c, this));
setRequestInfo(hash);
wrapInternal(hash, (Collection<T>) object);
return hash;
}
}
if ( object != null && clazz.isAssignableFrom( object.getClass() ) ) {
Map<String, Object> map = objectToMap(object, clazz);
SimpleHash model = new SimpleHash();
model.put( "properties", new MapModel(map, this) );
model.put( "className", clazz.getSimpleName() );
setRequestInfo(model);
wrapInternal(map, model, (T) object);
return model;
}
return super.wrap(object);
}
/**
* Converts the provided object to a map using reflection on on clazz.
*
* If any values
* have a class assignable to any class included in {@link #classesToExpand}, those values are likewise extracted
* into a map.
*
* @param object Object to convert.
* @param clazz The advertized class of the object, from which the map keys are generated.
* @return
*/
protected Map<String, Object> objectToMap(Object object, Class clazz) {
HashMap<String, Object> map = new HashMap<>();
ClassProperties cp = OwsUtils.getClassProperties(clazz);
for ( String p : cp.properties() ) {
if ( "Class".equals( p ) ) continue;
Object value;
try {
value = OwsUtils.get(object, p);
} catch(Exception e) {
LOGGER.log(Level.WARNING, "Could not resolve property " + p + " of bean " + object, e);
value = "** Failed to retrieve value of property " + p + ". Error message is: " + e.getMessage() + "**";
}
if ( value == null ) {
value = "null";
}
String key = Character.toLowerCase(p.charAt(0)) + p.substring(1);
Class valueClass = getClassForUnwrapping(value);
if (value instanceof Collection) {
List values = new ArrayList();
for (Object o : (Collection) value) {
valueClass = getClassForUnwrapping(o);
if (valueClass == null) {
values.add(o == null ? "" : o.toString());
} else {
values.add(objectToMap(o, valueClass));
}
}
map.put(key, new CollectionModel(values, this));
} else if (valueClass == null) {
map.put(key, value.toString());
} else {
map.put(key, objectToMap(value, valueClass));
}
}
return map;
}
private Class getClassForUnwrapping(Object o) {
for (Class clazz : classesToExpand) {
if (clazz.isAssignableFrom(o.getClass())) {
return clazz;
}
}
return null;
}
private static Set<Class<?>> getValueTypes()
{
Set<Class<?>> ret = new HashSet<>();
//primitives
ret.add(Boolean.class);
ret.add(Character.class);
ret.add(Byte.class);
ret.add(Short.class);
ret.add(Integer.class);
ret.add(Long.class);
ret.add(Float.class);
ret.add(Double.class);
ret.add(Void.class);
//other values
ret.add(String.class);
return ret;
}
/**
* Add {@link RequestInfo} to the freemarker model
*
* @param model
* @throws TemplateModelException
*/
protected void setRequestInfo(SimpleHash model) throws TemplateModelException {
final RequestInfo requestInfo = RequestInfo.get();
if (model.get("page") == null) {
if (requestInfo != null) {
model.put("page", requestInfo);
}
}
}
/**
* Template method to customize the returned template model.
* Called in the case of a map model
*
* @param properties A map of properties obtained reflectively from the object being
* serialized.
* @param model The resulting template model.
* @param object The object being serialized.
*/
protected void wrapInternal(Map<String, Object> properties, SimpleHash model, T object) {
}
/**
* Template method to customize the returned template model.
* Called in the case of a list model
*
* @param model The resulting template model.
* @param object The object being serialized.
*/
protected void wrapInternal(SimpleHash model, Collection<T> object) {
}
}