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 }