Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ReportNode deserialization by enriching ReportNode API with TreeContext #3155

Merged
merged 4 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ static ReportNodeBuilder newRootReportNode() {
*/
ReportNodeAdder newReportNode();

/**
* Get the {@link TreeContext} of the corresponding {@link ReportNode} tree.
*/
TreeContext getTreeContext();

/**
* Add a <code>ReportNode</code> as a child of current <code>ReportNode</code>.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,34 +27,34 @@
/**
* @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
*/
public class ReportNodeDeserializer extends StdDeserializer<ReportNodeImpl> {
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(ReportNodeImpl.class);
super(ReportNode.class);
}

@Override
public ReportNodeImpl deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
public ReportNode deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
ReportNodeImpl reportNode = null;
ObjectMapper objectMapper = new ObjectMapper().registerModule(new ReportNodeJsonModule());
ReportNodeVersion version = ReportConstants.CURRENT_VERSION;
RootContext rootContext = new RootContext();
TreeContextImpl treeContext = new TreeContextImpl();
while (p.nextToken() != JsonToken.END_OBJECT) {
switch (p.currentName()) {
case "version" -> version = ReportNodeVersion.of(p.nextTextValue());
case "dictionaries" -> readDictionary(p, objectMapper, rootContext, getDictionaryName(ctx));
case "reportRoot" -> reportNode = ReportNodeImpl.parseJsonNode(p, objectMapper, rootContext, version);
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, RootContext rootContext, String dictionaryName) throws IOException {
private void readDictionary(JsonParser p, ObjectMapper objectMapper, TreeContextImpl 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<>() {
};
Expand All @@ -71,7 +71,7 @@ private void readDictionary(JsonParser p, ObjectMapper objectMapper, RootContext
dictionary = dictionaryEntry.getValue();
}
}
dictionary.forEach(rootContext::addDictionaryEntry);
dictionary.forEach(treeContext::addDictionaryEntry);
}

private String getDictionaryName(DeserializationContext ctx) {
Expand Down Expand Up @@ -108,7 +108,7 @@ public static ReportNode read(InputStream jsonIs, String dictionary) {
Objects.requireNonNull(jsonIs);
Objects.requireNonNull(dictionary);
try {
return getReportNodeModelObjectMapper(dictionary).readValue(jsonIs, ReportNodeImpl.class);
return getReportNodeModelObjectMapper(dictionary).readValue(jsonIs, ReportNode.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,28 @@ public final class ReportNodeImpl implements ReportNode {
private final List<ReportNodeImpl> children = new ArrayList<>();
private final Collection<Map<String, TypedValue>> inheritedValuesMaps;
private final Map<String, TypedValue> values;
private final RefChain<RootContext> rootContext;
private final RefChain<TreeContextImpl> treeContext;
private boolean isRoot;
private Collection<Map<String, TypedValue>> valuesMapsInheritance;

static ReportNodeImpl createChildReportNode(String messageKey, String messageTemplate, Map<String, TypedValue> values, ReportNodeImpl parent) {
return createReportNode(messageKey, messageTemplate, values, parent.getValuesMapsInheritance(), parent.getRootContext(), false);
return createReportNode(messageKey, messageTemplate, values, parent.getValuesMapsInheritance(), parent.getTreeContextRef(), false);
}

static ReportNodeImpl createRootReportNode(String messageKey, String messageTemplate, Map<String, TypedValue> values, boolean timestamps, DateTimeFormatter timestampPattern) {
RefChain<RootContext> rootContext = new RefChain<>(new RefObj<>(new RootContext(timestamps, timestampPattern)));
return createReportNode(messageKey, messageTemplate, values, Collections.emptyList(), rootContext, true);
RefChain<TreeContextImpl> treeContext = new RefChain<>(new RefObj<>(new TreeContextImpl(timestamps, timestampPattern)));
return createReportNode(messageKey, messageTemplate, values, Collections.emptyList(), treeContext, true);
}

private static ReportNodeImpl createReportNode(String messageKey, String messageTemplate, Map<String, TypedValue> values,
Collection<Map<String, TypedValue>> inheritedValuesMaps, RefChain<RootContext> rootContextRef,
Collection<Map<String, TypedValue>> inheritedValuesMaps, RefChain<TreeContextImpl> treeContextRef,
boolean isRoot) {
RootContext rootContext = rootContextRef.get();
rootContext.addDictionaryEntry(Objects.requireNonNull(messageKey), Objects.requireNonNull(messageTemplate));
if (rootContext.isTimestampAdded()) {
values.put(ReportConstants.TIMESTAMP_KEY, rootContext.getTimestamp());
TreeContextImpl treeContext = treeContextRef.get();
treeContext.addDictionaryEntry(Objects.requireNonNull(messageKey), Objects.requireNonNull(messageTemplate));
if (treeContext.isTimestampAdded()) {
values.put(ReportConstants.TIMESTAMP_KEY, TypedValue.getTimestamp(treeContext.getTimestampFormatter()));
}
return new ReportNodeImpl(messageKey, values, inheritedValuesMaps, rootContextRef, isRoot);
return new ReportNodeImpl(messageKey, values, inheritedValuesMaps, treeContextRef, isRoot);
}

/**
Expand All @@ -74,15 +74,15 @@ private static ReportNodeImpl createReportNode(String messageKey, String message
* Be aware that any value in this map might, in all descendants, override a value of one of
* {@link ReportNode} ancestors.
* @param inheritedValuesMaps a {@link Collection} of inherited values maps
* @param rootContext the {@link RootContext} of the root of corresponding report tree
* @param treeContext the {@link TreeContextImpl} of the root of corresponding report tree
*/
private ReportNodeImpl(String messageKey, Map<String, TypedValue> values, Collection<Map<String, TypedValue>> inheritedValuesMaps, RefChain<RootContext> rootContext, boolean isRoot) {
private ReportNodeImpl(String messageKey, Map<String, TypedValue> values, Collection<Map<String, TypedValue>> inheritedValuesMaps, RefChain<TreeContextImpl> treeContext, boolean isRoot) {
this.messageKey = Objects.requireNonNull(messageKey);
checkMap(values);
Objects.requireNonNull(inheritedValuesMaps).forEach(ReportNodeImpl::checkMap);
this.values = Collections.unmodifiableMap(values);
this.inheritedValuesMaps = inheritedValuesMaps;
this.rootContext = Objects.requireNonNull(rootContext);
this.treeContext = Objects.requireNonNull(treeContext);
this.isRoot = isRoot;
}

Expand All @@ -100,7 +100,7 @@ public String getMessageKey() {

@Override
public String getMessageTemplate() {
return rootContext.get().getDictionary().get(messageKey);
return getTreeContext().getDictionary().get(messageKey);
}

@Override
Expand All @@ -110,7 +110,7 @@ public Map<String, TypedValue> getValues() {

@Override
public String getMessage() {
return Optional.ofNullable(rootContext.get().getDictionary().get(messageKey))
return Optional.ofNullable(getTreeContext().getDictionary().get(messageKey))
.map(messageTemplate -> new StringSubstitutor(vk -> getValueAsString(vk).orElse(null)).replace(messageTemplate))
.orElse("(missing message key in dictionary)");
}
Expand All @@ -119,8 +119,13 @@ public Optional<String> getValueAsString(String valueKey) {
return getValue(valueKey).map(TypedValue::getValue).map(Object::toString);
}

RefChain<RootContext> getRootContext() {
return rootContext;
@Override
public TreeContextImpl getTreeContext() {
return getTreeContextRef().get();
}

RefChain<TreeContextImpl> getTreeContextRef() {
return treeContext;
}

private Collection<Map<String, TypedValue>> getValuesMapsInheritance() {
Expand Down Expand Up @@ -160,8 +165,8 @@ public void include(ReportNode reportNode) {
reportNodeImpl.unroot();
children.add(reportNodeImpl);

rootContext.get().merge(reportNodeImpl.rootContext.get());
reportNodeImpl.rootContext.setRef(rootContext);
getTreeContext().merge(reportNodeImpl.getTreeContext());
reportNodeImpl.treeContext.setRef(treeContext);
}

private void unroot() {
Expand Down Expand Up @@ -198,21 +203,21 @@ private void print(Writer writer, String indent, String prefix) throws IOExcepti
writer.append(indent).append(prefix).append(getMessage()).append(System.lineSeparator());
}

public static ReportNodeImpl parseJsonNode(JsonParser parser, ObjectMapper objectMapper, RootContext rootContext, ReportNodeVersion version) throws IOException {
public static ReportNodeImpl parseJsonNode(JsonParser parser, ObjectMapper objectMapper, TreeContextImpl treeContext, ReportNodeVersion version) throws IOException {
Objects.requireNonNull(version, "ReportNode version is missing (null)");
Objects.requireNonNull(rootContext);
Objects.requireNonNull(treeContext);
return switch (version) {
case V_1_0, V_2_0 -> throw new PowsyblException("No backward compatibility of version " + version);
case V_2_1 -> parseJsonNode(parser, objectMapper, rootContext);
case V_2_1 -> parseJsonNode(parser, objectMapper, treeContext);
};
}

private static ReportNodeImpl parseJsonNode(JsonParser parser, ObjectMapper objectMapper, RootContext rootContext) throws IOException {
private static ReportNodeImpl parseJsonNode(JsonParser parser, ObjectMapper objectMapper, TreeContextImpl treeContext) throws IOException {
checkToken(parser, JsonToken.START_OBJECT); // remove start object token to read the ReportNode itself
return parseJsonNode(parser, objectMapper, new RefChain<>(new RefObj<>(rootContext)), Collections.emptyList(), true);
return parseJsonNode(parser, objectMapper, new RefChain<>(new RefObj<>(treeContext)), Collections.emptyList(), true);
}

private static ReportNodeImpl parseJsonNode(JsonParser p, ObjectMapper objectMapper, RefChain<RootContext> rootContext,
private static ReportNodeImpl parseJsonNode(JsonParser p, ObjectMapper objectMapper, RefChain<TreeContextImpl> treeContext,
Collection<Map<String, TypedValue>> inheritedValuesMaps, boolean rootReportNode) throws IOException {
ReportNodeImpl reportNode = null;
var parsingContext = new Object() {
Expand All @@ -230,21 +235,21 @@ private static ReportNodeImpl parseJsonNode(JsonParser p, ObjectMapper objectMap
}
case "children" -> {
// create the current reportNode to add the children to it
reportNode = new ReportNodeImpl(parsingContext.messageKey, parsingContext.values, inheritedValuesMaps, rootContext, rootReportNode);
reportNode = new ReportNodeImpl(parsingContext.messageKey, parsingContext.values, inheritedValuesMaps, treeContext, rootReportNode);

// Remove start array token to read each child
checkToken(p, JsonToken.START_ARRAY);

while (p.nextToken() != JsonToken.END_ARRAY) {
reportNode.addChild(parseJsonNode(p, objectMapper, rootContext, reportNode.getValuesMapsInheritance(), false));
reportNode.addChild(parseJsonNode(p, objectMapper, treeContext, reportNode.getValuesMapsInheritance(), false));
}
}
default -> throw new IllegalStateException("Unexpected value: " + p.currentName());
}
}

if (reportNode == null) {
reportNode = new ReportNodeImpl(parsingContext.messageKey, parsingContext.values, inheritedValuesMaps, rootContext, rootReportNode);
reportNode = new ReportNodeImpl(parsingContext.messageKey, parsingContext.values, inheritedValuesMaps, treeContext, rootReportNode);
}

return reportNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
public class ReportNodeJsonModule extends SimpleModule {

public ReportNodeJsonModule() {
addDeserializer(ReportNodeImpl.class, new ReportNodeDeserializer());
addDeserializer(ReportNode.class, new ReportNodeDeserializer());
addDeserializer(TypedValue.class, new ReportNodeDeserializer.TypedValueDeserializer());
addSerializer(ReportNodeImpl.class, new ReportNodeSerializer());
addSerializer(ReportNode.class, new ReportNodeSerializer());
addSerializer(TypedValue.class, new ReportNodeSerializer.TypedValueSerializer());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public ReportNodeAdder newReportNode() {
return new ChildAdder();
}

@Override
public TreeContext getTreeContext() {
return null;
}

@Override
public void include(ReportNode reportRoot) {
// No-op
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,21 @@
/**
* @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
*/
public class ReportNodeSerializer extends StdSerializer<ReportNodeImpl> {
public class ReportNodeSerializer extends StdSerializer<ReportNode> {

ReportNodeSerializer() {
super(ReportNodeImpl.class);
super(ReportNode.class);
}

@Override
public void serialize(ReportNodeImpl reportNode, JsonGenerator generator, SerializerProvider serializerProvider) throws IOException {
public void serialize(ReportNode reportNode, JsonGenerator generator, SerializerProvider serializerProvider) throws IOException {
generator.writeStartObject();

generator.writeStringField("version", ReportConstants.CURRENT_VERSION.toString());

generator.writeFieldName("dictionaries");
generator.writeStartObject();
generator.writeObjectField("default", reportNode.getRootContext().get().getDictionary());
generator.writeObjectField("default", reportNode.getTreeContext().getDictionary());
generator.writeEndObject();

generator.writeFieldName("reportRoot");
Expand Down
41 changes: 41 additions & 0 deletions commons/src/main/java/com/powsybl/commons/report/TreeContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2024, 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 java.time.format.DateTimeFormatter;
import java.util.Map;

/**
* Context attached to a {@link ReportNode} tree.
* It contains parameters intended to be used by all the {@link ReportNode} sharing the same tree.
* Hence, all the nodes of a tree should point to the same {@link TreeContext} through {@link ReportNode#getTreeContext()}.
*
* @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
*/
public interface TreeContext {

/**
* Get the dictionary of message templates indexed by their key.
*/
Map<String, String> getDictionary();

/**
* Get the {@link DateTimeFormatter} to use for timestamps, if enabled.
*/
DateTimeFormatter getTimestampFormatter();

/**
* Return whether a timestamp is added at each {@link ReportNode} creation.
*/
boolean isTimestampAdded();

/**
* Merge given {@link TreeContext} with current one.
*/
void merge(TreeContext treeContext);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,51 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

/**
* @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
*/
public class RootContext {
private static final Logger LOGGER = LoggerFactory.getLogger(RootContext.class);
public class TreeContextImpl implements TreeContext {
private static final Logger LOGGER = LoggerFactory.getLogger(TreeContextImpl.class);
private final SortedMap<String, String> dictionary = new TreeMap<>();
private final boolean timestamps;
private final DateTimeFormatter timestampFormatter;

public RootContext() {
public TreeContextImpl() {
this(false);
}

public RootContext(boolean timestamps) {
public TreeContextImpl(boolean timestamps) {
this(timestamps, ReportConstants.DEFAULT_TIMESTAMP_FORMATTER);
}

public RootContext(boolean timestamps, DateTimeFormatter dateTimeFormatter) {
public TreeContextImpl(boolean timestamps, DateTimeFormatter dateTimeFormatter) {
this.timestamps = timestamps;
this.timestampFormatter = Objects.requireNonNull(dateTimeFormatter);
}

@Override
public Map<String, String> getDictionary() {
return Collections.unmodifiableMap(dictionary);
}

@Override
public DateTimeFormatter getTimestampFormatter() {
return timestampFormatter;
}

@Override
public boolean isTimestampAdded() {
return timestamps;
}

@Override
public synchronized void merge(TreeContext otherContext) {
otherContext.getDictionary().forEach(this::addDictionaryEntry);
}

public synchronized void addDictionaryEntry(String key, String messageTemplate) {
dictionary.merge(key, messageTemplate, (prevMsg, newMsg) -> mergeEntries(key, prevMsg, newMsg));
}
Expand All @@ -50,16 +65,4 @@ private static String mergeEntries(String key, String previousMessageTemplate, S
}
return previousMessageTemplate;
}

public synchronized void merge(RootContext otherContext) {
otherContext.dictionary.forEach(this::addDictionaryEntry);
}

public boolean isTimestampAdded() {
return timestamps;
}

public TypedValue getTimestamp() {
return new TypedValue(timestampFormatter.format(ZonedDateTime.now()), TypedValue.TIMESTAMP);
}
}
Loading