/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU General Public License, version 2 as published by the Free Software * Foundation. * * You should have received a copy of the GNU General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/gpl-2.0.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * * Copyright 2009 Pentaho Corporation. All rights reserved. * */ package org.pentaho.platform.engine.services.solution; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.actionsequence.dom.ActionSequenceDocument; import org.pentaho.actionsequence.dom.IActionInput; import org.pentaho.actionsequence.dom.IActionOutput; import org.pentaho.actionsequence.dom.IActionResource; import org.pentaho.actionsequence.dom.IActionSequenceOutput; import org.pentaho.platform.api.action.ActionPreProcessingException; import org.pentaho.platform.api.action.IAction; import org.pentaho.platform.api.action.IDefinitionAwareAction; import org.pentaho.platform.api.action.ILoggingAction; import org.pentaho.platform.api.action.IPreProcessingAction; import org.pentaho.platform.api.action.ISessionAwareAction; import org.pentaho.platform.api.action.IStreamingAction; import org.pentaho.platform.api.action.IVarArgsAction; import org.pentaho.platform.api.engine.ActionExecutionException; import org.pentaho.platform.api.engine.ActionValidationException; import org.pentaho.platform.api.engine.IComponent; import org.pentaho.platform.api.engine.ILogger; import org.pentaho.platform.api.repository.IContentItem; import org.pentaho.platform.engine.core.output.SimpleContentItem; import org.pentaho.platform.engine.services.messages.Messages; import org.pentaho.platform.util.web.MimeHelper; /** * The purpose of the {@link ActionDelegate} is to represent an action object * (which implements {@link IAction}) as an {@link IComponent}. * * @see IAction */ @SuppressWarnings("serial") public class ActionDelegate extends ComponentBase { private static ActionBeanUtil beanUtil; private Object actionBean; private IActionInput[] actionDefintionInputs; private IActionOutput[] actionDefintionOutputs; public ActionDelegate(Object actionBean) { this.actionBean = actionBean; beanUtil = new ActionBeanUtil(); } public Object getActionBean() { return actionBean; } /** * Clean-up should happen in the {@link IAction#execute()} **/ @Override public void done() { } /** * This method will tell you if an output in the action definition references an * output stream that has a global/public destination, such as "response", or "content". * An action definition output is considered thusly, if it has a counterpart of the * same name in the action sequence outputs AND that output is of type "content" * AND it has declared one or more destinations. * @param contentOutput the action definition output to check * @return true if this output corresponds to a public destintion-bound output */ protected boolean hasPublicDestination(IActionOutput contentOutput) { String resolvedName = contentOutput.getPublicName(); IActionSequenceOutput publicOutput = getActionDefinition().getDocument().getOutput(resolvedName); if (publicOutput == null) { return false; } return (publicOutput.getType().equals(ActionSequenceDocument.CONTENT_TYPE) && publicOutput.getDestinations().length > 0); } /** * Wires up inputs outputs and resources to an Action and executes it. */ @Override protected boolean executeAction() throws Throwable { // //Create a map for passing undeclared inputs if an IVarArgsAction // Map<String, Object> varArgsMap = null; if (actionBean instanceof IVarArgsAction) { varArgsMap = new HashMap<String, Object>(); ((IVarArgsAction) actionBean).setVarArgs(varArgsMap); } // //Set inputs // InputOps inputOps = new InputOps(varArgsMap); for (IActionInput input : getActionDefinition().getInputs()) { // inputOps.setValue(input.getName(), input.getValue()); inputOps.setInput(input); } // //Set resources // ResourceOps resOps = new ResourceOps(); for (IActionResource res : getActionDefinition().getResources()) { resOps.setResource(res); } // //Provide output stream for the streaming action. We are going to look for all outputs where //type = "content", and derive output streams to hand to the IStreamingAction. // Map<String, IContentItem> outputContentItems = new HashMap<String, IContentItem>(); StreamingOutputOps streamOutputOps = new StreamingOutputOps(outputContentItems); IActionOutput[] contentOutputs = getActionDefinition().getOutputs(ActionSequenceDocument.CONTENT_TYPE); if (contentOutputs.length > 0) { for (IActionOutput contentOutput : contentOutputs) { streamOutputOps.setOutputStream(contentOutput); } } //else, This is not necessarily an error condition. Let the action bean decide. // //Execute the Action if the bean is executable // if (actionBean instanceof IAction) { ((IAction) actionBean).execute(); } // //Get and store outputs // for (IActionOutput output : actionDefintionOutputs) { String outputName = output.getName(); outputName = compatibilityToCamelCase(outputName); //if streaming output, add it to the context and don't try to get it from the Action bean if (outputContentItems.containsKey(outputName)) { IContentItem contentItem = outputContentItems.get(outputName); if (!(contentItem instanceof SimpleContentItem)) { //this is a special output for streaming actions and does not require a bean accessor output.setValue(contentItem); } else { // warn(SimpleContentItem.class.getSimpleName() + " is for testing purposes only and should not be used in production."); } } else if (beanUtil.isReadable(actionBean, outputName)) { Object outputVal = beanUtil.getValue(actionBean, outputName); output.setValue(outputVal); } else { if (loggingLevel <= ILogger.WARN) { warn(Messages.getInstance().getString("ActionDelegate.WARN_OUTPUT_NOT_READABLE", //$NON-NLS-1$ outputName, output.getType(), actionBean.getClass().getSimpleName())); } } } return true; } abstract class BeanOpsTemplate { abstract public void failedToSetValue(String name, Object value, String beanPropertyType, Throwable cause) throws ActionExecutionException; public String getPropertyNameSuffix() { return ""; //$NON-NLS-1$ } abstract public void propertyNotWritable(String name) throws Exception; abstract public Object getValueToSet(String name) throws Exception; /** * Converts to a bean utils consumable expression and applies other customizations * as necessary, such as suffix additions. * @param name the property name to format * @return the formatted property name ready for use in bean utils */ public String format(String name) { String formattedName = name; int indexDelimiter = name.lastIndexOf('_'); if (indexDelimiter > 0) { //implies there is a name and an index like 'b_1' or 'a_b' String possibleIndex = name.substring(indexDelimiter + 1); try { int index = Integer.parseInt(possibleIndex); String propertyName = name.substring(0, indexDelimiter); formattedName = propertyName + getPropertyNameSuffix() + "[" + index + "]"; //$NON-NLS-1$ //$NON-NLS-2$ return formattedName; } catch (NumberFormatException e) { //we don't have a numeric index, so just return the original expression } } return formattedName + getPropertyNameSuffix(); } public void setValue(String name) throws Exception { name = compatibilityToCamelCase(name); name = format(name); //here we check if we can set the input value on the bean. There are three ways that bean utils will go about this //1. use a simple property setter method //.. in the case of an indexed property there are two methods: //2. if there is an indexed setter method bean utils will that (note: a simple getter is required as well though it will not be invoked) //3. if there is an array-based getter like List<String> getNames(), bean utils will insert the new value into the array reference // it gets from the array getter. if (beanUtil.isWriteable(actionBean, name)) { //we get the value at the latest point possible Object val = getValueToSet(name); try { //trying our best to set the input value to the type specified by the action bean beanUtil.setValue(actionBean, name, val); } catch (Exception e) { String propertyType = ""; //$NON-NLS-1$ try { propertyType = beanUtil.getClass(actionBean, name).getName(); } catch (Throwable t) { //we are in a nested catch, we should never let an exception escape here } failedToSetValue(name, val, propertyType, e); } } else { propertyNotWritable(name); } } } class StreamingOutputOps extends BeanOpsTemplate { private Map<String, IContentItem> outputContentItems; private boolean streamingCheckPerformed = false; private IActionOutput curActionOutput; public StreamingOutputOps(Map<String, IContentItem> outputContentItems) { this.outputContentItems = outputContentItems; } public void setOutputStream(IActionOutput actionOutput) throws Exception { curActionOutput = actionOutput; super.setValue(actionOutput.getName()); } @Override public String getPropertyNameSuffix() { return "Stream"; //$NON-NLS-1$ } @Override public void failedToSetValue(String name, Object value, String destPropertyType, Throwable cause) throws ActionExecutionException { throw new ActionExecutionException(Messages.getInstance().getErrorString( "ActionDelegate.ERROR_0008_FAILED_TO_SET_STREAM", name, OutputStream.class.getName(), //$NON-NLS-1$ actionBean.getClass().getSimpleName(), destPropertyType), cause); } @Override public void propertyNotWritable(String name) { if (loggingLevel <= ILogger.WARN) { warn(Messages.getInstance().getString("ActionDelegate.WARN_INPUT_NOT_WRITABLE", actionBean //$NON-NLS-1$ .getClass().getSimpleName(), name, OutputStream.class.getName())); } } @Override public Object getValueToSet(String name) throws Exception { //fail early if we cannot handle stream outputs if (!streamingCheckPerformed && !(actionBean instanceof IStreamingAction)) { throw new ActionExecutionException(Messages.getInstance().getErrorString( "ActionDelegate.ERROR_0002_ACTION_CANNOT_ACCEPT_STREAM", //$NON-NLS-1$ name, actionBean.getClass().getSimpleName())); } streamingCheckPerformed = true; String mimeType = ((IStreamingAction) actionBean).getMimeType(name); if (StringUtils.isEmpty(mimeType)) { throw new ActionValidationException(Messages.getInstance().getErrorString( "ActionDelegate.ERROR_0001_MIMETYPE_NOT_DECLARED")); //$NON-NLS-1$ } IContentItem contentItem = null; // //If the output is mapped publicly and has a destination associated with it, then we will be asking //the current IOuputHandler to create an IContentItem (OuputStream) for us. Otherwise, we will asking the //IContentOutputHandler impl registered to handle content destined for "contentrepo" to create //an IContentItem (OutputStream) for us. // if (hasPublicDestination(curActionOutput)) { //most output handlers will manage multiple destinations for us and hand us back a MultiContentItem contentItem = getRuntimeContext().getOutputContentItem(curActionOutput.getPublicName(), mimeType); } else { String extension = MimeHelper.getExtension(mimeType); if(extension == null) { extension = ".bin"; //$NON-NLS-1$ } contentItem = getRuntimeContext().getOutputItem(curActionOutput.getName(), mimeType, extension); } if (contentItem == null) { //this is the best I can do here to point users to a tangible problem without unwrapping code in RuntimeEngine - AP throw new ActionValidationException(Messages.getInstance().getErrorString( "ActionDelegate.ERROR_0003_OUTPUT_STREAM_NOT_AVAILABLE_1", //$NON-NLS-1$ curActionOutput.getPublicName())); } //this will be a MultiOutputStream in the case where there is more than one destination for the content output OutputStream contentOutputStream = contentItem.getOutputStream(getActionName()); if (contentOutputStream == null) { throw new ActionExecutionException(Messages.getInstance().getErrorString( "ActionDelegate.ERROR_0004_OUTPUT_STREAM_NOT_AVAILABLE_2", //$NON-NLS-1$ actionBean.getClass().getSimpleName())); } //save this for later when we set the action outputs outputContentItems.put(curActionOutput.getName(), contentItem); return contentOutputStream; } }; class ResourceOps extends BeanOpsTemplate { private IActionResource curResource; public void setResource(IActionResource resource) throws Exception { curResource = resource; super.setValue(resource.getName()); } @Override public void failedToSetValue(String name, Object value, String destPropertyType, Throwable cause) throws ActionExecutionException { String className = (value != null) ? value.getClass().getName() : "ClassNameNotAvailable"; //$NON-NLS-1$ throw new ActionExecutionException(Messages.getInstance().getErrorString( "ActionDelegate.ERROR_0006_FAILED_TO_SET_RESOURCE", //$NON-NLS-1$ name, className, actionBean.getClass().getSimpleName(), destPropertyType), cause); } @Override public void propertyNotWritable(String name) { if (loggingLevel <= ILogger.WARN) { warn(Messages.getInstance().getString("ActionDelegate.WARN_RESOURCE_NOT_WRITABLE", actionBean //$NON-NLS-1$ .getClass().getSimpleName(), name, InputStream.class.getName())); } } @Override public Object getValueToSet(String name) throws Exception { return curResource.getInputStream(); } } class InputOps extends BeanOpsTemplate { private Map<String, Object> varArgsMap; private IActionInput curInput; public InputOps(Map<String, Object> varArgsMap) { this.varArgsMap = varArgsMap; } public void setInput(IActionInput input) throws Exception { curInput = input; super.setValue(input.getName()); } @Override public void failedToSetValue(String name, Object value, String destPropertyType, Throwable cause) throws ActionExecutionException { String className = (value != null) ? value.getClass().getName() : "ClassNameNotAvailable"; //$NON-NLS-1$ throw new ActionExecutionException(Messages.getInstance().getErrorString( "ActionDelegate.ERROR_0005_FAILED_TO_SET_INPUT", //$NON-NLS-1$ name, className, actionBean.getClass().getSimpleName(), destPropertyType), cause); } @Override public void propertyNotWritable(String name) throws Exception { //the input is undeclared, put it in the varArgsMap if anyone cares Object value = getValueToSet(name); if (varArgsMap != null) { varArgsMap.put(name, value); } else if (loggingLevel <= ILogger.WARN) { //log a warning if there is no way to get this input to the Action String valueType = (value == null) ? null : value.getClass().getName(); warn(Messages.getInstance().getString( "ActionDelegate.WARN_INPUT_NOT_WRITABLE", actionBean.getClass().getSimpleName(), //$NON-NLS-1$ name, valueType)); } } @Override public Object getValueToSet(String name) throws Exception { return curInput.getValue(); } } /** * Any initialization can be done in the {@link IPreProcessingAction#doPreExecution()} */ @Override public boolean init() { return true; } /** * Validation of Action input values should happen in the {@link IAction#execute()} * This method is used as a pre execution hook where we setup as much runtime information as * possible prior to the actual execute call. **/ @Override protected boolean validateAction() { if (actionBean == null) { throw new IllegalArgumentException(Messages.getInstance().getErrorString( "ActionDelegate.ERROR_0007_NO_ACTION_BEAN_SPECIFIED")); //$NON-NLS-1$ } // //Provide a commons logging logger for logging actions //The log name will be the name of the Action class // if (actionBean instanceof ILoggingAction) { ((ILoggingAction) actionBean).setLogger(LogFactory.getLog(actionBean.getClass())); } // //Provide a session to the Action if an ISessionAwareAction // if (actionBean instanceof ISessionAwareAction) { ((ISessionAwareAction) actionBean).setSession(getSession()); } actionDefintionInputs = getActionDefinition().getInputs(); actionDefintionOutputs = getActionDefinition().getOutputs(); // // If an Action is action-definition aware, then here is the place (prior to // execution) to tell it about the action definition. // List<String> inputNames = new ArrayList<String>(); for (IActionInput input : actionDefintionInputs) { inputNames.add(input.getName()); } List<String> outputNames = new ArrayList<String>(); for (IActionOutput output : actionDefintionOutputs) { outputNames.add(output.getName()); } if (actionBean instanceof IDefinitionAwareAction) { IDefinitionAwareAction definitionAwareAction = (IDefinitionAwareAction) actionBean; definitionAwareAction.setInputNames(inputNames); definitionAwareAction.setOutputNames(outputNames); } // // Invoke any pre-execution processing if the Action requires it. // if (actionBean instanceof IPreProcessingAction) { try { ((IPreProcessingAction) actionBean).doPreExecution(); } catch (ActionPreProcessingException e) { throw new RuntimeException(e); } } //we do not use the return value to indicate failure. return true; } @Override protected boolean validateSystemSettings() { return true; } @Override public Log getLogger() { return LogFactory.getLog(ActionDelegate.class); } /** * This method exists to make old-style action sequence inputs, * outputs and resource names work bean utils which adheres * to the Java bean spec. All action definition inputs outputs * and resources should be named in camel case and dash characters * "-" should be avoided. This method will convert a dash-style * arg name into camel case and print a warning, or just return the * original name if there are no dashes found. * @param name argument name to convert, if needed. * @return camel case representation of name */ protected String compatibilityToCamelCase(String name) { String[] parts = name.split("-", 0); //$NON-NLS-1$ if (parts.length > 1) { String camelCaseName = ""; //$NON-NLS-1$ for (int i = 0; i < parts.length; i++) { if (i > 0) { camelCaseName += StringUtils.capitalize(parts[i]); } else { camelCaseName += parts[i]; } } getLogger().warn( Messages.getInstance().getString("ActionDelegate.WARN_USING_IO_COMPATIBILITY_MODE", camelCaseName, name)); //$NON-NLS-1$ return camelCaseName; } return name; } }