[RM] Feature Implement the ability to define a message structure with cyclic references
MODIFY
BackEnd/Core/sailfish-common/src/main/java/com/exactpro/sf/common/messages/structures/impl/MessageStructure.java
BackEnd/Core/sailfish-common/src/main/java/com/exactpro/sf/common/messages/structures/impl/MessageStructure.java
/*******************************************************************************
- * Copyright 2009-2019 Exactpro (Exactpro Systems Limited)
+ * Copyright 2009-2021 Exactpro (Exactpro Systems Limited)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,19 +17,19 @@ package com.exactpro.sf.common.messages.structures.impl;
import java.util.Collections;
import java.util.Map;
+import java.util.function.Supplier;
import com.exactpro.sf.common.impl.messages.xml.configuration.JavaType;
import com.exactpro.sf.common.messages.structures.IAttributeStructure;
import com.exactpro.sf.common.messages.structures.IFieldStructure;
import com.exactpro.sf.common.messages.structures.IMessageStructure;
import com.exactpro.sf.common.messages.structures.StructureType;
-import com.exactpro.sf.common.util.EPSCommonException;
/**
* This structure should be immutable
*/
public class MessageStructure extends FieldStructure implements IMessageStructure {
- private final Map<String, IFieldStructure> fields;
+ private final Supplier<Map<String, IFieldStructure>> fieldsSupplier;
private final IMessageStructure reference;
public MessageStructure(String name, String namespace, boolean isCollection, IMessageStructure reference) {
@@ -51,11 +51,12 @@ public class MessageStructure extends FieldStructure implements IMessageStructur
super(name, namespace, description, reference != null ? reference.getName() : null, attributes,
null, null, isRequired, isCollection, false, null, StructureType.COMPLEX);
if (fields != null) {
- this.fields = Collections.unmodifiableMap(fields); // no reference, message-to-message reference
+ Map<String, IFieldStructure> map = Collections.unmodifiableMap(fields); // no reference, message-to-message reference
+ fieldsSupplier = () -> map;
} else if (reference != null) {
- this.fields = reference.getFields(); // field-to-message reference
+ fieldsSupplier = reference::getFields; // field-to-message reference
} else {
- this.fields = Collections.emptyMap(); // probably should not happen :(
+ fieldsSupplier = Collections::emptyMap; // probably should not happen :(
}
this.reference = reference;
@@ -64,7 +65,7 @@ public class MessageStructure extends FieldStructure implements IMessageStructur
@Override
public Map<String, IFieldStructure> getFields() {
- return fields;
+ return fieldsSupplier.get();
}
@Override

ADD
BackEnd/Core/sailfish-common/src/main/java/com/exactpro/sf/common/messages/structures/impl/MessageStructurePromise.java
+/*
+ * Copyright 2009-2021 Exactpro (Exactpro Systems Limited)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.exactpro.sf.common.messages.structures.impl;
+
+import static com.exactpro.sf.common.messages.structures.StructureType.COMPLEX;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.jetbrains.annotations.NotNull;
+
+import com.exactpro.sf.common.impl.messages.xml.configuration.JavaType;
+import com.exactpro.sf.common.messages.structures.IAttributeStructure;
+import com.exactpro.sf.common.messages.structures.IFieldStructure;
+import com.exactpro.sf.common.messages.structures.IMessageStructure;
+import com.exactpro.sf.common.messages.structures.StructureType;
+
+public class MessageStructurePromise implements IMessageStructure {
+ private final AtomicReference<IMessageStructure> origin = new AtomicReference<>();
+ private final String name;
+
+ public MessageStructurePromise(@NotNull String name) {
+ this.name = Objects.requireNonNull(name, "Name can't be null");
+ }
+
+ public void setOrigin(@NotNull IMessageStructure messageStructure) {
+ if (!name.equals(messageStructure.getName())) {
+ throw new IllegalArgumentException("Origin message structure has incorrect name, expected '" + name + "', actual '" + messageStructure.getName() + '\'');
+ }
+ if (!origin.compareAndSet(null, Objects.requireNonNull(messageStructure, "Origin message strucure can not be null"))) {
+ throw new IllegalStateException("Origin message for promise '" + getName() + "' already set");
+ }
+ }
+
+ private IMessageStructure getOrigin() {
+ IMessageStructure msgStrucuture = origin.get();
+ if (msgStrucuture == null) {
+ throw new IllegalStateException("Origin message for promise '" + getName() + "' isn't set yet");
+ }
+ return msgStrucuture;
+ }
+
+ @Override
+ public boolean isComplex() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnum() {
+ return false;
+ }
+
+ @Override
+ public boolean isSimple() {
+ return false;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getDescription() {
+ return getOrigin().getDescription();
+ }
+
+ @Override
+ public Map<String, IFieldStructure> getFields() {
+ return getOrigin().getFields();
+ }
+
+ @Override
+ public Map<String, IAttributeStructure> getAttributes() {
+ return getOrigin().getAttributes();
+ }
+
+ @Override
+ public Map<String, IAttributeStructure> getValues() {
+ throw new UnsupportedOperationException("Messages don't have values. Message: " + getName());
+ }
+
+ @Override
+ public StructureType getStructureType() {
+ return COMPLEX;
+ }
+
+ @Override
+ public JavaType getJavaType() {
+ throw new UnsupportedOperationException("Messages don't have a java type. Message: " + getName());
+ }
+
+ @Override
+ public boolean isRequired() {
+ return getOrigin().isRequired();
+ }
+
+ @Override
+ public boolean isCollection() {
+ return getOrigin().isCollection();
+ }
+
+ @Override
+ public boolean isServiceName() {
+ throw new UnsupportedOperationException("Messages don't have a 'serviceName' parameter. Message: " + getName());
+ }
+
+ @Override
+ public <T> T getDefaultValue() {
+ throw new UnsupportedOperationException("Messages don't have a default value. Message: " + getName());
+ }
+
+ @Override
+ public String getNamespace() {
+ return getOrigin().getNamespace();
+ }
+
+ @Override
+ public String getReferenceName() {
+ return getOrigin().getReferenceName();
+ }
+}

MODIFY
BackEnd/Core/sailfish-common/src/main/java/com/exactpro/sf/common/messages/structures/loaders/AbstractDictionaryStructureLoader.java
BackEnd/Core/sailfish-common/src/main/java/com/exactpro/sf/common/messages/structures/loaders/AbstractDictionaryStructureLoader.java
/*******************************************************************************
- * Copyright 2009-2019 Exactpro (Exactpro Systems Limited)
+ * Copyright 2009-2021 Exactpro (Exactpro Systems Limited)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -47,6 +47,7 @@ import com.exactpro.sf.common.messages.structures.impl.AttributeStructure;
import com.exactpro.sf.common.messages.structures.impl.DictionaryStructure;
import com.exactpro.sf.common.messages.structures.impl.FieldStructure;
import com.exactpro.sf.common.messages.structures.impl.MessageStructure;
+import com.exactpro.sf.common.messages.structures.impl.MessageStructurePromise;
import com.exactpro.sf.common.util.EPSCommonException;
import com.sun.istack.NotNull;
@@ -57,9 +58,16 @@ public abstract class AbstractDictionaryStructureLoader implements IDictionarySt
protected final boolean aggregate;
protected final Set<String> pendingMessages = new HashSet<>();
+ protected final Map<String, MessageStructurePromise> promiseMap = new HashMap<>();
protected final Logger logger;
+ private boolean isRecursion;
+ private IField lastMessage;
+ private IFieldStructure messageFinalRecursionField;
+ private String startRecursionMessageName;
+ private boolean isCircledAttribute;
+
public AbstractDictionaryStructureLoader(boolean aggregate) {
this.aggregate = aggregate;
logger = LoggerFactory.getLogger(this.getClass());
@@ -180,7 +188,9 @@ public abstract class AbstractDictionaryStructureLoader implements IDictionarySt
convertMessage(dictionary.getName(), builder, msg);
}
+ startRecursionMessageName = null;
pendingMessages.clear();
+ promiseMap.clear();
}
Map<String, IAttributeStructure> dictAttributes = getDictionaryAttributes(dictionary);
@@ -189,11 +199,14 @@ public abstract class AbstractDictionaryStructureLoader implements IDictionarySt
builder.getMsgStructureMap(), builder.getFieldStructureMap());
}
- private void convertMessage(String namespace, StructureBuilder builder, IMessage message) {
-
+ private IMessageStructure convertMessage(String namespace, StructureBuilder builder, IMessage message) {
if (!pendingMessages.add(message.getId())) {
+ if (message.getReference() instanceof IMessage
+ && message.getReference().getReference() instanceof IMessage) {
throw new EPSCommonException(String.format("Recursion at message id: '%s' has been detected!", message.getId()));
}
+ return promiseMap.computeIfAbsent(message.getId(), (key) -> new MessageStructurePromise(message.getName()));
+ }
IField reference = message.getReference();
@@ -207,8 +220,14 @@ public abstract class AbstractDictionaryStructureLoader implements IDictionarySt
try {
IMessageStructure messageStructure = convertMessageInternal(namespace, builder, message, reference);
-
builder.addMessageStructure(messageStructure);
+
+ MessageStructurePromise promise = promiseMap.get(message.getId());
+ if (promise != null) {
+ promise.setOrigin(messageStructure);
+ }
+
+ return messageStructure;
} catch (RuntimeException e) {
throw new EPSCommonException("Message '" + message.getName() + "', problem with content", e);
}
@@ -322,8 +341,7 @@ public abstract class AbstractDictionaryStructureLoader implements IDictionarySt
struct = builder.getMessageStructure(field.getReference().getName());
if (struct == null) {
- convertMessage(namespace, builder, (IMessage)field.getReference());
- struct = builder.getMessageStructure(field.getReference().getName());
+ struct = convertMessage(namespace, builder, (IMessage)field.getReference());
}
}
@@ -456,6 +474,14 @@ public abstract class AbstractDictionaryStructureLoader implements IDictionarySt
if (aggregate) {
IField reference = field.getReference();
if (reference != null) {
+ if (isRecursion && reference.getName().equals(startRecursionMessageName)) {
+ if (isCircledAttribute) {
+ return;
+ } else {
+ collectFieldAttributesOrValues(reference, target, isValue);
+ }
+ isCircledAttribute = !isCircledAttribute;
+ }
collectFieldAttributesOrValues(reference, target, isValue);
}
}

MODIFY
BackEnd/Core/sailfish-common/src/test/java/com/exactpro/sf/common/impl/messages/TestDictionaryStructureLoader.java
BackEnd/Core/sailfish-common/src/test/java/com/exactpro/sf/common/impl/messages/TestDictionaryStructureLoader.java
/*******************************************************************************
- * Copyright 2009-2019 Exactpro (Exactpro Systems Limited)
+ * Copyright 2009-2021 Exactpro (Exactpro Systems Limited)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,7 +22,6 @@ import static java.util.Collections.singleton;
import java.io.File;
import java.io.FileInputStream;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
@@ -115,7 +114,7 @@ public class TestDictionaryStructureLoader extends EPSTestCase {
Assert.assertEquals(JavaType.JAVA_LANG_INTEGER, dictionary.getMessages().get("MessageHeader").getFields().get("enum").getJavaType());
Assert.assertEquals(JavaType.JAVA_LANG_INTEGER, dictionary.getMessages().get("MessageHeader").getFields().get("MessageLength").getJavaType());
Assert.assertEquals(JavaType.JAVA_LANG_INTEGER, dictionary.getFields().get("enum").getJavaType());
- Assert.assertEquals(true, dictionary.getFields().get("enum").isEnum());
+ Assert.assertTrue(dictionary.getFields().get("enum").isEnum());
Assert.assertEquals(2, dictionary.getFields().get("enum").getValues().size());
}
@@ -175,23 +174,13 @@ public class TestDictionaryStructureLoader extends EPSTestCase {
@Test
public void testMessageCircularReferenceInInheritance() throws Exception {
exception.expect(EPSCommonException.class);
- exception.expectMessage("Recursion at message id: 'Message1Id' has been detected");
+ exception.expectMessage("Recursion at message id: 'Message1Id' has been detected!");
try (InputStream stream = new FileInputStream(path + "message-circular-reference-in-inheritance.xml")) {
new XmlDictionaryStructureLoader().load(stream);
}
}
- @Test
- public void testMessageCircularReferenceThroughInheritance() throws Exception {
- exception.expect(EPSCommonException.class);
- exception.expectMessage("Message 'Message2', problem with content");
-
- try (InputStream stream = new FileInputStream(path + "message-circular-reference-through-inheritance.xml")) {
- new XmlDictionaryStructureLoader().load(stream);
- }
- }
-
@Test
public void testMessageInheritance() throws Exception {
try (InputStream stream = new FileInputStream(path + "message-inheritance.xml")) {
@@ -295,4 +284,21 @@ public class TestDictionaryStructureLoader extends EPSTestCase {
);
}
+
+ @Test
+ public void testMessageCyclicReferences() throws Exception {
+ IDictionaryStructureLoader loader = new XmlDictionaryStructureLoader();
+ IDictionaryStructure dictionaryStructure;
+ try (InputStream inputStream = new FileInputStream(Paths.get(path,"messageCyclicReferences.xml").toFile())) {
+ dictionaryStructure = loader.load(inputStream);
+ }
+ Map<String, IMessageStructure> messages = dictionaryStructure.getMessages();
+ IFieldStructure selfMessage = messages.get("A").getFields().get("self");
+ Assert.assertTrue(selfMessage instanceof IMessageStructure);
+ IFieldStructure fieldFromReference = messages.get("mes2").getFields().get("field2").getFields().get("field3");
+ IFieldStructure fieldStructure = messages.get("mes3").getFields().get("field3");
+ Assert.assertEquals(fieldFromReference, fieldStructure);
+
+
+ }
}
\ No newline at end of file

MODIFY
BackEnd/Core/sailfish-common/src/test/java/com/exactpro/sf/common/impl/messages/TestJsonYamlDictionaryStructureLoader.java
BackEnd/Core/sailfish-common/src/test/java/com/exactpro/sf/common/impl/messages/TestJsonYamlDictionaryStructureLoader.java
/*******************************************************************************
- * Copyright 2009-2019 Exactpro (Exactpro Systems Limited)
+ * Copyright 2009-2021 Exactpro (Exactpro Systems Limited)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -187,11 +187,10 @@ public class TestJsonYamlDictionaryStructureLoader extends EPSTestCase {
@Test
public void testRecursion() throws Exception {
- exception.expect(EPSCommonException.class);
- exception.expectMessage("Message 'self', problem with content");
- exception.expectCause(new CauseMatcher(EPSCommonException.class, "Recursion at message id: 'self' has been detected!"));
- loadDictionaryFrom("recursion.json");
+ IDictionaryStructure dictionaryStructure = loadDictionaryFrom("recursion.json");
+ IFieldStructure selfMessage = dictionaryStructure.getMessages().get("self").getFields().get("s_name");
+ Assert.assertTrue(selfMessage instanceof IMessageStructure);
}
@Test
@@ -249,10 +248,8 @@ public class TestJsonYamlDictionaryStructureLoader extends EPSTestCase {
@Test
public void testMessageCircularReferenceThroughInheritance() throws Exception {
- exception.expect(EPSCommonException.class);
- exception.expectMessage("Message 'Message2', problem with content");
-
- loadDictionaryFrom("message-circular-reference-through-inheritance.json");
+ IDictionaryStructure dictionaryStructure = loadDictionaryFrom("message-circular-reference-through-inheritance.json");
+ Assert.assertTrue(dictionaryStructure.getMessages().get("Message2").getFields().get("Field1") instanceof IMessageStructure);
}
@Test
@@ -375,4 +372,11 @@ public class TestJsonYamlDictionaryStructureLoader extends EPSTestCase {
loadDictionaryFrom("messageInFields.json");
}
+
+ @Test
+ public void testMessageCiclicReferences() throws Exception {
+ IDictionaryStructure dictionary = loadDictionaryFrom("cyclicReferences.json");
+ Assert.assertTrue(dictionary.getMessages().get("A").getFields().get("self") instanceof IMessageStructure);
+ Assert.assertTrue(dictionary.getMessages().get("InnerMessage").getFields().get("circlied") instanceof IMessageStructure);
+ }
}

MODIFY
BackEnd/Core/sailfish-common/src/test/java/com/exactpro/sf/common/xml/ReadConfigTest.java
BackEnd/Core/sailfish-common/src/test/java/com/exactpro/sf/common/xml/ReadConfigTest.java
@@ -218,15 +218,9 @@ public class ReadConfigTest extends EPSTestCase {
File xmlFile = new File(PATH_PREFIX, "recursion.xml");
try (InputStream in = new FileInputStream(xmlFile)) {
- try {
- loader.load(in);
- } catch (EPSCommonException e) {
- Assert.assertEquals("Message 'message_name', problem with content", e.getMessage());
- Assert.assertEquals("Recursion at message id: 'self' has been detected!", e.getCause().getMessage());
- return;
- }
+ IDictionaryStructure dictionaryStructure = loader.load(in);
+ Assert.assertTrue(dictionaryStructure.getMessages().get("message_name").getFields().get("self") instanceof IMessageStructure);
}
- Assert.fail("Incorrect structure has been sucess loaded.");
}
@Test

ADD
BackEnd/Core/sailfish-common/src/test/resources/cyclicReferences.json
+{
+ "name": "CyclicReferences",
+ "messages": {
+ "A": {
+ "fields": {
+ "self": {
+ "reference": "A"
+ }
+ }
+ },
+ "B": {
+ "fields": {
+ "inner": {
+ "reference": "InnerMessage"
+ }
+ }
+ },
+ "InnerMessage": {
+ "fields": {
+ "circlied": {
+ "reference": "B"
+ }
+ }
+ }
+ }
+}

ADD
BackEnd/Core/sailfish-common/src/test/resources/messageCyclicReferences.xml
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Copyright 2009-2021 Exactpro (Exactpro Systems Limited)
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-->
+<dictionary name="messageCyclicReferences"
+ xmlns="http://exactprosystems.com/dictionary">
+ <messages>
+ <message id="M_T" name="T">
+ <field name="F" type="java.lang.String"/>
+ </message>
+ <message id="M_A" name="A">
+ <field name="self" reference="M_A"/>
+ </message>
+ <message id="M_mes1" name="mes1">
+ <field name="field1" reference="M_mes2"/>
+ </message>
+ <message id="M_mes2" name="mes2">
+ <field name="field2" reference="M_mes3"/>
+ </message>
+ <message id="M_mes3" name="mes3">
+ <field name="field3" reference="M_mes2"/>
+ </message>
+ </messages>
+</dictionary>

[RM] Feature #74734. Implement the ability to define a message structure with cyclic references

Change-Id: Ie58a5189fc27cfc2245e5c573d320f0ace1c66a8