/**
* 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.impl;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import org.drools.ChangeSet;
import org.drools.KnowledgeBase;
import org.drools.KnowledgeBaseFactory;
import org.drools.RuleBase;
import org.drools.SystemEventListener;
import org.drools.SystemEventListenerFactory;
import org.drools.agent.KnowledgeAgent;
import org.drools.agent.KnowledgeAgentConfiguration;
import org.drools.agent.ResourceDiffProducer;
import org.drools.builder.KnowledgeBuilder;
import org.drools.builder.KnowledgeBuilderConfiguration;
import org.drools.builder.KnowledgeBuilderFactory;
import org.drools.builder.ResourceType;
import org.drools.common.AbstractRuleBase;
import org.drools.common.InternalRuleBase;
import org.drools.core.util.DroolsStreamUtils;
import org.drools.definition.KnowledgeDefinition;
import org.drools.definition.KnowledgePackage;
import org.drools.definition.process.Process;
import org.drools.definitions.impl.KnowledgePackageImp;
import org.drools.event.KnowledgeAgentEventSupport;
import org.drools.event.io.ResourceChangeListener;
import org.drools.event.knowledgeagent.KnowledgeAgentEventListener;
import org.drools.impl.KnowledgeBaseImpl;
import org.drools.impl.StatelessKnowledgeSessionImpl;
import org.drools.io.Resource;
import org.drools.io.ResourceFactory;
import org.drools.io.ResourcedObject;
import org.drools.io.impl.ClassPathResource;
import org.drools.io.impl.ResourceChangeNotifierImpl;
import org.drools.io.internal.InternalResource;
import org.drools.reteoo.ReteooRuleBase;
import org.drools.rule.Function;
import org.drools.rule.Package;
import org.drools.rule.Query;
import org.drools.rule.Rule;
import org.drools.rule.TypeDeclaration;
import org.drools.runtime.KnowledgeSessionConfiguration;
import org.drools.runtime.StatefulKnowledgeSession;
import org.drools.runtime.StatelessKnowledgeSession;
import org.drools.xml.ChangeSetSemanticModule;
import org.drools.xml.SemanticModules;
import org.drools.xml.XmlChangeSetReader;
/**
* Drools Implementation of the KnowledgeAgent interface. Implements itself as a
* ResourceChangeListener as well so it can act as an agent service to provide
* incremental of the KnowledgeBase which connects to this or entirely new
* rebuilds for new KnowledgeBases.
*
* @author Mark Proctor, Sam Romano
*/
public class KnowledgeAgentImpl
implements
KnowledgeAgent,
ResourceChangeListener {
private String name;
private Set<Resource> resourceDirectories;
private KnowledgeBase kbase;
private ResourceChangeNotifierImpl notifier;
private boolean newInstance;
private SystemEventListener listener;
private boolean scanDirectories;
private boolean useKBaseClassLoaderForCompiling;
private LinkedBlockingQueue<ChangeSet> queue;
private Thread thread;
private ChangeSetNotificationDetector changeSetNotificationDetector;
private SemanticModules semanticModules;
private final RegisteredResourceMap registeredResources = new RegisteredResourceMap();
private Map<Resource, String> dslResources = new HashMap<Resource, String>();
private KnowledgeAgentEventSupport eventSupport = new KnowledgeAgentEventSupport();
private KnowledgeBuilderConfiguration builderConfiguration;
/**
* Default constructor for KnowledgeAgentImpl
*
* @param name
* @param kbase
* @param configuration
*/
public KnowledgeAgentImpl(String name,
KnowledgeBase kbase,
KnowledgeAgentConfiguration configuration,
KnowledgeBuilderConfiguration builderConfiguration) {
this.name = name;
this.kbase = kbase;
this.builderConfiguration = builderConfiguration;
this.resourceDirectories = new HashSet<Resource>();
// this.listener = listener;
this.listener = SystemEventListenerFactory.getSystemEventListener();
this.queue = new LinkedBlockingQueue<ChangeSet>();
boolean scanResources = false;
boolean monitor = false;
if ( configuration != null ) {
// New Instance describes if we do incremental builds or not
this.newInstance = ((KnowledgeAgentConfigurationImpl) configuration).isNewInstance();
this.useKBaseClassLoaderForCompiling = ((KnowledgeAgentConfigurationImpl) configuration).isUseKBaseClassLoaderForCompiling();
this.notifier = (ResourceChangeNotifierImpl) ResourceFactory.getResourceChangeNotifierService();
if ( ((KnowledgeAgentConfigurationImpl) configuration).isMonitorChangeSetEvents() ) {
monitor = true;
}
if ( ((KnowledgeAgentConfigurationImpl) configuration).isScanDirectories() ) {
this.scanDirectories = true;
}
scanResources = ((KnowledgeAgentConfigurationImpl) configuration).isScanResources();
if ( scanResources ) {
this.notifier.addResourceChangeMonitor( ResourceFactory.getResourceChangeScannerService() );
monitor = true; // if scanning, monitor must be true;
}
}
monitorResourceChangeEvents( monitor );
autoBuildResourceMapping();
this.listener.info( "KnowledgeAgent created, with configuration:\nmonitorChangeSetEvents="
+ monitor
+ " scanResources="
+ scanResources
+ " scanDirectories="
+ this.scanDirectories
+ " newInstance=" + this.newInstance );
}
public void setSystemEventListener(SystemEventListener listener) {
this.listener = listener;
}
public Set<Resource> getResourceDirectories() {
return this.resourceDirectories;
}
public boolean isNewInstance() {
return this.newInstance;
}
public void applyChangeSet(Resource resource) {
applyChangeSet( getChangeSet( resource ) );
}
public void applyChangeSet(ChangeSet changeSet) {
synchronized ( this.registeredResources ) {
this.eventSupport.fireBeforeChangeSetApplied( changeSet );
this.listener.info( "KnowledgeAgent applying ChangeSet" );
ChangeSetState changeSetState = new ChangeSetState();
changeSetState.scanDirectories = this.scanDirectories;
// incremental build is inverse of newInstance
changeSetState.incrementalBuild = !(this.newInstance);
// Process the new ChangeSet
processChangeSet( changeSet,
changeSetState );
// Rebuild or do an update to the KnowledgeBase
buildKnowledgeBase( changeSetState );
// Rebuild the resource mapping
//buildResourceMapping();
this.eventSupport.fireAfterChangeSetApplied( changeSet );
}
}
public void processChangeSet(Resource resource,
ChangeSetState changeSetState) {
processChangeSet( getChangeSet( resource ),
changeSetState );
}
/**
* Processes a changeSet.
* If {@link ChangeSetState#incrementalBuild} is set to true, this method
* fill the lists and Maps of <code>changeSetState</code>.
*
* @param changeSet
* @param changeSetState
*/
public void processChangeSet(ChangeSet changeSet,
ChangeSetState changeSetState) {
synchronized ( this.registeredResources ) {
this.eventSupport.fireBeforeChangeSetProcessed( changeSet );
/*
* Process the added resources from a ChangeSet by subscribing to
* the notifier and inserting a new ResourceMapping.
*/
for ( Resource resource : changeSet.getResourcesAdded() ) {
this.eventSupport.fireBeforeResourceProcessed( changeSet,
resource,
((InternalResource) resource).getResourceType(),
ResourceStatus.RESOURCE_ADDED );
if ( ((InternalResource) resource).getResourceType() == ResourceType.DSL ) {
this.notifier.subscribeResourceChangeListener( this,
resource );
try {
this.retrieveDSLResource( resource );
} catch ( IOException ex ) {
this.listener.exception( "KnowledgeAgent Fails trying to read DSL Resource: "
+ resource,
ex );
}
} else if ( ((InternalResource) resource).getResourceType() == ResourceType.CHANGE_SET ) {
// @TODO We should not ignore an added change set
this.listener.debug( "KnowledgeAgent processing sub ChangeSet="
+ resource );
processChangeSet( resource,
changeSetState );
} else if ( ((InternalResource) resource).isDirectory() ) {
this.resourceDirectories.add( resource );
this.listener.debug( "KnowledgeAgent subscribing to directory="
+ resource );
this.notifier.subscribeResourceChangeListener( this,
resource );
// if it's a dir, subscribe it's children first
for ( Resource child : ((InternalResource) resource).listResources() ) {
// ignore sub directories
if ( ((InternalResource) child).isDirectory() ) {
continue;
}
((InternalResource) child).setResourceType( ((InternalResource) resource).getResourceType() );
this.addDefinitionMapping( child,
null,
true );
if ( this.addResourceMapping( child,
true )
&& changeSetState.incrementalBuild ) {
changeSetState.addedResources.add( child );
}
}
} else {
if ( this.addResourceMapping( resource,
true )
&& changeSetState.incrementalBuild ) {
changeSetState.addedResources.add( resource );
}
}
this.eventSupport.fireAfterResourceProcessed( changeSet,
resource,
((InternalResource) resource).getResourceType(),
ResourceStatus.RESOURCE_ADDED );
}
/*
* For those marked as removed by the ChangeSet, remove their
* mappings, index them if we are doing incremental builds so the
* incremental building process knows what to remove.
*/
for ( Resource resource : changeSet.getResourcesRemoved() ) {
this.eventSupport.fireBeforeResourceProcessed( changeSet,
resource,
((InternalResource) resource).getResourceType(),
ResourceStatus.RESOURCE_MODIFIED );
if ( ((InternalResource) resource).getResourceType() == ResourceType.DSL ) {
this.notifier.unsubscribeResourceChangeListener( this,
resource );
this.dslResources.remove( resource );
} else if ( ((InternalResource) resource).getResourceType() == ResourceType.CHANGE_SET ) {
// @TODO Is this true? Shouldn't we just ignore it in
// removed?
processChangeSet( resource,
changeSetState );
} else if ( changeSetState.scanDirectories
&& ((InternalResource) resource).isDirectory() ) {
this.listener.debug( "KnowledgeAgent unsubscribing from directory resource="
+ resource );
this.resourceDirectories.remove( resource );
this.notifier.unsubscribeResourceChangeListener( this,
resource );
} else {
Set<KnowledgeDefinition> definitions = this.removeResourceMapping( resource,
true );
if ( definitions != null && changeSetState.incrementalBuild ) {
changeSetState.removedResourceMappings.put( resource,
definitions );
}
}
this.eventSupport.fireAfterResourceProcessed( changeSet,
resource,
((InternalResource) resource).getResourceType(),
ResourceStatus.RESOURCE_MODIFIED );
}
/*
* For those marked as modified, remove their ResourceMapping,
* attach it to the ChangeSetState, and add a new one - it will be
* repopulated with the KnowledgeDefinitions later after rebuilding.
* Process any modified ChangeSets - treat them as if they were new.
*/
for ( Resource resource : changeSet.getResourcesModified() ) {
this.eventSupport.fireBeforeResourceProcessed( changeSet,
resource,
((InternalResource) resource).getResourceType(),
ResourceStatus.RESOURCE_REMOVED );
if ( ((InternalResource) resource).getResourceType() == ResourceType.DSL ) {
try {
this.retrieveDSLResource( resource );
} catch ( IOException ex ) {
this.listener.exception( "KnowledgeAgent Fails trying to read DSL Resource: " + resource,
ex );
}
} else if ( ((InternalResource) resource).getResourceType() == ResourceType.CHANGE_SET ) {
// processChangeSet(resource, changeSetState);
continue;
} else if ( ((InternalResource) resource).isDirectory() ) {
if ( this.resourceDirectories.add( resource ) ) {
this.listener.warning( "KnowledgeAgent is subscribing to a modified directory=" + resource+
" when it should have already been subscribed" );
this.notifier.subscribeResourceChangeListener( this,
resource );
}
// if it's a dir, subscribe it's children first
for ( Resource child : ((InternalResource) resource).listResources() ) {
// ignore sub directories
if ( ((InternalResource) child).isDirectory() ) {
continue;
}
if ( this.addResourceMapping( child,
true ) ) {
((InternalResource) child).setResourceType( ((InternalResource) resource).getResourceType() );
if ( changeSetState.incrementalBuild ) {
changeSetState.addedResources.add( child );
}
}
}
} else {
boolean isResourceMapped = this.registeredResources.isResourceMapped( resource );
if ( !isResourceMapped ) {
this.listener.warning( "KnowledgeAgent subscribing to new resource="
+ resource
+ ", though it was marked as modified." );
this.addResourceMapping( resource,
true );
if ( changeSetState.incrementalBuild ) {
changeSetState.addedResources.add( resource );
}
} else {
if ( changeSetState.incrementalBuild ) {
Set<KnowledgeDefinition> definitions = this.removeResourceMapping( resource,
true );
changeSetState.modifiedResourceMappings.put( resource,
definitions );
//adds a new empty mapping that will be filled in buildKnowledgeBase()
this.addResourceMapping( resource,
false );
}
}
}
this.eventSupport.fireAfterResourceProcessed( changeSet,
resource,
((InternalResource) resource).getResourceType(),
ResourceStatus.RESOURCE_REMOVED );
}
this.eventSupport.fireAfterChangeSetProcessed( changeSet,
changeSetState.addedResources,
changeSetState.modifiedResourceMappings,
changeSetState.removedResourceMappings );
}
}
/**
* Returns a ChangeSet based on a resource with a resource type of
* ChangeSet.
*
* @param resource
* A resource with the type set to ChangeSet
* @return A ChangeSet that can be processed by this Agent.
*/
public ChangeSet getChangeSet(Resource resource) {
if ( this.semanticModules == null ) {
this.semanticModules = new SemanticModules();
this.semanticModules.addSemanticModule( new ChangeSetSemanticModule() );
}
XmlChangeSetReader reader = new XmlChangeSetReader( this.semanticModules );
if ( resource instanceof ClassPathResource ) {
reader.setClassLoader( ((ClassPathResource) resource).getClassLoader(),
null );
} else {
reader.setClassLoader( ((AbstractRuleBase) (((KnowledgeBaseImpl) this.kbase).ruleBase)).getConfiguration().getClassLoader(),
null );
}
ChangeSet changeSet = null;
try {
changeSet = reader.read( resource.getReader() );
} catch ( Exception e ) {
this.listener.exception( new RuntimeException(
"Unable to parse ChangeSet",
e ) );
}
if ( changeSet == null ) {
this.listener.exception( new RuntimeException(
"Unable to parse ChangeSet" ) );
}
return changeSet;
}
/**
* Keeps state information during the 'state' of a ChangeSet alteration so
* past information can be kept along the way.
*
* @author Mark Proctor
*/
public static class ChangeSetState {
List<Resource> addedResources = new ArrayList<Resource>();
/**
* Map of removed definitions. The Set of kdefinitions is the original
* definitions of the resource (before the deletion).
*/
Map<Resource, Set<KnowledgeDefinition>> removedResourceMappings = new HashMap<Resource, Set<KnowledgeDefinition>>();
/**
* Map of modified definitions. The Set of kdefinitions is the original
* definitions of the resource (before the modification).
*/
Map<Resource, Set<KnowledgeDefinition>> modifiedResourceMappings = new HashMap<Resource, Set<KnowledgeDefinition>>();
/**
*Map of created Packages. The agent will create this packages when
* processing added and modified resources
*/
Map<Resource, KnowledgePackage> createdPackages = new LinkedHashMap<Resource, KnowledgePackage>();
boolean scanDirectories;
boolean incrementalBuild;
}
/**
* Same as {@link #buildResourceMapping(org.drools.rule.Package, org.drools.io.Resource, boolean)
* buildResourceMapping(org.drools.rule.Package, org.drools.io.Resource, false)}.
* If <code>resource</code> is null, this method does nothing.
* @param pkg
* @param resource
*/
private void buildResourceMapping(Package pkg,
Resource resource) {
if ( resource == null ) {
this.listener.warning( "KnowledgeAgent: trying to build a resource map for a null resource!" );
return;
}
this.buildResourceMapping( pkg,
resource,
false );
}
/**
* Iterates over the pkg's definitions and maps it to resource.
* If autoDiscoverResource is set to true, the resource used in the mapping
* will be taken from each definition. In this case, the parameter <code>
* resource</code> is not used and should be null. This is useful for packages
* that contains definitions from more than one resource.
* If <code>autoDiscoverResource</code> is false and <code>resource</code>
* is null, this method does nothig.
* @param pkg the definitions present in this package will be iterated and
* mapped to <code>resource</code>
* @param resource The resouce where the pkg's definition will be mapped. If
* <code>autoDiscoverResource</code> is true, this parameter should be null;
* it will not be used.
* @param autoDiscoverResource if set to true, the resource to do the mapping
* will be taken from each definition. If that is the case, the parameter
* <code>resource</code> is not used.
*/
private void buildResourceMapping(Package pkg,
Resource resource,
boolean autoDiscoverResource) {
synchronized ( this.registeredResources ) {
if ( !autoDiscoverResource && resource == null ) {
this.listener.warning( "KnowledgeAgent: Impossible to map to a null resource! Use autoDiscoverResource = true " );
return;
}
if ( autoDiscoverResource && resource != null ) {
this.listener.warning( "KnowledgeAgent: building resource map with resource set and autoDiscoverResource=true. Resource value wil be overwritten!" );
}
for ( Rule rule : pkg.getRules() ) {
if ( resource != null && !((InternalResource) resource).hasURL() ) {
this.listener.debug( "KnowledgeAgent no resource mapped for rule="
+ rule );
}
if ( autoDiscoverResource ) {
resource = rule.getResource();
}
this.addDefinitionMapping( resource,
rule,
true );
}
for ( Process process : pkg.getRuleFlows().values() ) {
if ( resource != null && !((InternalResource) resource).hasURL() ) {
this.listener.debug( "KnowledgeAgent no resource mapped for process="
+ process );
}
if ( autoDiscoverResource ) {
resource = ((ResourcedObject) process).getResource();
}
this.addDefinitionMapping( resource,
process,
true );
}
for ( TypeDeclaration typeDeclaration : pkg.getTypeDeclarations().values() ) {
if ( resource != null && !((InternalResource) resource).hasURL() ) {
this.listener.debug( "KnowledgeAgent no resource mapped for type="
+ typeDeclaration );
}
if ( autoDiscoverResource ) {
resource = typeDeclaration.getResource();
}
this.addDefinitionMapping( resource,
typeDeclaration,
true );
}
for ( Function function : pkg.getFunctions().values() ) {
if ( resource != null && !((InternalResource) resource).hasURL() ) {
this.listener.debug( "KnowledgeAgent no resource mapped for function="
+ function );
}
if ( autoDiscoverResource ) {
resource = function.getResource();
}
this.addDefinitionMapping( resource,
function,
true );
}
}
}
/**
* This indexes the rules, flows, type declarations, etc against their
* respective URLs if they have any, to allow more fine grained removal and
* not just removing of an entire package
*/
public void autoBuildResourceMapping() {
this.listener.debug( "KnowledgeAgent building resource map" );
synchronized ( this.registeredResources ) {
RuleBase rbase = ((KnowledgeBaseImpl) this.kbase).ruleBase;
for ( Package pkg : rbase.getPackages() ) {
this.buildResourceMapping( pkg,
null,
true );
}
}
}
public KnowledgeBase getKnowledgeBase() {
synchronized ( this.registeredResources ) {
return this.kbase;
}
}
public StatelessKnowledgeSession newStatelessKnowledgeSession() {
return new StatelessKnowledgeSessionImpl( null,
this,
null );
}
public StatelessKnowledgeSession newStatelessKnowledgeSession(
KnowledgeSessionConfiguration conf) {
return new StatelessKnowledgeSessionImpl( null,
this,
conf );
}
public void resourcesChanged(ChangeSet changeSet) {
try {
this.listener.debug( "KnowledgeAgent received ChangeSet changed notification" );
this.queue.put( changeSet );
} catch ( InterruptedException e ) {
this.listener.exception( new RuntimeException(
"KnowledgeAgent error while adding ChangeSet notification to queue",
e ) );
}
}
/**
* Rebuilds and creates a new KnowledgeBase for this KnowledgeAgent when
* called based on the ChangeSet that comes in and if newInstance is set to
* true. If incremental building or KnowledgeBase updates is on, then this
* will attempt to update the KnowledgeBase instead.
*
* @param changeSetState
* The state that the ChangeSet performed
*/
public void buildKnowledgeBase(ChangeSetState changeSetState) {
this.listener.debug( "KnowledgeAgent rebuilding KnowledgeBase using ChangeSet" );
synchronized ( this.registeredResources ) {
/*
* Do the following only if we are building a new instance,
* otherwise, do an incremental build/update
*/
if ( this.newInstance ) {
rebuildResources( changeSetState );
} else {
incrementalBuildResources( changeSetState );
}
/*
* If the ruleBase is sequential, after rebuilding or incremental
* update, do an ordering of the ReteooBuilder
*/
// FIXME: this same code exists in ReteooRuleBase#newStatelessSession()
InternalRuleBase ruleBase = (InternalRuleBase) ((KnowledgeBaseImpl) this.kbase).ruleBase;
ruleBase.lock(); // XXX: readlock might be enough, no idea what order() does.
try {
if ( ruleBase.getConfiguration().isSequential() ) {
ruleBase.getReteooBuilder().order();
}
} finally {
ruleBase.unlock();
}
}
this.eventSupport.fireKnowledgeBaseUpdated( this.kbase );
this.listener.debug( "KnowledgeAgent finished rebuilding KnowledgeBase using ChangeSet" );
}
/**
* Same as {@link #createPackageFromResource(org.drools.io.Resource, org.drools.builder.KnowledgeBuilder)
* createPackageFromResource(org.drools.io.Resource, null)}
*
* @param resource
* @return
* @see #createPackageFromResource(org.drools.io.Resource, org.drools.builder.KnowledgeBuilder)
*/
private KnowledgePackageImp createPackageFromResource(Resource resource) {
return this.createPackageFromResource( resource,
null );
}
/**
* Compiles the resource and returns the created package using the passed
* kbuilder. If kbuilder is null, a new instance of a Builder is used.
* Kbuilder is not used for resources that already are packages.
* @param resource the resource to compile.
* @param kbuilder the builder used to compile the resource. If the resource
* is already a package, this builder is not used.
* @return the package resulting of the compilation of resource.
*/
private KnowledgePackageImp createPackageFromResource(Resource resource,
KnowledgeBuilder kbuilder) {
if ( ((InternalResource) resource).getResourceType() != ResourceType.PKG ) {
if ( kbuilder == null ) {
kbuilder = this.createKBuilder();
}
kbuilder.add( resource,
((InternalResource) resource).getResourceType() );
if ( kbuilder.hasErrors() ) {
this.eventSupport.fireResourceCompilationFailed( kbuilder,
resource,
((InternalResource) resource).getResourceType() );
this.listener.warning(
"KnowledgeAgent has KnowledgeBuilder errors ",
kbuilder.getErrors() );
}
if ( kbuilder.getKnowledgePackages().iterator().hasNext() ) {
return (KnowledgePackageImp) kbuilder.getKnowledgePackages().iterator().next();
}
return null;
} else {
// .pks are handled as a special case.
InputStream is = null;
KnowledgePackageImp kpkg = null;
try {
is = resource.getInputStream();
Object object = DroolsStreamUtils.streamIn( is );
if ( object instanceof KnowledgePackageImp ) {
kpkg = ((KnowledgePackageImp) object);
} else {
kpkg = new KnowledgePackageImp( (Package) object );
}
for ( Rule rule : kpkg.pkg.getRules() ) {
rule.setResource( resource );
}
} catch ( Exception ex ) {
this.listener.exception( new RuntimeException( "KnowledgeAgent exception while trying to deserialize KnowledgeDefinitionsPackage ",
ex ) );
} finally {
try {
if ( is != null ) {
is.close();
}
} catch ( IOException e ) {
this.listener.exception( new RuntimeException( "KnowledgeAgent exception while trying to close KnowledgeDefinitionsPackage ",
e ) );
}
}
return kpkg;
}
}
/**
* This method is meant to rebuild the entire KnowledgeBase. Cached
* references outside of this Agent will no longer be valid to the current
* KnowledgeBase
*
* @param changeSetState
* The ChangeSetState
*/
private void rebuildResources(ChangeSetState changeSetState) {
if ( !this.newInstance ) {
listener.warning( "KnowledgeAgent rebuilding KnowledgeBase when newInstance is false" );
}
/*
* Rebuild a new knowledge base. Try to use the old configuration if
* possible
*/
if ( this.kbase != null ) {
this.kbase = KnowledgeBaseFactory.newKnowledgeBase( ((InternalRuleBase) ((KnowledgeBaseImpl) this.kbase).ruleBase).getConfiguration() );
} else {
this.kbase = KnowledgeBaseFactory.newKnowledgeBase();
}
//puts all the resources as added in the changeSet.
changeSetState.addedResources.clear();
changeSetState.addedResources.addAll( this.registeredResources.getAllResources() );
addResourcesToKnowledgeBase( changeSetState );
this.listener.info( "KnowledgeAgent new KnowledgeBase now built and in use" );
}
/**
* Processes {@link ChangeSetState#removedResourceMappings},
* {@link ChangeSetState#addedResources} and
* {@link ChangeSetState#modifiedResourceMappings} of <code>changeSetState</code>
* and apply them to {@link #kbase}.
* The way the lists are processed is:
* <ol>
* <li>
* Each element of {@link ChangeSetState#removedResourceMappings} is removed
* from {@link #kbase} using {@link #removeKnowledgeDefinitionFromBase(org.drools.definition.KnowledgeDefinition) }.
* </li>
* <li>
* Each element of {@link ChangeSetState#modifiedResourceMappings} is compiled
* using {@link #createPackageFromResource(org.drools.io.Resource) } and
* diffed against the previous version of the resource. The diff dictates
* wich definitions should be removed and what should be updated. The
* ones that should be removed are deleted using
* {@link #removeKnowledgeDefinitionFromBase(org.drools.definition.KnowledgeDefinition) },
* the ones that should be update/added are put into
* {@link ChangeSetState#createdPackages} of <code>changeSetState</code>.
* </li>
* <li>
* Each element of {@link ChangeSetState#addedResources} is compiled
* using {@link #createPackageFromResource(org.drools.io.Resource) }
* and added to {@link ChangeSetState#createdPackages} of
* <code>changeSetState</code>.
* </li>
* </ol>
* Because the elements of {@link ChangeSetState#addedResources} and
* {@link ChangeSetState#modifiedResourceMappings} were already processed and
* added as elements of {@link ChangeSetState#createdPackages}, these two lists
* are emtpied.
* The <code>changeSetState</code> is then passed to
* {@link #addResourcesToKnowledgeBase(org.drools.agent.impl.KnowledgeAgentImpl.ChangeSetState) }
* in order to process {@link ChangeSetState#createdPackages}.
* @param changeSetState the ChangeSetState
*/
private void incrementalBuildResources(ChangeSetState changeSetState) {
if ( this.newInstance ) {
this.listener.warning( "KnowledgeAgent incremental build of KnowledgeBase when newInstance is true" );
}
// Incrementally rebuild the resources
synchronized ( this.registeredResources ) {
this.listener.info( "KnowledgeAgent performing an incremental build of the ChangeSet" );
// Create the knowledge base if one does not exist
if ( this.kbase == null ) {
this.kbase = KnowledgeBaseFactory.newKnowledgeBase();
}
// Remove all rules from the resources removed and also those
// modified
for ( Map.Entry<Resource, Set<KnowledgeDefinition>> entry : changeSetState.removedResourceMappings.entrySet() ) {
for ( KnowledgeDefinition kd : entry.getValue() ) {
removeKnowledgeDefinitionFromBase( kd );
}
}
for ( Map.Entry<Resource, Set<KnowledgeDefinition>> entry : changeSetState.modifiedResourceMappings.entrySet() ) {
KnowledgePackageImp kpkg = createPackageFromResource( entry.getKey() );
if ( kpkg == null ) {
this.listener.warning( "KnowledgeAgent: The resource didn't create any package: " + entry.getKey() );
continue;
}
this.listener.debug( "KnowledgeAgent: Diffing: " + entry.getKey() );
ResourceDiffProducer rdp = new BinaryResourceDiffProducerImpl();
//we suppose that the package definition didn't change in the resource.
//That's why we are serching the current package as
//this.kbase.getKnowledgePackage(kpkg.getName())
ResourceDiffResult diff = rdp.diff( entry.getValue(),
kpkg,
(KnowledgePackageImp) this.kbase.getKnowledgePackage( kpkg.getName() ) );
for ( KnowledgeDefinition kd : diff.getRemovedDefinitions() ) {
this.listener.debug( "KnowledgeAgent: Removing: " + kd );
removeKnowledgeDefinitionFromBase( kd );
}
//because all the mappings for "resource" were removed, we
//need to map again the definitions that didn't change.
//Those modified or added will be mapped in addResourcesToKnowledgeBase()
for ( KnowledgeDefinition knowledgeDefinition : diff.getUnmodifiedDefinitions() ) {
this.addDefinitionMapping( entry.getKey(),
knowledgeDefinition,
false );
}
changeSetState.createdPackages.put( entry.getKey(),
diff.getPkg() );
}
/*
* Compile the newly added resources
*/
for ( Resource resource : changeSetState.addedResources ) {
///compile the new resource
KnowledgePackageImp kpkg = createPackageFromResource( resource );
if ( kpkg == null ) {
this.listener.warning( "KnowledgeAgent: The resource didn't create any package: " + resource );
continue;
}
changeSetState.createdPackages.put( resource,
kpkg );
}
//the added and modified resources were already processed and
//converted to createdPackages. We must clear the lists.
changeSetState.addedResources.clear();
changeSetState.modifiedResourceMappings.clear();
addResourcesToKnowledgeBase( changeSetState );
}
this.listener.info( "KnowledgeAgent incremental build of KnowledgeBase finished and in use" );
}
/**
* Removes a definition from {@link #kbase}.
* @param kd the definition to be removed.
*/
private void removeKnowledgeDefinitionFromBase(KnowledgeDefinition kd) {
try {
if ( kd instanceof Query ) {
Query query = (Query) kd;
this.listener.debug( "KnowledgeAgent removing Query=" + query
+ " from package=" + query.getPackageName() );
this.kbase.removeQuery( query.getPackageName(),
query.getName() );
} else if ( kd instanceof Rule ) {
Rule rule = (Rule) kd;
this.listener.debug( "KnowledgeAgent removing Rule=" + rule
+ " from package=" + rule.getPackageName() );
this.kbase.removeRule( rule.getPackageName(),
rule.getName() );
} else if ( kd instanceof Process ) {
Process process = (Process) kd;
this.listener.debug( "KnowledgeAgent removing Process=" + process );
this.kbase.removeProcess( process.getId() );
} else if ( kd instanceof TypeDeclaration ) {
// @TODO Handle Type Declarations... is there a way to remove this?
} else if ( kd instanceof Function ) {
Function function = (Function) kd;
this.kbase.removeFunction( function.getNamespace(),
function.getName() );
}
} catch ( IllegalArgumentException e ) {
//it could be possible that a definition does not longer exists
//in the kbase.
this.listener.warning( e.getMessage() );
}
}
/**
* Adds the resources to the current KnowledgeBase on this KnowledgeAgent.
* This method processes {@link ChangeSetState#addedResources} and
* {@link ChangeSetState#createdPackages} lists in two different ways:
* <ul>
* <li>
* The elments of {@link ChangeSetState#addedResources} are compiled using
* {@link #createPackageFromResource(org.drools.io.Resource, org.drools.builder.KnowledgeBuilder)}
* and added to {@link ChangeSetState#createdPackages}. The same kbuilder
* is used for all the elements.
* </li>
* <li>
* The elments of {@link ChangeSetState#createdPackages} are added to
* {@link #kbase}. Each package is mapped to the original resource
* using {@link #buildResourceMapping(org.drools.rule.Package, org.drools.io.Resource)}.
* </li>
* </ul>
*
*
* @param changeSetState the object containing the added resources list and
* created pacages list.
*/
private void addResourcesToKnowledgeBase(ChangeSetState changeSetState) {
KnowledgeBuilder kbuilder = this.createKBuilder();
List<Package> packages = new ArrayList<Package>();
for ( Resource resource : changeSetState.addedResources ) {
KnowledgePackageImp createdPackage = this.createPackageFromResource( resource,
kbuilder );
changeSetState.createdPackages.put( resource,
createdPackage );
}
//createPackageFromResource already log this
// if (kbuilder.hasErrors()) {
// this.listener.warning(
// "KnowledgeAgent has KnowledgeBuilder errors ", kbuilder.getErrors());
// }
for ( Map.Entry<Resource, KnowledgePackage> entry : changeSetState.createdPackages.entrySet() ) {
// For PKG (.pks) just add them
Resource resource = entry.getKey();
this.listener.debug( "KnowledgeAgent obtaining pkg resource="
+ resource );
try {
Package pkg = ((KnowledgePackageImp) entry.getValue()).pkg;
for ( Rule rule : pkg.getRules() ) {
rule.setResource( resource );
}
packages.add( pkg );
this.buildResourceMapping( pkg,
resource );
} catch ( Exception e ) {
this.listener.exception( new RuntimeException(
"KnowledgeAgent exception while trying to deserialize KnowledgeDefinitionsPackage ",
e ) );
}
}
// if (kbuilder
// != null) {
// // Log any errors we come across
// if (kbuilder.hasErrors()) {
// this.listener.warning(
// "KnowledgeAgent has KnowledgeBuilder errors ", kbuilder.getErrors());
// }
// this.listener.debug("KnowledgeAgent adding KnowledgePackages from KnowledgeBuilder");
// this.kbase.addKnowledgePackages(kbuilder.getKnowledgePackages());
// }
/*
* Add all the packages we found, but did not build, from the resources
* now
*/
for ( Package pkg : packages ) {
this.listener.debug( "KnowledgeAgent adding KnowledgeDefinitionsPackage "
+ pkg.getName() );
((KnowledgeBaseImpl) this.kbase).ruleBase.addPackage( pkg );
}
}
/*
* (non-Javadoc)
*
* @see org.drools.agent.KnowledgeAgent#getName()
*/
public String getName() {
return this.name;
}
/**
* Kicks off the monitoring service for handling ResourceChangeEvents on a
* separate process.
*
* @boolean monitor True if monitoring should take place, false otherwise
*/
public void monitorResourceChangeEvents(boolean monitor) {
Set<Resource> allResources = new HashSet<Resource>();
allResources.addAll( this.resourceDirectories );
allResources.addAll( this.registeredResources.getAllResources() );
allResources.addAll( this.dslResources.keySet() );
//subscribe/unsubscribe from resources
for ( Resource resource : allResources ) {
if ( monitor ) {
this.listener.debug( "KnowledgeAgent subscribing from resource="
+ resource );
this.notifier.subscribeResourceChangeListener( this,
resource );
} else {
this.listener.debug( "KnowledgeAgent unsubscribing from resource="
+ resource );
this.notifier.unsubscribeResourceChangeListener( this,
resource );
}
}
if ( !monitor && this.changeSetNotificationDetector != null ) {
// we are running, but it wants to stop
// this will stop the thread
this.changeSetNotificationDetector.stop();
this.thread.interrupt();
this.changeSetNotificationDetector = null;
} else if ( monitor && this.changeSetNotificationDetector == null ) {
this.changeSetNotificationDetector = new ChangeSetNotificationDetector(
this,
this.queue,
this.listener );
this.thread = new Thread( this.changeSetNotificationDetector );
this.thread.start();
}
}
/**
*
* @param resource
* @param notify
* @return
*/
public boolean addResourceMapping(Resource resource,
boolean notify) {
boolean newMapping = this.registeredResources.createNewResourceEntry( resource );
if ( notify && newMapping ) {
this.listener.debug( "KnowledgeAgent notifier subscribing to resource="
+ resource );
this.notifier.subscribeResourceChangeListener( this,
resource );
return true;
}
return false;
}
/**
* Add an resource/definition entry to registeredResources. Optionaly it
* adds a listener to the resource added.
* @param resource
* @param definition
* @param notify
* @return if the resource/definition didn't exist in registeredResources.
*/
public boolean addDefinitionMapping(Resource resource,
KnowledgeDefinition definition,
boolean notify) {
if ( resource == null ) {
listener.warning( "KnowledgeAgent: impossible to add a map for a null resource! skiping." );
return false;
}
this.listener.debug( "KnowledgeAgent mapping resource="
+ resource + " to KnowledgeDefinition=" + definition );
boolean isNewResource = this.registeredResources.isResourceMapped( resource );
boolean isNewDefinition = true;
if ( definition != null ) {
isNewDefinition = this.registeredResources.putDefinition( resource,
definition );
}
if ( notify && isNewResource ) {
this.listener.debug( "KnowledgeAgent notifier subscribing to resource="
+ resource );
this.notifier.subscribeResourceChangeListener( this,
resource );
}
return isNewDefinition;
}
public Set<KnowledgeDefinition> removeResourceMapping(Resource resource,
boolean unsubscribe) {
this.listener.debug( "KnowledgeAgent removing mappings for resource="
+ resource + " with unsubscribe=" + unsubscribe );
Set<KnowledgeDefinition> definitions = this.registeredResources.removeDefinitions( resource );
if ( definitions != null ) {
if ( unsubscribe ) {
this.listener.debug( "KnowledgeAgent notifier unsubscribing to resource="
+ resource );
this.notifier.unsubscribeResourceChangeListener(
this,
resource );
}
}
return definitions;
}
/**
* A class to monitor and handle ChangeSets fired by the
* ResourceChangeNotifier on a separate service (or process).
*
* @author Mark Proctor
*/
public static class ChangeSetNotificationDetector
implements
Runnable {
private LinkedBlockingQueue<ChangeSet> queue;
private volatile boolean monitor;
private KnowledgeAgentImpl kagent;
private SystemEventListener listener;
public ChangeSetNotificationDetector(KnowledgeAgentImpl kagent,
LinkedBlockingQueue<ChangeSet> queue,
SystemEventListener listener) {
this.queue = queue;
this.kagent = kagent;
this.listener = listener;
this.monitor = true;
}
public void stop() {
this.monitor = false;
}
public void run() {
if ( this.monitor ) {
this.listener.info( "KnowledegAgent has started listening for ChangeSet notifications" );
}
while ( this.monitor ) {
Exception exception = null;
try {
kagent.applyChangeSet( this.queue.take() );
} catch ( InterruptedException e ) {
exception = e;
}
Thread.yield();
if ( this.monitor && exception != null ) {
this.listener.exception( new RuntimeException(
"KnowledgeAgent ChangeSet notification thread has been interrupted, but shutdown was not scheduled",
exception ) );
}
}
this.listener.info( "KnowledegAgent has stopped listening for ChangeSet notifications" );
}
}
private static class RegisteredResourceMap {
private Map<Resource, Set<KnowledgeDefinition>> map = new HashMap<Resource, Set<KnowledgeDefinition>>();
/**
* Creates a new entry for resource with an empty Set<KnowledgeDefinition>.
* If the map already contains an entry for the resource, then nothing
* is changed.
* @param resource
* @return true if the resource was not previously mapped.
*/
public boolean createNewResourceEntry(Resource resource) {
if ( !this.isResourceMapped( resource ) ) {
this.map.put( resource,
new HashSet<KnowledgeDefinition>() );
return true;
}
return false;
}
public boolean putDefinition(Resource resource,
KnowledgeDefinition definition) {
Set<KnowledgeDefinition> defList = map.get( resource );
if ( defList == null ) {
defList = new HashSet<KnowledgeDefinition>();
this.map.put( resource,
defList );
}
//support for lazy loading
if ( definition != null ) {
boolean isNew = defList.add( definition );
return isNew;
}
return false;
}
public Set<KnowledgeDefinition> removeDefinitions(Resource resource) {
return this.map.remove( resource );
}
public Set<KnowledgeDefinition> getDefinitions(Resource resource) {
return this.getDefinitions( resource,
false );
}
public Set<KnowledgeDefinition> getDefinitions(Resource resource,
boolean returnEmptyIfNull) {
Set<KnowledgeDefinition> definitions = this.map.get( resource );
if ( returnEmptyIfNull && definitions == null ) {
definitions = new HashSet<KnowledgeDefinition>();
}
return definitions;
}
public boolean isResourceMapped(Resource resource) {
return this.map.containsKey( resource );
}
public Set<Resource> getAllResources() {
return this.map.keySet();
}
public boolean onlyHasPKGResources() {
for ( Resource resource : map.keySet() ) {
if ( ((InternalResource) resource).getResourceType() != ResourceType.PKG ) {
return false;
}
}
return true;
}
}
/**
* Creates a kbuilder if necessary. The built kbuilder will contain all the
* DSL resources the agent is managing. It will also apply any
* KnowledgeBuilderConfiguration present in {@link #builderConfiguration} or
* will copy any KnowledgeBuilderConfiguration from the current kbase if
* {@link #useKBaseClassLoaderForCompiling} is true.
* This method will return null if the agent is only managing binary resources.
* This avoids drools-compiler dependency.
*
* @return a new kbuilder or null if all the managed resources are binaries.
*/
private KnowledgeBuilder createKBuilder() {
if ( this.registeredResources.onlyHasPKGResources() ) {
return null;
}
KnowledgeBuilder kbuilder = null;
if ( this.builderConfiguration != null ) {
kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder( this.builderConfiguration );
} else if ( this.useKBaseClassLoaderForCompiling ) {
kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder( KnowledgeBuilderFactory.newKnowledgeBuilderConfiguration( null,
((ReteooRuleBase) ((KnowledgeBaseImpl) this.getKnowledgeBase()).getRuleBase()).getRootClassLoader() ) );
} else {
kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder();
}
if ( this.dslResources != null ) {
for ( Map.Entry<Resource, String> entry : this.dslResources.entrySet() ) {
kbuilder.add( ResourceFactory.newByteArrayResource( entry.getValue().getBytes() ),
ResourceType.DSL );
}
}
return kbuilder;
}
private void retrieveDSLResource(Resource resource) throws IOException {
BufferedReader bufferedReader = new BufferedReader( resource.getReader() );
String line = null;
StringBuilder content = new StringBuilder();
while ( (line = bufferedReader.readLine()) != null ) {
content.append( line );
content.append( "\n" );
}
this.dslResources.put( resource,
content.toString() );
}
public void addEventListener(KnowledgeAgentEventListener listener) {
this.eventSupport.addEventListener( listener );
}
public void removeEventListener(KnowledgeAgentEventListener listener) {
this.eventSupport.removeEventListener( listener );
}
public void dispose() {
synchronized ( this.registeredResources ) {
//all kbase's ksessions must be disposed
if ( this.kbase != null ) {
Collection<StatefulKnowledgeSession> statefulSessions = this.kbase.getStatefulKnowledgeSessions();
if ( statefulSessions != null && statefulSessions.size() > 0 ) {
String message = "The kbase still contains " + statefulSessions.size() + " stateful sessions. You must dispose them first.";
this.listener.warning( message );
throw new IllegalStateException( message );
}
}
//stop changeSet Notification Detector
this.monitorResourceChangeEvents( false );
}
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#finalize()
*/
@Override
protected void finalize() throws Throwable {
// users should turn off monitoring, but just in case when this class is
// GC'd we turn off the thread
if ( this.changeSetNotificationDetector != null ) {
this.changeSetNotificationDetector.monitor = false;
}
}
}