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 }