content: /*
   * 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.List;
  import java.util.Map;
  import java.util.Properties;
  import java.util.Set;
  import java.util.TreeSet;
  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();
    }
    
  }
  