ReportNodeDeserializer.java
/**
* Copyright (c) 2021, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* SPDX-License-Identifier: MPL-2.0
*/
package com.powsybl.commons.report;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
*/
public class ReportNodeDeserializer extends StdDeserializer<ReportNode> {
private static final Logger LOGGER = LoggerFactory.getLogger(ReportNodeDeserializer.class);
public static final String DICTIONARY_VALUE_ID = "dictionary";
public static final String DICTIONARY_DEFAULT_NAME = "default";
ReportNodeDeserializer() {
super(ReportNode.class);
}
@Override
public ReportNode deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
ReportNodeImpl reportNode = null;
ObjectMapper objectMapper = new ObjectMapper().registerModule(new ReportNodeJsonModule());
ReportNodeVersion version = ReportConstants.CURRENT_VERSION;
TreeContext treeContext = new TreeContextImpl();
while (p.nextToken() != JsonToken.END_OBJECT) {
switch (p.currentName()) {
case "version" -> version = ReportNodeVersion.of(p.nextTextValue());
case "dictionaries" -> readDictionary(p, objectMapper, treeContext, getDictionaryName(ctx));
case "reportRoot" -> reportNode = ReportNodeImpl.parseJsonNode(p, objectMapper, treeContext, version);
default -> throw new IllegalStateException("Unexpected value: " + p.currentName());
}
}
return reportNode;
}
private void readDictionary(JsonParser p, ObjectMapper objectMapper, TreeContext treeContext, String dictionaryName) throws IOException {
checkToken(p, JsonToken.START_OBJECT); // remove start object token to read the underlying map
TypeReference<HashMap<String, HashMap<String, String>>> dictionariesTypeRef = new TypeReference<>() {
};
// We could avoid constructing the full HashMap, at the cost of code readability (the case "dictionaryName not found" makes it tricky)
HashMap<String, HashMap<String, String>> dictionaries = objectMapper.readValue(p, dictionariesTypeRef);
Map<String, String> dictionary = dictionaries.get(dictionaryName);
if (dictionary == null) {
var dictionaryEntry = dictionaries.entrySet().stream().findFirst().orElse(null);
if (dictionaryEntry == null) {
LOGGER.warn("No dictionary found! `dictionaries` root entry is empty");
return;
} else {
LOGGER.warn("Cannot find `{}` dictionary, taking first entry (`{}`)", dictionaryName, dictionaryEntry.getKey());
dictionary = dictionaryEntry.getValue();
}
}
dictionary.forEach((k, message) -> treeContext.addDictionaryEntry(k, (key, locale) -> message));
}
private String getDictionaryName(DeserializationContext ctx) {
try {
BeanProperty bp = new BeanProperty.Std(new PropertyName("Language for dictionary"), null, null, null, null);
Object dicNameInjected = ctx.findInjectableValue(DICTIONARY_VALUE_ID, bp, null);
return dicNameInjected instanceof String name ? name : DICTIONARY_DEFAULT_NAME;
} catch (JsonMappingException | IllegalArgumentException e) {
LOGGER.info("No injectable value found for id `{}` in DeserializationContext, therefore taking `{}` dictionary",
DICTIONARY_VALUE_ID, DICTIONARY_DEFAULT_NAME);
return DICTIONARY_DEFAULT_NAME;
}
}
public static ReportNode read(Path jsonFile) {
return read(jsonFile, DICTIONARY_DEFAULT_NAME);
}
public static ReportNode read(InputStream jsonIs) {
return read(jsonIs, DICTIONARY_DEFAULT_NAME);
}
public static ReportNode read(Path jsonFile, String dictionary) {
Objects.requireNonNull(jsonFile);
Objects.requireNonNull(dictionary);
try (InputStream is = Files.newInputStream(jsonFile)) {
return read(is, dictionary);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static ReportNode read(InputStream jsonIs, String dictionary) {
Objects.requireNonNull(jsonIs);
Objects.requireNonNull(dictionary);
try {
return getReportNodeModelObjectMapper(dictionary).readValue(jsonIs, ReportNode.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static ObjectMapper getReportNodeModelObjectMapper(String dictionary) {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new ReportNodeJsonModule());
mapper.setInjectableValues(new InjectableValues.Std().addValue(DICTIONARY_VALUE_ID, dictionary));
return mapper;
}
static void checkToken(JsonParser p, JsonToken startObject) throws IOException {
if (p.nextToken() != startObject) {
throw new IllegalStateException("Unexpected token " + p.currentToken());
}
}
protected static final class TypedValueDeserializer extends StdDeserializer<TypedValue> {
TypedValueDeserializer() {
super(TypedValue.class);
}
@Override
public TypedValue deserialize(JsonParser p, DeserializationContext deserializationContext) throws IOException {
Object value = null;
String type = TypedValue.UNTYPED_TYPE;
while (p.nextToken() != JsonToken.END_OBJECT) {
switch (p.currentName()) {
case "value":
p.nextToken();
value = p.readValueAs(Object.class);
break;
case "type":
type = p.nextTextValue();
break;
default:
throw new IllegalStateException("Unexpected field: " + p.currentName());
}
}
return new TypedValue(value, type);
}
}
}