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    }