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 }