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 }