001    /*
002     * JBoss, Home of Professional Open Source.
003     * Copyright 2008, Red Hat Middleware LLC, and individual contributors
004     * as indicated by the @author tags. See the copyright.txt file in the
005     * distribution for a full listing of individual contributors. 
006     *
007     * This is free software; you can redistribute it and/or modify it
008     * under the terms of the GNU Lesser General Public License as
009     * published by the Free Software Foundation; either version 2.1 of
010     * the License, or (at your option) any later version.
011     *
012     * This software is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015     * Lesser General Public License for more details.
016     *
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this software; if not, write to the Free
019     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021     */
022    package org.jboss.dna.repository.rules;
023    
024    import java.io.IOException;
025    import java.io.Reader;
026    import java.io.StringReader;
027    import java.rmi.RemoteException;
028    import java.util.ArrayList;
029    import java.util.Arrays;
030    import java.util.Collection;
031    import java.util.Collections;
032    import java.util.HashMap;
033    import java.util.List;
034    import java.util.Map;
035    import java.util.concurrent.CountDownLatch;
036    import java.util.concurrent.TimeUnit;
037    import java.util.concurrent.locks.ReadWriteLock;
038    import java.util.concurrent.locks.ReentrantReadWriteLock;
039    import javax.rules.ConfigurationException;
040    import javax.rules.RuleRuntime;
041    import javax.rules.RuleServiceProvider;
042    import javax.rules.RuleServiceProviderManager;
043    import javax.rules.RuleSession;
044    import javax.rules.StatelessRuleSession;
045    import javax.rules.admin.LocalRuleExecutionSetProvider;
046    import javax.rules.admin.RuleAdministrator;
047    import javax.rules.admin.RuleExecutionSet;
048    import javax.rules.admin.RuleExecutionSetCreateException;
049    import javax.rules.admin.RuleExecutionSetDeregistrationException;
050    import net.jcip.annotations.GuardedBy;
051    import net.jcip.annotations.ThreadSafe;
052    import org.jboss.dna.common.SystemFailureException;
053    import org.jboss.dna.common.component.ClassLoaderFactory;
054    import org.jboss.dna.common.component.StandardClassLoaderFactory;
055    import org.jboss.dna.common.util.CheckArg;
056    import org.jboss.dna.common.util.Logger;
057    import org.jboss.dna.repository.RepositoryI18n;
058    import org.jboss.dna.repository.services.AbstractServiceAdministrator;
059    import org.jboss.dna.repository.services.AdministeredService;
060    import org.jboss.dna.repository.services.ServiceAdministrator;
061    
062    /**
063     * A rule service that is capable of executing rule sets using one or more JSR-94 rule engines. Sets of rules are
064     * {@link #addRuleSet(RuleSet) added}, {@link #updateRuleSet(RuleSet) updated}, and {@link #removeRuleSet(String) removed}
065     * (usually by some other component), and then these named rule sets can be {@link #executeRules(String, Map, Object...) run} with
066     * inputs and facts to obtain output.
067     * <p>
068     * This service is thread safe. While multiple rule sets can be safely {@link #executeRules(String, Map, Object...) executed} at
069     * the same time, all executions will be properly synchronized with methods to {@link #addRuleSet(RuleSet) add},
070     * {@link #updateRuleSet(RuleSet) update}, and {@link #removeRuleSet(String) remove} rule sets.
071     * </p>
072     * 
073     * @author Randall Hauch
074     */
075    @ThreadSafe
076    public class RuleService implements AdministeredService {
077    
078        protected static final ClassLoaderFactory DEFAULT_CLASSLOADER_FACTORY = new StandardClassLoaderFactory(
079                                                                                                               RuleService.class.getClassLoader());
080    
081        /**
082         * The administrative component for this service.
083         * 
084         * @author Randall Hauch
085         */
086        protected class Administrator extends AbstractServiceAdministrator {
087    
088            protected Administrator() {
089                super(RepositoryI18n.ruleServiceName, State.PAUSED);
090            }
091    
092            /**
093             * {@inheritDoc}
094             */
095            @Override
096            protected void doShutdown( State fromState ) {
097                super.doShutdown(fromState);
098                // Remove all rule sets ...
099                removeAllRuleSets();
100            }
101    
102            /**
103             * {@inheritDoc}
104             */
105            @Override
106            protected boolean doCheckIsTerminated() {
107                return RuleService.this.isTerminated();
108            }
109    
110            /**
111             * {@inheritDoc}
112             */
113            public boolean awaitTermination( long timeout,
114                                             TimeUnit unit ) throws InterruptedException {
115                return doAwaitTermination(timeout, unit);
116            }
117    
118        }
119    
120        private Logger logger;
121        private ClassLoaderFactory classLoaderFactory = DEFAULT_CLASSLOADER_FACTORY;
122        private final Administrator administrator = new Administrator();
123        private final ReadWriteLock lock = new ReentrantReadWriteLock();
124        @GuardedBy( "lock" )
125        private final Map<String, RuleSet> ruleSets = new HashMap<String, RuleSet>();
126        private final CountDownLatch shutdownLatch = new CountDownLatch(1);
127    
128        /**
129         * Create a new rule service, configured with no rule sets. Upon construction, the system is
130         * {@link ServiceAdministrator#isPaused() paused} and must be configured and then {@link ServiceAdministrator#start() started}
131         * .
132         */
133        public RuleService() {
134            this.logger = Logger.getLogger(this.getClass());
135        }
136    
137        /**
138         * Return the administrative component for this service.
139         * 
140         * @return the administrative component; never null
141         */
142        public ServiceAdministrator getAdministrator() {
143            return this.administrator;
144        }
145    
146        /**
147         * Get the class loader factory that should be used to load sequencers. By default, this service uses a factory that will
148         * return either the {@link Thread#getContextClassLoader() current thread's context class loader} (if not null) or the class
149         * loader that loaded this class.
150         * 
151         * @return the class loader factory; never null
152         * @see #setClassLoaderFactory(ClassLoaderFactory)
153         */
154        public ClassLoaderFactory getClassLoaderFactory() {
155            return this.classLoaderFactory;
156        }
157    
158        /**
159         * Set the Maven Repository that should be used to load the sequencer classes. By default, this service uses a class loader
160         * factory that will return either the {@link Thread#getContextClassLoader() current thread's context class loader} (if not
161         * null) or the class loader that loaded this class.
162         * 
163         * @param classLoaderFactory the class loader factory reference, or null if the default class loader factory should be used.
164         * @see #getClassLoaderFactory()
165         */
166        public void setClassLoaderFactory( ClassLoaderFactory classLoaderFactory ) {
167            this.classLoaderFactory = classLoaderFactory != null ? classLoaderFactory : DEFAULT_CLASSLOADER_FACTORY;
168        }
169    
170        /**
171         * Obtain the rule sets that are currently available in this service.
172         * 
173         * @return an unmodifiable copy of the rule sets; never null, but possibly empty ...
174         */
175        public Collection<RuleSet> getRuleSets() {
176            List<RuleSet> results = new ArrayList<RuleSet>();
177            try {
178                this.lock.readLock().lock();
179                // Make a copy of the rule sets ...
180                if (ruleSets.size() != 0) results.addAll(this.ruleSets.values());
181            } finally {
182                this.lock.readLock().unlock();
183            }
184            return Collections.unmodifiableList(results);
185        }
186    
187        /**
188         * Add a rule set, or update any existing one that represents the {@link RuleSet#equals(Object) same rule set}
189         * 
190         * @param ruleSet the new rule set
191         * @return true if the rule set was added, or false if the rule set was not added (because it wasn't necessary)
192         * @throws IllegalArgumentException if <code>ruleSet</code> is null
193         * @throws InvalidRuleSetException if the supplied rule set is invalid, incomplete, incorrectly defined, or uses a JSR-94
194         *         service provider that cannot be found
195         * @see #updateRuleSet(RuleSet)
196         * @see #removeRuleSet(String)
197         */
198        public boolean addRuleSet( RuleSet ruleSet ) {
199            CheckArg.isNotNull(ruleSet, "rule set");
200            final String providerUri = ruleSet.getProviderUri();
201            final String ruleSetName = ruleSet.getName();
202            final String rules = ruleSet.getRules();
203            final Map<?, ?> properties = ruleSet.getExecutionSetProperties();
204            final Reader ruleReader = new StringReader(rules);
205            boolean updatedRuleSets = false;
206            try {
207                this.lock.writeLock().lock();
208    
209                // Make sure the rule service provider is available ...
210                RuleServiceProvider ruleServiceProvider = findRuleServiceProvider(ruleSet);
211                assert ruleServiceProvider != null;
212    
213                // Now register a new execution set ...
214                RuleAdministrator ruleAdmin = ruleServiceProvider.getRuleAdministrator();
215                if (ruleAdmin == null) {
216                    throw new InvalidRuleSetException(
217                                                      RepositoryI18n.unableToObtainJsr94RuleAdministrator.text(providerUri,
218                                                                                                               ruleSet.getComponentClassname(),
219                                                                                                               ruleSetName));
220                }
221    
222                // Is there is an existing rule set and, if so, whether it has changed ...
223                RuleSet existing = this.ruleSets.get(ruleSetName);
224    
225                // Create the rule execution set (do this before deregistering, in case there is a problem)...
226                LocalRuleExecutionSetProvider ruleExecutionSetProvider = ruleAdmin.getLocalRuleExecutionSetProvider(null);
227                RuleExecutionSet executionSet = ruleExecutionSetProvider.createRuleExecutionSet(ruleReader, properties);
228    
229                // We should add the execiting rule set if there wasn't one or if the rule set has changed ...
230                boolean shouldAdd = existing == null || ruleSet.hasChanged(existing);
231                if (existing != null && shouldAdd) {
232                    // There is an existing execution set and it needs to be updated, so deregister it ...
233                    ruleServiceProvider = deregister(ruleSet);
234                }
235                if (shouldAdd) {
236                    boolean rollback = false;
237                    try {
238                        // Now register the new execution set and update the rule set managed by this service ...
239                        ruleAdmin.registerRuleExecutionSet(ruleSetName, executionSet, null);
240                        this.ruleSets.remove(ruleSet.getName());
241                        this.ruleSets.put(ruleSet.getName(), ruleSet);
242                        updatedRuleSets = true;
243                    } catch (Throwable t) {
244                        rollback = true;
245                        throw new InvalidRuleSetException(RepositoryI18n.errorAddingOrUpdatingRuleSet.text(ruleSet.getName()), t);
246                    } finally {
247                        if (rollback) {
248                            try {
249                                // There was a problem, so re-register the original existing rule set ...
250                                if (existing != null) {
251                                    final String oldRules = existing.getRules();
252                                    final Map<?, ?> oldProperties = existing.getExecutionSetProperties();
253                                    final Reader oldRuleReader = new StringReader(oldRules);
254                                    ruleServiceProvider = findRuleServiceProvider(existing);
255                                    assert ruleServiceProvider != null;
256                                    executionSet = ruleExecutionSetProvider.createRuleExecutionSet(oldRuleReader, oldProperties);
257                                    ruleAdmin.registerRuleExecutionSet(ruleSetName, executionSet, null);
258                                    this.ruleSets.remove(ruleSetName);
259                                    this.ruleSets.put(ruleSetName, existing);
260                                }
261                            } catch (Throwable rollbackError) {
262                                // There was a problem rolling back to the existing rule set, and we're going to throw the
263                                // exception associated with the updated/new rule set, so just log this problem
264                                this.logger.error(rollbackError, RepositoryI18n.errorRollingBackRuleSetAfterUpdateFailed, ruleSetName);
265                            }
266                        }
267                    }
268                }
269            } catch (InvalidRuleSetException e) {
270                throw e;
271            } catch (ConfigurationException t) {
272                throw new InvalidRuleSetException(
273                                                  RepositoryI18n.unableToObtainJsr94RuleAdministrator.text(providerUri,
274                                                                                                           ruleSet.getComponentClassname(),
275                                                                                                           ruleSetName));
276            } catch (RemoteException t) {
277                throw new InvalidRuleSetException(
278                                                  RepositoryI18n.errorUsingJsr94RuleAdministrator.text(providerUri,
279                                                                                                       ruleSet.getComponentClassname(),
280                                                                                                       ruleSetName));
281            } catch (IOException t) {
282                throw new InvalidRuleSetException(RepositoryI18n.errorReadingRulesAndProperties.text(ruleSetName));
283            } catch (RuleExecutionSetDeregistrationException t) {
284                throw new InvalidRuleSetException(RepositoryI18n.errorDeregisteringRuleSetBeforeUpdatingIt.text(ruleSetName));
285            } catch (RuleExecutionSetCreateException t) {
286                throw new InvalidRuleSetException(RepositoryI18n.errorRecreatingRuleSet.text(ruleSetName));
287            } finally {
288                this.lock.writeLock().unlock();
289            }
290            return updatedRuleSets;
291        }
292    
293        /**
294         * Update the configuration for a sequencer, or add it if there is no {@link RuleSet#equals(Object) matching configuration}.
295         * 
296         * @param ruleSet the rule set to be updated
297         * @return true if the rule set was updated, or false if the rule set was not updated (because it wasn't necessary)
298         * @throws InvalidRuleSetException if the supplied rule set is invalid, incomplete, incorrectly defined, or uses a JSR-94
299         *         service provider that cannot be found
300         * @see #addRuleSet(RuleSet)
301         * @see #removeRuleSet(String)
302         */
303        public boolean updateRuleSet( RuleSet ruleSet ) {
304            return addRuleSet(ruleSet);
305        }
306    
307        /**
308         * Remove a rule set.
309         * 
310         * @param ruleSetName the name of the rule set to be removed
311         * @return true if the rule set was removed, or if it was not an existing rule set
312         * @throws IllegalArgumentException if <code>ruleSetName</code> is null or empty
313         * @throws SystemFailureException if the rule set was found but there was a problem removing it
314         * @see #addRuleSet(RuleSet)
315         * @see #updateRuleSet(RuleSet)
316         */
317        public boolean removeRuleSet( String ruleSetName ) {
318            CheckArg.isNotEmpty(ruleSetName, "rule set");
319            try {
320                this.lock.writeLock().lock();
321                RuleSet ruleSet = this.ruleSets.remove(ruleSetName);
322                if (ruleSet != null) {
323                    try {
324                        deregister(ruleSet);
325                    } catch (Throwable t) {
326                        // There was a problem deregistering the rule set, so put it back ...
327                        this.ruleSets.put(ruleSetName, ruleSet);
328                    }
329                    return true;
330                }
331            } catch (Throwable t) {
332                throw new SystemFailureException(RepositoryI18n.errorRemovingRuleSet.text(ruleSetName), t);
333            } finally {
334                this.lock.writeLock().unlock();
335            }
336            return false;
337        }
338    
339        /**
340         * Get the logger for this system
341         * 
342         * @return the logger
343         */
344        public Logger getLogger() {
345            return this.logger;
346        }
347    
348        /**
349         * Set the logger for this system.
350         * 
351         * @param logger the logger, or null if the standard logging should be used
352         */
353        public void setLogger( Logger logger ) {
354            this.logger = logger != null ? logger : Logger.getLogger(this.getClass());
355        }
356    
357        /**
358         * Execute the set of rules defined by the supplied rule set name. This method is safe to be concurrently called by multiple
359         * threads, and is properly synchronized with the methods to {@link #addRuleSet(RuleSet) add}, {@link #updateRuleSet(RuleSet)
360         * update}, and {@link #removeRuleSet(String) remove} rule sets.
361         * 
362         * @param ruleSetName the {@link RuleSet#getName() name} of the {@link RuleSet} that should be used
363         * @param globals the global variables
364         * @param facts the facts
365         * @return the results of executing the rule set
366         * @throws IllegalArgumentException if the rule set name is null, empty or blank, or if there is no rule set with the given
367         *         name
368         * @throws SystemFailureException if there is no JSR-94 rule service provider with the {@link RuleSet#getProviderUri() 
369         *         RuleSet's provider URI}.
370         */
371        public List<?> executeRules( String ruleSetName,
372                                     Map<String, Object> globals,
373                                     Object... facts ) {
374            CheckArg.isNotEmpty(ruleSetName, "rule set name");
375            List<?> result = null;
376            List<?> factList = Arrays.asList(facts);
377            try {
378                this.lock.readLock().lock();
379    
380                // Find the rule set ...
381                RuleSet ruleSet = this.ruleSets.get(ruleSetName);
382                if (ruleSet == null) {
383                    throw new IllegalArgumentException(RepositoryI18n.unableToFindRuleSet.text(ruleSetName));
384                }
385    
386                // Look up the provider ...
387                RuleServiceProvider ruleServiceProvider = findRuleServiceProvider(ruleSet);
388                assert ruleServiceProvider != null;
389    
390                // Create the rule session ...
391                RuleRuntime ruleRuntime = ruleServiceProvider.getRuleRuntime();
392                String executionSetName = ruleSet.getRuleSetUri();
393                RuleSession session = ruleRuntime.createRuleSession(executionSetName, globals, RuleRuntime.STATELESS_SESSION_TYPE);
394                try {
395                    StatelessRuleSession statelessSession = (StatelessRuleSession)session;
396                    result = statelessSession.executeRules(factList);
397                } finally {
398                    session.release();
399                }
400                if (this.logger.isTraceEnabled()) {
401                    String msg = "Executed rule set '{1}' with globals {2} and facts {3} resulting in {4}";
402                    this.logger.trace(msg, ruleSetName, globals, Arrays.asList(facts), result);
403                }
404            } catch (Throwable t) {
405                String msg = RepositoryI18n.errorExecutingRuleSetWithGlobalsAndFacts.text(ruleSetName, globals, Arrays.asList(facts));
406                throw new SystemFailureException(msg, t);
407            } finally {
408                this.lock.readLock().unlock();
409            }
410            return result;
411        }
412    
413        protected void removeAllRuleSets() {
414            try {
415                lock.writeLock().lock();
416                for (RuleSet ruleSet : ruleSets.values()) {
417                    try {
418                        deregister(ruleSet);
419                    } catch (Throwable t) {
420                        logger.error(t, RepositoryI18n.errorRemovingRuleSetUponShutdown, ruleSet.getName());
421                    }
422                }
423            } finally {
424                lock.writeLock().unlock();
425            }
426            this.shutdownLatch.countDown();
427        }
428    
429        protected boolean doAwaitTermination( long timeout,
430                                              TimeUnit unit ) throws InterruptedException {
431            return this.shutdownLatch.await(timeout, unit);
432        }
433    
434        protected boolean isTerminated() {
435            return this.shutdownLatch.getCount() == 0;
436        }
437    
438        /**
439         * Finds the JSR-94 service provider instance and returns it. If it could not be found, this method attempts to load it.
440         * 
441         * @param ruleSet the rule set for which the service provider is to be found; may not be null
442         * @return the rule service provider; never null
443         * @throws ConfigurationException if there is a problem loading the service provider
444         * @throws InvalidRuleSetException if the service provider could not be found
445         */
446        private RuleServiceProvider findRuleServiceProvider( RuleSet ruleSet ) throws ConfigurationException {
447            assert ruleSet != null;
448            String providerUri = ruleSet.getProviderUri();
449            RuleServiceProvider ruleServiceProvider = null;
450            try {
451                // If the provider could not be found, then a ConfigurationException will be thrown ...
452                ruleServiceProvider = RuleServiceProviderManager.getRuleServiceProvider(providerUri);
453            } catch (ConfigurationException e) {
454                try {
455                    // Use JSR-94 to load the RuleServiceProvider instance ...
456                    ClassLoader loader = this.classLoaderFactory.getClassLoader(ruleSet.getComponentClasspathArray());
457                    // Don't call ClassLoader.loadClass(String), as this doesn't initialize the class!!
458                    Class.forName(ruleSet.getComponentClassname(), true, loader);
459                    ruleServiceProvider = RuleServiceProviderManager.getRuleServiceProvider(providerUri);
460                    this.logger.debug("Loaded the rule service provider {0} ({1})", providerUri, ruleSet.getComponentClassname());
461                } catch (ConfigurationException ce) {
462                    throw ce;
463                } catch (Throwable t) {
464                    throw new InvalidRuleSetException(
465                                                      RepositoryI18n.unableToObtainJsr94ServiceProvider.text(providerUri,
466                                                                                                             ruleSet.getComponentClassname()),
467                                                      t);
468                }
469            }
470            if (ruleServiceProvider == null) {
471                throw new InvalidRuleSetException(
472                                                  RepositoryI18n.unableToObtainJsr94ServiceProvider.text(providerUri,
473                                                                                                         ruleSet.getComponentClassname()));
474            }
475            return ruleServiceProvider;
476        }
477    
478        /**
479         * Deregister the supplied rule set, if it could be found. This method does nothing if any of the service provider components
480         * could not be found.
481         * 
482         * @param ruleSet the rule set to be deregistered; may not be null
483         * @return the service provider reference, or null if the service provider could not be found ...
484         * @throws ConfigurationException
485         * @throws RuleExecutionSetDeregistrationException
486         * @throws RemoteException
487         */
488        private RuleServiceProvider deregister( RuleSet ruleSet )
489            throws ConfigurationException, RuleExecutionSetDeregistrationException, RemoteException {
490            assert ruleSet != null;
491            // Look up the provider ...
492            String providerUri = ruleSet.getProviderUri();
493            assert providerUri != null;
494    
495            // Look for the provider ...
496            RuleServiceProvider ruleServiceProvider = RuleServiceProviderManager.getRuleServiceProvider(providerUri);
497            if (ruleServiceProvider != null) {
498                // Deregister the rule set ...
499                RuleAdministrator ruleAdmin = ruleServiceProvider.getRuleAdministrator();
500                if (ruleAdmin != null) {
501                    ruleAdmin.deregisterRuleExecutionSet(ruleSet.getRuleSetUri(), null);
502                }
503            }
504            return ruleServiceProvider;
505        }
506    
507    }