/* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
* This code is licensed under the GPL 2.0 license, availible at the root
* application directory.
*/
package org.geoserver.rest.format;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.ows.util.ClassProperties;
import org.geoserver.ows.util.OwsUtils;
import org.geoserver.rest.PageInfo;
import org.geotools.util.logging.Logging;
import org.restlet.data.MediaType;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.ext.freemarker.TemplateRepresentation;
import org.restlet.resource.Representation;
import org.restlet.resource.Resource;
import freemarker.core.ParseException;
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.CollectionModel;
import freemarker.ext.beans.MapModel;
import freemarker.template.Configuration;
import freemarker.template.ObjectWrapper;
import freemarker.template.SimpleHash;
import freemarker.template.Template;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
/**
* Data format for serializing objects as HTML.
* <p>
* <h2>Template Lookup</h2>
* When serializing an object the following this class looks up a template using the
* following methods:
* <ol>
* <li>The result of {@link #getTemplateName(Object)} if not null.
* <li>The resource uri being requested. If the requested uri is "/rest/foo" then a template
* named "foo.ftl" is searched for.
* <li>The class of the object being serialized. If an object of org.acme.Foo is
* being serialized then a template named "Foo.ftl" is searched for.
* </ol>
* </p>
* <p>
* <h2>Template Variables</h2>
* Variables provided to the template are created via reflection on the object
* being serialized and placed into a map called "properties". For instance consider
* the following class of object:
* <pre>
* class MyObject {
*
* String getFoo();
*
* Integer getBar();
* }
* </pre>
* In the template values for the properties "foo" and "bar" would be available via:
* <pre>
* ${properties.foo}
* ${properties.bar}
* </pre>
* </p>
* <p>
* A variable called "page" is also provided which contains information about the resource being
* accessed. See {@link #createPageDetails(Request)} for more details.
* </p>
* <p>
* <h2>Reflection</h2>
* The template data model is created reflectively via the {@link ObjectToMapWrapper} class. This
* class can be extended to provide custom properties. See the javadoc for more details.
* </p>
*
* @author Justin Deoliveira, OpenGEO
*
*/
public class ReflectiveHTMLFormat extends DataFormat {
private static final Logger LOGGER = Logging.getLogger("org.geoserver.rest");
/**
* The request/response being serviced
*/
Request request;
Response response;
/**
* The resource
*/
Resource resource;
/**
* The class used for reflection
*/
protected Class clazz;
/**
* Creates a new instance of the format.
*/
public ReflectiveHTMLFormat(Request request,Response response,Resource resource) {
this(null,request,response,resource);
}
/**
* Creates a new instance of the format specifying the type of object being serialized.
* <p>
* This constructor is useful when reflection should be executed against an interface as opposed
* to the concrete class of object being serialized.
* </p>
*/
public ReflectiveHTMLFormat( Class clazz, Request request,Response response, Resource resource ) {
super( MediaType.TEXT_HTML );
this.clazz = clazz;
this.request = request;
this.resource = resource;
}
@Override
public Representation toRepresentation(Object object) {
Class clazz = this.clazz != null ? this.clazz : object.getClass();
Configuration configuration = createConfiguration(object, clazz);
final ObjectWrapper wrapper = configuration.getObjectWrapper();
configuration.setObjectWrapper(new ObjectWrapper() {
public TemplateModel wrap(Object obj) throws TemplateModelException {
TemplateModel model = wrapper.wrap(obj);
if ( model instanceof SimpleHash ) {
SimpleHash hash = (SimpleHash) model;
if ( hash.get( "page" ) == null ) {
PageInfo pageInfo = (PageInfo) request.getAttributes().get( PageInfo.KEY );
if ( pageInfo != null ) {
hash.put( "page", pageInfo );
}
}
}
return model;
}
});
Template template = null;
//first try finding a name directly
String templateName = getTemplateName( object );
if ( templateName != null ) {
template = tryLoadTemplate(configuration, templateName);
if(template == null)
template = tryLoadTemplate(configuration, templateName + ".ftl");
}
//next look up by the resource being requested
if ( template == null && request != null ) {
//could not find a template bound to the class directly, search by the resource
// being requested
String r = request.getResourceRef().getLastSegment();
if(r.equals(""))
r = request.getResourceRef().getBaseRef().getLastSegment();
int i = r.lastIndexOf( "." );
if ( i != -1 ) {
r = r.substring( 0, i );
}
template = tryLoadTemplate(configuration, r + ".ftl");
}
//finally try to find by class
while( template == null && clazz != null ) {
template = tryLoadTemplate(configuration, clazz.getSimpleName() + ".ftl");
if(template == null) {
for ( Class interfaze : clazz.getInterfaces() ) {
template = tryLoadTemplate(configuration, interfaze.getSimpleName() + ".ftl" );
if(template != null)
break;
}
}
//move up the class hierachy to continue to look for a matching template
if ( clazz.getSuperclass() == Object.class ) {
break;
}
clazz = clazz.getSuperclass();
}
if ( template != null ) {
templateName = template.getName();
}
else {
//use a fallback
templateName = "Object.ftl";
}
return new TemplateRepresentation( templateName, configuration, object, getMediaType() );
}
/**
* Tries to load a template, will return null if it's not found. If the template exists
* but it contains syntax errors an exception will be thrown instead
*
* @param configuration The template configuration.
* @param templateName The name of the template to load.
*/
protected Template tryLoadTemplate(Configuration configuration, String templateName) {
try {
return configuration.getTemplate(templateName);
} catch(ParseException e) {
throw new RuntimeException(e);
} catch(IOException io) {
LOGGER.log(Level.FINE, "Failed to lookup template " + templateName, io);
return null;
}
}
protected Configuration createConfiguration(Object data, Class clazz) {
Configuration cfg = new Configuration( );
cfg.setObjectWrapper( new ObjectToMapWrapper( clazz ));
cfg.setClassForTemplateLoading(ReflectiveHTMLFormat.class,"");
return cfg;
}
/**
* @throws UnsupportedOperationException
*/
public Object readRepresentation(Representation representation) {
throw new UnsupportedOperationException( "Reading not implemented.");
}
/**
* A hook into the template look-up mechanism.
* <p>
* This implementation returns null but subclasses may overide to explicitly specify the name
* of the template to be used.
* </p>
* @param data The object being serialized.
*/
protected String getTemplateName( Object data ) {
return null;
}
/**
* Wraps the object being serialized in a {@link SimpleHash} template model.
* <p>
* The method {@link #wrapInternal(Map, SimpleHash, Object)} may be overriden to customize
* the returned model.
* </p>
*/
protected class ObjectToMapWrapper<T> extends BeansWrapper {
/**
* The class of object being serialized.
*/
Class<T> clazz;
public ObjectToMapWrapper( Class<T> clazz ) {
this.clazz = clazz;
}
@Override
public TemplateModel wrap(Object object) throws TemplateModelException {
if ( object instanceof Collection ) {
Collection c = (Collection) object;
if (c.isEmpty()) {
SimpleHash hash = new SimpleHash();
hash.put( "values", new CollectionModel( c, this ) );
return hash;
}
else {
Object o = c.iterator().next();
if ( clazz.isAssignableFrom( o.getClass() ) ) {
SimpleHash hash = new SimpleHash();
hash.put( "values", new CollectionModel( c, this ) );
return hash;
}
}
}
if ( object != null && clazz.isAssignableFrom( object.getClass() ) ) {
HashMap map = new HashMap();
ClassProperties cp = OwsUtils.getClassProperties(clazz);
for ( String p : cp.properties() ) {
if ( "Class".equals( p ) ) continue;
Object value = OwsUtils.get(object, p);
if ( value == null ) {
value = "null";
}
map.put( Character.toLowerCase(p.charAt(0)) + p.substring(1), value.toString());
}
SimpleHash model = new SimpleHash();
model.put( "properties", new MapModel(map, this) );
model.put( "className", clazz.getSimpleName() );
wrapInternal(map, model, (T) object);
return model;
}
return super.wrap(object);
}
/**
* Template method to customize the returned template 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 properties, SimpleHash model, T object ) {
}
}
}