/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
This program 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.util;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
/**
* @author jblok
*/
@SuppressWarnings("nls")
public abstract class JarManager
{
public static final String JAVA_BEAN_ATTRIBUTE = "Java-Bean";
public static final String SERVOY_PLUGIN_ATTRIBUTE = "Servoy-Plugin";
public static void addCommonPackageToDefinitions(Extension< ? >[] extensions, Map<String, List<ExtensionResource>> packageJarMapping)
{
ExtensionResource lastJar = null;
List<String> workingClassNames = new ArrayList<String>();
//group by jarFileName
for (Extension< ? > ext : extensions)
{
if (lastJar != null && !ext.jar.jarFileName.equals(lastJar))
{
addCommonPackageToDefinitions(lastJar, workingClassNames, packageJarMapping);
workingClassNames = new ArrayList<String>();
}
workingClassNames.add(ext.instanceClass.getName());
lastJar = ext.jar;
}
if (workingClassNames.size() != 0)
{
addCommonPackageToDefinitions(lastJar, workingClassNames, packageJarMapping);
}
}
protected static void addCommonPackageToDefinitions(ExtensionResource ext, List<String> workingClassNames,
Map<String, List<ExtensionResource>> packageJarMapping)
{
boolean matched = true;
int wantedLength = 3;
do
{
matched = true;
String commonPart = null;
Iterator<String> it = workingClassNames.iterator();
while (it.hasNext())
{
String className = it.next();
int count = 0;
StringBuffer nameToCheck = new StringBuffer();
StringTokenizer tokenizer = new StringTokenizer(className, "."); //$NON-NLS-1$
while (tokenizer.hasMoreTokens() && count != wantedLength)
{
if (count != 0) nameToCheck.append("."); //$NON-NLS-1$
nameToCheck.append(tokenizer.nextToken());
count++;
}
if (commonPart == null)
{
commonPart = nameToCheck.toString();
}
else if (!nameToCheck.toString().equals(commonPart))
{
matched = false;
}
}
if (!matched)
{
wantedLength--;
}
else
{
List<ExtensionResource> prev = packageJarMapping.get(commonPart);
if (prev != null)
{
prev.add(ext);
}
else
{
List<ExtensionResource> exts = new ArrayList<ExtensionResource>(1);
exts.add(ext);
packageJarMapping.put(commonPart, exts);
}
break;
}
}
while (!matched && wantedLength > 0);
}
//searchURLs can be subset of all urls in classloader to speedup loading
@SuppressWarnings("unchecked")
public <C> Class<C>[] getAssignableClasses(ExtendableURLClassLoader loader, Class<C> type, List<ExtensionResource> searchURLs)
{
List<Class< ? >> classes = new ArrayList<Class< ? >>();
Extension<C>[] exts = getExtensions(loader, type, searchURLs);
for (Extension<C> element : exts)
{
classes.add(element.instanceClass);
}
return classes.toArray(new Class[classes.size()]);
}
/**
* An extension represents one archive
*/
public static class ExtensionResource
{
public final URL jarUrl;
public final String jarFileName;
public final long jarFileModTime;
public boolean hasClasses = true;
public boolean refersToBeans = false;
public List<ExtensionResource> libs;
public ExtensionResource(URL url, String fileName, long lastModified)
{
if (url == null) throw new IllegalArgumentException("Extension cannot accept null url");
jarUrl = url;
if (fileName == null)
{
String name = jarUrl.getFile();
int index = name.lastIndexOf('/');
if (index != -1)
{
name = name.substring(index + 1);
}
jarFileName = name;
}
else
{
jarFileName = fileName;
}
jarFileModTime = lastModified;
}
public ExtensionResource(URL jarUrl, long lastModified)
{
this(jarUrl, null, lastModified);
}
@Override
public String toString()
{
return "ExtensionResource[" + jarFileName + '=' + jarUrl + ']'; //$NON-NLS-1$
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + (int)(jarFileModTime ^ (jarFileModTime >>> 32));
result = prime * result + ((jarUrl == null) ? 0 : jarUrl.hashCode());
return result;
}
@Override
public boolean equals(Object obj)
{
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
ExtensionResource other = (ExtensionResource)obj;
if (jarFileModTime != other.jarFileModTime) return false;
if (jarUrl == null)
{
if (other.jarUrl != null) return false;
}
else if (!jarUrl.equals(other.jarUrl)) return false;
return true;
}
}
public static class Extension<T>
{
public final ExtensionResource jar;
public final Class<T> searchType;
public final Class<T> instanceClass;
public Extension(ExtensionResource jar, Class<T> cls, Class<T> searchType)
{
this.jar = jar;
instanceClass = cls;
this.searchType = searchType;
}
@Override
public String toString()
{
return "Extension[" + instanceClass.getName() + "=searchtype:" + searchType.getName() + ']'; //$NON-NLS-1$
}
}
//searchURLs: url->pair(jarname,jarmodtime)
//loader must know all the urls already
public <C> Extension<C>[] getExtensions(ExtendableURLClassLoader loader, Class<C> searchType, List<ExtensionResource> searchURLs)
{
List<Extension<C>> extensions = new ArrayList<Extension<C>>();
Iterator<ExtensionResource> it = searchURLs.iterator();
while (it.hasNext())
{
ExtensionResource entry = it.next();
URL url = entry.jarUrl;
InputStream is = null;
boolean tryWithFiles = false;
try
{
is = url.openStream();
if (is != null)
{
final ZipInputStream zip = new ZipInputStream(is);
readZipEntries(loader, searchType, extensions, entry, new Enumeration<ZipEntry>()
{
private ZipEntry nextEntry = zip.getNextEntry();
public boolean hasMoreElements()
{
return nextEntry != null;
}
public ZipEntry nextElement()
{
ZipEntry retValue = nextEntry;
try
{
nextEntry = zip.getNextEntry();
}
catch (IOException e)
{
nextEntry = null;
}
return retValue;
}
});
zip.close();
}
else tryWithFiles = true;
}
catch (IOException e1)
{
Debug.trace(e1);
tryWithFiles = true;
}
finally
{
Utils.closeInputStream(is);
}
if (tryWithFiles)
{
File file = null;
try
{
file = new File(new URI(url.toExternalForm()));
}
catch (Exception e)
{
Debug.error("Error occured trying to load: " + url + ", error: " + e.getMessage()); //$NON-NLS-1$ //$NON-NLS-2$
continue;
}
if (!file.isDirectory() && file.exists() && file.canRead())
{
ZipFile zipFile = null;
try
{
zipFile = new ZipFile(file);
}
catch (Exception ex)
{
Debug.error("Error occured trying to load: " + file.getAbsolutePath() + ", error: " + ex.getMessage()); //$NON-NLS-1$ //$NON-NLS-2$
continue;
}
Enumeration< ? extends ZipEntry> enumeration = zipFile.entries();
readZipEntries(loader, searchType, extensions, entry, enumeration);
}
}
}
return extensions.toArray(new Extension[extensions.size()]);
}
/**
* @param loader
* @param searchType
* @param extensions
* @param entry
* @param url
* @param enumeration
*/
private <C> void readZipEntries(ExtendableURLClassLoader loader, Class<C> searchType, List<Extension<C>> extensions, ExtensionResource entry,
Enumeration< ? extends ZipEntry> enumeration)
{
boolean seenClass = false;
Class< ? > cls = null;
while (enumeration.hasMoreElements())
{
ZipEntry ze = enumeration.nextElement();
if (!ze.isDirectory())
{
String entryName = ze.getName();
if (!seenClass && entryName.endsWith(".class")) seenClass = true;
String className = Utils.changeFileNameToClassName(entryName);
if (className != null)
{
try
{
cls = loader.loadClass(className);
if (searchType.isAssignableFrom(cls) && !Modifier.isAbstract(cls.getModifiers()))
{
Extension<C> ei = new Extension<C>(entry, (Class<C>)cls, searchType);
extensions.add(ei);
}
}
catch (Throwable th)
{
if (th instanceof SecurityException)
{
Debug.warn(th.toString());
}
else
{
Debug.trace(th.toString());
}
}
}
}
}
entry.hasClasses = seenClass;
}
protected List<ExtensionResource> loadLibs(File dir)
{
List<ExtensionResource> retval = new ArrayList<ExtensionResource>();
readDir(dir, retval, null, null, false);
return retval;
}
protected List<String> readDir(File dir, List<ExtensionResource> baseRetval, List<ExtensionResource> subDirRetval,
Map<String, List<ExtensionResource>> packageJarMapping, boolean isSubDir)
{
List<String> foundBeanClassNames = new ArrayList<String>();
if (dir != null && dir.isDirectory())
{
String[] filesa = dir.list();
List<String> files = Arrays.asList(filesa);
//make sure we have a predefined load order (== alphabetically)
Collections.sort(files);
Iterator<String> it = files.iterator();
while (it.hasNext())
{
String fileName = it.next();
if (fileName.toLowerCase().endsWith(".zip") || fileName.toLowerCase().endsWith(".jar")) //$NON-NLS-1$ //$NON-NLS-2$
{
try
{
File jarFile = new File(dir, fileName);
if (isSubDir)
{
ExtensionResource ext = new ExtensionResource(jarFile.toURI().toURL(), fileName, jarFile.lastModified());
if (!subDirRetval.contains(ext)) subDirRetval.add(ext);
}
else
{
ExtensionResource ext = new ExtensionResource(jarFile.toURI().toURL(), fileName, jarFile.lastModified());
baseRetval.add(ext);
if (packageJarMapping != null)
{
JarFile file = new JarFile(jarFile);
Manifest mf = file.getManifest();
if (mf != null)
{
List<String> beanClassNames = getClassNamesForKey(mf, JAVA_BEAN_ATTRIBUTE);
if (beanClassNames.size() > 0)
{
ext.refersToBeans = true;
if (ext.libs == null) ext.libs = new ArrayList<ExtensionResource>();
addCommonPackageToDefinitions(ext, beanClassNames, packageJarMapping);
foundBeanClassNames.addAll(beanClassNames);
Map<String, File> classPathReferences = getManifestClassPath(jarFile, dir);
if (classPathReferences != null && classPathReferences.size() > 0)
{
for (String reference : classPathReferences.keySet())
{
File f = classPathReferences.get(reference);
if (f != null)
{
ExtensionResource ref = new ExtensionResource(f.toURI().toURL(), reference, f.lastModified());
if (!ext.libs.contains(ref)) ext.libs.add(ref);
addCommonPackageToDefinitions(ref, beanClassNames, packageJarMapping);
if (!subDirRetval.contains(ref)) subDirRetval.add(ref);
}
}
}
}
else
{
List<ExtensionResource> exts = new ArrayList<ExtensionResource>(1);
exts.add(ext);
packageJarMapping.put("#" + packageJarMapping.size(), exts);//is likly jar for applet //$NON-NLS-1$
}
}
}
}
}
catch (IOException ex)
{
Debug.error("Unable to load lib: " + fileName + " error: " + ex.getMessage()); //$NON-NLS-1$ //$NON-NLS-2$
Debug.error(ex);
}
}
else
{
if (subDirRetval != null)
{
File test = new File(dir, fileName);
if (test.isDirectory())
{
foundBeanClassNames.addAll(readDir(test, baseRetval, subDirRetval, packageJarMapping, true));//we don't add subdirs plugins to the jnlp list to prevent clutter
}
}
}
}
}
return foundBeanClassNames;
}
public static List<String> getManifestClassPath(URL jarUrl)
{
ArrayList<String> lst = new ArrayList<String>();
JarInputStream jis = null;
try
{
InputStream is = (InputStream)jarUrl.getContent(new Class[] { InputStream.class });
if (is != null)
{
jis = new JarInputStream(is, false);
Manifest mf = jis.getManifest();
String classpath = mf.getMainAttributes().getValue(Attributes.Name.CLASS_PATH);
if (classpath != null)
{
StringTokenizer st = new StringTokenizer(classpath, " "); //$NON-NLS-1$
while (st.hasMoreTokens())
{
lst.add(st.nextToken());
}
}
}
}
catch (Exception e)
{
Debug.error(e);
}
finally
{
Utils.closeInputStream(jis);
}
return lst;
}
private static Map<String, File> getManifestClassPath(File jarFile, File contextDir)
{
Map<String, File> references = new HashMap<String, File>();
try
{
JarFile file = new JarFile(jarFile);
Manifest mf = file.getManifest();
if (mf != null)
{
String classpath = (String)mf.getMainAttributes().get(Attributes.Name.CLASS_PATH);
if (classpath != null)
{
StringTokenizer st = new StringTokenizer(classpath, " "); //$NON-NLS-1$
while (st.hasMoreTokens())
{
String classPathJar = st.nextToken();
File classPathFile = null;
if (classPathJar.startsWith("/")) //$NON-NLS-1$
{
classPathFile = new File(contextDir.getParentFile(), classPathJar);
}
else
{
classPathFile = new File(contextDir, classPathJar);
}
if (classPathFile.exists())
{
references.put(classPathJar, classPathFile);
}
else
{
Debug.log("Classpath entry: " + classPathJar + " of jar: " + jarFile.getAbsolutePath() + " not found"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}
}
}
}
catch (Exception ex)
{
Debug.error(ex);
}
return references;
}
public static List<String> getClassNamesForKey(Manifest mf, String attributeName)
{
HashMap<String, Boolean> beans = new HashMap<String, Boolean>();
Map<String, Attributes> entries = mf.getEntries();
Iterator<String> it = entries.keySet().iterator();
if (!it.hasNext())
{
checkIfHasAttribute(mf.getMainAttributes(), attributeName, null, beans);
}
while (it.hasNext())
{
String key = it.next();
Attributes attr = entries.get(key);
checkIfHasAttribute(attr, attributeName, key, beans);
}
ArrayList<String> beanNames = new ArrayList<String>();
Iterator<String> it2 = beans.keySet().iterator();
while (it2.hasNext())
{
String key = it2.next();
beanNames.add(key);
}
return beanNames;
}
private static void checkIfHasAttribute(Attributes attr, String attributeName, String key, Map<String, Boolean> beans)
{
if (attr == null) return;
String name = key;
if (name == null) name = (String)attr.get(new Attributes.Name("Name")); //$NON-NLS-1$
String isBean = (String)attr.get(new Attributes.Name(attributeName));
if (name != null && isBean != null && isBean.equalsIgnoreCase("True")) //$NON-NLS-1$
{
String beanName;
boolean fromPrototype = true;
if (name.endsWith(".class")) //$NON-NLS-1$
{
fromPrototype = false;
beanName = name.substring(0, name.length() - 6);
}
else if (name.endsWith(".ser")) //$NON-NLS-1$
{
beanName = name.substring(0, name.length() - 4);
}
else
{
beanName = name;
}
beanName = beanName.replace('/', '.');
beans.put(beanName, new Boolean(fromPrototype));
}
}
public static URL[] getUrls(List<ExtensionResource> exts)
{
List<URL> allUrls = new ArrayList<URL>(exts.size());
for (ExtensionResource ext : exts)
{
allUrls.add(ext.jarUrl);
}
return allUrls.toArray(new URL[allUrls.size()]);
}
public static List<ExtensionResource> getExtensions(Map<String, List<ExtensionResource>> definitions, String filename)
{
String jarFileName = filename;
int index = jarFileName.lastIndexOf('/');
if (index != -1)
{
jarFileName = jarFileName.substring(index + 1);
}
Iterator<List<ExtensionResource>> it = definitions.values().iterator();
while (it.hasNext())
{
List<ExtensionResource> exts = it.next();
for (ExtensionResource ext : exts)
{
String name = ext.jarFileName;
index = name.lastIndexOf('/');
if (index == -1) index = 0;
else index++;
name = name.substring(index);
if (jarFileName.equals(name))
{
return exts;
}
}
}
return null;
}
}