package org.drools.lang.dsl;
/*
* Copyright 2005 JBoss Inc
*
* 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.
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Formatter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.drools.lang.Expander;
import org.drools.lang.ExpanderException;
/**
* The default expander uses String templates to provide pseudo natural language,
* as well as general DSLs.
*
* For most people, this should do the job just fine.
*/
public class DefaultExpander
implements
Expander {
private static final String ruleOrQuery =
"^(?: " + // alternatives rule...end, query...end
"\\p{Blank}*(rule\\b.+?^\\s*when\\b)" + // 1: rule, name, attributes. when starts a line
"(.*?) " + // 2: condition
"^(\\s*then) " + // 3: then starts a line
"(.*?) " + // 4: consequence
"(^\\s*end.*?$) " + // 5: end starts a line
"|\\s*(query\\s+ " +
"(?:\"[^\"]+\"|'[^']+'|\\S+)" +
"(?:\\s+\\([^)]+)?) " + // 6: query, name, arguments
"(.*?) " + // 7: condition
"(^\\s*end.*?$) " + // 8: end starts a line
")";
private static final Pattern finder =
Pattern.compile( ruleOrQuery,
Pattern.DOTALL | Pattern.MULTILINE | Pattern.COMMENTS );
// This pattern is used to find a pattern's constraint list
private static final Pattern patternFinder = Pattern.compile( "\\((.*?)\\)" );
// Pattern for finding a variable reference, restricted to Unicode letters and digits.
private static final Pattern varRefPat = Pattern.compile( "\\{([\\p{L}\\d]+)\\}" );
// Pattern for initial integer
private static final Pattern intPat = Pattern.compile( "^(-?\\d+).*$" );
private final List<DSLMappingEntry> keywords = new LinkedList<DSLMappingEntry>();
private final List<DSLMappingEntry> condition = new LinkedList<DSLMappingEntry>();
private final List<DSLMappingEntry> consequence = new LinkedList<DSLMappingEntry>();
private final List<DSLMappingEntry> cleanup = new LinkedList<DSLMappingEntry>();
private List<Map<String,String>> substitutions;
private List<ExpanderException> errors = Collections.emptyList();
private boolean showResult = false;
private boolean showSteps = false;
private boolean showWhen = false;
private boolean showThen = false;
private boolean showKeyword = false;
/**
* Creates a new DefaultExpander
*/
public DefaultExpander() {
this.cleanup.add( new AntlrDSLMappingEntry( DSLMappingEntry.KEYWORD,
DSLMappingEntry.EMPTY_METADATA,
"expander {name}",
"",
"expander (.*?)",
"" ) );
}
/**
* Add the new mapping to this expander.
* @param mapping
*/
public void addDSLMapping(final DSLMapping mapping) {
for ( DSLMappingEntry entry : mapping.getEntries() ) {
if ( DSLMappingEntry.KEYWORD.equals( entry.getSection() ) ) {
this.keywords.add( entry );
} else if ( DSLMappingEntry.CONDITION.equals( entry.getSection() ) ) {
this.condition.add( entry );
} else if ( DSLMappingEntry.CONSEQUENCE.equals( entry.getSection() ) ) {
this.consequence.add( entry );
} else {
// if any, then add to them both condition and consequence
this.condition.add( entry );
this.consequence.add( entry );
}
}
if( mapping.getOption( "result" ) ) showResult = true;
if( mapping.getOption( "steps" ) ) showSteps = true;
if( mapping.getOption( "keyword" ) ) showThen = true;
if( mapping.getOption( "when" ) ) showWhen = true;
if( mapping.getOption( "then" ) ) showThen = true;
}
/**
* @inheritDoc
* @throws IOException
*/
public String expand(final Reader drlReader) throws IOException {
return this.expand( this.loadDrlFile( drlReader ) );
}
/**
* @inheritDoc
* @throws IOException
*/
public String expand(String drl) {
drl = expandKeywords( drl );
drl = cleanupExpressions( drl );
final StringBuffer buf = expandConstructions( drl );
if( showResult ){
StringBuffer show = new StringBuffer();
Formatter fmt = new Formatter( show );
int offset = 0;
int nlPos;
int iLine = 1;
while( (nlPos = buf.indexOf( "\n", offset ) ) >= 0 ){
fmt.format( "%4d %s", Integer.valueOf(iLine++), buf.substring( offset, nlPos + 1 ) );
offset = nlPos + 1;
}
System.out.println( "=== DRL xpanded from DSLR ===" );
System.out.println( show.toString() );
System.out.println( "=============================" );
}
return buf.toString();
}
private static final String nl = System.getProperty( "line.separator" );
private static int countNewlines( final String drl, int start, int end ){
int count = 0;
int pos = start;
while( (pos = drl.indexOf( nl, pos )) >= 0 ){
// System.out.println( "pos at " + pos );
if( pos >= end ) break;
pos += nl.length();
count++;
}
return count;
}
/**
* Expand constructions like rules and queries
*
* @param drl
* @return
*/
private StringBuffer expandConstructions(final String drl) {
// display keys if requested
if( showKeyword ){
for( DSLMappingEntry entry: this.keywords ){
System.out.println( "keyword: " + entry.getMappingKey() );
System.out.println( " " + entry.getKeyPattern() );
}
}
if( showWhen ){
for( DSLMappingEntry entry: this.condition ){
System.out.println( "when: " + entry.getMappingKey() );
System.out.println( " " + entry.getKeyPattern() );
// System.out.println( " " + entry.getValuePattern() );
}
}
if( showThen ){
for( DSLMappingEntry entry: this.consequence ){
System.out.println( "then: " + entry.getMappingKey() );
System.out.println( " " + entry.getKeyPattern() );
}
}
// parse and expand specific areas
final Matcher m = finder.matcher( drl );
final StringBuffer buf = new StringBuffer();
int drlPos = 0;
int linecount = 0;
while ( m.find() ) {
final StringBuilder expanded = new StringBuilder();
int newPos = m.start();
linecount += countNewlines( drl, drlPos, newPos );
drlPos = newPos;
String constr = m.group().trim();
if( constr.startsWith( "rule" ) ){
String headerFragment = m.group( 1 );
expanded.append( headerFragment ); // adding rule header and attributes
String lhsFragment = m.group( 2 );
expanded.append( this.expandLHS( lhsFragment, linecount + countNewlines( drl, drlPos, m.start(2) ) + 1 ) );
String thenFragment = m.group( 3 );
expanded.append( thenFragment ); // adding "then" header
String rhsFragment = this.expandRHS( m.group( 4 ), linecount + countNewlines( drl, drlPos, m.start(4) ) + 1 );
expanded.append( rhsFragment );
expanded.append( m.group( 5 ) ); // adding rule trailer
} else if( constr.startsWith( "query" ) ){
String fragment = m.group( 6 );
expanded.append( fragment ); // adding query header and attributes
String lhsFragment = this.expandLHS( m.group( 7 ), linecount + countNewlines( drl, drlPos, m.start(7) ) + 1 );
expanded.append( lhsFragment );
expanded.append( m.group( 8 ) ); // adding query trailer
} else {
// strange behavior
this.addError( new ExpanderException( "Unable to expand statement: " + constr, 0 ) );
}
m.appendReplacement( buf, Matcher.quoteReplacement( expanded.toString() ) );
}
m.appendTail( buf );
return buf;
}
/**
* Clean up constructions that exists only in the unexpanded code
*
* @param drl
* @return
*/
private String cleanupExpressions(String drl) {
// execute cleanup
for ( final DSLMappingEntry entry : this.cleanup ) {
drl = entry.getKeyPattern().matcher( drl ).replaceAll( entry.getValuePattern() );
}
return drl;
}
/**
* Expand all configured keywords
*
* @param drl
* @return
*/
private String expandKeywords(String drl) {
// apply all keywords templates
for ( final DSLMappingEntry entry : this.keywords ) {
drl = entry.getKeyPattern().matcher( drl ).replaceAll( entry.getValuePattern() );
}
return drl;
}
/**
* Perform the substitutions.
* @param exp a DSLR source line to be expanded
* @param entries the appropriate DSL keys and values
* @param line line number
* @return the expanden line
*/
private String substitute( String exp, List<DSLMappingEntry> entries, int line ){
if( entries.size() == 0 ){
this.addError( new ExpanderException( "No mapping entries for expanding: " + exp, line ) );
return exp;
}
if( showSteps ){
System.out.println("to expand: |" + exp + "|");
}
Map<String,String> key2value = new HashMap<String,String>();
for ( final DSLMappingEntry entry : entries ) {
Map<String,Integer> vars = entry.getVariables();
String vp = entry.getValuePattern();
Pattern kp = entry.getKeyPattern();
Matcher m = kp.matcher( exp );
int startPos = 0;
boolean match = false;
while( startPos < exp.length() && m.find( startPos ) ){
match = true;
if( showSteps ){
System.out.println(" matches: " + kp.toString() );
}
// Replace the range of group 0.
String target = m.group( 0 );
if( ! vars.keySet().isEmpty() ){
// Build a pattern matching any variable enclosed in braces.
StringBuilder sb = new StringBuilder( );
String del = "\\{(";
for( String key: vars.keySet() ){
sb.append( del ).append( Pattern.quote( key ) );
del = "|";
}
sb.append( ")(?:!(uc|lc|ucfirst|num|.*))?\\}" );
Pattern allkeyPat = Pattern.compile( sb.toString() );
Matcher allkeyMat = allkeyPat.matcher( vp );
// While the pattern matches, get the actual key and replace by '$' + index
while( allkeyMat.find() ){
String theKey = allkeyMat.group( 1 );
String theFunc = allkeyMat.group( 2 );
String theValue = m.group( vars.get( theKey ) );
if( theFunc != null ){
if( "uc".equals( theFunc ) ){
theValue = theValue.toUpperCase();
} else if( "lc".equals( theFunc ) ){
theValue = theValue.toLowerCase();
} else if( "ucfirst".equals( theFunc ) && theValue.length() > 0 ){
theValue = theValue.substring( 0, 1 ).toUpperCase() +
theValue.substring( 1 ).toLowerCase();
} else if( theFunc.startsWith( "num" ) ){
// kill all non-digits, but keep '-'
String numStr = theValue.replaceAll( "[^-\\d]+", "" );
try {
long numLong = Long.parseLong( numStr );
if( theValue.matches( "^.*[.,]\\d\\d(?:\\D.*|$)") ){
numStr = Long.toString( numLong );
theValue = numStr.substring( 0, numStr.length()-2) + '.' + numStr.substring(numStr.length()-2);
} else {
theValue = Long.toString( numLong );
}
} catch( NumberFormatException nfe ){
// silently ignore - keep the value as it is
}
} else {
StringTokenizer strTok = new StringTokenizer( theFunc, "?/", true );
boolean compare = true;
int toks = strTok.countTokens();
while( toks >= 4 ) {
String key = strTok.nextToken();
String qmk = strTok.nextToken(); // '?'
String val = strTok.nextToken(); // to use
String sep = strTok.nextToken(); // '/'
if( key.equals( theValue ) ){
theValue = val;
break;
}
toks -= 4;
if( toks < 4 ){
theValue = strTok.nextToken();
break;
}
}
}
}
vp = vp.substring(0, allkeyMat.start()) + theValue + vp.substring( allkeyMat.end() );
allkeyMat.reset( vp );
key2value.put( theKey, theValue );
}
}
// Try to find any matches from previous lines.
Matcher varRefMat = varRefPat.matcher( vp );
while( varRefMat.find() ){
String theKey = varRefMat.group( 1 );
for( int ientry = substitutions.size() - 1; ientry >= 0; ientry-- ){
String theValue = substitutions.get( ientry ).get( theKey );
if( theValue != null ){
// replace it
vp = vp.substring(0, varRefMat.start()) + theValue + vp.substring( varRefMat.end() );
varRefMat.reset( vp );
break;
}
}
}
// add the new set of substitutions
if( key2value.size() > 0 ){
substitutions.add( key2value );
}
// now replace the target
exp = exp.substring(0, m.start()) + vp + exp.substring( m.end() );
if( match && showSteps ){
System.out.println(" result: |" + exp + "|" );
}
startPos = m.start() + vp.length();
m.reset( exp );
}
}
return exp;
}
/**
* Expand LHS for a construction
* @param lhs
* @param lineOffset
* @return
*/
private String expandLHS( final String lhs, int lineOffset) {
substitutions = new ArrayList<Map<String,String>>();
// System.out.println( "*** LHS>" + lhs + "<" );
final StringBuilder buf = new StringBuilder();
final String[] lines = lhs.split( "\n", -1 ); // since we assembled the string, we know line breaks are \n
final String[] expanded = new String[lines.length]; // buffer for expanded lines
int lastExpanded = -1;
int lastPattern = -1;
for ( int i = 0; i < lines.length - 1; i++ ) {
final String trimmed = lines[i].trim();
expanded[++lastExpanded] = lines[i];
if ( trimmed.length() == 0 || trimmed.startsWith( "#" ) || trimmed.startsWith( "//" ) ) { // comments
// do nothing
} else if ( trimmed.startsWith( ">" ) ) { // passthrough code
// simply remove the passthrough mark character
expanded[lastExpanded] = lines[i].replaceFirst( ">", " " );
} else { // regular expansion
// expand the expression
expanded[lastExpanded] = substitute( expanded[lastExpanded], this.condition, i + lineOffset );
// do we need to report errors for that?
if ( lines[i].equals( expanded[lastExpanded] ) ) {
// report error
this.addError( new ExpanderException( "Unable to expand: " + lines[i].replaceAll( "[\n\r]",
"" ).trim(),
i + lineOffset ) );
}
// but if the original starts with a "-", it means we need to add it
// as a constraint to the previous pattern
if ( trimmed.startsWith( "-" ) && (!lines[i].equals( expanded[lastExpanded] )) ) {
int lastMatchStart = -1;
int lastMatchEnd = -1;
String constraints = "";
if ( lastPattern >= 0 ) {
final Matcher m2 = patternFinder.matcher( expanded[lastPattern] );
while ( m2.find() ) {
lastMatchStart = m2.start();
lastMatchEnd = m2.end();
constraints = m2.group( 1 ).trim();
}
}
if ( lastMatchStart > -1 ) {
// rebuilding previous pattern structure
expanded[lastPattern] = expanded[lastPattern].substring( 0,
lastMatchStart ) + "( " + constraints + ((constraints.length() == 0) ? "" : ", ") + expanded[lastExpanded].trim() + " )"
+ expanded[lastPattern].substring( lastMatchEnd );
} else {
// error, pattern not found to add constraint to
this.addError( new ExpanderException( "No pattern was found to add the constraint to: " + lines[i].trim(),
i + lineOffset ) );
}
lastExpanded--;
} else {
lastPattern = lastExpanded;
}
}
}
for ( int i = 0; i <= lastExpanded; i++ ) {
buf.append( expanded[i] );
buf.append( "\n" );
}
return buf.toString();
}
/**
* Expand RHS for rules
*
* @param lhs
* @return
*/
private String expandRHS(final String lhs, int lineOffset) {
final StringBuilder buf = new StringBuilder();
final String[] lines = lhs.split( "\n", -1 ); // since we assembled the string, we know line breaks are \n
for ( int i = 0; i < lines.length -1; i++ ) {
final String trimmed = lines[i].trim();
if ( trimmed.length() == 0 || trimmed.startsWith( "#" ) || trimmed.startsWith( "//" ) ) { // comments
buf.append( lines[i] );
} else if ( trimmed.startsWith( ">" ) ) { // passthrough code
buf.append( lines[i].replaceFirst( ">",
"" ) );
} else { // regular expansions
String expanded = substitute( lines[i], this.consequence, i + lineOffset );
buf.append( expanded );
// do we need to report errors for that?
if ( lines[i].equals( expanded ) ) {
// report error
this.addError( new ExpanderException( "Unable to expand: " + lines[i],
i + lineOffset ) );
}
}
buf.append( "\n" );
}
if ( lines.length == 0 ) {
buf.append( "\n" );
}
return buf.toString();
}
// Reads the stream into a String
private String loadDrlFile(final Reader drl) throws IOException {
final StringBuilder buf = new StringBuilder();
final BufferedReader input = new BufferedReader( drl );
String line = null;
while ( (line = input.readLine()) != null ) {
buf.append( line );
buf.append( "\n" );
}
return buf.toString();
}
private void addError(final ExpanderException error) {
if ( this.errors == Collections.EMPTY_LIST ) {
this.errors = new LinkedList<ExpanderException>();
}
this.errors.add( error );
}
/**
* @inheritDoc
*/
public List<ExpanderException> getErrors() {
return this.errors;
}
/**
* @inheritDoc
*/
public boolean hasErrors() {
return !this.errors.isEmpty();
}
}