首页 > 解决方案 > 如何将具有复合键的键值结构映射到Java中的json对象

问题描述

我有一个保存键值对象的数据库。键可能与分隔符复合,例如stackoverflow.questions.ask. 我想将这样的对象映射到 JSON。因此,对于stackoverflow.questions.ask我想要得到的密钥

{
    'stackoverflow' : {
        'questions' : {
                'ask' : 'value'
            }
        }
}

有什么简单的方法可以自动完成吗?

标签: javajsondatabasegson

解决方案


我不认为 Gson 是一个合适的工具,但我真的很喜欢这个问题。这是一个通用算法,您可以自己实现它并对其进行泛化以支持各种场景。这是一个示例实现,但由于我不擅长编写算法,它可以让您了解以更好的方式重新实现它。

final class Split {

    private Split() {
    }

    static <E, R, K, V> R split(final Iterator<E> iterator, final Splitter<E, R, K, V> splitter) {
        final R outerResult = splitter.newResult();
        while ( iterator.hasNext() ) {
            final E e = iterator.next();
            doSplit(splitter, outerResult, e);
        }
        return outerResult;
    }

    static <E, R, K, V> R split(final Iterable<E> iterable, final Splitter<E, R, K, V> splitter) {
        final R outerResult = splitter.newResult();
        for ( final E e : iterable ) {
            doSplit(splitter, outerResult, e);
        }
        return outerResult;
    }

    static <E, R, K, V> Collector<E, ?, R> asCollector(final Splitter<E, R, K, V> splitter) {
        return Collector.of(
                splitter::newResult,
                (outerResult, e) -> doSplit(splitter, outerResult, e),
                (r1, r2) -> {
                    throw new UnsupportedOperationException();
                },
                Function.identity()
        );
    }

    private static <E, R, K, V> void doSplit(final Splitter<E, R, K, V> splitter, final R outerResult, final E e) {
        final K elementKey = splitter.elementToKey(e);
        final K[] keyGroup = splitter.keyToKeyGroup(elementKey);
        R result = outerResult;
        final int lastI = keyGroup.length - 1;
        for ( int i = 0; i < lastI; i++ ) {
            final K innerKey = keyGroup[i];
            final R candidateInnerResult = splitter.fromInnerResult(result, innerKey);
            if ( candidateInnerResult == null ) {
                final R newTargetResult = splitter.newResult();
                @SuppressWarnings("unchecked")
                final V castNewTargetResult = (V) newTargetResult;
                splitter.toInnerResult(result, innerKey, castNewTargetResult);
                result = newTargetResult;
            } else {
                result = candidateInnerResult;
            }
        }
        final V value = splitter.elementToValue(e);
        splitter.toInnerResult(result, keyGroup[lastI], value);
    }

}

通常,doSplit(...)尝试为任何元素序列“拆分”键,因此您可以拆分任何内容,而不仅仅是地图。

这是上面使用的接口。它的所有方法都用于doSplit(...)执行不同的任务。

// E - type of elements it can process
// R - the result object type
// K - key type
// K - value type
interface Splitter<E, R, K, V> {

    // A factory method to create the outer of an inner result  
    R newResult();

    // A method to extract a key from the element
    K elementToKey(E element);

    // A method to extract a value from the element
    V elementToValue(E element);

    // A method to split a key to a key group so we can have a nested objects identitied with
    K[] keyToKeyGroup(K key);

    // A method to extract an inner result from existing inner result
    R fromInnerResult(R innerResult, K innerKey);

    // A method to put a key/value pair to the result
    void toInnerResult(R innerResult, K innerKey, V value);

    // A convenience method similar to Collector.of
    static <E, R, K, V> Splitter<E, R, K, V> of(
            final Function<? super E, ? extends K> elementToKey,
            final Function<? super E, ? extends V> elementToValue,
            final Function<? super K, ? extends K[]> keyToKeyGroup,
            final Supplier<? extends R> newResult,
            final BiFunction<? super R, ? super K, ? extends R> fromInnerResult,
            final TriConsumer<? super R, ? super K, ? super V> toInnerResult
    ) {
        return new Splitter<E, R, K, V>() {
            @Override
            public R newResult() {
                return newResult.get();
            }

            @Override
            public K elementToKey(final E element) {
                return elementToKey.apply(element);
            }

            @Override
            public V elementToValue(final E element) {
                return elementToValue.apply(element);
            }

            @Override
            public K[] keyToKeyGroup(final K key) {
                return keyToKeyGroup.apply(key);
            }

            @Override
            public R fromInnerResult(final R innerResult, final K innerKey) {
                return fromInnerResult.apply(innerResult, innerKey);
            }

            @Override
            public void toInnerResult(final R innerResult, final K innerKey, final V value) {
                toInnerResult.accept(innerResult, innerKey, value);
            }
        };
    }

}

由于 Java 8 中没有三消费者:

interface TriConsumer<T, U, V> {

    void accept(T t, U u, V v);

}

现在,由于这是一种通用方法,您可以有多个实现。例如,可以将序列拆分为映射的拆分器:

final class MapSplitters {

    private MapSplitters() {
    }

    static <K, V> Splitter<Map.Entry<K, V>, Map<K, V>, K, V> of(final Function<? super K, ? extends K[]> keyToKeyGroup) {
        return of(keyToKeyGroup, LinkedTreeMap::new);
    }

    static <K, V> Splitter<Map.Entry<K, V>, Map<K, V>, K, V> of(final Function<? super K, ? extends K[]> keyToKeyGroup,
            final Supplier<? extends Map<K, V>> mapFactory) {
        return Splitter.of(
                Map.Entry::getKey,
                Map.Entry::getValue,
                keyToKeyGroup, mapFactory, (innerMap, key) -> {
                    @SuppressWarnings("unchecked")
                    final Map<K, V> castInnerMap = (Map<K, V>) innerMap.get(key);
                    return castInnerMap;
                },
                Map::put
        );
    }

}

或 Gson JsonElementJsonObject特别是:

final class JsonElementSplitters {

    private JsonElementSplitters() {
    }

    static <V> Splitter<Map.Entry<String, V>, JsonObject, String, V> of(final Function<? super String, ? extends String[]> keyToKeyGroup) {
        return of(keyToKeyGroup, JsonElementSplitters::simpleObjectToSimpleJsonElement);
    }

    static <V> Splitter<Map.Entry<String, V>, JsonObject, String, V> of(final Function<? super String, ? extends String[]> keyToKeyGroup,
            final Gson gson) {
        return of(keyToKeyGroup, gson::toJsonTree);
    }

    static <V> Splitter<Map.Entry<String, V>, JsonObject, String, V> of(final Function<? super String, ? extends String[]> keyToKeyGroup,
            final Function<? super V, ? extends JsonElement> valueToJsonElement) {
        return Splitter.of(
                Map.Entry::getKey,
                Map.Entry::getValue, keyToKeyGroup,
                JsonObject::new,
                (innerJsonObject, key) -> {
                    final JsonElement jsonElement = innerJsonObject.get(key);
                    return jsonElement != null ? jsonElement.getAsJsonObject() : null;
                },
                (jsonObject, property, value) -> jsonObject.add(property, valueToJsonElement.apply(value))
        );
    }

    // In simple cases we can do a primitive box value to a simple JSON value   
    private static JsonElement simpleObjectToSimpleJsonElement(final Object o) {
        if ( o == null ) {
            return JsonNull.INSTANCE;
        }
        if ( o instanceof JsonElement ) {
            return (JsonElement) o;
        }
        if ( o instanceof Boolean ) {
            return new JsonPrimitive((Boolean) o);
        }
        if ( o instanceof Number ) {
            return new JsonPrimitive((Number) o);
        }
        if ( o instanceof String ) {
            return new JsonPrimitive((String) o);
        }
        if ( o instanceof Character ) {
            return new JsonPrimitive((Character) o);
        }
        throw new IllegalArgumentException("Cannot convert " + o.getClass());
    }

}

使用示例:

private static final Pattern dotPattern = Pattern.compile("\\.");

public static void main(final String... args) {
    final Map<String, Object> map = ImmutableMap.of("stackoverflow.questions.value", "value");
    final Splitter<Map.Entry<String, Object>, Map<String, Object>, String, Object> toMapSplitter = MapSplitters.of(dotPattern::split);
    final Splitter<Map.Entry<String, Object>, JsonObject, String, Object> toJsonObjectSplitter = JsonElementSplitters.of(dotPattern::split);
    // A simple to-inner-maps split example
    System.out.println(Split.split(map.entrySet(), toMapSplitter));
    // A simple to-nested-JSON-objects split example
    System.out.println(Split.split(map.entrySet(), toJsonObjectSplitter));
    // Or even use it with Java 8 Stream API
    System.out.println(
            map.entrySet()
                    .stream()
                    .map(e -> new AbstractMap.SimpleImmutableEntry<>(e.getKey().toUpperCase(), e.getValue()))
                    .collect(Split.asCollector(toMapSplitter))
    );
}

输出:

{stackoverflow={questions={value=value}}}
{"stackoverflow":{"questions":{"value":"value"}}}
{STACKOVERFLOW={QUESTIONS={VALUE=value}}}

我不确定是否有任何内置的 Java 8Collector可以做到这一点,但它看起来像一个分组收集器。


推荐阅读