From 978c775c662a172698c3e9f155d2390bb9c44e6c Mon Sep 17 00:00:00 2001 From: Dai MIKURUBE Date: Fri, 24 May 2024 17:22:31 +0900 Subject: [PATCH] Implement Compat.toMap to retrieve a Map object from DataSource --- .../java/org/embulk/util/config/Compat.java | 117 ++++++++++++++++++ .../embulk/util/config/DataSourceImpl.java | 3 +- .../org/embulk/util/config/TestCompat.java | 12 ++ 3 files changed, 131 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/embulk/util/config/Compat.java b/src/main/java/org/embulk/util/config/Compat.java index 971e6a0..70a2ae7 100644 --- a/src/main/java/org/embulk/util/config/Compat.java +++ b/src/main/java/org/embulk/util/config/Compat.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Map; import java.util.Optional; import org.embulk.config.DataSource; import org.embulk.util.config.rebuild.ObjectNodeRebuilder; @@ -36,6 +37,36 @@ private Compat() { // No instantiation. } + /** + * Rebuilds a {@link java.util.Map} representation from {@code org.embulk.config.DataSource}. + */ + static Map toMap(final DataSource source) throws IOException { + final Optional> map = callToMapIfAvailable(source); + if (map.isPresent()) { + // In case of newer Embulk versions since v0.10.41 -- `DataSource` has the `toMap` method. + // + // In this case, it uses the straightforward `toMap` method to convert into a Map. + return map.get(); + } + + // In case of older Embulk versions than v0.10.41 -- the `toMap` method is not defined in `DataSource`. + // + // In this case, it falls back to "toJson". + final String jsonString = toJson(source); + if (jsonString == null) { + throw new NullPointerException("DataSource(Impl)#toJson() returned null."); + } + + final JsonNode jsonNode = SIMPLE_MAPPER.readTree(jsonString); + if (!jsonNode.isObject()) { + throw new ClassCastException( + "DataSource(Impl)#toJson() returned not a JSON object: " + jsonNode.getClass().getCanonicalName()); + } + + final ObjectNode jsonObjectNode = (ObjectNode) jsonNode; + return DataSourceImpl.nodeToMap(jsonObjectNode); + } + /** * Rebuilds a stringified JSON representation from {@code org.embulk.config.DataSource}. */ @@ -94,6 +125,46 @@ static ObjectNode rebuildObjectNode(final DataSource source) throws IOException return callGetObjectNodeAndRebuildIfAvailable(source, SIMPLE_MAPPER); } + private static Optional> callToMapIfAvailable(final DataSource source) { + final Method toMap = getToMapMethod(source); + if (toMap == null) { + return Optional.empty(); + } + + final Object mapObject; + try { + mapObject = toMap.invoke(source); + } catch (final InvocationTargetException ex) { + final Throwable targetException = ex.getTargetException(); + if (targetException instanceof UnsupportedOperationException) { + // If the plugin's embulk-util-config does not implement toMap, it cannot retrieve a map. + return Optional.empty(); + } + if (targetException instanceof RuntimeException) { + throw (RuntimeException) targetException; + } + if (targetException instanceof Error) { + throw (Error) targetException; + } + throw new IllegalStateException("DataSource(Impl)#toMap() threw unexpected Exception.", targetException); + } catch (final IllegalAccessException ex) { + logger.debug("DataSource(Impl)#toMap is not accessible unexpectedly. DataSource: {}, toMap: {}, ", + source.getClass(), toMap); + throw new IllegalStateException("DataSource(Impl)#toMap() is not accessible.", ex); + } + + if (mapObject == null) { + throw new NullPointerException("DataSource(Impl)#toMap() returned null."); + } + if (!(mapObject instanceof Map)) { + throw new ClassCastException( + "DataSource(Impl)#toMap() returned not a Map: " + + mapObject.getClass().getCanonicalName()); + } + + return Optional.of(normalizeMap((Map) mapObject)); + } + private static Optional callToJsonIfAvailable(final DataSource source) { final Method toJson = getToJsonMethod(source); if (toJson == null) { @@ -170,6 +241,46 @@ private static ObjectNode callGetObjectNodeAndRebuildIfAvailable(final DataSourc return ObjectNodeRebuilder.rebuild(coreObjectNode, mapper); } + private static Method getToMapMethod(final DataSource source) { + try { + // Getting the "toMap" method from embulk-spi's public interface "org.embulk.config.DataSource", not from an implementation class, + // for example "org.embulk.(util.)config.DataSourceImpl", so that invoking the method does not throw IllegalAccessException. + // + // If the method instance is retrieved from a non-public implementation class, invoking it can fail like: + // java.lang.IllegalAccessException: + // Class org.embulk.util.config.Compat can not access a member of class org.embulk.util.config.DataSourceImpl with modifiers "public" + // + // See also: + // https://stackoverflow.com/questions/25020756/java-lang-illegalaccessexception-can-not-access-a-member-of-class-java-util-col + // + // A method instance retrieved from the public interface "org.embulk.config.DataSource" would solve the problem. + return DataSource.class.getMethod("toMap"); + } catch (final NoSuchMethodException ex) { + // Expected: toMap is not defined in "org.embulk.config.DataSource" when a user is running + // Embulk v0.10.40 or earlier. + // + // Even in the case, the received DataSource instance can still be of embulk-util-config's + // "org.embulk.util.config.DataSourceImpl" from another plugin (ex. input <=> parser), or + // from itself. As "org.embulk.util.config.DataSourceImpl" does not have "getObjectNode", + // it must still be rebuilt with "toMap" retrieved in some way. + // + // Pass-through to the next trial to retrieve the "toMap" method, then. + } + + final Class dataSourceImplClass = source.getClass(); + try { + // Getting the "toMap" method from the implementation class embulk-core's "org.embulk.config.DataSourceImpl", + // or embulk-util-config's "org.embulk.util.config.DataSourceImpl". + return dataSourceImplClass.getMethod("toMap"); + } catch (final NoSuchMethodException ex) { + // Still expected: toMap is not defined in embulk-core's "org.embulk.config.DataSourceImpl" + // in Embulk v0.10.40 or earlier. + // + // Returning null in this case so that it fallbacks to call the "getObjectNode" method instead. + return null; + } + } + private static Method getToJsonMethod(final DataSource source) { try { // Getting the "toJson" method from embulk-spi's public interface "org.embulk.config.DataSource", not from an implementation class, @@ -224,6 +335,12 @@ private static Method getGetObjectNodeMethod(final Class c } } + @SuppressWarnings("unchecked") + private static Map normalizeMap(final Map map) { + // TODO: Validate the map. + return (Map) map; + } + private static final ObjectMapper SIMPLE_MAPPER = new ObjectMapper(); private static final Logger logger = LoggerFactory.getLogger(Compat.class); diff --git a/src/main/java/org/embulk/util/config/DataSourceImpl.java b/src/main/java/org/embulk/util/config/DataSourceImpl.java index 2c15cae..7407158 100644 --- a/src/main/java/org/embulk/util/config/DataSourceImpl.java +++ b/src/main/java/org/embulk/util/config/DataSourceImpl.java @@ -454,7 +454,8 @@ private static void mergeJsonArray(final ArrayNode src, final ArrayNode other) { } } - private static Map nodeToMap(final ObjectNode object) { + // Not private for Compat.toMap. + static Map nodeToMap(final ObjectNode object) { final LinkedHashMap map = new LinkedHashMap<>(); for (final Map.Entry field : (Iterable>) () -> object.fields()) { map.put(field.getKey(), nodeToJavaObject(field.getValue())); diff --git a/src/test/java/org/embulk/util/config/TestCompat.java b/src/test/java/org/embulk/util/config/TestCompat.java index 9b22d12..197c77a 100644 --- a/src/test/java/org/embulk/util/config/TestCompat.java +++ b/src/test/java/org/embulk/util/config/TestCompat.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.IOException; +import java.util.LinkedHashMap; import org.junit.jupiter.api.Test; public class TestCompat { @@ -32,5 +33,16 @@ public void testToJson() throws IOException { assertEquals("{\"foo\":\"bar\"}", Compat.toJson(impl)); } + @Test + public void testToMap() throws IOException { + final ObjectNode node = SIMPLE_MAPPER.createObjectNode(); + node.put("foo", "bar"); + final DataSourceImpl impl = new DataSourceImpl(node, SIMPLE_MAPPER); + + final LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("foo", "bar"); + assertEquals(expected, Compat.toMap(impl)); + } + private static final ObjectMapper SIMPLE_MAPPER = new ObjectMapper(); }