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 }