/* * JBoss, Home of Professional Open Source. * Copyright 2010, 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.jboss.as.server.deployment.module; import java.io.Closeable; import java.io.IOException; import java.util.ArrayDeque; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.Manifest; import org.jboss.as.server.logging.ServerLogger; import org.jboss.as.server.deployment.Attachable; import org.jboss.as.server.deployment.Attachments; import org.jboss.as.server.deployment.DeploymentPhaseContext; import org.jboss.as.server.deployment.DeploymentUnit; import org.jboss.as.server.deployment.DeploymentUnitProcessingException; import org.jboss.as.server.deployment.DeploymentUnitProcessor; import org.jboss.as.server.deployment.DeploymentUtils; import org.jboss.as.server.deployment.MountedDeploymentOverlay; import org.jboss.as.server.deployment.SubDeploymentMarker; import org.jboss.as.server.deployment.annotation.ResourceRootIndexer; import org.jboss.as.server.moduleservice.ExternalModuleService; import org.jboss.as.server.moduleservice.ServiceModuleLoader; import org.jboss.modules.ModuleIdentifier; import org.jboss.vfs.VFS; import org.jboss.vfs.VFSUtils; import org.jboss.vfs.VirtualFile; /** * A processor which adds class path entries for each manifest entry. * <p/> * <p/> * <p/> * </li> * <li> * If the Class-Path entry is external to the deployment then it is handled by the external jar service.</li> * <li> * If the entry refers to a sibling deployment then a dependency is added on that deployment. If this deployment is * not present then this deployment will block until it is.</li> * <li> * If the Class-Path entry points to a jar inside the ear that is not a deployment and not a /lib jar then a reference is added * to this jars {@link AdditionalModuleSpecification}</li> * </ul> * * @author Stuart Douglas * @author Ales Justin */ public final class ManifestClassPathProcessor implements DeploymentUnitProcessor { private static final String[] EMPTY_STRING_ARRAY = {}; /** * We only allow a single deployment at a time to be run through the class path processor. * <p/> * This is because if multiple sibling deployments reference the same item we need to make sure that they end up * with the same external module, and do not both create an external module with the same name. */ public synchronized void deploy(final DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException { final DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit(); final DeploymentUnit parent = deploymentUnit.getParent(); final DeploymentUnit topLevelDeployment = parent == null ? deploymentUnit : parent; final VirtualFile topLevelRoot = topLevelDeployment.getAttachment(Attachments.DEPLOYMENT_ROOT).getRoot(); final ExternalModuleService externalModuleService = topLevelDeployment.getAttachment(Attachments.EXTERNAL_MODULE_SERVICE); final ResourceRoot deploymentRoot = deploymentUnit.getAttachment(Attachments.DEPLOYMENT_ROOT); //These are resource roots that are already accessible by default //such as ear/lib jars an web-inf/lib jars final Set<VirtualFile> existingAccessibleRoots = new HashSet<VirtualFile>(); final Map<VirtualFile, ResourceRoot> subDeployments = new HashMap<VirtualFile, ResourceRoot>(); for (ResourceRoot root : DeploymentUtils.allResourceRoots(topLevelDeployment)) { if (SubDeploymentMarker.isSubDeployment(root)) { subDeployments.put(root.getRoot(), root); } else if (ModuleRootMarker.isModuleRoot(root)) { //top level module roots are already accessible, as they are either //ear/lib jars, or jars that are already part of the deployment existingAccessibleRoots.add(root.getRoot()); } } final ArrayDeque<RootEntry> resourceRoots = new ArrayDeque<RootEntry>(); if (deploymentUnit.getParent() != null) { //top level deployments already had their exiting roots processed above for (ResourceRoot root : DeploymentUtils.allResourceRoots(deploymentUnit)) { if (ModuleRootMarker.isModuleRoot(root)) { //if this is a sub deployment of an ear we need to make sure we don't //re-add existing module roots as class path entries //this will mainly be WEB-INF/(lib|classes) entries existingAccessibleRoots.add(root.getRoot()); } } } for (ResourceRoot root : DeploymentUtils.allResourceRoots(deploymentUnit)) { //add this to the list of roots to be processed resourceRoots.add(new RootEntry(deploymentUnit, root)); } // build a map of the additional module locations // note that if a resource root has been added to two different additional modules // and is then referenced via a Class-Path entry the behaviour is undefined final Map<VirtualFile, AdditionalModuleSpecification> additionalModules = new HashMap<VirtualFile, AdditionalModuleSpecification>(); for (AdditionalModuleSpecification module : topLevelDeployment.getAttachmentList(Attachments.ADDITIONAL_MODULES)) { for (ResourceRoot additionalModuleResourceRoot : module.getResourceRoots()) { additionalModules.put(additionalModuleResourceRoot.getRoot(), module); } } //additional resource roots may be added as while (!resourceRoots.isEmpty()) { final RootEntry entry = resourceRoots.pop(); final ResourceRoot resourceRoot = entry.resourceRoot; final Attachable target = entry.target; //if this is a top level deployment we do not want to process sub deployments if (SubDeploymentMarker.isSubDeployment(resourceRoot) && resourceRoot != deploymentRoot) { continue; } final String[] items = getClassPathEntries(resourceRoot); for (final String item : items) { if (item.isEmpty() || item.equals(".")) { //a class path of . causes problems and is unnecessary, see WFLY-2725 continue; } //first try and resolve relative to the manifest resource root final VirtualFile classPathFile = resourceRoot.getRoot().getParent().getChild(item); //then resolve relative to the deployment root final VirtualFile topLevelClassPathFile = deploymentRoot.getRoot().getParent().getChild(item); if (item.startsWith("/")) { if (externalModuleService.isValid(item)) { final ModuleIdentifier moduleIdentifier = externalModuleService.addExternalModule(item); target.addToAttachmentList(Attachments.CLASS_PATH_ENTRIES, moduleIdentifier); ServerLogger.DEPLOYMENT_LOGGER.debugf("Resource %s added as external jar %s", classPathFile, resourceRoot.getRoot()); } else { ServerLogger.DEPLOYMENT_LOGGER.classPathEntryNotValid(item, resourceRoot.getRoot().getPathName()); } } else { if (classPathFile.exists()) { //we need to check that this class path item actually lies within the deployment boolean found = false; VirtualFile file = classPathFile.getParent(); while (file != null) { if (file.equals(topLevelRoot)) { found = true; } file = file.getParent(); } if (!found) { ServerLogger.DEPLOYMENT_LOGGER.classPathEntryNotValid(item, resourceRoot.getRoot().getPathName()); } else { handlingExistingClassPathEntry(resourceRoots, topLevelDeployment, topLevelRoot, subDeployments, additionalModules, existingAccessibleRoots, resourceRoot, target, classPathFile); } } else if (topLevelClassPathFile.exists()) { boolean found = false; VirtualFile file = topLevelClassPathFile.getParent(); while (file != null) { if (file.equals(topLevelRoot)) { found = true; } file = file.getParent(); } if (!found) { ServerLogger.DEPLOYMENT_LOGGER.classPathEntryNotValid(item, resourceRoot.getRoot().getPathName()); } else { handlingExistingClassPathEntry(resourceRoots, topLevelDeployment, topLevelRoot, subDeployments, additionalModules, existingAccessibleRoots, resourceRoot, target, topLevelClassPathFile); } } else { ServerLogger.DEPLOYMENT_LOGGER.classPathEntryNotValid(item, resourceRoot.getRoot().getPathName()); } } } } } private void handlingExistingClassPathEntry(final ArrayDeque<RootEntry> resourceRoots, final DeploymentUnit topLevelDeployment, final VirtualFile topLevelRoot, final Map<VirtualFile, ResourceRoot> subDeployments, final Map<VirtualFile, AdditionalModuleSpecification> additionalModules, final Set<VirtualFile> existingAccessibleRoots, final ResourceRoot resourceRoot, final Attachable target, final VirtualFile classPathFile) throws DeploymentUnitProcessingException { if (existingAccessibleRoots.contains(classPathFile)) { ServerLogger.DEPLOYMENT_LOGGER.debugf("Class-Path entry %s in %s ignored, as target is already accessible", classPathFile, resourceRoot.getRoot()); } else if (additionalModules.containsKey(classPathFile)) { final AdditionalModuleSpecification moduleSpecification = additionalModules.get(classPathFile); //as class path entries are exported, transitive dependencies will also be available target.addToAttachmentList(Attachments.CLASS_PATH_ENTRIES, moduleSpecification.getModuleIdentifier()); } else if (subDeployments.containsKey(classPathFile)) { //now we need to calculate the sub deployment module identifier //unfortunately the sub deployment has not been setup yet, so we cannot just //get it from the sub deployment directly final ResourceRoot otherRoot = subDeployments.get(classPathFile); target.addToAttachmentList(Attachments.CLASS_PATH_ENTRIES, ModuleIdentifierProcessor.createModuleIdentifier(otherRoot.getRootName(), otherRoot, topLevelDeployment, topLevelRoot, false)); } else { ModuleIdentifier identifier = createAdditionalModule(resourceRoot, topLevelDeployment, topLevelRoot, additionalModules, classPathFile, resourceRoots); target.addToAttachmentList(Attachments.CLASS_PATH_ENTRIES, identifier); } } private ModuleIdentifier createAdditionalModule(final ResourceRoot resourceRoot, final DeploymentUnit topLevelDeployment, final VirtualFile topLevelRoot, final Map<VirtualFile, AdditionalModuleSpecification> additionalModules, final VirtualFile classPathFile, final ArrayDeque<RootEntry> resourceRoots) throws DeploymentUnitProcessingException { final ResourceRoot root = createResourceRoot(classPathFile, topLevelDeployment, topLevelRoot); final String pathName = root.getRoot().getPathNameRelativeTo(topLevelRoot); ModuleIdentifier identifier = ModuleIdentifier.create(ServiceModuleLoader.MODULE_PREFIX + topLevelDeployment.getName() + "." + pathName); AdditionalModuleSpecification module = new AdditionalModuleSpecification(identifier, root); topLevelDeployment.addToAttachmentList(Attachments.ADDITIONAL_MODULES, module); additionalModules.put(classPathFile, module); resourceRoot.addToAttachmentList(Attachments.CLASS_PATH_RESOURCE_ROOTS, root); //add this to the list of roots to be processed, so transitive class path entries will be respected resourceRoots.add(new RootEntry(module, root)); return identifier; } private static String[] getClassPathEntries(final ResourceRoot resourceRoot) { final Manifest manifest; try { manifest = VFSUtils.getManifest(resourceRoot.getRoot()); } catch (IOException e) { throw new RuntimeException(e); } if (manifest == null) { // no class path to process! return EMPTY_STRING_ARRAY; } final String classPathString = manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH); if (classPathString == null) { // no entry return EMPTY_STRING_ARRAY; } return classPathString.split("\\s+"); } /** * Creates a {@link ResourceRoot} for the passed {@link VirtualFile file} and adds it to the list of {@link ResourceRoot}s * in the {@link DeploymentUnit deploymentUnit} * * * @param file The file for which the resource root will be created * @return Returns the created {@link ResourceRoot} * @throws java.io.IOException */ private synchronized ResourceRoot createResourceRoot(final VirtualFile file, final DeploymentUnit deploymentUnit, final VirtualFile deploymentRoot) throws DeploymentUnitProcessingException { try { Map<String, MountedDeploymentOverlay> overlays = deploymentUnit.getAttachment(Attachments.DEPLOYMENT_OVERLAY_LOCATIONS); String relativeName = file.getPathNameRelativeTo(deploymentRoot); MountedDeploymentOverlay overlay = overlays.get(relativeName); Closeable closable = null; if(overlay != null) { overlay.remountAsZip(false); } else if(file.isFile()) { closable = VFS.mountZip(file, file, TempFileProviderService.provider()); } final MountHandle mountHandle = new MountHandle(closable); final ResourceRoot resourceRoot = new ResourceRoot(file, mountHandle); ModuleRootMarker.mark(resourceRoot); ResourceRootIndexer.indexResourceRoot(resourceRoot); return resourceRoot; } catch (IOException e) { throw new RuntimeException(e); } } /** * {@inheritDoc} */ public void undeploy(final DeploymentUnit context) { } private class RootEntry { private final ResourceRoot resourceRoot; private final Attachable target; private RootEntry(final Attachable target, final ResourceRoot resourceRoot) { this.target = target; this.resourceRoot = resourceRoot; } } }