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 /**
040 * Base class for rules.
041 * @author Pavel Vlasov
042 * @revision $Revision$
043 */
044 public class Rule extends AbstractRule {
045
046 /**
047 * Interface to detect changes in arguments passed to inference methods.
048 * Default implementation detects changes in versioned and observable objects.
049 * @author Pavel Vlasov
050 * @revision $Revision$
051 */
052 public interface ChangeDetector {
053 boolean hasChanged();
054 }
055
056 /**
057 * Creates default change detector, which detects changes in versioned and observable objects.
058 * Subclasses can override this method to detect changes as appropriate for the application
059 * domain.
060 * @param obj
061 * @return Change detector or null if change detection is not needed.
062 */
063 protected ChangeDetector newChangeDetector(final Object obj) {
064 return new ChangeDetector() {
065 private int version;
066 private boolean changed;
067
068 private Observer observer=new Observer() {
069
070 public void update(Observable observable, Object arg) {
071 changed=true;
072 }
073
074 };
075
076 {
077 if (obj instanceof Versioned) {
078 version=((Versioned) obj).getObjectVersion();
079 } else if (obj instanceof Observable) {
080 ((Observable) obj).addObserver(observer);
081 }
082 }
083
084 public boolean hasChanged() {
085 if (obj instanceof Versioned) {
086 return version!=((Versioned) obj).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, Collection negators, Collection actions) {
128 super();
129 this.method = method;
130 this.args=args;
131 this.negators=negators;
132 this.actions=actions;
133 }
134
135 boolean addNegator(Negator negator) {
136 if (negators!=null && negators.add(negator)) {
137 return true;
138 }
139
140 return false;
141 }
142
143 public void addPostTrace(Object fact) {
144 for (int i=0; i<args.length; i++) {
145 actions.add(((ActionTracer) owner).createPostAction(args[i], fact));
146 }
147 }
148
149 public void addRemoveTrace(Object fact) {
150 for (int i=0; i<args.length; i++) {
151 actions.add(((ActionTracer) owner).createPostAction(args[i], fact));
152 }
153 }
154 }
155
156 /**
157 * Each addtion to this class increments version number.
158 * @author Pavel Vlasov
159 * @revision $Revision$
160 */
161 private class VersionedHashSet extends HashSet {
162 private int version;
163
164 public int getVersion() {
165 return version;
166 }
167
168 public boolean add(Object arg) {
169 if (super.add(arg)) {
170 ++version;
171 return true;
172 }
173
174 return false;
175 }
176 }
177
178 /**
179 * Default constructor.
180 * Uses "infer" for infer methods, "remove" for remove methods,
181 * and "accept" for filters.
182 *
183 */
184 public Rule() {
185 this("infer", "remove", "accept");
186 }
187
188 /**
189 * Instances of this interface are passed to <code>accept()</code> methods as second parameter. This parameter
190 * can be used if one <code>accept()</code> method is bound to several <code>join()</code> methods or to
191 * several <code>join()</code> method's parameters.
192 * @author Pavel Vlasov
193 * @revision $Revision$
194 */
195 public interface AcceptInfo {
196
197 /**
198 * @return Parameter index in the target method.
199 */
200 int parameterIndex();
201
202 /**
203 * @return Target <code>join()</code> method.
204 */
205 Method targetMethod();
206 }
207
208 /**
209 * Creates handlers for join method
210 * @param method Join method.
211 * @param acceptMethods accept methods associated with this join method.
212 */
213 void createJoinHandlers(final Method method, final Method[] acceptMethods) {
214 Class[] parameterTypes = method.getParameterTypes();
215 final Collection[] collections=new Collection[parameterTypes.length];
216
217 StringBuffer msb=new StringBuffer(method.getName());
218 msb.append("(");
219 for (int i=0; i<parameterTypes.length; i++) {
220 if (i>0) {
221 msb.append(",");
222 }
223 msb.append(parameterTypes[i].getName());
224 }
225 msb.append(")");
226
227 final String signature=msb.toString();
228
229 for (int i=0; i<parameterTypes.length; i++) {
230 collections[i]=getCollection(signature+"["+i+"]", collections);
231 }
232
233 for (int i=0; i<parameterTypes.length; i++) {
234 final int parameterIndex=i;
235 final Class parameterType=parameterTypes[i];
236
237 addHandlers.add(new InvocationHandler() {
238
239 public void invoke(Object arg, ResultConsumer resultConsumer) throws Throwable {
240 if (acceptMethods[parameterIndex]!=null) {
241 AcceptInfo ai=new AcceptInfo() {
242
243 public int parameterIndex() {
244 return parameterIndex;
245 }
246
247 public Method targetMethod() {
248 return method;
249 }
250
251 public String toString() {
252 return "[AcceptInfo] "+method+", parameter index: "+parameterIndex;
253 }
254
255 };
256
257 // Don't do anything if not accepted.
258 if (Boolean.FALSE.equals(acceptMethods[parameterIndex].invoke(Rule.this, new Object[] {arg, ai}))) {
259 return;
260 }
261 }
262
263 Collection actions = doTracing ? new ArrayList() : null;
264
265 synchronized (collections) {
266 // Add argument to its collection, run join if it was actually added
267 if (collections[parameterIndex].add(arg)) {
268 Object[] args=new Object[collections.length];
269 args[parameterIndex]=arg;
270 VersionedHashSet negators = new VersionedHashSet(); // Negators posted in this iteration over collections.
271 int[] negatorVersions = new int[collections.length];
272 for (int i=0; i<collections.length; i++) {
273 negatorVersions[i]=negators.getVersion();
274 }
275
276 doJoin(args, 0, negators, negatorVersions, actions);
277 }
278 }
279
280 if (doTracing && !actions.isEmpty()) {
281 ((ActionTracer) owner).addActions(actions);
282 }
283 }
284
285 public String toString() {
286 return "["+method.getName()+" handler] Target method: "+method+", target parameter: "+parameterIndex+" ("+parameterType+")";
287 }
288
289 /**
290 * @return last non-null argument.
291 */
292 private int doJoin(Object[] args, int idx, VersionedHashSet negators, int[] negatorVersions, Collection actions) throws Throwable {
293 if (idx==args.length) {
294 Object prevInvocation=currentInvocation.get();
295 try {
296 currentInvocation.set(new InvocationEntry(method, args, negators, actions));
297 // change detection setup
298 ChangeDetector[] cda=new ChangeDetector[args.length];
299 for (int i=0; i<cda.length; i++) {
300 cda[i]=newChangeDetector(args[i]);
301 }
302
303 Object ret=method.invoke(Rule.this, args);
304 ++invocationCounter;
305
306 // change detection
307 for (int i=0; i<cda.length; i++) {
308 if (cda[i]!=null && cda[i].hasChanged()) {
309 update(args[i]);
310 }
311 }
312
313 if (ret!=null) {
314 post(ret);
315
316 for (int i=0; i<args.length; i++) {
317 Iterator it=negators.iterator();
318 while (it.hasNext()) {
319 if (Conclusion.object2Negator(args[i], (Negator) it.next())) {
320 args[i]=null;
321 return i-1; // Last non-negated argument index.
322 }
323 }
324 }
325 }
326 return idx;
327 } finally {
328 currentInvocation.set(prevInvocation);
329 }
330 } else if (idx==parameterIndex) {
331 return doJoin(args, idx+1, negators, negatorVersions, actions);
332 } else {
333 // Figure out whether negators shall be applied to collection entries
334 boolean applyNegators = negators.getVersion() > negatorVersions[idx];
335 negatorVersions[idx]=negators.getVersion();
336
337 Iterator it=collections[idx].iterator();
338 Z: while (it.hasNext()) {
339 args[idx]=it.next();
340
341 // Apply negators to the next element if needed
342 if (applyNegators) {
343 Iterator nit=negators.iterator();
344 while (nit.hasNext()) {
345 if (Conclusion.object2Negator(args[idx], (Negator) nit.next())) {
346 it.remove();
347 continue Z;
348 }
349 }
350 }
351
352 int lna = doJoin(args, idx+1, negators, negatorVersions, actions);
353 if (lna<idx) {
354 return lna;
355 }
356 applyNegators = applyNegators || negators.getVersion() > negatorVersions[idx];
357 }
358 return idx;
359 }
360 }
361
362 public Class getParameterType() {
363 return parameterType;
364 }
365
366 });
367
368 }
369 }
370
371 /**
372 *
373 * @param inferMethodName Methods with this name and one or more arguments are invoked when an object of type
374 * compatible with one of parameters is posted to the object bus.
375 * @param removeMethodName Single-argument methods with this name will be invoked when rule set's remove
376 * method with compatible type is invoked. Generally rules shall not implement this method because collection
377 * manager and handle manager take care of removal of the object and conclusions made based on this object from
378 * the knowledge base.
379 * @param acceptMethodName Methods with this name and two arguments - the first of equal type and the second
380 * of <code>AcceptInfo</code> type are used to filter inputs to infer methods with more than one parameter.
381 * Type of the first argument of <code>accept</code> method and corresponding argument of <code>infer</code>
382 * must be equal. <code>accept()</code> method's return type must be <code>boolean</code>.
383 */
384 protected Rule(String inferMethodName, String removeMethodName, String acceptMethodName) {
385 this.inferMethodName=inferMethodName;
386 this.removeMethodName=removeMethodName;
387 this.acceptMethodName=acceptMethodName;
388 }
389
390 private long invocationCounter;
391
392 /**
393 * @return Number of invocations. Call of reset() method zeroes the counter.
394 */
395 public long getInvocationCounter() {
396 return invocationCounter;
397 }
398
399 private void createHandlers(final String methodName, Collection handlers) {
400
401 Method[] methods=getClass().getMethods();
402 for (int i=0; i<methods.length; i++) {
403 if (methods[i].getParameterTypes().length == 1) {
404 if (methodName.equals(methods[i].getName())) {
405 final Method method=methods[i];
406 final Class actualParameterType = method.getParameterTypes()[0];
407
408 // Make sure that target method will accept parameterType arguments
409 handlers.add(
410 new InvocationHandler() {
411
412 public void invoke(Object arg, ResultConsumer resultConsumer) throws Throwable {
413 // Invoke only with compatible parameters.
414 if (actualParameterType.isInstance(arg)) {
415 Object prevInvocation=currentInvocation.get();
416 try {
417 Object[] args = new Object[] {arg};
418 Collection actions = doTracing ? new ArrayList() : null;
419 currentInvocation.set(new InvocationEntry(method, args, null, actions));
420 ChangeDetector cd=newChangeDetector(arg);
421 Object ret=method.invoke(Rule.this, args);
422 ++invocationCounter;
423 if (cd!=null && cd.hasChanged()) {
424 update(arg);
425 }
426 if (ret!=null) {
427 post(ret);
428 }
429
430 if (doTracing && !actions.isEmpty()) {
431 ((ActionTracer) owner).addActions(actions);
432 }
433 } finally {
434 currentInvocation.set(prevInvocation);
435 }
436 }
437 }
438
439 public Class getParameterType() {
440 return actualParameterType;
441 }
442
443 public String toString() {
444 return "["+methodName+" handler] Target method: "+method;
445 }
446
447 });
448 }
449 }
450 }
451
452 }
453
454 private void createJoinHandlers(String acceptMethodName, String joinMethodName) {
455
456 Method[] methods=getClass().getMethods();
457 for (int i=0; i<methods.length; i++) {
458 if (methods[i].getParameterTypes().length > 1) {
459 if (joinMethodName.equals(methods[i].getName())) {
460 Method joinMethod=methods[i];
461 Class[] parameterTypes = joinMethod.getParameterTypes();
462 Method[] acceptMethods=new Method[parameterTypes.length];
463 for (int j=0; j<parameterTypes.length; j++) { // For each parameter type
464 for (int k=0; k<methods.length; k++) { // Find accept method
465 Class[] acceptParameterTypes = methods[k].getParameterTypes();
466 if (acceptMethodName.equals(methods[k].getName())
467 && acceptParameterTypes.length==2
468 && boolean.class.equals(methods[k].getReturnType())
469 && parameterTypes[j].equals(acceptParameterTypes[0])
470 && AcceptInfo.class.equals(acceptParameterTypes[1])) {
471 acceptMethods[j]=methods[k];
472 }
473 }
474 }
475
476 createJoinHandlers(joinMethod, acceptMethods);
477 }
478 }
479 }
480 }
481
482 private boolean doTracing;
483
484 /**
485 * Locates collection manager.
486 */
487 public void start() throws ConfigurationException {
488 super.start();
489 addHandlers.clear();
490 removeHandlers.clear();
491 createHandlers(inferMethodName, addHandlers);
492 createJoinHandlers(acceptMethodName, inferMethodName);
493 createHandlers(removeMethodName, removeHandlers);
494 doTracing = owner instanceof ActionTracer;
495
496 }
497
498 public Collection getInvocationHandlers() {
499 return addHandlers;
500 }
501
502 /**
503 * @return Collection of remove handlers.
504 */
505 public Collection getRemoveHandlers() {
506 return removeHandlers;
507 }
508
509 /**
510 * Adds new fact to knowledge base.
511 * Returning value from inference methods has the same effect.
512 * @param fact
513 */
514 protected void post(Object fact) {
515 if (fact!=null) {
516 InvocationEntry ie=(InvocationEntry) currentInvocation.get();
517 if (ie!=null && fact instanceof Conclusion) {
518 Derivation derivation=new Derivation(Rule.this, ie.method);
519 for (int i=0; i<ie.args.length; i++) {
520 derivation.addSourceFact(ie.args[i]);
521 }
522 ((Conclusion) fact).addDerivation(derivation);
523 }
524
525 if (fact instanceof Negator) {
526 ie.addNegator((Negator) fact);
527 }
528
529 if (doTracing) {
530 ie.addPostTrace(fact);
531 }
532
533 super.post(fact);
534 }
535 }
536
537 /**
538 * Invokes remove method of the knowledge base and adds trace action.
539 */
540 protected void remove(Object fact) {
541 if (fact!=null) {
542 InvocationEntry ie=(InvocationEntry) currentInvocation.get();
543
544 if (doTracing) {
545 ie.addRemoveTrace(fact);
546 }
547 }
548 super.remove(fact);
549 }
550
551 public void reset() {
552 super.reset();
553 resetInvocationCounter();
554 }
555
556 /**
557 * Resets invocation counter.
558 * @return counter value before reset.
559 */
560 protected long resetInvocationCounter() {
561 long ret = invocationCounter;
562 invocationCounter = 0;
563 return ret;
564 }
565 }