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 }