org.drools.decisiontable.parser.LhsBuilder Maven / Gradle / Ivy
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* 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 org.drools.decisiontable.parser;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.drools.template.model.SnippetBuilder;
import org.drools.template.parser.DecisionTableParseException;
/**
* This utility will build up a list of constraints for a column.
* For instance, the column has been spanned across multiple cells, and the cells below
* contain the constraints.
*/
public class LhsBuilder implements SourceBuilder {
private static final char QUOTE_DOUBLE = 0x22;
private static final char QUOTE_LEFT = 0x201c;
private static final char QUOTE_RIGHT = 0x201d;
private int headerRow;
private int headerCol;
private String colDefPrefix;
private String colDefSuffix;
private boolean multiple;
private boolean forAll;
private String andop;
private Map constraints;
private List values;
private boolean hasValues;
private Map fieldTypes;
private static Set operators;
private static Set annotations;
static {
operators = new HashSet<>();
operators.add( "==" );
operators.add( "=" );
operators.add( "!=" );
operators.add( "<" );
operators.add( ">" );
operators.add( "<=" );
operators.add( ">=" );
operators.add( "contains" );
operators.add( "matches" );
operators.add( "memberOf" );
operators.add( "str[startsWith]" );
operators.add( "str[endsWith]" );
operators.add( "str[length]" );
annotations = new HashSet<>();
annotations.add( "@watch" );
}
private static final Pattern patParFrm = Pattern.compile( "\\(\\s*\\)\\s*from\\b" );
private static final Pattern patFrm = Pattern.compile( "\\s+from\\s+" );
private static final Pattern patPar = Pattern.compile( "\\(\\s*\\)\\s*\\Z" );
private static final Pattern patEval = Pattern.compile( "\\beval\\s*(?:\\(\\s*\\)\\s*)?$" );
private static final Pattern patOopath = Pattern.compile( ".*\\:\\s*/.*" );
/**
* @param colDefinition
* The initial column definition that is shared via merged cells.
*/
public LhsBuilder( int row,
int column,
String colDefinition ) {
this.headerRow = row;
this.headerCol = column;
this.constraints = new HashMap<>();
this.fieldTypes = new HashMap<>();
this.values = new ArrayList<>();
this.forAll = false;
String colDef = colDefinition == null ? "" : colDefinition;
String annDef = "";
int annPos = findFirstAnnotationPos(colDef);
if (annPos > 0) {
annDef = " " + colDef.substring( annPos );
colDef = colDef.substring( 0, annPos ).trim();
}
if ( "".equals( colDef ) ) {
colDefPrefix = colDefSuffix = "";
multiple = false;
andop = "";
return;
}
multiple = true;
// ...eval
Matcher matEval = patEval.matcher( colDef );
if ( matEval.find() ) {
colDefPrefix = colDef.substring( 0, matEval.start() ) + "eval(";
colDefSuffix = ")";
andop = " && ";
return;
}
andop = ", ";
// ...( ) from...
Matcher matParFrm = patParFrm.matcher( colDef );
if ( matParFrm.find() ) {
colDefPrefix = colDef.substring( 0, matParFrm.start() ) + '(';
colDefSuffix = ") from" + colDef.substring( matParFrm.end() ) + annDef;
return;
}
// ...from...
Matcher matFrm = patFrm.matcher( colDef );
if ( matFrm.find() ) {
colDefPrefix = colDef.substring( 0, matFrm.start() ) + "(";
colDefSuffix = ") from " + colDef.substring( matFrm.end() ) + annDef;
return;
}
// ...( )...
Matcher matPar = patPar.matcher( colDef );
if ( matPar.find() ) {
colDefPrefix = colDef.substring( 0, matPar.start() ) + '(';
colDefSuffix = ")" + colDef.substring( matPar.end() ) + annDef;
return;
}
if ( patOopath.matcher( colDef ).matches() ) {
colDefPrefix = colDef + '[';
colDefSuffix = "]" + annDef;
return;
}
//
if (colDef.endsWith( ")" )) {
colDefPrefix = colDef;
colDefSuffix = annDef;
} else {
colDefPrefix = colDef + '(';
colDefSuffix = ")" + annDef;
}
}
private int findFirstAnnotationPos(String colDef) {
int pos = -1;
for (String annotation : annotations) {
int annPos = colDef.indexOf( annotation );
if (annPos > 0) {
pos = pos < 0 ? annPos : Math.min( pos, annPos );
}
}
return pos;
}
public ActionType.Code getActionTypeCode() {
return ActionType.Code.CONDITION;
}
public void addTemplate( int row,
int column,
String content ) {
content = content.trim();
if ( constraints.containsKey( column ) ) {
throw new IllegalArgumentException( "Internal error: Can't have a constraint added twice to one spreadsheet col." );
}
if ( fieldTypes.containsKey( column ) ) {
throw new IllegalArgumentException( "Internal error: Can't have a FieldType added twice to one spreadsheet col." );
}
//we can wrap all values in quotes, it all works
final FieldType fieldType = calcFieldType( content );
if ( !isMultipleConstraints() ) {
constraints.put( column, content );
} else {
switch (fieldType) {
case FORALL_FIELD:
forAll = true;
constraints.put( column, content );
break;
case NORMAL_FIELD:
constraints.put( column, content );
break;
case SINGLE_FIELD:
constraints.put( column, content + " == \"" + SnippetBuilder.PARAM_STRING + "\"" );
break;
case OPERATOR_FIELD:
constraints.put( column, content + " \"" + SnippetBuilder.PARAM_STRING + "\"" );
break;
case QUESTION_FIELD:
constraints.put( column, content.substring( 0, content.length()-1 ) );
break;
}
}
this.fieldTypes.put( column, fieldType );
}
public void clearValues() {
this.hasValues = false;
this.values.clear();
}
public void addCellValue( int row, int column, String value) {
addCellValue( row, column, value, true );
}
public void addCellValue( int row, int column, String value, boolean trim) {
this.hasValues = true;
if (this.constraints.isEmpty()) {
return;
}
Integer key = Integer.valueOf( column );
String content = this.constraints.get( key );
if ( content == null ) {
throw new DecisionTableParseException( "No code snippet for CONDITION in cell " +
RuleSheetParserUtil.rc2name( this.headerRow + 2, this.headerCol ) );
}
SnippetBuilder snip = new SnippetBuilder( content, trim );
String result = snip.build( fixValue( column, value ) );
this.values.add( result );
}
/**
* If the type of the column is either FieldType.SINGLE_FIELD or FieldType.OPERATOR_FIELD we have
* added quotation-marks around the template parameter. Consequentially if a cell value included the
* quotation-marks (i.e. for an empty-string or white-space) we need to remove the additional
* quotation-marks.
* @param value
* @return
*/
private String fixValue( final int column,
final String value ) {
String _value = value;
final FieldType fieldType = this.fieldTypes.get( column );
if ( fieldType == FieldType.NORMAL_FIELD || !isMultipleConstraints() || isForAll() ) {
return value;
}
if ( isDelimitedString( _value ) ) {
_value = _value.substring( 1,
_value.length() - 1 );
}
return _value;
}
public String getResult() {
StringBuilder buf = new StringBuilder();
if ( !isMultipleConstraints() ) {
String nl = "";
for ( String content : values ) {
buf.append( nl ).append( content );
nl = "\n";
}
return buf.toString();
} else {
buf.append( this.colDefPrefix );
String sep = "";
for ( String constraint : values ) {
buf.append( sep ).append( constraint );
sep = this.andop;
}
buf.append( colDefSuffix );
return buf.toString();
}
}
/**
* Returns true if this is building up multiple constraints as in:
* Foo(a ==b, c == d) etc...
* If not, then it it really just like the "classic" style DTs.
*/
boolean isMultipleConstraints() {
return multiple;
}
/**
* Check whether the column definition is a 'forall' construct. In these
* situations we do not attempt to strip quotation marks from field values.
* @return true if the column definition is 'forall'
*/
boolean isForAll() {
return forAll;
}
/**
* Work out the type of "field" that is being specified,
* as in :
* age
* age <
* age == $param
* age == $1 || age == $2
* forall{age < $}{,}
*
* etc. as we treat them all differently.
*/
public FieldType calcFieldType( String content ) {
final SnippetBuilder.SnippetType snippetType = SnippetBuilder.getType( content );
if ( snippetType == SnippetBuilder.SnippetType.FORALL ) {
return FieldType.FORALL_FIELD;
} else if ( snippetType != SnippetBuilder.SnippetType.SINGLE ) {
return FieldType.NORMAL_FIELD;
}
for ( String op : operators ) {
if ( content.endsWith( op ) ) {
return FieldType.OPERATOR_FIELD;
}
}
return content.endsWith( "?" ) ? FieldType.QUESTION_FIELD : FieldType.SINGLE_FIELD;
}
enum FieldType {
SINGLE_FIELD, OPERATOR_FIELD, NORMAL_FIELD, QUESTION_FIELD, FORALL_FIELD;
}
public boolean hasValues() {
return hasValues;
}
private boolean isDelimitedString( final String content ) {
return isDelimitedString( content,
QUOTE_DOUBLE,
QUOTE_DOUBLE ) ||
isDelimitedString( content,
QUOTE_LEFT,
QUOTE_RIGHT ) ||
isDelimitedString( content,
QUOTE_LEFT,
QUOTE_LEFT ) ||
isDelimitedString( content,
QUOTE_RIGHT,
QUOTE_RIGHT );
}
private boolean isDelimitedString( final String content,
final char openQuote,
final char closeQuote ) {
return ( content.indexOf( openQuote ) == 0 && content.indexOf( closeQuote,
1 ) == content.length() - 1 );
}
public int getColumn() {
return headerCol;
}
}