001/*
002 * Copyright (c) 2007-2022 The Cascading Authors. All Rights Reserved.
003 *
004 * Project and contact information: https://cascading.wensel.net/
005 *
006 * This file is part of the Cascading project.
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *     http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020
021package cascading.operation.expression;
022
023import java.io.IOException;
024import java.lang.reflect.InvocationTargetException;
025import java.util.Arrays;
026
027import cascading.flow.FlowProcess;
028import cascading.management.annotation.Property;
029import cascading.management.annotation.PropertyDescription;
030import cascading.management.annotation.Visibility;
031import cascading.operation.BaseOperation;
032import cascading.operation.OperationCall;
033import cascading.operation.OperationException;
034import cascading.tuple.Fields;
035import cascading.tuple.Tuple;
036import cascading.tuple.TupleEntry;
037import cascading.tuple.Tuples;
038import cascading.tuple.coerce.Coercions;
039import cascading.tuple.type.CoercibleType;
040import cascading.tuple.util.TupleViews;
041import cascading.util.Util;
042import org.codehaus.commons.compiler.CompileException;
043import org.codehaus.janino.ScriptEvaluator;
044
045/**
046 *
047 */
048public abstract class ScriptOperation extends BaseOperation<ScriptOperation.Context>
049  {
050  /** Field expression */
051  protected final String block;
052  /** Field parameterTypes */
053  protected Class[] parameterTypes;
054  /** Field parameterNames */
055  protected String[] parameterNames;
056  /** returnType */
057  protected Class returnType = Object.class;
058
059  public ScriptOperation( int numArgs, Fields fieldDeclaration, String block )
060    {
061    super( numArgs, fieldDeclaration );
062    this.block = block;
063    this.returnType = fieldDeclaration.getTypeClass( 0 ) == null ? this.returnType : fieldDeclaration.getTypeClass( 0 );
064    }
065
066  public ScriptOperation( int numArgs, Fields fieldDeclaration, String block, Class returnType )
067    {
068    super( numArgs, fieldDeclaration );
069    this.block = block;
070    this.returnType = returnType == null ? this.returnType : returnType;
071    }
072
073  public ScriptOperation( int numArgs, Fields fieldDeclaration, String block, Class returnType, Class[] expectedTypes )
074    {
075    super( numArgs, fieldDeclaration );
076    this.block = block;
077    this.returnType = returnType == null ? this.returnType : returnType;
078
079    if( expectedTypes == null )
080      throw new IllegalArgumentException( "expectedTypes may not be null" );
081
082    this.parameterTypes = Arrays.copyOf( expectedTypes, expectedTypes.length );
083    }
084
085  public ScriptOperation( int numArgs, Fields fieldDeclaration, String block, Class returnType, String[] parameterNames, Class[] parameterTypes )
086    {
087    super( numArgs, fieldDeclaration );
088    this.parameterNames = parameterNames == null ? null : Arrays.copyOf( parameterNames, parameterNames.length );
089    this.block = block;
090    this.returnType = returnType == null ? this.returnType : returnType;
091    this.parameterTypes = Arrays.copyOf( parameterTypes, parameterTypes.length );
092
093    if( getParameterNamesInternal().length != getParameterTypesInternal().length )
094      throw new IllegalArgumentException( "parameterNames must be same length as parameterTypes" );
095    }
096
097  public ScriptOperation( int numArgs, String block, Class returnType )
098    {
099    super( numArgs );
100    this.block = block;
101    this.returnType = returnType == null ? this.returnType : returnType;
102    }
103
104  public ScriptOperation( int numArgs, String block, Class returnType, Class[] expectedTypes )
105    {
106    super( numArgs );
107    this.block = block;
108    this.returnType = returnType == null ? this.returnType : returnType;
109
110    if( expectedTypes == null || expectedTypes.length == 0 )
111      throw new IllegalArgumentException( "expectedTypes may not be null or empty" );
112
113    this.parameterTypes = Arrays.copyOf( expectedTypes, expectedTypes.length );
114    }
115
116  public ScriptOperation( int numArgs, String block, Class returnType, String[] parameterNames, Class[] parameterTypes )
117    {
118    super( numArgs );
119    this.parameterNames = parameterNames == null ? null : Arrays.copyOf( parameterNames, parameterNames.length );
120    this.block = block;
121    this.returnType = returnType == null ? this.returnType : returnType;
122    this.parameterTypes = Arrays.copyOf( parameterTypes, parameterTypes.length );
123
124    if( getParameterNamesInternal().length != getParameterTypesInternal().length )
125      throw new IllegalArgumentException( "parameterNames must be same length as parameterTypes" );
126    }
127
128  @Property(name = "source", visibility = Visibility.PRIVATE)
129  @PropertyDescription("The Java source to execute.")
130  public String getBlock()
131    {
132    return block;
133    }
134
135  private boolean hasParameterNames()
136    {
137    return parameterNames != null;
138    }
139
140  @Property(name = "parameterNames", visibility = Visibility.PUBLIC)
141  @PropertyDescription("The declared parameter names.")
142  public String[] getParameterNames()
143    {
144    return Util.copy( parameterNames );
145    }
146
147  private String[] getParameterNamesInternal()
148    {
149    if( parameterNames != null )
150      return parameterNames;
151
152    try
153      {
154      parameterNames = guessParameterNames();
155      }
156    catch( IOException exception )
157      {
158      throw new OperationException( "could not read expression: " + block, exception );
159      }
160    catch( CompileException exception )
161      {
162      throw new OperationException( "could not compile expression: " + block, exception );
163      }
164
165    return parameterNames;
166    }
167
168  protected String[] guessParameterNames() throws CompileException, IOException
169    {
170    throw new OperationException( "parameter names are required" );
171    }
172
173  private Fields getParameterFields()
174    {
175    return makeFields( getParameterNamesInternal() );
176    }
177
178  private boolean hasParameterTypes()
179    {
180    return parameterTypes != null;
181    }
182
183  @Property(name = "parameterTypes", visibility = Visibility.PUBLIC)
184  @PropertyDescription("The declared parameter types.")
185  public Class[] getParameterTypes()
186    {
187    return Util.copy( parameterTypes );
188    }
189
190  private Class[] getParameterTypesInternal()
191    {
192    if( !hasParameterNames() )
193      return parameterTypes;
194
195    if( parameterNames.length == parameterTypes.length )
196      return parameterTypes;
197
198    if( parameterNames.length > 0 && parameterTypes.length != 1 )
199      throw new IllegalStateException( "wrong number of parameter types, expects: " + parameterNames.length );
200
201    Class[] types = new Class[ parameterNames.length ];
202
203    Arrays.fill( types, parameterTypes[ 0 ] );
204
205    parameterTypes = types;
206
207    return parameterTypes;
208    }
209
210  /**
211   * Return a Class that the expression or script should extend, allowing for direct access to methods.
212   *
213   * @return a Class to extend
214   */
215  public Class<?> getExtendedClass()
216    {
217    return null;
218    }
219
220  protected Evaluator getEvaluator( Class returnType, String[] parameterNames, Class[] parameterTypes )
221    {
222    try
223      {
224      ScriptEvaluator evaluator = new ScriptEvaluator();
225
226      evaluator.setReturnType( returnType );
227      evaluator.setParameters( parameterNames, parameterTypes );
228      evaluator.setExtendedClass( getExtendedClass() );
229      evaluator.cook( block );
230
231      return evaluator::evaluate;
232      }
233    catch( CompileException exception )
234      {
235      throw new OperationException( "could not compile script: " + block, exception );
236      }
237    }
238
239  private Fields makeFields( String[] parameters )
240    {
241    Comparable[] fields = new Comparable[ parameters.length ];
242
243    for( int i = 0; i < parameters.length; i++ )
244      {
245      String parameter = parameters[ i ];
246
247      if( parameter.startsWith( "$" ) )
248        fields[ i ] = parse( parameter ); // returns parameter if not a number after $
249      else
250        fields[ i ] = parameter;
251      }
252
253    return new Fields( fields );
254    }
255
256  private Comparable parse( String parameter )
257    {
258    try
259      {
260      return Integer.parseInt( parameter.substring( 1 ) );
261      }
262    catch( NumberFormatException exception )
263      {
264      return parameter;
265      }
266    }
267
268  @Override
269  public void prepare( FlowProcess flowProcess, OperationCall<Context> operationCall )
270    {
271    if( operationCall.getContext() == null )
272      operationCall.setContext( new Context() );
273
274    Context context = operationCall.getContext();
275
276    Fields argumentFields = operationCall.getArgumentFields();
277
278    if( hasParameterNames() && hasParameterTypes() )
279      {
280      context.parameterNames = getParameterNamesInternal();
281      context.parameterFields = argumentFields.select( getParameterFields() ); // inherit argument types
282      context.parameterTypes = getParameterTypesInternal();
283      }
284    else if( hasParameterTypes() )
285      {
286      context.parameterNames = toNames( argumentFields );
287      context.parameterFields = argumentFields.applyTypes( getParameterTypesInternal() );
288      context.parameterTypes = getParameterTypesInternal();
289      }
290    else
291      {
292      context.parameterNames = toNames( argumentFields );
293      context.parameterFields = argumentFields;
294      context.parameterTypes = argumentFields.getTypesClasses();
295
296      if( argumentFields.isNone() )
297        context.parameterTypes = new Class[ 0 ]; // to match names
298
299      if( context.parameterTypes == null )
300        throw new IllegalArgumentException( "field types may not be empty, incoming tuple stream should declare field types" );
301      }
302
303    context.parameterCoercions = Coercions.coercibleArray( context.parameterFields );
304    context.parameterArray = new Object[ context.parameterTypes.length ]; // re-use object array
305    context.scriptEvaluator = getEvaluator( getReturnType(), context.parameterNames, context.parameterTypes );
306    context.intermediate = TupleViews.createNarrow( argumentFields.getPos( context.parameterFields ) );
307    context.result = Tuple.size( 1 ); // re-use the output tuple
308    }
309
310  private String[] toNames( Fields argumentFields )
311    {
312    String[] names = new String[ argumentFields.size() ];
313
314    for( int i = 0; i < names.length; i++ )
315      {
316      Comparable comparable = argumentFields.get( i );
317      if( comparable instanceof String )
318        names[ i ] = (String) comparable;
319      else
320        names[ i ] = "$" + comparable;
321      }
322
323    return names;
324    }
325
326  public Class getReturnType()
327    {
328    return returnType;
329    }
330
331  /**
332   * Performs the actual expression evaluation.
333   *
334   * @param context
335   * @param input   of type TupleEntry
336   * @return Comparable
337   */
338  protected Object evaluate( Context context, TupleEntry input )
339    {
340    try
341      {
342      if( context.parameterTypes.length == 0 )
343        return context.scriptEvaluator.evaluate( null );
344
345      Tuple parameterTuple = TupleViews.reset( context.intermediate, input.getTuple() );
346      Object[] arguments = Tuples.asArray( parameterTuple, context.parameterCoercions, context.parameterTypes, context.parameterArray );
347
348      return context.scriptEvaluator.evaluate( arguments );
349      }
350    catch( IllegalArgumentException exception )
351      {
352      throw new OperationException( "could not evaluate expression: " + block + ", typed: " + Arrays.toString( context.parameterTypes ) + " coerced by: " + Arrays.toString( context.parameterCoercions ), exception );
353      }
354    catch( InvocationTargetException exception )
355      {
356      throw new OperationException( "could not evaluate expression: " + block + ", typed: " + Arrays.toString( context.parameterTypes ) + " coerced by: " + Arrays.toString( context.parameterCoercions ), exception.getTargetException() );
357      }
358    }
359
360  @Override
361  public boolean equals( Object object )
362    {
363    if( this == object )
364      return true;
365    if( !( object instanceof ExpressionOperation ) )
366      return false;
367    if( !super.equals( object ) )
368      return false;
369
370    ExpressionOperation that = (ExpressionOperation) object;
371
372    if( block != null ? !block.equals( that.block ) : that.block != null )
373      return false;
374    if( !Arrays.equals( parameterNames, that.parameterNames ) )
375      return false;
376    if( !Arrays.equals( parameterTypes, that.parameterTypes ) )
377      return false;
378
379    return true;
380    }
381
382  @Override
383  public int hashCode()
384    {
385    int result = super.hashCode();
386    result = 31 * result + ( block != null ? block.hashCode() : 0 );
387    result = 31 * result + ( parameterTypes != null ? Arrays.hashCode( parameterTypes ) : 0 );
388    result = 31 * result + ( parameterNames != null ? Arrays.hashCode( parameterNames ) : 0 );
389    return result;
390    }
391
392  protected interface Evaluator
393    {
394    Object evaluate( Object[] arguments ) throws InvocationTargetException;
395    }
396
397  public static class Context
398    {
399    protected Tuple result;
400    private Class[] parameterTypes;
401    private Evaluator scriptEvaluator;
402    private Fields parameterFields;
403    private CoercibleType[] parameterCoercions;
404    private String[] parameterNames;
405    private Object[] parameterArray;
406    private Tuple intermediate;
407    }
408  }