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.management.annotation.Property;
029    import cascading.management.annotation.PropertyDescription;
030    import cascading.management.annotation.Visibility;
031    import cascading.operation.BaseOperation;
032    import cascading.operation.OperationCall;
033    import cascading.operation.OperationException;
034    import cascading.tuple.Fields;
035    import cascading.tuple.Tuple;
036    import cascading.tuple.TupleEntry;
037    import cascading.tuple.Tuples;
038    import cascading.tuple.coerce.Coercions;
039    import cascading.tuple.type.CoercibleType;
040    import cascading.tuple.util.TupleViews;
041    import cascading.util.Util;
042    import org.codehaus.commons.compiler.CompileException;
043    import org.codehaus.janino.ScriptEvaluator;
044    
045    /**
046     *
047     */
048    public 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( hasParameterNames() && 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      protected ScriptEvaluator getEvaluator( Class returnType, String[] parameterNames, Class[] parameterTypes )
211        {
212        try
213          {
214          return new ScriptEvaluator( block, returnType, parameterNames, parameterTypes );
215          }
216        catch( CompileException exception )
217          {
218          throw new OperationException( "could not compile script: " + block, exception );
219          }
220        }
221    
222      private Fields makeFields( String[] parameters )
223        {
224        Comparable[] fields = new Comparable[ parameters.length ];
225    
226        for( int i = 0; i < parameters.length; i++ )
227          {
228          String parameter = parameters[ i ];
229    
230          if( parameter.startsWith( "$" ) )
231            fields[ i ] = parse( parameter ); // returns parameter if not a number after $
232          else
233            fields[ i ] = parameter;
234          }
235    
236        return new Fields( fields );
237        }
238    
239      private Comparable parse( String parameter )
240        {
241        try
242          {
243          return Integer.parseInt( parameter.substring( 1 ) );
244          }
245        catch( NumberFormatException exception )
246          {
247          return parameter;
248          }
249        }
250    
251      @Override
252      public void prepare( FlowProcess flowProcess, OperationCall<Context> operationCall )
253        {
254        if( operationCall.getContext() == null )
255          operationCall.setContext( new Context() );
256    
257        Context context = operationCall.getContext();
258    
259        Fields argumentFields = operationCall.getArgumentFields();
260    
261        if( hasParameterNames() && hasParameterTypes() )
262          {
263          context.parameterNames = getParameterNamesInternal();
264          context.parameterFields = argumentFields.select( getParameterFields() ); // inherit argument types
265          context.parameterTypes = getParameterTypesInternal();
266          }
267        else if( hasParameterTypes() )
268          {
269          context.parameterNames = toNames( argumentFields );
270          context.parameterFields = argumentFields.applyTypes( getParameterTypesInternal() );
271          context.parameterTypes = getParameterTypesInternal();
272          }
273        else
274          {
275          context.parameterNames = toNames( argumentFields );
276          context.parameterFields = argumentFields;
277          context.parameterTypes = argumentFields.getTypesClasses();
278    
279          if( context.parameterTypes == null )
280            throw new IllegalArgumentException( "field types may not be empty" );
281          }
282    
283        context.parameterCoercions = Coercions.coercibleArray( context.parameterFields );
284        context.parameterArray = new Object[ context.parameterTypes.length ]; // re-use object array
285        context.scriptEvaluator = getEvaluator( getReturnType(), context.parameterNames, context.parameterTypes );
286        context.intermediate = TupleViews.createNarrow( argumentFields.getPos( context.parameterFields ) );
287        context.result = Tuple.size( 1 ); // re-use the output tuple
288        }
289    
290      private String[] toNames( Fields argumentFields )
291        {
292        String[] names = new String[ argumentFields.size() ];
293    
294        for( int i = 0; i < names.length; i++ )
295          {
296          Comparable comparable = argumentFields.get( i );
297          if( comparable instanceof String )
298            names[ i ] = (String) comparable;
299          else
300            names[ i ] = "$" + comparable;
301          }
302    
303        return names;
304        }
305    
306      public Class getReturnType()
307        {
308        return returnType;
309        }
310    
311      /**
312       * Performs the actual expression evaluation.
313       *
314       * @param context
315       * @param input   of type TupleEntry
316       * @return Comparable
317       */
318      protected Object evaluate( Context context, TupleEntry input )
319        {
320        try
321          {
322          if( context.parameterTypes.length == 0 )
323            return context.scriptEvaluator.evaluate( null );
324    
325          Tuple parameterTuple = TupleViews.reset( context.intermediate, input.getTuple() );
326          Object[] arguments = Tuples.asArray( parameterTuple, context.parameterCoercions, context.parameterTypes, context.parameterArray );
327    
328          return context.scriptEvaluator.evaluate( arguments );
329          }
330        catch( InvocationTargetException exception )
331          {
332          throw new OperationException( "could not evaluate expression: " + block, exception.getTargetException() );
333          }
334        }
335    
336      @Override
337      public boolean equals( Object object )
338        {
339        if( this == object )
340          return true;
341        if( !( object instanceof ExpressionOperation ) )
342          return false;
343        if( !super.equals( object ) )
344          return false;
345    
346        ExpressionOperation that = (ExpressionOperation) object;
347    
348        if( block != null ? !block.equals( that.block ) : that.block != null )
349          return false;
350        if( !Arrays.equals( parameterNames, that.parameterNames ) )
351          return false;
352        if( !Arrays.equals( parameterTypes, that.parameterTypes ) )
353          return false;
354    
355        return true;
356        }
357    
358      @Override
359      public int hashCode()
360        {
361        int result = super.hashCode();
362        result = 31 * result + ( block != null ? block.hashCode() : 0 );
363        result = 31 * result + ( parameterTypes != null ? Arrays.hashCode( parameterTypes ) : 0 );
364        result = 31 * result + ( parameterNames != null ? Arrays.hashCode( parameterNames ) : 0 );
365        return result;
366        }
367    
368      public static class Context
369        {
370        private Class[] parameterTypes;
371        private ScriptEvaluator scriptEvaluator;
372        private Fields parameterFields;
373        private CoercibleType[] parameterCoercions;
374        private String[] parameterNames;
375        private Object[] parameterArray;
376        private Tuple intermediate;
377        protected Tuple result;
378        }
379      }