/*! * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.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 Lesser General Public License for more details. * * Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved. */ package org.pentaho.platform.plugin.action.javascript; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.mozilla.javascript.Context; import org.mozilla.javascript.ContextFactory; import org.mozilla.javascript.NativeArray; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.pentaho.actionsequence.dom.ActionInputConstant; import org.pentaho.actionsequence.dom.IActionOutput; import org.pentaho.actionsequence.dom.actions.JavascriptAction; import org.pentaho.commons.connection.IPentahoResultSet; import org.pentaho.platform.api.engine.IActionSequenceResource; import org.pentaho.platform.engine.services.solution.ComponentBase; import org.pentaho.platform.plugin.action.messages.Messages; import org.pentaho.platform.plugin.condition.javascript.RhinoScriptable; import org.pentaho.platform.plugin.services.connections.javascript.JavaScriptResultSet; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; /** * @author James Dixon * * TODO To change the template for this generated type comment go to Window - Preferences - Java - Code Style - * Code Templates */ public class JavascriptRule extends ComponentBase { /** * */ private static final long serialVersionUID = -8305132222755452461L; private boolean oldStyleOutputs; @Override public Log getLogger() { return LogFactory.getLog( JavascriptRule.class ); } @Override protected boolean validateSystemSettings() { // This component does not have any system settings to validate return true; } @Override protected boolean validateAction() { boolean actionValidated = true; JavascriptAction jscriptAction = null; if ( getActionDefinition() instanceof JavascriptAction ) { jscriptAction = (JavascriptAction) getActionDefinition(); // get report connection setting if ( jscriptAction.getScript() == ActionInputConstant.NULL_INPUT ) { error( Messages.getInstance().getErrorString( "JSRULE.ERROR_0001_SCRIPT_NOT_DEFINED", getActionName() ) ); //$NON-NLS-1$ actionValidated = false; } if ( actionValidated ) { if ( jscriptAction.getOutputs().length <= 0 ) { error( Messages.getInstance().getString( "Template.ERROR_0002_OUTPUT_COUNT_WRONG" ) ); //$NON-NLS-1$ actionValidated = false; } } if ( actionValidated ) { IActionOutput[] outputs = jscriptAction.getOutputs(); // getOutputNames(); /* * if the number of action def outputs is more than 1 and if the fist output var defined in the input section is * named output1 then the xaction is defined oldstyle and we should check if there is corresponding o/p variable * defined in the input section for each output var defined in the output section. NOTE: With the old style you * can only have output defined as "output#" where # is a number. */ if ( ( outputs.length > 1 ) && ( jscriptAction.getInput( "output1" ) != ActionInputConstant.NULL_INPUT ) ) { //$NON-NLS-1$ oldStyleOutputs = true; for ( int i = 1; i <= outputs.length; ++i ) { if ( jscriptAction.getInput( "output" + i ) == ActionInputConstant.NULL_INPUT ) { //$NON-NLS-1$ error( Messages.getInstance().getErrorString( "JavascriptRule.ERROR_0006_NO_MAPPED_OUTPUTS", String.valueOf( outputs.length ), String.valueOf( i ) ) ); //$NON-NLS-1$ actionValidated = false; break; } } } } } else { actionValidated = false; error( Messages.getInstance().getErrorString( "ComponentBase.ERROR_0001_UNKNOWN_ACTION_TYPE", getActionDefinition().getElement().asXML() ) ); //$NON-NLS-1$ } return actionValidated; } @Override public void done() { } /* * (non-Javadoc) * * @see org.pentaho.component.ComponentBase#execute() */ @Override protected boolean executeAction() { Context cx = ContextFactory.getGlobal().enterContext(); StringBuffer buffer = new StringBuffer(); @SuppressWarnings( "unchecked" ) Iterator<String> iter = getResourceNames().iterator(); while ( iter.hasNext() ) { IActionSequenceResource resource = getResource( iter.next().toString() ); // If this is a javascript resource then append it to the script string if ( "text/javascript".equalsIgnoreCase( resource.getMimeType() ) ) { //$NON-NLS-1$ buffer.append( getResourceAsString( resource ) ); } } List<String> outputNames = new ArrayList<String>(); JavascriptAction jscriptAction = (JavascriptAction) getActionDefinition(); IActionOutput[] actionOutputs = jscriptAction.getOutputs(); if ( actionOutputs.length == 1 ) { String outputName = actionOutputs[0].getName(); outputNames.add( outputName ); } else { if ( oldStyleOutputs ) { int i = 1; while ( true ) { if ( jscriptAction.getInput( "output" + i ) != ActionInputConstant.NULL_INPUT ) { //$NON-NLS-1$ outputNames.add( jscriptAction.getInput( "output" + i ).getStringValue() ); //$NON-NLS-1$ } else { break; } i++; } } else { for ( IActionOutput element : actionOutputs ) { outputNames.add( element.getName() ); } } } boolean success = false; try { String script = jscriptAction.getScript().getStringValue(); if ( script == null ) { error( Messages.getInstance().getErrorString( "JSRULE.ERROR_0001_SCRIPT_NOT_DEFINED", getActionName() ) ); //$NON-NLS-1$ } else { buffer.append( script ); script = buffer.toString(); if ( ComponentBase.debug ) { debug( "script=" + script ); //$NON-NLS-1$ } try { ScriptableObject scriptable = new RhinoScriptable(); // initialize the standard javascript objects Scriptable scope = cx.initStandardObjects( scriptable ); Object resultObject = executeScript( scriptable, scope, script, cx ); if ( oldStyleOutputs ) { if ( resultObject instanceof org.mozilla.javascript.NativeArray ) { // we need to convert this to an ArrayList NativeArray jsArray = (NativeArray) resultObject; int length = (int) jsArray.getLength(); for ( int i = 0; i < length; i++ ) { Object value = jsArray.get( i, scriptable ); if ( i < outputNames.size() ) { jscriptAction.getOutput( outputNames.get( i ).toString() ) .setValue( convertWrappedJavaObject( value ) ); } else { break; } } } else { jscriptAction.getOutput( outputNames.get( 0 ).toString() ).setValue( convertWrappedJavaObject( resultObject ) ); } } else { if ( ( outputNames.size() == 1 ) && ( resultObject != null ) ) { jscriptAction.getOutput( outputNames.get( 0 ).toString() ).setValue( convertWrappedJavaObject( resultObject ) ); } else { List<String> setOutputs = new ArrayList<String>( outputNames.size() ); Object[] ids = ScriptableObject.getPropertyIds( scope ); for ( Object element : ids ) { int idx = outputNames.indexOf( element.toString() ); if ( idx >= 0 ) { jscriptAction.getOutput( outputNames.get( idx ).toString() ).setValue( convertWrappedJavaObject( ScriptableObject.getProperty( scope, (String) element ) ) ); setOutputs.add( outputNames.get( idx ) ); } } // Javascript Component defined an output, but // didn't set it to anything. // So, set it to null. if ( setOutputs.size() != outputNames.size() ) { for ( int i = 0; i < outputNames.size(); i++ ) { if ( setOutputs.indexOf( outputNames.get( i ) ) < 0 ) { // An output that wasn't set in the // javascript component jscriptAction.getOutput( outputNames.get( i ).toString() ).setValue( null ); } } } } } success = true; } catch ( Exception e ) { error( Messages.getInstance().getErrorString( "JSRULE.ERROR_0003_EXECUTION_FAILED" ), e ); //$NON-NLS-1$ } } } finally { Context.exit(); } return success; } protected Object executeScript( final ScriptableObject scriptable, final Scriptable scope, final String script, final Context cx ) throws Exception { ScriptableObject.defineClass( scope, JavaScriptResultSet.class ); @SuppressWarnings( "unchecked" ) Set<String> inputNames = getInputNames(); Iterator<String> inputNamesIterator = inputNames.iterator(); String inputName; Object inputValue; while ( inputNamesIterator.hasNext() ) { inputName = (String) inputNamesIterator.next(); if ( inputName.indexOf( '-' ) >= 0 ) { throw new IllegalArgumentException( Messages.getInstance().getErrorString( "JSRULE.ERROR_0006_INVALID_JS_VARIABLE", inputName ) ); //$NON-NLS-1$ } inputValue = getInputValue( inputName ); Object wrapper; if ( inputValue instanceof IPentahoResultSet ) { JavaScriptResultSet results = new JavaScriptResultSet(); // Required as of Rhino 1.7R1 to resolve caching, base object // inheritance and property tree results.setPrototype( scriptable ); results.setResultSet( (IPentahoResultSet) inputValue ); wrapper = Context.javaToJS( inputValue, results ); } else { wrapper = Context.javaToJS( inputValue, scope ); } ScriptableObject.putProperty( scope, inputName, wrapper ); } // Add system out and this object to the scope Object wrappedOut = Context.javaToJS( System.out, scope ); Object wrappedThis = Context.javaToJS( this, scope ); ScriptableObject.putProperty( scope, "out", wrappedOut ); //$NON-NLS-1$ ScriptableObject.putProperty( scope, "rule", wrappedThis ); //$NON-NLS-1$ // evaluate the script return cx.evaluateString( scope, script, "<cmd>", 1, null ); //$NON-NLS-1$ } protected Object convertWrappedJavaObject( final Object obj ) { // If we wrap an object going in, and simply return the object, // without unwrapping it, we're left with an object we can't // use. Case in point was a Java array going in, and being // wrapped as a org.mozilla.javascript.NativeJavaArray. On // the way back into the context though, it stayed a mozilla // object. This unwraps objects properly so that they can be // recognized throughout the system. if ( obj instanceof org.mozilla.javascript.NativeJavaObject ) { return ( (org.mozilla.javascript.NativeJavaObject) obj ).unwrap(); } else { return obj; } } @Override public boolean init() { return true; } }