ReportNodeImpl.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.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.ref.RefChain;
import com.powsybl.commons.ref.RefObj;
import org.apache.commons.io.IOUtils;
import org.apache.commons.text.StringSubstitutor;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Stream;
import static com.powsybl.commons.report.ReportNodeDeserializer.checkToken;
/**
* An in-memory implementation of {@link ReportNode}.
*
* <p>Being an implementation of {@link ReportNode}, instances of <code>ReportNodeImpl</code> are not thread-safe.
* As such, a <code>ReportNodeImpl</code> is not meant to be shared with other threads.
* Therefore, it should not be saved as a class parameter of an object which could be used by separate threads.
* In those cases it should instead be passed on in methods through their arguments.
*
* @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
*/
public final class ReportNodeImpl implements ReportNode {
private final String messageKey;
private final List<ReportNodeImpl> children = new ArrayList<>();
private final Collection<Map<String, TypedValue>> inheritedValuesMaps;
private final Map<String, TypedValue> values;
private final RefChain<TreeContext> treeContext;
private final MessageTemplateProvider messageTemplateProvider;
private boolean isRoot;
private Collection<Map<String, TypedValue>> valuesMapsInheritance;
static ReportNodeImpl createChildReportNode(String messageKey, Map<String, TypedValue> values, ReportNodeImpl parent,
MessageTemplateProvider messageTemplateProvider) {
ReportNodeImpl child = new ReportNodeImpl(messageKey, values, parent.getValuesMapsInheritance(), parent.getTreeContextRef(), false, messageTemplateProvider);
parent.addChild(child);
return child;
}
static ReportNodeImpl createRootReportNode(String messageKey, Map<String, TypedValue> values, TreeContext treeContext,
MessageTemplateProvider messageTemplateProvider) {
RefChain<TreeContext> treeContextRef = new RefChain<>(new RefObj<>(treeContext));
return new ReportNodeImpl(messageKey, values, Collections.emptyList(), treeContextRef, true, messageTemplateProvider);
}
/**
* ReportNodeImpl constructor, with no associated values.
*
* @param messageKey the key identifying the corresponding task
* @param values a map of {@link TypedValue} indexed by their key, which may be referred to within the messageTemplate
* or within any descendants of the created {@link ReportNode}.
* 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 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<TreeContext> treeContext, boolean isRoot, MessageTemplateProvider messageTemplateProvider) {
this.messageKey = Objects.requireNonNull(messageKey);
checkMap(values);
Objects.requireNonNull(inheritedValuesMaps).forEach(ReportNodeImpl::checkMap);
this.values = values;
this.inheritedValuesMaps = inheritedValuesMaps;
this.treeContext = Objects.requireNonNull(treeContext);
this.messageTemplateProvider = Objects.requireNonNull(messageTemplateProvider);
this.isRoot = isRoot;
}
private static void checkMap(Map<String, TypedValue> values) {
Objects.requireNonNull(values).forEach((k, v) -> {
Objects.requireNonNull(k);
Objects.requireNonNull(v);
});
}
@Override
public String getMessageKey() {
return messageKey;
}
@Override
public String getMessageTemplate() {
return getTreeContext().getDictionary().get(messageKey);
}
@Override
public Map<String, TypedValue> getValues() {
return Collections.unmodifiableMap(values);
}
@Override
public String getMessage(ReportFormatter formatter) {
return Optional.ofNullable(getTreeContext().getDictionary().get(messageKey))
.map(messageTemplate -> new StringSubstitutor(vk -> getValueAsString(vk, formatter).orElse(null)).replace(messageTemplate))
.orElse("(missing message key in dictionary)");
}
public Optional<String> getValueAsString(String valueKey, ReportFormatter formatter) {
return getValue(valueKey).map(formatter::format);
}
@Override
public TreeContext getTreeContext() {
return getTreeContextRef().get();
}
RefChain<TreeContext> getTreeContextRef() {
return treeContext;
}
private Collection<Map<String, TypedValue>> getValuesMapsInheritance() {
if (valuesMapsInheritance == null) {
valuesMapsInheritance = new ArrayList<>(1 + inheritedValuesMaps.size());
valuesMapsInheritance.add(values);
valuesMapsInheritance.addAll(inheritedValuesMaps);
}
return valuesMapsInheritance;
}
@Override
public Optional<TypedValue> getValue(String valueKey) {
return Stream.concat(Stream.of(values), inheritedValuesMaps.stream())
.map(m -> m.get(valueKey))
.filter(Objects::nonNull)
.findFirst();
}
@Override
public ReportNodeAdder newReportNode() {
return new ReportNodeChildAdderImpl(this, messageTemplateProvider);
}
@Override
public void include(ReportNode reportRoot) {
if (!(reportRoot instanceof ReportNodeImpl reportNodeImpl)) {
throw new PowsyblException("Cannot mix implementations of ReportNode, included reportNode should be/extend ReportNodeImpl");
}
if (!reportNodeImpl.isRoot) {
throw new PowsyblException("Cannot include non-root reportNode");
}
if (getTreeContext() == reportNodeImpl.getTreeContext()) {
throw new PowsyblException("The given reportNode cannot be included as it is the root of the reportNode");
}
reportNodeImpl.unroot();
children.add(reportNodeImpl);
getTreeContext().merge(reportNodeImpl.getTreeContext());
reportNodeImpl.treeContext.setRef(treeContext);
}
@Override
public void addCopy(ReportNode reportNode) {
var om = new ObjectMapper().registerModule(new ReportNodeJsonModule());
var sw = new StringWriter();
try {
om.writeValue(sw, reportNode);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
ReportNodeImpl copiedReportNode = (ReportNodeImpl) ReportNodeDeserializer.read(IOUtils.toInputStream(sw.toString(), StandardCharsets.UTF_8));
children.add(copiedReportNode);
getTreeContext().merge(copiedReportNode.getTreeContext());
copiedReportNode.treeContext.setRef(treeContext);
}
private void unroot() {
this.isRoot = false;
}
void addChild(ReportNodeImpl reportNode) {
children.add(reportNode);
}
@Override
public List<ReportNode> getChildren() {
return Collections.unmodifiableList(children);
}
@Override
public ReportNodeImpl addTypedValue(String key, String value, String type) {
values.put(key, TypedValue.of(value, type));
return this;
}
@Override
public ReportNodeImpl addUntypedValue(String key, String value) {
values.put(key, TypedValue.untyped(value));
return this;
}
@Override
public ReportNodeImpl addTypedValue(String key, double value, String type) {
values.put(key, TypedValue.of(value, type));
return this;
}
@Override
public ReportNodeImpl addUntypedValue(String key, double value) {
values.put(key, TypedValue.untyped(value));
return this;
}
@Override
public ReportNodeImpl addTypedValue(String key, float value, String type) {
values.put(key, TypedValue.of(value, type));
return this;
}
@Override
public ReportNodeImpl addUntypedValue(String key, float value) {
values.put(key, TypedValue.untyped(value));
return this;
}
@Override
public ReportNodeImpl addTypedValue(String key, int value, String type) {
values.put(key, TypedValue.of(value, type));
return this;
}
@Override
public ReportNodeImpl addUntypedValue(String key, int value) {
values.put(key, TypedValue.untyped(value));
return this;
}
@Override
public ReportNodeImpl addTypedValue(String key, long value, String type) {
values.put(key, TypedValue.of(value, type));
return this;
}
@Override
public ReportNodeImpl addUntypedValue(String key, long value) {
values.put(key, TypedValue.untyped(value));
return this;
}
@Override
public ReportNodeImpl addTypedValue(String key, boolean value, String type) {
values.put(key, TypedValue.of(value, type));
return this;
}
@Override
public ReportNodeImpl addUntypedValue(String key, boolean value) {
values.put(key, TypedValue.untyped(value));
return this;
}
@Override
public ReportNodeImpl addSeverity(TypedValue severity) {
TypedValue.checkSeverityType(severity);
values.put(ReportConstants.SEVERITY_KEY, severity);
return this;
}
@Override
public ReportNodeImpl addSeverity(String severity) {
values.put(ReportConstants.SEVERITY_KEY, TypedValue.of(severity, TypedValue.SEVERITY));
return this;
}
@Override
public void print(Writer writer, ReportFormatter formatter) throws IOException {
print(writer, "", formatter);
}
private void print(Writer writer, String indentationStart, ReportFormatter formatter) throws IOException {
if (children.isEmpty()) {
print(writer, indentationStart, "", formatter);
} else {
print(writer, indentationStart, "+ ", formatter);
String childrenIndent = indentationStart + " ";
for (ReportNodeImpl child : children) {
child.print(writer, childrenIndent, formatter);
}
}
}
private void print(Writer writer, String indent, String prefix, ReportFormatter formatter) throws IOException {
writer.append(indent).append(prefix).append(getMessage(formatter)).append(System.lineSeparator());
}
public static ReportNodeImpl parseJsonNode(JsonParser parser, ObjectMapper objectMapper, TreeContext treeContext, ReportNodeVersion version) throws IOException {
Objects.requireNonNull(version, "ReportNode version is missing (null)");
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, V_3_0 -> parseJsonNode(parser, objectMapper, treeContext);
};
}
private static ReportNodeImpl parseJsonNode(JsonParser parser, ObjectMapper objectMapper, TreeContext 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<>(treeContext)), Collections.emptyList(), true);
}
private static ReportNodeImpl parseJsonNode(JsonParser p, ObjectMapper objectMapper, RefChain<TreeContext> treeContext,
Collection<Map<String, TypedValue>> inheritedValuesMaps, boolean rootReportNode) throws IOException {
ReportNodeImpl reportNode = null;
var parsingContext = new Object() {
String messageKey;
Map<String, TypedValue> values = Collections.emptyMap();
};
while (p.nextToken() != JsonToken.END_OBJECT) {
switch (p.currentName()) {
case "messageKey" -> parsingContext.messageKey = p.nextTextValue();
case "values" -> {
checkToken(p, JsonToken.START_OBJECT); // Remove start object token to read the underlying map
parsingContext.values = objectMapper.readValue(p, new TypeReference<HashMap<String, TypedValue>>() {
});
}
case "children" -> {
// create the current reportNode to add the children to it
reportNode = new ReportNodeImpl(parsingContext.messageKey, parsingContext.values, inheritedValuesMaps, treeContext, rootReportNode, MessageTemplateProvider.EMPTY);
// Remove start array token to read each child
checkToken(p, JsonToken.START_ARRAY);
while (p.nextToken() != JsonToken.END_ARRAY) {
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, treeContext, rootReportNode, MessageTemplateProvider.EMPTY);
}
return reportNode;
}
@Override
public void writeJson(JsonGenerator generator) throws IOException {
generator.writeStringField("messageKey", getMessageKey());
if (!values.isEmpty()) {
generator.writeObjectField("values", values);
}
if (!children.isEmpty()) {
generator.writeFieldName("children");
generator.writeStartArray();
for (ReportNodeImpl messageNode : children) {
generator.writeStartObject();
messageNode.writeJson(generator);
generator.writeEndObject();
}
generator.writeEndArray();
}
}
}