package no.ssb.jsonstat.v2.deser;
import com.fasterxml.jackson.core.JsonParser;
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.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import no.ssb.jsonstat.v2.Dataset;
import no.ssb.jsonstat.v2.DatasetBuildable;
import no.ssb.jsonstat.v2.DatasetBuilder;
import no.ssb.jsonstat.v2.Dimension;
import java.io.IOException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.util.*;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Deserializer for Dataset.
*
* TODO: Use builder instead.
* TODO: Check {@link com.fasterxml.jackson.databind.deser.ResolvableDeserializer}
*/
public class DatasetDeserializer extends StdDeserializer {
static final TypeReference> VALUES_LIST = new TypeReference>() {
};
static final TypeReference> DIMENSION_MAP = new TypeReference>() {
};
static final TypeReference> ID_SET = new TypeReference>() {
};
static final TypeReference> SIZE_LIST = new TypeReference>() {
};
static final TypeReference> ROLE_MULTIMAP = new TypeReference>() {
};
static final TypeReference> VALUES_MAP = new TypeReference>() {
};
static final DateTimeFormatter ECMA_FORMATTER = new DateTimeFormatterBuilder()
.appendPattern("uuuu").optionalStart().appendPattern("-MM").optionalStart().appendPattern("-dd")
.optionalEnd()
.optionalEnd()
.optionalStart().appendLiteral("T").appendPattern("HH:mm").optionalStart().appendPattern(":ss")
.optionalStart().appendPattern(".SSS").optionalEnd().optionalEnd().optionalStart()
.appendPattern("z").optionalEnd().optionalEnd()
.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)
.parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
.parseDefaulting(ChronoField.HOUR_OF_DAY, 1)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
.parseDefaulting(ChronoField.MILLI_OF_SECOND, 0)
.parseDefaulting(ChronoField.OFFSET_SECONDS, 0).toFormatter();
public DatasetDeserializer() {
super(DatasetBuildable.class);
}
Instant parseEcmaDate(String value) {
return Instant.from(ECMA_FORMATTER.parse(value));
}
@Override
public Collection getKnownPropertyNames() {
return Arrays.asList(
"class", "version", "label",
"source", "updated", "id",
"size", "dimension", "value",
"link", "status", "extension"
);
}
@Override
public DatasetBuildable deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (p.getCurrentToken() == JsonToken.START_OBJECT) {
p.nextToken();
}
Set ids = Collections.emptySet();
List sizes = Collections.emptyList();
Multimap roles = ArrayListMultimap.create();
Map dims = Collections.emptyMap();
List values = Collections.emptyList();
DatasetBuilder builder = Dataset.create();
Optional version = Optional.empty();
Optional clazz = Optional.empty();
Optional extension = Optional.empty();
while (p.nextValue() != JsonToken.END_OBJECT) {
switch (p.getCurrentName()) {
case "label":
builder.withLabel(_parseString(p, ctxt));
break;
case "source":
builder.withSource(_parseString(p, ctxt));
break;
case "href":
break;
case "updated":
Instant updated = parseEcmaDate(_parseString(p, ctxt));
builder.updatedAt(updated);
break;
case "value":
values = parseValues(p, ctxt);
break;
case "dimension":
if (!version.orElse("1.x").equals("2.0")) {
dims = Maps.newHashMap();
// Deal with the id, size and role inside dimension.
while (p.nextValue() != JsonToken.END_OBJECT) {
switch (p.getCurrentName()) {
case "id":
ids = p.readValueAs(ID_SET);
break;
case "size":
sizes = p.readValueAs(SIZE_LIST);
break;
case "role":
roles = p.readValueAs(ROLE_MULTIMAP);
break;
default:
dims.put(
p.getCurrentName(),
ctxt.readValue(p, Dimension.Builder.class)
);
}
}
} else {
dims = p.readValueAs(DIMENSION_MAP);
}
break;
case "id":
ids = p.readValueAs(ID_SET);
break;
case "size":
sizes = p.readValueAs(SIZE_LIST);
break;
case "role":
roles = p.readValueAs(ROLE_MULTIMAP);
break;
case "extension":
extension = Optional.of(ctxt.readValue(
p, ObjectNode.class
));
break;
case "link":
case "status":
// TODO
p.skipChildren();
break;
case "version":
version = Optional.of(_parseString(p, ctxt));
break;
case "class":
// TODO
clazz = Optional.of(_parseString(p, ctxt));
break;
default:
boolean handled = ctxt.handleUnknownProperty(
p, this, Dimension.Builder.class, p.getCurrentName()
);
if (!handled)
p.skipChildren();
break;
}
}
// Setup roles
for (Map.Entry dimRole : roles.entries()) {
Dimension.Roles role = Dimension.Roles.valueOf(
dimRole.getKey().toUpperCase()
);
Dimension.Builder dimension = checkNotNull(
dims.get(dimRole.getValue()),
"could not assign the role {} to the dimension {}. The dimension did not exist",
role, dimRole.getValue()
);
dimension.withRole(role);
}
List orderedDimensions = Lists.newArrayList();
for (String dimensionName : ids) {
orderedDimensions.add(dims.get(dimensionName));
}
// TODO: Check size?
// Check ids and add to the data set.
checkArgument(ids.size() == dims.size(),
"dimension and size did not match"
);
if (extension.isPresent()) {
builder.withExtension(extension.get());
}
return builder.withDimensions(orderedDimensions).withValues(values);
}
List parseValues(JsonParser p, DeserializationContext ctxt) throws IOException {
List result = Collections.emptyList();
switch (p.getCurrentToken()) {
case START_OBJECT:
SortedMap map = p.readValueAs(VALUES_MAP);
result = new AbstractList() {
@Override
public int size() {
return map.lastKey() + 1;
}
@Override
public Number get(int index) {
return map.get(index);
}
};
break;
case START_ARRAY:
result = p.readValueAs(VALUES_LIST);
break;
default:
ctxt.handleUnexpectedToken(
this._valueClass, p.getCurrentToken(), p, "msg"
);
}
return result;
}
}