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 }