org.geoserver.template.FeatureWrapper Maven / Gradle / Ivy
/* 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.template;
import java.sql.Time;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.util.AbstractMap;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.geotools.data.DataUtilities;
import org.geotools.feature.FeatureCollection;
import org.geotools.util.MapEntry;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import com.vividsolutions.jts.geom.Geometry;
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.CollectionModel;
import freemarker.ext.beans.IteratorModel;
import freemarker.template.Configuration;
import freemarker.template.SimpleHash;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
/**
* Wraps a {@link Feature} in the freemarker {@link BeansWrapper} interface
* allowing a template to be directly applied to a {@link Feature} or
* {@link FeatureCollection}.
*
* When a {@link FeatureCollection} is being processed by the template, it is
* available via the $features
variable, which can be broken down
* into single features and attributes following this hierarchy:
*
* - features -> feature
*
* - fid (String)
* - typeName (String)
* - attributes -> attribute
*
* - value (String), a default String representation of the attribute value
* - rawValue (Object), the actual attribute value if it's non null, the
* empty string otherwise
* - name (String)
* - type (String)
* - isGeometry (Boolean)
*
*
*
* Example of a template processing a feature collection which will print out
* the features id of every feature in the collection.
*
*
* <#list features as feature>
* FeatureId: ${feature.fid}
* </#list>
*
*
*
*
* To use this wrapper,use the
* {@link Configuration#setObjectWrapper(freemarker.template.ObjectWrapper)}
* method:
*
*
*
* //features we want to apply template to
* FeatureCollection features = ...;
* //create the configuration and set the wrapper
* Configuration cfg = new Configuration();
* cfg.setObjectWrapper( new FeatureWrapper() );
* //get the template and go
* Template template = cfg.getTemplate( "foo.ftl" );
* template.process( features, System.out );
*
*
*
*
*
*
* @author Justin Deoliveira, The Open Planning Project, [email protected]
* @author Andrea Aime, TOPP
* @author Gabriel Roldan, TOPP
*/
public class FeatureWrapper extends BeansWrapper {
public FeatureWrapper() {
setSimpleMapWrapper(true);
}
/**
* Returns a sensible String value for attributes so they are easily used by
* templates.
*
* Special cases:
*
* - for Date values returns a default {@link DateFormat} representation
* - for Boolean values returns "true" or "false"
* - for null values returns an empty string
* - for any other value returns its toString()
*
*
*
* @param o
* could be an instance of Date (a special case)
* @return the formated date as a String, or the object
*/
protected String wrapValue(Object o) {
return valueToString(o);
}
/**
* Returns a sensible String value for attributes so they are easily used by
* templates.
*
* Special cases:
*
* - for Date values returns a default {@link DateFormat} representation
* - for Boolean values returns "true" or "false"
* - for null values returns an empty string
* - for any other value returns its toString()
*
*
*
* @param o
* the object for which to return a String representation
* suitable to be used as template content
* @return the formated date as a String, or the object
*/
private static String valueToString(Object o) {
if (o == null) {
// nulls throw tempaltes off, use empty string
return "";
}
if (o instanceof Date) {
if ( o instanceof Timestamp ) {
return DateFormat.getDateTimeInstance().format((Date)o);
}
if ( o instanceof Time ) {
return DateFormat.getTimeInstance().format((Date)o);
}
return DateFormat.getInstance().format((Date) o);
}
if (o instanceof Boolean) {
return ((Boolean) o).booleanValue() ? "true" : "false";
}
if (o instanceof Geometry) {
return String.valueOf(o);
}
return String.valueOf(o);
}
public TemplateModel wrap(Object object) throws TemplateModelException {
// check for feature collection
if (object instanceof FeatureCollection) {
// create a model with just one variable called 'features'
SimpleHash map = new SimpleHash();
FeatureCollection featureCollection = (FeatureCollection) object;
// this will load all the features into memory!
//List features = DataUtilities.list( featureCollection );
//map.put("features", new CollectionModel( features, this));
// this has the risk of leaking memory (if anything goes wrong)
// map.put("features", new IteratorModel( featureCollection.iterator(), this));
// this one is almost right; depends on finalizer to close iterator
map.put("features", new FeatureCollectionModel( featureCollection, this ) );
map.put("type", wrap(((FeatureCollection) object).getSchema()));
return map;
} else if (object instanceof SimpleFeatureType) {
SimpleFeatureType ft = (SimpleFeatureType) object;
// create a variable "attributes" which his a list of all the
// attributes, but at the same time, is a map keyed by name
Map attributeMap = new LinkedHashMap();
for (int i = 0; i < ft.getAttributeCount(); i++) {
AttributeDescriptor type = ft.getDescriptor(i);
Map attribute = new HashMap();
attribute.put("name", type.getLocalName());
attribute.put("type", type.getType().getBinding().getName());
attribute.put("isGeometry", Boolean.valueOf(Geometry.class.isAssignableFrom(type.getType().getBinding())));
attributeMap.put(type.getLocalName(), attribute);
}
// build up the result, feature type is represented by its name an
// attributes
SimpleHash map = new SimpleHash();
map.put("attributes", new SequenceMapModel(attributeMap, this));
map.put("name", ft.getTypeName());
return map;
} else if (object instanceof SimpleFeature) {
SimpleFeature feature = (SimpleFeature) object;
// create the model
SimpleHash map = new SimpleHash();
// first add the feature id
map.put("fid", feature.getID());
map.put("typeName", feature.getFeatureType().getTypeName());
// next create the Map representing the per attribute useful
// properties for a template
Map attributeMap = new FeatureAttributesMap(feature);
map.putAll(attributeMap);
// create a variable "attributes" which his a list of all the
// attributes, but at the same time, is a map keyed by name
map.put("attributes", new SequenceMapModel(attributeMap, this));
return map;
}
return super.wrap(object);
}
/**
* Adapts a Feature to a java.util.Map, where the map keys are the feature
* attribute names and the values other Map representing the Feature
* name/value attributes.
*
* A special purpose Map implementation is used in order to lazily return
* the attribute properties, most notably the toString representation of
* attribute values.
*
*
* @author Gabriel Roldan
* @see AttributeMap
*/
private static class FeatureAttributesMap extends AbstractMap {
private Set entrySet;
private SimpleFeature feature;
public FeatureAttributesMap(SimpleFeature feature) {
this.feature = feature;
}
public Set entrySet() {
if (entrySet == null) {
entrySet = new LinkedHashSet();
final List types = feature.getFeatureType().getAttributeDescriptors();
final int attributeCount = types.size();
String attName;
Map attributesMap;
for (int i = 0; i < attributeCount; i++) {
attName = types.get(i).getLocalName();
attributesMap = new AttributeMap(attName, feature);
entrySet.add(new MapEntry(attName, attributesMap));
}
}
return entrySet;
}
}
/**
* Wraps a Feature as a
* Map<String, Map<String, Object>>
.
*
* The Map keys are the wrapped feature's property names and the Map values
* are Maps with appropriate key/value pairs for each feature attribute.
*
*
* For instance, the value attribute Maps hold the following properties:
*
* - name: String holding the attribute name
* - type: String with the java class name bound to the attribute type
* - value: String representation of the attribute value suitable to be
* used directly in a template expression.
null
values are
* returned as the empty string, non String values as per
* {@link FeatureWrapper#valueToString(Object)}
* - rawValue: the actual attribute value as it is in the Feature
* - isGeometry: Boolean indicating whether the attribute is of a
* geometric type
*
*
*
*/
private static class AttributeMap extends AbstractMap {
private final String attributeName;
private final SimpleFeature feature;
private Set entrySet;
/**
* Builds an "attribute map" as used in templates for the given
* attribute of the given feature.
*
* @param attributeName
* the name of the feature attribute this attribute map is
* built for
* @param feature
* the feature where to lazily grab the attribute named
* attributeName
from
*/
public AttributeMap(final String attributeName, final SimpleFeature feature) {
this.attributeName = attributeName;
this.feature = feature;
}
/**
* Override so asking for the hashCode does not implies traversing the
* whole map and thus calling entrySet() prematurely
*/
public int hashCode() {
return attributeName.hashCode();
}
/**
* Returns this map's entry set. An entry for each of the properties
* mentioned in this class's javadoc is returned. Of special interest is
* the entry for the "value"
property, which is lazily
* evaluated through the use of a {@link DeferredValueEntry}
*/
public Set entrySet() {
if (entrySet == null) {
entrySet = new LinkedHashSet();
final SimpleFeatureType featureType = feature.getFeatureType();
final AttributeDescriptor attributeType = featureType.getDescriptor(attributeName);
final Object value = feature.getAttribute(attributeName);
entrySet.add(new DeferredValueEntry("value", value));
entrySet.add(new MapEntry("name", attributeName));
entrySet.add(new MapEntry("type", attributeType.getType().getBinding().getName()));
Object rawValue = value == null ? "" : value;
boolean isGeometry = Geometry.class.isAssignableFrom(attributeType.getType().getBinding());
entrySet.add(new MapEntry("isGeometry", Boolean.valueOf(isGeometry)));
entrySet.add(new MapEntry("rawValue", rawValue));
}
return entrySet;
}
/**
* A special purpose Map.Entry whose value is transformed to String on
* demand, thus avoiding to hold both the actual value object and its
* string value.
*
* @see FeatureWrapper#valueToString(Object)
*/
private static class DeferredValueEntry extends MapEntry {
private static final long serialVersionUID = -3919798947862996744L;
public DeferredValueEntry(String key, Object attribute) {
super(key, attribute);
}
/**
* Returns the value corresponding to this entry, as a String.
*/
public Object getValue() {
Object actualValue = super.getValue();
String stringValue = FeatureWrapper.valueToString(actualValue);
return stringValue;
}
}
}
}