/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed 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.springframework.flex.http;
import java.io.ByteArrayOutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.AbstractView;
import flex.messaging.FlexContext;
import flex.messaging.io.SerializationContext;
import flex.messaging.io.amf.Amf3Output;
import flex.messaging.io.amf.AmfTrace;
/**
* Spring-MVC {@link View} that renders AMF content by serializing the model for the current request using
* BlazeDS's AMF serialization/deserialization APIs.
*
* <p>By default, the entire contents of the model map (with the exception of framework-specific classes) will be
* encoded as AMF. For cases where the contents of the map need to be filtered, users may specify a specific set of
* model attributes to encode via the {@link #setRenderedAttributes(Set) renderedAttributes} property.
*
* @author Jeremy Grelle
*/
public class AmfView extends AbstractView {
public static final String DEFAULT_CONTENT_TYPE = "application/x-amf";
private static final Log log = LogFactory.getLog(AmfView.class);
private Set<String> renderedAttributes;
private boolean disableCaching = true;
public AmfView() {
setContentType(DEFAULT_CONTENT_TYPE);
}
/**
* Returns the attributes in the model that should be rendered by this view.
*/
public Set<String> getRenderedAttributes() {
return renderedAttributes;
}
/**
* Sets the attributes in the model that should be rendered by this view. When set, all other model attributes will be
* ignored.
*/
public void setRenderedAttributes(Set<String> renderedAttributes) {
this.renderedAttributes = renderedAttributes;
}
/**
* Disables caching of the generated AMF response.
*
* <p>Default is {@code true}, which will prevent the client from caching the generated AMF response.
*/
public void setDisableCaching(boolean disableCaching) {
this.disableCaching = disableCaching;
}
/**
* {@inheritDoc}
*/
@Override
protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
response.setContentType(getContentType());
response.setCharacterEncoding("UTF-8");
if (disableCaching) {
response.addHeader("Pragma", "no-cache");
response.addHeader("Cache-Control", "no-cache, no-store, max-age=0");
response.addDateHeader("Expires", 1L);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
Object value = filterModel(model);
try {
AmfTrace trace = null;
if (log.isDebugEnabled()) {
trace = new AmfTrace();
}
ByteArrayOutputStream outBuffer = new ByteArrayOutputStream();
SerializationContext context = new SerializationContext();
Amf3Output out = new Amf3Output(context);
if (trace != null) {
out.setDebugTrace(trace);
}
out.setOutputStream(outBuffer);
out.writeObject(value);
out.flush();
outBuffer.flush();
response.setContentLength(outBuffer.size());
outBuffer.writeTo(response.getOutputStream());
if (log.isDebugEnabled()) {
log.debug("Wrote AMF message:\n" + trace);
}
} finally {
FlexContext.clearThreadLocalObjects();
SerializationContext.clearThreadLocalObjects();
}
}
/**
* Filters out undesired attributes from the given model. The return value can be either another {@link Map}, or a
* single value object. If only a single attribute is present in the model map, that value will be returned instead
* of the full map.
*
* <p>Default implementation removes {@link BindingResult} instances and entries not included in the {@link
* #setRenderedAttributes(Set) renderedAttributes} property.
*
* @param model the model, as passed on to {@link #renderMergedOutputModel}
* @return the object to be rendered
*/
protected Object filterModel(Map<String, Object> model) {
Map<String, Object> result = new HashMap<String, Object>(model.size());
Set<String> renderedAttributes =
!CollectionUtils.isEmpty(this.renderedAttributes) ? this.renderedAttributes : model.keySet();
for (Map.Entry<String, Object> entry : model.entrySet()) {
if (!(entry.getValue() instanceof BindingResult) && renderedAttributes.contains(entry.getKey())) {
result.put(entry.getKey(), entry.getValue());
}
}
if (result.size() == 1) {
return result.values().iterator().next();
} else {
return result;
}
}
}