diff --git a/commons/src/main/java/com/powsybl/commons/json/JsonUtil.java b/commons/src/main/java/com/powsybl/commons/json/JsonUtil.java index 64daed9c409..02eda019060 100644 --- a/commons/src/main/java/com/powsybl/commons/json/JsonUtil.java +++ b/commons/src/main/java/com/powsybl/commons/json/JsonUtil.java @@ -433,21 +433,49 @@ public static void assertLessThanReferenceVersion(String contextName, String ele } } + /** + * Called by variants of {@link #parseObject} on each encountered field. + * Should return false if an unexpected field was encountered. + */ @FunctionalInterface public interface FieldHandler { - boolean onField(String name) throws IOException; } + /** + * Parses an object from the current parser position, using the provided field handler. + * The parsing will expect the starting position to be START_OBJECT. + */ public static void parseObject(JsonParser parser, FieldHandler fieldHandler) { + parseObject(parser, false, fieldHandler); + } + + /** + * Parses an object from the current parser position, using the provided field handler. + * The parsing will accept the starting position to be either a START_OBJECT or a FIELD_NAME, + * see contract for {@link JsonDeserializer#deserialize(JsonParser, DeserializationContext)}. + */ + public static void parsePolymorphicObject(JsonParser parser, FieldHandler fieldHandler) { + parseObject(parser, true, fieldHandler); + } + + /** + * Parses an object from the current parser position, using the provided field handler. + * If {@code polymorphic} is {@code true}, the parsing will accept the starting position + * to be either a START_OBJECT or a FIELD_NAME, see contract for {@link JsonDeserializer#deserialize(JsonParser, DeserializationContext)}. + */ + public static void parseObject(JsonParser parser, boolean polymorphic, FieldHandler fieldHandler) { Objects.requireNonNull(parser); Objects.requireNonNull(fieldHandler); try { JsonToken token = parser.currentToken(); - if (token != JsonToken.START_OBJECT) { + if (!polymorphic && token != JsonToken.START_OBJECT) { throw new PowsyblException("Start object token was expected instead got: " + token); } - while ((token = parser.nextToken()) != null) { + if (token == JsonToken.START_OBJECT) { + token = parser.nextToken(); + } + while (token != null) { if (token == JsonToken.FIELD_NAME) { String fieldName = parser.getCurrentName(); boolean found = fieldHandler.onField(fieldName); @@ -459,6 +487,7 @@ public static void parseObject(JsonParser parser, FieldHandler fieldHandler) { } else { throw new PowsyblException("Unexpected token " + token); } + token = parser.nextToken(); } } catch (IOException e) { throw new UncheckedIOException(e); diff --git a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/SecurityAnalysisJsonModule.java b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/SecurityAnalysisJsonModule.java index 34d2ff7d362..96ac9c45229 100644 --- a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/SecurityAnalysisJsonModule.java +++ b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/SecurityAnalysisJsonModule.java @@ -6,25 +6,50 @@ */ package com.powsybl.security.json; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.powsybl.commons.util.ServiceLoaderCache; import com.powsybl.contingency.json.ContingencyJsonModule; import com.powsybl.security.*; -import com.powsybl.security.action.Action; -import com.powsybl.security.action.ActionList; +import com.powsybl.security.action.*; import com.powsybl.security.condition.Condition; -import com.powsybl.security.json.action.ActionDeserializer; -import com.powsybl.security.json.action.ActionListDeserializer; -import com.powsybl.security.json.action.ActionListSerializer; -import com.powsybl.security.json.action.ActionSerializer; +import com.powsybl.security.json.action.*; +import com.powsybl.security.results.*; import com.powsybl.security.strategy.OperatorStrategy; import com.powsybl.security.strategy.OperatorStrategyList; -import com.powsybl.security.results.*; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; /** * @author Geoffroy Jamgotchian */ public class SecurityAnalysisJsonModule extends ContingencyJsonModule { + private final List plugins; + public SecurityAnalysisJsonModule() { + this(getServices()); + } + + private static List getServices() { + return new ServiceLoaderCache<>(SecurityAnalysisJsonPlugin.class).getServices(); + } + + /** + * Deserializer for actions will be chosen based on the "type" property. + */ + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true) + interface ActionMixIn { + } + + public SecurityAnalysisJsonModule(Collection plugins) { + Objects.requireNonNull(plugins); + this.plugins = List.copyOf(plugins); addSerializer(SecurityAnalysisResult.class, new SecurityAnalysisResultSerializer()); addSerializer(NetworkMetadata.class, new NetworkMetadataSerializer()); addSerializer(PostContingencyResult.class, new PostContingencyResultSerializer()); @@ -38,7 +63,6 @@ public SecurityAnalysisJsonModule() { addSerializer(OperatorStrategyResult.class, new OperatorStrategyResultSerializer()); addSerializer(OperatorStrategy.class, new OperatorStrategySerializer()); addSerializer(OperatorStrategyList.class, new OperatorStrategyListSerializer()); - addSerializer(Action.class, new ActionSerializer()); addSerializer(ActionList.class, new ActionListSerializer()); addSerializer(Condition.class, new ConditionSerializer()); @@ -55,9 +79,35 @@ public SecurityAnalysisJsonModule() { addDeserializer(OperatorStrategyResult.class, new OperatorStrategyResultDeserializer()); addDeserializer(OperatorStrategy.class, new OperatorStrategyDeserializer()); addDeserializer(OperatorStrategyList.class, new OperatorStrategyListDeserializer()); - addDeserializer(Action.class, new ActionDeserializer()); addDeserializer(ActionList.class, new ActionListDeserializer()); addDeserializer(Condition.class, new ConditionDeserializer()); addDeserializer(NetworkResult.class, new NetworkResultDeserializer()); + + configureActionsSerialization(); + } + + private void configureActionsSerialization() { + setMixInAnnotation(Action.class, ActionMixIn.class); + registerActionType(SwitchAction.class, SwitchAction.NAME, + new SwitchActionSerializer(), new SwitchActionDeserializer()); + registerActionType(LineConnectionAction.class, LineConnectionAction.NAME, + new LineConnectionActionSerializer(), new LineConnectionActionDeserializer()); + registerActionType(MultipleActionsAction.class, MultipleActionsAction.NAME, + new MultipleActionsActionSerializer(), new MultipleActionsActionDeserializer()); + registerActionType(PhaseTapChangerTapPositionAction.class, PhaseTapChangerTapPositionAction.NAME, + new PhaseTapChangerTapPositionActionSerializer(), new PhaseTapChangerTapPositionActionDeserializer()); + } + + private void registerActionType(Class actionClass, String typeName, JsonSerializer serializer, JsonDeserializer deserializer) { + registerSubtypes(new NamedType(actionClass, typeName)); + addDeserializer(actionClass, deserializer); + addSerializer(actionClass, serializer); + } + + @Override + public Iterable getDependencies() { + return () -> plugins.stream() + .flatMap(plugin -> plugin.getJsonModules().stream()) + .iterator(); } } diff --git a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/SecurityAnalysisJsonPlugin.java b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/SecurityAnalysisJsonPlugin.java new file mode 100644 index 00000000000..660f42d4a56 --- /dev/null +++ b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/SecurityAnalysisJsonPlugin.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022, 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/. + */ +package com.powsybl.security.json; + +import com.fasterxml.jackson.databind.Module; + +import java.util.List; + +/** + * @author Sylvain Leclerc + */ +public interface SecurityAnalysisJsonPlugin { + + /** + * Provide third-party jackson modules that should be registered as dependencies of the + * main security analysis module. + * + * @return A list of modules to be registered for use in security analysis related serialization. + */ + List getJsonModules(); +} diff --git a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/ActionDeserializer.java b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/ActionDeserializer.java deleted file mode 100644 index b11412131b9..00000000000 --- a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/ActionDeserializer.java +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2022, 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/. - */ -package com.powsybl.security.json.action; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.powsybl.security.action.*; - -import java.io.IOException; - -/** - * @author Etienne Lesot - */ -public class ActionDeserializer extends StdDeserializer { - - public ActionDeserializer() { - super(Action.class); - } - - @Override - public Action deserialize(JsonParser parser, DeserializationContext deserializationContext) throws IOException { - parser.nextToken(); - while (parser.getCurrentName() != null) { - if ("type".equals(parser.getCurrentName())) { - switch (parser.nextTextValue()) { - case LineConnectionAction.NAME: - LineConnectionActionDeserializer lineConnectionActionDeserializer = new LineConnectionActionDeserializer(); - return lineConnectionActionDeserializer.deserialize(parser, deserializationContext); - case MultipleActionsAction.NAME: - MultipleActionsActionDeserializer multipleActionsActionDeserializer = new MultipleActionsActionDeserializer(); - return multipleActionsActionDeserializer.deserialize(parser, deserializationContext); - case PhaseTapChangerTapPositionAction.NAME: - PhaseTapChangerTapPositionActionDeserializer phaseTapChangerTapPositionActionDeserializer = new PhaseTapChangerTapPositionActionDeserializer(); - return phaseTapChangerTapPositionActionDeserializer.deserialize(parser, deserializationContext); - case SwitchAction.NAME: - SwitchActionDeserializer switchActionDeserializer = new SwitchActionDeserializer(); - return switchActionDeserializer.deserialize(parser, deserializationContext); - default: - throw JsonMappingException.from(parser, "Unknown action type: " + parser.getCurrentName()); - } - } - parser.nextToken(); - } - throw new IllegalArgumentException("contingency List needs a type"); - } -} diff --git a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/LineConnectionActionDeserializer.java b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/LineConnectionActionDeserializer.java index 482297cb6d7..f240849adf0 100644 --- a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/LineConnectionActionDeserializer.java +++ b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/LineConnectionActionDeserializer.java @@ -7,10 +7,10 @@ package com.powsybl.security.json.action; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.powsybl.commons.json.JsonUtil; import com.powsybl.security.action.LineConnectionAction; import java.io.IOException; @@ -34,26 +34,31 @@ private static class ParsingContext { @Override public LineConnectionAction deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { ParsingContext context = new ParsingContext(); - while (jsonParser.nextToken() != JsonToken.END_OBJECT) { - switch (jsonParser.getCurrentName()) { + JsonUtil.parsePolymorphicObject(jsonParser, name -> { + switch (name) { + case "type": + if (!LineConnectionAction.NAME.equals(jsonParser.nextTextValue())) { + throw JsonMappingException.from(jsonParser, "Expected type " + LineConnectionAction.NAME); + } + return true; case "id": context.id = jsonParser.nextTextValue(); - break; + return true; case "lineId": context.lineId = jsonParser.nextTextValue(); - break; + return true; case "openSide1": jsonParser.nextToken(); context.openSide1 = jsonParser.getValueAsBoolean(); - break; + return true; case "openSide2": jsonParser.nextToken(); context.openSide2 = jsonParser.getValueAsBoolean(); - break; + return true; default: - throw new IllegalArgumentException(""); + return false; } - } + }); if (context.openSide1 == null || context.openSide2 == null) { throw JsonMappingException.from(jsonParser, "for line action openSide1 and openSide2 fields can't be null"); } diff --git a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/LineConnectionActionSerializer.java b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/LineConnectionActionSerializer.java new file mode 100644 index 00000000000..090044bfb7b --- /dev/null +++ b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/LineConnectionActionSerializer.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2022, 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/. + */ +package com.powsybl.security.json.action; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.powsybl.security.action.LineConnectionAction; + +import java.io.IOException; + +/** + * @author Etienne Lesot + */ +public class LineConnectionActionSerializer extends StdSerializer { + + public LineConnectionActionSerializer() { + super(LineConnectionAction.class); + } + + @Override + public void serialize(LineConnectionAction action, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("type", action.getType()); + jsonGenerator.writeStringField("id", action.getId()); + jsonGenerator.writeStringField("lineId", action.getLineId()); + jsonGenerator.writeBooleanField("openSide1", action.isOpenSide1()); + jsonGenerator.writeBooleanField("openSide2", action.isOpenSide2()); + jsonGenerator.writeEndObject(); + } +} diff --git a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/MultipleActionsActionDeserializer.java b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/MultipleActionsActionDeserializer.java index 06a0d487015..cd4fa9ca042 100644 --- a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/MultipleActionsActionDeserializer.java +++ b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/MultipleActionsActionDeserializer.java @@ -7,10 +7,11 @@ package com.powsybl.security.json.action; 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.JsonMappingException; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.powsybl.commons.json.JsonUtil; import com.powsybl.security.action.Action; import com.powsybl.security.action.MultipleActionsAction; @@ -35,20 +36,25 @@ private static class ParsingContext { @Override public MultipleActionsAction deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { ParsingContext context = new ParsingContext(); - while (jsonParser.nextToken() != JsonToken.END_OBJECT) { - switch (jsonParser.getCurrentName()) { + JsonUtil.parsePolymorphicObject(jsonParser, name -> { + switch (name) { + case "type": + if (!MultipleActionsAction.NAME.equals(jsonParser.nextTextValue())) { + throw JsonMappingException.from(jsonParser, "Expected type " + MultipleActionsAction.NAME); + } + return true; case "id": context.id = jsonParser.nextTextValue(); - break; + return true; case "actions": jsonParser.nextToken(); context.actions = jsonParser.readValueAs(new TypeReference>() { }); - break; + return true; default: - throw new IllegalArgumentException(""); + return false; } - } + }); return new MultipleActionsAction(context.id, context.actions); } } diff --git a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/MultipleActionsActionSerializer.java b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/MultipleActionsActionSerializer.java new file mode 100644 index 00000000000..ba3bffb3c02 --- /dev/null +++ b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/MultipleActionsActionSerializer.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2022, 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/. + */ +package com.powsybl.security.json.action; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.powsybl.security.action.MultipleActionsAction; + +import java.io.IOException; + +/** + * @author Etienne Lesot + */ +public class MultipleActionsActionSerializer extends StdSerializer { + + public MultipleActionsActionSerializer() { + super(MultipleActionsAction.class); + } + + @Override + public void serialize(MultipleActionsAction action, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("type", action.getType()); + jsonGenerator.writeStringField("id", action.getId()); + jsonGenerator.writeObjectField("actions", action.getActions()); + jsonGenerator.writeEndObject(); + } +} diff --git a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/PhaseTapChangerTapPositionActionDeserializer.java b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/PhaseTapChangerTapPositionActionDeserializer.java index 94137cd9ea2..4df17b121bc 100644 --- a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/PhaseTapChangerTapPositionActionDeserializer.java +++ b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/PhaseTapChangerTapPositionActionDeserializer.java @@ -7,10 +7,10 @@ package com.powsybl.security.json.action; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.powsybl.commons.json.JsonUtil; import com.powsybl.iidm.network.ThreeWindingsTransformer; import com.powsybl.security.action.PhaseTapChangerTapPositionAction; @@ -36,29 +36,34 @@ private static class ParsingContext { @Override public PhaseTapChangerTapPositionAction deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { ParsingContext context = new ParsingContext(); - while (jsonParser.nextToken() != JsonToken.END_OBJECT) { - switch (jsonParser.getCurrentName()) { + JsonUtil.parsePolymorphicObject(jsonParser, name -> { + switch (name) { + case "type": + if (!PhaseTapChangerTapPositionAction.NAME.equals(jsonParser.nextTextValue())) { + throw JsonMappingException.from(jsonParser, "Expected type " + PhaseTapChangerTapPositionAction.NAME); + } + return true; case "id": context.id = jsonParser.nextTextValue(); - break; + return true; case "transformerId": context.transformerId = jsonParser.nextTextValue(); - break; + return true; case "value": jsonParser.nextToken(); context.value = jsonParser.getValueAsInt(); - break; + return true; case "relativeValue": jsonParser.nextToken(); context.relativeValue = jsonParser.getValueAsBoolean(); - break; + return true; case "side": context.side = ThreeWindingsTransformer.Side.valueOf(jsonParser.nextTextValue()); - break; + return true; default: - throw new IllegalArgumentException(""); + return false; } - } + }); if (context.relativeValue == null) { throw JsonMappingException.from(jsonParser, "for phase tap changer tap position action relative value field can't be null"); } diff --git a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/PhaseTapChangerTapPositionActionSerializer.java b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/PhaseTapChangerTapPositionActionSerializer.java new file mode 100644 index 00000000000..523416100f7 --- /dev/null +++ b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/PhaseTapChangerTapPositionActionSerializer.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2022, 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/. + */ +package com.powsybl.security.json.action; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.powsybl.security.action.PhaseTapChangerTapPositionAction; + +import java.io.IOException; +import java.io.UncheckedIOException; + +/** + * @author Etienne Lesot + */ +public class PhaseTapChangerTapPositionActionSerializer extends StdSerializer { + + public PhaseTapChangerTapPositionActionSerializer() { + super(PhaseTapChangerTapPositionAction.class); + } + + @Override + public void serialize(PhaseTapChangerTapPositionAction action, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("type", action.getType()); + + jsonGenerator.writeStringField("id", action.getId()); + jsonGenerator.writeStringField("transformerId", action.getTransformerId()); + jsonGenerator.writeNumberField("value", action.getValue()); + jsonGenerator.writeBooleanField("relativeValue", action.isRelativeValue()); + action.getSide().ifPresent(side -> { + try { + jsonGenerator.writeStringField("side", side.toString()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + jsonGenerator.writeEndObject(); + } +} diff --git a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/SwitchActionDeserializer.java b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/SwitchActionDeserializer.java index 965fe6a0511..b319f0ec3df 100644 --- a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/SwitchActionDeserializer.java +++ b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/SwitchActionDeserializer.java @@ -7,10 +7,10 @@ package com.powsybl.security.json.action; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.powsybl.commons.json.JsonUtil; import com.powsybl.security.action.SwitchAction; import java.io.IOException; @@ -31,26 +31,31 @@ private static class ParsingContext { } @Override - public SwitchAction deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + public SwitchAction deserialize(JsonParser parser, DeserializationContext deserializationContext) throws IOException { ParsingContext context = new ParsingContext(); - while (jsonParser.nextToken() != JsonToken.END_OBJECT) { - switch (jsonParser.getCurrentName()) { + JsonUtil.parsePolymorphicObject(parser, name -> { + switch (name) { + case "type": + if (!SwitchAction.NAME.equals(parser.nextTextValue())) { + throw JsonMappingException.from(parser, "Expected type " + SwitchAction.NAME); + } + return true; case "id": - context.id = jsonParser.nextTextValue(); - break; + context.id = parser.nextTextValue(); + return true; case "switchId": - context.switchId = jsonParser.nextTextValue(); - break; + context.switchId = parser.nextTextValue(); + return true; case "open": - jsonParser.nextToken(); - context.open = jsonParser.getValueAsBoolean(); - break; + parser.nextToken(); + context.open = parser.getValueAsBoolean(); + return true; default: - throw new IllegalArgumentException(""); + return false; } - } + }); if (context.open == null) { - throw JsonMappingException.from(jsonParser, "for switch action open field can't be null"); + throw JsonMappingException.from(parser, "for switch action open field can't be null"); } return new SwitchAction(context.id, context.switchId, context.open); } diff --git a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/SwitchActionSerializer.java b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/SwitchActionSerializer.java new file mode 100644 index 00000000000..c537c3ca301 --- /dev/null +++ b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/json/action/SwitchActionSerializer.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022, 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/. + */ +package com.powsybl.security.json.action; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.powsybl.security.action.SwitchAction; + +import java.io.IOException; + +/** + * @author Etienne Lesot + */ +public class SwitchActionSerializer extends StdSerializer { + + public SwitchActionSerializer() { + super(SwitchAction.class); + } + + @Override + public void serialize(SwitchAction action, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("type", action.getType()); + jsonGenerator.writeStringField("id", action.getId()); + jsonGenerator.writeStringField("switchId", action.getSwitchId()); + jsonGenerator.writeBooleanField("open", action.isOpen()); + jsonGenerator.writeEndObject(); + } +} diff --git a/security-analysis/security-analysis-api/src/test/java/com/powsybl/security/json/JsonActionAndOperatorStrategyTest.java b/security-analysis/security-analysis-api/src/test/java/com/powsybl/security/json/JsonActionAndOperatorStrategyTest.java index f03073ed350..aebba580b7d 100644 --- a/security-analysis/security-analysis-api/src/test/java/com/powsybl/security/json/JsonActionAndOperatorStrategyTest.java +++ b/security-analysis/security-analysis-api/src/test/java/com/powsybl/security/json/JsonActionAndOperatorStrategyTest.java @@ -6,6 +6,13 @@ */ package com.powsybl.security.json; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.powsybl.commons.AbstractConverterTest; import com.powsybl.iidm.network.ThreeWindingsTransformer; import com.powsybl.security.action.*; @@ -17,15 +24,18 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import static org.junit.Assert.*; -import static org.junit.Assert.assertEquals; /** * @author Etienne Lesot */ public class JsonActionAndOperatorStrategyTest extends AbstractConverterTest { + @Test public void actionRoundTrip() throws IOException { List actions = new ArrayList<>(); @@ -62,4 +72,41 @@ public void wrongActions() { " at [Source: (BufferedInputStream); line: 8, column: 3] (through reference chain: java.util.ArrayList[0])", assertThrows(UncheckedIOException.class, () -> ActionList.readJsonInputStream(inputStream2)).getMessage()); } + + @JsonTypeName(DummyAction.NAME) + static class DummyAction extends AbstractAction { + + static final String NAME = "dummy-action"; + + @JsonCreator + protected DummyAction(@JsonProperty("id") String id) { + super(id); + } + + @JsonProperty(value = "type", access = JsonProperty.Access.READ_ONLY) + @Override + public String getType() { + return NAME; + } + } + + @Test + public void testJsonPlugins() throws JsonProcessingException { + + Module jsonModule = new SimpleModule() + .registerSubtypes(DummyAction.class); + SecurityAnalysisJsonPlugin plugin = () -> List.of(jsonModule); + ObjectMapper mapper = new ObjectMapper() + .registerModule(new SecurityAnalysisJsonModule(List.of(plugin))); + + DummyAction action = new DummyAction("hello"); + ActionList actions = new ActionList(List.of(action)); + String serialized = mapper.writeValueAsString(actions); + ActionList parsed = mapper.readValue(serialized, ActionList.class); + + assertEquals(1, parsed.getActions().size()); + Action parsedAction = parsed.getActions().get(0); + assertTrue(parsedAction instanceof DummyAction); + assertEquals("hello", parsedAction.getId()); + } }