001: /*
002: * hammurapi-rules @mesopotamia.version@
003: * Hammurapi rules engine.
004: * Copyright (C) 2005 Hammurapi Group
005: *
006: * This program is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation; either
009: * version 2 of the License, or (at your option) any later version.
010: *
011: * This program is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: *
016: * You should have received a copy of the GNU Lesser General Public
017: * License along with this library; if not, write to the Free Software
018: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019: *
020: * URL: http://http://www.hammurapi.biz
021: * e-Mail: support@hammurapi.biz
022: */
023: package biz.hammurapi.rules;
024:
025: import java.lang.reflect.Method;
026: import java.util.ArrayList;
027: import java.util.Collection;
028: import java.util.HashSet;
029: import java.util.Iterator;
030:
031: import biz.hammurapi.config.ConfigurationException;
032: import biz.hammurapi.dispatch.InvocationHandler;
033: import biz.hammurapi.dispatch.ResultConsumer;
034: import biz.hammurapi.util.Observable;
035: import biz.hammurapi.util.Observer;
036: import biz.hammurapi.util.Versioned;
037:
038: /**
039: * Base class for rules.
040: * @author Pavel Vlasov
041: * @revision $Revision$
042: */
043: public class Rule extends AbstractRule {
044:
045: /**
046: * Interface to detect changes in arguments passed to inference methods.
047: * Default implementation detects changes in versioned and observable objects.
048: * @author Pavel Vlasov
049: * @revision $Revision$
050: */
051: public interface ChangeDetector {
052: boolean hasChanged();
053: }
054:
055: /**
056: * Creates default change detector, which detects changes in versioned and observable objects.
057: * Subclasses can override this method to detect changes as appropriate for the application
058: * domain.
059: * @param obj
060: * @return Change detector or null if change detection is not needed.
061: */
062: protected ChangeDetector newChangeDetector(final Object obj) {
063: return new ChangeDetector() {
064: private int version;
065: private boolean changed;
066:
067: private Observer observer = new Observer() {
068:
069: public void update(Observable observable, Object arg) {
070: changed = true;
071: }
072:
073: };
074:
075: {
076: if (obj instanceof Versioned) {
077: version = ((Versioned) obj).getObjectVersion();
078: } else if (obj instanceof Observable) {
079: ((Observable) obj).addObserver(observer);
080: }
081: }
082:
083: public boolean hasChanged() {
084: if (obj instanceof Versioned) {
085: return version != ((Versioned) obj)
086: .getObjectVersion();
087: } else if (obj instanceof Observable) {
088: ((Observable) obj).removeObserver(observer);
089: return changed;
090: }
091: return false;
092: }
093:
094: };
095: }
096:
097: /**
098: * Holds reference to the method currently being invoked.
099: * Used for injecting derivations in add() method.
100: */
101: private ThreadLocal currentInvocation = new ThreadLocal();
102:
103: /**
104: *
105: */
106: private static final long serialVersionUID = 5320143875248203766L;
107:
108: private Collection removeHandlers = new ArrayList();
109: private Collection addHandlers = new ArrayList();
110:
111: private String inferMethodName;
112:
113: private String removeMethodName;
114:
115: private String acceptMethodName;
116:
117: class InvocationEntry {
118: Method method;
119: Object[] args;
120: Collection negators;
121: Collection actions;
122:
123: /**
124: * @param method
125: * @param consumer
126: */
127: public InvocationEntry(Method method, Object[] args,
128: Collection negators, Collection actions) {
129: super ();
130: this .method = method;
131: this .args = args;
132: this .negators = negators;
133: this .actions = actions;
134: }
135:
136: boolean addNegator(Negator negator) {
137: if (negators != null && negators.add(negator)) {
138: return true;
139: }
140:
141: return false;
142: }
143:
144: public void addPostTrace(Object fact) {
145: for (int i = 0; i < args.length; i++) {
146: actions.add(((ActionTracer) owner).createPostAction(
147: args[i], fact));
148: }
149: }
150:
151: public void addRemoveTrace(Object fact) {
152: for (int i = 0; i < args.length; i++) {
153: actions.add(((ActionTracer) owner).createPostAction(
154: args[i], fact));
155: }
156: }
157: }
158:
159: /**
160: * Each addtion to this class increments version number.
161: * @author Pavel Vlasov
162: * @revision $Revision$
163: */
164: private class VersionedHashSet extends HashSet {
165: private int version;
166:
167: public int getVersion() {
168: return version;
169: }
170:
171: public boolean add(Object arg) {
172: if (super .add(arg)) {
173: ++version;
174: return true;
175: }
176:
177: return false;
178: }
179: }
180:
181: /**
182: * Default constructor.
183: * Uses "infer" for infer methods, "remove" for remove methods,
184: * and "accept" for filters.
185: *
186: */
187: public Rule() {
188: this ("infer", "remove", "accept");
189: }
190:
191: /**
192: * Instances of this interface are passed to <code>accept()</code> methods as second parameter. This parameter
193: * can be used if one <code>accept()</code> method is bound to several <code>join()</code> methods or to
194: * several <code>join()</code> method's parameters.
195: * @author Pavel Vlasov
196: * @revision $Revision$
197: */
198: public interface AcceptInfo {
199:
200: /**
201: * @return Parameter index in the target method.
202: */
203: int parameterIndex();
204:
205: /**
206: * @return Target <code>join()</code> method.
207: */
208: Method targetMethod();
209: }
210:
211: /**
212: * Creates handlers for join method
213: * @param method Join method.
214: * @param acceptMethods accept methods associated with this join method.
215: */
216: void createJoinHandlers(final Method method,
217: final Method[] acceptMethods) {
218: Class[] parameterTypes = method.getParameterTypes();
219: final Collection[] collections = new Collection[parameterTypes.length];
220:
221: StringBuffer msb = new StringBuffer(method.getName());
222: msb.append("(");
223: for (int i = 0; i < parameterTypes.length; i++) {
224: if (i > 0) {
225: msb.append(",");
226: }
227: msb.append(parameterTypes[i].getName());
228: }
229: msb.append(")");
230:
231: final String signature = msb.toString();
232:
233: for (int i = 0; i < parameterTypes.length; i++) {
234: collections[i] = getCollection(signature + "[" + i + "]",
235: collections);
236: }
237:
238: for (int i = 0; i < parameterTypes.length; i++) {
239: final int parameterIndex = i;
240: final Class parameterType = parameterTypes[i];
241:
242: addHandlers.add(new InvocationHandler() {
243:
244: public void invoke(Object arg,
245: ResultConsumer resultConsumer) throws Throwable {
246: if (acceptMethods[parameterIndex] != null) {
247: AcceptInfo ai = new AcceptInfo() {
248:
249: public int parameterIndex() {
250: return parameterIndex;
251: }
252:
253: public Method targetMethod() {
254: return method;
255: }
256:
257: public String toString() {
258: return "[AcceptInfo] " + method
259: + ", parameter index: "
260: + parameterIndex;
261: }
262:
263: };
264:
265: // Don't do anything if not accepted.
266: if (Boolean.FALSE
267: .equals(acceptMethods[parameterIndex]
268: .invoke(
269: Rule.this ,
270: new Object[] { arg, ai }))) {
271: return;
272: }
273: }
274:
275: Collection actions = doTracing ? new ArrayList()
276: : null;
277:
278: synchronized (collections) {
279: // Add argument to its collection, run join if it was actually added
280: if (collections[parameterIndex].add(arg)) {
281: Object[] args = new Object[collections.length];
282: args[parameterIndex] = arg;
283: VersionedHashSet negators = new VersionedHashSet(); // Negators posted in this iteration over collections.
284: int[] negatorVersions = new int[collections.length];
285: for (int i = 0; i < collections.length; i++) {
286: negatorVersions[i] = negators
287: .getVersion();
288: }
289:
290: doJoin(args, 0, negators, negatorVersions,
291: actions);
292: }
293: }
294:
295: if (doTracing && !actions.isEmpty()) {
296: ((ActionTracer) owner).addActions(actions);
297: }
298: }
299:
300: public String toString() {
301: return "[" + method.getName()
302: + " handler] Target method: " + method
303: + ", target parameter: " + parameterIndex
304: + " (" + parameterType + ")";
305: }
306:
307: /**
308: * @return last non-null argument.
309: */
310: private int doJoin(Object[] args, int idx,
311: VersionedHashSet negators,
312: int[] negatorVersions, Collection actions)
313: throws Throwable {
314: if (idx == args.length) {
315: Object prevInvocation = currentInvocation.get();
316: try {
317: currentInvocation.set(new InvocationEntry(
318: method, args, negators, actions));
319: // change detection setup
320: ChangeDetector[] cda = new ChangeDetector[args.length];
321: for (int i = 0; i < cda.length; i++) {
322: cda[i] = newChangeDetector(args[i]);
323: }
324:
325: Object ret = method.invoke(Rule.this , args);
326: ++invocationCounter;
327:
328: // change detection
329: for (int i = 0; i < cda.length; i++) {
330: if (cda[i] != null
331: && cda[i].hasChanged()) {
332: update(args[i]);
333: }
334: }
335:
336: if (ret != null) {
337: post(ret);
338:
339: for (int i = 0; i < args.length; i++) {
340: Iterator it = negators.iterator();
341: while (it.hasNext()) {
342: if (Conclusion.object2Negator(
343: args[i], (Negator) it
344: .next())) {
345: args[i] = null;
346: return i - 1; // Last non-negated argument index.
347: }
348: }
349: }
350: }
351: return idx;
352: } finally {
353: currentInvocation.set(prevInvocation);
354: }
355: } else if (idx == parameterIndex) {
356: return doJoin(args, idx + 1, negators,
357: negatorVersions, actions);
358: } else {
359: // Figure out whether negators shall be applied to collection entries
360: boolean applyNegators = negators.getVersion() > negatorVersions[idx];
361: negatorVersions[idx] = negators.getVersion();
362:
363: Iterator it = collections[idx].iterator();
364: Z: while (it.hasNext()) {
365: args[idx] = it.next();
366:
367: // Apply negators to the next element if needed
368: if (applyNegators) {
369: Iterator nit = negators.iterator();
370: while (nit.hasNext()) {
371: if (Conclusion.object2Negator(
372: args[idx], (Negator) nit
373: .next())) {
374: it.remove();
375: continue Z;
376: }
377: }
378: }
379:
380: int lna = doJoin(args, idx + 1, negators,
381: negatorVersions, actions);
382: if (lna < idx) {
383: return lna;
384: }
385: applyNegators = applyNegators
386: || negators.getVersion() > negatorVersions[idx];
387: }
388: return idx;
389: }
390: }
391:
392: public Class getParameterType() {
393: return parameterType;
394: }
395:
396: });
397:
398: }
399: }
400:
401: /**
402: *
403: * @param inferMethodName Methods with this name and one or more arguments are invoked when an object of type
404: * compatible with one of parameters is posted to the object bus.
405: * @param removeMethodName Single-argument methods with this name will be invoked when rule set's remove
406: * method with compatible type is invoked. Generally rules shall not implement this method because collection
407: * manager and handle manager take care of removal of the object and conclusions made based on this object from
408: * the knowledge base.
409: * @param acceptMethodName Methods with this name and two arguments - the first of equal type and the second
410: * of <code>AcceptInfo</code> type are used to filter inputs to infer methods with more than one parameter.
411: * Type of the first argument of <code>accept</code> method and corresponding argument of <code>infer</code>
412: * must be equal. <code>accept()</code> method's return type must be <code>boolean</code>.
413: */
414: protected Rule(String inferMethodName, String removeMethodName,
415: String acceptMethodName) {
416: this .inferMethodName = inferMethodName;
417: this .removeMethodName = removeMethodName;
418: this .acceptMethodName = acceptMethodName;
419: }
420:
421: private long invocationCounter;
422:
423: /**
424: * @return Number of invocations. Call of reset() method zeroes the counter.
425: */
426: public long getInvocationCounter() {
427: return invocationCounter;
428: }
429:
430: private void createHandlers(final String methodName,
431: Collection handlers) {
432:
433: Method[] methods = getClass().getMethods();
434: for (int i = 0; i < methods.length; i++) {
435: if (methods[i].getParameterTypes().length == 1) {
436: if (methodName.equals(methods[i].getName())) {
437: final Method method = methods[i];
438: final Class actualParameterType = method
439: .getParameterTypes()[0];
440:
441: // Make sure that target method will accept parameterType arguments
442: handlers.add(new InvocationHandler() {
443:
444: public void invoke(Object arg,
445: ResultConsumer resultConsumer)
446: throws Throwable {
447: // Invoke only with compatible parameters.
448: if (actualParameterType.isInstance(arg)) {
449: Object prevInvocation = currentInvocation
450: .get();
451: try {
452: Object[] args = new Object[] { arg };
453: Collection actions = doTracing ? new ArrayList()
454: : null;
455: currentInvocation
456: .set(new InvocationEntry(
457: method, args, null,
458: actions));
459: ChangeDetector cd = newChangeDetector(arg);
460: Object ret = method.invoke(
461: Rule.this , args);
462: ++invocationCounter;
463: if (cd != null && cd.hasChanged()) {
464: update(arg);
465: }
466: if (ret != null) {
467: post(ret);
468: }
469:
470: if (doTracing && !actions.isEmpty()) {
471: ((ActionTracer) owner)
472: .addActions(actions);
473: }
474: } finally {
475: currentInvocation
476: .set(prevInvocation);
477: }
478: }
479: }
480:
481: public Class getParameterType() {
482: return actualParameterType;
483: }
484:
485: public String toString() {
486: return "[" + methodName
487: + " handler] Target method: "
488: + method;
489: }
490:
491: });
492: }
493: }
494: }
495:
496: }
497:
498: private void createJoinHandlers(String acceptMethodName,
499: String joinMethodName) {
500:
501: Method[] methods = getClass().getMethods();
502: for (int i = 0; i < methods.length; i++) {
503: if (methods[i].getParameterTypes().length > 1) {
504: if (joinMethodName.equals(methods[i].getName())) {
505: Method joinMethod = methods[i];
506: Class[] parameterTypes = joinMethod
507: .getParameterTypes();
508: Method[] acceptMethods = new Method[parameterTypes.length];
509: for (int j = 0; j < parameterTypes.length; j++) { // For each parameter type
510: for (int k = 0; k < methods.length; k++) { // Find accept method
511: Class[] acceptParameterTypes = methods[k]
512: .getParameterTypes();
513: if (acceptMethodName.equals(methods[k]
514: .getName())
515: && acceptParameterTypes.length == 2
516: && boolean.class.equals(methods[k]
517: .getReturnType())
518: && parameterTypes[j]
519: .equals(acceptParameterTypes[0])
520: && AcceptInfo.class
521: .equals(acceptParameterTypes[1])) {
522: acceptMethods[j] = methods[k];
523: }
524: }
525: }
526:
527: createJoinHandlers(joinMethod, acceptMethods);
528: }
529: }
530: }
531: }
532:
533: private boolean doTracing;
534:
535: /**
536: * Locates collection manager.
537: */
538: public void start() throws ConfigurationException {
539: super .start();
540: addHandlers.clear();
541: removeHandlers.clear();
542: createHandlers(inferMethodName, addHandlers);
543: createJoinHandlers(acceptMethodName, inferMethodName);
544: createHandlers(removeMethodName, removeHandlers);
545: doTracing = owner instanceof ActionTracer;
546:
547: }
548:
549: public Collection getInvocationHandlers() {
550: return addHandlers;
551: }
552:
553: /**
554: * @return Collection of remove handlers.
555: */
556: public Collection getRemoveHandlers() {
557: return removeHandlers;
558: }
559:
560: /**
561: * Adds new fact to knowledge base.
562: * Returning value from inference methods has the same effect.
563: * @param fact
564: */
565: protected void post(Object fact) {
566: if (fact != null) {
567: InvocationEntry ie = (InvocationEntry) currentInvocation
568: .get();
569: if (ie != null && fact instanceof Conclusion) {
570: Derivation derivation = new Derivation(Rule.this ,
571: ie.method);
572: for (int i = 0; i < ie.args.length; i++) {
573: derivation.addSourceFact(ie.args[i]);
574: }
575: ((Conclusion) fact).addDerivation(derivation);
576: }
577:
578: if (fact instanceof Negator) {
579: ie.addNegator((Negator) fact);
580: }
581:
582: if (doTracing) {
583: ie.addPostTrace(fact);
584: }
585:
586: super .post(fact);
587: }
588: }
589:
590: /**
591: * Invokes remove method of the knowledge base and adds trace action.
592: */
593: protected void remove(Object fact) {
594: if (fact != null) {
595: InvocationEntry ie = (InvocationEntry) currentInvocation
596: .get();
597:
598: if (doTracing) {
599: ie.addRemoveTrace(fact);
600: }
601: }
602: super .remove(fact);
603: }
604:
605: public void reset() {
606: super .reset();
607: resetInvocationCounter();
608: }
609:
610: /**
611: * Resets invocation counter.
612: * @return counter value before reset.
613: */
614: protected long resetInvocationCounter() {
615: long ret = invocationCounter;
616: invocationCounter = 0;
617: return ret;
618: }
619: }
|