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