/*
* 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 2006 - 2013 Pentaho Corporation. All rights reserved.
*/
package org.pentaho.platform.engine.services.solution;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.actionsequence.dom.ActionInputConstant;
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.engine.ActionExecutionException;
import org.pentaho.platform.api.engine.ActionValidationException;
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.beans.ActionHarness;
import org.pentaho.platform.util.beans.AlternateIndexFormatter;
import org.pentaho.platform.util.beans.PropertyNameFormatter;
import org.pentaho.platform.util.beans.SuffixAppenderFormatter;
import org.pentaho.platform.util.beans.ValueGenerator;
import org.pentaho.platform.util.beans.ValueSetErrorCallback;
import org.pentaho.platform.util.web.MimeHelper;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 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 ActionHarness actionHarness;
protected static final PropertyNameFormatter ALTERNATE_INDEX_FORMATTER = new AlternateIndexFormatter();
protected static final PropertyNameFormatter COMPATIBILITY_FORMATTER = new ActionSequenceCompatibilityFormatter();
protected static final PropertyNameFormatter STREAM_APPENDER_FORMATTER = new SuffixAppenderFormatter( "Stream" ); //$NON-NLS-1$
private Object actionBean;
private IActionInput[] actionDefintionInputs;
private IActionOutput[] actionDefintionOutputs;
public ActionDelegate( Object actionBean ) {
this.actionBean = actionBean;
actionHarness = new ActionHarness( (IAction) actionBean );
}
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 destination-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 {
//
// Set inputs
//
InputErrorCallback errorCallback = new InputErrorCallback();
for ( IActionInput input : getActionDefinition().getInputs() ) {
Object inputValue = input.getValue();
if ( input instanceof ActionInputConstant ) {
// if the input is coming from the component definition section,
// do parameter replacement on the string and the result of that
// is the input value
inputValue = input.getStringValue( true );
}
errorCallback.setValue( inputValue );
actionHarness.setValue( input.getName(), inputValue, errorCallback, COMPATIBILITY_FORMATTER,
ALTERNATE_INDEX_FORMATTER );
}
//
// Set resources
//
ResourceCallback resourceCallback = new ResourceCallback();
for ( IActionResource res : getActionDefinition().getResources() ) {
actionHarness.setValue( res.getName(), res.getInputStream(), resourceCallback, COMPATIBILITY_FORMATTER,
ALTERNATE_INDEX_FORMATTER );
}
//
// 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>();
StreamOutputErrorCallback streamingOutputCallback = new StreamOutputErrorCallback();
OuputStreamGenerator outputStreamGenerator = new OuputStreamGenerator( outputContentItems );
IActionOutput[] contentOutputs = getActionDefinition().getOutputs( ActionSequenceDocument.CONTENT_TYPE );
if ( contentOutputs.length > 0 ) {
for ( IActionOutput contentOutput : contentOutputs ) {
outputStreamGenerator.setContentOutput( contentOutput );
actionHarness.setValue( contentOutput.getName(), outputStreamGenerator, streamingOutputCallback,
STREAM_APPENDER_FORMATTER, COMPATIBILITY_FORMATTER, ALTERNATE_INDEX_FORMATTER );
}
}
// 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 = COMPATIBILITY_FORMATTER.format( 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 if ( actionHarness.isReadable( outputName ) ) {
Object outputVal = actionHarness.getValue( 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;
}
class StreamOutputErrorCallback implements ValueSetErrorCallback {
public void failedToSetValue( Object bean, 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 );
}
public void propertyNotWritable( Object bean, 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() ) );
}
}
};
class OuputStreamGenerator implements ValueGenerator {
private Map<String, IContentItem> outputContentItems;
private IActionOutput curActionOutput;
private boolean streamingCheckPerformed = false;
public OuputStreamGenerator( Map<String, IContentItem> outputContentItems ) {
this.outputContentItems = outputContentItems;
}
public void setContentOutput( IActionOutput actionOutput ) throws Exception {
curActionOutput = actionOutput;
}
public Object getValue( 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 ResourceCallback implements ValueSetErrorCallback {
public void failedToSetValue( Object bean, 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 );
}
public void propertyNotWritable( Object bean, 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() ) );
}
}
}
class InputErrorCallback implements ValueSetErrorCallback {
private Object curValue;
public void setValue( Object value ) throws Exception {
curValue = value;
}
public void failedToSetValue( Object bean, 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 );
}
public void propertyNotWritable( Object bean, String name ) throws Exception {
if ( loggingLevel <= ILogger.WARN ) {
// log a warning if there is no way to get this input to the Action
String valueType = ( curValue == null ) ? null : curValue.getClass().getName();
warn( Messages.getInstance().getString(
"ActionDelegate.WARN_INPUT_NOT_WRITABLE", actionBean.getClass().getSimpleName(), //$NON-NLS-1$
name, valueType ) );
}
}
}
/**
* 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 );
}
}