/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.tools.ant.taskdefs.optional.ejb; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.util.Enumeration; import java.util.HashSet; import java.util.Hashtable; import java.util.Set; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.zip.ZipEntry; import javax.xml.parsers.SAXParser; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.Location; import org.apache.tools.ant.Project; import org.apache.tools.ant.Task; import org.apache.tools.ant.taskdefs.optional.ejb.EjbJar.DTDLocation; import org.apache.tools.ant.types.FileSet; import org.apache.tools.ant.types.Path; import org.apache.tools.ant.util.depend.DependencyAnalyzer; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * A deployment tool which creates generic EJB jars. Generic jars contains * only those classes and META-INF entries specified in the EJB 1.1 standard * * This class is also used as a framework for the creation of vendor specific * deployment tools. A number of template methods are provided through which the * vendor specific tool can hook into the EJB creation process. * */ public class GenericDeploymentTool implements EJBDeploymentTool { /** The default buffer byte size to use for IO */ public static final int DEFAULT_BUFFER_SIZE = 1024; /** The level to use for compression */ public static final int JAR_COMPRESS_LEVEL = 9; /** The standard META-INF directory in jar files */ protected static final String META_DIR = "META-INF/"; /** The standard MANIFEST file */ protected static final String MANIFEST = META_DIR + "MANIFEST.MF"; /** Name for EJB Deployment descriptor within EJB jars */ protected static final String EJB_DD = "ejb-jar.xml"; /** A dependency analyzer name to find ancestor classes */ public static final String ANALYZER_SUPER = "super"; /** A dependency analyzer name to find all related classes */ public static final String ANALYZER_FULL = "full"; /** A dependency analyzer name for no analyzer */ public static final String ANALYZER_NONE = "none"; /** The default analyzer */ public static final String DEFAULT_ANALYZER = ANALYZER_SUPER; /** The analyzer class for the super analyzer */ public static final String ANALYZER_CLASS_SUPER = "org.apache.tools.ant.util.depend.bcel.AncestorAnalyzer"; /** The analyzer class for the super analyzer */ public static final String ANALYZER_CLASS_FULL = "org.apache.tools.ant.util.depend.bcel.FullAnalyzer"; /** * The configuration from the containing task. This config combined * with the settings of the individual attributes here constitues the * complete config for this deployment tool. */ private EjbJar.Config config; /** Stores a handle to the directory to put the Jar files in */ private File destDir; /** The classpath to use with this deployment tool. This is appended to any paths from the ejbjar task itself.*/ private Path classpath; /** Instance variable that stores the suffix for the generated jarfile. */ private String genericJarSuffix = "-generic.jar"; /** * The task to which this tool belongs. This is used to access services * provided by the ant core, such as logging. */ private Task task; /** * The classloader generated from the given classpath to load * the super classes and super interfaces. */ private ClassLoader classpathLoader = null; /** * Set of files have been loaded into the EJB jar */ private Set<String> addedfiles; /** * Handler used to parse the EJB XML descriptor */ private DescriptorHandler handler; /** * Dependency analyzer used to collect class dependencies */ private DependencyAnalyzer dependencyAnalyzer; /** * Set the destination directory; required. * @param inDir the destination directory. */ public void setDestdir(File inDir) { this.destDir = inDir; } /** * Get the destination directory. * * @return the destination directory into which EJB jars are to be written */ protected File getDestDir() { return destDir; } /** * Set the task which owns this tool * * @param task the Task to which this deployment tool is associated. */ @Override public void setTask(Task task) { this.task = task; } /** * Get the task for this tool. * * @return the Task instance this tool is associated with. */ protected Task getTask() { return task; } /** * Get the basename terminator. * * @return an ejbjar task configuration */ protected EjbJar.Config getConfig() { return config; } /** * Indicate if this build is using the base jar name. * * @return true if the name of the generated jar is coming from the * basejarname attribute */ protected boolean usingBaseJarName() { return config.baseJarName != null; } /** * Set the suffix for the generated jar file. * @param inString the string to use as the suffix. */ public void setGenericJarSuffix(String inString) { this.genericJarSuffix = inString; } /** * Add the classpath for the user classes * * @return a Path instance to be configured by Ant. */ public Path createClasspath() { if (classpath == null) { classpath = new Path(task.getProject()); } return classpath.createPath(); } /** * Set the classpath to be used for this compilation. * * @param classpath the classpath to be used for this build. */ public void setClasspath(Path classpath) { this.classpath = classpath; } /** * Get the classpath by combining the one from the surrounding task, if any * and the one from this tool. * * @return the combined classpath */ protected Path getCombinedClasspath() { Path combinedPath = classpath; if (config.classpath != null) { if (combinedPath == null) { combinedPath = config.classpath; } else { combinedPath.append(config.classpath); } } return combinedPath; } /** * Log a message to the Ant output. * * @param message the message to be logged. * @param level the severity of this message. */ protected void log(String message, int level) { getTask().log(message, level); } /** * Get the build file location associated with this element's task. * * @return the task's location instance. */ protected Location getLocation() { return getTask().getLocation(); } private void createAnalyzer() { String analyzer = config.analyzer; if (analyzer == null) { analyzer = DEFAULT_ANALYZER; } if (analyzer.equals(ANALYZER_NONE)) { return; } String analyzerClassName = null; if (analyzer.equals(ANALYZER_SUPER)) { analyzerClassName = ANALYZER_CLASS_SUPER; } else if (analyzer.equals(ANALYZER_FULL)) { analyzerClassName = ANALYZER_CLASS_FULL; } else { analyzerClassName = analyzer; } try { Class<? extends DependencyAnalyzer> analyzerClass = Class.forName(analyzerClassName) .asSubclass(DependencyAnalyzer.class); dependencyAnalyzer = analyzerClass.newInstance(); dependencyAnalyzer.addClassPath(new Path(task.getProject(), config.srcDir.getPath())); dependencyAnalyzer.addClassPath(config.classpath); } catch (NoClassDefFoundError e) { dependencyAnalyzer = null; task.log("Unable to load dependency analyzer: " + analyzerClassName + " - dependent class not found: " + e.getMessage(), Project.MSG_WARN); } catch (Exception e) { dependencyAnalyzer = null; task.log("Unable to load dependency analyzer: " + analyzerClassName + " - exception: " + e.getMessage(), Project.MSG_WARN); } } /** * Configure this tool for use in the ejbjar task. * * @param config the configuration from the surrounding ejbjar task. */ @Override public void configure(EjbJar.Config config) { this.config = config; createAnalyzer(); classpathLoader = null; } /** * Utility method that encapsulates the logic of adding a file entry to * a .jar file. Used by execute() to add entries to the jar file as it is * constructed. * @param jStream A JarOutputStream into which to write the * jar entry. * @param inputFile A File from which to read the * contents the file being added. * @param logicalFilename A String representing the name, including * all relevant path information, that should be stored for the entry * being added. * @throws BuildException if there is a problem. */ protected void addFileToJar(JarOutputStream jStream, File inputFile, String logicalFilename) throws BuildException { if (!addedfiles.contains(logicalFilename)) { try (InputStream iStream = Files.newInputStream(inputFile.toPath())) { // Create the zip entry and add it to the jar file ZipEntry zipEntry = new ZipEntry(logicalFilename.replace('\\', '/')); jStream.putNextEntry(zipEntry); // Create the file input stream, and buffer everything over // to the jar output stream byte[] byteBuffer = new byte[2 * DEFAULT_BUFFER_SIZE]; int count = 0; do { jStream.write(byteBuffer, 0, count); count = iStream.read(byteBuffer, 0, byteBuffer.length); } while (count != -1); //add it to list of files in jar addedfiles.add(logicalFilename); } catch (IOException ioe) { log("WARNING: IOException while adding entry " + logicalFilename + " to jarfile from " + inputFile.getPath() + " " + ioe.getClass().getName() + "-" + ioe.getMessage(), Project.MSG_WARN); } } } /** * Get a descriptionHandler. * @param srcDir the source directory. * @return a handler. */ protected DescriptorHandler getDescriptorHandler(File srcDir) { DescriptorHandler h = new DescriptorHandler(getTask(), srcDir); registerKnownDTDs(h); // register any DTDs supplied by the user for (DTDLocation dtdLocation : getConfig().dtdLocations) { h.registerDTD(dtdLocation.getPublicId(), dtdLocation.getLocation()); } return h; } /** * Register the locations of all known DTDs. * * vendor-specific subclasses should override this method to define * the vendor-specific locations of the EJB DTDs * @param handler no used in this class. */ protected void registerKnownDTDs(DescriptorHandler handler) { // none to register for generic } /** {@inheritDoc}. */ @Override public void processDescriptor(String descriptorFileName, SAXParser saxParser) { checkConfiguration(descriptorFileName, saxParser); try { handler = getDescriptorHandler(config.srcDir); // Retrive the files to be added to JAR from EJB descriptor Hashtable<String, File> ejbFiles = parseEjbFiles(descriptorFileName, saxParser); // Add any support classes specified in the build file addSupportClasses(ejbFiles); // Determine the JAR filename (without filename extension) String baseName = getJarBaseName(descriptorFileName); String ddPrefix = getVendorDDPrefix(baseName, descriptorFileName); File manifestFile = getManifestFile(ddPrefix); if (manifestFile != null) { ejbFiles.put(MANIFEST, manifestFile); } // First the regular deployment descriptor ejbFiles.put(META_DIR + EJB_DD, new File(config.descriptorDir, descriptorFileName)); // now the vendor specific files, if any addVendorFiles(ejbFiles, ddPrefix); // add any dependent files checkAndAddDependants(ejbFiles); // Lastly create File object for the Jar files. If we are using // a flat destination dir, then we need to redefine baseName! if (config.flatDestDir && baseName.length() != 0) { int startName = baseName.lastIndexOf(File.separator); if (startName == -1) { startName = 0; } int endName = baseName.length(); baseName = baseName.substring(startName, endName); } File jarFile = getVendorOutputJarFile(baseName); // Check to see if we need a build and start doing the work! if (needToRebuild(ejbFiles, jarFile)) { // Log that we are going to build... log("building " + jarFile.getName() + " with " + String.valueOf(ejbFiles.size()) + " files", Project.MSG_INFO); // Use helper method to write the jarfile String publicId = getPublicId(); writeJar(baseName, jarFile, ejbFiles, publicId); } else { // Log that the file is up to date... log(jarFile.toString() + " is up to date.", Project.MSG_VERBOSE); } } catch (SAXException se) { throw new BuildException( "SAXException while parsing '" + descriptorFileName + "'. This probably indicates badly-formed XML." + " Details: " + se.getMessage(), se); } catch (IOException ioe) { throw new BuildException( "IOException while parsing'" + descriptorFileName + "'. This probably indicates that the descriptor" + " doesn't exist. Details: " + ioe.getMessage(), ioe); } } /** * This method is called as the first step in the processDescriptor method * to allow vendor-specific subclasses to validate the task configuration * prior to processing the descriptor. If the configuration is invalid, * a BuildException should be thrown. * * @param descriptorFileName String representing the file name of an EJB * descriptor to be processed * @param saxParser SAXParser which may be used to parse the XML * descriptor * @throws BuildException if there is a problem. */ protected void checkConfiguration(String descriptorFileName, SAXParser saxParser) throws BuildException { /* * For the GenericDeploymentTool, do nothing. Vendor specific * subclasses should throw a BuildException if the configuration is * invalid for their server. */ } /** * This method returns a list of EJB files found when the specified EJB * descriptor is parsed and processed. * * @param descriptorFileName String representing the file name of an EJB * descriptor to be processed * @param saxParser SAXParser which may be used to parse the XML * descriptor * @return Hashtable of EJB class (and other) files to be * added to the completed JAR file * @throws SAXException Any SAX exception, possibly wrapping another * exception * @throws IOException An IOException from the parser, possibly from a * the byte stream or character stream */ protected Hashtable<String, File> parseEjbFiles(String descriptorFileName, SAXParser saxParser) throws IOException, SAXException { /* Parse the ejb deployment descriptor. While it may not * look like much, we use a SAXParser and an inner class to * get hold of all the classfile names for the descriptor. */ try (InputStream descriptorStream = Files.newInputStream( new File(config.descriptorDir, descriptorFileName).toPath())) { saxParser.parse(new InputSource(descriptorStream), handler); return handler.getFiles(); } } /** * Adds any classes the user specifies using <i>support</i> nested elements * to the <code>ejbFiles</code> Hashtable. * * @param ejbFiles Hashtable of EJB classes (and other) files that will be * added to the completed JAR file */ protected void addSupportClasses(Hashtable<String, File> ejbFiles) { // add in support classes if any Project project = task.getProject(); for (FileSet supportFileSet : config.supportFileSets) { File supportBaseDir = supportFileSet.getDir(project); DirectoryScanner supportScanner = supportFileSet.getDirectoryScanner(project); for (String supportFile : supportScanner.getIncludedFiles()) { ejbFiles.put(supportFile, new File(supportBaseDir, supportFile)); } } } /** * Using the EJB descriptor file name passed from the <code>ejbjar</code> * task, this method returns the "basename" which will be used to name the * completed JAR file. * * @param descriptorFileName String representing the file name of an EJB * descriptor to be processed * @return The "basename" which will be used to name the * completed JAR file */ protected String getJarBaseName(String descriptorFileName) { String baseName = ""; // Work out what the base name is if (EjbJar.NamingScheme.BASEJARNAME.equals(config.namingScheme.getValue())) { String canonicalDescriptor = descriptorFileName.replace('\\', '/'); int index = canonicalDescriptor.lastIndexOf('/'); if (index != -1) { baseName = descriptorFileName.substring(0, index + 1); } baseName += config.baseJarName; } else if (EjbJar.NamingScheme.DESCRIPTOR.equals(config.namingScheme.getValue())) { int lastSeparatorIndex = descriptorFileName.lastIndexOf(File.separator); int endBaseName = -1; if (lastSeparatorIndex != -1) { endBaseName = descriptorFileName.indexOf(config.baseNameTerminator, lastSeparatorIndex); } else { endBaseName = descriptorFileName.indexOf(config.baseNameTerminator); } if (endBaseName != -1) { baseName = descriptorFileName.substring(0, endBaseName); } else { throw new BuildException( "Unable to determine jar name from descriptor \"%s\"", descriptorFileName); } } else if (EjbJar.NamingScheme.DIRECTORY.equals(config.namingScheme.getValue())) { File descriptorFile = new File(config.descriptorDir, descriptorFileName); String path = descriptorFile.getAbsolutePath(); int lastSeparatorIndex = path.lastIndexOf(File.separator); if (lastSeparatorIndex == -1) { throw new BuildException("Unable to determine directory name holding descriptor"); } String dirName = path.substring(0, lastSeparatorIndex); int dirSeparatorIndex = dirName.lastIndexOf(File.separator); if (dirSeparatorIndex != -1) { dirName = dirName.substring(dirSeparatorIndex + 1); } baseName = dirName; } else if (EjbJar.NamingScheme.EJB_NAME.equals(config.namingScheme.getValue())) { baseName = handler.getEjbName(); } return baseName; } /** * Get the prefix for vendor deployment descriptors. * * This will contain the path and the start of the descriptor name, * depending on the naming scheme * @param baseName the base name to use. * @param descriptorFileName the file name to use. * @return the prefix. */ public String getVendorDDPrefix(String baseName, String descriptorFileName) { String ddPrefix = null; if (config.namingScheme.getValue().equals(EjbJar.NamingScheme.DESCRIPTOR)) { ddPrefix = baseName + config.baseNameTerminator; } else if (config.namingScheme.getValue().equals(EjbJar.NamingScheme.BASEJARNAME) || config.namingScheme.getValue().equals(EjbJar.NamingScheme.EJB_NAME) || config.namingScheme.getValue().equals(EjbJar.NamingScheme.DIRECTORY)) { String canonicalDescriptor = descriptorFileName.replace('\\', '/'); int index = canonicalDescriptor.lastIndexOf('/'); if (index == -1) { ddPrefix = ""; } else { ddPrefix = descriptorFileName.substring(0, index + 1); } } return ddPrefix; } /** * Add any vendor specific files which should be included in the * EJB Jar. * @param ejbFiles a hashtable entryname -> file. * @param ddPrefix a prefix to use. */ protected void addVendorFiles(Hashtable<String, File> ejbFiles, String ddPrefix) { // nothing to add for generic tool. } /** * Get the vendor specific name of the Jar that will be output. The modification date * of this jar will be checked against the dependent bean classes. * @param baseName the basename to use. */ File getVendorOutputJarFile(String baseName) { return new File(destDir, baseName + genericJarSuffix); } /** * This method checks the timestamp on each file listed in the <code> * ejbFiles</code> and compares them to the timestamp on the <code>jarFile * </code>. If the <code>jarFile</code>'s timestamp is more recent than * each EJB file, <code>true</code> is returned. Otherwise, <code>false * </code> is returned. * TODO: find a way to check the manifest-file, that is found by naming convention * * @param ejbFiles Hashtable of EJB classes (and other) files that will be * added to the completed JAR file * @param jarFile JAR file which will contain all of the EJB classes (and * other) files * @return boolean indicating whether or not the <code>jarFile</code> * is up to date */ protected boolean needToRebuild(Hashtable<String, File> ejbFiles, File jarFile) { if (jarFile.exists()) { long lastBuild = jarFile.lastModified(); // Loop through the files seeing if any has been touched // more recently than the destination jar. for (File currentFile : ejbFiles.values()) { if (lastBuild < currentFile.lastModified()) { log("Build needed because " + currentFile.getPath() + " is out of date", Project.MSG_VERBOSE); return true; } } return false; } return true; } /** * Returns the Public ID of the DTD specified in the EJB descriptor. Not * every vendor-specific <code>DeploymentTool</code> will need to reference * this value or may want to determine this value in a vendor-specific way. * * @return Public ID of the DTD specified in the EJB descriptor. */ protected String getPublicId() { return handler.getPublicId(); } /** * Get the manifest file to use for building the generic jar. * * If the file does not exist the global manifest from the config is used * otherwise the default Ant manifest will be used. * * @param prefix the prefix where to llook for the manifest file based on * the naming convention. * * @return the manifest file or null if the manifest file does not exist */ protected File getManifestFile(String prefix) { File manifestFile = new File(getConfig().descriptorDir, prefix + "manifest.mf"); if (manifestFile.exists()) { return manifestFile; } if (config.manifest != null) { return config.manifest; } return null; } /** * Method used to encapsulate the writing of the JAR file. Iterates over the * filenames/java.io.Files in the Hashtable stored on the instance variable * ejbFiles. * @param baseName the base name to use. * @param jarfile the jar file to write to. * @param files the files to write to the jar. * @param publicId the id to use. * @throws BuildException if there is a problem. */ protected void writeJar(String baseName, File jarfile, Hashtable<String, File> files, String publicId) throws BuildException { // clean the addedfiles set if (addedfiles == null) { addedfiles = new HashSet<>(); } else { addedfiles.clear(); } try { /* If the jarfile already exists then whack it and recreate it. * Should probably think of a more elegant way to handle this * so that in case of errors we don't leave people worse off * than when we started =) */ if (jarfile.exists()) { jarfile.delete(); } jarfile.getParentFile().mkdirs(); jarfile.createNewFile(); InputStream in = null; Manifest manifest = null; try { File manifestFile = files.get(MANIFEST); if (manifestFile != null && manifestFile.exists()) { in = Files.newInputStream(manifestFile.toPath()); } else { String defaultManifest = "/org/apache/tools/ant/defaultManifest.mf"; in = this.getClass().getResourceAsStream(defaultManifest); if (in == null) { throw new BuildException( "Could not find default manifest: %s", defaultManifest); } } manifest = new Manifest(in); } catch (IOException e) { throw new BuildException ("Unable to read manifest", e, getLocation()); } finally { if (in != null) { in.close(); } } // Create the streams necessary to write the jarfile try (JarOutputStream jarStream = new JarOutputStream( Files.newOutputStream(jarfile.toPath()), manifest)) { jarStream.setMethod(JarOutputStream.DEFLATED); // Loop through all the class files found and add them to the jar for (String entryName : files.keySet()) { if (entryName.equals(MANIFEST)) { continue; } File entryFile = files.get(entryName); log("adding file '" + entryName + "'", Project.MSG_VERBOSE); addFileToJar(jarStream, entryFile, entryName); // See if there are any inner classes for this class and add them in if there are InnerClassFilenameFilter flt = new InnerClassFilenameFilter(entryFile.getName()); File entryDir = entryFile.getParentFile(); String[] innerfiles = entryDir.list(flt); if (innerfiles != null) { for (String innerfile : innerfiles) { //get and clean up innerclass name int entryIndex = entryName.lastIndexOf(entryFile.getName()) - 1; if (entryIndex < 0) { entryName = innerfile; } else { entryName = entryName.substring(0, entryIndex) + File.separatorChar + innerfile; } // link the file entryFile = new File(config.srcDir, entryName); log("adding innerclass file '" + entryName + "'", Project.MSG_VERBOSE); addFileToJar(jarStream, entryFile, entryName); } } } } } catch (IOException ioe) { String msg = "IOException while processing ejb-jar file '" + jarfile.toString() + "'. Details: " + ioe.getMessage(); throw new BuildException(msg, ioe); } } // end of writeJar /** * Add all available classes, that depend on Remote, Home, Bean, PK * @param checkEntries files, that are extracted from the deployment descriptor * @throws BuildException if there is a problem. */ protected void checkAndAddDependants(Hashtable<String, File> checkEntries) throws BuildException { if (dependencyAnalyzer == null) { return; } dependencyAnalyzer.reset(); for (String entryName : checkEntries.keySet()) { if (entryName.endsWith(".class")) { String className = entryName.substring(0, entryName.length() - ".class".length()); className = className.replace(File.separatorChar, '/'); className = className.replace('/', '.'); dependencyAnalyzer.addRootClass(className); } } Enumeration<String> e = dependencyAnalyzer.getClassDependencies(); while (e.hasMoreElements()) { String classname = e.nextElement(); String location = classname.replace('.', File.separatorChar) + ".class"; File classFile = new File(config.srcDir, location); if (classFile.exists()) { checkEntries.put(location, classFile); log("dependent class: " + classname + " - " + classFile, Project.MSG_VERBOSE); } } } /** * Returns a Classloader object which parses the passed in generic EjbJar classpath. * The loader is used to dynamically load classes from javax.ejb.* and the classes * being added to the jar. * @return a classloader. */ protected ClassLoader getClassLoaderForBuild() { if (classpathLoader != null) { return classpathLoader; } Path combinedClasspath = getCombinedClasspath(); // only generate a new ClassLoader if we have a classpath if (combinedClasspath == null) { classpathLoader = getClass().getClassLoader(); } else { // Memory leak in line below classpathLoader = getTask().getProject().createClassLoader(combinedClasspath); } return classpathLoader; } /** * Called to validate that the tool parameters have been configured. * * @throws BuildException If the Deployment Tool's configuration isn't * valid */ @Override public void validateConfigured() throws BuildException { if ((destDir == null) || (!destDir.isDirectory())) { throw new BuildException( "A valid destination directory must be specified using the \"destdir\" attribute.", getLocation()); } } }