blob: 1ed22a267c3a525d472c1d94376e9ad68470af80 [file] [log] [blame]
/*
* 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.jruby;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.lang.reflect.Method;
import org.apache.pig.backend.executionengine.ExecException;
import org.apache.pig.data.DataBag;
import org.apache.pig.data.DataByteArray;
import org.apache.pig.data.Tuple;
import org.apache.pig.data.TupleFactory;
import org.apache.pig.impl.logicalLayer.schema.Schema;
import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.runtime.load.Library;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBignum;
import org.jruby.RubyBoolean;
import org.jruby.RubyFixnum;
import org.jruby.RubyFloat;
import org.jruby.RubyHash;
import org.jruby.RubyInteger;
import org.jruby.RubyNil;
import org.jruby.RubyString;
import org.jruby.RubySymbol;
import org.jruby.RubyEnumerator;
import org.jruby.runtime.builtin.IRubyObject;
import com.google.common.collect.Maps;
/**
* This class provides the ability to present to Ruby a library that was written in Java.
* In JRuby, there are two ways to present a library to ruby: one is to implement it in
* ruby as a module or class, and the other is to implement it in Java and then register
* it with the runtime. For the Pig datatypes we provide Ruby implementations for,
* it was easier to implement them in Java and provide a Ruby interface via the annotations
* that Jruby provides.
*
* Additionally, this class provides static object conversion functionality to and from Pig
* and JRuby.
*/
public class PigJrubyLibrary implements Library {
/**
* This method is called from JRuby to register any classes in
* the library.
*
* @param runtime the current Ruby runtime
* @param wrap ignored
* @throws IOException
*/
@Override
public void load(Ruby runtime, boolean wrap) throws IOException {
RubyDataBag.define(runtime);
RubyDataByteArray.define(runtime);
RubySchema.define(runtime);
}
private static final TupleFactory tupleFactory = TupleFactory.getInstance();
/**
* This method facilitates conversion from Ruby objects to Pig objects. This is
* a general class which detects the subclass and invokes the appropriate conversion
* routine. It will fail on an unsupported datatype.
*
* @param rbObject a Ruby object to convert
* @return the Pig analogue of the Ruby object
* @throws ExecException if rbObject is not of a known type that can be converted
*/
@SuppressWarnings("unchecked")
public static Object rubyToPig(IRubyObject rbObject) throws ExecException {
if (rbObject == null || rbObject instanceof RubyNil) {
return null;
} else if (rbObject instanceof RubyArray) {
return rubyToPig((RubyArray)rbObject);
} else if (rbObject instanceof RubyHash) {
return rubyToPig((RubyHash)rbObject);
} else if (rbObject instanceof RubyString) {
return rubyToPig((RubyString)rbObject);
} else if (rbObject instanceof RubyBignum) {
return rubyToPig((RubyBignum)rbObject);
} else if (rbObject instanceof RubyFixnum) {
return rubyToPig((RubyFixnum)rbObject);
} else if (rbObject instanceof RubyFloat) {
return rubyToPig((RubyFloat)rbObject);
} else if (rbObject instanceof RubyInteger) {
return rubyToPig((RubyInteger)rbObject);
} else if (rbObject instanceof RubyDataBag) {
return rubyToPig((RubyDataBag)rbObject);
} else if (rbObject instanceof RubyDataByteArray) {
return rubyToPig((RubyDataByteArray)rbObject);
} else if (rbObject instanceof RubySchema) {
return rubyToPig((RubySchema)rbObject);
} else if (rbObject instanceof RubyBoolean) {
return rubyToPig((RubyBoolean)rbObject);
} else {
throw new ExecException("Cannot cast into any pig supported type: " + rbObject.getClass().getName());
}
}
/**
* A type specific conversion routine.
*
* @param rbObject object to convert
* @return analogous Pig type
*/
public static Tuple rubyToPig(RubyArray rbObject) throws ExecException {
Tuple out = tupleFactory.newTuple(rbObject.size());
int i = 0;
for (IRubyObject arrayObj : rbObject.toJavaArray())
out.set(i++, rubyToPig(arrayObj));
return out;
}
//TODO need to convert to output a String
/**
* A type specific conversion routine.
*
* @param rbObject object to convert
* @return analogous Pig type
*/
@SuppressWarnings("unchecked")
public static Map<String, ?> rubyToPig(RubyHash rbObject) throws ExecException {
Map<String, Object> newMap = Maps.newHashMap();
for (Map.Entry<Object, Object> entry : (Set<Map.Entry<Object, Object>>)rbObject.entrySet()) {
Object key = entry.getKey();
if (!(key instanceof String || key instanceof RubyString || key instanceof RubySymbol))
throw new ExecException("Hash must have String or Symbol key. Was given: " + key.getClass().getName());
String keyStr = key.toString();
if (entry.getValue() instanceof IRubyObject) {
newMap.put(keyStr, rubyToPig((IRubyObject)entry.getValue()));
} else {
newMap.put(keyStr, entry.getValue());
}
}
return newMap;
}
/**
* A type specific conversion routine.
*
* @param rbObject object to convert
* @return analogous Pig type
*/
public static Boolean rubyToPig(RubyBoolean rbObject) {
return rbObject.isTrue();
}
/**
* A type specific conversion routine.
*
* @param rbObject object to convert
* @return analogous Pig type
*/
public static Schema rubyToPig(RubySchema rbObject) {
return rbObject.getInternalSchema();
}
/**
* A type specific conversion routine.
*
* @param rbObject object to convert
* @return analogous Pig type
*/
public static String rubyToPig(RubyString rbObject) {
return rbObject.toString();
}
/**
* A type specific conversion routine.
*
* @param rbObject object to convert
* @return analogous Pig type
*/
public static Long rubyToPig(RubyBignum rbObject) {
return rbObject.getLongValue();
}
/**
* A type specific conversion routine.
*
* @param rbObject object to convert
* @return analogous Pig type
*/
public static Long rubyToPig(RubyFixnum rbObject) {
return rbObject.getLongValue();
}
/**
* A type specific conversion routine.
*
* @param rbObject object to convert
* @return analogous Pig type
*/
public static Double rubyToPig(RubyFloat rbObject) {
return rbObject.getDoubleValue();
}
/**
* A type specific conversion routine.
*
* @param rbObject object to convert
* @return analogous Pig type
*/
public static Integer rubyToPig(RubyInteger rbObject) {
return (Integer)rbObject.toJava(Integer.class);
}
/**
* A type specific conversion routine.
*
* @param rbObject object to convert
* @return analogous Pig type
*/
public static DataBag rubyToPig(RubyDataBag rbObject) {
return rbObject.getBag();
}
/**
* A type specific conversion routine.
*
* @param rbObject object to convert
* @return analogous Pig type
*/
public static DataByteArray rubyToPig(RubyDataByteArray rbObject) {
return rbObject.getDBA();
}
/**
* A type specific conversion routine.
*
* @param rbObject object to convert
* @return analogous Pig type
*/
public static Object rubyToPig(RubyNil rbObject) {
return null;
}
/**
* This is the method which provides conversion from Pig to Ruby. In this case, an
* instance of the Ruby runtime is necessary. This method provides the general detection
* of the type and then calls the more specific conversion methods.
*
* @param ruby the Ruby runtime to create objects in
* @param object the Pig object to convert to Ruby
* @return Ruby analogue of object
* @throws ExecException object is not a convertible Pig type
*/
@SuppressWarnings("unchecked")
public static IRubyObject pigToRuby(Ruby ruby, Object object) throws ExecException {
if (object == null) {
return ruby.getNil();
} else if (object instanceof Tuple) {
return pigToRuby(ruby, (Tuple)object);
} else if (object instanceof DataBag) {
return pigToRuby(ruby, (DataBag)object);
} else if (object instanceof Map<?, ?>) {
return pigToRuby(ruby, (Map<String, ?>)object);
} else if (object instanceof DataByteArray) {
return pigToRuby(ruby, (DataByteArray)object);
} else if (object instanceof Schema) {
return pigToRuby(ruby, ((Schema)object));
} else if (object instanceof String) {
return pigToRuby(ruby, (String)object);
} else if (object instanceof Integer) {
return pigToRuby(ruby, (Integer)object);
} else if (object instanceof Long) {
return pigToRuby(ruby, (Long)object);
} else if (object instanceof Float) {
return pigToRuby(ruby, (Float)object);
} else if (object instanceof Double) {
return pigToRuby(ruby, (Double)object);
} else if (object instanceof Boolean) {
return pigToRuby(ruby, (Boolean)object);
} else {
throw new ExecException("Object of unknown type " + object.getClass().getName() + " passed to pigToRuby");
}
}
/**
* A type specific conversion routine.
*
* @param ruby the Ruby runtime to create objects in
* @param object object to convert
* @return analogous Ruby type
* @throws ExecException object contained an object that could not convert
*/
public static RubyArray pigToRuby(Ruby ruby, Tuple object) throws ExecException{
RubyArray rubyArray = ruby.newArray();
for (Object o : object.getAll())
rubyArray.add(pigToRuby(ruby, o));
return rubyArray;
}
/**
* A type specific conversion routine.
*
* @param ruby the Ruby runtime to create objects in
* @param object object to convert
* @return analogous Ruby type
*/
public static RubyBoolean pigToRuby(Ruby ruby, Boolean object) {
return RubyBoolean.newBoolean(ruby, object.booleanValue());
}
/**
* A type specific conversion routine.
*
* @param ruby the Ruby runtime to create objects in
* @param object object to convert
* @return analogous Ruby type
*/
public static RubyDataBag pigToRuby(Ruby ruby, DataBag object) {
return new RubyDataBag(ruby, ruby.getClass("DataBag"), object);
}
/**
* A type specific conversion routine.
*
* @param ruby the Ruby runtime to create objects in
* @param object object to convert
* @return analogous Ruby type
*/
public static RubySchema pigToRuby(Ruby ruby, Schema object) {
return new RubySchema(ruby, ruby.getClass("Schema"), object);
}
/**
* A type specific conversion routine for Pig Maps. This only deals with maps
* with String keys, which is all that Pig supports.
*
* @param ruby the Ruby runtime to create objects in
* @param object map to convert. In Pig, only maps with String keys are
* supported
* @return analogous Ruby type
* @throws ExecException object contains a key that can't be convert to a Ruby type
*/
public static <T> RubyHash pigToRuby(Ruby ruby, Map<T, ?> object) throws ExecException {
RubyHash newMap = RubyHash.newHash(ruby);
boolean checkType = false;
for (Map.Entry<T, ?> entry : object.entrySet()) {
T key = entry.getKey();
if (!checkType) {
if (!(key instanceof String))
throw new ExecException("pigToRuby only supports converting Maps with String keys");
checkType = true;
}
newMap.put(key, pigToRuby(ruby, entry.getValue()));
}
return newMap;
}
/**
* A type specific conversion routine.
*
* @param ruby the Ruby runtime to create objects in
* @param object object to convert
* @return analogous Ruby type
*/
public static RubyDataByteArray pigToRuby(Ruby ruby, DataByteArray object) {
return new RubyDataByteArray(ruby, ruby.getClass("DataByteArray"), object);
}
/**
* A type specific conversion routine.
*
* @param ruby the Ruby runtime to create objects in
* @param object object to convert
* @return analogous Ruby type
*/
public static RubyString pigToRuby(Ruby ruby, String object) {
return ruby.newString(object.toString());
}
/**
* A type specific conversion routine.
*
* @param ruby the Ruby runtime to create objects in
* @param object object to convert
* @return analogous Ruby type
*/
public static RubyFixnum pigToRuby(Ruby ruby, Integer object) {
return RubyFixnum.newFixnum(ruby, object.longValue());
}
/**
* A type specific conversion routine.
*
* @param ruby the Ruby runtime to create objects in
* @param object object to convert
* @return analogous Ruby type
*/
public static RubyFixnum pigToRuby(Ruby ruby, Long object) {
return RubyFixnum.newFixnum(ruby, object);
}
/**
* A type specific conversion routine.
*
* @param ruby the Ruby runtime to create objects in
* @param object object to convert
* @return analogous Ruby type
*/
public static RubyFloat pigToRuby(Ruby ruby, Float object) {
return RubyFloat.newFloat(ruby, object.doubleValue());
}
/**
* A type specific conversion routine.
*
* @param ruby the Ruby runtime to create objects in
* @param object object to convert
* @return analogous Ruby type
*/
public static RubyFloat pigToRuby(Ruby ruby, Double object) {
return RubyFloat.newFloat(ruby, object);
}
/**
* A type specific conversion routine.
*
* @param ruby the Ruby runtime to create objects in
* @param object object to convert
* @return analogous Ruby type
*/
private static final Method enumeratorizeMethod;
static {
try {
enumeratorizeMethod = RubyEnumerator.class.getDeclaredMethod("enumeratorize", Ruby.class, IRubyObject.class, String.class);
enumeratorizeMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException("Enumeratorize method not found", e);
}
}
/**
* This is a hack to get around the fact that in JRuby 1.6.7, the enumeratorize method
* isn't public. In 1.7.0, it will be made public and this can be gotten rid of, but
* until then the Jruby API doesn't provide an easy way (or even a difficult way, really)
* to provide this functionality; thus, it felt much cleaner to use reflection to make
* public a method that will soon be public anyway instead of doing something much hairier.
*
* @param runtime the Ruby runtime to create objects in
* @param obj the Enumerable object to wrap
* @param name the name of the method that still needs a block (ie each or flatten)
* @return enumerator Ruby object wrapping the given Enumerable
*/
public static IRubyObject enumeratorize(Ruby runtime, IRubyObject obj, String name) {
try {
return (IRubyObject)enumeratorizeMethod.invoke(null, runtime, obj, name);
} catch (Exception e) {
throw new RuntimeException("Unable to properly enumeratorize", e);
}
}
}