/** * Copyright (c) 2015, Lucee Assosication Switzerland. All rights reserved. * * This library 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 library 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 library. If not, see <http://www.gnu.org/licenses/>. * */ package lucee.runtime.osgi; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import lucee.commons.io.CharsetUtil; import lucee.commons.io.IOUtil; import lucee.commons.io.SystemUtil; import lucee.commons.io.res.Resource; import lucee.commons.lang.StringUtil; import lucee.loader.util.Util; import lucee.runtime.exp.ApplicationException; import lucee.runtime.type.util.ArrayUtil; import lucee.runtime.type.util.ListUtil; import org.osgi.framework.BundleException; import org.osgi.framework.Version; public class BundleBuilderFactory { //Indicates the OSGi specification to use for reading this bundle. public static final int MANIFEST_VERSION=2; private static final Set<String> INDIVIDUAL_FILTER = new HashSet<String>(); private static final Set<String> MAIN_FILTER = new HashSet<String>(); static { MAIN_FILTER.add("SHA1-Digest-Manifest"); MAIN_FILTER.add("MD5-Digest-Manifest"); //MAIN_FILTER.add("Sealed"); INDIVIDUAL_FILTER.add("SHA1-Digest"); INDIVIDUAL_FILTER.add("MD5-Digest"); //INDIVIDUAL_FILTER.add("Sealed"); } private String name; private final String symbolicName; private String description; private Manifest manifest; private Set<String> existingPackages=new HashSet<String>(); private boolean ignoreExistingManifest=false; private String activator; //private List<Resource> jars=new ArrayList<Resource>(); private List<String> exportPackage; private List<String> fragmentHost; private List<String> importPackage; private List<String> requireBundle; private List<String> requireBundleFragment; private List<String> dynImportPackage; private List<String> classPath; //private BundleFile bf; private Resource jar; private Version version; private String bundleActivationPolicy; /** * * @param symbolicName this entry specifies a unique identifier for a bundle, based on the reverse domain name convention (used also by the java packages). * @param name Defines a human-readable name for this bundle, Simply assigns a short name to the bundle. * @param description A description of the bundle's functionality. * @param version Designates a version number to the bundle. * @param activator Indicates the class name to be invoked once a bundle is activated. * @param name * @throws IOException * @throws BundleException * @throws BundleBuilderFactoryException */ public BundleBuilderFactory(Resource jar, String symbolicName) throws IOException, BundleException { if(!jar.isFile())throw new IOException("["+jar+"] is not a file"); this.jar=jar; //bf = new BundleFile(jar); if(StringUtil.isEmpty(symbolicName)) { //if(StringUtil.isEmpty(name)) throw new BundleException("symbolic name is reqired"); } this.symbolicName=toSymbolicName(symbolicName); } public BundleBuilderFactory(Resource jar) throws ApplicationException { if(!jar.isFile())throw new ApplicationException("["+jar+"] is not a file"); this.jar=jar; this.symbolicName=createSymbolicName(jar); } public static String createSymbolicName(Resource jar) { String name=jar.getName(); int index=name.lastIndexOf('.'); if(index!=-1) { name=name.substring(0,index); } return toSymbolicName(name); } public void setName(String name) { this.name=name;; } public void setIgnoreExistingManifest(boolean ignoreExistingManifest) { this.ignoreExistingManifest=ignoreExistingManifest;; } public Version getVersion() { return version; } public void setVersion(String version) throws BundleException { if(StringUtil.isEmpty(version,true))return ; this.version=OSGiUtil.toVersion(version); } public void setVersion(Version version) { if(version==null)return ; this.version=version; } private static String toSymbolicName(String name) { name=name.replace(' ', '.'); name=name.replace('_', '.'); name=name.replace('-', '.'); return name; } public List<String> getExportPackage() { return exportPackage; } public void addExportPackage(String strExportPackage) { if(StringUtil.isEmpty(strExportPackage)) return; if(exportPackage==null)exportPackage=new ArrayList<String>(); addPackages(exportPackage,strExportPackage); } public List<String> getRequireBundle() { return requireBundle; } public void addRequireBundle(String strRequireBundle) { if(StringUtil.isEmpty(strRequireBundle)) return; if(requireBundle==null) requireBundle=new ArrayList<String>(); addPackages(requireBundle,strRequireBundle); } public List<String> getRequireBundleFragment() { return requireBundleFragment; } public void addRequireBundleFragment(String strRequireBundleFragment) { if(StringUtil.isEmpty(strRequireBundleFragment)) return; if(requireBundleFragment==null) requireBundleFragment=new ArrayList<String>(); addPackages(requireBundleFragment,strRequireBundleFragment); } public List<String> getFragmentHost() { return fragmentHost; } public void addFragmentHost(String strExportPackage) { if(StringUtil.isEmpty(strExportPackage)) return; if(fragmentHost==null)fragmentHost=new ArrayList<String>(); addPackages(fragmentHost,strExportPackage); } public void setBundleActivationPolicy(String bundleActivationPolicy) { this.bundleActivationPolicy=bundleActivationPolicy; } private static void addPackages(Collection<String> packages, String str) { StringTokenizer st=new StringTokenizer(str,","); while(st.hasMoreTokens()){ packages.add(st.nextToken().trim()); } } public List<String> getImportPackage() { return importPackage; } public List<String> getDynamicImportPackage() { return dynImportPackage; } public void addImportPackage(String strImportPackage) { if(StringUtil.isEmpty(strImportPackage)) return; if(importPackage==null)importPackage=new ArrayList<String>(); addPackages(importPackage,strImportPackage); } public void addDynamicImportPackage(String strDynImportPackage) { if(StringUtil.isEmpty(strDynImportPackage)) return; if(dynImportPackage==null)dynImportPackage=new ArrayList<String>(); addPackages(dynImportPackage,strDynImportPackage); } public List<String> getClassPath() { return classPath; } public void addClassPath(String str) { if(classPath==null)classPath=new ArrayList<String>(); addPackages(classPath,str); } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getActivator() { return activator; } public void setActivator(String activator) { this.activator = activator; } private void extendManifest(Manifest mf){ Attributes attrs = mf.getMainAttributes(); attrs.putValue("Bundle-ManifestVersion", ""+MANIFEST_VERSION); if(!StringUtil.isEmpty(name)) attrs.putValue("Bundle-Name",name); attrs.putValue("Bundle-SymbolicName",symbolicName); if(!StringUtil.isEmpty(description))attrs.putValue("Bundle-Description",description); if(!StringUtil.isEmpty(bundleActivationPolicy)) attrs.putValue("Bundle-ActivationPolicy",bundleActivationPolicy); if(version!=null) attrs.putValue("Bundle-Version",version.toString()); if(!StringUtil.isEmpty(activator)) { if(!activator.equalsIgnoreCase("none")) { attrs.putValue("Bundle-Activator",activator); addImportPackage("org.osgi.framework"); } else { //attrs.remove("Bundle-Activator"); attrs.putValue("Bundle-Activator",""); } } // Export-Package String str = ignoreExistingManifest?null:attrs.getValue("Export-Package"); // no existing Export-Package Set<String> set; if(Util.isEmpty(str,true)) { set=existingPackages; } else { set=new HashSet<String>(); addPackages(set, str); } if(!ArrayUtil.isEmpty(exportPackage) && !isAsterix(exportPackage)) { Iterator<String> it = exportPackage.iterator(); while(it.hasNext()) { set.add(it.next()); } } exportPackage=ListUtil.toList(set); addList(attrs,"Export-Package",exportPackage); // Require-Bundle str = attrs.getValue("Require-Bundle"); if(Util.isEmpty(str,true)) addList(attrs,"Require-Bundle",requireBundle); // Require-Bundle str = attrs.getValue("Require-Bundle-Fragment"); if(Util.isEmpty(str,true)) addList(attrs,"Require-Bundle-Fragment",requireBundleFragment); //str = attrs.getValue("Fragment-Host"); //if(Util.isEmpty(str,true)) attrs.remove("Fragment-Host"); addList(attrs,"Fragment-Host",fragmentHost); str = attrs.getValue("Import-Package"); if(Util.isEmpty(str,true)) addList(attrs,"Import-Package",importPackage); str = attrs.getValue("DynamicImport-Package"); if(Util.isEmpty(str,true)) addList(attrs,"DynamicImport-Package",dynImportPackage); str = attrs.getValue("Bundle-ClassPath"); if(Util.isEmpty(str,true)) addList(attrs,"Bundle-ClassPath",classPath); } /*private static List<String> createExportPackageFromResource(Resource jar) { // get all directories List<Resource> dirs = ResourceUtil.listRecursive(jar,DirectoryResourceFilter.FILTER); List<String> rtn=new ArrayList<String>(); // remove directories with no files (of any kind) Iterator<Resource> it = dirs.iterator(); Resource[] children; int count; while(it.hasNext()) { Resource r = it.next(); children = r.listResources(); count=0; if(children!=null)for(int i=0;i<children.length;i++){ if(children[i].isFile())count++; } // has files if(count>0) { } } return null; }*/ private boolean isAsterix(List<String> list) { if(list==null) return false; Iterator<String> it = list.iterator(); while(it.hasNext()){ if("*".equals(it.next())) return true; } return false; } private void addList(Attributes attrs,String name,List<String> values) { if(values==null || values.isEmpty()) return; StringBuilder sb=new StringBuilder(); Iterator<String> it = values.iterator(); boolean first=true; while(it.hasNext()) { if(!first) { sb.append(','); } sb.append(it.next()); first=false; } attrs.putValue(name, sb.toString()); } public void build() throws IOException { Resource res = SystemUtil.getTempFile(".jar", false); try{ build(res); IOUtil.copy(res, jar); } finally { res.delete(); } } public void build(Resource target) throws IOException { OutputStream os = target.getOutputStream(); try{ build(os); } finally { IOUtil.closeEL(os); } } public void build(OutputStream os) throws IOException { ZipOutputStream zos=new MyZipOutputStream(os,CharsetUtil.UTF8); try{ // jar handleEntry(zos,jar, new JarEntryListener(zos)); // Manifest (do a blank one when method above has not loaded one) if(manifest==null)manifest=new Manifest(); extendManifest(manifest); String mf = ManifestUtil.toString(manifest,128,MAIN_FILTER,INDIVIDUAL_FILTER); InputStream is=new ByteArrayInputStream(mf.getBytes(CharsetUtil.UTF8)); ZipEntry ze=new ZipEntry("META-INF/MANIFEST.MF"); zos.putNextEntry(ze); try { copy(is,zos); } finally { IOUtil.closeEL(is); zos.closeEntry(); } } finally { IOUtil.closeEL(zos); } } private void handleEntry(ZipOutputStream target, Resource file, EntryListener listener) throws IOException { ZipInputStream zis = new ZipInputStream(file.getInputStream()); try{ ZipEntry entry; while((entry=zis.getNextEntry())!=null){ listener.handleEntry(file,zis,entry); zis.closeEntry(); } } finally { IOUtil.closeEL(zis); } } class JarEntryListener implements EntryListener { private ZipOutputStream zos; public JarEntryListener(ZipOutputStream zos) { this.zos=zos; } @Override public void handleEntry(Resource zipFile,ZipInputStream source,ZipEntry entry) throws IOException { // log for export-package if(!entry.isDirectory()) { String name=entry.getName(); int index=name.lastIndexOf('/'); if(index!=-1 && !name.startsWith("META-INF")) { name=name.substring(0,index); if(name.length()>0) existingPackages.add(ListUtil.trim(name.replace('/', '.'), ".")); } } // security if("META-INF/IDRSIG.DSA".equalsIgnoreCase(entry.getName()) || "META-INF/IDRSIG.SF".equalsIgnoreCase(entry.getName()) || "META-INF/INDEX.LIST".equalsIgnoreCase(entry.getName())) { return; } // manifest if("META-INF/MANIFEST.MF".equalsIgnoreCase(entry.getName())) { if(!ignoreExistingManifest) { manifest = new Manifest(source); Attributes attrs = manifest.getMainAttributes(); // they are in bootdelegation //ManifestUtil.removeFromList(attrs,"Import-Package","javax.*"); ManifestUtil.removeOptional(attrs,"Import-Package"); //ManifestUtil.removeFromList(attrs,"Import-Package","org.osgi.*"); } return; } // ignore the following stuff if(entry.getName().endsWith(".DS_Store")|| entry.getName().startsWith("__MACOSX")) { return; } MyZipEntry ze=new MyZipEntry(entry.getName()); ze.setComment(entry.getComment()); ze.setTime(entry.getTime()); ze.setFile(zipFile); try { zos.putNextEntry(ze); } catch(NameAlreadyExistsException naee){ if(entry.isDirectory()) { return; } log("--------------------------------"); log(ze.getName()); log("before:"+naee.getFile()); log("curren:"+zipFile); log("size:"+naee.getSize()+"=="+entry.getSize()); return; // TODO throw naee; } try { copy(source,zos); } finally { zos.closeEntry(); } } } public interface EntryListener { public void handleEntry(Resource zipFile, ZipInputStream source, ZipEntry entry) throws IOException; } public class MyZipOutputStream extends ZipOutputStream { private Map<String,Resource> names=new HashMap<String,Resource>(); public MyZipOutputStream(OutputStream out,Charset charset) { super(out); } @Override public void putNextEntry(ZipEntry e) throws IOException { Resource file = names.get(e.getName()); if(names.containsKey(e.getName())) throw new NameAlreadyExistsException(e.getName(),file,e.getSize()); if(e instanceof MyZipEntry)names.put(e.getName(),((MyZipEntry)e).getFile()); super.putNextEntry(e); } } public class MyZipEntry extends ZipEntry { private Resource file; public MyZipEntry(String name) { super(name); } public void setFile(Resource file) { this.file=file; } public MyZipEntry(ZipEntry e) { super(e); } public Resource getFile(){ return file; } } private final static void copy(InputStream in, OutputStream out) throws IOException { byte[] buffer = new byte[0xffff]; int len; while((len = in.read(buffer)) !=-1) out.write(buffer, 0, len); } public void log(String str) { System.out.println(str); } }