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