diff --git a/ragconnect.base/src/main/jastadd/Analysis.jrag b/ragconnect.base/src/main/jastadd/Analysis.jrag index d4e166a34a2b62150d76dfa0decf3c0a827bb649..4baff8e5763af0b3a1c8870fa1d2b0a935bb9a24 100644 --- a/ragconnect.base/src/main/jastadd/Analysis.jrag +++ b/ragconnect.base/src/main/jastadd/Analysis.jrag @@ -15,6 +15,19 @@ aspect Analysis { } return numberOfSameDefs > 1; } + eq RelationEndpointTarget.isAlreadyDefined() { + // define lookup here, as not used elsewhere + int numberOfSameDefs = 0; + for (EndpointTarget target : ragconnect().givenEndpointTargetList()) { + if (target.isRelationEndpointTarget()) { + RelationEndpointTarget other = target.asRelationEndpointTarget(); + if (other.getRole().equals(this.getRole())) { + numberOfSameDefs += 1; + } + } + } + return numberOfSameDefs > 1; + } eq TokenEndpointTarget.isAlreadyDefined() { return lookupTokenEndpointDefinitions(getToken()).stream() .filter(containingEndpointDefinition()::matchesType) @@ -49,7 +62,8 @@ aspect Analysis { return target.primitivePrettyPrint().equals(this.primitivePrettyPrint()); } syn String JavaTypeUse.primitivePrettyPrint() { - switch (getName()) { + String name = getName(); + switch (name) { case "boolean": case "Boolean": return "boolean"; @@ -78,6 +92,7 @@ aspect Analysis { syn boolean EndpointTarget.hasAttributeResetMethod(); eq AttributeEndpointTarget.hasAttributeResetMethod() = false; + eq RelationEndpointTarget.hasAttributeResetMethod() = false; eq TokenEndpointTarget.hasAttributeResetMethod() = getToken().getNTA(); eq TypeEndpointTarget.hasAttributeResetMethod() = getType().getNTA(); eq ContextFreeTypeEndpointTarget.hasAttributeResetMethod() = false; diff --git a/ragconnect.base/src/main/jastadd/Intermediate.jadd b/ragconnect.base/src/main/jastadd/Intermediate.jadd index 5ca17cdc4ca04db353dbf073fc0cff3d8dd7df93..69ff5d8932cd33a97e44b2fa3562b13cd52617f2 100644 --- a/ragconnect.base/src/main/jastadd/Intermediate.jadd +++ b/ragconnect.base/src/main/jastadd/Intermediate.jadd @@ -59,6 +59,7 @@ aspect SharedMustache { case "warn": case "error": case "exception": + //noinspection DuplicateBranchesInSwitch return "ASTNode." + logConsoleErr(); default: return "unknownLoggingLevelForConsole_" + level + "_"; @@ -74,9 +75,8 @@ aspect SharedMustache { } } syn boolean EndpointTarget.typeIsList() = false; - eq TypeEndpointTarget.typeIsList() { - return getType().isListComponent(); - } + eq TypeEndpointTarget.typeIsList() = getType().isListComponent(); + eq RelationEndpointTarget.typeIsList() = getRole().isListRole(); syn boolean EndpointTarget.typeIsOpt() = false; eq TypeEndpointTarget.typeIsOpt() { @@ -215,6 +215,10 @@ aspect MustacheMappingApplicationAndDefinition { eq MAttributeSendDefinition.preemptiveExpectedValue() = lastValueGetterCall(); eq MAttributeSendDefinition.preemptiveReturn() = "return false;"; + eq MRelationSendDefinition.firstInputVarName() = getterMethodCall(); + eq MRelationSendDefinition.preemptiveExpectedValue() = lastValueGetterCall(); + eq MRelationSendDefinition.preemptiveReturn() = "return false;"; + eq MTokenReceiveDefinition.firstInputVarName() = "message"; eq MTokenReceiveDefinition.preemptiveExpectedValue() = getterMethodCall(); eq MTokenReceiveDefinition.preemptiveReturn() = "return;"; @@ -425,6 +429,15 @@ aspect MustacheReceiveAndSendAndHandleUri { eq AttributeEndpointTarget.parentTypeName() = getParentTypeDecl().getName(); eq AttributeEndpointTarget.entityName() = getName(); + eq RelationEndpointTarget.getterMethodName() = forwardingNTA_Name(); + eq RelationEndpointTarget.parentTypeName() = getRole().getType().getName(); + eq RelationEndpointTarget.entityName() = getRole().getName(); + eq RelationEndpointTarget.realGetterMethodName() = "get" + getRole().getterMethodName(); + eq RelationEndpointTarget.realGetterMethodCall() = realGetterMethodName() + (containingEndpointDefinition().indexedSend() ? "(index)" : "()"); + + syn String NavigableRole.getterMethodName() = getName(); + eq ListRole.getterMethodName() = getName() + "List"; + eq TokenEndpointTarget.getterMethodName() = "get" + getToken().getName(); eq TokenEndpointTarget.parentTypeName() = getToken().containingTypeDecl().getName(); eq TokenEndpointTarget.entityName() = getToken().getName(); @@ -481,6 +494,8 @@ aspect MustacheSendDefinition { syn String EndpointDefinition.forwardingNTA_Name() = getEndpointTarget().forwardingNTA_Name(); syn String EndpointDefinition.forwardingNTA_Type() = getEndpointTarget().forwardingNTA_Type(); + syn boolean EndpointDefinition.relationEndpointWithListRole() = getEndpointTarget().relationEndpointWithListRole(); + syn String EndpointDefinition.senderName() = getEndpointTarget().senderName(); syn boolean EndpointDefinition.shouldNotResetValue() = getSend() && !getEndpointTarget().hasAttributeResetMethod(); @@ -494,13 +509,17 @@ aspect MustacheSendDefinition { // === attributes needed for computing above ones === syn boolean EndpointTarget.needForwardingNTA() = false; eq TypeEndpointTarget.needForwardingNTA() = containingEndpointDefinition().getSend() && !getType().getNTA(); + eq RelationEndpointTarget.needForwardingNTA() = containingEndpointDefinition().getSend(); - syn String EndpointTarget.forwardingNTA_Name() = null; + syn String EndpointTarget.forwardingNTA_Name() = null; // only needed, if needForwardingNTA evaluates to true eq TypeEndpointTarget.forwardingNTA_Name() = ragconnect().internalRagConnectPrefix() + getType().getName(); + eq RelationEndpointTarget.forwardingNTA_Name() = ragconnect().internalRagConnectPrefix() + getRole().getName(); - syn String EndpointTarget.forwardingNTA_Type() = null; + syn String EndpointTarget.forwardingNTA_Type() = null; // only needed, if needForwardingNTA evaluates to true eq TypeEndpointTarget.forwardingNTA_Type() = getType().forwardingNTA_Type( containingEndpointDefinition().getIndexBasedListAccess()); + eq RelationEndpointTarget.forwardingNTA_Type() = getRole().forwardingNTA_Type( +containingEndpointDefinition().getIndexBasedListAccess()); syn String TypeComponent.forwardingNTA_Type(boolean indexBasedListAccess); eq NormalComponent.forwardingNTA_Type(boolean indexBasedListAccess) = getTypeDecl().getName(); @@ -510,6 +529,14 @@ aspect MustacheSendDefinition { getTypeDecl().getName() : ragconnect().configJastAddList() + "<" + getTypeDecl().getName() + ">"; + syn String Role.forwardingNTA_Type(boolean indexBasedListAccess) = oppositeRole().getType().getName(); + eq ListRole.forwardingNTA_Type(boolean indexBasedListAccess) = indexBasedListAccess ? + oppositeRole().getType().getName() : + "java.util.List<" + oppositeRole().getType().getName() + ">"; + + syn boolean EndpointTarget.relationEndpointWithListRole() = false; + eq RelationEndpointTarget.relationEndpointWithListRole() = getRole().isListRole(); + syn String EndpointTarget.senderName() = ragconnect().internalRagConnectPrefix() + "_sender_" + entityName(); eq ContextFreeTypeEndpointTarget.senderName() = null; @@ -519,6 +546,9 @@ aspect MustacheSendDefinition { eq MAttributeSendDefinition.updateMethodName() = ragconnect().internalRagConnectPrefix() + "_update_attr_" + getEndpointDefinition().entityName(); eq MAttributeSendDefinition.writeMethodName() = ragconnect().internalRagConnectPrefix() + "_writeLastValue_attr_" + getEndpointDefinition().entityName(); + eq MRelationSendDefinition.updateMethodName() = ragconnect().internalRagConnectPrefix() + "_update_" + getEndpointDefinition().entityName(); + eq MRelationSendDefinition.writeMethodName() = ragconnect().internalRagConnectPrefix() + "_writeLastValue_" + getEndpointDefinition().entityName(); + eq MTokenReceiveDefinition.updateMethodName() = null; eq MTokenReceiveDefinition.writeMethodName() = null; @@ -641,6 +671,12 @@ aspect AttributesForMustache { } return new MAttributeSendDefinition(); } + MEndpointDefinition RelationEndpointTarget.createMEndpointDefinition(boolean isSend) { + if (!isSend) { + throw new IllegalArgumentException("RelationEndpointTarget can only be sent!"); + } + return new MRelationSendDefinition(); + } MEndpointDefinition TokenEndpointTarget.createMEndpointDefinition(boolean isSend) { return isSend ? new MTokenSendDefinition() : new MTokenReceiveDefinition(); } diff --git a/ragconnect.base/src/main/jastadd/Intermediate.relast b/ragconnect.base/src/main/jastadd/Intermediate.relast index 486158a4a4094291a756cf8c7715ed6104f5e252..8fa32f92b7bc1fdc0b6fad196b82523a660dc6c2 100644 --- a/ragconnect.base/src/main/jastadd/Intermediate.relast +++ b/ragconnect.base/src/main/jastadd/Intermediate.relast @@ -2,6 +2,7 @@ abstract MEndpointDefinition ::= InnerMappingDefinition:MInnerMappingDefinition* rel MEndpointDefinition.EndpointDefinition -> EndpointDefinition; MAttributeSendDefinition : MEndpointDefinition; +MRelationSendDefinition : MEndpointDefinition; abstract MTokenEndpointDefinition : MEndpointDefinition; MTokenReceiveDefinition : MTokenEndpointDefinition; MTokenSendDefinition : MTokenEndpointDefinition; diff --git a/ragconnect.base/src/main/jastadd/Mappings.jrag b/ragconnect.base/src/main/jastadd/Mappings.jrag index aa070695a83e31f819d62308b094679b27125a8e..9fd55aa4cad7eb22f75913f7942b547ca91ddec2 100644 --- a/ragconnect.base/src/main/jastadd/Mappings.jrag +++ b/ragconnect.base/src/main/jastadd/Mappings.jrag @@ -1,10 +1,18 @@ aspect DefaultMappings { private String RagConnect.baseDefaultMappingTypeNamePart(String typeName) { - return capitalize(typeName).replace("[]", "s").replace("<", "").replace(">", "List"); + if (typeName.contains(".")) { + StringBuilder sb = new StringBuilder(); + for (String part : typeName.split("\\.")) { + sb.append(baseDefaultMappingTypeNamePart(part)); + } + return sb.toString(); + } else { + return capitalize(typeName).replace("[]", "s").replace("<", "").replace(">", "List"); + } } - private MappingDefinitionType RagConnect.baseDefaultMappingTypeFromName(String typeName) { + private MappingDefinitionType RagConnect.baseDefaultMappingTypeName(String typeName) { return typeName.endsWith("[]") ? new JavaArrayMappingDefinitionType(new SimpleJavaTypeUse(typeName.replace("[]", ""))) : new JavaMappingDefinitionType(new SimpleJavaTypeUse(typeName)); @@ -13,9 +21,9 @@ aspect DefaultMappings { private DefaultMappingDefinition RagConnect.createDefaultMappingDefinition(String prefix, String fromTypeName, String toTypeName, String content) { DefaultMappingDefinition result = new DefaultMappingDefinition(); result.setID(prefix + baseDefaultMappingTypeNamePart(fromTypeName) + "To" + baseDefaultMappingTypeNamePart(toTypeName) + "Mapping"); - result.setFromType(baseDefaultMappingTypeFromName(fromTypeName)); + result.setFromType(baseDefaultMappingTypeName(fromTypeName)); result.setFromVariableName("input"); - result.setToType(baseDefaultMappingTypeFromName(toTypeName)); + result.setToType(baseDefaultMappingTypeName(toTypeName)); result.setContent(content); return result; } @@ -67,7 +75,7 @@ aspect DefaultMappings { ); } - syn nta DefaultMappingDefinition RagConnect.defaultBytesToListTreeMapping(String typeName) { + syn nta DefaultMappingDefinition RagConnect.defaultBytesToListMapping(String typeName) { return treeDefaultMappingDefinition("byte[]", configJastAddList() + "<" + typeName + ">", "String content = new String(input);\n" + "com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();\n" + @@ -78,7 +86,7 @@ aspect DefaultMappings { "return result;" ); } - syn nta DefaultMappingDefinition RagConnect.defaultListTreeToBytesMapping() { + syn nta DefaultMappingDefinition RagConnect.defaultListToBytesMapping() { return treeDefaultMappingDefinition(configJastAddList(), "byte[]", "java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream();\n" + "com.fasterxml.jackson.core.JsonFactory factory = new com.fasterxml.jackson.core.JsonFactory();\n" + @@ -88,6 +96,16 @@ aspect DefaultMappings { "return outputStream.toString().getBytes();" ); } + syn nta DefaultMappingDefinition RagConnect.defaultJavaUtilListToBytesMapping() { + return treeDefaultMappingDefinition("java.util.List", "byte[]", + "java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream();\n" + + "com.fasterxml.jackson.core.JsonFactory factory = new com.fasterxml.jackson.core.JsonFactory();\n" + + "com.fasterxml.jackson.core.JsonGenerator generator = factory.createGenerator(outputStream, com.fasterxml.jackson.core.JsonEncoding.UTF8);\n" + + "serializeJavaUtilList(input, generator);\n" + + "generator.flush();\n" + + "return outputStream.toString().getBytes();" + ); + } syn nta DefaultMappingDefinition RagConnect.defaultBooleanToBytesMapping() = baseDefaultMappingDefinition( "boolean", "byte[]", "return java.nio.ByteBuffer.allocate(1).put((byte) (input ? 1 : 0)).array();"); @@ -165,6 +183,10 @@ aspect Mappings { } // --- isPrimitiveType --- + syn boolean EndpointDefinition.isPrimitiveType() = getEndpointTarget().isPrimitiveType(); + syn boolean EndpointTarget.isPrimitiveType() = false; + eq TokenEndpointTarget.isPrimitiveType() = getToken().isPrimitiveType(); + eq AttributeEndpointTarget.isPrimitiveType() = new SimpleJavaTypeUse(getTypeName()).isPrimitiveType(); syn boolean TokenComponent.isPrimitiveType() = effectiveJavaTypeUse().isPrimitiveType(); syn boolean JavaTypeUse.isPrimitiveType() = false; eq SimpleJavaTypeUse.isPrimitiveType() { @@ -218,7 +240,7 @@ aspect Mappings { default: try { TypeDecl typeDecl = program().resolveTypeDecl(targetTypeName()); - return getEndpointTarget().isTypeEndpointTarget() && typeIsList() && !getIndexBasedListAccess() ? ragconnect().defaultBytesToListTreeMapping(typeDecl.getName()) : ragconnect().defaultBytesToTreeMapping(typeDecl.getName()); + return getEndpointTarget().isTypeEndpointTarget() && typeIsList() && !getIndexBasedListAccess() ? ragconnect().defaultBytesToListMapping(typeDecl.getName()) : ragconnect().defaultBytesToTreeMapping(typeDecl.getName()); } catch (Exception ignore) { } System.err.println("Could not find suitable default mapping for " + targetTypeName() + " on " + this); @@ -254,7 +276,10 @@ aspect Mappings { return ragconnect().defaultStringToBytesMapping(); default: if (getEndpointTarget().isTypeEndpointTarget() && typeIsList() && !getIndexBasedListAccess()) { - return ragconnect().defaultListTreeToBytesMapping(); + return ragconnect().defaultListToBytesMapping(); + } + if (getEndpointTarget().isRelationEndpointTarget() && typeIsList() && !getIndexBasedListAccess()) { + return ragconnect().defaultJavaUtilListToBytesMapping(); } try { TypeDecl typeDecl = program().resolveTypeDecl(targetTypeName()); @@ -281,10 +306,14 @@ aspect Mappings { } syn String EndpointTarget.targetTypeName(); eq AttributeEndpointTarget.targetTypeName() = getTypeName(); + eq RelationEndpointTarget.targetTypeName() = getRole().oppositeRole().targetTypeName(); eq TokenEndpointTarget.targetTypeName() = getToken().effectiveJavaTypeUse().getName(); eq TypeEndpointTarget.targetTypeName() = getType().getTypeDecl().getName(); eq ContextFreeTypeEndpointTarget.targetTypeName() = getTypeDecl().getName(); + syn String Role.targetTypeName() = getType().getName(); + eq ListRole.targetTypeName() = "java.util.List<" + getType().getName() + ">"; + // eq ReceiveFromRestDefinition.suitableDefaultMapping() { // String typeName = getMappingList().isEmpty() ? // getToken().getJavaTypeUse().getName() : @@ -360,9 +389,10 @@ aspect Mappings { for (TypeDecl typeDecl : getProgram().typeDecls()) { result.add(defaultBytesToTreeMapping(typeDecl.getName())); result.add(defaultTreeToBytesMapping(typeDecl.getName())); - result.add(defaultBytesToListTreeMapping(typeDecl.getName())); + result.add(defaultBytesToListMapping(typeDecl.getName())); } - result.add(defaultListTreeToBytesMapping()); + result.add(defaultListToBytesMapping()); + result.add(defaultJavaUtilListToBytesMapping()); // // string conversion // result.add(defaultStringToBooleanMapping()); // result.add(defaultStringToIntMapping()); diff --git a/ragconnect.base/src/main/jastadd/NameResolution.jrag b/ragconnect.base/src/main/jastadd/NameResolution.jrag index 6d9b1a78abb66f3628d66c259d92f1e90dce94b4..0690eca4a260b5f7396dfe2ca6905b3aa98e0aa5 100644 --- a/ragconnect.base/src/main/jastadd/NameResolution.jrag +++ b/ragconnect.base/src/main/jastadd/NameResolution.jrag @@ -146,4 +146,39 @@ aspect RagConnectNameResolution { return null; } + // rel ___ -> Navigable + refine RefResolverStubs eq ASTNode.globallyResolveNavigableRoleByToken(String id) { + NavigableRole result = tryGloballyResolveNavigableRoleByToken(id); + if (result == null) { + System.err.println("Could not resolve role '" + id + "'."); + } + return result; + } + syn NavigableRole ASTNode.tryGloballyResolveNavigableRoleByToken(String id) { + // id is of the form 'type_name + "." + role_name' + int dotIndex = id.indexOf("."); + String typeName = id.substring(0, dotIndex); + String roleName = id.substring(dotIndex + 1); + for (Relation relation : program().relations()) { + if (relation.isDirectedRelation()) { + if (relation.asDirectedRelation().getSource().matches(typeName, roleName)) { + return relation.asDirectedRelation().getSource(); + } + } else { + if (relation.asBidirectionalRelation().getLeft().matches(typeName, roleName)) { + return relation.asBidirectionalRelation().getLeft(); + } + if (relation.asBidirectionalRelation().getRight().matches(typeName, roleName)) { + return relation.asBidirectionalRelation().getRight(); + } + } + } + return null; + } + + syn boolean Role.matches(String typeName, String roleName) = false; + eq NavigableRole.matches(String typeName, String roleName) { + return getType().getName().equals(typeName) && getName().equals(roleName); + } + } diff --git a/ragconnect.base/src/main/jastadd/Navigation.jrag b/ragconnect.base/src/main/jastadd/Navigation.jrag index dac193142b1382f01b61e696a0f9d5b02dd8b038..5288a9f14f2cc8e3298d3d20fa53076fd72be09e 100644 --- a/ragconnect.base/src/main/jastadd/Navigation.jrag +++ b/ragconnect.base/src/main/jastadd/Navigation.jrag @@ -1,4 +1,4 @@ -aspect NewStuff { +aspect GeneratedNavigation { /** Tests if EndpointTarget is a TokenEndpointTarget. * @return 'true' if this is a TokenEndpointTarget, otherwise 'false' @@ -30,6 +30,12 @@ aspect NewStuff { syn boolean EndpointTarget.isAttributeEndpointTarget() = false; eq AttributeEndpointTarget.isAttributeEndpointTarget() = true; + /** Tests if EndpointTarget is a RelationEndpointTarget. + * @return 'true' if this is a RelationEndpointTarget, otherwise 'false' + */ + syn boolean EndpointTarget.isRelationEndpointTarget() = false; + eq RelationEndpointTarget.isRelationEndpointTarget() = true; + /** casts a EndpointTarget into a TokenEndpointTarget if possible. * @return 'this' cast to a TokenEndpointTarget or 'null' */ @@ -64,6 +70,13 @@ aspect NewStuff { syn AttributeEndpointTarget EndpointTarget.asAttributeEndpointTarget(); eq EndpointTarget.asAttributeEndpointTarget() = null; eq AttributeEndpointTarget.asAttributeEndpointTarget() = this; + + /** casts a EndpointTarget into a RelationEndpointTarget if possible. + * @return 'this' cast to a RelationEndpointTarget or 'null' + */ + syn RelationEndpointTarget EndpointTarget.asRelationEndpointTarget(); + eq EndpointTarget.asRelationEndpointTarget() = null; + eq RelationEndpointTarget.asRelationEndpointTarget() = this; } aspect RagConnectNavigation { @@ -121,6 +134,13 @@ aspect RagConnectNavigation { // --- effectiveJavaTypeUse (should be in preprocessor) --- syn lazy JavaTypeUse TokenComponent.effectiveJavaTypeUse() = hasJavaTypeUse() ? getJavaTypeUse() : new SimpleJavaTypeUse("String"); + // --- oppositeRole --- + inh Role Role.oppositeRole(); + eq DirectedRelation.getSource().oppositeRole() = getTarget(); + eq DirectedRelation.getTarget().oppositeRole() = getSource(); + eq BidirectionalRelation.getLeft().oppositeRole() = getRight(); + eq BidirectionalRelation.getRight().oppositeRole() = getLeft(); + // --- isDefaultMappingDefinition --- syn boolean MappingDefinition.isDefaultMappingDefinition() = false; eq DefaultMappingDefinition.isDefaultMappingDefinition() = true; diff --git a/ragconnect.base/src/main/jastadd/RagConnect.relast b/ragconnect.base/src/main/jastadd/RagConnect.relast index 5605324e99fd6206f4dc4c7eb3a92da3342e224d..5037cff1ffa0a7c803ce0d74207637417b1afbda 100644 --- a/ragconnect.base/src/main/jastadd/RagConnect.relast +++ b/ragconnect.base/src/main/jastadd/RagConnect.relast @@ -13,12 +13,11 @@ TypeEndpointTarget : EndpointTarget; rel TypeEndpointTarget.Type <-> TypeComponent.TypeEndpointTarget*; ContextFreeTypeEndpointTarget : EndpointTarget; rel ContextFreeTypeEndpointTarget.TypeDecl <-> TypeDecl.ContextFreeTypeEndpointTarget*; -UntypedEndpointTarget : EndpointTarget ::= <TypeName> <ChildName> <IsAttribute:boolean>; // only used by parser -// to be integrated: AttributeEndpointTarget : EndpointTarget ::= <Name> <TypeName> ; rel AttributeEndpointTarget.ParentTypeDecl <-> TypeDecl.AttributeEndpointTarget*; -//RelationEndpointTarget : EndpointTarget ; -//rel RelationEndpointTarget.Role <-> Role.RelationEndpointTarget* ; +RelationEndpointTarget : EndpointTarget ; +rel RelationEndpointTarget.Role <-> NavigableRole.RelationEndpointTarget* ; +UntypedEndpointTarget : EndpointTarget ::= <TypeName> <ChildName> <IsAttribute:boolean>; // only used by parser DependencyDefinition ::= <ID>; rel DependencyDefinition.Source <-> TokenComponent.DependencySourceDefinition*; diff --git a/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag b/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag index 04e8cadc459653830e5f29e2d189b1aa2c6c0ad5..70f778c7b8f63b86d2d60bf0d71d1beb6cdeb41e 100644 --- a/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag +++ b/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag @@ -16,6 +16,14 @@ aspect ParserRewrites { return result; } + when (getChildName() != null && tryGloballyResolveNavigableRoleByToken(combinedName()) != null) + to RelationEndpointTarget { + RelationEndpointTarget result = new RelationEndpointTarget(); + result.copyOtherValuesFrom(this); + result.setRole(NavigableRole.createRef(this.combinedName())); + return result; + } + when (getChildName() == "") to ContextFreeTypeEndpointTarget { ContextFreeTypeEndpointTarget result = new ContextFreeTypeEndpointTarget(); diff --git a/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java b/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java index dfe3f9789681b3dee17c7fec7b2c671ef400808c..f671349843dddf5e81099660365668fbf5ae4c66 100644 --- a/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java +++ b/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java @@ -124,9 +124,7 @@ public class Compiler extends AbstractCompiler { compiler.run(args); } catch (CompilerException e) { System.err.println(e.getMessage()); - if (compiler.isVerbose()) { - e.printStackTrace(); - } + e.printStackTrace(); System.exit(1); } } diff --git a/ragconnect.base/src/main/resources/ListAspect.mustache b/ragconnect.base/src/main/resources/ListAspect.mustache index 31eaf5ac82a5854956077403e92124c9b6160353..e7ec88e5e579e7a7b50bbeb83d0779bc64203991 100644 --- a/ragconnect.base/src/main/resources/ListAspect.mustache +++ b/ragconnect.base/src/main/resources/ListAspect.mustache @@ -11,6 +11,19 @@ public void {{configJastAddList}}.serialize(com.fasterxml.jackson.core.JsonGener } } +protected static <T extends ASTNode> void ASTNode.serializeJavaUtilList( + java.util.List<T> input, com.fasterxml.jackson.core.JsonGenerator g) throws SerializationException { + try { + g.writeStartArray(); + for (T child : input) { + child.serialize(g); + } + g.writeEndArray(); + } catch (java.io.IOException e) { + throw new SerializationException("unable to serialize list", e); + } +} + {{#typesForReceivingListEndpoints}} public static {{configJastAddList}}<{{Name}}> {{Name}}.deserializeList(com.fasterxml.jackson.databind.node.ArrayNode node) throws DeserializationException { {{configJastAddList}}<{{Name}}> result = new {{configJastAddList}}<>(); diff --git a/ragconnect.base/src/main/resources/mappingApplication.mustache b/ragconnect.base/src/main/resources/mappingApplication.mustache index 836e8d438397db9fc8e862c828e5b57eeb34d2f1..482d54783782ada82cacd9ce75b81d30c1e21e68 100644 --- a/ragconnect.base/src/main/resources/mappingApplication.mustache +++ b/ragconnect.base/src/main/resources/mappingApplication.mustache @@ -1,3 +1,10 @@ +{{#Send}} +{{^PrimitiveType}} +if ({{firstInputVarName}} == null) { + {{preemptiveReturn}} +} +{{/PrimitiveType}} +{{/Send}} {{{lastDefinitionToType}}} {{lastResult}}; try { {{#innerMappingDefinitions}} diff --git a/ragconnect.base/src/main/resources/sendDefinition.mustache b/ragconnect.base/src/main/resources/sendDefinition.mustache index b066c690444a48cb8aadd64ccdd2dcdc2a46f78d..1e66fdedcc7620aefe31a89fcece47dbcf9708ac 100644 --- a/ragconnect.base/src/main/resources/sendDefinition.mustache +++ b/ragconnect.base/src/main/resources/sendDefinition.mustache @@ -14,7 +14,13 @@ public boolean {{parentTypeName}}.{{connectMethodName}}(String {{connectParamete {{#configLoggingEnabledForWrites}} {{logDebug}}("[Send] {{entityName}} = {{log_}} -> {{log_}}", {{getterMethodCall}}, {{connectParameterName}}); {{/configLoggingEnabledForWrites}} - handler.publish(topic, {{lastValueGetterCall}}); + if ({{lastValueGetterCall}} != null) { + handler.publish(topic, {{lastValueGetterCall}}); + {{#configLoggingEnabledForWrites}} + } else { + {{logWarn}}("[Send] {{entityName}} -> {{log_}}: can't send null.", {{connectParameterName}}); + {{/configLoggingEnabledForWrites}} + } }{{#IndexBasedListAccess}}, index{{/IndexBasedListAccess}}, connectToken); {{updateMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}}); if (writeCurrentValue) { @@ -117,5 +123,20 @@ protected void {{parentTypeName}}.{{writeMethodName}}({{#IndexBasedListAccess}}i } {{#needForwardingNTA}} -syn {{{forwardingNTA_Type}}} {{parentTypeName}}.{{forwardingNTA_Name}}({{#IndexBasedListAccess}}int index{{/IndexBasedListAccess}}) = {{realGetterMethodCall}}.{{touchedTerminalsMethodName}}(); +syn {{{forwardingNTA_Type}}} {{parentTypeName}}.{{forwardingNTA_Name}}({{#IndexBasedListAccess}}int index{{/IndexBasedListAccess}}) { +{{#relationEndpointWithListRole}} +// for (var element : {{realGetterMethodCall}}) { +// element.{{touchedTerminalsMethodName}}(); +// } + {{realGetterMethodCall}}.stream().forEach(element -> element.{{touchedTerminalsMethodName}}()); + return {{realGetterMethodCall}}; +{{/relationEndpointWithListRole}} +{{^relationEndpointWithListRole}} + {{{forwardingNTA_Type}}} result = {{realGetterMethodCall}}; + if (result == null) { + return null; + } + return result.{{touchedTerminalsMethodName}}(); +{{/relationEndpointWithListRole}} +} {{/needForwardingNTA}} diff --git a/ragconnect.tests/build.gradle b/ragconnect.tests/build.gradle index 468c22acdfb055d1820c30158049248e4e0b98be..821617bef166830e8c3710b81034a6e962073ee1 100644 --- a/ragconnect.tests/build.gradle +++ b/ragconnect.tests/build.gradle @@ -151,6 +151,8 @@ def JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL = JASTADD_INCREMENTAL_OPTIONS.clone JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL.set(0, '--tracing=all') JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL.set(1, '--incremental=param,debug') +classes.dependsOn(':ragconnect.base:jar') + // --- Test: Example --- task compileExampleTest(type: RagConnectTest) { ragconnect { @@ -657,7 +659,30 @@ task compileAttributeIncremental(type: RagConnectTest) { extraOptions = JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL } } -compileAttributeIncremental.outputs.upToDateWhen { false } + +// --- Test: relation-incremental --- +task compileRelationIncremental(type: RagConnectTest) { + ragconnect { + outputDir = file('src/test/02-after-ragconnect/relationInc') + inputFiles = [file('src/test/01-input/relation/Test.relast'), + file('src/test/01-input/relation/Test.connect')] + rootNode = 'Root' + logWrites = true + extraOptions = defaultRagConnectOptionsAnd(['--experimental-jastadd-329']) + } + relast { + useJastAddNames = true + grammarName = 'src/test/03-after-relast/relationInc/relationInc' + serializer = 'jackson' + } + jastadd { + jastAddList = 'JastAddList' + packageName = 'relationInc.ast' + inputFiles = [file('src/test/01-input/relation/Test.jadd')] + extraOptions = JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL + } +} +compileRelationIncremental.outputs.upToDateWhen { false } static ArrayList<String> defaultRagConnectOptionsAnd(ArrayList<String> options = []) { if (!options.contains('--logTarget=slf4j')) { diff --git a/ragconnect.tests/src/test/01-input/relation/README.md b/ragconnect.tests/src/test/01-input/relation/README.md new file mode 100644 index 0000000000000000000000000000000000000000..12556e1ad76171b40a2e4a8e58717dfa41d4c52b --- /dev/null +++ b/ragconnect.tests/src/test/01-input/relation/README.md @@ -0,0 +1,3 @@ +# Relation + +Idea: Use send definitions for relations. diff --git a/ragconnect.tests/src/test/01-input/relation/Test.connect b/ragconnect.tests/src/test/01-input/relation/Test.connect new file mode 100644 index 0000000000000000000000000000000000000000..61e81d25e3a3bcb898a59112340402c7f21ecccc --- /dev/null +++ b/ragconnect.tests/src/test/01-input/relation/Test.connect @@ -0,0 +1,43 @@ +send SenderRoot.MyA; +send SenderRoot.OptionalA; +send SenderRoot.ManyA; + +send SenderRoot.BiMyA; +send SenderRoot.BiOptionalA; +send SenderRoot.BiManyA; + +send SenderRoot.MyB using ConcatValues; +send SenderRoot.OptionalB using ConcatValues; +send SenderRoot.ManyB using ConcatValueList; + +send SenderRoot.BiMyB using ConcatValues; +send SenderRoot.BiOptionalB using ConcatValues; +send SenderRoot.BiManyB using ConcatValueList; + +ConcatValues maps B b to String {: + return b.getValue() + "+" + b.getInner().getInnerValue(); +:} + +ConcatValueList maps java.util.List<B> list to String {: + StringBuilder sb = new StringBuilder(); + for (B b : list) { + sb.append(b.getValue() + "+" + b.getInner().getInnerValue()).append(";"); + } + return sb.toString(); +:} + +receive ReceiverRoot.FromMyA; +receive ReceiverRoot.FromOptionalA; +receive ReceiverRoot.FromManyA; + +receive ReceiverRoot.FromBiMyA; +receive ReceiverRoot.FromBiOptionalA; +receive ReceiverRoot.FromBiManyA; + +receive ReceiverRoot.FromMyB; +receive ReceiverRoot.FromOptionalB; +receive ReceiverRoot.FromManyB; + +receive ReceiverRoot.FromBiMyB; +receive ReceiverRoot.FromBiOptionalB; +receive ReceiverRoot.FromBiManyB; diff --git a/ragconnect.tests/src/test/01-input/relation/Test.jadd b/ragconnect.tests/src/test/01-input/relation/Test.jadd new file mode 100644 index 0000000000000000000000000000000000000000..ee797705faeb4814f881f9778a539c508402d77b --- /dev/null +++ b/ragconnect.tests/src/test/01-input/relation/Test.jadd @@ -0,0 +1,36 @@ +aspect Computation { +} +aspect MakeCodeCompile { + +} +aspect MakeCodeWork { +} +aspect NameResolution { + // overriding customID guarantees to produce the same JSON representation for equal lists + // otherwise, the value for id is different each time + @Override + protected String A.customID() { + return getClass().getSimpleName() + getValue(); + } + @Override + protected String B.customID() { + return getClass().getSimpleName() + getValue(); + } + @Override + protected String Inner.customID() { + return getClass().getSimpleName() + getInnerValue(); + } + + // override resolving of SenderRoot for As in ReceiverRoot + refine RefResolverStubs eq ASTNode.globallyResolveSenderRootByToken(String id) = getParent() != null && !getParent().isListWithoutParent() ? resolveSenderRootInh(id) : null; + + syn boolean ASTNode.isList() = false; + eq JastAddList.isList() = true; + syn boolean ASTNode.isListWithoutParent() = isList() && getParent() == null; + + inh SenderRoot ASTNode.resolveSenderRootInh(String id); + eq SenderRoot.getChild().resolveSenderRootInh(String id) = globallyResolveSenderRootByToken(id); + eq ReceiverRoot.getChild().resolveSenderRootInh(String id) = getFakeSenderRoot(id); + + syn nta SenderRoot ReceiverRoot.getFakeSenderRoot(String id) = new SenderRoot(); +} diff --git a/ragconnect.tests/src/test/01-input/relation/Test.relast b/ragconnect.tests/src/test/01-input/relation/Test.relast new file mode 100644 index 0000000000000000000000000000000000000000..7effc867e0f5f45f444ac87607be607f1a23e31a --- /dev/null +++ b/ragconnect.tests/src/test/01-input/relation/Test.relast @@ -0,0 +1,28 @@ +Root ::= SenderRoot* ReceiverRoot; +SenderRoot ::= A* B* ; +rel SenderRoot.MyA -> A; +rel SenderRoot.OptionalA? -> A; +rel SenderRoot.ManyA* -> A; + +rel SenderRoot.BiMyA <-> A.ToMyA?; +rel SenderRoot.BiOptionalA? <-> A.ToOptionalA?; +rel SenderRoot.BiManyA* <-> A.ToManyA?; + +rel SenderRoot.MyB -> B; +rel SenderRoot.OptionalB? -> B; +rel SenderRoot.ManyB* -> B; + +rel SenderRoot.BiMyB <-> B.ToMyB?; +rel SenderRoot.BiOptionalB? <-> B.ToOptionalB?; +rel SenderRoot.BiManyB* <-> B.ToManyB?; + +ReceiverRoot ::= +FromMyA:A FromOptionalA:A FromManyA:A* +FromBiMyA:A FromBiOptionalA:A FromBiManyA:A* +<FromMyB:String> <FromOptionalB:String> <FromManyB:String> +<FromBiMyB:String> <FromBiOptionalB:String> <FromBiManyB:String> +; + +A ::= <Value> Inner ; +B ::= <Value> Inner ; +Inner ::= <InnerValue> ; diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java index a3394ea07518bd5ca668fee774c7702ef5334c35..655d61e1710014d37c7aeaa3fdcc140995568f9d 100644 --- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java @@ -3,6 +3,7 @@ package org.jastadd.ragconnect.tests; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; +import org.assertj.core.groups.Tuple; import org.awaitility.Awaitility; import org.awaitility.core.ConditionFactory; import org.jastadd.ragconnect.compiler.Compiler; @@ -20,6 +21,8 @@ import java.nio.file.Paths; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -155,6 +158,134 @@ public class TestUtils { event, node, attribute, params, value); } + public static class TestChecker { + static class ActualAndExpected<T> { + Callable<T> actual; + T expected; + BiConsumer<String, T> customCheck; + + ActualAndExpected(String key) { + expected = null; + } + + void check(String name) { + if (customCheck != null) { + customCheck.accept(name, expected); + return; + } + if (actual == null) { + fail("No actual getter defined for " + name); + } + T actualValue = null; + try { + actualValue = this.actual.call(); + } catch (Exception e) { + fail(e); + } + assertThat(actualValue).as(name).isEqualTo(expected); + } + + void checkAwait(String name) { + if (customCheck != null) { + logger.warn("Custom check set for {}. Can't await for that.", name); + customCheck.accept(name, expected); + return; + } + if (actual == null) { + fail("No actual getter defined for " + name); + } + awaitMqtt().alias(name).until(actual, Predicate.isEqual(expected)); + } + } + private final static String NUMBER_OF_VALUES = "numberOfValues"; + private final Map<String, ActualAndExpected<String>> stringValues = new HashMap<>(); + private final Map<String, ActualAndExpected<Tuple>> tupleValues = new HashMap<>(); + private final Map<String, ActualAndExpected<Integer>> intValues = new HashMap<>(); + private boolean needManualWait = true; + + public TestChecker incNumberOfValues() { + return addToNumberOfValues(1); + } + + public TestChecker addToNumberOfValues(int increment) { + // if there is at least one call to this, we do not need to manually wait in the next check() + needManualWait = false; + Integer currentExpected = intValues.computeIfAbsent(NUMBER_OF_VALUES, ActualAndExpected::new).expected; + return put(NUMBER_OF_VALUES, currentExpected + increment); + } + + public TestChecker setActualNumberOfValues(Callable<Integer> actual) { + setActualInteger(NUMBER_OF_VALUES, actual); + intValues.get(NUMBER_OF_VALUES).expected = 0; + return this; + } + + public TestChecker setActualString(String name, Callable<String> actual) { + stringValues.computeIfAbsent(name, ActualAndExpected::new).actual = actual; + return this; + } + + public TestChecker setCheckForString(String name, BiConsumer<String, String> check) { + stringValues.computeIfAbsent(name, ActualAndExpected::new).customCheck = check; + return this; + } + + public TestChecker put(String name, String expected) { + stringValues.computeIfAbsent(name, ActualAndExpected::new).expected = expected; + return this; + } + + public TestChecker setActualTuple(String name, Callable<Tuple> actual) { + tupleValues.computeIfAbsent(name, ActualAndExpected::new).actual = actual; + return this; + } + + public TestChecker setCheckForTuple(String name, BiConsumer<String, Tuple> check) { + tupleValues.computeIfAbsent(name, ActualAndExpected::new).customCheck = check; + return this; + } + + public TestChecker put(String name, Tuple expected) { + tupleValues.computeIfAbsent(name, ActualAndExpected::new).expected = expected; + return this; + } + + public TestChecker setActualInteger(String name, Callable<Integer> actual) { + intValues.computeIfAbsent(name, ActualAndExpected::new).actual = actual; + return this; + } + + public TestChecker setCheckForInteger(String name, BiConsumer<String, Integer> check) { + intValues.computeIfAbsent(name, ActualAndExpected::new).customCheck = check; + return this; + } + + public TestChecker put(String name, Integer expected) { + intValues.computeIfAbsent(name, ActualAndExpected::new).expected = expected; + return this; + } + + public void check() { + if (needManualWait) { + try { + waitForMqtt(); + } catch (InterruptedException e) { + fail(e); + } + } + intValues.get(NUMBER_OF_VALUES).checkAwait(NUMBER_OF_VALUES); + + stringValues.forEach((name, aae) -> aae.check(name)); + tupleValues.forEach((name, aae) -> aae.check(name)); + intValues.forEach((name, aae) -> { + if (!name.equals(NUMBER_OF_VALUES)) { + aae.check(name); + } + }); + needManualWait = true; + } + } + public static class IntList { private final List<Integer> integers = newArrayList(); public IntList(Integer... values) { diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/relation/RelationTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/relation/RelationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a88f97fefabd47159f91d268a96ffe67a3dcee7e --- /dev/null +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/relation/RelationTest.java @@ -0,0 +1,690 @@ +package org.jastadd.ragconnect.tests.relation; + +import org.jastadd.ragconnect.tests.AbstractMqttTest; +import org.jastadd.ragconnect.tests.TestUtils; +import org.junit.jupiter.api.Tag; +import relationInc.ast.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; +import static org.jastadd.ragconnect.tests.TestUtils.TestChecker; +import static org.jastadd.ragconnect.tests.TestUtils.mqttUri; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case "relation". + * + * @author rschoene - Initial contribution + */ +@Tag("Incremental") +@Tag("New") +public class RelationTest extends AbstractMqttTest { + + private static final String TOPIC_WILDCARD = "rel/#"; + private static final String TOPIC_MY_A = "rel/my_a"; + private static final String TOPIC_OPTIONAL_A = "rel/optional_a"; + private static final String TOPIC_MANY_A = "rel/many_a"; + private static final String TOPIC_BI_MY_A = "rel/bi_my_a"; + private static final String TOPIC_BI_OPTIONAL_A = "rel/bi_optional_a"; + private static final String TOPIC_BI_MANY_A = "rel/bi_many_a"; + private static final String TOPIC_MY_B = "rel/my_b"; + private static final String TOPIC_OPTIONAL_B = "rel/optional_b"; + private static final String TOPIC_MANY_B = "rel/many_b"; + private static final String TOPIC_BI_MY_B = "rel/bi_my_b"; + private static final String TOPIC_BI_OPTIONAL_B = "rel/bi_optional_b"; + private static final String TOPIC_BI_MANY_B = "rel/bi_many_b"; + + private MqttHandler handler; + private ReceiverData data; + private TestChecker checker; + + private Root model; + private SenderRoot senderUni; + private SenderRoot senderBi; + private ReceiverRoot receiverRoot; + + @Override + protected void createModel() { + model = new Root(); +// model.trace().setReceiver(TestUtils::logEvent); + senderUni = createSenderRoot("uni"); + senderUni.setMyA(uniA(1)); + senderUni.setMyB(uniB(1)); + + senderBi = createSenderRoot("bi"); + senderBi.setBiMyA(biA(1)); + senderBi.setBiMyB(biB(1)); + + receiverRoot = new ReceiverRoot(); + model.addSenderRoot(senderUni); + model.addSenderRoot(senderBi); + model.setReceiverRoot(receiverRoot); + } + + private SenderRoot createSenderRoot(String name) { + SenderRoot result = new SenderRoot(); + A dummyA = createA(name + "-dummyA"); + result.addA(dummyA); + result.addA(createA(name + "-a1")); + result.addA(createA(name + "-a2")); + result.addA(createA(name + "-a3")); + + B dummyB = createB(name + "-dummyB"); + result.addB(dummyB); + result.addB(createB(name + "-b1")); + result.addB(createB(name + "-b2")); + result.addB(createB(name + "-b3")); + + result.setMyA(dummyA); + result.setBiMyA(dummyA); + result.setMyB(dummyB); + result.setBiMyB(dummyB); + + return result; + } + + private A createA(String value) { + return new A().setValue(value) + .setInner(new Inner().setInnerValue("inner-" + value)); + } + + private B createB(String value) { + return new B().setValue(value) + .setInner(new Inner().setInnerValue("inner-" + value)); + } + + private A uniA(int index) { + return senderUni.getA(index); + } + + private B uniB(int index) { + return senderUni.getB(index); + } + + private A biA(int index) { + return senderBi.getA(index); + } + + private B biB(int index) { + return senderBi.getB(index); + } + + @Override + protected void setupReceiverAndConnect() throws IOException, InterruptedException { + model.ragconnectSetupMqttWaitUntilReady(2, TimeUnit.SECONDS); + handler = new MqttHandler().setHost(TestUtils.getMqttHost()).dontSendWelcomeMessage(); + assertTrue(handler.waitUntilReady(2, TimeUnit.SECONDS)); + + data = new ReceiverData(); + assertTrue(handler.newConnection(TOPIC_WILDCARD, bytes -> data.numberOfValues += 1)); + + checker = new TestChecker(); + checker.setActualNumberOfValues(() -> data.numberOfValues).setCheckForString(TOPIC_MY_A, + (name, expected) -> assertNullOrA(expected, receiverRoot.getFromMyA(), name)) + .setCheckForString(TOPIC_OPTIONAL_A, + (name, expected) -> assertNullOrA(expected, receiverRoot.getFromOptionalA(), name)) + .setCheckForTuple(TOPIC_MANY_A, + (name, expected) -> assertListEqualsForA(expected.toList(), receiverRoot.getFromManyAList(), name)) + .setCheckForString(TOPIC_BI_MY_A, + (name, expected) -> assertNullOrA(expected, receiverRoot.getFromBiMyA(), name)) + .setCheckForString(TOPIC_BI_OPTIONAL_A, + (name, expected) -> assertNullOrA(expected, receiverRoot.getFromBiOptionalA(), name)) + .setCheckForTuple(TOPIC_BI_MANY_A, + (name, expected) -> assertListEqualsForA(expected.toList(), receiverRoot.getFromBiManyAList(), name)) + .setCheckForString(TOPIC_MY_B, + (name, expected) -> assertNullOrB(expected, receiverRoot.getFromMyB(), name)) + .setCheckForString(TOPIC_OPTIONAL_B, + (name, expected) -> assertNullOrB(expected, receiverRoot.getFromOptionalB(), name)) + .setCheckForTuple(TOPIC_MANY_B, + (name, expected) -> assertListEqualsForB(expected.toList(), receiverRoot.getFromManyB(), name)) + .setCheckForString(TOPIC_BI_MY_B, + (name, expected) -> assertNullOrB(expected, receiverRoot.getFromBiMyB(), name)) + .setCheckForString(TOPIC_BI_OPTIONAL_B, + (name, expected) -> assertNullOrB(expected, receiverRoot.getFromBiOptionalB(), name)) + .setCheckForTuple(TOPIC_BI_MANY_B, + (name, expected) -> assertListEqualsForB(expected.toList(), receiverRoot.getFromBiManyB(), name)); + + // connect receive + assertTrue(receiverRoot.connectFromMyA(mqttUri(TOPIC_MY_A))); + assertTrue(receiverRoot.connectFromOptionalA(mqttUri(TOPIC_OPTIONAL_A))); + assertTrue(receiverRoot.connectFromManyAList(mqttUri(TOPIC_MANY_A))); + assertTrue(receiverRoot.connectFromBiMyA(mqttUri(TOPIC_BI_MY_A))); + assertTrue(receiverRoot.connectFromBiOptionalA(mqttUri(TOPIC_BI_OPTIONAL_A))); + assertTrue(receiverRoot.connectFromBiManyAList(mqttUri(TOPIC_BI_MANY_A))); + assertTrue(receiverRoot.connectFromMyB(mqttUri(TOPIC_MY_B))); + assertTrue(receiverRoot.connectFromOptionalB(mqttUri(TOPIC_OPTIONAL_B))); + assertTrue(receiverRoot.connectFromManyB(mqttUri(TOPIC_MANY_B))); + assertTrue(receiverRoot.connectFromBiMyB(mqttUri(TOPIC_BI_MY_B))); + assertTrue(receiverRoot.connectFromBiOptionalB(mqttUri(TOPIC_BI_OPTIONAL_B))); + assertTrue(receiverRoot.connectFromBiManyB(mqttUri(TOPIC_BI_MANY_B))); + + // connect send, and wait to receive (if writeCurrentValue is set) + assertTrue(senderUni.connectMyA(mqttUri(TOPIC_MY_A), isWriteCurrentValue())); + assertTrue(senderUni.connectOptionalA(mqttUri(TOPIC_OPTIONAL_A), isWriteCurrentValue())); + assertTrue(senderUni.connectManyA(mqttUri(TOPIC_MANY_A), isWriteCurrentValue())); + + assertTrue(senderBi.connectBiMyA(mqttUri(TOPIC_BI_MY_A), isWriteCurrentValue())); + assertTrue(senderBi.connectBiOptionalA(mqttUri(TOPIC_BI_OPTIONAL_A), isWriteCurrentValue())); + assertTrue(senderBi.connectBiManyA(mqttUri(TOPIC_BI_MANY_A), isWriteCurrentValue())); + + assertTrue(senderUni.connectMyB(mqttUri(TOPIC_MY_B), isWriteCurrentValue())); + assertTrue(senderUni.connectOptionalB(mqttUri(TOPIC_OPTIONAL_B), isWriteCurrentValue())); + assertTrue(senderUni.connectManyB(mqttUri(TOPIC_MANY_B), isWriteCurrentValue())); + + assertTrue(senderBi.connectBiMyB(mqttUri(TOPIC_BI_MY_B), isWriteCurrentValue())); + assertTrue(senderBi.connectBiOptionalB(mqttUri(TOPIC_BI_OPTIONAL_B), isWriteCurrentValue())); + assertTrue(senderBi.connectBiManyB(mqttUri(TOPIC_BI_MANY_B), isWriteCurrentValue())); + } + + @Override + protected void communicateSendInitialValue() throws IOException { + checker.addToNumberOfValues(8) + .put(TOPIC_MY_A, "uni-a1") + .put(TOPIC_OPTIONAL_A, (String) null) + .put(TOPIC_MANY_A, tuple()) + .put(TOPIC_BI_MY_A, "bi-a1") + .put(TOPIC_BI_OPTIONAL_A, (String) null) + .put(TOPIC_BI_MANY_A, tuple()) + .put(TOPIC_MY_B, "uni-b1") + .put(TOPIC_OPTIONAL_B, (String) null) + .put(TOPIC_MANY_B, tuple()) + .put(TOPIC_BI_MY_B, "bi-b1") + .put(TOPIC_BI_OPTIONAL_B, (String) null) + .put(TOPIC_BI_MANY_B, tuple()); + + communicateBoth(); + } + + @Override + protected void communicateOnlyUpdatedValue() throws IOException, InterruptedException { + checker.put(TOPIC_MY_A, (String) null) + .put(TOPIC_OPTIONAL_A, (String) null) + .put(TOPIC_MANY_A, tuple()) + .put(TOPIC_BI_MY_A, (String) null) + .put(TOPIC_BI_OPTIONAL_A, (String) null) + .put(TOPIC_BI_MANY_A, tuple()) + .put(TOPIC_MY_B, (String) null) + .put(TOPIC_OPTIONAL_B, (String) null) + .put(TOPIC_MANY_B, tuple()) + .put(TOPIC_BI_MY_B, (String) null) + .put(TOPIC_BI_OPTIONAL_B, (String) null) + .put(TOPIC_BI_MANY_B, tuple()); + + communicateBoth(); + } + + protected void communicateBoth() throws IOException { + checker.check(); + + // myA -> uni-a1, myB -> uni-b1 + // --- testing unmapped unidirectional normal role --- // + + uniA(1).setValue("test-1"); + checker.incNumberOfValues().put(TOPIC_MY_A, "test-1:inner-uni-a1").check(); + + senderUni.setMyA(uniA(2)); + checker.incNumberOfValues().put(TOPIC_MY_A, "uni-a2").check(); + + // changing something that was previously the relation target must not trigger a message + uniA(1).setValue("test-2-ignored"); + checker.check(); + + uniA(2).setValue("test-3"); + checker.incNumberOfValues().put(TOPIC_MY_A, "test-3:inner-uni-a2").check(); + + uniA(2).getInner().setInnerValue("test-4"); + checker.incNumberOfValues().put(TOPIC_MY_A, "test-3:test-4").check(); + + // setting a new relation target resulting in the same serialization must not trigger a message + uniA(1).setValue("test-3"); + uniA(1).getInner().setInnerValue("test-4"); + senderUni.setMyA(uniA(1)); + checker.check(); + + // --- testing unmapped unidirectional optional role --- // + + // reset a2 + uniA(2).setValue("uni-a2"); + uniA(2).getInner().setInnerValue("inner-uni-a2"); + + senderUni.setOptionalA(uniA(2)); + checker.incNumberOfValues().put(TOPIC_OPTIONAL_A, "uni-a2").check(); + + uniA(2).setValue("test-5"); + checker.incNumberOfValues().put(TOPIC_OPTIONAL_A, "test-5:inner-uni-a2").check(); + + senderUni.setOptionalA(uniA(1)); + checker.incNumberOfValues().put(TOPIC_OPTIONAL_A, "test-3:test-4").check(); + + // change a nonterminal target of two relations must trigger two messages + uniA(1).getInner().setInnerValue("test-6"); + checker.addToNumberOfValues(2) + .put(TOPIC_MY_A, "test-3:test-6") + .put(TOPIC_OPTIONAL_A, "test-3:test-6") + .check(); + + // setting an optional relation to null is allowed, but must not trigger a message + senderUni.setOptionalA(null); + checker.check(); + + // setting the previous nonterminal as relation target again won't trigger a message + senderUni.setOptionalA(uniA(1)); + checker.check(); + + // --- testing unmapped unidirectional list role --- // + + senderUni.addManyA(uniA(3)); + checker.incNumberOfValues().put(TOPIC_MANY_A, tuple("uni-a3")).check(); + + uniA(3).setValue("test-7"); + checker.incNumberOfValues().put(TOPIC_MANY_A, tuple("test-7:inner-uni-a3")).check(); + + senderUni.addManyA(uniA(2)); + checker.incNumberOfValues().put(TOPIC_MANY_A, tuple("test-7:inner-uni-a3", "test-5:inner-uni-a2")).check(); + + senderUni.addManyA(uniA(1)); + checker.incNumberOfValues().put(TOPIC_MANY_A, tuple("test-7:inner-uni-a3", "test-5:inner-uni-a2", "test-3:test-6")).check(); + + uniA(2).getInner().setInnerValue("test-8"); + checker.incNumberOfValues().put(TOPIC_MANY_A, tuple("test-7:inner-uni-a3", "test-5:test-8", "test-3:test-6")).check(); + + senderUni.removeManyA(uniA(2)); + checker.incNumberOfValues().put(TOPIC_MANY_A, tuple("test-7:inner-uni-a3", "test-3:test-6")).check(); + + // disconnect my-a, optional-a, many-a - resetting afterwards must not trigger a message + senderUni.disconnectMyA(mqttUri(TOPIC_MY_A)); + senderUni.disconnectOptionalA(mqttUri(TOPIC_OPTIONAL_A)); + senderUni.disconnectManyA(mqttUri(TOPIC_MANY_A)); + uniA(1).setValue("a1"); + uniA(1).getInner().setInnerValue("inner-a1"); + uniA(2).setValue("a2"); + uniA(2).getInner().setInnerValue("inner-a2"); + uniA(3).setValue("a3"); + uniA(3).getInner().setInnerValue("inner-a3"); + checker.check(); + + // "reset" values in receiver-root to make check method call shorted + receiverRoot.setFromMyA(createA("uni-a1")); + receiverRoot.setFromOptionalA(null); + receiverRoot.setFromManyAList(new JastAddList<>()); + checker.put(TOPIC_MY_A, "uni-a1") + .put(TOPIC_OPTIONAL_A, (String) null) + .put(TOPIC_MANY_A, tuple()); + checker.check(); + + // biMyA -> bi-a1, biMyB -> bi-b1 + // --- testing unmapped bidirectional normal role --- // + biA(1).setValue("test-9"); + checker.incNumberOfValues().put(TOPIC_BI_MY_A, "test-9:inner-bi-a1").check(); + + // set opposite role of relation must trigger message + biA(2).setToMyA(senderBi); + checker.incNumberOfValues().put(TOPIC_BI_MY_A, "bi-a2").check(); + + // changing something that was previously the relation target must not trigger a message + biA(1).setValue("test-9-ignored"); + checker.check(); + + biA(2).setValue("test-10"); + checker.incNumberOfValues().put(TOPIC_BI_MY_A, "test-10:inner-bi-a2").check(); + + biA(2).getInner().setInnerValue("test-11"); + checker.incNumberOfValues().put(TOPIC_BI_MY_A, "test-10:test-11").check(); + + // setting a new relation target resulting in the same serialization must not trigger a message + biA(1).setValue("test-10"); + biA(1).getInner().setInnerValue("test-11"); + biA(1).setToMyA(senderBi); + checker.check(); + + // --- testing unmapped bidirectional optional role --- // + // reset a2 + biA(2).setValue("bi-a2"); + biA(2).getInner().setInnerValue("inner-bi-a2"); + + senderBi.setBiOptionalA(biA(2)); + checker.incNumberOfValues().put(TOPIC_BI_OPTIONAL_A, "bi-a2").check(); + + biA(2).setValue("test-12"); + checker.incNumberOfValues().put(TOPIC_BI_OPTIONAL_A, "test-12:inner-bi-a2").check(); + + // set opposite role of relation must trigger message + biA(1).setToOptionalA(senderBi); + checker.incNumberOfValues().put(TOPIC_BI_OPTIONAL_A, "test-10:test-11").check(); + + // change a nonterminal target of two relations must trigger two messages + biA(1).getInner().setInnerValue("test-13"); + checker.addToNumberOfValues(2) + .put(TOPIC_BI_MY_A, "test-10:test-13") + .put(TOPIC_BI_OPTIONAL_A, "test-10:test-13").check(); + + // setting an optional relation to null is allowed, but must not trigger a message + senderBi.setBiOptionalA(null); + checker.check(); + + // setting the previous nonterminal as relation target again won't trigger a message + biA(1).setToOptionalA(senderBi); + checker.check(); + + // --- testing unmapped bidirectional list role --- // + biA(3).setToManyA(senderBi); + checker.incNumberOfValues().put(TOPIC_BI_MANY_A, tuple("bi-a3")).check(); + + biA(3).setValue("test-14"); + checker.incNumberOfValues().put(TOPIC_BI_MANY_A, tuple("test-14:inner-bi-a3")).check(); + + senderBi.addBiManyA(biA(2)); + checker.incNumberOfValues().put(TOPIC_BI_MANY_A, tuple("test-14:inner-bi-a3", "test-12:inner-bi-a2")).check(); + + biA(1).setToManyA(senderBi); + checker.incNumberOfValues().put(TOPIC_BI_MANY_A, tuple("test-14:inner-bi-a3", "test-12:inner-bi-a2", "test-10:test-13")).check(); + + biA(2).getInner().setInnerValue("test-15"); + // currently, an additional message is sent at bi_optional_a for biA1 as the serialization includes relations from A to SenderRoot and the relation to ToManyA was added for biA1. + // this appears to be a bug in either jastadd or ragconnect + // numberOfValues should actually be only 37 here. + checker.addToNumberOfValues(2).put(TOPIC_BI_MANY_A, tuple("test-14:inner-bi-a3", "test-12:test-15", "test-10:test-13")).check(); + + senderBi.removeBiManyA(biA(2)); + // the bug from above does not occur here, although the situation is similar + // so, only one message is sent + checker.incNumberOfValues().put(TOPIC_BI_MANY_A, tuple("test-14:inner-bi-a3", "test-10:test-13")).check(); + + biA(3).setToManyA(null); + checker.incNumberOfValues().put(TOPIC_BI_MANY_A, tuple("test-10:test-13")).check(); + + // change a nonterminal target of three relations must trigger three messages + biA(1).setValue("test-16"); + checker.addToNumberOfValues(3) + .put(TOPIC_BI_MY_A, "test-16:test-13") + .put(TOPIC_BI_OPTIONAL_A, "test-16:test-13") + .put(TOPIC_BI_MANY_A, tuple("test-16:test-13")) + .check(); + + // disconnect bi-my-a, bi-optional-a, bi-many-a - resetting afterwards must not trigger a message + senderBi.disconnectBiMyA(mqttUri(TOPIC_BI_MY_A)); + senderBi.disconnectBiOptionalA(mqttUri(TOPIC_BI_OPTIONAL_A)); + senderBi.disconnectBiManyA(mqttUri(TOPIC_BI_MANY_A)); + biA(1).setValue("bi-a1"); + biA(1).getInner().setInnerValue("inner-bi-a1"); + biA(2).setValue("bi-a2"); + biA(2).getInner().setInnerValue("inner-bi-a2"); + biA(3).setValue("bi-a3"); + biA(3).getInner().setInnerValue("inner-bi-a3"); + checker.check(); + + // "reset" values in receiver-root to make check method call shorted + receiverRoot.setFromBiMyA(createA("bi-a1")); + receiverRoot.setFromBiOptionalA(null); + receiverRoot.setFromBiManyAList(new JastAddList<>()); + checker.put(TOPIC_BI_MY_A, "bi-a1") + .put(TOPIC_BI_OPTIONAL_A, (String) null) + .put(TOPIC_BI_MANY_A, tuple()) + .check(); + + // --- testing transformed unidirectional normal role --- // + uniB(1).setValue("test-17"); + checker.incNumberOfValues().put(TOPIC_MY_B, "test-17:inner-uni-b1").check(); + + senderUni.setMyB(uniB(2)); + checker.incNumberOfValues().put(TOPIC_MY_B, "uni-b2").check(); + + // changing something that was previously the relation target must not trigger a message + uniB(1).setValue("test-17-ignored"); + checker.check(); + + uniB(2).setValue("test-18"); + checker.incNumberOfValues().put(TOPIC_MY_B, "test-18:inner-uni-b2").check(); + + uniB(2).getInner().setInnerValue("test-19"); + checker.incNumberOfValues().put(TOPIC_MY_B, "test-18:test-19").check(); + + // setting a new relation target resulting in the same serialization must not trigger a message + uniB(1).setValue("test-18"); + uniB(1).getInner().setInnerValue("test-19"); + senderUni.setMyB(uniB(1)); + checker.check(); + + // --- testing transformed unidirectional optional role --- // + + // reset a2 + uniB(2).setValue("uni-b2"); + uniB(2).getInner().setInnerValue("inner-uni-b2"); + + senderUni.setOptionalB(uniB(2)); + checker.incNumberOfValues().put(TOPIC_OPTIONAL_B, "uni-b2").check(); + + uniB(2).setValue("test-20"); + checker.incNumberOfValues().put(TOPIC_OPTIONAL_B, "test-20:inner-uni-b2").check(); + + senderUni.setOptionalB(uniB(1)); + checker.incNumberOfValues().put(TOPIC_OPTIONAL_B, "test-18:test-19").check(); + + // change a nonterminal target of two relations must trigger two messages + uniB(1).getInner().setInnerValue("test-21"); + checker.addToNumberOfValues(2) + .put(TOPIC_MY_B, "test-18:test-21") + .put(TOPIC_OPTIONAL_B, "test-18:test-21") + .check(); + + // setting an optional relation to null is allowed, but must not trigger a message + senderUni.setOptionalB(null); + checker.check(); + + // setting the previous nonterminal as relation target again won't trigger a message + senderUni.setOptionalB(uniB(1)); + checker.check(); + + // --- testing transformed unidirectional list role --- // + senderUni.addManyB(uniB(3)); + checker.incNumberOfValues().put(TOPIC_MANY_B, tuple("uni-b3")).check(); + + uniB(3).setValue("test-22"); + checker.incNumberOfValues().put(TOPIC_MANY_B, tuple("test-22:inner-uni-b3")).check(); + + senderUni.addManyB(uniB(2)); + checker.incNumberOfValues().put(TOPIC_MANY_B, tuple("test-22:inner-uni-b3", "test-20:inner-uni-b2")).check(); + + senderUni.addManyB(uniB(1)); + checker.incNumberOfValues().put(TOPIC_MANY_B, tuple("test-22:inner-uni-b3", "test-20:inner-uni-b2", "test-18:test-21")).check(); + + uniB(2).getInner().setInnerValue("test-23"); + checker.incNumberOfValues().put(TOPIC_MANY_B, tuple("test-22:inner-uni-b3", "test-20:test-23", "test-18:test-21")).check(); + + senderUni.removeManyB(uniB(2)); + checker.incNumberOfValues().put(TOPIC_MANY_B, tuple("test-22:inner-uni-b3", "test-18:test-21")).check(); + + // disconnect my-b, optional-b, many-b - resetting afterwards must not trigger a message + senderUni.disconnectMyB(mqttUri(TOPIC_MY_B)); + senderUni.disconnectOptionalB(mqttUri(TOPIC_OPTIONAL_B)); + senderUni.disconnectManyB(mqttUri(TOPIC_MANY_B)); + uniB(1).setValue("b1"); + uniB(1).getInner().setInnerValue("inner-b1"); + uniB(2).setValue("b2"); + uniB(2).getInner().setInnerValue("inner-b2"); + uniB(3).setValue("b3"); + uniB(3).getInner().setInnerValue("inner-b3"); + + // "reset" values in receiver-root to make check method call shorted + receiverRoot.setFromMyB("uni-b1+inner-uni-b1"); + receiverRoot.setFromOptionalB(null); + receiverRoot.setFromManyB(null); + checker.put(TOPIC_MY_B, "uni-b1") + .put(TOPIC_OPTIONAL_B, (String) null) + .put(TOPIC_MANY_B, tuple()) + .check(); + + // --- testing transformed bidirectional normal role --- // + biB(1).setValue("test-24"); + checker.incNumberOfValues().put(TOPIC_BI_MY_B, "test-24:inner-bi-b1").check(); + + // set opposite role of relation must trigger message + biB(2).setToMyB(senderBi); + checker.incNumberOfValues().put(TOPIC_BI_MY_B, "bi-b2").check(); + + // changing something that was previously the relation target must not trigger a message + biB(1).setValue("test-24-ignored"); + checker.check(); + + biB(2).setValue("test-25"); + checker.incNumberOfValues().put(TOPIC_BI_MY_B, "test-25:inner-bi-b2").check(); + + biB(2).getInner().setInnerValue("test-26"); + checker.incNumberOfValues().put(TOPIC_BI_MY_B, "test-25:test-26").check(); + + // setting a new relation target resulting in the same serialization must not trigger a message + biB(1).setValue("test-25"); + biB(1).getInner().setInnerValue("test-26"); + biB(1).setToMyB(senderBi); + checker.check(); + + // --- testing transformed bidirectional optional role --- // + // reset b2 + biB(2).setValue("bi-b2"); + biB(2).getInner().setInnerValue("inner-bi-b2"); + + senderBi.setBiOptionalB(biB(2)); + checker.incNumberOfValues().put(TOPIC_BI_OPTIONAL_B, "bi-b2").check(); + + biB(2).setValue("test-27"); + checker.incNumberOfValues().put(TOPIC_BI_OPTIONAL_B, "test-27:inner-bi-b2").check(); + + // set opposite role of relation must trigger message + biB(1).setToOptionalB(senderBi); + checker.incNumberOfValues().put(TOPIC_BI_OPTIONAL_B, "test-25:test-26").check(); + + // change a nonterminal target of two relations must trigger two messages + biB(1).getInner().setInnerValue("test-28"); + checker.addToNumberOfValues(2) + .put(TOPIC_BI_MY_B, "test-25:test-28") + .put(TOPIC_BI_OPTIONAL_B, "test-25:test-28") + .check(); + + // setting an optional relation to null is allowed, but must not trigger a message + senderBi.setBiOptionalB(null); + checker.check(); + + // setting the previous nonterminal as relation target again won't trigger a message + biB(1).setToOptionalB(senderBi); + checker.check(); + + // --- testing transformed bidirectional list role --- // + biB(3).setToManyB(senderBi); + checker.incNumberOfValues().put(TOPIC_BI_MANY_B, tuple("bi-b3")).check(); + + biB(3).setValue("test-29"); + checker.incNumberOfValues().put(TOPIC_BI_MANY_B, tuple("test-29:inner-bi-b3")).check(); + + senderBi.addBiManyB(biB(2)); + checker.incNumberOfValues().put(TOPIC_BI_MANY_B, tuple("test-29:inner-bi-b3", "test-27:inner-bi-b2")).check(); + + biB(1).setToManyB(senderBi); + checker.incNumberOfValues().put(TOPIC_BI_MANY_B, tuple("test-29:inner-bi-b3", "test-27:inner-bi-b2", "test-25:test-28")).check(); + + biB(2).getInner().setInnerValue("test-30"); + // the bug appearing in the unmapped bidirectional list case does not appear here, because here only a string representation of value + inner value is sent. so changes to relations do not trigger a message + checker.incNumberOfValues().put(TOPIC_BI_MANY_B, tuple("test-29:inner-bi-b3", "test-27:test-30", "test-25:test-28")).check(); + + senderBi.removeBiManyB(biB(2)); + // the bug from above does not occur here, although the situation is similar + // so, only one message is sent + checker.incNumberOfValues().put(TOPIC_BI_MANY_B, tuple("test-29:inner-bi-b3", "test-25:test-28")).check(); + + biB(3).setToManyB(null); + checker.incNumberOfValues().put(TOPIC_BI_MANY_B, tuple("test-25:test-28")).check(); + + // change a nonterminal target of three relations must trigger three messages + biB(1).setValue("test-31"); + checker.addToNumberOfValues(3) + .put(TOPIC_BI_MY_B, "test-31:test-28") + .put(TOPIC_BI_OPTIONAL_B, "test-31:test-28") + .put(TOPIC_BI_MANY_B, tuple("test-31:test-28")) + .check(); + + // disconnect bi-my-a, bi-optional-a, bi-many-a - resetting afterwards must not trigger a message + senderBi.disconnectBiMyB(mqttUri(TOPIC_BI_MY_B)); + senderBi.disconnectBiOptionalB(mqttUri(TOPIC_BI_OPTIONAL_B)); + senderBi.disconnectBiManyB(mqttUri(TOPIC_BI_MANY_B)); + biB(1).setValue("bi-b1"); + biB(1).getInner().setInnerValue("inner-bi-b1"); + biB(2).setValue("bi-b2"); + biB(2).getInner().setInnerValue("inner-bi-b2"); + biB(3).setValue("bi-b3"); + biB(3).getInner().setInnerValue("inner-bi-b3"); + checker.check(); + + } + + private void assertNullOrA(String expectedValue, A actual, String alias) { + if (expectedValue == null) { + assertNull(actual, alias); + return; + } + final String expectedInner; + if (expectedValue.contains(":")) { + String[] tokens = expectedValue.split(":"); + assertEquals(2, tokens.length); + expectedValue = tokens[0]; + expectedInner = tokens[1]; + } else { + expectedInner = "inner-" + expectedValue; + } + assertThat(actual.getValue()).describedAs(alias + ".Value").isEqualTo(expectedValue); + assertThat(actual.getInner()).describedAs(alias + ".inner != null").isNotNull(); + assertThat(actual.getInner().getInnerValue()).describedAs(alias + ".inner.Value").isEqualTo(expectedInner); + } + + private void assertListEqualsForA(List<Object> expected, JastAddList<A> actual, String alias) { + assertEquals(expected.size(), actual.getNumChild(), alias + ".size"); + for (int i = 0, expectedSize = expected.size(); i < expectedSize; i++) { + String s = (String) expected.get(i); + assertNullOrA(s, actual.getChild(i), alias + "[" + i + "]"); + } + } + + private void assertNullOrB(String expectedValue, String actual, String alias) { + final String expectedTransformed; + if (expectedValue == null) { + expectedTransformed = ""; + } else { + final String expectedInner; + if (expectedValue.contains(":")) { + String[] tokens = expectedValue.split(":"); + assertEquals(2, tokens.length); + expectedValue = tokens[0]; + expectedInner = tokens[1]; + } else { + expectedInner = "inner-" + expectedValue; + } + expectedTransformed = expectedValue + "+" + expectedInner; + } + assertEquals(expectedTransformed, actual, alias); + } + + private void assertListEqualsForB(List<Object> expected, String actual, String alias) { + String[] actualTokens = actual.isEmpty() ? new String[0] : actual.split(";"); + assertEquals(expected.size(), actualTokens.length, alias + ".size"); + for (int i = 0, expectedSize = expected.size(); i < expectedSize; i++) { + String s = (String) expected.get(i); + assertNullOrB(s, actualTokens[i], alias + "[" + i + "]"); + } + } + + @Override + protected void closeConnections() { + if (handler != null) { + handler.close(); + } + if (model != null) { + model.ragconnectCloseConnections(); + } + } + + private static class ReceiverData { + int numberOfValues = 0; + } +}