001: package org.drools.lang.dsl;
002:
003: /*
004: * Copyright 2005 JBoss Inc
005: *
006: * Licensed under the Apache License, Version 2.0 (the "License");
007: * you may not use this file except in compliance with the License.
008: * You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS,
014: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: * See the License for the specific language governing permissions and
016: * limitations under the License.
017: */
018:
019: import java.io.BufferedReader;
020: import java.io.IOException;
021: import java.io.Reader;
022: import java.util.Collections;
023: import java.util.HashMap;
024: import java.util.Iterator;
025: import java.util.LinkedList;
026: import java.util.List;
027: import java.util.Map;
028: import java.util.regex.Matcher;
029: import java.util.regex.Pattern;
030:
031: import org.drools.compiler.DroolsError;
032: import org.drools.lang.Expander;
033: import org.drools.lang.ExpanderException;
034:
035: /**
036: * The default expander uses String templates to provide pseudo natural language,
037: * as well as general DSLs.
038: *
039: * For most people, this should do the job just fine.
040: */
041: public class DefaultExpander implements Expander {
042:
043: // Be EXTREMELLY careful if you decide to change bellow regexp's
044: //
045: // bellow regexp is used to find and parse rule parts: header, LHS, RHS, trailer, etc
046: private static final String rulesExpr = "(^\\s*rule.*?$.*?^\\s*when.*?$)(.*?)(^\\s*then.*?$)(.*?)(^\\s*end)";
047: // bellow regexp is used to find and parse query parts: header, condition, trailer
048: private static final String queryExpr = "(^\\s*query.*?$)(.*?)(^\\s*end)";
049:
050: // bellow we combine and compile above expressions into a pattern object
051: private static final Pattern finder = Pattern.compile("("
052: + rulesExpr + "|" + queryExpr + ")", Pattern.DOTALL
053: | Pattern.MULTILINE);
054: // bellow pattern is used to find a pattern's constraint list
055: private static final Pattern patternFinder = Pattern
056: .compile("\\((.*?)\\)");
057:
058: private final Map mappings = new HashMap();
059: private final List keywords = new LinkedList();
060: private final List condition = new LinkedList();
061: private final List consequence = new LinkedList();
062: private final List cleanup = new LinkedList();
063:
064: private List errors = Collections.EMPTY_LIST;
065:
066: /**
067: * Creates a new DefaultExpander
068: */
069: public DefaultExpander() {
070: this .cleanup.add(new DefaultDSLMappingEntry(
071: DSLMappingEntry.KEYWORD, null, "expander {name}", ""));
072: }
073:
074: /**
075: * Add the new mapping to this expander.
076: * @param mapping
077: */
078: public void addDSLMapping(final DSLMapping mapping) {
079: this .mappings.put(mapping.getIdentifier(), mapping);
080: for (final Iterator it = mapping.getEntries().iterator(); it
081: .hasNext();) {
082: final DSLMappingEntry entry = (DSLMappingEntry) it.next();
083: if (entry.getSection() == DSLMappingEntry.KEYWORD) {
084: this .keywords.add(entry);
085: } else if (entry.getSection() == DSLMappingEntry.CONDITION) {
086: this .condition.add(entry);
087: } else if (entry.getSection() == DSLMappingEntry.CONSEQUENCE) {
088: this .consequence.add(entry);
089: } else {
090: // if any, then add to them both condition and consequence
091: this .condition.add(entry);
092: this .consequence.add(entry);
093: }
094: }
095: }
096:
097: /**
098: * @inheritDoc
099: * @throws IOException
100: */
101: public String expand(final Reader drlReader) throws IOException {
102: return this .expand(this .loadDrlFile(drlReader));
103: }
104:
105: /**
106: * @inheritDoc
107: * @throws IOException
108: */
109: public String expand(String drl) {
110: drl = expandKeywords(drl);
111: drl = cleanupExpressions(drl);
112: final StringBuffer buf = expandConstructions(drl);
113: return buf.toString();
114: }
115:
116: /**
117: * Expand constructions like rules and queries
118: *
119: * @param drl
120: * @return
121: */
122: private StringBuffer expandConstructions(final String drl) {
123: // parse and expand specific areas
124: final Matcher m = finder.matcher(drl);
125:
126: final StringBuffer buf = new StringBuffer();
127: while (m.find()) {
128: final StringBuffer expanded = new StringBuffer();
129: final String constr = m.group(1).trim();
130: if (constr.startsWith("rule")) {
131: // match rule
132: String headerFragment = m.group(2);
133: expanded.append(headerFragment); // adding rule header and attributes
134: String lhsFragment = m.group(3);
135: expanded.append(this .expandLHS(lhsFragment,
136: countNewlines(headerFragment) + 1)); // adding expanded LHS
137: String thenFragment = m.group(4);
138:
139: expanded.append(thenFragment); // adding "then" header
140: expanded.append(this .expandRHS(m.group(5),
141: countNewlines(headerFragment + lhsFragment
142: + thenFragment) + 1)); // adding expanded RHS
143: expanded.append(m.group(6)); // adding rule trailer
144: expanded.append("\n");
145: } else if (constr.startsWith("query")) {
146: // match query
147: String fragment = m.group(7);
148: expanded.append(fragment); // adding query header and attributes
149: expanded.append(this .expandLHS(m.group(8),
150: countNewlines(fragment) + 1)); // adding expanded LHS
151: expanded.append(m.group(9)); // adding query trailer
152: expanded.append("\n");
153: } else {
154: // strange behavior
155: this .addError(new ExpanderException(
156: "Unable to expand statement: " + constr, 0));
157: }
158: m.appendReplacement(buf, expanded.toString().replaceAll(
159: "\\$", "\\\\\\$"));
160: }
161: m.appendTail(buf);
162: return buf;
163: }
164:
165: private int countNewlines(final String drl) {
166: char[] cs = drl.toCharArray();
167: int count = 0;
168: for (int i = 0; i < cs.length; i++) {
169: {
170: if (cs[i] == '\n')
171: count++;
172: }
173: }
174: return count;
175: }
176:
177: /**
178: * Clean up constructions that exists only in the unexpanded code
179: *
180: * @param drl
181: * @return
182: */
183: private String cleanupExpressions(String drl) {
184: // execute cleanup
185: for (final Iterator it = this .cleanup.iterator(); it.hasNext();) {
186: final DSLMappingEntry entry = (DSLMappingEntry) it.next();
187: drl = entry.getKeyPattern().matcher(drl).replaceAll(
188: entry.getValuePattern());
189: }
190: return drl;
191: }
192:
193: /**
194: * Expand all configured keywords
195: *
196: * @param drl
197: * @return
198: */
199: private String expandKeywords(String drl) {
200: // apply all keywords templates
201: for (final Iterator it = this .keywords.iterator(); it.hasNext();) {
202: final DSLMappingEntry entry = (DSLMappingEntry) it.next();
203: drl = entry.getKeyPattern().matcher(drl).replaceAll(
204: entry.getValuePattern());
205: }
206: return drl;
207: }
208:
209: /**
210: * Expand LHS for a construction
211: * @param lhs
212: * @param lineOffset
213: * @return
214: */
215: private String expandLHS(final String lhs, int lineOffset) {
216: final StringBuffer buf = new StringBuffer();
217: final String[] lines = lhs.split("\n"); // since we assembled the string, we know line breaks are \n
218: final String[] expanded = new String[lines.length]; // buffer for expanded lines
219: int lastExpanded = -1;
220: int lastPattern = -1;
221: for (int i = 0; i < lines.length; i++) {
222: final String trimmed = lines[i].trim();
223: expanded[++lastExpanded] = lines[i];
224:
225: if (trimmed.length() == 0 || trimmed.startsWith("#")
226: || trimmed.startsWith("//")) { // comments
227: // do nothing
228: } else if (trimmed.startsWith(">")) { // passthrough code
229: // simply remove the passthrough mark character
230: expanded[lastExpanded] = lines[i]
231: .replaceFirst(">", " ");
232: } else { // regular expansion
233: // expand the expression
234: for (final Iterator it = this .condition.iterator(); it
235: .hasNext();) {
236: final DSLMappingEntry entry = (DSLMappingEntry) it
237: .next();
238: expanded[lastExpanded] = entry.getKeyPattern()
239: .matcher(expanded[lastExpanded])
240: .replaceAll(entry.getValuePattern());
241: }
242:
243: // do we need to report errors for that?
244: if (lines[i].equals(expanded[lastExpanded])) {
245: // report error
246: this .addError(new ExpanderException(
247: "Unable to expand: [" + lines[i] + "]", i
248: + lineOffset));
249: }
250: // but if the original starts with a "-", it means we need to add it
251: // as a constraint to the previous pattern
252: if (trimmed.startsWith("-")
253: && (!lines[i].equals(expanded[lastExpanded]))) {
254: int lastMatchStart = -1;
255: int lastMatchEnd = -1;
256: String constraints = "";
257: if (lastPattern >= 0) {
258: final Matcher m2 = patternFinder
259: .matcher(expanded[lastPattern]);
260: while (m2.find()) {
261: lastMatchStart = m2.start();
262: lastMatchEnd = m2.end();
263: constraints = m2.group(1).trim();
264: }
265: }
266: if (lastMatchStart > -1) {
267: // rebuilding previous pattern structure
268: expanded[lastPattern] = expanded[lastPattern]
269: .substring(0, lastMatchStart)
270: + "( "
271: + constraints
272: + ((constraints.length() == 0) ? ""
273: : ", ")
274: + expanded[lastExpanded].trim()
275: + " )"
276: + expanded[lastPattern]
277: .substring(lastMatchEnd);
278: } else {
279: // error, pattern not found to add constraint to
280: // TODO: can we report character position?
281: this .addError(new ExpanderException(
282: "No pattern was found to add the constraint to: "
283: + lines[i], i + lineOffset));
284: }
285: lastExpanded--;
286: } else {
287: lastPattern = lastExpanded;
288: }
289: }
290: }
291: for (int i = 0; i <= lastExpanded; i++) {
292: buf.append(expanded[i]);
293: buf.append("\n");
294: }
295:
296: return buf.toString();
297: }
298:
299: /**
300: * Expand RHS for rules
301: *
302: * @param lhs
303: * @return
304: */
305: private String expandRHS(final String lhs, int lineOffset) {
306: final StringBuffer buf = new StringBuffer();
307: final String[] lines = lhs.split("\n"); // since we assembled the string, we know line breaks are \n
308: for (int i = 0; i < lines.length; i++) {
309: final String trimmed = lines[i].trim();
310:
311: if (trimmed.length() == 0 || trimmed.startsWith("#")
312: || trimmed.startsWith("//")) { // comments
313: buf.append(lines[i]);
314: } else if (trimmed.startsWith(">")) { // passthrough code
315: buf.append(lines[i].replaceFirst(">", ""));
316: } else { // regular expansions
317: String expanded = lines[i];
318: for (final Iterator it = this .consequence.iterator(); it
319: .hasNext();) {
320: final DSLMappingEntry entry = (DSLMappingEntry) it
321: .next();
322: expanded = entry.getKeyPattern().matcher(expanded)
323: .replaceAll(entry.getValuePattern());
324: }
325: buf.append(expanded);
326: // do we need to report errors for that?
327: if (lines[i].equals(expanded)) {
328: // report error
329: this .addError(new ExpanderException(
330: "Unable to expand: " + lines[i], i
331: + lineOffset));
332: }
333: }
334: buf.append("\n");
335: }
336:
337: return buf.toString();
338: }
339:
340: // Reads the stream into a String
341: private String loadDrlFile(final Reader drl) throws IOException {
342: final StringBuffer buf = new StringBuffer();
343: final BufferedReader input = new BufferedReader(drl);
344: String line = null;
345: while ((line = input.readLine()) != null) {
346: buf.append(line);
347: buf.append("\n");
348: }
349: return buf.toString();
350: }
351:
352: private void addError(final DroolsError error) {
353: if (this .errors == Collections.EMPTY_LIST) {
354: this .errors = new LinkedList();
355: }
356: this .errors.add(error);
357: }
358:
359: /**
360: * @inheritDoc
361: */
362: public List getErrors() {
363: return this .errors;
364: }
365:
366: /**
367: * @inheritDoc
368: */
369: public boolean hasErrors() {
370: return !this.errors.isEmpty();
371: }
372:
373: }
|