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.Arrays; 028 import java.util.Collection; 029 import java.util.HashMap; 030 import java.util.HashSet; 031 import java.util.Iterator; 032 import java.util.Map; 033 034 import biz.hammurapi.config.ConfigurationException; 035 import biz.hammurapi.dispatch.InvocationHandler; 036 import biz.hammurapi.dispatch.ResultConsumer; 037 import biz.hammurapi.util.Observable; 038 import biz.hammurapi.util.Observer; 039 import biz.hammurapi.util.Versioned; 040 041 042 /** 043 * Base class for rules. 044 * @author Pavel Vlasov 045 * @revision $Revision$ 046 */ 047 public class Rule extends AbstractRule { 048 049 /** 050 * Interface to detect changes in arguments passed to inference methods. 051 * Default implementation detects changes in versioned and observable objects. 052 * @author Pavel Vlasov 053 * @revision $Revision$ 054 */ 055 public interface ChangeDetector { 056 boolean hasChanged(); 057 } 058 059 /** 060 * Creates default change detector, which detects changes in versioned and observable objects. 061 * Subclasses can override this method to detect changes as appropriate for the application 062 * domain. 063 * @param obj 064 * @return Change detector or null if change detection is not needed. 065 */ 066 protected ChangeDetector newChangeDetector(final Object obj) { 067 return new ChangeDetector() { 068 private int version; 069 private boolean changed; 070 071 private Observer observer=new Observer() { 072 073 public void update(Observable observable, Object arg) { 074 changed=true; 075 } 076 077 }; 078 079 { 080 if (obj instanceof Versioned) { 081 version=((Versioned) obj).getObjectVersion(); 082 } else if (obj instanceof Observable) { 083 ((Observable) obj).addObserver(observer); 084 } 085 } 086 087 public boolean hasChanged() { 088 if (obj instanceof Versioned) { 089 return version!=((Versioned) obj).getObjectVersion(); 090 } else if (obj instanceof Observable) { 091 ((Observable) obj).removeObserver(observer); 092 return changed; 093 } 094 return false; 095 } 096 097 }; 098 } 099 100 /** 101 * Holds reference to the method currently being invoked. 102 * Used for injecting derivations in add() method. 103 */ 104 private ThreadLocal currentResultConsumerWrapper=new ThreadLocal(); 105 106 /** 107 * 108 */ 109 private static final long serialVersionUID = 5320143875248203766L; 110 111 private Collection removeHandlers=new ArrayList(); 112 private Collection addHandlers=new ArrayList(); 113 114 private String inferMethodName; 115 116 private String removeMethodName; 117 118 private String acceptMethodName; 119 120 /** 121 * Each addtion to this class increments version number. 122 * @author Pavel Vlasov 123 * @revision $Revision$ 124 */ 125 private class VersionedHashSet extends HashSet { 126 private int version; 127 128 public int getVersion() { 129 return version; 130 } 131 132 public boolean add(Object arg) { 133 if (super.add(arg)) { 134 ++version; 135 return true; 136 } 137 138 return false; 139 } 140 } 141 142 /** 143 * Default constructor. 144 * Uses "infer" for infer methods, "remove" for remove methods, 145 * and "accept" for filters. 146 * 147 */ 148 public Rule() { 149 this("infer", "remove", "accept"); 150 } 151 152 /** 153 * Instances of this interface are passed to <code>accept()</code> methods as second parameter. This parameter 154 * can be used if one <code>accept()</code> method is bound to several <code>join()</code> methods or to 155 * several <code>join()</code> method's parameters. 156 * @author Pavel Vlasov 157 * @revision $Revision$ 158 */ 159 public interface AcceptInfo { 160 161 /** 162 * @return Parameter index in the target method. 163 */ 164 int parameterIndex(); 165 166 /** 167 * @return Target <code>join()</code> method. 168 */ 169 Method targetMethod(); 170 } 171 172 /** 173 * Creates handlers for join method 174 * @param method Join method. 175 * @param acceptMethods accept methods associated with this join method. 176 */ 177 void createJoinHandlers(final Method method, final Method[] acceptMethods) { 178 Class[] parameterTypes = method.getParameterTypes(); 179 final Collection[] collections=new Collection[parameterTypes.length]; 180 181 StringBuffer msb=new StringBuffer(method.getName()); 182 msb.append("("); 183 for (int i=0; i<parameterTypes.length; i++) { 184 if (i>0) { 185 msb.append(","); 186 } 187 msb.append(parameterTypes[i].getName()); 188 } 189 msb.append(")"); 190 191 final String signature=msb.toString(); 192 193 for (int i=0; i<parameterTypes.length; i++) { 194 collections[i]=getCollection(signature+"["+i+"]", collections); 195 } 196 197 for (int i=0; i<parameterTypes.length; i++) { 198 final int parameterIndex=i; 199 final Class parameterType=parameterTypes[i]; 200 201 addHandlers.add(new InvocationHandler() { 202 203 public void invoke(Object arg, ResultConsumer resultConsumer) throws Throwable { 204 if (acceptMethods[parameterIndex]!=null) { 205 AcceptInfo ai=new AcceptInfo() { 206 207 public int parameterIndex() { 208 return parameterIndex; 209 } 210 211 public Method targetMethod() { 212 return method; 213 } 214 215 public String toString() { 216 return "[AcceptInfo] "+method+", parameter index: "+parameterIndex; 217 } 218 219 }; 220 221 // Don't do anything if not accepted. 222 if (Boolean.FALSE.equals(acceptMethods[parameterIndex].invoke(Rule.this, new Object[] {arg, ai}))) { 223 return; 224 } 225 } 226 227 Collection actions = doTracing ? new ArrayList() : null; 228 229 synchronized (collections) { 230 // Add argument to its collection, run join if it was actually added 231 if (collections[parameterIndex].add(arg)) { 232 Object[] args=new Object[collections.length]; 233 args[parameterIndex]=arg; 234 VersionedHashSet negators = new VersionedHashSet(); // Negators posted in this iteration over collections. 235 int[] negatorVersions = new int[collections.length]; 236 for (int i=0; i<collections.length; i++) { 237 negatorVersions[i]=negators.getVersion(); 238 } 239 240 doJoin(args, 0, negators, negatorVersions, actions, resultConsumer); 241 } 242 } 243 244 if (doTracing && !actions.isEmpty()) { 245 ((ActionTracer) owner).addActions(actions); 246 } 247 } 248 249 public String toString() { 250 return "["+method.getName()+" handler] Target method: "+method+", target parameter: "+parameterIndex+" ("+parameterType+")"; 251 } 252 253 /** 254 * @return last non-null argument. 255 */ 256 private int doJoin(Object[] args, int idx, VersionedHashSet negators, int[] negatorVersions, Collection actions, ResultConsumer rawConsumer) throws Throwable { 257 if (idx==args.length) { 258 Object prevResultConsumerWrapper=currentResultConsumerWrapper.get(); 259 try { 260 ResultConsumerWrapper resultConsumer = new ResultConsumerWrapper(method, args, negators, actions, rawConsumer); 261 currentResultConsumerWrapper.set(resultConsumer); 262 // change detection setup 263 ChangeDetector[] cda=new ChangeDetector[args.length]; 264 for (int i=0; i<cda.length; i++) { 265 cda[i]=newChangeDetector(args[i]); 266 } 267 268 Object ret=method.invoke(Rule.this, args); 269 ++invocationCounter; 270 271 // change detection 272 for (int i=0; i<cda.length; i++) { 273 if (cda[i]!=null && cda[i].hasChanged()) { 274 update(args[i]); 275 } 276 } 277 278 if (ret!=null) { 279 resultConsumer.consume(ret); 280 281 for (int i=0; i<args.length; i++) { 282 Iterator it=negators.iterator(); 283 while (it.hasNext()) { 284 if (Conclusion.object2Negator(args[i], (Negator) it.next())) { 285 args[i]=null; 286 return i-1; // Last non-negated argument index. 287 } 288 } 289 } 290 } 291 return idx; 292 } finally { 293 currentResultConsumerWrapper.set(prevResultConsumerWrapper); 294 } 295 } else if (idx==parameterIndex) { 296 return doJoin(args, idx+1, negators, negatorVersions, actions, rawConsumer); 297 } else { 298 // Figure out whether negators shall be applied to collection entries 299 boolean applyNegators = negators.getVersion() > negatorVersions[idx]; 300 negatorVersions[idx]=negators.getVersion(); 301 302 Iterator it=collections[idx].iterator(); 303 Z: while (it.hasNext()) { 304 args[idx]=it.next(); 305 306 // Apply negators to the next element if needed 307 if (applyNegators) { 308 Iterator nit=negators.iterator(); 309 while (nit.hasNext()) { 310 if (Conclusion.object2Negator(args[idx], (Negator) nit.next())) { 311 it.remove(); 312 continue Z; 313 } 314 } 315 } 316 317 int lna = doJoin(args, idx+1, negators, negatorVersions, actions, rawConsumer); 318 if (lna<idx) { 319 return lna; 320 } 321 applyNegators = applyNegators || negators.getVersion() > negatorVersions[idx]; 322 } 323 return idx; 324 } 325 } 326 327 public Class getParameterType() { 328 return parameterType; 329 } 330 331 public Class[] getFactTypes() { 332 Class[] ret = getMethodFactTypes(method.getParameterTypes()); 333 if (ret!=null) { 334 return ret; 335 } 336 337 Class mrt = method.getReturnType(); 338 return void.class.equals(mrt) ? new Class[] {} : new Class[] {mrt}; 339 } 340 341 }); 342 343 } 344 } 345 346 /** 347 * 348 * @param inferMethodName Methods with this name and one or more arguments are invoked when an object of type 349 * compatible with one of parameters is posted to the object bus. 350 * @param removeMethodName Single-argument methods with this name will be invoked when rule set's remove 351 * method with compatible type is invoked. Generally rules shall not implement this method because collection 352 * manager and handle manager take care of removal of the object and conclusions made based on this object from 353 * the knowledge base. 354 * @param acceptMethodName Methods with this name and two arguments - the first of equal type and the second 355 * of <code>AcceptInfo</code> type are used to filter inputs to infer methods with more than one parameter. 356 * Type of the first argument of <code>accept</code> method and corresponding argument of <code>infer</code> 357 * must be equal. <code>accept()</code> method's return type must be <code>boolean</code>. 358 */ 359 protected Rule(String inferMethodName, String removeMethodName, String acceptMethodName) { 360 this.inferMethodName=inferMethodName; 361 this.removeMethodName=removeMethodName; 362 this.acceptMethodName=acceptMethodName; 363 } 364 365 private long invocationCounter; 366 367 /** 368 * @return Number of invocations. Call of reset() method zeroes the counter. 369 */ 370 public long getInvocationCounter() { 371 return invocationCounter; 372 } 373 374 /** 375 * Wraps result consumer to inject derivations and do other stuff. 376 * @author Pavel 377 * 378 */ 379 private class ResultConsumerWrapper implements ResultConsumer { 380 381 private Method method; 382 private Object[] args; 383 private Collection negators; 384 private Collection actions; 385 private ResultConsumer master; 386 387 /** 388 * @param method 389 * @param consumer 390 */ 391 ResultConsumerWrapper( 392 Method method, 393 Object[] args, 394 Collection negators, 395 Collection actions, 396 ResultConsumer resultConsumer) { 397 398 this.method = method; 399 this.args=args; 400 this.negators=negators; 401 this.actions=actions; 402 this.master = resultConsumer; 403 } 404 405 public void consume(Object fact) { 406 if (fact!=null) { 407 if (fact instanceof Conclusion) { 408 Derivation derivation=new Derivation(Rule.this, method); 409 for (int i=0; i<args.length; i++) { 410 derivation.addSourceFact(args[i]); 411 } 412 ((Conclusion) fact).addDerivation(derivation); 413 } 414 415 if (fact instanceof Negator) { 416 addNegator((Negator) fact); 417 } 418 419 if (doTracing) { 420 addPostTrace(fact); 421 } 422 423 master.consume(fact); 424 } 425 } 426 427 boolean addNegator(Negator negator) { 428 if (negators!=null && negators.add(negator)) { 429 return true; 430 } 431 432 return false; 433 } 434 435 public void addPostTrace(Object fact) { 436 for (int i=0; i<args.length; i++) { 437 actions.add(((ActionTracer) owner).createPostAction(args[i], fact)); 438 } 439 } 440 441 public void addRemoveTrace(Object fact) { 442 for (int i=0; i<args.length; i++) { 443 actions.add(((ActionTracer) owner).createPostAction(args[i], fact)); 444 } 445 } 446 447 448 }; 449 450 451 452 private void createHandlers(final String methodName, Collection handlers) { 453 454 Method[] methods=getClass().getMethods(); 455 for (int i=0; i<methods.length; i++) { 456 if (methods[i].getParameterTypes().length == 1) { 457 if (methodName.equals(methods[i].getName())) { 458 final Method method=methods[i]; 459 final Class actualParameterType = method.getParameterTypes()[0]; 460 461 // Make sure that target method will accept parameterType arguments 462 handlers.add( 463 new InvocationHandler() { 464 465 public void invoke(Object arg, final ResultConsumer rawConsumer) throws Throwable { 466 // Invoke only with compatible parameters. 467 if (actualParameterType.isInstance(arg)) { 468 Object prevResultConsumer=currentResultConsumerWrapper.get(); 469 try { 470 471 Object[] args = new Object[] {arg}; 472 Collection actions = doTracing ? new ArrayList() : null; 473 ResultConsumerWrapper resultConsumer = new ResultConsumerWrapper(method, args, null, actions, rawConsumer); 474 currentResultConsumerWrapper.set(resultConsumer); 475 ChangeDetector cd=newChangeDetector(arg); 476 Object ret=method.invoke(Rule.this, args); 477 ++invocationCounter; 478 if (cd!=null && cd.hasChanged()) { 479 update(arg); 480 } 481 if (ret!=null) { 482 resultConsumer.consume(ret); 483 } 484 485 if (doTracing && !actions.isEmpty()) { 486 ((ActionTracer) owner).addActions(actions); 487 } 488 } finally { 489 currentResultConsumerWrapper.set(prevResultConsumer); 490 } 491 } 492 } 493 494 public Class getParameterType() { 495 return actualParameterType; 496 } 497 498 public String toString() { 499 return "["+methodName+" handler] Target method: "+method; 500 } 501 502 public Class[] getFactTypes() { 503 Class[] ret = getMethodFactTypes(method.getParameterTypes()); 504 if (ret!=null) { 505 return ret; 506 } 507 508 Class mrt = method.getReturnType(); 509 return void.class.equals(mrt) ? new Class[] {} : new Class[] {mrt}; 510 } 511 512 }); 513 } 514 } 515 } 516 517 } 518 519 private void createJoinHandlers(String acceptMethodName, String joinMethodName) { 520 521 Method[] methods=getClass().getMethods(); 522 for (int i=0; i<methods.length; i++) { 523 if (methods[i].getParameterTypes().length > 1) { 524 if (joinMethodName.equals(methods[i].getName())) { 525 Method joinMethod=methods[i]; 526 Class[] parameterTypes = joinMethod.getParameterTypes(); 527 Method[] acceptMethods=new Method[parameterTypes.length]; 528 for (int j=0; j<parameterTypes.length; j++) { // For each parameter type 529 for (int k=0; k<methods.length; k++) { // Find accept method 530 Class[] acceptParameterTypes = methods[k].getParameterTypes(); 531 if (acceptMethodName.equals(methods[k].getName()) 532 && acceptParameterTypes.length==2 533 && boolean.class.equals(methods[k].getReturnType()) 534 && parameterTypes[j].equals(acceptParameterTypes[0]) 535 && AcceptInfo.class.equals(acceptParameterTypes[1])) { 536 acceptMethods[j]=methods[k]; 537 } 538 } 539 } 540 541 createJoinHandlers(joinMethod, acceptMethods); 542 } 543 } 544 } 545 } 546 547 private boolean doTracing; 548 549 private boolean started; 550 551 /** 552 * Locates collection manager. 553 */ 554 public void start() throws ConfigurationException { 555 super.start(); 556 addHandlers.clear(); 557 removeHandlers.clear(); 558 createHandlers(inferMethodName, addHandlers); 559 createJoinHandlers(acceptMethodName, inferMethodName); 560 createHandlers(removeMethodName, removeHandlers); 561 doTracing = owner instanceof ActionTracer; 562 started = true; 563 } 564 565 public Collection getInvocationHandlers() { 566 return addHandlers; 567 } 568 569 /** 570 * @return Collection of remove handlers. 571 */ 572 public Collection getRemoveHandlers() { 573 return removeHandlers; 574 } 575 576 /** 577 * Adds new fact to knowledge base. 578 * Returning value from inference methods has the same effect. 579 * @param fact 580 */ 581 protected void post(Object fact) { 582 if (fact!=null) { 583 ResultConsumerWrapper resultConsumer = (ResultConsumerWrapper) currentResultConsumerWrapper.get(); 584 if (resultConsumer==null) { 585 throw new IllegalStateException("post() can be invoked only within rule method call."); 586 } 587 588 resultConsumer.consume(fact); 589 } 590 } 591 592 /** 593 * Invokes remove method of the knowledge base and adds trace action. 594 */ 595 protected void remove(final Object fact) { 596 if (fact!=null) { 597 ResultConsumerWrapper resultConsumer = (ResultConsumerWrapper) currentResultConsumerWrapper.get(); 598 if (resultConsumer==null) { 599 throw new IllegalStateException("remove() can be invoked only within rule method call."); 600 } 601 602 if (doTracing) { 603 resultConsumer.addRemoveTrace(fact); 604 } 605 606 resultConsumer.consume(new KnowledgeBase.KnowledgeBaseCommand() { 607 608 public void execute(KnowledgeBase knowledgeBase) { 609 knowledgeBase.remove(fact); 610 611 } 612 613 }); 614 } 615 } 616 617 /** 618 * Invokes update method of the knowledge base and adds trace action. 619 */ 620 protected void update(final Object fact) { 621 if (fact!=null) { 622 ResultConsumerWrapper resultConsumer = (ResultConsumerWrapper) currentResultConsumerWrapper.get(); 623 if (resultConsumer==null) { 624 throw new IllegalStateException("update() can be invoked only within rule method call."); 625 } 626 627 if (doTracing) { 628 resultConsumer.addRemoveTrace(fact); 629 } 630 631 resultConsumer.consume(new KnowledgeBase.KnowledgeBaseCommand() { 632 633 public void execute(KnowledgeBase knowledgeBase) { 634 knowledgeBase.remove(fact); 635 knowledgeBase.add(fact); 636 } 637 638 }); 639 } 640 } 641 642 public void reset() { 643 super.reset(); 644 resetInvocationCounter(); 645 } 646 647 /** 648 * Resets invocation counter. 649 * @return counter value before reset. 650 */ 651 protected long resetInvocationCounter() { 652 long ret = invocationCounter; 653 invocationCounter = 0; 654 return ret; 655 } 656 657 // List<Class> -> Class[] 658 private Map methodFactTypes = new HashMap(); 659 660 /** 661 * Rule methods can return facts and post facts. Backward reasoning engine 662 * needs to know types of rule outputs. From rule class introspection 663 * the rule system knows about rule method return type, but it doesn't know 664 * about types posted through <code>post()</code>. 665 * 666 * Also, return type might not be enough, as returned instances may implement 667 * interfaces which other rules are interested in, but which are not declared 668 * in the rule return type. 669 * 670 * This method allows rules to inform the inference system about posted types. 671 * <P> 672 * This method shall be invoked before rule is started. Place invocations of 673 * this method in rule constructors. 674 * <P> 675 * Only rules used in backward reasoning need/must call this method to ensure 676 * proper reasoning. For methods without fact types information set through 677 * this method, the rule system uses method return type. 678 * 679 * @param parameterTypes Reasoning method parameter types. Method name is not 680 * required as it is known. 681 * @param factTypes Fact types produced by given reasoning (inference) method. 682 * If rule method returns facts as well as posts them, then method return type 683 * must be included in the factTypes array. 684 */ 685 protected final void setMethodFactTypes(Class[] parameterTypes, Class[] factTypes) { 686 if (started) { 687 throw new IllegalStateException("Method fact types cannot be set once rule is started."); 688 } 689 690 methodFactTypes.put(new ArrayList(Arrays.asList(parameterTypes)), factTypes); 691 } 692 693 /** 694 * Convenience method, delegates to <code>setMethodFactTypes(Class[] parameterTypes, Class[] factTypes)</code> 695 * @param parameterType 696 * @param factTypes 697 */ 698 protected final void setMethodFactTypes(Class parameterType, Class[] factTypes) { 699 setMethodFactTypes(new Class[] {parameterType}, factTypes); 700 } 701 702 /** 703 * Convenience method, delegates to <code>setMethodFactTypes(Class[] parameterTypes, Class[] factTypes)</code> 704 * @param parameterType 705 * @param factTypes 706 */ 707 protected final void setMethodFactTypes(Class parameterType, Class factType) { 708 setMethodFactTypes(parameterType, new Class[] {factType}); 709 } 710 711 /** 712 * Convenience method, delegates to <code>setMethodFactTypes(Class[] parameterTypes, Class[] factTypes)</code> 713 * @param parameterType 714 * @param factTypes 715 */ 716 protected final void setMethodFactTypes(Class[] parameterTypes, Class factType) { 717 setMethodFactTypes(parameterTypes, new Class[] {factType}); 718 } 719 720 private Class[] getMethodFactTypes(Class[] parameterTypes) { 721 return (Class[]) methodFactTypes.get(new ArrayList(Arrays.asList(parameterTypes))); 722 } 723 }