/* * 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 WARRANTIESOR 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.aries.web.converter.impl; import static org.apache.aries.web.converter.WarToWabConverter.WEB_CONTEXT_PATH; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.TreeSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import org.apache.aries.web.converter.WabConversion; import org.apache.aries.web.converter.WarToWabConverter.InputStreamProvider; import org.objectweb.asm.ClassReader; import org.osgi.framework.Constants; public class WarToWabConverterImpl implements WabConversion { private static final String DEFAULT_BUNDLE_VERSION = "1.0"; private static final String DEFAULT_BUNDLE_MANIFESTVERSION = "2"; private static final String INITIAL_CLASSPATH_ENTRY = "WEB-INF/classes"; private static final String CLASSPATH_LIB_PREFIX = "WEB-INF/lib/"; private static final String SERVLET_IMPORTS = "javax.servlet;version=2.5," + "javax.servlet.http;version=2.5"; private static final String JSP_IMPORTS = "javax.servlet.jsp;version=2.1," + "javax.servlet.jsp.el;version=2.1," + "javax.servlet.jsp.tagext;version=2.1," + "javax.servlet.jsp.resources;version=2.1"; private static final String DEFAULT_IMPORT_PACKAGE_LIST = SERVLET_IMPORTS + "," + JSP_IMPORTS; private CaseInsensitiveMap properties; // InputStream for the new WAB file private CachedOutputStream wab; private Manifest wabManifest; private String warName; private InputStreamProvider input; // State used for updating the manifest private Set<String> importPackages; private Set<String> exemptPackages; private Map<String, Manifest> manifests; private ArrayList<String> classPath; private boolean signed; public WarToWabConverterImpl(InputStreamProvider warFile, String name, Properties properties) throws IOException { this(warFile, name, new CaseInsensitiveMap(properties)); } public WarToWabConverterImpl(InputStreamProvider warFile, String name, CaseInsensitiveMap properties) throws IOException { this.properties = properties; classPath = new ArrayList<String>(); importPackages = new TreeSet<String>(); exemptPackages = new TreeSet<String>(); input = warFile; this.warName = name; } private void generateManifest() throws IOException { if (wabManifest != null) { // WAB manifest is already generated return; } JarInputStream jarInput = null; try { jarInput = new JarInputStream(input.getInputStream()); Manifest manifest = jarInput.getManifest(); if (isBundle(manifest)) { wabManifest = updateBundleManifest(manifest); } else { scanForDependencies(jarInput); // Add the new properties to the manifest byte stream wabManifest = updateManifest(manifest); } } finally { try { if (jarInput != null) jarInput.close(); } catch (IOException e) { e.printStackTrace(); } } } private void convert() throws IOException { if (wab != null) { // WAB is already converted return; } generateManifest(); CachedOutputStream output = new CachedOutputStream(); JarOutputStream jarOutput = null; JarInputStream jarInput = null; ZipEntry entry = null; // Copy across all entries from the original jar int val; try { jarOutput = new JarOutputStream(output, wabManifest); jarInput = new JarInputStream(input.getInputStream()); byte[] buffer = new byte[2048]; while ((entry = jarInput.getNextEntry()) != null) { // skip signature files if war is signed if (signed && isSignatureFile(entry.getName())) { continue; } jarOutput.putNextEntry(entry); while ((val = jarInput.read(buffer)) > 0) { jarOutput.write(buffer, 0, val); } } } finally { if (jarOutput != null) { jarOutput.close(); } if (jarInput != null) { jarInput.close(); } } wab = output; } private boolean isBundle(Manifest manifest) { if (manifest == null) { return false; } // Presence of _any_ of these headers indicates a bundle... Attributes attributes = manifest.getMainAttributes(); if (attributes.getValue(Constants.BUNDLE_SYMBOLICNAME) != null || attributes.getValue(Constants.BUNDLE_VERSION) != null || attributes.getValue(Constants.BUNDLE_MANIFESTVERSION) != null || attributes.getValue(Constants.IMPORT_PACKAGE) != null || attributes.getValue(WEB_CONTEXT_PATH) != null) { return true; } return false; } private void scanRecursive(final JarInputStream jarInput, boolean topLevel) throws IOException { ZipEntry entry; while ((entry = jarInput.getNextEntry()) != null) { if (entry.getName().endsWith(".class")) { PackageFinder pkgFinder = new PackageFinder(); new ClassReader(jarInput).accept(pkgFinder, ClassReader.SKIP_DEBUG); importPackages.addAll(pkgFinder.getImportPackages()); exemptPackages.addAll(pkgFinder.getExemptPackages()); } else if (entry.getName().endsWith(".jsp")) { Collection<String> thisJSPsImports = JSPImportParser.getImports(jarInput); importPackages.addAll(thisJSPsImports); } else if (entry.getName().endsWith(".jar")) { JarInputStream newJar = new JarInputStream(new InputStream() { @Override public int read() throws IOException { return jarInput.read(); } }); // discard return, we only care about the top level jars scanRecursive(newJar,false); // do not add jar embedded in already embedded jars if (topLevel) { manifests.put(entry.getName(), newJar.getManifest()); } } } } /** * * Read in the filenames inside the war (used for manifest update) Also * analyse the bytecode of any .class files in order to find any required * imports */ private void scanForDependencies(final JarInputStream jarInput) throws IOException { manifests = new HashMap<String, Manifest>(); scanRecursive(jarInput, true); // Process manifests from jars in order to work out classpath dependencies ClassPathBuilder classPathBuilder = new ClassPathBuilder(manifests); for (String fileName : manifests.keySet()) if (fileName.startsWith(CLASSPATH_LIB_PREFIX)) { classPath.add(fileName); classPath = classPathBuilder.updatePath(fileName, classPath); } // Remove packages that are part of the classes we searched through for (String s : exemptPackages) if (importPackages.contains(s)) importPackages.remove(s); } protected Manifest updateBundleManifest(Manifest manifest) throws IOException { String webCPath = properties.get(WEB_CONTEXT_PATH); if (webCPath == null) { webCPath = manifest.getMainAttributes().getValue(WEB_CONTEXT_PATH); } if (webCPath == null) { throw new IOException("Must specify " + WEB_CONTEXT_PATH + " parameter. The " + WEB_CONTEXT_PATH + " header is not defined in the source bundle."); } else { webCPath = addSlash(webCPath); manifest.getMainAttributes().put(new Attributes.Name(WEB_CONTEXT_PATH), webCPath); } // converter is not allowed to specify and override the following properties // when source is already a bundle checkParameter(Constants.BUNDLE_VERSION); checkParameter(Constants.BUNDLE_MANIFESTVERSION); checkParameter(Constants.BUNDLE_SYMBOLICNAME); checkParameter(Constants.IMPORT_PACKAGE); checkParameter(Constants.BUNDLE_CLASSPATH); return manifest; } private void checkParameter(String parameter) throws IOException { if (properties.containsKey(parameter)) { throw new IOException("Cannot override " + parameter + " header when converting a bundle"); } } protected Manifest updateManifest(Manifest manifest) throws IOException { // If for some reason no manifest was generated, we start our own so that we don't null pointer later on if (manifest == null) { manifest = new Manifest(); manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1"); } else { // remove digest attributes if was is signed signed = removeDigestAttributes(manifest); } // Compare the manifest and the supplied properties // // Web-ContextPath // String webCPath = properties.get(WEB_CONTEXT_PATH); if (webCPath == null) { throw new IOException(WEB_CONTEXT_PATH + " parameter is missing."); } properties.put(WEB_CONTEXT_PATH, addSlash(webCPath)); // // Bundle-Version // if (manifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION) == null && !properties.containsKey(Constants.BUNDLE_VERSION)) { properties.put(Constants.BUNDLE_VERSION, DEFAULT_BUNDLE_VERSION); } // // Bundle-ManifestVersion // String manifestVersion = properties.get(Constants.BUNDLE_MANIFESTVERSION); if (manifestVersion == null) { manifestVersion = manifest.getMainAttributes().getValue(Constants.BUNDLE_MANIFESTVERSION); if (manifestVersion == null) { manifestVersion = DEFAULT_BUNDLE_MANIFESTVERSION; } } else if (!manifestVersion.equals("2")) { throw new IOException("Unsupported bundle manifest version " + manifestVersion); } properties.put(Constants.BUNDLE_MANIFESTVERSION, manifestVersion); // // Bundle-ClassPath // ArrayList<String> classpath = new ArrayList<String>(); // Set initial entry into classpath classpath.add(INITIAL_CLASSPATH_ENTRY); // Add any files from the WEB-INF/lib directory + their dependencies classpath.addAll(classPath); // Get the list from the URL and add to classpath (removing duplicates) mergePathList(properties.get(Constants.BUNDLE_CLASSPATH), classpath, ","); // Get the existing list from the manifest file and add to classpath // (removing duplicates) mergePathList(manifest.getMainAttributes().getValue(Constants.BUNDLE_CLASSPATH), classpath, ","); // Construct the classpath string and set it into the properties StringBuffer classPathValue = new StringBuffer(); for (String entry : classpath) { classPathValue.append(","); classPathValue.append(entry); } if (!classpath.isEmpty()) { properties.put(Constants.BUNDLE_CLASSPATH, classPathValue.toString().substring(1)); } @SuppressWarnings("serial") ArrayList<String> packages = new ArrayList<String>() { @Override public boolean contains(Object elem) { // Check for exact match of export list if (super.contains(elem)) return true; if (!!!(elem instanceof String)) return false; String expPackageStmt = (String) elem; String expPackage = expPackageStmt.split("\\s*;\\s*")[0]; Pattern p = Pattern.compile("^\\s*"+Pattern.quote(expPackage)+"((;|\\s).*)?\\s*$"); for (String s : this) { Matcher m = p.matcher(s); if (m.matches()) { return true; } } return false; } }; // // Import-Package // packages.clear(); // Get the list from the URL and add to classpath (removing duplicates) mergePathList(properties.get(Constants.IMPORT_PACKAGE), packages, ","); // Get the existing list from the manifest file and add to classpath // (removing duplicates) mergePathList(manifest.getMainAttributes().getValue(Constants.IMPORT_PACKAGE), packages, ","); // Add the default set of packages mergePathList(DEFAULT_IMPORT_PACKAGE_LIST, packages, ","); // Analyse the bytecode of any .class files in the jar to find any other // required imports if (!!!importPackages.isEmpty()) { StringBuffer generatedImports = new StringBuffer(); for (String entry : importPackages) { generatedImports.append(','); generatedImports.append(entry); generatedImports.append(";resolution:=optional"); } mergePathList(generatedImports.substring(1), packages, ","); } // Construct the string and set it into the properties StringBuffer importValues = new StringBuffer(); for (String entry : packages) { importValues.append(","); importValues.append(entry); } if (!packages.isEmpty()) { properties.put(Constants.IMPORT_PACKAGE, importValues.toString().substring(1)); } // Take the properties map and add them to the manifest file for (Map.Entry<String, String> entry : properties.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); manifest.getMainAttributes().putValue(key, value); } // // Bundle-SymbolicName // if (manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME) == null) { manifest.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, warName + "_" + manifest.hashCode()); } return manifest; } private static String addSlash(String contextPath) { if (!contextPath.startsWith("/")) { contextPath = "/" + contextPath; } return contextPath; } // pathlist = A "delim" delimitted list of path entries private static void mergePathList(String pathlist, ArrayList<String> paths, String delim) { if (pathlist != null) { List<String> tokens = parseDelimitedString(pathlist, delim, true); for (String token : tokens) { if (!paths.contains(token)) { paths.add(token); } } } } private static List<String> parseDelimitedString(String value, String delim, boolean includeQuotes) { if (value == null) { value = ""; } List<String> list = new ArrayList<String>(); int CHAR = 1; int DELIMITER = 2; int STARTQUOTE = 4; int ENDQUOTE = 8; StringBuffer sb = new StringBuffer(); int expecting = (CHAR | DELIMITER | STARTQUOTE); for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); boolean isDelimiter = (delim.indexOf(c) >= 0); boolean isQuote = (c == '"'); if (isDelimiter && ((expecting & DELIMITER) > 0)) { list.add(sb.toString().trim()); sb.delete(0, sb.length()); expecting = (CHAR | DELIMITER | STARTQUOTE); } else if (isQuote && ((expecting & STARTQUOTE) > 0)) { if (includeQuotes) { sb.append(c); } expecting = CHAR | ENDQUOTE; } else if (isQuote && ((expecting & ENDQUOTE) > 0)) { if (includeQuotes) { sb.append(c); } expecting = (CHAR | STARTQUOTE | DELIMITER); } else if ((expecting & CHAR) > 0) { sb.append(c); } else { throw new IllegalArgumentException("Invalid delimited string: " + value); } } if (sb.length() > 0) { list.add(sb.toString().trim()); } return list; } private static boolean removeDigestAttributes(Manifest manifest) { boolean foundDigestAttribute = false; for (Map.Entry<String, Attributes> entry : manifest.getEntries().entrySet()) { Attributes attributes = entry.getValue(); for (Object attributeName : attributes.keySet()) { String name = ((Attributes.Name) attributeName).toString(); name = name.toLowerCase(); if (name.endsWith("-digest") || name.contains("-digest-")) { attributes.remove(attributeName); foundDigestAttribute = true; } } } return foundDigestAttribute; } private static boolean isSignatureFile(String entryName) { String[] parts = entryName.split("/"); if (parts.length == 2) { String name = parts[1].toLowerCase(); return (parts[0].equals("META-INF") && (name.endsWith(".sf") || name.endsWith(".dsa") || name.endsWith(".rsa") || name.startsWith("sig-"))); } else { return false; } } public InputStream getWAB() throws IOException { convert(); return wab.getInputStream(); } public Manifest getWABManifest() throws IOException { generateManifest(); return wabManifest; } public int getWabLength() throws IOException { convert(); return wab.size(); } }