/*
* JBoss, Home of Professional Open Source.
* Copyright 2015, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.wildfly.extension.batch.jberet.deployment;
import java.io.IOException;
import java.io.InputStream;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.stream.Collectors;
import javax.xml.stream.XMLResolver;
import javax.xml.stream.XMLStreamException;
import org.jberet.job.model.Job;
import org.jberet.job.model.JobParser;
import org.jberet.spi.JobXmlResolver;
import org.jboss.as.ee.structure.DeploymentType;
import org.jboss.as.ee.structure.DeploymentTypeMarker;
import org.jboss.as.server.deployment.Attachments;
import org.jboss.as.server.deployment.DeploymentUnit;
import org.jboss.as.server.deployment.DeploymentUnitProcessingException;
import org.jboss.as.server.deployment.SubDeploymentMarker;
import org.jboss.as.server.deployment.module.ResourceRoot;
import org.jboss.modules.Module;
import org.jboss.vfs.VirtualFile;
import org.jboss.vfs.VirtualFileFilter;
import org.wildfly.extension.batch.jberet._private.BatchLogger;
import org.wildfly.security.manager.WildFlySecurityManager;
/**
* A {@linkplain JobXmlResolver job XML resolver} for WildFly. A deployments resolvers are loaded via a
* {@link ServiceLoader} and processed before XML found in the deployment itself.
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public class WildFlyJobXmlResolver implements JobXmlResolver {
private final Set<JobXmlResolver> jobXmlResolvers;
private final Map<String, String> jobXmlNames;
private final Map<String, VirtualFile> jobXmlFiles;
private final Map<String, Set<String>> jobNames;
private WildFlyJobXmlResolver(final Map<String, VirtualFile> jobXmlFiles) {
jobXmlNames = new LinkedHashMap<>();
jobXmlResolvers = new LinkedHashSet<>();
jobNames = new LinkedHashMap<>();
this.jobXmlFiles = jobXmlFiles;
}
/**
* Creates the {@linkplain JobXmlResolver resolver} for the deployment inheriting any visible resolvers and job XML
* files from dependencies.
*
* @param deploymentUnit the deployment to process
*
* @return the resolve
*
* @throws DeploymentUnitProcessingException if an error occurs processing the deployment
*/
public static WildFlyJobXmlResolver forDeployment(final DeploymentUnit deploymentUnit) throws DeploymentUnitProcessingException {
// If this deployment unit already has a resolver, just use it
if (deploymentUnit.hasAttachment(BatchAttachments.JOB_XML_RESOLVER)) {
return deploymentUnit.getAttachment(BatchAttachments.JOB_XML_RESOLVER);
}
// Get the module for it's class loader
final Module module = deploymentUnit.getAttachment(Attachments.MODULE);
final ClassLoader classLoader = module.getClassLoader();
WildFlyJobXmlResolver resolver;
// If we're an EAR we need to skip sub-deployments as they'll be process later, however all sub-deployments have
// access to the EAR/lib directory so those resources need to be processed
if (DeploymentTypeMarker.isType(DeploymentType.EAR, deploymentUnit)) {
// Create a new WildFlyJobXmlResolver without jobs from sub-deployments as they'll be processed later
final List<ResourceRoot> resources = deploymentUnit.getAttachmentList(Attachments.RESOURCE_ROOTS)
.stream()
.filter(r -> !SubDeploymentMarker.isSubDeployment(r))
.collect(Collectors.toList());
resolver = create(classLoader, resources);
deploymentUnit.putAttachment(BatchAttachments.JOB_XML_RESOLVER, resolver);
} else {
// Create a new resolver for this deployment
if (deploymentUnit.hasAttachment(Attachments.RESOURCE_ROOTS)) {
resolver = create(classLoader, deploymentUnit.getAttachmentList(Attachments.RESOURCE_ROOTS));
} else {
resolver = create(classLoader, Collections.singletonList(deploymentUnit.getAttachment(Attachments.DEPLOYMENT_ROOT)));
}
deploymentUnit.putAttachment(BatchAttachments.JOB_XML_RESOLVER, resolver);
// Process all accessible sub-deployments
final List<DeploymentUnit> accessibleDeployments = deploymentUnit.getAttachmentList(Attachments.ACCESSIBLE_SUB_DEPLOYMENTS);
for (DeploymentUnit subDeployment : accessibleDeployments) {
// Skip our self
if (deploymentUnit.equals(subDeployment)) {
continue;
}
if (subDeployment.hasAttachment(BatchAttachments.JOB_XML_RESOLVER)) {
final WildFlyJobXmlResolver toCopy = subDeployment.getAttachment(BatchAttachments.JOB_XML_RESOLVER);
WildFlyJobXmlResolver.merge(resolver, toCopy);
} else {
// We need to create a resolver for the sub-deployment and merge the two
final WildFlyJobXmlResolver toCopy = forDeployment(subDeployment);
subDeployment.putAttachment(BatchAttachments.JOB_XML_RESOLVER, toCopy);
WildFlyJobXmlResolver.merge(resolver, toCopy);
}
}
}
return resolver;
}
@Override
public InputStream resolveJobXml(final String jobXml, final ClassLoader classLoader) throws IOException {
if (jobXmlFiles.isEmpty() && jobXmlResolvers.isEmpty()) {
return null;
}
for (JobXmlResolver resolver : jobXmlResolvers) {
final InputStream in = resolver.resolveJobXml(jobXml, classLoader);
if (in != null) {
return in;
}
}
final VirtualFile file = jobXmlFiles.get(jobXml);
if (file == null) {
return null;
}
if (WildFlySecurityManager.isChecking()) {
return AccessController.doPrivileged((PrivilegedAction<InputStream>) () -> {
try {
return file.openStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
return file.openStream();
}
@Override
public Collection<String> getJobXmlNames(final ClassLoader classLoader) {
return new ArrayList<>(jobXmlNames.keySet());
}
@Override
public String resolveJobName(final String jobXml, final ClassLoader classLoader) {
return jobXmlNames.get(jobXml);
}
/**
* Validates whether or not the job name exists for this deployment.
*
* @param jobName the job name to check
*
* @return {@code true} if the job exists, otherwise {@code false}
*/
boolean isValidJobName(final String jobName) {
return jobNames.containsKey(jobName);
}
/**
* Returns the job XML file names which contain the job name.
*
* @param jobName the job name to find the job XML files for
*
* @return the set of job XML files the job can be run from
*/
Set<String> getJobXmlNames(final String jobName) {
if (jobNames.containsKey(jobName)) {
return Collections.unmodifiableSet(jobNames.get(jobName));
}
return Collections.emptySet();
}
/**
* Returns all the job names available from this resolver.
*
* @return the job names available from this resolver
*/
Set<String> getJobNames() {
return new LinkedHashSet<>(jobNames.keySet());
}
/**
* Validates whether or not the job XML descriptor exists for this deployment.
*
* @param jobXmlName the job XML descriptor name
*
* @return {@code true} if the job XML descriptor exists for this deployment, otherwise {@code false}
*/
boolean isValidJobXmlName(final String jobXmlName) {
return jobXmlNames.containsKey(jobXmlName);
}
private static WildFlyJobXmlResolver create(final ClassLoader classLoader, final List<ResourceRoot> resources) throws DeploymentUnitProcessingException {
final Map<String, VirtualFile> foundJobXmlFiles = new LinkedHashMap<>();
for (ResourceRoot r : resources) {
final VirtualFile root = r.getRoot();
try {
addJobXmlFiles(foundJobXmlFiles, root.getChild(DEFAULT_PATH));
} catch (IOException e) {
throw BatchLogger.LOGGER.errorProcessingBatchJobsDir(e);
}
}
final WildFlyJobXmlResolver jobXmlResolver = new WildFlyJobXmlResolver(foundJobXmlFiles);
// Initialize this instance
jobXmlResolver.init(classLoader);
return jobXmlResolver;
}
private static void addJobXmlFiles(final Map<String, VirtualFile> foundJobXmlFiles, final VirtualFile jobsDir) throws IOException {
if (jobsDir != null && jobsDir.exists()) {
// We may have some job XML files
final Map<String, VirtualFile> xmlFiles = jobsDir.getChildren(JobXmlFilter.INSTANCE)
.stream()
.collect(Collectors.toMap(VirtualFile::getName, (f) -> f));
foundJobXmlFiles.putAll(xmlFiles);
}
}
private static void merge(final WildFlyJobXmlResolver target, final WildFlyJobXmlResolver toCopy) {
toCopy.jobXmlNames.forEach(target.jobXmlNames::putIfAbsent);
toCopy.jobXmlFiles.forEach(target.jobXmlFiles::putIfAbsent);
target.jobXmlResolvers.addAll(toCopy.jobXmlResolvers);
}
/**
* Initializes the state of an instance
*/
private void init(final ClassLoader classLoader) {
// Load the user defined resolvers
for (JobXmlResolver resolver : ServiceLoader.load(JobXmlResolver.class, classLoader)) {
jobXmlResolvers.add(resolver);
for (String jobXml : resolver.getJobXmlNames(classLoader)) {
addJob(jobXml, resolver.resolveJobName(jobXml, classLoader));
}
}
// Load the default names
for (Map.Entry<String, VirtualFile> entry : jobXmlFiles.entrySet()) {
try {
// Parsing the entire job XML seems excessive to just get the job name. There are two reasons for this:
// 1) If an error occurs during parsing there's no real need to consider this a valid job
// 2) Using the implementation parser seems less error prone for future-proofing
final Job job = JobParser.parseJob(entry.getValue().openStream(), classLoader, new XMLResolver() {
// this is essentially what JBeret does, but it's ugly. JBeret might need an API to handle this
@Override
public Object resolveEntity(final String publicID, final String systemID, final String baseURI, final String namespace) throws XMLStreamException {
try {
return (jobXmlFiles.containsKey(systemID) ? jobXmlFiles.get(systemID).openStream() : null);
} catch (IOException e) {
throw new XMLStreamException(e);
}
}
});
addJob(entry.getKey(), job.getId());
} catch (XMLStreamException | IOException e) {
// Report the possible error as we don't want to fail the deployment. The job may never be run.
BatchLogger.LOGGER.invalidJobXmlFile(entry.getKey());
}
}
}
private void addJob(final String jobXmlName, final String jobName) {
jobXmlNames.put(jobXmlName, jobName);
final Set<String> xmlDescriptors = jobNames.computeIfAbsent(jobName, s -> new LinkedHashSet<>());
xmlDescriptors.add(jobXmlName);
}
private static class JobXmlFilter implements VirtualFileFilter {
static final JobXmlFilter INSTANCE = new JobXmlFilter();
@Override
public boolean accepts(final VirtualFile file) {
return file.isFile() && file.getName().endsWith(".xml");
}
}
}