001 /* 002 * Copyright (c) 2007-2014 Concurrent, Inc. All Rights Reserved. 003 * 004 * Project and contact information: http://www.cascading.org/ 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 021 package cascading.operation.expression; 022 023 import java.io.IOException; 024 import java.lang.reflect.InvocationTargetException; 025 import java.util.Arrays; 026 027 import cascading.flow.FlowProcess; 028 import cascading.operation.BaseOperation; 029 import cascading.operation.OperationCall; 030 import cascading.operation.OperationException; 031 import cascading.tuple.Fields; 032 import cascading.tuple.Tuple; 033 import cascading.tuple.TupleEntry; 034 import cascading.tuple.Tuples; 035 import cascading.tuple.coerce.Coercions; 036 import cascading.tuple.type.CoercibleType; 037 import cascading.tuple.util.TupleViews; 038 import cascading.util.Util; 039 import org.codehaus.commons.compiler.CompileException; 040 import org.codehaus.janino.ScriptEvaluator; 041 042 /** 043 * 044 */ 045 public abstract class ScriptOperation extends BaseOperation<ScriptOperation.Context> 046 { 047 /** Field expression */ 048 protected final String block; 049 /** Field parameterTypes */ 050 protected Class[] parameterTypes; 051 /** Field parameterNames */ 052 protected String[] parameterNames; 053 /** returnType */ 054 protected Class returnType = Object.class; 055 056 public ScriptOperation( int numArgs, Fields fieldDeclaration, String block ) 057 { 058 super( numArgs, fieldDeclaration ); 059 this.block = block; 060 this.returnType = fieldDeclaration.getTypeClass( 0 ) == null ? this.returnType : fieldDeclaration.getTypeClass( 0 ); 061 } 062 063 public ScriptOperation( int numArgs, Fields fieldDeclaration, String block, Class returnType ) 064 { 065 super( numArgs, fieldDeclaration ); 066 this.block = block; 067 this.returnType = returnType == null ? this.returnType : returnType; 068 } 069 070 public ScriptOperation( int numArgs, Fields fieldDeclaration, String block, Class returnType, Class[] expectedTypes ) 071 { 072 super( numArgs, fieldDeclaration ); 073 this.block = block; 074 this.returnType = returnType == null ? this.returnType : returnType; 075 076 if( expectedTypes == null ) 077 throw new IllegalArgumentException( "expectedTypes may not be null" ); 078 079 this.parameterTypes = Arrays.copyOf( expectedTypes, expectedTypes.length ); 080 } 081 082 public ScriptOperation( int numArgs, Fields fieldDeclaration, String block, Class returnType, String[] parameterNames, Class[] parameterTypes ) 083 { 084 super( numArgs, fieldDeclaration ); 085 this.parameterNames = parameterNames == null ? null : Arrays.copyOf( parameterNames, parameterNames.length ); 086 this.block = block; 087 this.returnType = returnType == null ? this.returnType : returnType; 088 this.parameterTypes = Arrays.copyOf( parameterTypes, parameterTypes.length ); 089 090 if( getParameterNamesInternal().length != getParameterTypesInternal().length ) 091 throw new IllegalArgumentException( "parameterNames must be same length as parameterTypes" ); 092 } 093 094 public ScriptOperation( int numArgs, String block, Class returnType ) 095 { 096 super( numArgs ); 097 this.block = block; 098 this.returnType = returnType == null ? this.returnType : returnType; 099 } 100 101 public ScriptOperation( int numArgs, String block, Class returnType, Class[] expectedTypes ) 102 { 103 super( numArgs ); 104 this.block = block; 105 this.returnType = returnType == null ? this.returnType : returnType; 106 107 if( expectedTypes == null || expectedTypes.length == 0 ) 108 throw new IllegalArgumentException( "expectedTypes may not be null or empty" ); 109 110 this.parameterTypes = Arrays.copyOf( expectedTypes, expectedTypes.length ); 111 } 112 113 public ScriptOperation( int numArgs, String block, Class returnType, String[] parameterNames, Class[] parameterTypes ) 114 { 115 super( numArgs ); 116 this.parameterNames = parameterNames == null ? null : Arrays.copyOf( parameterNames, parameterNames.length ); 117 this.block = block; 118 this.returnType = returnType == null ? this.returnType : returnType; 119 this.parameterTypes = Arrays.copyOf( parameterTypes, parameterTypes.length ); 120 121 if( getParameterNamesInternal().length != getParameterTypesInternal().length ) 122 throw new IllegalArgumentException( "parameterNames must be same length as parameterTypes" ); 123 } 124 125 protected String getBlock() 126 { 127 return block; 128 } 129 130 private boolean hasParameterNames() 131 { 132 return parameterNames != null; 133 } 134 135 public String[] getParameterNames() 136 { 137 return Util.copy( parameterNames ); 138 } 139 140 private String[] getParameterNamesInternal() 141 { 142 if( parameterNames != null ) 143 return parameterNames; 144 145 try 146 { 147 parameterNames = guessParameterNames(); 148 } 149 catch( IOException exception ) 150 { 151 throw new OperationException( "could not read expression: " + block, exception ); 152 } 153 catch( CompileException exception ) 154 { 155 throw new OperationException( "could not compile expression: " + block, exception ); 156 } 157 158 return parameterNames; 159 } 160 161 protected String[] guessParameterNames() throws CompileException, IOException 162 { 163 throw new OperationException( "parameter names are required" ); 164 } 165 166 private Fields getParameterFields() 167 { 168 return makeFields( getParameterNamesInternal() ); 169 } 170 171 private boolean hasParameterTypes() 172 { 173 return parameterTypes != null; 174 } 175 176 public Class[] getParameterTypes() 177 { 178 return Util.copy( parameterTypes ); 179 } 180 181 private Class[] getParameterTypesInternal() 182 { 183 if( !hasParameterNames() ) 184 return parameterTypes; 185 186 if( hasParameterNames() && parameterNames.length == parameterTypes.length ) 187 return parameterTypes; 188 189 if( parameterNames.length > 0 && parameterTypes.length != 1 ) 190 throw new IllegalStateException( "wrong number of parameter types, expects: " + parameterNames.length ); 191 192 Class[] types = new Class[ parameterNames.length ]; 193 194 Arrays.fill( types, parameterTypes[ 0 ] ); 195 196 parameterTypes = types; 197 198 return parameterTypes; 199 } 200 201 protected ScriptEvaluator getEvaluator( Class returnType, String[] parameterNames, Class[] parameterTypes ) 202 { 203 try 204 { 205 return new ScriptEvaluator( block, returnType, parameterNames, parameterTypes ); 206 } 207 catch( CompileException exception ) 208 { 209 throw new OperationException( "could not compile script: " + block, exception ); 210 } 211 } 212 213 private Fields makeFields( String[] parameters ) 214 { 215 Comparable[] fields = new Comparable[ parameters.length ]; 216 217 for( int i = 0; i < parameters.length; i++ ) 218 { 219 String parameter = parameters[ i ]; 220 221 if( parameter.startsWith( "$" ) ) 222 fields[ i ] = parse( parameter ); // returns parameter if not a number after $ 223 else 224 fields[ i ] = parameter; 225 } 226 227 return new Fields( fields ); 228 } 229 230 private Comparable parse( String parameter ) 231 { 232 try 233 { 234 return Integer.parseInt( parameter.substring( 1 ) ); 235 } 236 catch( NumberFormatException exception ) 237 { 238 return parameter; 239 } 240 } 241 242 @Override 243 public void prepare( FlowProcess flowProcess, OperationCall<Context> operationCall ) 244 { 245 if( operationCall.getContext() == null ) 246 operationCall.setContext( new Context() ); 247 248 Context context = operationCall.getContext(); 249 250 Fields argumentFields = operationCall.getArgumentFields(); 251 252 if( hasParameterNames() && hasParameterTypes() ) 253 { 254 context.parameterNames = getParameterNamesInternal(); 255 context.parameterFields = argumentFields.select( getParameterFields() ); // inherit argument types 256 context.parameterTypes = getParameterTypesInternal(); 257 } 258 else if( hasParameterTypes() ) 259 { 260 context.parameterNames = toNames( argumentFields ); 261 context.parameterFields = argumentFields.applyTypes( getParameterTypesInternal() ); 262 context.parameterTypes = getParameterTypesInternal(); 263 } 264 else 265 { 266 context.parameterNames = toNames( argumentFields ); 267 context.parameterFields = argumentFields; 268 context.parameterTypes = argumentFields.getTypesClasses(); 269 270 if( context.parameterTypes == null ) 271 throw new IllegalArgumentException( "field types may not be empty" ); 272 } 273 274 context.parameterCoercions = Coercions.coercibleArray( context.parameterFields ); 275 context.parameterArray = new Object[ context.parameterTypes.length ]; // re-use object array 276 context.scriptEvaluator = getEvaluator( getReturnType(), context.parameterNames, context.parameterTypes ); 277 context.intermediate = TupleViews.createNarrow( argumentFields.getPos( context.parameterFields ) ); 278 context.result = Tuple.size( 1 ); // re-use the output tuple 279 } 280 281 private String[] toNames( Fields argumentFields ) 282 { 283 String[] names = new String[ argumentFields.size() ]; 284 285 for( int i = 0; i < names.length; i++ ) 286 { 287 Comparable comparable = argumentFields.get( i ); 288 if( comparable instanceof String ) 289 names[ i ] = (String) comparable; 290 else 291 names[ i ] = "$" + comparable; 292 } 293 294 return names; 295 } 296 297 public Class getReturnType() 298 { 299 return returnType; 300 } 301 302 /** 303 * Performs the actual expression evaluation. 304 * 305 * @param context 306 * @param input of type TupleEntry 307 * @return Comparable 308 */ 309 protected Object evaluate( Context context, TupleEntry input ) 310 { 311 try 312 { 313 if( context.parameterTypes.length == 0 ) 314 return context.scriptEvaluator.evaluate( null ); 315 316 Tuple parameterTuple = TupleViews.reset( context.intermediate, input.getTuple() ); 317 Object[] arguments = Tuples.asArray( parameterTuple, context.parameterCoercions, context.parameterTypes, context.parameterArray ); 318 319 return context.scriptEvaluator.evaluate( arguments ); 320 } 321 catch( InvocationTargetException exception ) 322 { 323 throw new OperationException( "could not evaluate expression: " + block, exception.getTargetException() ); 324 } 325 } 326 327 @Override 328 public boolean equals( Object object ) 329 { 330 if( this == object ) 331 return true; 332 if( !( object instanceof ExpressionOperation ) ) 333 return false; 334 if( !super.equals( object ) ) 335 return false; 336 337 ExpressionOperation that = (ExpressionOperation) object; 338 339 if( block != null ? !block.equals( that.block ) : that.block != null ) 340 return false; 341 if( !Arrays.equals( parameterNames, that.parameterNames ) ) 342 return false; 343 if( !Arrays.equals( parameterTypes, that.parameterTypes ) ) 344 return false; 345 346 return true; 347 } 348 349 @Override 350 public int hashCode() 351 { 352 int result = super.hashCode(); 353 result = 31 * result + ( block != null ? block.hashCode() : 0 ); 354 result = 31 * result + ( parameterTypes != null ? Arrays.hashCode( parameterTypes ) : 0 ); 355 result = 31 * result + ( parameterNames != null ? Arrays.hashCode( parameterNames ) : 0 ); 356 return result; 357 } 358 359 public static class Context 360 { 361 private Class[] parameterTypes; 362 private ScriptEvaluator scriptEvaluator; 363 private Fields parameterFields; 364 private CoercibleType[] parameterCoercions; 365 private String[] parameterNames; 366 private Object[] parameterArray; 367 private Tuple intermediate; 368 protected Tuple result; 369 } 370 }