package org.drools.agent.impl;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
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.builder.KnowledgeBuilder;
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.io.ResourceChangeListener;
import org.drools.impl.KnowledgeBaseImpl;
import org.drools.impl.StatelessKnowledgeSessionImpl;
import org.drools.io.Resource;
import org.drools.io.ResourceFactory;
import org.drools.io.impl.ClassPathResource;
import org.drools.io.impl.ResourceChangeNotifierImpl;
import org.drools.io.internal.InternalResource;
import org.drools.agent.ResourceDiffProducer;
import org.drools.rule.Function;
import org.drools.rule.Package;
import org.drools.rule.Rule;
import org.drools.rule.TypeDeclaration;
import org.drools.runtime.KnowledgeSessionConfiguration;
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 LinkedBlockingQueue<ChangeSet> queue;
private Thread thread;
private ChangeSetNotificationDetector changeSetNotificationDetector;
private SemanticModules semanticModules;
private final RegisteredResourceMap registeredResources = new RegisteredResourceMap();
/**
* Default constructor for KnowledgeAgentImpl
*
* @param name
* @param kbase
* @param configuration
*/
public KnowledgeAgentImpl(String name, KnowledgeBase kbase,
KnowledgeAgentConfiguration configuration) {
this.name = name;
this.kbase = kbase;
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.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 void applyChangeSet(Resource resource) {
applyChangeSet(getChangeSet(resource));
}
public void applyChangeSet(ChangeSet changeSet) {
synchronized (this.registeredResources) {
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();
}
}
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) {
/*
* Process the added resources from a ChangeSet by subscribing to
* the notifier and inserting a new ResourceMapping.
*/
for (Resource resource : changeSet.getResourcesAdded()) {
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);
}
}
}
/*
* 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()) {
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);
}
}
}
/*
* 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()) {
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);
}
}
}
}
}
}
/**
* 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 rule="
+ process);
}
if (autoDiscoverResource) {
resource = ((org.drools.process.core.Process) 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 rule="
+ 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 rule="
+ 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
*/
InternalRuleBase ruleBase = (InternalRuleBase) ((KnowledgeBaseImpl) this.kbase).ruleBase;
synchronized (ruleBase.getPackagesMap()) {
if (ruleBase.getConfiguration().isSequential()) {
ruleBase.getReteooBuilder().order();
}
}
}
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 (kbuilder == null){
kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder();
}
if (((InternalResource) resource).getResourceType() != ResourceType.PKG) {
kbuilder.add(resource, ((InternalResource) resource).getResourceType());
if (kbuilder.hasErrors()) {
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 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 = KnowledgeBuilderFactory.newKnowledgeBuilder();
List<Package> packages = new ArrayList<Package>();
for (Resource resource : changeSetState.addedResources) {
KnowledgePackageImp createdPackage = this.createPackageFromResource(resource, kbuilder);
changeSetState.createdPackages.put(resource, createdPackage);
}
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) {
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();
}
}
/*
* (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;
}
}
}