org.appng.api.support.CallableAction Maven / Gradle / Ivy
/*
* Copyright 2011-2019 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.appng.api.support;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadFactory;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.appng.api.ActionProvider;
import org.appng.api.BusinessException;
import org.appng.api.Environment;
import org.appng.api.FieldProcessor;
import org.appng.api.FormValidator;
import org.appng.api.Option;
import org.appng.api.Options;
import org.appng.api.ParameterSupport;
import org.appng.api.PermissionOwner;
import org.appng.api.PermissionProcessor;
import org.appng.api.ProcessingException;
import org.appng.api.model.Application;
import org.appng.api.model.Site;
import org.appng.el.ExpressionEvaluator;
import org.appng.xml.platform.Action;
import org.appng.xml.platform.ActionRef;
import org.appng.xml.platform.Bean;
import org.appng.xml.platform.Condition;
import org.appng.xml.platform.Data;
import org.appng.xml.platform.DataConfig;
import org.appng.xml.platform.Datafield;
import org.appng.xml.platform.DatasourceRef;
import org.appng.xml.platform.Event;
import org.appng.xml.platform.FieldDef;
import org.appng.xml.platform.FieldType;
import org.appng.xml.platform.Message;
import org.appng.xml.platform.MessageType;
import org.appng.xml.platform.Messages;
import org.appng.xml.platform.MetaData;
import org.appng.xml.platform.OptionGroup;
import org.appng.xml.platform.PageDefinition;
import org.appng.xml.platform.PageReference;
import org.appng.xml.platform.Permissions;
import org.appng.xml.platform.Section;
import org.appng.xml.platform.SectionelementDef;
import org.appng.xml.platform.Selection;
import org.appng.xml.platform.UserData;
import org.appng.xml.platform.UserInputField;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import lombok.extern.slf4j.Slf4j;
/**
*
* A {@code CallableAction} is responsible for preparing and performing an {@link Action}, based on a given
* {@link ActionRef} which is part of a {@link PageDefinition}'s {@link SectionelementDef}.
*
* @author Matthias Müller
*/
@Slf4j
public class CallableAction {
private ApplicationRequest applicationRequest;
private Site site;
private Application application;
private boolean include = false;
private boolean execute = false;
private boolean errors = false;
private ElementHelper elementHelper;
private String onSuccess;
private ActionRef actionRef;
private ParameterSupport actionParamSupport;
private Action action;
/**
* Creates a new {@code CallableAction} and also determines the return values for
*
* - {@link #doInclude()}
*
- {@link #doExecute()}
*
- {@link #getOnSuccess()}
*
* , based on {@link Permissions} and {@link Condition}s.
*
* @param site
* the current {@link Site}
* @param application
* the current {@link Application}
* @param applicationRequest
* the current {@link ApplicationRequest}
* @param actionRef
* the {@link ActionRef} as given in the {@link SectionelementDef} of a {@link PageDefinition}.
* @throws ProcessingException
* if an error occurs while assembling the {@code CallableAction}
*/
public CallableAction(Site site, Application application, ApplicationRequest applicationRequest,
ActionRef actionRef) throws ProcessingException {
this.site = site;
this.application = application;
this.applicationRequest = applicationRequest;
this.actionRef = actionRef;
String eventId = actionRef.getEventId();
Event event = applicationRequest.getApplicationConfig().getEvent(eventId);
if (null == event) {
throw new ProcessingException("no such event: " + eventId, new FieldProcessorImpl(eventId));
}
PermissionProcessor permissionProcessor = applicationRequest.getPermissionProcessor();
if (permissionProcessor.hasPermissions(new PermissionOwner(event))) {
if (permissionProcessor.hasPermissions(new PermissionOwner(actionRef))) {
initializeAction(event);
} else {
LOGGER.debug("no permission for actionRef '{}' of event '{}'", actionRef.getId(), event.getId());
}
} else {
LOGGER.debug("no permission for event '{}'", eventId);
}
}
// for testing
protected CallableAction(Site site, ApplicationRequest applicationRequest, ElementHelper elementHelper) {
this.site = site;
this.applicationRequest = applicationRequest;
this.elementHelper = elementHelper;
}
private void initializeAction(Event event) throws ProcessingException {
String actionId = actionRef.getId();
String eventId = event.getId();
this.action = applicationRequest.getApplicationConfig().getAction(eventId, actionId);
if (null == action) {
throw new ProcessingException("no such action: " + actionId, new FieldProcessorImpl(actionId));
}
if (applicationRequest.getPermissionProcessor().hasPermissions(new PermissionOwner(action))) {
ExpressionEvaluator pageExpressionEvaluator = applicationRequest.getExpressionEvaluator();
if (actionRef.getClientValidation() != null) {
boolean clientValidation = pageExpressionEvaluator.getBoolean(actionRef.getClientValidation());
this.action.setClientValidation(String.valueOf(clientValidation));
}
this.elementHelper = new ElementHelper(site, application);
DataConfig config = getAction().getConfig();
getAction().setEventId(eventId);
Map actionParameters = elementHelper.initializeParameters(
"action '" + actionRef.getId() + "' (" + actionRef.getEventId() + ")", applicationRequest,
applicationRequest.getParameterSupportDollar(), actionRef.getParams(), config.getParams());
LOGGER.trace("parameters for action '{}' of event '{}': {}", actionId, eventId, actionParameters);
actionParamSupport = new DollarParameterSupport(actionParameters);
Condition includeCondition = actionRef.getCondition();
this.include = elementHelper.conditionMatches(pageExpressionEvaluator, includeCondition);
if (include) {
elementHelper.processConfig(applicationRequest.getApplicationConfig(), applicationRequest,
action.getConfig(), actionParameters);
elementHelper.addTemplates(applicationRequest.getApplicationConfig(), event.getConfig());
LOGGER.debug("including action '{}' of event '{}'", actionId, eventId);
} else {
LOGGER.debug("include condition for action '{}' of event '{}' did not match - {}", actionId, eventId,
includeCondition.getExpression());
}
Condition executeCondition = action.getCondition();
this.execute = elementHelper.conditionMatches(executeCondition);
if (!execute) {
LOGGER.debug("execute condition for action '{}' of event '{}' did not match - {}", actionId, eventId,
executeCondition.getExpression());
}
if (include || execute) {
Bean bean = getAction().getBean();
if (null != bean) {
elementHelper.initOptions(bean.getOptions());
}
setOnSuccess();
}
} else {
LOGGER.debug("no permission(s) for action '{}' of event '{}'", actionId, eventId);
}
}
public Action getAction() {
return action;
}
protected boolean retrieveData(boolean setBeanNull) throws ProcessingException {
boolean dataOk = true;
ParameterSupport fieldParams = null;
DatasourceRef datasourceRef = action.getDatasource();
if (null != datasourceRef) {
CallableDataSource datasourceElement = new CallableDataSource(site, application, applicationRequest,
actionParamSupport, datasourceRef);
if (datasourceElement.doInclude()) {
List before = new ArrayList<>();
Environment environment = applicationRequest.getEnvironment();
if (elementHelper.hasMessages(environment)) {
before.addAll(elementHelper.getMessages(environment).getMessageList());
}
Data data = datasourceElement.perform(null, setBeanNull, true);
action.setData(data);
DataConfig dsConfig = datasourceElement.getDatasource().getConfig();
elementHelper.addTemplates(applicationRequest.getApplicationConfig(), dsConfig);
action.getConfig().setMetaData(dsConfig.getMetaData());
List after = new ArrayList<>();
if (elementHelper.hasMessages(environment)) {
after.addAll(elementHelper.getMessages(environment).getMessageList());
}
@SuppressWarnings("unchecked")
Collection addedMessages = CollectionUtils.disjunction(before, after);
if (!addedMessages.isEmpty()) {
for (Message message : addedMessages) {
dataOk &= !MessageType.ERROR.equals(message.getClazz());
}
if (!dataOk) {
Messages messages = elementHelper.getMessages(environment);
messages.getMessageList().removeAll(addedMessages);
Messages actionMessages = new Messages();
actionMessages.getMessageList().addAll(addedMessages);
getAction().setMessages(actionMessages);
}
}
if (null != data && null != data.getResult()) {
Map fieldValues = new HashMap<>();
for (Datafield datafield : data.getResult().getFields()) {
fieldValues.put(datafield.getName(), datafield.getValue());
}
fieldParams = new HashParameterSupport(fieldValues);
}
}
}
applicationRequest.setLabels(action.getConfig(), fieldParams);
return dataOk;
}
private void setOnSuccess() {
onSuccess = actionRef.getOnSuccess();
if (!StringUtils.isBlank(onSuccess)) {
if (!onSuccess.startsWith("/")) {
onSuccess = "/" + onSuccess;
}
ParameterSupport pageParamSupport = applicationRequest.getParameterSupportDollar();
onSuccess = application.getName() + pageParamSupport.replaceParameters(onSuccess);
actionRef.setOnSuccess(onSuccess);
getAction().setOnSuccess(onSuccess);
}
}
/**
* Performs this {@link CallableAction}.
* If the {@link Action} is actually included and/or executed depends on the returns values of {@link #doInclude()}
* and {@link #doExecute()}. If the {@link Action} is executed and a forward-path exists, a redirect is send via
* {@link Site#sendRedirect(Environment, String)}.
*
* @return a {@link FieldProcessor}, only non-{@code null} if the {@link Action} has been executed successfully
* @throws ProcessingException
* if an error occurred while performing
* @see #doInclude()
* @see #doExecute()
* @see #doForward()
* @see #getOnSuccess()
*/
public FieldProcessor perform() throws ProcessingException {
return perform(false);
}
/**
* Performs this {@link CallableAction}.
* If the {@link Action} is actually included and/or executed depends on the returns values of {@link #doInclude()}
* and {@link #doExecute()}. If the {@link Action} is executed and a forward-path exists, a redirect is send via
* {@link Site#sendRedirect(Environment, String)}.
*
* @param isSectionHidden
* whether this action is part of a hidden {@link Section}, meaning no {@link Messages} should be set for
* the action.
*
* @return a {@link FieldProcessor}, only non-{@code null} if the {@link Action} has been executed successfully
* @throws ProcessingException
* if an error occurred while performing
* @see #doInclude()
* @see #doExecute()
* @see #doForward()
* @see #getOnSuccess()
*/
public FieldProcessor perform(boolean isSectionHidden) throws ProcessingException {
FieldProcessor fp = null;
if (doExecute()) {
execute = retrieveData(false);
if (doExecute()) {
fp = execute();
if (doForward() || forceForward()) {
String outputPrefix = elementHelper.getOutputPrefix(applicationRequest.getEnvironment());
StringBuilder target = new StringBuilder();
if (null != outputPrefix) {
target.append(outputPrefix);
}
target.append(getOnSuccess());
site.sendRedirect(applicationRequest.getEnvironment(), target.toString());
getAction().setOnSuccess(target.toString());
applicationRequest.setRedirectTarget(target.toString());
} else if (!isSectionHidden) {
Messages messages = elementHelper.removeMessages(applicationRequest.getEnvironment());
getAction().setMessages(messages);
}
}
}
if (doInclude()) {
if (!doExecute() || !doForward()) {
retrieveData(false);
handleSelections();
}
}
return fp;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private FieldProcessor execute() throws ProcessingException {
Bean bean = getAction().getBean();
FieldProcessor fieldProcessor = null;
if (null != bean) {
MetaData metaData = elementHelper.getFilteredMetaData(applicationRequest, action.getConfig().getMetaData(),
true);
try {
String beanId = bean.getId();
String actionId = actionRef.getId();
LOGGER.trace("retrieving action '{}'", beanId);
final ActionProvider actionProvider = this.application.getBean(beanId, ActionProvider.class);
LOGGER.trace("action '{}' is of Type '{}'", beanId, actionProvider.getClass().getName());
fieldProcessor = new FieldProcessorImpl(actionId, metaData);
fieldProcessor.addLinkPanels(action.getConfig().getLinkpanel());
final Environment env = applicationRequest.getEnvironment();
final Options options = elementHelper.getOptions(bean.getOptions());
LOGGER.trace("options for action '{}' of event '{}': {}", actionId, actionRef.getEventId(), options);
Object bindObject = getBindObject(fieldProcessor);
validateBindObject(fieldProcessor, actionProvider, options, bindObject);
if (!fieldProcessor.hasErrors()) {
final BeanWrapper bindObjectWrapper;
if (null != bindObject) {
bindObjectWrapper = new BeanWrapperImpl(bindObject);
} else {
bindObjectWrapper = new BeanWrapperImpl();
}
Boolean async = applicationRequest.getExpressionEvaluator().getBoolean(actionRef.getAsync());
action.setAsync(String.valueOf(async));
if (Boolean.TRUE.equals(async)) {
LOGGER.debug("validation OK, asynchronously calling bean '{}' of application '{}'", beanId,
application.getName());
final String actionTitle = getAction().getConfig().getTitle().getValue();
final FieldProcessor innerFieldProcessor = new FieldProcessorImpl(actionId, metaData);
innerFieldProcessor.addLinkPanels(action.getConfig().getLinkpanel());
final Locale locale = applicationRequest.getEnvironment().getLocale();
String pattern = String.format("%s-%s-async-%s", site.getName(), application.getName(), "%d");
ThreadFactory threadFactory = new BasicThreadFactory.Builder().namingPattern(pattern).build();
ExecutorService executor = Executors.newFixedThreadPool(1, threadFactory);
FutureTask futureTask = new FutureTask(new Callable() {
public Void call() throws Exception {
try {
Object innerBindObject = bindObjectWrapper.getWrappedInstance();
actionProvider.perform(site, application, env, options, applicationRequest,
innerBindObject, innerFieldProcessor);
String message = application.getMessage(locale, "backgroundTaskFinished",
actionTitle);
innerFieldProcessor.addOkMessage(message);
ElementHelper.addMessages(env, innerFieldProcessor.getMessages());
LOGGER.debug("Background task '{}' finished.", actionTitle);
} catch (Exception e) {
String id = String.valueOf(e.hashCode());
LOGGER.error(String.format("error while executing background task %s, ID: %s",
actionTitle, id), e);
String backgroundTaskError = application.getMessage(locale, "backgroundTaskError",
actionTitle, id);
innerFieldProcessor.addErrorMessage(backgroundTaskError);
ElementHelper.addMessages(env, innerFieldProcessor.getMessages());
throw e;
}
return null;
}
});
executor.execute(futureTask);
executor.shutdown();
String message = application.getMessage(locale, "backgroundTaskStarted", actionTitle);
fieldProcessor.addOkMessage(message);
LOGGER.debug("Background task '{}' started.", actionTitle);
} else {
LOGGER.debug("validation OK, calling bean '{}' of application '{}'", beanId,
application.getName());
actionProvider.perform(site, application, env, options, applicationRequest, bindObject,
fieldProcessor);
}
}
errors = fieldProcessor.hasErrors();
ElementHelper.addMessages(env, fieldProcessor.getMessages());
if (errors) {
LOGGER.debug("validation returned errors");
addUserdata(metaData.getFields());
handleSelections();
}
} catch (Exception e) {
String id = String.valueOf(e.hashCode());
if (!elementHelper.isMessageParam(e) && null != fieldProcessor) {
fieldProcessor.clearMessages();
String message = applicationRequest.getMessage(ElementHelper.INTERNAL_ERROR, id);
fieldProcessor.addErrorMessage(message);
}
throw new ProcessingException(String.format("error performing action '%s' of event '%s', ID: %s",
action.getId(), actionRef.getEventId(), id), e, fieldProcessor);
}
}
return fieldProcessor;
}
/**
* Creates, fills and returns a new bindobject.
*
* @param fieldProcessor
* the {@link FieldProcessor} to use
* @return a new bindobject
* @throws BusinessException
* if
* {@link ApplicationRequest#getBindObject(FieldProcessor, org.appng.forms.RequestContainer, ClassLoader)}
* throws such an exception
* @see ApplicationRequest#getBindObject(FieldProcessor, org.appng.forms.RequestContainer, ClassLoader)
*/
protected Object getBindObject(FieldProcessor fieldProcessor) throws BusinessException {
Object bindObject = null;
if (fieldProcessor.getMetaData().getBindClass() != null) {
bindObject = applicationRequest.getBindObject(fieldProcessor, applicationRequest,
site.getSiteClassLoader());
} else {
LOGGER.debug("no bindclass given for action '{}'", getAction().getId());
}
return bindObject;
}
private void validateBindObject(FieldProcessor fieldProcessor, final ActionProvider> actionProvider,
final Options options, Object bindObject) {
if (null != bindObject) {
applicationRequest.validateBean(bindObject, fieldProcessor,
elementHelper.getValidationGroups(fieldProcessor.getMetaData(), bindObject));
Environment env = applicationRequest.getEnvironment();
if (bindObject instanceof FormValidator) {
((FormValidator) bindObject).validate(site, application, env, options, applicationRequest,
fieldProcessor);
}
if (actionProvider instanceof FormValidator) {
((FormValidator) actionProvider).validate(site, application, env, options, applicationRequest,
fieldProcessor);
}
}
}
/**
* Adds the {@link UserData}-object to the given {@link Action}. The {@link UserData} contains the form-input made
* by the user.
*
* @see #handleSelections()
* @param action
* @param request
* @param writeableFields
* a list of writeable {@link FieldDef}initions
*/
private void addUserdata(List writeableFields) {
MetaData metaData = action.getConfig().getMetaData();
if (null != metaData) {
action.setUserdata(new UserData());
addFieldsToUserdata(writeableFields);
}
}
private void addFieldsToUserdata(List fields) {
for (FieldDef fieldDef : fields) {
// do NOT evaluate expressions here!
boolean isWriteable = !Boolean.TRUE.toString().equalsIgnoreCase(fieldDef.getReadonly());
if (isWriteable) {
String binding = fieldDef.getBinding();
FieldType type = fieldDef.getType();
if (!FieldType.PASSWORD.equals(type)) {
List parameterList = applicationRequest.getParameterList(binding);
if (!parameterList.isEmpty()) {
for (String value : parameterList) {
UserInputField f = new UserInputField();
f.setName(binding);
f.setContent(value);
action.getUserdata().getInput().add(f);
}
}
}
List childFields = fieldDef.getFields();
addFieldsToUserdata(childFields);
}
}
}
/**
* Checks whether or not to include this {@code CallableAction} on the {@link PageReference}.
*
* @return {@code true} if this {@code CallableAction} should be included, {@code false} otherwise
*/
public boolean doInclude() {
return include;
}
/**
* Checks whether or not this {@code CallableAction} should execute the {@link ActionProvider} provided by the
* referenced {@link Action}.
*
* @return {@code true} if this {@code CallableAction} should be executed, {@code false} otherwise
*/
public boolean doExecute() {
return execute;
}
/**
* Checks whether some errors occurred while performing the {@link Action}.
*
* @return {@code true} if some errors occurred, {@code false} otherwise
*/
public boolean hasErrors() {
return errors;
}
/**
* Returns the forward-path that was defined by the {@link ActionRef} this {@link CallableAction} was build from.
*
* @return the forward-path, may be {@code null}
*/
public String getOnSuccess() {
return onSuccess;
}
/**
* Checks whether this {@link CallableAction} causes a page-forward, which is the case when {@link #hasErrors()}
* returns {@code false} and {@link #getOnSuccess()} a non-{@code null} value
*
* @return {@code true} if a page-forward is caused, {@code false} otherwise
*/
public boolean doForward() {
return !hasErrors() && null != onSuccess;
}
private boolean forceForward() {
return null != onSuccess && actionRef.isForceForward();
}
/**
* selects the {@link Option}s of a {@link Selection} based on the users form-inputs, which are stored in the
* {@link Action}s {@link UserData}
* Don't really like this solution...open for improvement
*
*
* @see #addUserdata(List)
* @param action
*/
// XXX MM this is the root of all evil
private void handleSelections() {
if (null != action.getData()) {
Map selectionMap = new HashMap<>();
for (Selection selection : action.getData().getSelections()) {
selectionMap.put(selection.getId(), selection);
}
UserData userdata = action.getUserdata();
if (null != userdata) {
Map> userinput = new HashMap<>();
for (UserInputField userInputField : userdata.getInput()) {
String name = userInputField.getName();
if (!userinput.containsKey(name)) {
userinput.put(name, new ArrayList<>());
}
userinput.get(name).add(userInputField.getContent());
}
List fields = action.getConfig().getMetaData().getFields();
for (FieldDef fieldDef : fields) {
Selection selection = selectionMap.get(fieldDef.getName());
String binding = fieldDef.getBinding();
List userInputs = userinput.get(binding);
if (null != selection && null != userInputs) {
deselectOptions(selection.getOptions());
for (OptionGroup optionGroup : selection.getOptionGroups()) {
deselectOptions(optionGroup.getOptions());
}
for (String value : userInputs) {
selectOptions(value, selection.getOptions());
for (OptionGroup optionGroup : selection.getOptionGroups()) {
selectOptions(value, optionGroup.getOptions());
}
}
}
}
}
}
}
private void deselectOptions(List options) {
for (org.appng.xml.platform.Option option : options) {
option.setSelected(false);
}
}
private void selectOptions(String value, List options) {
for (org.appng.xml.platform.Option option : options) {
boolean selected = Objects.equals(option.getValue(), value);
if (selected) {
option.setSelected(selected);
}
}
}
@Override
public String toString() {
return actionRef.getEventId() + ":" + actionRef.getId();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy