diff --git a/logstash-core/src/main/java/org/logstash/Valuefier.java b/logstash-core/src/main/java/org/logstash/Valuefier.java index 384817bab..78ca3cef1 100644 --- a/logstash-core/src/main/java/org/logstash/Valuefier.java +++ b/logstash-core/src/main/java/org/logstash/Valuefier.java @@ -1,9 +1,9 @@ package org.logstash; -import java.io.Serializable; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.joda.time.DateTime; import org.jruby.RubyArray; import org.jruby.RubyBoolean; @@ -22,85 +22,132 @@ import org.logstash.bivalues.BiValues; import org.logstash.ext.JrubyTimestampExtLibrary; public final class Valuefier { - private static final String PROXY_ERR_TEMPLATE = "Missing Valuefier handling for full class name=%s, simple name=%s, wrapped object=%s"; - private static final String ERR_TEMPLATE = "Missing Valuefier handling for full class name=%s, simple name=%s"; - private Valuefier(){} + private static final Valuefier.Converter IDENTITY = input -> input; - private static Object convertJavaProxy(final JavaProxy jp) { - final Object obj = JavaUtil.unwrapJavaObject(jp); - if (obj instanceof IRubyObject[]) { - return ConvertedList.newFromRubyArray((IRubyObject[]) obj); - } - if (obj instanceof List) { - return ConvertedList.newFromList((Collection) obj); - } - try { - return BiValues.newBiValue(jp); - } catch (IllegalArgumentException e) { - final Class cls = obj.getClass(); - throw new IllegalArgumentException(String.format(PROXY_ERR_TEMPLATE, cls.getName(), cls.getSimpleName(), obj.getClass().getName()), e); - } - } + private static final Valuefier.Converter FLOAT_CONVERTER = + input -> RubyUtil.RUBY.newFloat(((Number) input).doubleValue()); - private static Object convertNonCollection(Object o) { - try { - return BiValues.newBiValue(o); - } catch (IllegalArgumentException e) { - final Class cls = o.getClass(); - throw new IllegalArgumentException(String.format(ERR_TEMPLATE, cls.getName(), cls.getSimpleName()), e); - } + private static final Valuefier.Converter JAVAPROXY_CONVERTER = + input -> { + final Object obj = JavaUtil.unwrapJavaObject((JavaProxy) input); + if (obj instanceof IRubyObject[]) { + return ConvertedList.newFromRubyArray((IRubyObject[]) obj); + } + if (obj instanceof List) { + return ConvertedList.newFromList((Collection) obj); + } + try { + return BiValues.newBiValue(input); + } catch (IllegalArgumentException e) { + final Class cls = obj.getClass(); + throw new IllegalArgumentException(String.format( + "Missing Valuefier handling for full class name=%s, simple name=%s, wrapped object=%s", + cls.getName(), cls.getSimpleName(), obj.getClass().getName() + ), e); + } + }; + + private static final Valuefier.Converter BIVALUES_CONVERTER = BiValues::newBiValue; + + private static final Map, Valuefier.Converter> CONVERTER_MAP = initConverters(); + + private Valuefier() { } public static Object convert(final Object o) { - if (o instanceof RubyString || o instanceof RubyFloat - || o instanceof JrubyTimestampExtLibrary.RubyTimestamp - || o instanceof ConvertedMap || o instanceof ConvertedList - || o instanceof BiValue || o instanceof RubyBoolean) { - return o; + if (o == null) { + return BiValues.NULL_BI_VALUE; } - if (o instanceof String) { - return RubyUtil.RUBY.newString((String) o); + final Class cls = o.getClass(); + final Valuefier.Converter converter = CONVERTER_MAP.get(cls); + if (converter != null) { + return converter.convert(o); } - if (o instanceof Float || o instanceof Double) { - return RubyUtil.RUBY.newFloat(((Number) o).doubleValue()); + return fallbackConvert(o, cls); + } + + /** + * Fallback for types not covered by {@link Valuefier#convert(Object)} as a result of no + * {@link Valuefier.Converter} having been cached for the given class. Uses the fact that + * the only subclasses of the keys in {@link Valuefier#CONVERTER_MAP} as set up by + * {@link Valuefier#initConverters()} can be converted here and hence find the appropriate + * super class for unknown types by checking each entry in {@link Valuefier#CONVERTER_MAP} for + * being a supertype of the given class. If this fails {@link Valuefier#BIVALUES_CONVERTER} + * will be cached and used. + * @param o Object to convert + * @param cls Class of given object {@code o} + * @return Conversion result equivalent to what {@link Valuefier#convert(Object)} would return + */ + private static Object fallbackConvert(final Object o, final Class cls) { + for (final Map.Entry, Valuefier.Converter> entry : CONVERTER_MAP.entrySet()) { + if (entry.getKey().isAssignableFrom(cls)) { + final Valuefier.Converter found = entry.getValue(); + CONVERTER_MAP.put(cls, found); + return found.convert(o); + } } - if (o instanceof Boolean) { - return RubyUtil.RUBY.newBoolean((Boolean) o); - } - if (o instanceof Timestamp) { - return JrubyTimestampExtLibrary.RubyTimestamp.newRubyTimestamp( - RubyUtil.RUBY, (Timestamp) o - ); - } - if (o instanceof RubyTime) { - return JrubyTimestampExtLibrary.RubyTimestamp.newRubyTimestamp( - RubyUtil.RUBY, new Timestamp(((RubyTime) o).getDateTime()) - ); - } - if (o instanceof DateTime) { - return JrubyTimestampExtLibrary.RubyTimestamp.newRubyTimestamp( - RubyUtil.RUBY, new Timestamp((DateTime) o) - ); - } - if (o instanceof RubyHash) { - return ConvertedMap.newFromRubyHash((RubyHash) o); - } - if (o instanceof RubyArray) { - return ConvertedList.newFromRubyArray((RubyArray) o); - } - if (o instanceof Map) { - return ConvertedMap.newFromMap((Map) o); - } - if (o instanceof List) { - return ConvertedList.newFromList((List) o); - } - if (o instanceof MapJavaProxy){ - return ConvertedMap.newFromMap((Map)((MapJavaProxy) o).getObject()); - } - if (o instanceof ArrayJavaProxy || o instanceof ConcreteJavaProxy){ - return convertJavaProxy((JavaProxy) o); - } - return o == null ? BiValues.NULL_BI_VALUE : convertNonCollection(o); + CONVERTER_MAP.put(cls, BIVALUES_CONVERTER); + return BIVALUES_CONVERTER.convert(o); + } + + private static Map, Valuefier.Converter> initConverters() { + final Map, Valuefier.Converter> converters = + new ConcurrentHashMap<>(50, 0.2F, 1); + converters.put(RubyString.class, IDENTITY); + converters.put(JrubyTimestampExtLibrary.RubyTimestamp.class, IDENTITY); + converters.put(RubyFloat.class, IDENTITY); + converters.put(ConvertedMap.class, IDENTITY); + converters.put(ConvertedList.class, IDENTITY); + converters.put(RubyBoolean.class, IDENTITY); + converters.put(BiValue.class, IDENTITY); + converters.put(String.class, input -> RubyUtil.RUBY.newString((String) input)); + converters.put(Float.class, FLOAT_CONVERTER); + converters.put(Double.class, FLOAT_CONVERTER); + converters.put(Boolean.class, input -> RubyUtil.RUBY.newBoolean((Boolean) input)); + converters.put( + Timestamp.class, + input -> JrubyTimestampExtLibrary.RubyTimestamp.newRubyTimestamp( + RubyUtil.RUBY, (Timestamp) input + ) + ); + converters.put( + RubyTime.class, input -> JrubyTimestampExtLibrary.RubyTimestamp.newRubyTimestamp( + RubyUtil.RUBY, new Timestamp(((RubyTime) input).getDateTime()) + ) + ); + converters.put( + DateTime.class, input -> JrubyTimestampExtLibrary.RubyTimestamp.newRubyTimestamp( + RubyUtil.RUBY, new Timestamp((DateTime) input) + ) + ); + converters.put(RubyHash.class, input -> ConvertedMap.newFromRubyHash((RubyHash) input)); + converters.put(Map.class, input -> ConvertedMap.newFromMap((Map) input)); + converters.put(List.class, input -> ConvertedList.newFromList((List) input)); + converters.put(ArrayJavaProxy.class, JAVAPROXY_CONVERTER); + converters.put(ConcreteJavaProxy.class, JAVAPROXY_CONVERTER); + converters.put( + MapJavaProxy.class, + input -> ConvertedMap.newFromMap((Map) ((MapJavaProxy) input).getObject()) + ); + converters.put( + RubyArray.class, input -> ConvertedList.newFromRubyArray((RubyArray) input) + ); + return converters; + } + + /** + * Converter from either a Java or a Ruby type to a type that both {@link Javafier} and + * {@link Rubyfier} are able to convert back their respective types efficiently. + */ + private interface Converter { + + /** + * Converts a Java or a Ruby typed object to an object that can be efficiently converted + * back to Java as well as Ruby. + * @param input Either a Java or Ruby type object + * @return Object that can be converted back to Java as well as Ruby efficiently + */ + Object convert(Object input); } } diff --git a/logstash-core/src/main/java/org/logstash/bivalues/BiValues.java b/logstash-core/src/main/java/org/logstash/bivalues/BiValues.java index 8a7d9c4aa..d259f04a9 100644 --- a/logstash-core/src/main/java/org/logstash/bivalues/BiValues.java +++ b/logstash-core/src/main/java/org/logstash/bivalues/BiValues.java @@ -28,7 +28,12 @@ public final class BiValues { final Class cls = o.getClass(); final BiValues.BiValueType type = CONVERTER_CACHE.get(cls); if (type == null) { - throw new IllegalArgumentException("Unsupported class " + cls); + throw new IllegalArgumentException( + String.format( + "Missing Converter handling for full class name=%s, simple name=%s", + cls.getName(), cls.getSimpleName() + ) + ); } return type.build(o); } diff --git a/logstash-core/src/test/java/org/logstash/ValuefierTest.java b/logstash-core/src/test/java/org/logstash/ValuefierTest.java index bcdcf0879..d864219bd 100644 --- a/logstash-core/src/test/java/org/logstash/ValuefierTest.java +++ b/logstash-core/src/test/java/org/logstash/ValuefierTest.java @@ -81,7 +81,7 @@ public class ValuefierTest extends TestBase { public void testUnhandledObject() { RubyMatchData md = new RubyMatchData(ruby); exception.expect(IllegalArgumentException.class); - exception.expectMessage("Missing Valuefier handling for full class name=org.jruby.RubyMatchData, simple name=RubyMatchData"); + exception.expectMessage("Missing Converter handling for full class name=org.jruby.RubyMatchData, simple name=RubyMatchData"); Valuefier.convert(md); }