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 }