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.common.util.StringUtil;
058    import org.jboss.dna.repository.RepositoryI18n;
059    import org.jboss.dna.repository.services.AbstractServiceAdministrator;
060    import org.jboss.dna.repository.services.AdministeredService;
061    import org.jboss.dna.repository.services.ServiceAdministrator;
062    
063    /**
064     * A rule service that is capable of executing rule sets using one or more JSR-94 rule engines. Sets of rules are
065     * {@link #addRuleSet(RuleSet) added}, {@link #updateRuleSet(RuleSet) updated}, and {@link #removeRuleSet(String) removed}
066     * (usually by some other component), and then these named rule sets can be {@link #executeRules(String, Map, Object...) run} with
067     * inputs and facts to obtain output.
068     * <p>
069     * This service is thread safe. While multiple rule sets can be safely {@link #executeRules(String, Map, Object...) executed} at
070     * the same time, all executions will be properly synchronized with methods to {@link #addRuleSet(RuleSet) add},
071     * {@link #updateRuleSet(RuleSet) update}, and {@link #removeRuleSet(String) remove} rule sets.
072     * </p>
073     * 
074     * @author Randall Hauch
075     */
076    @ThreadSafe
077    public class RuleService implements AdministeredService {
078    
079        protected static final ClassLoaderFactory DEFAULT_CLASSLOADER_FACTORY = new StandardClassLoaderFactory(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, TimeUnit unit ) throws InterruptedException {
114                return doAwaitTermination(timeout, unit);
115            }
116    
117        }
118    
119        private Logger logger;
120        private ClassLoaderFactory classLoaderFactory = DEFAULT_CLASSLOADER_FACTORY;
121        private final Administrator administrator = new Administrator();
122        private final ReadWriteLock lock = new ReentrantReadWriteLock();
123        @GuardedBy( "lock" )
124        private final Map<String, RuleSet> ruleSets = new HashMap<String, RuleSet>();
125        private final CountDownLatch shutdownLatch = new CountDownLatch(1);
126    
127        /**
128         * Create a new rule service, configured with no rule sets. Upon construction, the system is
129         * {@link ServiceAdministrator#isPaused() paused} and must be configured and then {@link ServiceAdministrator#start() started}.
130         */
131        public RuleService() {
132            this.logger = Logger.getLogger(this.getClass());
133        }
134    
135        /**
136         * Return the administrative component for this service.
137         * 
138         * @return the administrative component; never null
139         */
140        public ServiceAdministrator getAdministrator() {
141            return this.administrator;
142        }
143    
144        /**
145         * Get the class loader factory that should be used to load sequencers. By default, this service uses a factory that will
146         * return either the {@link Thread#getContextClassLoader() current thread's context class loader} (if not null) or the class
147         * loader that loaded this class.
148         * 
149         * @return the class loader factory; never null
150         * @see #setClassLoaderFactory(ClassLoaderFactory)
151         */
152        public ClassLoaderFactory getClassLoaderFactory() {
153            return this.classLoaderFactory;
154        }
155    
156        /**
157         * Set the Maven Repository that should be used to load the sequencer classes. By default, this service uses a class loader
158         * factory that will return either the {@link Thread#getContextClassLoader() current thread's context class loader} (if not
159         * null) or the class loader that loaded this class.
160         * 
161         * @param classLoaderFactory the class loader factory reference, or null if the default class loader factory should be used.
162         * @see #getClassLoaderFactory()
163         */
164        public void setClassLoaderFactory( ClassLoaderFactory classLoaderFactory ) {
165            this.classLoaderFactory = classLoaderFactory != null ? classLoaderFactory : DEFAULT_CLASSLOADER_FACTORY;
166        }
167    
168        /**
169         * Obtain the rule sets that are currently available in this service.
170         * 
171         * @return an unmodifiable copy of the rule sets; never null, but possibly empty ...
172         */
173        public Collection<RuleSet> getRuleSets() {
174            List<RuleSet> results = new ArrayList<RuleSet>();
175            try {
176                this.lock.readLock().lock();
177                // Make a copy of the rule sets ...
178                if (ruleSets.size() != 0) results.addAll(this.ruleSets.values());
179            } finally {
180                this.lock.readLock().unlock();
181            }
182            return Collections.unmodifiableList(results);
183        }
184    
185        /**
186         * Add a rule set, or update any existing one that represents the {@link RuleSet#equals(Object) same rule set}
187         * 
188         * @param ruleSet the new rule set
189         * @return true if the rule set was added, or false if the rule set was not added (because it wasn't necessary)
190         * @throws IllegalArgumentException if <code>ruleSet</code> is null
191         * @throws InvalidRuleSetException if the supplied rule set is invalid, incomplete, incorrectly defined, or uses a JSR-94
192         * service provider that cannot be found
193         * @see #updateRuleSet(RuleSet)
194         * @see #removeRuleSet(String)
195         */
196        public boolean addRuleSet( RuleSet ruleSet ) {
197            CheckArg.isNotNull(ruleSet, "rule set");
198            final String providerUri = ruleSet.getProviderUri();
199            final String ruleSetName = ruleSet.getName();
200            final String rules = ruleSet.getRules();
201            final Map<?, ?> properties = ruleSet.getExecutionSetProperties();
202            final Reader ruleReader = new StringReader(rules);
203            boolean updatedRuleSets = false;
204            try {
205                this.lock.writeLock().lock();
206    
207                // Make sure the rule service provider is available ...
208                RuleServiceProvider ruleServiceProvider = findRuleServiceProvider(ruleSet);
209                assert ruleServiceProvider != null;
210    
211                // Now register a new execution set ...
212                RuleAdministrator ruleAdmin = ruleServiceProvider.getRuleAdministrator();
213                if (ruleAdmin == null) {
214                    throw new InvalidRuleSetException(RepositoryI18n.unableToObtainJsr94RuleAdministrator.text(providerUri, ruleSet.getComponentClassname(), ruleSetName));
215                }
216    
217                // Is there is an existing rule set and, if so, whether it has changed ...
218                RuleSet existing = this.ruleSets.get(ruleSetName);
219    
220                // Create the rule execution set (do this before deregistering, in case there is a problem)...
221                LocalRuleExecutionSetProvider ruleExecutionSetProvider = ruleAdmin.getLocalRuleExecutionSetProvider(null);
222                RuleExecutionSet executionSet = ruleExecutionSetProvider.createRuleExecutionSet(ruleReader, properties);
223    
224                // We should add the execiting rule set if there wasn't one or if the rule set has changed ...
225                boolean shouldAdd = existing == null || ruleSet.hasChanged(existing);
226                if (existing != null && shouldAdd) {
227                    // There is an existing execution set and it needs to be updated, so deregister it ...
228                    ruleServiceProvider = deregister(ruleSet);
229                }
230                if (shouldAdd) {
231                    boolean rollback = false;
232                    try {
233                        // Now register the new execution set and update the rule set managed by this service ...
234                        ruleAdmin.registerRuleExecutionSet(ruleSetName, executionSet, null);
235                        this.ruleSets.remove(ruleSet.getName());
236                        this.ruleSets.put(ruleSet.getName(), ruleSet);
237                        updatedRuleSets = true;
238                    } catch (Throwable t) {
239                        rollback = true;
240                        throw new InvalidRuleSetException(RepositoryI18n.errorAddingOrUpdatingRuleSet.text(ruleSet.getName()), t);
241                    } finally {
242                        if (rollback) {
243                            try {
244                                // There was a problem, so re-register the original existing rule set ...
245                                if (existing != null) {
246                                    final String oldRules = existing.getRules();
247                                    final Map<?, ?> oldProperties = existing.getExecutionSetProperties();
248                                    final Reader oldRuleReader = new StringReader(oldRules);
249                                    ruleServiceProvider = findRuleServiceProvider(existing);
250                                    assert ruleServiceProvider != null;
251                                    executionSet = ruleExecutionSetProvider.createRuleExecutionSet(oldRuleReader, oldProperties);
252                                    ruleAdmin.registerRuleExecutionSet(ruleSetName, executionSet, null);
253                                    this.ruleSets.remove(ruleSetName);
254                                    this.ruleSets.put(ruleSetName, existing);
255                                }
256                            } catch (Throwable rollbackError) {
257                                // There was a problem rolling back to the existing rule set, and we're going to throw the
258                                // exception associated with the updated/new rule set, so just log this problem
259                                this.logger.error(rollbackError, RepositoryI18n.errorRollingBackRuleSetAfterUpdateFailed, ruleSetName);
260                            }
261                        }
262                    }
263                }
264            } catch (InvalidRuleSetException e) {
265                throw e;
266            } catch (ConfigurationException t) {
267                throw new InvalidRuleSetException(RepositoryI18n.unableToObtainJsr94RuleAdministrator.text(providerUri, ruleSet.getComponentClassname(), ruleSetName));
268            } catch (RemoteException t) {
269                throw new InvalidRuleSetException(RepositoryI18n.errorUsingJsr94RuleAdministrator.text(providerUri, ruleSet.getComponentClassname(), ruleSetName));
270            } catch (IOException t) {
271                throw new InvalidRuleSetException(RepositoryI18n.errorReadingRulesAndProperties.text(ruleSetName));
272            } catch (RuleExecutionSetDeregistrationException t) {
273                throw new InvalidRuleSetException(RepositoryI18n.errorDeregisteringRuleSetBeforeUpdatingIt.text(ruleSetName));
274            } catch (RuleExecutionSetCreateException t) {
275                throw new InvalidRuleSetException(RepositoryI18n.errorRecreatingRuleSet.text(ruleSetName));
276            } finally {
277                this.lock.writeLock().unlock();
278            }
279            return updatedRuleSets;
280        }
281    
282        /**
283         * Update the configuration for a sequencer, or add it if there is no {@link RuleSet#equals(Object) matching configuration}.
284         * 
285         * @param ruleSet the rule set to be updated
286         * @return true if the rule set was updated, or false if the rule set was not updated (because it wasn't necessary)
287         * @throws InvalidRuleSetException if the supplied rule set is invalid, incomplete, incorrectly defined, or uses a JSR-94
288         * service provider that cannot be found
289         * @see #addRuleSet(RuleSet)
290         * @see #removeRuleSet(String)
291         */
292        public boolean updateRuleSet( RuleSet ruleSet ) {
293            return addRuleSet(ruleSet);
294        }
295    
296        /**
297         * Remove a rule set.
298         * 
299         * @param ruleSetName the name of the rule set to be removed
300         * @return true if the rule set was removed, or if it was not an existing rule set
301         * @throws IllegalArgumentException if <code>ruleSetName</code> is null or empty
302         * @throws SystemFailureException if the rule set was found but there was a problem removing it
303         * @see #addRuleSet(RuleSet)
304         * @see #updateRuleSet(RuleSet)
305         */
306        public boolean removeRuleSet( String ruleSetName ) {
307            CheckArg.isNotEmpty(ruleSetName, "rule set");
308            try {
309                this.lock.writeLock().lock();
310                RuleSet ruleSet = this.ruleSets.remove(ruleSetName);
311                if (ruleSet != null) {
312                    try {
313                        deregister(ruleSet);
314                    } catch (Throwable t) {
315                        // There was a problem deregistering the rule set, so put it back ...
316                        this.ruleSets.put(ruleSetName, ruleSet);
317                    }
318                    return true;
319                }
320            } catch (Throwable t) {
321                throw new SystemFailureException(RepositoryI18n.errorRemovingRuleSet.text(ruleSetName), t);
322            } finally {
323                this.lock.writeLock().unlock();
324            }
325            return false;
326        }
327    
328        /**
329         * Get the logger for this system
330         * 
331         * @return the logger
332         */
333        public Logger getLogger() {
334            return this.logger;
335        }
336    
337        /**
338         * Set the logger for this system.
339         * 
340         * @param logger the logger, or null if the standard logging should be used
341         */
342        public void setLogger( Logger logger ) {
343            this.logger = logger != null ? logger : Logger.getLogger(this.getClass());
344        }
345    
346        /**
347         * Execute the set of rules defined by the supplied rule set name. This method is safe to be concurrently called by multiple
348         * threads, and is properly synchronized with the methods to {@link #addRuleSet(RuleSet) add},
349         * {@link #updateRuleSet(RuleSet) update}, and {@link #removeRuleSet(String) remove} rule sets.
350         * 
351         * @param ruleSetName the {@link RuleSet#getName() name} of the {@link RuleSet} that should be used
352         * @param globals the global variables
353         * @param facts the facts
354         * @return the results of executing the rule set
355         * @throws IllegalArgumentException if the rule set name is null, empty or blank, or if there is no rule set with the given
356         * name
357         * @throws SystemFailureException if there is no JSR-94 rule service provider with the
358         * {@link RuleSet#getProviderUri() RuleSet's provider URI}.
359         */
360        public List<?> executeRules( String ruleSetName, Map<String, Object> globals, Object... facts ) {
361            CheckArg.isNotEmpty(ruleSetName, "rule set name");
362            List<?> result = null;
363            List<?> factList = Arrays.asList(facts);
364            try {
365                this.lock.readLock().lock();
366    
367                // Find the rule set ...
368                RuleSet ruleSet = this.ruleSets.get(ruleSetName);
369                if (ruleSet == null) {
370                    throw new IllegalArgumentException(RepositoryI18n.unableToFindRuleSet.text(ruleSetName));
371                }
372    
373                // Look up the provider ...
374                RuleServiceProvider ruleServiceProvider = findRuleServiceProvider(ruleSet);
375                assert ruleServiceProvider != null;
376    
377                // Create the rule session ...
378                RuleRuntime ruleRuntime = ruleServiceProvider.getRuleRuntime();
379                String executionSetName = ruleSet.getRuleSetUri();
380                RuleSession session = ruleRuntime.createRuleSession(executionSetName, globals, RuleRuntime.STATELESS_SESSION_TYPE);
381                try {
382                    StatelessRuleSession statelessSession = (StatelessRuleSession)session;
383                    result = statelessSession.executeRules(factList);
384                } finally {
385                    session.release();
386                }
387                if (this.logger.isTraceEnabled()) {
388                    String msg = "Executed rule set '{1}' with globals {2} and facts {3} resulting in {4}";
389                    this.logger.trace(msg, ruleSetName, StringUtil.readableString(globals), StringUtil.readableString(facts), StringUtil.readableString(result));
390                }
391            } catch (Throwable t) {
392                throw new SystemFailureException(RepositoryI18n.errorExecutingRuleSetWithGlobalsAndFacts.text(ruleSetName, StringUtil.readableString(globals), StringUtil.readableString(facts)), t);
393            } finally {
394                this.lock.readLock().unlock();
395            }
396            return result;
397        }
398    
399        protected void removeAllRuleSets() {
400            try {
401                lock.writeLock().lock();
402                for (RuleSet ruleSet : ruleSets.values()) {
403                    try {
404                        deregister(ruleSet);
405                    } catch (Throwable t) {
406                        logger.error(t, RepositoryI18n.errorRemovingRuleSetUponShutdown, ruleSet.getName());
407                    }
408                }
409            } finally {
410                lock.writeLock().unlock();
411            }
412            this.shutdownLatch.countDown();
413        }
414    
415        protected boolean doAwaitTermination( long timeout, TimeUnit unit ) throws InterruptedException {
416            return this.shutdownLatch.await(timeout, unit);
417        }
418    
419        protected boolean isTerminated() {
420            return this.shutdownLatch.getCount() == 0;
421        }
422    
423        /**
424         * Finds the JSR-94 service provider instance and returns it. If it could not be found, this method attempts to load it.
425         * 
426         * @param ruleSet the rule set for which the service provider is to be found; may not be null
427         * @return the rule service provider; never null
428         * @throws ConfigurationException if there is a problem loading the service provider
429         * @throws InvalidRuleSetException if the service provider could not be found
430         */
431        private RuleServiceProvider findRuleServiceProvider( RuleSet ruleSet ) throws ConfigurationException {
432            assert ruleSet != null;
433            String providerUri = ruleSet.getProviderUri();
434            RuleServiceProvider ruleServiceProvider = null;
435            try {
436                // If the provider could not be found, then a ConfigurationException will be thrown ...
437                ruleServiceProvider = RuleServiceProviderManager.getRuleServiceProvider(providerUri);
438            } catch (ConfigurationException e) {
439                try {
440                    // Use JSR-94 to load the RuleServiceProvider instance ...
441                    ClassLoader loader = this.classLoaderFactory.getClassLoader(ruleSet.getComponentClasspathArray());
442                    // Don't call ClassLoader.loadClass(String), as this doesn't initialize the class!!
443                    Class.forName(ruleSet.getComponentClassname(), true, loader);
444                    ruleServiceProvider = RuleServiceProviderManager.getRuleServiceProvider(providerUri);
445                    this.logger.debug("Loaded the rule service provider {0} ({1})", providerUri, ruleSet.getComponentClassname());
446                } catch (ConfigurationException ce) {
447                    throw ce;
448                } catch (Throwable t) {
449                    throw new InvalidRuleSetException(RepositoryI18n.unableToObtainJsr94ServiceProvider.text(providerUri, ruleSet.getComponentClassname()), t);
450                }
451            }
452            if (ruleServiceProvider == null) {
453                throw new InvalidRuleSetException(RepositoryI18n.unableToObtainJsr94ServiceProvider.text(providerUri, ruleSet.getComponentClassname()));
454            }
455            return ruleServiceProvider;
456        }
457    
458        /**
459         * Deregister the supplied rule set, if it could be found. This method does nothing if any of the service provider components
460         * could not be found.
461         * 
462         * @param ruleSet the rule set to be deregistered; may not be null
463         * @return the service provider reference, or null if the service provider could not be found ...
464         * @throws ConfigurationException
465         * @throws RuleExecutionSetDeregistrationException
466         * @throws RemoteException
467         */
468        private RuleServiceProvider deregister( RuleSet ruleSet ) throws ConfigurationException, RuleExecutionSetDeregistrationException, RemoteException {
469            assert ruleSet != null;
470            // Look up the provider ...
471            String providerUri = ruleSet.getProviderUri();
472            assert providerUri != null;
473    
474            // Look for the provider ...
475            RuleServiceProvider ruleServiceProvider = RuleServiceProviderManager.getRuleServiceProvider(providerUri);
476            if (ruleServiceProvider != null) {
477                // Deregister the rule set ...
478                RuleAdministrator ruleAdmin = ruleServiceProvider.getRuleAdministrator();
479                if (ruleAdmin != null) {
480                    ruleAdmin.deregisterRuleExecutionSet(ruleSet.getRuleSetUri(), null);
481                }
482            }
483            return ruleServiceProvider;
484        }
485    
486    }