/**
* Copyright 2010 JBoss Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.drools.agent;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;
import org.drools.RuleBase;
import org.drools.RuleBaseConfiguration;
import org.drools.RuleBaseFactory;
import org.drools.RuntimeDroolsException;
import org.drools.rule.Package;
/**
* This manages a single rulebase, based on the properties given.
* You should only have ONE instance of this agent per rulebase configuration.
* You can get the rulebase from this agent repeatedly, as needed, or if you keep the rulebase,
* under most configurations it will be automatically updated.
*
* How this behaves depends on the properties that you pass into it (documented below)
*
* CONFIG OPTIONS (to be passed in as properties):
* <code>newInstance</code>: setting this to "true" means that each time the rules are changed
* a new instance of the rulebase is created (as opposed to updated in place)
* the default is to update in place. DEFAULT: false. If you set this to true,
* then you will need to call getRuleBase() each time you want to use it. If it is false,
* then it means you can keep your reference to the rulebase and it will be updated automatically
* (as well as any stateful sessions).
*
* <code>poll</code>The number of seconds to poll for changes. Polling
* happens in a background thread. eg: poll=30 #30 second polling.
*
* <code>file</code>: a space seperated listing of files that make up the
* packages of the rulebase. Each package can only be in one file. You can't have
* packages spread across files. eg: file=/your/dir/file1.pkg file=/your/dir/file2.pkg
* If the file has a .pkg extension, then it will be loaded as a binary Package (eg from the BRMS). If its a
* DRL file (ie a file with a .drl extension with rule source in it), then it will attempt to compile it (of course, you will need the drools-compiler and its dependencies
* available on your classpath).
*
* <code>dir</code>: a single file system directory to monitor for packages.
* As with files, each package must be in its own file.
* eg: dir=/your/dir
*
* <code>url</code>: A space seperated URL to a binary rulebase in the BRMS.
* eg: url=http://server/drools-guvnor/packages/somePakage/VERSION_1
* For URL you will also want a local cache directory setup:
* eg: localCacheDir=/some/dir/that/exists
* This is needed so that the runtime can startup and load packages even if the BRMS
* is not available (or the network).
*
* <code>name</code>
* the Name is used in any logging, so each agent can be differentiated (you may have one agent per rulebase
* that you need in your application).
*
* There is also an AgentEventListener interface which you can provide which will call back when lifecycle
* events happen, or errors/warnings occur. As the updating happens in a background thread, this may be important.
* The default event listener logs to the System.err output stream.
*
* @author Michael Neale
*/
public class RuleAgent {
/**
* Following are property keys to be used in the property
* config file.
*/
public static final String NEW_INSTANCE = "newInstance";
public static final String FILES = "file";
public static final String DIRECTORY = "dir";
public static final String URLS = "url";
public static final String POLL_INTERVAL = "poll";
public static final String CONFIG_NAME = "name"; //name is optional
public static final String USER_NAME = "username";
public static final String PASSWORD = "password";
public static final String ENABLE_BASIC_AUTHENTICATION = "enableBasicAuthentication";
//this is needed for cold starting when BRMS is down (ie only for URL).
public static final String LOCAL_URL_CACHE = "localCacheDir";
/**
* Here is where we have a map of providers to the key that appears on the configuration.
*/
public static Map PACKAGE_PROVIDERS = new HashMap() {
{
put( FILES,
FileScanner.class );
put( DIRECTORY,
DirectoryScanner.class );
put( URLS,
URLScanner.class );
}
};
String name;
/**
* This is true if the rulebase is created anew each time.
*/
private boolean newInstance;
/**
* The rule base that is being managed.
*/
private RuleBase ruleBase;
/**
* the configuration for the RuleBase
*/
private RuleBaseConfiguration ruleBaseConf;
/**
* The timer that is used to monitor for changes and deal with them.
*/
private Timer timer;
/**
* The providers that actually do the work.
*/
List providers;
/**
* This keeps the packages around that have been loaded.
*/
Map packages = new HashMap();
/**
* For logging events (important for stuff that happens in the background).
*/
AgentEventListener listener = getDefaultListener();
/**
* Polling interval value, in seconds, used in the Timer.
*/
private int secondsToRefresh;
/**
* Properties configured to load up packages into a rulebase (and monitor them
* for changes).
*/
public static RuleAgent newRuleAgent(Properties config) {
return newRuleAgent( config,
null,
null );
}
/**
* Properties configured to load up packages into a rulebase with the provided
* configuration (and monitor them for changes).
*/
public static RuleAgent newRuleAgent(Properties config,
RuleBaseConfiguration ruleBaseConf) {
return newRuleAgent( config,
null,
ruleBaseConf );
}
/**
* This allows an optional listener to be passed in.
* The default one prints some stuff out to System.err only when really needed.
*/
public static RuleAgent newRuleAgent(Properties config,
AgentEventListener listener) {
return newRuleAgent( config,
listener,
null );
}
/**
* This allows an optional listener to be passed in.
* The default one prints some stuff out to System.err only when really needed.
*/
public static RuleAgent newRuleAgent(Properties config,
AgentEventListener listener,
RuleBaseConfiguration ruleBaseConf) {
RuleAgent agent = new RuleAgent( ruleBaseConf );
if ( listener != null ) {
agent.listener = listener;
}
if ( ruleBaseConf == null ) {
agent.init( config,
true );
} else {
agent.init( config );
}
return agent;
}
void init(Properties config) {
init( config,
false );
}
/**
*
* @param config
* @param lookForRuleBaseConfigurations true if config contains rule base configuration data that should be used.
*/
void init(Properties config,
boolean lookForRuleBaseConfigurations) {
boolean newInstance = Boolean.valueOf( config.getProperty( NEW_INSTANCE,
"false" ) ).booleanValue();
int secondsToRefresh = Integer.parseInt( config.getProperty( POLL_INTERVAL,
"-1" ) );
final String name = config.getProperty( CONFIG_NAME,
"default" );
listener.setAgentName( name );
listener.info( "Configuring with newInstance=" + newInstance + ", secondsToRefresh=" + secondsToRefresh );
List provs = new ArrayList();
Properties droolsProperties = new Properties();
for ( Iterator iter = config.keySet().iterator(); iter.hasNext(); ) {
String key = (String) iter.next();
if ( ruleBaseConf != null && key.startsWith( "drools." ) ) {
droolsProperties.setProperty( key,
config.getProperty( key ) );
} else {
PackageProvider prov = getProvider( key,
config );
if ( prov != null ) {
listener.info( "Configuring package provider : " + prov.toString() );
provs.add( prov );
}
}
}
// If there is no ruleBase and config file had rule base properties, set properties.
if ( lookForRuleBaseConfigurations && !droolsProperties.isEmpty() ) {
ruleBaseConf = new RuleBaseConfiguration( droolsProperties );
}
configure( newInstance,
provs,
secondsToRefresh );
}
/**
* Pass in the name and full path to a config file that is on the classpath.
*/
public static RuleAgent newRuleAgent(String propsFileName) {
return newRuleAgent( loadFromProperties( propsFileName ) );
}
/**
* Pass in the name and full path to a config file that is on the classpath.
*/
public static RuleAgent newRuleAgent(String propsFileName,
RuleBaseConfiguration ruleBaseConfiguration) {
return newRuleAgent( loadFromProperties( propsFileName ),
ruleBaseConfiguration );
}
/**
* This takes in an optional listener. Listener must not be null in this case.
*/
public static RuleAgent newRuleAgent(String propsFileName,
AgentEventListener listener) {
return newRuleAgent( loadFromProperties( propsFileName ),
listener );
}
/**
* This takes in an optional listener and RuleBaseConfiguration. Listener must not be null in this case.
*/
public static RuleAgent newRuleAgent(String propsFileName,
AgentEventListener listener,
RuleBaseConfiguration ruleBaseConfiguration) {
return newRuleAgent( loadFromProperties( propsFileName ),
listener,
ruleBaseConfiguration );
}
public void setName(String name) {
this.name = name;
if ( this.listener != null ) {
this.listener.setAgentName( this.name );
}
}
static Properties loadFromProperties(String propsFileName) {
InputStream in = RuleAgent.class.getResourceAsStream( propsFileName );
Properties props = new Properties();
try {
props.load( in );
return props;
} catch ( IOException e ) {
throw new RuntimeDroolsException( "Unable to load properties. Needs to be the path and name of a config file on your classpath.",
e );
} finally {
if ( null != in ) {
try {
in.close();
} catch ( IOException e ) {
throw new RuntimeDroolsException( "Unable to load properties. Could not close InputStream.",
e );
}
}
}
}
/**
* Return a configured provider ready to go.
*/
private PackageProvider getProvider(String key,
Properties config) {
if ( !PACKAGE_PROVIDERS.containsKey( key ) ) {
return null;
}
Class clz = (Class) PACKAGE_PROVIDERS.get( key );
try {
PackageProvider prov = (PackageProvider) clz.newInstance();
prov.setAgentListener( listener );
prov.configure( config );
return prov;
} catch ( InstantiationException e ) {
throw new RuntimeDroolsException( "Unable to load up a package provider for " + key,
e );
} catch ( IllegalAccessException e ) {
throw new RuntimeDroolsException( "Unable to load up a package provider for " + key,
e );
}
}
synchronized void configure(boolean newInstance,
List provs,
int secondsToRefresh) {
this.newInstance = newInstance;
this.providers = provs;
//run it the first time for each.
refreshRuleBase();
if ( secondsToRefresh != -1 ) {
startPolling( secondsToRefresh );
}
}
public void refreshRuleBase() {
List<Package> changedPackages = new ArrayList<Package>();
List<String> removedPackages = new ArrayList<String>();
for ( Iterator iter = providers.iterator(); iter.hasNext(); ) {
PackageProvider prov = (PackageProvider) iter.next();
PackageChangeInfo info = checkForChanges( prov );
Collection<Package> changes = info.getChangedPackages();
Collection<String> removed = info.getRemovedPackages();
if ( changes != null && changes.size() > 0 ) {
changedPackages.addAll( changes );
}
if ( removed != null && removed.size() > 0 ) {
removedPackages.addAll( removed );
}
}
// Update changes.
if ( changedPackages.size() > 0 || removedPackages.size() > 0 ) {
listener.info( "Applying changes to the rulebase." );
//we have a change
if ( this.newInstance ) {
listener.info( "Creating a new rulebase as per settings." );
//blow away old
this.ruleBase = RuleBaseFactory.newRuleBase( this.ruleBaseConf );
// Remove removed packages.
for ( String name : removedPackages ) {
this.packages.remove( name );
}
//need to store ALL packages
for ( Package element : changedPackages ) {
this.packages.put( element.getName(),
element ); //replace
}
//get packages from full name
PackageProvider.applyChanges( this.ruleBase,
false,
this.packages.values(),
this.listener );
} else {
PackageProvider.applyChanges( this.ruleBase,
true,
changedPackages,
removedPackages,
this.listener );
}
}
}
private synchronized PackageChangeInfo checkForChanges(PackageProvider prov) {
listener.debug( "SCANNING FOR CHANGE " + prov.toString() );
if ( this.ruleBase == null ) ruleBase = RuleBaseFactory.newRuleBase( this.ruleBaseConf );
PackageChangeInfo info = prov.loadPackageChanges();
return info;
}
/**
* Convert a space separated list into a List of stuff.
* If a filename or whatnot has a space in it, you can put double quotes around it
* and it will read it in as one token.
*/
static List list(String property) {
if ( property == null ) return Collections.EMPTY_LIST;
char[] cs = property.toCharArray();
boolean inquotes = false;
List items = new ArrayList();
String current = "";
for ( int i = 0; i < cs.length; i++ ) {
char c = cs[i];
switch ( c ) {
case '\"' :
if ( inquotes ) {
items.add( current );
current = "";
}
inquotes = !inquotes;
break;
default :
if ( !inquotes && (c == ' ' || c == '\n' || c == '\r' || c == '\t') ) {
if ( !"".equals( current.trim() ) ) {
items.add( current );
current = "";
}
} else {
current = current + c;
}
break;
}
}
if ( !"".equals( current.trim() ) ) {
items.add( current );
}
return items;
}
/**
* Return a current rulebase.
* Depending on the configuration, this may be a new object each time
* the rules are updated.
*
*/
public synchronized RuleBase getRuleBase() {
return this.ruleBase;
}
RuleAgent(RuleBaseConfiguration ruleBaseConf) {
if ( ruleBaseConf == null ) {
this.ruleBaseConf = new RuleBaseConfiguration();
} else {
this.ruleBaseConf = ruleBaseConf;
}
}
/**
* Stop the polling (if it is happening)
*/
public synchronized void stopPolling() {
if ( this.timer != null ) timer.cancel();
timer = null;
}
/**
* Will start polling. If polling is already running it does nothing.
*
*/
public synchronized void startPolling() {
if ( this.timer == null ) {
startPolling( this.secondsToRefresh );
}
}
/**
* Will start polling. If polling is already happening and of the same interval
* it will do nothing, if the interval is different it will stop the current Timer
* and create a new Timer for the new interval.
* @param secondsToRefresh
*/
public synchronized void startPolling(int secondsToRefresh) {
if ( this.timer != null ) {
if ( this.secondsToRefresh != secondsToRefresh ) {
stopPolling();
} else {
// do nothing.
return;
}
}
this.secondsToRefresh = secondsToRefresh;
int interval = this.secondsToRefresh * 1000;
//now schedule it for polling
timer = new Timer( true );
timer.schedule( new TimerTask() {
public void run() {
try {
listener.debug( "Checking for updates." );
refreshRuleBase();
} catch ( Exception e ) {
//don't want to stop execution here.
listener.exception( e );
}
}
},
interval,
interval );
}
boolean isNewInstance() {
return newInstance;
}
public synchronized boolean isPolling() {
return this.timer != null;
}
/**
* This should only be used once, on setup.
* @return
*/
private AgentEventListener getDefaultListener() {
return new AgentEventListener() {
private String name;
public String time() {
Date d = new Date();
return d.toString();
}
public void exception(String message, Throwable e) {
System.err.println( "RuleAgent(" + name + ") EXCEPTION (" + time() + "): " + e.getMessage() + ". Stack trace should follow." );
e.printStackTrace( System.err );
}
public void exception(Throwable e) {
System.err.println( "RuleAgent(" + name + ") EXCEPTION (" + time() + "): " + e.getMessage() + ". Stack trace should follow." );
e.printStackTrace( System.err );
}
public void info(String message) {
System.err.println( "RuleAgent(" + name + ") INFO (" + time() + "): " + message );
}
public void warning(String message) {
System.err.println( "RuleAgent(" + name + ") WARNING (" + time() + "): " + message );
}
public void debug(String message) {
//do nothing...
}
public void setAgentName(String name) {
this.name = name;
}
public void debug(String message,
Object object) {
}
public void info(String message,
Object object) {
}
public void warning(String message,
Object object) {
}
};
}
RuleBaseConfiguration getRuleBaseConfiguration() {
return ruleBaseConf;
}
}