AbstractMvcView.java

/*
 * Copyright 2004-2020 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
 *
 *      https://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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.SourceVersion;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.PropertyAccessorUtils;
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.beanwrapper.BeanWrapperExpressionParser;
import org.springframework.binding.expression.support.SimpleParserContext;
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 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 final ExpressionParser emptyValueExpressionParser = new BeanWrapperExpressionParser();

	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<>();
		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 SimpleParserContext(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 SimpleParserContext(model.getClass());
		Expression target = emptyValueExpressionParser.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 SimpleParserContext(model.getClass());
		if (expressionParser instanceof BeanWrapperExpressionParser || checkModelProperty(parameter, model)) {
			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);
		}
	}

	/**
	 * Perform basic checks on the given expression to see if it looks like a property path.
	 * Check if the top nested property is a readable property on the Model.
	 * Check if the remaining nested properties are valid Java identifiers.
	 */
	private boolean checkModelProperty(String expression, Object model) {
		List<String> propertyNames = new ArrayList<>();
		while (true) {
			int index = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(expression);
			String nestedProperty = index != -1 ? expression.substring(0, index) : expression;
			nestedProperty = PropertyAccessorUtils.getPropertyName(nestedProperty);
			propertyNames.add(nestedProperty);
			if (index == -1) {
				break;
			}
			if (expression.length() == index + 1) {
				return false;
			}
			expression = expression.substring(index + 1);
		}
		BeanWrapperImpl beanWrapper = new BeanWrapperImpl(model);
		if (!beanWrapper.isReadableProperty(propertyNames.get(0))) {
			return false;
		}
		for (String propertyName : propertyNames) {
			if (!SourceVersion.isName(propertyName)) {
				return false;
			}
		}
		return true;
	}

	// 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 + "'";
		}
	}

}