no.ssb.jsonstat.v2.deser.DimensionDeserializer.java Source code

Java tutorial

Introduction

Here is the source code for no.ssb.jsonstat.v2.deser.DimensionDeserializer.java

Source

/**
 * Copyright (C) 2016 Hadrien Kohl (hadrien.kohl@gmail.com) and contributors
 *
 *     DimensionDeserializer.java
 *
 * Licensed 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 no.ssb.jsonstat.v2.deser;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.google.common.base.Functions;
import com.google.common.collect.*;
import no.ssb.jsonstat.v2.Dimension;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.IntStream;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static me.yanaga.guava.stream.MoreCollectors.toImmutableMap;

/**
 * Deserialize dimensions to {@link no.ssb.jsonstat.v2.Dimension.Builder}.
 */
public class DimensionDeserializer extends StdDeserializer<Dimension.Builder> {

    static final TypeReference<Map<String, String>> LABEL_MAP = new TypeReference<Map<String, String>>() {
    };
    static final TypeReference<List<String>> INDEX_LIST = new TypeReference<List<String>>() {
    };
    static final TypeReference<Map<String, Integer>> INDEX_MAP = new TypeReference<Map<String, Integer>>() {
    };

    public DimensionDeserializer() {
        super(Dimension.Builder.class);
    }

    @Override
    public Dimension.Builder deserialize(JsonParser p, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {

        // Get the name first.
        String name = parseName(p, ctxt);

        if (p.getCurrentToken() == JsonToken.START_OBJECT) {
            p.nextToken();
        }

        Dimension.Builder dimension;
        dimension = Dimension.create(name);

        while (p.nextValue() != JsonToken.END_OBJECT) {
            switch (p.getCurrentName()) {
            case "category":
                parseCategory(dimension, p, ctxt);
                break;
            case "label":
                dimension.withLabel(parseLabel(p, ctxt));
                break;
            default:
                ctxt.handleUnknownProperty(p, this, Dimension.Builder.class, p.getCurrentName());
                break;
            }
        }

        return dimension;
    }

    private void parseCategory(Dimension.Builder dimension, JsonParser p, DeserializationContext ctxt)
            throws IOException {
        Map<String, String> index = null;
        Map<String, String> label = null;
        while (p.nextValue() != JsonToken.END_OBJECT) {
            switch (p.getCurrentName()) {
            case "index":
                index = parseIndex(p, ctxt);
                break;
            case "label":
                label = parseCategoryLabel(p, ctxt);
                break;
            case "unit":
                // TODO: Support units.
                parseUnit(p, ctxt);
                break;
            default:
                ctxt.handleUnknownProperty(p, this, Dimension.Builder.class, p.getCurrentName());
                break;
            }
        }
        checkArgument(!(index == null && label == null), "either label or index is required");

        // Once we have everything, we can build the dimension.

        if (index == null) {
            checkArgument(label.size() >= 1,
                    "category label must contain a least one element if " + "no index is provided");
            dimension.withIndexedLabels(ImmutableMap.copyOf(label));
            return;
        }
        if (label == null) {
            dimension.withCategories(ImmutableSet.copyOf(index.keySet()));
            return;
        }

        // TODO: Maybe the checks should reside inside the builder?
        checkArgument(label.size() == index.size(), "label and index's sizes were inconsistent");

        ImmutableMap<String, String> withIndex = index.keySet().stream()
                .collect(toImmutableMap(Function.identity(), label::get));
        dimension.withIndexedLabels(withIndex);

    }

    private Map<String, String> parseCategoryLabel(JsonParser p, DeserializationContext ctxt) throws IOException {
        if (p.currentToken() != JsonToken.START_OBJECT)
            ctxt.reportWrongTokenException(p, JsonToken.START_OBJECT, "label was not an object", (Object) null);

        Map<String, String> label = p.readValueAs(LABEL_MAP);

        return checkNotNull(label, "label object was null");
    }

    private void parseUnit(JsonParser p, DeserializationContext ctxt) throws IOException {
        p.skipChildren();
    }

    /**
     * Extract the dimension name.
     */
    private String parseName(JsonParser p, DeserializationContext ctxt) throws IOException {
        // TODO: Investigate this. The current name is the key value and token is object.
        //if (p.currentToken() != JsonToken.FIELD_NAME)
        //    ctxt.reportWrongTokenException(p, JsonToken.FIELD_NAME,
        //            "could not determine dimension name", p.getCurrentToken());

        String name = checkNotNull(p.getCurrentName(), "dimension name cannot be null");

        checkArgument(!name.isEmpty(), "dimension name cannot be empty");
        return name;
    }

    private String parseLabel(JsonParser p, DeserializationContext ctxt) throws IOException {
        String label = _parseString(p, ctxt);
        checkArgument(!label.trim().isEmpty(), "label cannot be empty");
        return label;
    }

    private ImmutableMap<String, String> parseIndex(JsonParser p, DeserializationContext ctxt) throws IOException {
        // Index can either be an array or object with id values. Here we transform
        // both to array.
        ImmutableMap<String, String> index = null;
        JsonToken token = p.currentToken();
        if (token == JsonToken.START_ARRAY)
            index = parseIndexAsArray(p);
        else if (token == JsonToken.START_OBJECT)
            index = parseIndexAsMap(p);
        else
            ctxt.reportMappingException(
                    "could not deserialize category index, need either an array " + "or an object, got %s", token);

        return checkNotNull(index, "could not parse index");

    }

    private ImmutableMap<String, String> parseIndexAsMap(JsonParser p) throws IOException {
        ImmutableMap<String, String> index;

        Map<String, Integer> mapIndex = p.readValueAs(INDEX_MAP);

        // Even though the type is String, the sorting actually uses the
        // integer value thanks to the forMap function.
        Ordering<String> byValue = Ordering.natural().onResultOf(Functions.forMap(mapIndex));

        index = ImmutableSortedMap.copyOf(Maps.transformValues(mapIndex, Object::toString), byValue);
        return index;
    }

    private ImmutableMap<String, String> parseIndexAsArray(JsonParser p) throws IOException {
        ImmutableMap<String, String> index;

        List<String> listIndex = p.readValueAs(INDEX_LIST);
        index = IntStream.range(0, listIndex.size()).boxed()
                .collect(toImmutableMap(listIndex::get, Object::toString));
        return index;
    }
}