/**
* Copyright 2005-2010 hdiv.org
*
* 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.hdiv.webflow.mvc.servlet;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
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.hdiv.webflow.validator.EditableParameterValidator;
import org.springframework.binding.convert.ConversionExecutor;
import org.springframework.binding.convert.ConversionService;
import org.springframework.binding.expression.EvaluationException;
import org.springframework.binding.expression.Expression;
import org.springframework.binding.expression.ExpressionParser;
import org.springframework.binding.expression.ParserContext;
import org.springframework.binding.expression.support.FluentParserContext;
import org.springframework.binding.expression.support.StaticExpression;
import org.springframework.binding.mapping.MappingResult;
import org.springframework.binding.mapping.MappingResults;
import org.springframework.binding.mapping.MappingResultsCriteria;
import org.springframework.binding.mapping.impl.DefaultMapper;
import org.springframework.binding.mapping.impl.DefaultMapping;
import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.message.MessageContextErrors;
import org.springframework.binding.message.MessageResolver;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.validation.BindingResult;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.web.servlet.View;
import org.springframework.web.util.WebUtils;
import org.springframework.webflow.context.ExternalContext;
import org.springframework.webflow.core.collection.AttributeMap;
import org.springframework.webflow.core.collection.ParameterMap;
import org.springframework.webflow.definition.TransitionDefinition;
import org.springframework.webflow.engine.builder.BinderConfiguration;
import org.springframework.webflow.engine.builder.BinderConfiguration.Binding;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.FlowExecutionKey;
import org.springframework.webflow.execution.RequestContext;
import org.springframework.webflow.mvc.view.AbstractMvcView;
import org.springframework.webflow.mvc.view.AbstractMvcViewFactory;
import org.springframework.webflow.mvc.view.BindingModel;
import org.springframework.webflow.mvc.view.ViewActionStateHolder;
import org.springframework.webflow.validation.ValidationHelper;
public class ServletMvcViewHDIV extends AbstractMvcView {
private final EditableParameterValidator editableParameterValidator = new EditableParameterValidator();
private static final Log logger = LogFactory.getLog(ServletMvcViewHDIV.class);
private static final MappingResultsCriteria PROPERTY_NOT_FOUND_ERROR = new PropertyNotFoundError();
private static final MappingResultsCriteria MAPPING_ERROR = new MappingError();
private final org.springframework.web.servlet.View view;
private final RequestContext requestContext;
private ExpressionParser expressionParser;
private ConversionService conversionService;
private String fieldMarkerPrefix = "_";
private String eventIdParameterName = "_eventId";
private String eventId;
private MappingResults mappingResults;
private BinderConfiguration binderConfiguration;
private MessageCodesResolver messageCodesResolver;
private boolean userEventProcessed;
public ServletMvcViewHDIV(View view, RequestContext context) {
super(view, context);
this.view = view;
this.requestContext = context;
}
/**
* Sets the expression parser to use to parse model expressions.
* @param expressionParser the expression parser
*/
@Override
public void setExpressionParser(ExpressionParser expressionParser) {
this.expressionParser = expressionParser;
}
/**
* Sets the service to use to expose formatters for field values.
* @param conversionService the conversion service
*/
@Override
public void setConversionService(ConversionService conversionService) {
this.conversionService = conversionService;
}
/**
* Sets the configuration describing how this view should bind to its model to access data for rendering.
* @param binderConfiguration the model binder configuration
*/
@Override
public void setBinderConfiguration(BinderConfiguration binderConfiguration) {
this.binderConfiguration = binderConfiguration;
}
/**
* Set the message codes resolver to use to resolve bind and validation failure message codes.
* @param messageCodesResolver the binding error message code resolver to use
*/
@Override
public void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) {
this.messageCodesResolver = messageCodesResolver;
}
/**
* Specify a prefix that can be used for parameters that mark potentially empty fields, having "prefix + field" as
* name. Such a marker parameter is checked by existence: You can send any value for it, for example "visible". This
* is particularly useful for HTML checkboxes and select options.
* <p>
* Default is "_", for "_FIELD" parameters (e.g. "_subscribeToNewsletter"). Set this to null if you want to turn off
* the empty field check completely.
* <p>
* HTML checkboxes only send a value when they're checked, so it is not possible to detect that a formerly checked
* box has just been unchecked, at least not with standard HTML means.
* <p>
* This auto-reset mechanism addresses this deficiency, provided that a marker parameter is sent for each checkbox
* field, like "_subscribeToNewsletter" for a "subscribeToNewsletter" field. As the marker parameter is sent in any
* case, the data binder can detect an empty field and automatically reset its value.
*/
@Override
public void setFieldMarkerPrefix(String fieldMarkerPrefix) {
this.fieldMarkerPrefix = fieldMarkerPrefix;
}
/**
* Sets the name of the request parameter to use to lookup user events signaled by this view. If not specified, the
* default is <code>_eventId</code>
* @param eventIdParameterName the event id parameter name
*/
@Override
public void setEventIdParameterName(String eventIdParameterName) {
this.eventIdParameterName = eventIdParameterName;
}
/* M�todos de AbstractMvcView */
@Override
public void render() throws IOException {
Map model = new HashMap();
model.putAll(flowScopes());
exposeBindingModel(model);
model.put("flowRequestContext", requestContext);
FlowExecutionKey key = requestContext.getFlowExecutionContext().getKey();
if (key != null) {
model.put("flowExecutionKey", requestContext.getFlowExecutionContext().getKey().toString());
model.put("flowExecutionUrl", requestContext.getFlowExecutionUrl());
}
model.put("currentUser", requestContext.getExternalContext().getCurrentUser());
try {
if (logger.isDebugEnabled()) {
logger.debug("Rendering MVC [" + view + "] with model map [" + model + "]");
}
doRender(model);
} catch (IOException e) {
throw e;
} catch (Exception e) {
IllegalStateException ise = new IllegalStateException("Exception occurred rendering view " + view);
ise.initCause(e);
throw ise;
}
}
@Override
public boolean userEventQueued() {
return !userEventProcessed && getEventId() != null;
}
@Override
public void processUserEvent() {
String eventId = getEventId();
if (eventId == null) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Processing user event '" + eventId + "'");
}
Object model = getModelObject();
if (model != null) {
if (logger.isDebugEnabled()) {
logger.debug("Resolved model " + model);
}
TransitionDefinition transition = requestContext.getMatchingTransition(eventId);
if (shouldBind(model, transition)) {
mappingResults = bind(model);
if (hasErrors(mappingResults)) {
if (logger.isDebugEnabled()) {
logger.debug("Model binding resulted in errors; adding error messages to context");
}
addErrorMessages(mappingResults);
}
if (shouldValidate(model, transition)) {
validate(model);
}
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("No model to bind to; done processing user event");
}
}
userEventProcessed = true;
}
@Override
public Serializable getUserEventState() {
return new ViewActionStateHolder(eventId, userEventProcessed, mappingResults);
}
@Override
public boolean hasFlowEvent() {
return userEventProcessed && !requestContext.getMessageContext().hasErrorMessages();
}
@Override
public Event getFlowEvent() {
if (!hasFlowEvent()) {
return null;
}
return new Event(this, getEventId(), requestContext.getRequestParameters().asAttributeMap());
}
@Override
public void saveState() {
}
@Override
public String toString() {
return new ToStringCreator(this).append("view", view).toString();
}
// subclassing hooks
/**
* Returns the current flow request context.
* @return the flow request context
*/
@Override
protected RequestContext getRequestContext() {
return requestContext;
}
/**
* Returns the Spring MVC view to render
* @return the view
*/
@Override
protected org.springframework.web.servlet.View getView() {
return view;
}
/**
* Returns the id of the user event being processed.
* @return the user event
*/
@Override
protected String getEventId() {
if (eventId == null) {
eventId = determineEventId(requestContext);
}
return this.eventId;
}
/**
* Determines if model data binding should be invoked given the Transition that matched the current user event being
* processed. Returns true unless the <code>bind</code> attribute of the Transition has been set to false.
* Subclasses may override.
* @param model the model data binding would be performed on
* @param transition the matched transition
* @return true if binding should occur, false if not
*/
@Override
protected boolean shouldBind(Object model, TransitionDefinition transition) {
if (transition == null) {
return true;
}
return transition.getAttributes().getBoolean("bind", Boolean.TRUE).booleanValue();
}
/**
* Returns the results of binding to the view's model, if model binding has occurred.
* @return the binding (mapping) results
*/
@Override
protected MappingResults getMappingResults() {
return mappingResults;
}
/**
* Returns the binding configuration that defines how to connect properties of the model to UI elements.
* @return an instance of {@link BinderConfiguration} or null.
*/
@Override
protected BinderConfiguration getBinderConfiguration() {
return binderConfiguration;
}
/**
* Returns the EL parser to be used for data binding purposes.
* @return an instance of {@link ExpressionParser}.
*/
@Override
protected ExpressionParser getExpressionParser() {
return expressionParser;
}
/**
* Returns the prefix that can be used for parameters that mark potentially empty fields.
* @return the prefix value.
*/
@Override
protected String getFieldMarkerPrefix() {
return fieldMarkerPrefix;
}
/**
* Obtain the user event from the current flow request. The default implementation returns the value of the request
* parameter with name {@link #setEventIdParameterName(String) eventIdParameterName}. Subclasses may override.
* @param context the current flow request context
* @return the user event that occurred
*/
@Override
protected String determineEventId(RequestContext context) {
return WebUtils.findParameterValue(context.getRequestParameters().asMap(), eventIdParameterName);
}
/**
* <p>
* Causes the model to be populated from information contained in request parameters.
* </p>
* <p>
* If a view has binding configuration then only model fields specified in the binding configuration will be
* considered. In the absence of binding configuration all request parameters will be used to update matching fields
* on the model.
* </p>
*
* @param model the model to be updated
* @return an instance of MappingResults with information about the results of the binding.
*/
@Override
protected MappingResults bind(Object model) {
if (logger.isDebugEnabled()) {
logger.debug("Binding to model");
}
DefaultMapper mapper = new DefaultMapper();
ParameterMap requestParameters = requestContext.getRequestParameters();
if (binderConfiguration != null) {
addModelBindings(mapper, requestParameters.asMap().keySet(), model);
} else {
addDefaultMappings(mapper, requestParameters.asMap().keySet(), model);
}
return mapper.map(requestParameters, model);
}
/**
* <p>
* Adds a {@link DefaultMapping} for every configured view {@link Binding} for which there is an incoming request
* parameter. If there is no matching incoming request parameter, a special mapping is created that will set the
* target field on the model to an empty value (typically null).
* </p>
*
* @param mapper the mapper to which mappings will be added
* @param parameterNames the request parameters
* @param model the model
*/
@Override
protected void addModelBindings(DefaultMapper mapper, Set parameterNames, Object model) {
Iterator it = binderConfiguration.getBindings().iterator();
while (it.hasNext()) {
Binding binding = (Binding) it.next();
String parameterName = binding.getProperty();
if (parameterNames.contains(parameterName)) {
addMapping(mapper, binding, model);
} else {
if (fieldMarkerPrefix != null && parameterNames.contains(fieldMarkerPrefix + parameterName)) {
addEmptyValueMapping(mapper, parameterName, model);
}
}
}
}
/**
* <p>
* Creates and adds a {@link DefaultMapping} for the given {@link Binding}. Information such as the model field
* name, if the field is required, and whether type conversion is needed will be passed on from the binding to the
* mapping.
* </p>
* <p>
* <b>Note:</b> with Spring 3 type conversion and formatting now in use in Web Flow, it is no longer necessary to
* use named converters on binding elements. The preferred approach is to register Spring 3 formatters. Named
* converters are supported for backwards compatibility only and will not result in use of the Spring 3 type
* conversion system at runtime.
* </p>
*
* @param mapper the mapper to add the mapping to
* @param binding the binding element
* @param model the model
*/
@Override
protected void addMapping(DefaultMapper mapper, Binding binding, Object model) {
Expression source = new RequestParameterExpression(binding.getProperty());
ParserContext parserContext = new FluentParserContext().evaluate(model.getClass());
Expression target = expressionParser.parseExpression(binding.getProperty(), parserContext);
DefaultMapping mapping = new DefaultMapping(source, target);
mapping.setRequired(binding.getRequired());
if (binding.getConverter() != null) {
Assert.notNull(conversionService,
"A ConversionService must be configured to use resolve custom converters to use during binding");
ConversionExecutor conversionExecutor = conversionService.getConversionExecutor(binding.getConverter(),
String.class, target.getValueType(model));
mapping.setTypeConverter(conversionExecutor);
}
if (logger.isDebugEnabled()) {
logger.debug("Adding mapping for parameter '" + binding.getProperty() + "'");
}
mapper.addMapping(mapping);
}
/**
* Add a {@link DefaultMapping} instance for all incoming request parameters except those having a special field
* marker prefix. This method is used when binding configuration was not specified on the view.
*
* @param mapper the mapper to add mappings to
* @param parameterNames the request parameter names
* @param model the model
*/
@Override
protected void addDefaultMappings(DefaultMapper mapper, Set parameterNames, Object model) {
for (Iterator it = parameterNames.iterator(); it.hasNext();) {
String parameterName = (String) it.next();
if (fieldMarkerPrefix != null && parameterName.startsWith(fieldMarkerPrefix)) {
String field = parameterName.substring(fieldMarkerPrefix.length());
if (!parameterNames.contains(field)) {
addEmptyValueMapping(mapper, field, model);
}
} else {
addDefaultMapping(mapper, parameterName, model);
}
}
}
/**
* Adds a special {@link DefaultMapping} that results in setting the target field on the model to an empty value
* (typically null).
*
* @param mapper the mapper to add the mapping to
* @param field the field for which a mapping is to be added
* @param model the model
*/
@Override
protected void addEmptyValueMapping(DefaultMapper mapper, String field, Object model) {
ParserContext parserContext = new FluentParserContext().evaluate(model.getClass());
Expression target = expressionParser.parseExpression(field, parserContext);
try {
Class propertyType = target.getValueType(model);
Expression source = new StaticExpression(getEmptyValue(propertyType));
DefaultMapping mapping = new DefaultMapping(source, target);
if (logger.isDebugEnabled()) {
logger.debug("Adding empty value mapping for parameter '" + field + "'");
}
mapper.addMapping(mapping);
} catch (EvaluationException e) {
}
}
/**
* Adds a {@link DefaultMapping} between the given request parameter name and a matching model field.
*
* @param mapper the mapper to add the mapping to
* @param parameter the request parameter name
* @param model the model
*/
@Override
protected void addDefaultMapping(DefaultMapper mapper, String parameter, Object model) {
Expression source = new RequestParameterExpression(parameter);
ParserContext parserContext = new FluentParserContext().evaluate(model.getClass());
Expression target = expressionParser.parseExpression(parameter, parserContext);
DefaultMapping mapping = new DefaultMapping(source, target);
if (logger.isDebugEnabled()) {
logger.debug("Adding default mapping for parameter '" + parameter + "'");
}
mapper.addMapping(mapping);
}
// package private
/**
* Restores the internal state of this view from the provided state holder.
* @see AbstractMvcViewFactory#getView(RequestContext)
*/
void restoreState(ViewActionStateHolder stateHolder) {
eventId = stateHolder.getEventId();
userEventProcessed = stateHolder.getUserEventProcessed();
mappingResults = stateHolder.getMappingResults();
}
/**
* Determines if model validation should execute given the Transition that matched the current user event being
* processed. Returns true unless the <code>validate</code> attribute of the Transition has been set to false, or
* model data binding errors occurred and the global <code>validateOnBindingErrors</code> flag is set to false.
* Subclasses may override.
* @param model the model data binding would be performed on
* @param transition the matched transition
* @return true if binding should occur, false if not
*/
private boolean shouldValidate(Object model, TransitionDefinition transition) {
Boolean validateAttribute = getValidateAttribute(transition);
if (validateAttribute != null) {
return validateAttribute.booleanValue();
} else {
AttributeMap flowExecutionAttributes = requestContext.getFlowExecutionContext().getAttributes();
Boolean validateOnBindingErrors = flowExecutionAttributes.getBoolean("validateOnBindingErrors");
if (validateOnBindingErrors != null) {
if (!validateOnBindingErrors.booleanValue() && mappingResults.hasErrorResults()) {
return false;
}
}
return true;
}
}
// internal helpers
private Map flowScopes() {
if (requestContext.getCurrentState().isViewState()) {
return requestContext.getConversationScope().union(requestContext.getFlowScope()).union(
requestContext.getViewScope()).union(requestContext.getFlashScope()).union(
requestContext.getRequestScope()).asMap();
} else {
return requestContext.getConversationScope().union(requestContext.getFlowScope()).union(
requestContext.getFlashScope()).union(requestContext.getRequestScope()).asMap();
}
}
private void exposeBindingModel(Map model) {
Object modelObject = getModelObject();
if (modelObject != null) {
BindingModel bindingModel = new BindingModel(getModelExpression().getExpressionString(), modelObject,
expressionParser, conversionService, requestContext.getMessageContext());
bindingModel.setBinderConfiguration(binderConfiguration);
bindingModel.setMappingResults(mappingResults);
model.put(BindingResult.MODEL_KEY_PREFIX + getModelExpression().getExpressionString(), bindingModel);
}
}
private Object getModelObject() {
Expression model = getModelExpression();
if (model != null) {
try {
return model.getValue(requestContext);
} catch (EvaluationException e) {
return null;
}
} else {
return null;
}
}
private Expression getModelExpression() {
return (Expression) requestContext.getCurrentState().getAttributes().get("model");
}
private Object getEmptyValue(Class fieldType) {
if (fieldType != null && boolean.class.equals(fieldType) || Boolean.class.equals(fieldType)) {
// Special handling of boolean property.
return Boolean.FALSE;
} else if (fieldType != null && fieldType.isArray()) {
// Special handling of array property.
return Array.newInstance(fieldType.getComponentType(), 0);
} else {
// Default value: try null.
return null;
}
}
private boolean hasErrors(MappingResults results) {
return results.hasErrorResults() && !onlyPropertyNotFoundErrorsPresent(results);
}
private boolean onlyPropertyNotFoundErrorsPresent(MappingResults results) {
return results.getResults(PROPERTY_NOT_FOUND_ERROR).size() == mappingResults.getErrorResults().size();
}
private void addErrorMessages(MappingResults results) {
List errors = results.getResults(MAPPING_ERROR);
for (Iterator it = errors.iterator(); it.hasNext();) {
MappingResult error = (MappingResult) it.next();
requestContext.getMessageContext().addMessage(createMessageResolver(error));
}
}
private MessageResolver createMessageResolver(MappingResult error) {
String model = getModelExpression().getExpressionString();
String field = error.getMapping().getTargetExpression().getExpressionString();
Class fieldType = error.getMapping().getTargetExpression().getValueType(getModelObject());
String[] messageCodes = messageCodesResolver.resolveMessageCodes(error.getCode(), model, field, fieldType);
return new MessageBuilder().error().source(field).codes(messageCodes).resolvableArg(field).defaultText(
error.getCode() + " on " + field).build();
}
private Boolean getValidateAttribute(TransitionDefinition transition) {
if (transition != null) {
return transition.getAttributes().getBoolean("validate");
} else {
return null;
}
}
private void validate(Object model) {
if (logger.isDebugEnabled()) {
logger.debug("Validating model");
}
// Validacion de editables de HDIV.
this.editableParameterValidator.validate(model, new MessageContextErrors(requestContext.getMessageContext(), eventId, model, expressionParser, messageCodesResolver, mappingResults));
new ValidationHelper(model, requestContext, eventId, getModelExpression().getExpressionString(),
expressionParser, messageCodesResolver, mappingResults).validate();
}
private static class PropertyNotFoundError implements MappingResultsCriteria {
public boolean test(MappingResult result) {
return result.isError() && "propertyNotFound".equals(result.getCode());
}
}
private static class MappingError implements MappingResultsCriteria {
public boolean test(MappingResult result) {
return result.isError() && !PROPERTY_NOT_FOUND_ERROR.test(result);
}
}
private static class RequestParameterExpression implements Expression {
private final String parameterName;
public RequestParameterExpression(String parameterName) {
this.parameterName = parameterName;
}
public String getExpressionString() {
return parameterName;
}
public Object getValue(Object context) throws EvaluationException {
ParameterMap parameters = (ParameterMap) context;
return parameters.asMap().get(parameterName);
}
public Class getValueType(Object context) {
return String.class;
}
public void setValue(Object context, Object value) throws EvaluationException {
throw new UnsupportedOperationException("Setting request parameters is not allowed");
}
@Override
public String toString() {
return "parameter:'" + parameterName + "'";
}
}
/* Método de ServletMvcView */
@Override
protected void doRender(Map model) throws Exception {
RequestContext context = getRequestContext();
ExternalContext externalContext = context.getExternalContext();
HttpServletRequest request = (HttpServletRequest) externalContext.getNativeRequest();
HttpServletResponse response = (HttpServletResponse) externalContext.getNativeResponse();
request.setAttribute(org.springframework.web.servlet.support.RequestContext.WEB_APPLICATION_CONTEXT_ATTRIBUTE, context.getActiveFlow().getApplicationContext());
getView().render(model, request, response);
}
}