/*
* Copyright 2014 Red Hat, Inc. and/or its affiliates.
*
* 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.jbpm.kie.services.impl;
import static org.kie.scanner.MavenRepository.getMavenRepository;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.EntityManagerFactory;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.commons.codec.binary.Base64;
import org.drools.compiler.kie.builder.impl.InternalKieModule;
import org.drools.compiler.kie.builder.impl.KieContainerImpl;
import org.drools.compiler.kproject.xml.DependencyFilter;
import org.drools.core.common.ProjectClassLoader;
import org.drools.core.marshalling.impl.ClassObjectMarshallingStrategyAcceptor;
import org.drools.core.marshalling.impl.SerializablePlaceholderResolverStrategy;
import org.drools.core.util.StringUtils;
import org.jbpm.kie.services.impl.bpmn2.ProcessDescriptor;
import org.jbpm.kie.services.impl.model.ProcessAssetDesc;
import org.jbpm.process.audit.event.AuditEventBuilder;
import org.jbpm.runtime.manager.impl.KModuleRegisterableItemsFactory;
import org.jbpm.runtime.manager.impl.deploy.DeploymentDescriptorImpl;
import org.jbpm.runtime.manager.impl.deploy.DeploymentDescriptorManager;
import org.jbpm.runtime.manager.impl.deploy.DeploymentDescriptorMerger;
import org.jbpm.runtime.manager.impl.jpa.EntityManagerFactoryManager;
import org.jbpm.services.api.DefinitionService;
import org.jbpm.services.api.model.DeployedAsset;
import org.jbpm.services.api.model.DeployedUnit;
import org.jbpm.services.api.model.DeploymentUnit;
import org.kie.api.KieBase;
import org.kie.api.KieServices;
import org.kie.api.builder.ReleaseId;
import org.kie.api.builder.model.KieBaseModel;
import org.kie.api.executor.ExecutorService;
import org.kie.api.marshalling.ObjectMarshallingStrategy;
import org.kie.api.remote.Remotable;
import org.kie.api.runtime.EnvironmentName;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.manager.RegisterableItemsFactory;
import org.kie.api.runtime.manager.RuntimeEnvironmentBuilder;
import org.kie.internal.runtime.conf.DeploymentDescriptor;
import org.kie.internal.runtime.conf.MergeMode;
import org.kie.internal.runtime.conf.NamedObjectModel;
import org.kie.internal.runtime.conf.ObjectModel;
import org.kie.internal.runtime.conf.ObjectModelResolver;
import org.kie.internal.runtime.conf.ObjectModelResolverProvider;
import org.kie.internal.runtime.conf.PersistenceMode;
import org.kie.internal.runtime.manager.InternalRuntimeManager;
import org.kie.scanner.MavenRepository;
import org.reflections.Reflections;
import org.reflections.util.ConfigurationBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource;
public class KModuleDeploymentService extends AbstractDeploymentService {
private static Logger logger = LoggerFactory.getLogger(KModuleDeploymentService.class);
private static final String DEFAULT_KBASE_NAME = "defaultKieBase";
private static final String PROCESS_ID_XPATH = "/*[local-name() = 'definitions']/*[local-name() = 'process']/@id";
private DefinitionService bpmn2Service;
private DeploymentDescriptorMerger merger = new DeploymentDescriptorMerger();
private FormManagerService formManagerService;
private ExecutorService executorService;
private XPathExpression processIdXPathExpression;
public KModuleDeploymentService() {
try {
processIdXPathExpression = XPathFactory.newInstance().newXPath().compile(PROCESS_ID_XPATH);
} catch (XPathExpressionException e) {
logger.error("Unable to parse '{}' XPath expression due to {}", PROCESS_ID_XPATH, e.getMessage());
}
}
public void onInit() {
EntityManagerFactoryManager.get().addEntityManagerFactory("org.jbpm.domain", getEmf());
}
@Override
public void deploy(DeploymentUnit unit) {
try {
super.deploy(unit);
if (!(unit instanceof KModuleDeploymentUnit)) {
throw new IllegalArgumentException("Invalid deployment unit provided - " + unit.getClass().getName());
}
KModuleDeploymentUnit kmoduleUnit = (KModuleDeploymentUnit) unit;
DeployedUnitImpl deployedUnit = new DeployedUnitImpl(unit);
// Create the release id
KieContainer kieContainer = kmoduleUnit.getKieContainer();
ReleaseId releaseId = null;
if (kieContainer == null) {
KieServices ks = KieServices.Factory.get();
releaseId = ks.newReleaseId(kmoduleUnit.getGroupId(), kmoduleUnit.getArtifactId(), kmoduleUnit.getVersion());
MavenRepository repository = getMavenRepository();
repository.resolveArtifact(releaseId.toExternalForm());
kieContainer = ks.newKieContainer(releaseId);
kmoduleUnit.setKieContainer(kieContainer);
}
releaseId = kieContainer.getReleaseId();
// retrieve the kbase name
String kbaseName = kmoduleUnit.getKbaseName();
if (StringUtils.isEmpty(kbaseName)) {
KieBaseModel defaultKBaseModel = ((KieContainerImpl)kieContainer).getKieProject().getDefaultKieBaseModel();
if (defaultKBaseModel != null) {
kbaseName = defaultKBaseModel.getName();
} else {
kbaseName = DEFAULT_KBASE_NAME;
}
}
InternalKieModule module = (InternalKieModule) ((KieContainerImpl)kieContainer).getKieModuleForKBase(kbaseName);
if (module == null) {
throw new IllegalStateException("Cannot find kbase, either it does not exist or there are multiple default kbases in kmodule.xml");
}
KieBase kbase = kieContainer.getKieBase(kbaseName);
Map<String, ProcessDescriptor> processDescriptors = new HashMap<String, ProcessDescriptor>();
for (org.kie.api.definition.process.Process process : kbase.getProcesses()) {
processDescriptors.put(process.getId(), (ProcessDescriptor) process.getMetaData().get("ProcessDescriptor"));
}
// TODO: add forms data?
Collection<String> files = module.getFileNames();
processResources(module, files, kieContainer, kmoduleUnit, deployedUnit, releaseId, processDescriptors);
// process the files in the deployment
if (module.getKieDependencies() != null) {
Collection<InternalKieModule> dependencies = module.getKieDependencies().values();
for (InternalKieModule depModule : dependencies) {
logger.debug("Processing dependency module " + depModule.getReleaseId());
files = depModule.getFileNames();
processResources(depModule, files, kieContainer, kmoduleUnit, deployedUnit, depModule.getReleaseId(), processDescriptors);
}
}
Collection<ReleaseId> dependencies = module.getJarDependencies(new DependencyFilter.ExcludeScopeFilter("test", "provided"));
// process deployment dependencies
if (dependencies != null && !dependencies.isEmpty()) {
// Classes 2: classes added from project and dependencies added
processClassloader(kieContainer, deployedUnit);
}
AuditEventBuilder auditLoggerBuilder = setupAuditLogger(identityProvider, unit.getIdentifier());
RuntimeEnvironmentBuilder builder = boostrapRuntimeEnvironmentBuilder(
kmoduleUnit, deployedUnit, kieContainer, kmoduleUnit.getMergeMode())
.knowledgeBase(kbase)
.classLoader(kieContainer.getClassLoader());
builder.registerableItemsFactory(getRegisterableItemsFactory(auditLoggerBuilder, kieContainer, kmoduleUnit));
commonDeploy(unit, deployedUnit, builder.get(), kieContainer);
kmoduleUnit.setDeployed(true);
} catch (Throwable e) {
logger.warn("Unexpected error while deploying unit {}", unit.getIdentifier(), e);
// catch all possible errors to be able to report them to caller as RuntimeException
throw new RuntimeException(e);
}
}
protected RegisterableItemsFactory getRegisterableItemsFactory(AuditEventBuilder auditLoggerBuilder,
KieContainer kieContainer,KModuleDeploymentUnit unit) {
KModuleRegisterableItemsFactory factory = new KModuleRegisterableItemsFactory(kieContainer, unit.getKsessionName());
factory.setAuditBuilder(auditLoggerBuilder);
return factory;
}
@Override
public void undeploy(DeploymentUnit unit) {
if (!(unit instanceof KModuleDeploymentUnit)) {
throw new IllegalArgumentException("Invalid deployment unit provided - " + unit.getClass().getName());
}
KModuleDeploymentUnit kmoduleUnit = (KModuleDeploymentUnit) unit;
super.undeploy(unit);
formManagerService.unRegisterForms( unit.getIdentifier() );
KieServices ks = KieServices.Factory.get();
ReleaseId releaseId = ks.newReleaseId(kmoduleUnit.getGroupId(), kmoduleUnit.getArtifactId(), kmoduleUnit.getVersion());
ks.getRepository().removeKieModule(releaseId);
}
/**
* This creates and fills a {@link RuntimeEnvironmentBuilder} instance, which is later used when creating services.
* </p>
* A lot of the logic here is used to process the information in the {@link DeploymentDescriptor} instance, which is
* part of the {@link DeploymentUnit}.
*
* @param deploymentUnit The {@link KModuleDeploymentUnit}, which is filled by the method
* @param deployedUnit The {@link DeployedUnit}, which is also filled by the method
* @param kieContainer The {@link KieContainer}, which contains information needed to fill the above two arguments
* @param mode The {@link MergeMode} used to resolve conflicts in the {@link DeploymentDescriptor}.
* @return A {@link RuntimeEnvironmentBuilder} instance ready for use
*/
protected RuntimeEnvironmentBuilder boostrapRuntimeEnvironmentBuilder(KModuleDeploymentUnit deploymentUnit,
DeployedUnit deployedUnit, KieContainer kieContainer, MergeMode mode) {
DeploymentDescriptor descriptor = deploymentUnit.getDeploymentDescriptor();
if (descriptor == null || ((DeploymentDescriptorImpl)descriptor).isEmpty()) { // skip empty descriptors as its default can override settings
DeploymentDescriptorManager descriptorManager = new DeploymentDescriptorManager("org.jbpm.domain");
List<DeploymentDescriptor> descriptorHierarchy = descriptorManager.getDeploymentDescriptorHierarchy(kieContainer);
descriptor = merger.merge(descriptorHierarchy, mode);
deploymentUnit.setDeploymentDescriptor(descriptor);
} else if (descriptor != null && !deploymentUnit.isDeployed()) {
DeploymentDescriptorManager descriptorManager = new DeploymentDescriptorManager("org.jbpm.domain");
List<DeploymentDescriptor> descriptorHierarchy = descriptorManager.getDeploymentDescriptorHierarchy(kieContainer);
descriptorHierarchy.add(0, descriptor);
descriptor = merger.merge(descriptorHierarchy, mode);
deploymentUnit.setDeploymentDescriptor(descriptor);
}
// first set on unit the strategy
deploymentUnit.setStrategy(descriptor.getRuntimeStrategy());
// setting up runtime environment via builder
RuntimeEnvironmentBuilder builder = null;
if (descriptor.getPersistenceMode() == PersistenceMode.NONE) {
builder = RuntimeEnvironmentBuilder.Factory.get().newDefaultInMemoryBuilder();
} else {
builder = RuntimeEnvironmentBuilder.Factory.get().newDefaultBuilder();
}
// populate various properties of the builder
EntityManagerFactory emf = EntityManagerFactoryManager.get().getOrCreate(descriptor.getPersistenceUnit());
builder.entityManagerFactory(emf);
Map<String, Object> contaxtParams = new HashMap<String, Object>();
contaxtParams.put("entityManagerFactory", emf);
contaxtParams.put("classLoader", kieContainer.getClassLoader());
// process object models that are globally configured (environment entries, session configuration)
for (NamedObjectModel model : descriptor.getEnvironmentEntries()) {
Object entry = getInstanceFromModel(model, kieContainer, contaxtParams);
builder.addEnvironmentEntry(model.getName(), entry);
}
for (NamedObjectModel model : descriptor.getConfiguration()) {
Object entry = getInstanceFromModel(model, kieContainer, contaxtParams);
builder.addConfiguration(model.getName(), (String) entry);
}
ObjectMarshallingStrategy[] mStrategies = new ObjectMarshallingStrategy[descriptor.getMarshallingStrategies().size() + 1];
int index = 0;
for (ObjectModel model : descriptor.getMarshallingStrategies()) {
Object strategy = getInstanceFromModel(model, kieContainer, contaxtParams);
mStrategies[index] = (ObjectMarshallingStrategy)strategy;
index++;
}
// lastly add the main default strategy
mStrategies[index] = new SerializablePlaceholderResolverStrategy(ClassObjectMarshallingStrategyAcceptor.DEFAULT);
builder.addEnvironmentEntry(EnvironmentName.OBJECT_MARSHALLING_STRATEGIES, mStrategies);
builder.addEnvironmentEntry("KieDeploymentDescriptor", descriptor);
builder.addEnvironmentEntry("KieContainer", kieContainer);
if (executorService != null) {
builder.addEnvironmentEntry("ExecutorService", executorService);
}
// populate all assets with roles for this deployment unit
List<String> requiredRoles = descriptor.getRequiredRoles(DeploymentDescriptor.TYPE_VIEW);
if (requiredRoles != null && !requiredRoles.isEmpty()) {
for (DeployedAsset desc : deployedUnit.getDeployedAssets()) {
if (desc instanceof ProcessAssetDesc) {
((ProcessAssetDesc) desc).setRoles(requiredRoles);
}
}
}
// Classes 3: classes added from descriptor
List<String> remoteableClasses = descriptor.getClasses();
if (remoteableClasses != null && !remoteableClasses.isEmpty()) {
for (String className : remoteableClasses) {
Class descriptorClass = null;
try {
descriptorClass = kieContainer.getClassLoader().loadClass(className);
logger.debug( "Loaded {} into the classpath from deployment descriptor {}", className, kieContainer.getReleaseId().toExternalForm());
} catch (ClassNotFoundException cnfe) {
throw new IllegalArgumentException("Class " + className + " not found in the project");
} catch (NoClassDefFoundError e) {
throw new IllegalArgumentException("Class " + className + " not found in the project");
}
addClassToDeployedUnit(descriptorClass, (DeployedUnitImpl) deployedUnit);
}
}
return builder;
}
protected Object getInstanceFromModel(ObjectModel model, KieContainer kieContainer, Map<String, Object> contaxtParams) {
ObjectModelResolver resolver = ObjectModelResolverProvider.get(model.getResolver());
if (resolver == null) {
// if we don't throw an exception here, we have an NPE below..
throw new IllegalStateException("Unable to find ObjectModelResolver for " + model.getResolver());
}
return resolver.getInstance(model, kieContainer.getClassLoader(), contaxtParams);
}
/**
* Goes through all files in a deployment, and processes them so that they are then ready
* for use after deployment.
*
* @param module The {@link InternalKieModule}, necessary to get form content
* @param files The {@link List} of file (names) to process.
* @param kieContainer The {@link KieContainer}, necesary in order to load classes
* @param deploymentUnit The {@link DeploymentUnit}, necessary to get the deployment id
* @param deployedUnit The {@link DeployedUnit}, which contains the results of actions here
*/
protected void processResources(InternalKieModule module, Collection<String> files,
KieContainer kieContainer, DeploymentUnit unit, DeployedUnitImpl deployedUnit, ReleaseId releaseId, Map<String, ProcessDescriptor> processes) {
for (String fileName : files) {
if(fileName.matches(".+bpmn[2]?$")) {
ProcessAssetDesc process;
try {
String processString = new String(module.getBytes(fileName), "UTF-8");
String processId = getProcessId(processString);
ProcessDescriptor processDesriptor = processes.get(processId);
if (processDesriptor != null) {
process = processDesriptor.getProcess();
if (process == null) {
throw new IllegalArgumentException("Unable to read process " + fileName);
}
process.setEncodedProcessSource(Base64.encodeBase64String(processString.getBytes()));
process.setDeploymentId(unit.getIdentifier());
deployedUnit.addAssetLocation(process.getId(), process);
bpmn2Service.addProcessDefinition(unit.getIdentifier(), processId, processDesriptor, kieContainer);
}
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Unsupported encoding while processing process " + fileName);
}
} else if (fileName.matches(".+ftl$") || fileName.matches(".+form$") || fileName.matches( ".+frm$" )) {
try {
String formContent = new String(module.getBytes(fileName), "UTF-8");
if (fileName.indexOf( "/" ) != -1) fileName = fileName.substring( fileName.lastIndexOf( "/" ) + 1);
formManagerService.registerForm(unit.getIdentifier(), fileName, formContent);
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Unsupported encoding while processing form " + fileName);
}
} else if( fileName.matches(".+class$")) {
// Classes 1: classes from deployment added
String className = fileName.replaceAll("/", ".");
className = className.substring(0, fileName.length() - ".class".length());
Class deploymentClass = null;
try {
deploymentClass = kieContainer.getClassLoader().loadClass(className);
} catch (ClassNotFoundException cnfe) {
throw new IllegalArgumentException("Class " + className + " not found in the project");
} catch (NoClassDefFoundError e) {
throw new IllegalArgumentException("Class " + className + " not found in the project");
}
addClassToDeployedUnit(deploymentClass, deployedUnit);
}
}
}
private void addClassToDeployedUnit(Class deploymentClass, DeployedUnitImpl deployedUnit) {
if( deploymentClass != null ) {
DeploymentUnit unit = deployedUnit.getDeploymentUnit();
Boolean limitClasses = false;
if( unit != null ) {
DeploymentDescriptor depDesc = ((KModuleDeploymentUnit) unit).getDeploymentDescriptor();
if( depDesc != null ) {
limitClasses = depDesc.getLimitSerializationClasses();
}
}
if( limitClasses != null && limitClasses ) {
filterClassesAddedToDeployedUnit(deployedUnit, deploymentClass);
} else {
logger.debug( "Loaded {} onto the classpath from deployment {}", deploymentClass.getName(), unit.getIdentifier());
deployedUnit.addClass(deploymentClass);
}
}
}
/**
* This processes the deployment dependencies, which are made available by the {@link KieContainer} {@link ClassLoader}.
*
* @param kieContainer The {@link KieContainer}, used to get the {@link ClassLoader}
* @param deployedUnit The {@link DeployedUnitImpl}, used to store the classes loaded
*/
protected void processClassloader(KieContainer kieContainer, DeployedUnitImpl deployedUnit) {
if (kieContainer.getClassLoader() instanceof ProjectClassLoader) {
ClassLoader parentCl = kieContainer.getClassLoader().getParent();
if (parentCl instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) parentCl).getURLs();
if (urls == null || urls.length == 0) {
return;
}
ConfigurationBuilder builder = new ConfigurationBuilder();
builder.addUrls(urls);
builder.addClassLoader(kieContainer.getClassLoader());
Reflections reflections = new Reflections(builder);
Set<Class<?>> xmlRootElemClasses = reflections.getTypesAnnotatedWith(XmlRootElement.class);
Set<Class<?>> xmlTypeClasses = reflections.getTypesAnnotatedWith(XmlType.class);
Set<Class<?>> remoteableClasses = reflections.getTypesAnnotatedWith(Remotable.class);
Set<Class<?>> allClasses = new HashSet<Class<?>>();
for( Set<Class<?>> classesToAdd : new Set[] { xmlRootElemClasses, xmlTypeClasses, remoteableClasses } ) {
if( classesToAdd != null ) {
allClasses.addAll(classesToAdd);
}
}
for (Class<?> clazz : allClasses) {
filterClassesAddedToDeployedUnit(deployedUnit, clazz);
}
}
}
}
/**
* This method is used to filter classes that are added to the {@link DeployedUnit}.
* </p>
* When this method is used, only classes that are meant to be used with serialization are
* added to the deployment. This feature can be used to, for example, make sure that non-serialization-compatible
* classes (such as interfaces), do not complicate the use of a deployment with the remote services (REST/JMS/WS).
* </p>
* Note to other developers, it's possible that classpath problems may arise, because
* of either classloader or lazy class resolution problems: I simply don't know enough about the
* inner workings of the JAXB implementations (plural!) to figure this out.
*
* @param deployedUnit The {@link DeployedUnit} to which the classes are added. The {@link DeployedUnit} to which the classes are added. The {@link DeployedUnit} to which the classes are added.
* @param classToAdd The class to add to the {@link DeployedUnit}.
*/
private static void filterClassesAddedToDeployedUnit( DeployedUnit deployedUnit, Class classToAdd) {
if( classToAdd.isInterface()
|| classToAdd.isAnnotation()
|| classToAdd.isLocalClass()
|| classToAdd.isMemberClass() ) {
return;
}
boolean jaxbClass = false;
boolean remoteableClass = false;
// @XmlRootElement and @XmlType may be used with inheritance
for( Annotation anno : classToAdd.getAnnotations() ) {
if( XmlRootElement.class.equals(anno.annotationType()) ) {
jaxbClass = true;
break;
}
if( XmlType.class.equals(anno.annotationType()) ) {
jaxbClass = true;
break;
}
}
// @Remotable is not inheritable, and may not be used as such
for( Annotation anno : classToAdd.getDeclaredAnnotations() ) {
if( Remotable.class.equals(anno.annotationType()) ) {
remoteableClass = true;
break;
}
}
if( jaxbClass || remoteableClass ) {
DeployedUnitImpl deployedUnitImpl = (DeployedUnitImpl) deployedUnit;
deployedUnitImpl.addClass(classToAdd);
}
}
public void setBpmn2Service(DefinitionService bpmn2Service) {
this.bpmn2Service = bpmn2Service;
}
public void setMerger(DeploymentDescriptorMerger merger) {
this.merger = merger;
}
public void setFormManagerService(FormManagerService formManagerService) {
this.formManagerService = formManagerService;
}
public void setExecutorService(ExecutorService executorService) {
this.executorService = executorService;
}
@Override
public void activate(String deploymentId) {
DeployedUnit deployed = getDeployedUnit(deploymentId);
if (deployed != null) {
((DeployedUnitImpl)deployed).setActive(true);
((InternalRuntimeManager)deployed.getRuntimeManager()).activate();
notifyOnActivate(deployed.getDeploymentUnit(), deployed);
}
}
@Override
public void deactivate(String deploymentId) {
DeployedUnit deployed = getDeployedUnit(deploymentId);
if (deployed != null) {
((DeployedUnitImpl)deployed).setActive(false);
((InternalRuntimeManager)deployed.getRuntimeManager()).deactivate();
notifyOnDeactivate(deployed.getDeploymentUnit(), deployed);
}
}
protected String getProcessId(String processSource) {
try {
InputSource inputSource = new InputSource(new StringReader(processSource));
String processId = (String) processIdXPathExpression.evaluate(inputSource, XPathConstants.STRING);
return processId;
} catch (XPathExpressionException e) {
logger.error("Unable to find process id from process source due to {}", e.getMessage());
return null;
}
}
}