/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.pig.scripting.groovy; import groovy.lang.Tuple; import groovy.util.ResourceException; import groovy.util.ScriptException; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.math.BigInteger; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.pig.FuncSpec; import org.apache.pig.PigServer; import org.apache.pig.builtin.OutputSchema; import org.apache.pig.data.DataBag; import org.apache.pig.data.DataByteArray; import org.apache.pig.impl.PigContext; import org.apache.pig.impl.logicalLayer.schema.Schema; import org.apache.pig.impl.util.ObjectSerializer; import org.apache.pig.impl.util.Utils; import org.apache.pig.scripting.ScriptEngine; import org.apache.pig.scripting.groovy.GroovyAlgebraicEvalFunc.BigDecimalGroovyAlgebraicEvalFunc; import org.apache.pig.scripting.groovy.GroovyAlgebraicEvalFunc.BigIntegerGroovyAlgebraicEvalFunc; import org.apache.pig.scripting.groovy.GroovyAlgebraicEvalFunc.BooleanGroovyAlgebraicEvalFunc; import org.apache.pig.scripting.groovy.GroovyAlgebraicEvalFunc.ChararrayGroovyAlgebraicEvalFunc; import org.apache.pig.scripting.groovy.GroovyAlgebraicEvalFunc.DataBagGroovyAlgebraicEvalFunc; import org.apache.pig.scripting.groovy.GroovyAlgebraicEvalFunc.DataByteArrayGroovyAlgebraicEvalFunc; import org.apache.pig.scripting.groovy.GroovyAlgebraicEvalFunc.DateTimeGroovyAlgebraicEvalFunc; import org.apache.pig.scripting.groovy.GroovyAlgebraicEvalFunc.DoubleGroovyAlgebraicEvalFunc; import org.apache.pig.scripting.groovy.GroovyAlgebraicEvalFunc.FloatGroovyAlgebraicEvalFunc; import org.apache.pig.scripting.groovy.GroovyAlgebraicEvalFunc.IntegerGroovyAlgebraicEvalFunc; import org.apache.pig.scripting.groovy.GroovyAlgebraicEvalFunc.LongGroovyAlgebraicEvalFunc; import org.apache.pig.scripting.groovy.GroovyAlgebraicEvalFunc.MapGroovyAlgebraicEvalFunc; import org.apache.pig.scripting.groovy.GroovyAlgebraicEvalFunc.TupleGroovyAlgebraicEvalFunc; import org.apache.pig.tools.pigstats.PigStats; public class GroovyScriptEngine extends ScriptEngine { private static final Log LOG = LogFactory.getLog(GroovyScriptEngine.class); private static groovy.util.GroovyScriptEngine gse; private static boolean isInitialized = false; static { try { gse = new groovy.util.GroovyScriptEngine(""); } catch (IOException ioe) { throw new RuntimeException(ioe); } } @Override protected Map<String, List<PigStats>> main(PigContext context, String scriptFile) throws IOException { PigServer pigServer = new PigServer(context, false); // // Register dependencies // String groovyJar = getJarPath(groovy.util.GroovyScriptEngine.class); if (null != groovyJar) { pigServer.registerJar(groovyJar); } // // Register UDFs // registerFunctions(scriptFile, null, context); try { // // Load the script // Class c = gse.loadScriptByName(scriptFile); // // Extract the main method // Method main = c.getMethod("main", String[].class); if (null == main || !Modifier.isStatic(main.getModifiers()) || !Modifier.isPublic(main.getModifiers()) || !Void.TYPE.equals(main.getReturnType())) { throw new IOException("No method 'public static void main(String[] args)' was found."); } // // Invoke the main method // Object[] args = new Object[1]; String[] argv = (String[])ObjectSerializer.deserialize(context.getProperties().getProperty(PigContext.PIG_CMD_ARGS_REMAINDERS)); args[0] = argv; main.invoke(null, args); } catch (Exception e) { throw new IOException(e); } return getPigStatsMap(); } @Override public void registerFunctions(String path, String namespace, PigContext pigContext) throws IOException { if (!isInitialized) { pigContext.scriptJars.add(getJarPath(groovy.util.GroovyScriptEngine.class)); isInitialized = true; } try { // // Read file // Class c = gse.loadScriptByName(path); // // Keep track of initial/intermed/final methods of Albegraic UDFs // Map<String, Method[]> algebraicMethods = new HashMap<String, Method[]>(); // // Keep track of accumulate/getValue/cleanup methods of Accumulator UDFs // Map<String, Method[]> accumulatorMethods = new HashMap<String, Method[]>(); // // Loop over the methods // Method[] methods = c.getMethods(); for (Method method : methods) { Annotation[] annotations = method.getAnnotations(); boolean isAccumulator = false; if (annotations.length > 0) { Schema schema = null; String schemaFunction = null; for (Annotation annotation : annotations) { if (annotation.annotationType().equals(OutputSchema.class)) { schema = Utils.getSchemaFromString(((OutputSchema) annotation).value()); } else if (annotation.annotationType().equals(OutputSchemaFunction.class)) { schemaFunction = ((OutputSchemaFunction) annotation).value(); } else if (isAlgebraic(annotation)) { String algebraic = null; int idx = 0; if (annotation.annotationType().equals(AlgebraicInitial.class)) { // // Check that method accepts a single Tuple as parameter. // Class<?>[] params = method.getParameterTypes(); if (1 != params.length || !Tuple.class.equals(params[0])) { throw new IOException(path + ": methods annotated with @AlgebraicInitial MUST take a single groovy.lang.Tuple as parameter."); } if (!method.getReturnType().equals(Tuple.class) && !method.getReturnType().equals(Object[].class)) { throw new IOException(path + ":" + method.getName() + " Algebraic UDF Initial method MUST return type groovy.lang.Tuple or Object[]."); } algebraic = ((AlgebraicInitial) annotation).value(); idx = 0; } else if (annotation.annotationType().equals(AlgebraicIntermed.class)) { // // Check that method accepts a single Tuple as parameter. // Class<?>[] params = method.getParameterTypes(); if (1 != params.length || !Tuple.class.equals(params[0])) { throw new IOException(path + ": methods annotated with @AlgebraicIntermed MUST take a single groovy.lang.Tuple as parameter."); } if (!method.getReturnType().equals(Tuple.class) && !method.getReturnType().equals(Object[].class)) { throw new IOException(path + ":" + method.getName() + " Algebraic UDF Intermed method MUST return type groovy.lang.Tuple or Object[]."); } algebraic = ((AlgebraicIntermed) annotation).value(); idx = 1; } else { // // Check that method accepts a single Tuple as parameter. // Class<?>[] params = method.getParameterTypes(); if (1 != params.length || !Tuple.class.equals(params[0])) { throw new IOException(path + ": methods annotated with @AlgebraicFinal MUST take a single groovy.lang.Tuple as parameter."); } algebraic = ((AlgebraicFinal) annotation).value(); idx = 2; } Method[] algmethods = algebraicMethods.get(algebraic); if (null == algmethods) { algmethods = new Method[3]; algebraicMethods.put(algebraic, algmethods); } if (null != algmethods[idx]) { throw new IOException(path + ": Algebraic UDF '" + algebraic + "' already has an " + annotation.annotationType().getSimpleName() + " method defined ('" + algmethods[idx] + "')"); } algmethods[idx] = method; } else if (isAccumulator(annotation)) { String accumulator = null; int idx = 0; if (annotation.annotationType().equals(AccumulatorAccumulate.class)) { // // Check that method accepts a single Tuple as parameter. // Class<?>[] params = method.getParameterTypes(); if (1 != params.length || !Tuple.class.equals(params[0])) { throw new IOException(path + ": methods annotated with @AccumulatorAccumulate MUST take a single groovy.lang.Tuple as parameter."); } accumulator = ((AccumulatorAccumulate) annotation).value(); idx = 0; } else if (annotation.annotationType().equals(AccumulatorGetValue.class)) { // // Check that method does not accept any parameters. // Class<?>[] params = method.getParameterTypes(); if (0 != params.length) { throw new IOException(path + ": methods annotated with @AccumulatorGetValue take no parameters."); } accumulator = ((AccumulatorGetValue) annotation).value(); isAccumulator = true; idx = 1; } else if (annotation.annotationType().equals(AccumulatorCleanup.class)) { // // Check that method does not accept any parameters. // Class<?>[] params = method.getParameterTypes(); if (0 != params.length) { throw new IOException(path + ": methods annotated with @AccumulatorCleanup take no parameters and return void."); } accumulator = ((AccumulatorCleanup) annotation).value(); idx = 2; } Method[] accumethods = accumulatorMethods.get(accumulator); if (null == accumethods) { accumethods = new Method[3]; accumulatorMethods.put(accumulator, accumethods); } if (null != accumethods[idx]) { throw new IOException(path + ": Accumulator UDF '" + accumulator + "' already has an " + annotation.annotationType().getSimpleName() + " method defined ('" + accumethods[idx] + "')"); } accumethods[idx] = method; } } // // Only register functions which have an output schema declared // if (null == schema && null == schemaFunction) { LOG.info(path + ": Only methods annotated with @OutputSchema or @OutputSchemaFunction (but not with @AccumulatorGetValue are exposed to Pig, skipping method '" + method.getName() + "'"); continue; } // // Only one of OutputSchema / OutputSchemaFunction can be defined // if (null != schema && null != schemaFunction) { LOG.info("Annotation @OutputSchemaFunction has precedence over @OutputSchema for method '" + method.getName() + "'"); } // // Register methods annotated with 'OutputSchema' or // 'OutputSchemaFunction, unless they are accumulators' getValue // methods // if (!isAccumulator) { namespace = (namespace == null) ? "" : namespace; FuncSpec spec = new FuncSpec(GroovyEvalFuncObject.class.getCanonicalName() + "('" + path + "','" + namespace + "','" + method.getName() + "')"); pigContext.registerFunction(("".equals(namespace) ? "" : (namespace + NAMESPACE_SEPARATOR)) + method.getName(), spec); LOG.info(path + ": Register Groovy UDF: " + ("".equals(namespace) ? "" : (namespace + NAMESPACE_SEPARATOR)) + method.getName()); } } } // // Now register algebraic methods // for (String algebraic : algebraicMethods.keySet()) { Method[] algmethods = algebraicMethods.get(algebraic); if (null == algmethods[0]) { throw new IOException(path + ": Algebratic UDF '" + algebraic + "' does not have an Initial method defined."); } else if (null == algmethods[1]) { throw new IOException(path + ": Algebratic UDF '" + algebraic + "' does not have an Intermed method defined."); } else if (null == algmethods[2]) { throw new IOException(path + ": Algebratic UDF '" + algebraic + "' does not have a Final method defined."); } // // Retrieve schema of 'Final' method // String className = null; Class<?> returnType = algmethods[2].getReturnType(); if (returnType.equals(Tuple.class) || returnType.equals(Object[].class) || returnType.equals(org.apache.pig.data.Tuple.class)) { className = TupleGroovyAlgebraicEvalFunc.class.getName(); } else if (returnType.equals(List.class) || returnType.equals(DataBag.class)) { className = DataBagGroovyAlgebraicEvalFunc.class.getName(); } else if (returnType.equals(org.joda.time.DateTime.class)) { className = DateTimeGroovyAlgebraicEvalFunc.class.getName(); } else if (returnType.equals(Boolean.class) || returnType.equals(boolean.class)) { className = BooleanGroovyAlgebraicEvalFunc.class.getName(); } else if (returnType.equals(byte[].class) || returnType.equals(DataByteArray.class)) { className = DataByteArrayGroovyAlgebraicEvalFunc.class.getName(); } else if (returnType.equals(String.class)) { className = ChararrayGroovyAlgebraicEvalFunc.class.getName(); } else if (returnType.equals(Double.class) || returnType.equals(double.class) || returnType.equals(BigDecimal.class)) { className = DoubleGroovyAlgebraicEvalFunc.class.getName(); } else if (returnType.equals(Float.class) || returnType.equals(float.class)) { className = FloatGroovyAlgebraicEvalFunc.class.getName(); } else if (returnType.equals(Byte.class) || returnType.equals(byte.class) || returnType.equals(Short.class) || returnType.equals(short.class) || returnType.equals(Integer.class) || returnType.equals(int.class)) { className = IntegerGroovyAlgebraicEvalFunc.class.getName(); } else if (returnType.equals(Long.class) || returnType.equals(long.class) || returnType.equals(BigInteger.class)) { className = LongGroovyAlgebraicEvalFunc.class.getName(); } else if (returnType.equals(Map.class)) { className = MapGroovyAlgebraicEvalFunc.class.getName(); } else if (returnType.equals(BigDecimal.class)) { className = BigDecimalGroovyAlgebraicEvalFunc.class.getName(); } else if (returnType.equals(BigInteger.class)) { className = BigIntegerGroovyAlgebraicEvalFunc.class.getName(); } else { throw new RuntimeException(path + ": Unknown return type for Algebraic UDF '" + algebraic + "'"); } FuncSpec spec = new FuncSpec(className + "('" + path + "','" + namespace + "','" + algebraic + "','" + algmethods[0].getName() + "','" + algmethods[1].getName() + "','" + algmethods[2].getName() + "')"); pigContext.registerFunction(("".equals(namespace) ? "" : (namespace + NAMESPACE_SEPARATOR)) + algebraic, spec); LOG.info("Register Groovy Algebraic UDF: " + ("".equals(namespace) ? "" : (namespace + NAMESPACE_SEPARATOR)) + algebraic); } // // Now register Accumulator UDFs // for (String accumulator : accumulatorMethods.keySet()) { Method[] accumethods = accumulatorMethods.get(accumulator); if (null == accumethods[0]) { throw new IOException(path + ": Accumulator UDF '" + accumulator + "' does not have an Accumulate method defined."); } else if (null == accumethods[1]) { throw new IOException(path + ": Accumulator UDF '" + accumulator + "' does not have a GetValue method defined."); } else if (null == accumethods[2]) { throw new IOException(path + ": Accumulator UDF '" + accumulator + "' does not have a Cleanup method defined."); } FuncSpec spec = new FuncSpec(GroovyAccumulatorEvalFunc.class.getName() + "('" + path + "','" + namespace + "','" + accumulator + "','" + accumethods[0].getName() + "','" + accumethods[1].getName() + "','" + accumethods[2].getName() + "')"); pigContext.registerFunction(("".equals(namespace) ? "" : (namespace + NAMESPACE_SEPARATOR)) + accumulator, spec); LOG.info("Register Groovy Accumulator UDF: " + ("".equals(namespace) ? "" : (namespace + NAMESPACE_SEPARATOR)) + accumulator); } } catch (ResourceException re) { throw new IOException(re); } catch (ScriptException se) { throw new IOException(se); } } @Override protected Map<String, Object> getParamsFromVariables() throws IOException { return null; } @Override protected String getScriptingLang() { return "groovy"; } protected static groovy.util.GroovyScriptEngine getEngine() { return gse; } private static boolean isAlgebraic(Annotation annotation) { return annotation.annotationType().equals(AlgebraicInitial.class) || annotation.annotationType().equals(AlgebraicIntermed.class) || annotation.annotationType().equals(AlgebraicFinal.class); } private static boolean isAccumulator(Annotation annotation) { return annotation.annotationType().equals(AccumulatorAccumulate.class) || annotation.annotationType().equals(AccumulatorGetValue.class) || annotation.annotationType().equals(AccumulatorCleanup.class); } }