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      }