Skip to content

Commit

Permalink
Merge pull request #79 from embulk/implement-Compat-toMap
Browse files Browse the repository at this point in the history
Implement Compat.toMap to retrieve a Map object from DataSource
  • Loading branch information
dmikurube authored May 25, 2024
2 parents 1a0b2f3 + 978c775 commit 824c46e
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 1 deletion.
117 changes: 117 additions & 0 deletions src/main/java/org/embulk/util/config/Compat.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,6 +37,36 @@ private Compat() {
// No instantiation.
}

/**
* Rebuilds a {@link java.util.Map} representation from {@code org.embulk.config.DataSource}.
*/
static Map<String, Object> toMap(final DataSource source) throws IOException {
final Optional<Map<String, Object>> 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}.
*/
Expand Down Expand Up @@ -94,6 +125,46 @@ static ObjectNode rebuildObjectNode(final DataSource source) throws IOException
return callGetObjectNodeAndRebuildIfAvailable(source, SIMPLE_MAPPER);
}

private static Optional<Map<String, Object>> 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<String> callToJsonIfAvailable(final DataSource source) {
final Method toJson = getToJsonMethod(source);
if (toJson == null) {
Expand Down Expand Up @@ -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<? extends DataSource> 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,
Expand Down Expand Up @@ -224,6 +335,12 @@ private static Method getGetObjectNodeMethod(final Class<? extends DataSource> c
}
}

@SuppressWarnings("unchecked")
private static Map<String, Object> normalizeMap(final Map map) {
// TODO: Validate the map.
return (Map<String, Object>) map;
}

private static final ObjectMapper SIMPLE_MAPPER = new ObjectMapper();

private static final Logger logger = LoggerFactory.getLogger(Compat.class);
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/embulk/util/config/DataSourceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,8 @@ private static void mergeJsonArray(final ArrayNode src, final ArrayNode other) {
}
}

private static Map<String, Object> nodeToMap(final ObjectNode object) {
// Not private for Compat.toMap.
static Map<String, Object> nodeToMap(final ObjectNode object) {
final LinkedHashMap<String, Object> map = new LinkedHashMap<>();
for (final Map.Entry<String, JsonNode> field : (Iterable<Map.Entry<String, JsonNode>>) () -> object.fields()) {
map.put(field.getKey(), nodeToJavaObject(field.getValue()));
Expand Down
12 changes: 12 additions & 0 deletions src/test/java/org/embulk/util/config/TestCompat.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<String, Object> expected = new LinkedHashMap<>();
expected.put("foo", "bar");
assertEquals(expected, Compat.toMap(impl));
}

private static final ObjectMapper SIMPLE_MAPPER = new ObjectMapper();
}

0 comments on commit 824c46e

Please sign in to comment.