/* ===================================================================
* SimpleXmlView.java
*
* Created Aug 14, 2008 5:07:50 PM
*
* Copyright (c) 2008 Solarnetwork.net Dev Team.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ===================================================================
*/
package net.solarnetwork.web.support;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.solarnetwork.util.ClassUtils;
/**
* Spring {@link org.springframework.web.servlet.View} for turning objects into
* XML through JavaBean introspection.
*
* <p>
* The character encoding of the output must be specified in the
* {@link #setContentType(String)} (e.g. {@literal text/xml;charset=UTF-8}).
* </p>
*
* <p>
* The configurable properties of this class are:
* </p>
*
* <dl>
* <dt>rootElementName</dt>
* <dd>The name of the root XML element to use.</dd>
*
* <dt>singleBeanAsRoot</dt>
* <dd>TODO</dd>
*
* <dt>useModelTimeZoneForDates</dt>
* <dd>TODO</dd>
*
* <dt>modelKey</dt>
* <dd>TODO</dd>
*
* <dt>rootElementAugmentor</dt>
* <dd>TODO</dd>
*
* <dt>classNamesAllowedForNesting</dt>
* <dd>TODO</dd>
* </dl>
*
* @author matt
* @version 1.1
*/
public class SimpleXmlView extends AbstractView {
/** Default content type. */
public static final String DEFAULT_XML_CONTENT_TYPE = "text/xml;charset=UTF-8";
private static final ThreadLocal<SimpleDateFormat> SDF = new ThreadLocal<SimpleDateFormat>();
private static final Pattern AMP = Pattern.compile("&(?!\\w+;)");
private String rootElementName = "root";
private boolean singleBeanAsRoot = true;
private boolean useModelTimeZoneForDates = true;
private String modelKey = null;
private ViewResponseAugmentor rootElementAugmentor = null;
private Set<String> classNamesAllowedForNesting = null;
/**
* Constructor.
*/
public SimpleXmlView() {
setContentType(DEFAULT_XML_CONTENT_TYPE);
}
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
Map<String, Object> finalModel = setupDateFormat(model);
response.setContentType(getContentType());
String charset = getResponseCharacterEncoding();
OutputStreamWriter out;
try {
out = new OutputStreamWriter(response.getOutputStream(), charset);
} catch ( UnsupportedEncodingException e ) {
throw new RuntimeException(e);
}
// write XML start
out.write("<?xml version=\"1.0\" encoding=\"");
out.write(charset);
out.write("\"?>\n");
Object singleBean = finalModel.size() == 1 && this.singleBeanAsRoot ? finalModel.values()
.iterator().next() : null;
if ( singleBean != null ) {
outputObject(singleBean, finalModel.keySet().iterator().next().toString(), out,
rootElementAugmentor);
} else {
writeElement(this.rootElementName, null, out, false, rootElementAugmentor);
for ( Map.Entry<String, Object> me : finalModel.entrySet() ) {
outputObject(me.getValue(), me.getKey(), out, null);
}
// end root element
closeElement(this.rootElementName, out);
}
out.flush();
SDF.remove();
}
/**
* Create a {@link SimpleDateFormat} and cache on the {@link #SDF}
* ThreadLocal to re-use for all dates within a single response.
*
* @param model
* the model, to look for a TimeZone to format the dates in
*/
private Map<String, Object> setupDateFormat(Map<String, Object> model) {
TimeZone tz = TimeZone.getTimeZone("GMT");
Map<String, Object> result = new LinkedHashMap<String, Object>();
for ( Map.Entry<String, Object> me : model.entrySet() ) {
Object o = me.getValue();
if ( useModelTimeZoneForDates && o instanceof TimeZone ) {
tz = (TimeZone) o;
} else if ( modelKey != null ) {
if ( modelKey.equals(me.getKey()) ) {
result.put(modelKey, o);
}
} else { //if ( !(o instanceof BindingResult) ) {
result.put(me.getKey(), o);
}
}
SimpleDateFormat sdf = new SimpleDateFormat();
if ( tz.getRawOffset() == 0 ) {
sdf.applyPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
} else {
sdf.applyPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
}
sdf.setTimeZone(tz);
if ( logger.isTraceEnabled() ) {
logger.trace("TZ offset " + tz.getRawOffset());
}
SDF.set(sdf);
return result;
}
private void outputObject(Object o, String name, Writer out, ViewResponseAugmentor augmentor)
throws IOException {
if ( o instanceof Collection ) {
Collection<?> col = (Collection<?>) o;
outputCollection(col, name, out, augmentor);
} else if ( o instanceof Map ) {
Map<?, ?> map = (Map<?, ?>) o;
outputMap(map, name, out, augmentor);
} else if ( o instanceof String || o instanceof Number ) {
// for simple types, write as unified <value type="String" value="foo"/>
// this happens often in collections / maps of simple data types
Map<String, Object> params = new LinkedHashMap<String, Object>(2);
params.put("type", org.springframework.util.ClassUtils.getShortName(o.getClass()));
params.put("value", o);
writeElement("value", params, out, true, augmentor);
} else {
String elementName = (o == null ? name : org.springframework.util.ClassUtils.getShortName(o
.getClass()));
writeElement(elementName, o, out, true, augmentor);
}
}
private void outputMap(Map<?, ?> map, String name, Writer out, ViewResponseAugmentor augmentor)
throws IOException {
writeElement(name, null, out, false, augmentor);
// for each entry, write an <entry> element
for ( Map.Entry<?, ?> me : map.entrySet() ) {
String entryName = me.getKey().toString();
out.write("<entry key=\"");
out.write(entryName);
out.write("\">");
Object value = me.getValue();
if ( value instanceof Collection ) {
// special collection case, we don't add nested element
for ( Object o : (Collection<?>) value ) {
outputObject(o, "value", out, augmentor);
}
} else {
outputObject(value, null, out, augmentor);
}
closeElement("entry", out);
}
closeElement(name, out);
}
private void outputCollection(Collection<?> col, String name, Writer out,
ViewResponseAugmentor augmentor) throws IOException {
writeElement(name, null, out, false, augmentor);
for ( Object o : col ) {
outputObject(o, null, out, null);
}
closeElement(name, out);
}
private void writeElement(String name, Map<?, ?> props, Writer out, boolean close,
ViewResponseAugmentor augmentor) throws IOException {
out.write('<');
out.write(name);
if ( augmentor != null ) {
augmentor.augmentResponse(out);
}
Map<String, Object> nested = null;
if ( props != null ) {
for ( Map.Entry<?, ?> me : props.entrySet() ) {
String key = me.getKey().toString();
Object val = me.getValue();
if ( getPropertySerializerRegistrar() != null ) {
val = getPropertySerializerRegistrar().serializeProperty(name, val.getClass(),
props, val);
}
if ( val instanceof Date ) {
SimpleDateFormat sdf = SDF.get();
// SimpleDateFormat has no way to create xs:dateTime with tz,
// so use trick here to insert required colon for non GMT dates
Date date = (Date) val;
StringBuilder buf = new StringBuilder(sdf.format(date));
if ( buf.charAt(buf.length() - 1) != 'Z' ) {
buf.insert(buf.length() - 2, ':');
}
val = buf.toString();
} else if ( val instanceof Collection ) {
if ( nested == null ) {
nested = new LinkedHashMap<String, Object>(5);
}
nested.put(key, val);
val = null;
} else if ( val instanceof Map<?, ?> ) {
if ( nested == null ) {
nested = new LinkedHashMap<String, Object>(5);
}
nested.put(key, val);
val = null;
} else if ( classNamesAllowedForNesting != null && !(val instanceof Enum<?>) ) {
for ( String prefix : classNamesAllowedForNesting ) {
if ( val.getClass().getName().startsWith(prefix) ) {
if ( nested == null ) {
nested = new LinkedHashMap<String, Object>(5);
}
nested.put(key, val);
val = null;
break;
}
}
}
if ( val != null ) {
// replace & with &
String attVal = val.toString();
Matcher matcher = AMP.matcher(attVal);
attVal = matcher.replaceAll("&");
attVal = attVal.replace("\"", """);
out.write(' ');
out.write(key);
out.write("=\"");
out.write(attVal);
out.write('"');
}
}
}
if ( close && nested == null ) {
out.write('/');
}
out.write('>');
if ( nested != null ) {
for ( Map.Entry<String, Object> me : nested.entrySet() ) {
outputObject(me.getValue(), me.getKey(), out, augmentor);
}
if ( close ) {
closeElement(name, out);
}
}
}
private void writeElement(String name, Object bean, Writer out, boolean close,
ViewResponseAugmentor augmentor) throws IOException {
if ( getPropertySerializerRegistrar() != null && bean != null ) {
// try whole-bean serialization first
Object o = getPropertySerializerRegistrar().serializeProperty(name, bean.getClass(), bean,
bean);
if ( o != bean ) {
if ( o != null ) {
outputObject(o, name, out, augmentor);
}
return;
}
}
Map<String, Object> props = ClassUtils.getBeanProperties(bean, null, true);
writeElement(name, props, out, close, augmentor);
}
private void closeElement(String name, Writer out) throws IOException {
out.write("</");
out.write(name);
out.write('>');
}
public String getRootElementName() {
return rootElementName;
}
public void setRootElementName(String rootElementName) {
this.rootElementName = rootElementName;
}
public boolean isSingleBeanAsRoot() {
return singleBeanAsRoot;
}
public void setSingleBeanAsRoot(boolean singleBeanAsRoot) {
this.singleBeanAsRoot = singleBeanAsRoot;
}
public boolean isUseModelTimeZoneForDates() {
return useModelTimeZoneForDates;
}
public void setUseModelTimeZoneForDates(boolean useModelTimeZoneForDates) {
this.useModelTimeZoneForDates = useModelTimeZoneForDates;
}
public String getModelKey() {
return modelKey;
}
public void setModelKey(String modelKey) {
this.modelKey = modelKey;
}
public ViewResponseAugmentor getRootElementAugmentor() {
return rootElementAugmentor;
}
public void setRootElementAugmentor(ViewResponseAugmentor rootElementAugmentor) {
this.rootElementAugmentor = rootElementAugmentor;
}
public Set<String> getClassNamesAllowedForNesting() {
return classNamesAllowedForNesting;
}
public void setClassNamesAllowedForNesting(Set<String> classNamesAllowedForNesting) {
this.classNamesAllowedForNesting = classNamesAllowedForNesting;
}
}