Package org.springframework.webflow.mvc.view

Source Code of org.springframework.webflow.mvc.view.AbstractMvcView$RequestParameterExpression

/*
* Copyright 2004-2014 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.webflow.mvc.view;

import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.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.validation.Validator;
import org.springframework.web.util.WebUtils;
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.execution.View;
import org.springframework.webflow.validation.BeanValidationHintResolver;
import org.springframework.webflow.validation.ValidationHelper;
import org.springframework.webflow.validation.ValidationHintResolver;

/**
* Base view implementation for the Spring Web MVC Servlet and Spring Web MVC Portlet frameworks.
*
* @author Keith Donald
*/
public abstract class AbstractMvcView implements View {

  private static final Log logger = LogFactory.getLog(AbstractMvcView.class);

  private static final MappingResultsCriteria PROPERTY_NOT_FOUND_ERROR = new PropertyNotFoundError();

  private static final MappingResultsCriteria MAPPING_ERROR = new MappingError();

  private org.springframework.web.servlet.View view;

  private RequestContext requestContext;

  private ExpressionParser expressionParser;

  private ConversionService conversionService;

  private Validator validator;

  private String fieldMarkerPrefix = "_";

  private String eventIdParameterName = "_eventId";

  private String eventId;

  private MappingResults mappingResults;

  private BinderConfiguration binderConfiguration;

  private MessageCodesResolver messageCodesResolver;

  private boolean userEventProcessed;

  private ValidationHintResolver validationHintResolver = new BeanValidationHintResolver();

  /**
   * Creates a new MVC view.
   * @param view the Spring MVC view to render
   * @param requestContext the current flow request context
   */
  public AbstractMvcView(org.springframework.web.servlet.View view, RequestContext requestContext) {
    this.view = view;
    this.requestContext = requestContext;
  }

  /**
   * Sets the expression parser to use to parse model expressions.
   * @param expressionParser the expression parser
   */
  public void setExpressionParser(ExpressionParser expressionParser) {
    this.expressionParser = expressionParser;
  }

  /**
   * Sets the service to use to expose formatters for field values.
   * @param conversionService the conversion service
   */
  public void setConversionService(ConversionService conversionService) {
    this.conversionService = conversionService;
  }

  public void setValidator(Validator validator) {
    this.validator = validator;
  }

  public void setValidationHintResolver(ValidationHintResolver validationHintResolver) {
    if (validationHintResolver != null) {
      this.validationHintResolver = validationHintResolver;
    }
  }

  /**
   * Sets the configuration describing how this view should bind to its model to access data for rendering.
   * @param binderConfiguration the model binder configuration
   */
  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
   */
  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.
   */
  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
   */
  public void setEventIdParameterName(String eventIdParameterName) {
    this.eventIdParameterName = eventIdParameterName;
  }

  public void render() throws IOException {
    Map<String, Object> model = new HashMap<String, Object>();
    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;
    }
  }

  public boolean userEventQueued() {
    return !userEventProcessed && getEventId() != null;
  }

  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, transition);
        }
      }
    } else {
      if (logger.isDebugEnabled()) {
        logger.debug("No model to bind to; done processing user event");
      }
    }
    userEventProcessed = true;
  }

  public Serializable getUserEventState() {
    return new ViewActionStateHolder(eventId, userEventProcessed, mappingResults);
  }

  public boolean hasFlowEvent() {
    return userEventProcessed && !requestContext.getMessageContext().hasErrorMessages();
  }

  public Event getFlowEvent() {
    if (!hasFlowEvent()) {
      return null;
    }
    return new Event(this, getEventId(), requestContext.getRequestParameters().asAttributeMap());
  }

  public void saveState() {

  }

  public String toString() {
    return new ToStringCreator(this).append("view", view).toString();
  }

  // subclassing hooks

  /**
   * Returns the current flow request context.
   * @return the flow request context
   */
  protected RequestContext getRequestContext() {
    return requestContext;
  }

  /**
   * Returns the Spring MVC view to render
   * @return the view
   */
  protected org.springframework.web.servlet.View getView() {
    return view;
  }

  /**
   * @return the configured ConversionService
   */
  protected ConversionService getConversionService() {
    return conversionService;
  }

  /**
   * Template method subclasses should override to execute the view rendering logic.
   * @param model the view model data
   * @throws Exception an exception occurred rendering the view
   */
  protected abstract void doRender(Map<String, ?> model) throws Exception;

  /**
   * Returns the id of the user event being processed.
   * @return the user event
   */
  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
   */
  protected boolean shouldBind(Object model, TransitionDefinition transition) {
    if (transition == null) {
      return true;
    }
    return transition.getAttributes().getBoolean("bind", true);
  }

  /**
   * Returns the results of binding to the view's model, if model binding has occurred.
   * @return the binding (mapping) results
   */
  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.
   */
  protected BinderConfiguration getBinderConfiguration() {
    return binderConfiguration;
  }

  /**
   * Returns the EL parser to be used for data binding purposes.
   * @return an instance of {@link ExpressionParser}.
   */
  protected ExpressionParser getExpressionParser() {
    return expressionParser;
  }

  /**
   * Returns the prefix that can be used for parameters that mark potentially empty fields.
   * @return the prefix value.
   */
  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
   */
  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.
   */
  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
   */
  protected void addModelBindings(DefaultMapper mapper, Set<String> parameterNames, Object model) {
    for (Binding binding : binderConfiguration.getBindings()) {
      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
   */
  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
   */
  protected void addDefaultMappings(DefaultMapper mapper, Set<String> parameterNames, Object model) {
    for (String parameterName : parameterNames) {
      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
   */
  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
   */
  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
   */
  protected boolean shouldValidate(Object model, TransitionDefinition transition) {
    Boolean validateAttribute = getValidateAttribute(transition);
    if (validateAttribute != null) {
      return validateAttribute;
    } else {
      AttributeMap<Object> flowExecutionAttributes = requestContext.getFlowExecutionContext().getAttributes();
      Boolean validateOnBindingErrors = flowExecutionAttributes.getBoolean("validateOnBindingErrors");
      if (validateOnBindingErrors != null) {
        if (!validateOnBindingErrors && mappingResults.hasErrorResults()) {
          return false;
        }
      }
      return true;
    }
  }

  // internal helpers

  private Map<String, Object> 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<String, Object> 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 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<MappingResult> errors = results.getResults(MAPPING_ERROR);
    for (MappingResult error : errors) {
      requestContext.getMessageContext().addMessage(createMessageResolver(error));
    }
  }

  protected 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, TransitionDefinition transition) {
    if (logger.isDebugEnabled()) {
      logger.debug("Validating model");
    }
    ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, getModelExpression()
        .getExpressionString(), expressionParser, messageCodesResolver, mappingResults, validationHintResolver);
    helper.setValidator(this.validator);
    helper.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 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");
    }

    public String toString() {
      return "parameter:'" + parameterName + "'";
    }
  }

}
TOP

Related Classes of org.springframework.webflow.mvc.view.AbstractMvcView$RequestParameterExpression

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.