[DROOLS-7306] Implement unification (#41)

* [DROOLS-7306] Implement unification
- Also [DROOLS-7307] Parse attribute agenda-group

* [DROOLS-7308] Parse attribute without value
- Also [DROOLS-7309] Parse attribute with parentheses
diff --git a/drools-drl/drools-drl10-parser/src/main/antlr4/org/drools/parser/DRLLexer.g4 b/drools-drl/drools-drl10-parser/src/main/antlr4/org/drools/parser/DRLLexer.g4
index cce083a..8aac818 100644
--- a/drools-drl/drools-drl10-parser/src/main/antlr4/org/drools/parser/DRLLexer.g4
+++ b/drools-drl/drools-drl10-parser/src/main/antlr4/org/drools/parser/DRLLexer.g4
@@ -124,7 +124,7 @@
 /////////////////
 
 HASH : '#';
-UNIFY :	':=' ;
+DRL_UNIFY :	':=' ;
 NULL_SAFE_DOT :	'!.' ;
 QUESTION_DIV :	'?/' ;
 
diff --git a/drools-drl/drools-drl10-parser/src/main/antlr4/org/drools/parser/DRLParser.g4 b/drools-drl/drools-drl10-parser/src/main/antlr4/org/drools/parser/DRLParser.g4
index 30ba845..80774cd 100644
--- a/drools-drl/drools-drl10-parser/src/main/antlr4/org/drools/parser/DRLParser.g4
+++ b/drools-drl/drools-drl10-parser/src/main/antlr4/org/drools/parser/DRLParser.g4
@@ -116,7 +116,7 @@
            | lhsPatternBind
            ) SEMI? ;
 
-lhsPatternBind : label? ( LPAREN lhsPattern (DRL_OR lhsPattern)* RPAREN | lhsPattern ) ;
+lhsPatternBind : (label|unif)? ( LPAREN lhsPattern (DRL_OR lhsPattern)* RPAREN | lhsPattern ) ;
 
 /*
 lhsPattern : xpathPrimary (OVER patternFilter)? |
@@ -253,6 +253,7 @@
     | drlExpression bop=INSTANCEOF (typeType | pattern)
     | drlExpression bop=DRL_MATCHES drlExpression
     | drlExpression DRL_NOT? DRL_MEMBEROF drlExpression
+    | drlExpression bop=DRL_UNIFY drlExpression
     | drlExpression bop=(EQUAL | NOTEQUAL) drlExpression
     | drlExpression bop=BITAND drlExpression
     | drlExpression bop=CARET drlExpression
@@ -438,12 +439,15 @@
     ;
 
 attributes : attribute ( COMMA? attribute )* ;
-attribute : ( 'salience' DECIMAL_LITERAL )
-          | ( 'enabled' | 'no-loop' | 'auto-focus' | 'lock-on-active' | 'refract' | 'direct' ) BOOL_LITERAL?
-          | ( 'agenda-group' | 'activation-group' | 'ruleflow-group' | 'date-effective' | 'date-expires' | 'dialect' ) DRL_STRING_LITERAL
-          |   'calendars' DRL_STRING_LITERAL ( COMMA DRL_STRING_LITERAL )*
-          |   'timer' ( DECIMAL_LITERAL | TEXT )
-          |   'duration' ( DECIMAL_LITERAL | TEXT ) ;
+attribute : name=( 'salience' | 'enabled' ) conditionalOrExpression #expressionAttribute
+          | name=( 'no-loop' | 'auto-focus' | 'lock-on-active' | 'refract' | 'direct' ) BOOL_LITERAL? #booleanAttribute
+          | name=( 'agenda-group' | 'activation-group' | 'ruleflow-group' | 'date-effective' | 'date-expires' | 'dialect' ) DRL_STRING_LITERAL #stringAttribute
+          | name='calendars' DRL_STRING_LITERAL ( COMMA DRL_STRING_LITERAL )* #stringListAttribute
+          | name='timer' ( DECIMAL_LITERAL | chunk ) #intOrChunkAttribute
+          | name='duration' ( DECIMAL_LITERAL | TIME_INTERVAL | LPAREN TIME_INTERVAL RPAREN ) #durationAttribute
+          ;
+
+chunk : LPAREN .+? RPAREN;
 
 assignmentOperator : ASSIGN
                    |   ADD_ASSIGN
@@ -457,7 +461,7 @@
                    |   LT LT ASSIGN ;
 
 label : IDENTIFIER COLON ;
-unif : IDENTIFIER UNIFY ;
+unif : IDENTIFIER DRL_UNIFY ;
 
 /* extending JavaParser variableInitializer */
 drlVariableInitializer
diff --git a/drools-drl/drools-drl10-parser/src/main/java/org/drools/parser/DRLVisitorImpl.java b/drools-drl/drools-drl10-parser/src/main/java/org/drools/parser/DRLVisitorImpl.java
index 2a09caf..3ca0016 100644
--- a/drools-drl/drools-drl10-parser/src/main/java/org/drools/parser/DRLVisitorImpl.java
+++ b/drools-drl/drools-drl10-parser/src/main/java/org/drools/parser/DRLVisitorImpl.java
@@ -40,6 +40,7 @@
 import org.drools.drl.ast.descr.TypeFieldDescr;
 import org.drools.drl.ast.descr.UnitDescr;
 import org.drools.drl.ast.descr.WindowDeclarationDescr;
+import org.drools.util.StringUtils;
 
 import static org.drools.parser.DRLParserHelper.getTextWithoutErrorNode;
 import static org.drools.parser.ParserStringUtils.getTextPreservingWhitespace;
@@ -95,10 +96,9 @@
                 packageDescr.addWindowDeclaration((WindowDeclarationDescr) descr);
             } else if (descr instanceof AttributeDescr) {
                 packageDescr.addAttribute((AttributeDescr) descr);
-            } else if (descr instanceof RuleDescr) {
+            } else if (descr instanceof RuleDescr) { // QueryDescr extends RuleDescr
                 packageDescr.addRule((RuleDescr) descr);
-            } else if (descr instanceof QueryDescr) {
-                packageDescr.addRule((QueryDescr) descr);
+                packageDescr.afterRuleAdded((RuleDescr) descr);
             }
         });
     }
@@ -301,12 +301,75 @@
     }
 
     @Override
-    public AttributeDescr visitAttribute(DRLParser.AttributeContext ctx) {
-        AttributeDescr attributeDescr = new AttributeDescr(ctx.getChild(0).getText());
-        if (ctx.getChildCount() > 1) {
-            // TODO : will likely split visitAttribute methods using labels (e.g. #stringAttribute)
-            String value = unescapeJava(safeStripStringDelimiters(ctx.getChild(1).getText()));
-            attributeDescr.setValue(value);
+    public AttributeDescr visitExpressionAttribute(DRLParser.ExpressionAttributeContext ctx) {
+        AttributeDescr attributeDescr = new AttributeDescr(ctx.name.getText());
+        attributeDescr.setValue(getTextPreservingWhitespace(ctx.conditionalOrExpression()));
+        attributeDescr.setType(AttributeDescr.Type.EXPRESSION);
+        return attributeDescr;
+    }
+
+    @Override
+    public AttributeDescr visitBooleanAttribute(DRLParser.BooleanAttributeContext ctx) {
+        AttributeDescr attributeDescr = new AttributeDescr(ctx.name.getText());
+        attributeDescr.setValue(ctx.BOOL_LITERAL() != null ? ctx.BOOL_LITERAL().getText() : "true");
+        attributeDescr.setType(AttributeDescr.Type.BOOLEAN);
+        return attributeDescr;
+    }
+
+    @Override
+    public AttributeDescr visitStringAttribute(DRLParser.StringAttributeContext ctx) {
+        AttributeDescr attributeDescr = new AttributeDescr(ctx.name.getText());
+        attributeDescr.setValue(unescapeJava(safeStripStringDelimiters(ctx.DRL_STRING_LITERAL().getText())));
+        attributeDescr.setType(AttributeDescr.Type.STRING);
+        return attributeDescr;
+    }
+
+    @Override
+    public AttributeDescr visitStringListAttribute(DRLParser.StringListAttributeContext ctx) {
+        AttributeDescr attributeDescr = new AttributeDescr(ctx.name.getText());
+        List<String> valueList = ctx.DRL_STRING_LITERAL().stream()
+                .map(ParseTree::getText)
+                .collect(Collectors.toList());
+        attributeDescr.setValue(createStringList(valueList));
+        attributeDescr.setType(AttributeDescr.Type.LIST);
+        return attributeDescr;
+    }
+
+    private static String createStringList(List<String> valueList) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("[ ");
+        for (int i = 0; i < valueList.size(); i++) {
+            sb.append(valueList.get(i));
+            if (i < valueList.size() - 1) {
+                sb.append(", ");
+            }
+        }
+        sb.append(" ]");
+        return sb.toString();
+    }
+
+    @Override
+    public AttributeDescr visitIntOrChunkAttribute(DRLParser.IntOrChunkAttributeContext ctx) {
+        AttributeDescr attributeDescr = new AttributeDescr(ctx.name.getText());
+        if (ctx.DECIMAL_LITERAL() != null) {
+            attributeDescr.setValue(ctx.DECIMAL_LITERAL().getText());
+            attributeDescr.setType(AttributeDescr.Type.NUMBER);
+        } else {
+            attributeDescr.setValue(getTextPreservingWhitespace(ctx.chunk()));
+            attributeDescr.setType(AttributeDescr.Type.EXPRESSION);
+        }
+        return attributeDescr;
+    }
+
+    @Override
+    public AttributeDescr visitDurationAttribute(DRLParser.DurationAttributeContext ctx) {
+        AttributeDescr attributeDescr = new AttributeDescr(ctx.name.getText());
+        if (ctx.DECIMAL_LITERAL() != null) {
+            attributeDescr.setValue(ctx.DECIMAL_LITERAL().getText());
+            attributeDescr.setType(AttributeDescr.Type.NUMBER);
+        } else {
+            attributeDescr.setValue(unescapeJava(safeStripStringDelimiters(ctx.TIME_INTERVAL().getText())));
+            attributeDescr.setType(AttributeDescr.Type.EXPRESSION);
         }
         return attributeDescr;
     }
@@ -338,6 +401,9 @@
                 .orElseThrow(() -> new IllegalStateException("lhsPatternBind must have at least one lhsPattern : " + ctx.getText()));
         if (ctx.label() != null) {
             patternDescr.setIdentifier(ctx.label().IDENTIFIER().getText());
+        } else if (ctx.unif() != null) {
+            patternDescr.setIdentifier(ctx.unif().IDENTIFIER().getText());
+            patternDescr.setUnification(true);
         }
         return patternDescr;
     }
diff --git a/drools-drl/drools-drl10-parser/src/test/java/org/drools/parser/MiscDRLParserTest.java b/drools-drl/drools-drl10-parser/src/test/java/org/drools/parser/MiscDRLParserTest.java
index 9e6a976..50e7f82 100644
--- a/drools-drl/drools-drl10-parser/src/test/java/org/drools/parser/MiscDRLParserTest.java
+++ b/drools-drl/drools-drl10-parser/src/test/java/org/drools/parser/MiscDRLParserTest.java
@@ -1651,7 +1651,6 @@
         assertThat(pkg.getName()).isEqualTo("foo.bar");
     }
 
-    @Disabled("Priority : High | Parse attribute without value => true")
     @Test
     public void parse_Attributes() throws Exception {
         final RuleDescr rule = parseAndGetFirstRuleDescrFromFile(
@@ -1731,7 +1730,6 @@
 
     }
 
-    @Disabled("Priority : High | Parse attribute without value => true")
     @Test
     public void parse_AttributeRefract() throws Exception {
         final String source = "rule Test refract when Person() then end";
@@ -1751,7 +1749,6 @@
 
     }
 
-    @Disabled("Priority : High | Parse attribute with parentheses")
     @Test
     public void parse_EnabledExpression() throws Exception {
         final RuleDescr rule = parseAndGetFirstRuleDescrFromFile(
@@ -1775,7 +1772,6 @@
         assertThat(at.getValue()).isEqualTo("true");
     }
 
-    @Disabled("Priority : High | Parse attribute with parentheses")
     @Test
     public void parse_DurationExpression() throws Exception {
         final RuleDescr rule = parseAndGetFirstRuleDescrFromFile(
@@ -1795,7 +1791,6 @@
         assertThat(at.getValue()).isEqualTo("true");
     }
 
-    @Disabled("Priority : Mid | Parse calendar attribute")
     @Test
     public void parse_Calendars() throws Exception {
         final RuleDescr rule = parseAndGetFirstRuleDescrFromFile(
@@ -1815,7 +1810,6 @@
         assertThat(at.getValue()).isEqualTo("true");
     }
 
-    @Disabled("Priority : Mid | Parse calendar attribute")
     @Test
     public void parse_Calendars2() throws Exception {
         final RuleDescr rule = parseAndGetFirstRuleDescrFromFile(
@@ -1905,7 +1899,6 @@
         pat.getConstraint();
     }
 
-    @Disabled("Priority : High | Parse attribute agenda-group")
     @Test
     public void parse_PackageAttributes() throws Exception {
         final PackageDescr pkg = parseAndGetPackageDescrFromFile(
@@ -3132,7 +3125,6 @@
 
     }
 
-    @Disabled("Priority : High | Implement unification")
     @Test
     public void parse_UnificationBinding() throws Exception {
         final String text = "rule X when $p := Person( $name := name, $loc : location ) then end";