package com.github.dynamicextensionsalfresco.osgi; import com.springsource.util.osgi.manifest.BundleManifest; import com.springsource.util.osgi.manifest.ExportedPackage; import com.springsource.util.osgi.manifest.internal.StandardBundleManifest; import com.springsource.util.osgi.manifest.parse.BundleManifestParseException; import com.springsource.util.osgi.manifest.parse.DummyParserLogger; import org.alfresco.service.descriptor.DescriptorService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.Resource; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.util.Assert; import org.springframework.web.context.ServletContextAware; import org.springframework.web.context.support.ServletContextResourcePatternResolver; import javax.servlet.ServletContext; import java.io.File; import java.io.IOException; import java.util.*; import java.util.Map.Entry; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.zip.ZipEntry; import static com.github.dynamicextensionsalfresco.osgi.ManifestUtils.getImplementationVersion; import static java.util.Arrays.asList; /** * Provides operations for discovering Java packages. * * @author Laurens Fridael * */ public class JavaPackageScanner implements ServletContextAware { private static final String OSGI_PACKAGE = "org.osgi"; private static final String FELIX_PACKAGE = "org.apache.felix"; private static final Collection<String> frameworkPackages = asList(OSGI_PACKAGE, FELIX_PACKAGE); private final Logger logger = LoggerFactory.getLogger(getClass()); /* Dependencies */ private ServletContextResourcePatternResolver resourcePatternResolver; private ServletContext servletContext; private DescriptorService descriptorService; /* Main operations */ /** * * Obtains the Java packages by scanning JARs in /WEB-INF/lib. * <p> * This implementation uses a {@link MetadataReader} to obtain class information, without actually loading the * classses into the VM. * <p> * Note: calling this for a second time during Framework restart will currently fail. */ public Set<SystemPackage> scanWebApplicationPackages() { if (resourcePatternResolver == null) { if (logger.isWarnEnabled()) { logger.warn("ResourcePatternResolver was not configured. This is normal during a unit test."); } return Collections.emptySet(); } logger.debug("Scanning for Java packages."); final long before = System.currentTimeMillis(); final Set<SystemPackage> systemPackages = new LinkedHashSet<SystemPackage>(4500, 0.1f); try { for (final Resource jarResource : resourcePatternResolver.getResources("/WEB-INF/lib/*.jar")) { final String jarPath = servletContext.getRealPath(String.format("/WEB-INF/lib/%s", jarResource.getFilename())); final JarFile jarFile = new JarFile(jarPath); final Set<SystemPackage> exportPackages = scanBundleExportPackages(jarFile); if (exportPackages != null) { systemPackages.addAll(exportPackages); } else { final Set<SystemPackage> javaPackages = scanJavaPackages(jarFile); systemPackages.addAll(javaPackages); } } final long after = System.currentTimeMillis(); if (logger.isInfoEnabled()) { logger.info("Found {} Java packages. Time taken: {}ms.", systemPackages.size(), (after - before)); } } catch (final IOException e) { if (logger.isWarnEnabled()) { logger.warn("Error scanning Java packages. This will cause Dynamic Extensions framework not to work!", e.getMessage(), e); } } return systemPackages; } protected boolean isFrameworkPackage(final String packageName) { for (final String frameworkPackage : frameworkPackages) { if (packageName.startsWith(frameworkPackage)) { return true; } } return false; } /** * Scans the given {@link JarFile} for an OSGi-compliant manifest and uses the 'Export-Package' header to determine * the {@link SystemPackage}s. * * @param jarFile * @return * @throws IOException */ protected Set<SystemPackage> scanBundleExportPackages(final JarFile jarFile) throws IOException { Set<SystemPackage> exportPackages = null; final Manifest manifest = jarFile.getManifest(); if (manifest != null) { final Map<String, String> contents = convertAttributesToMap(manifest.getMainAttributes()); try { final BundleManifest bundleManifest = new StandardBundleManifest(new DummyParserLogger(), contents); if (bundleManifest.getBundleName() != null) { final List<ExportedPackage> exportedPackages = bundleManifest.getExportPackage().getExportedPackages(); exportPackages = new LinkedHashSet<SystemPackage>(exportedPackages.size()); for (final ExportedPackage exportedPackage : exportedPackages) { final SystemPackage exportPackage = new SystemPackage(exportedPackage.getPackageName(), exportedPackage.getVersion().toString()); exportPackages.add(exportPackage); } } } catch (BundleManifestParseException e) { if (logger.isDebugEnabled()) { logger.debug(String.format("Failed to parse manifest for %s, reverting to content scanning.", jarFile), e); } } } return exportPackages; } protected Map<String, String> convertAttributesToMap(final Attributes attributes) { final Map<String, String> map = new HashMap<String, String>(); for (final Entry<Object, Object> entry : attributes.entrySet()) { final String key = entry.getKey().toString(); map.put(key, entry.getValue() != null ? entry.getValue().toString() : null); } return map; } /** * Scans the given {@link JarFile} for Java packages, using the 'Implementation-Version' header from the JAR * manifest to determine the {@link SystemPackage} version. * <p> * For 'org.alfresco' packages the implementation uses the {@link DescriptorService} to determine the platform * version. * * @param jarFile * @return * @throws IOException */ protected Set<SystemPackage> scanJavaPackages(final JarFile jarFile) throws IOException { final String implementationVersion = getImplementationVersion(jarFile); final Set<SystemPackage> systemPackages = new LinkedHashSet<SystemPackage>(30); final Set<String> foundPackages = new HashSet<String>(30, 0.1f); for (final Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) { final JarEntry jarEntry = entries.nextElement(); if (isJavaClassInPackage(jarEntry) == false) { continue; } final String name = jarEntry.getName(); final String packageName = name.substring(0, name.lastIndexOf('/')).replace('/', '.'); if (isFrameworkPackage(packageName)) { continue; } if (foundPackages.contains(packageName)) { continue; } if (isAlfrescoPackage(packageName)) { final String alfrescoVersion = descriptorService.getServerDescriptor().getVersionNumber().toString(); systemPackages.add(new SystemPackage(packageName, alfrescoVersion)); } else { systemPackages.add(new SystemPackage(packageName, implementationVersion)); } foundPackages.add(packageName); } return systemPackages; } /** * Tests if the given {@link ZipEntry} represents a Java class in a package. (I.e. one that is not in the default * package.) * * @param jarEntry * @return */ protected boolean isJavaClassInPackage(final JarEntry jarEntry) { final String name = jarEntry.getName(); return name.lastIndexOf('/') > -1 && name.endsWith(".class"); } protected boolean isAlfrescoPackage(final String packageName) { return packageName.startsWith("org.alfresco"); } /** * Check if the cache node exists and postdates the WEB-INF/lib directory * @param systemPackageCache file storing the package list * @return true if node exists and postdates the lib directory */ public boolean isCacheValid(File systemPackageCache) { if (!systemPackageCache.isFile()) { return false; } final Resource libDir = resourcePatternResolver.getResource("/WEB-INF/lib"); try { final boolean stale = libDir.lastModified() > systemPackageCache.lastModified(); if (stale) { logger.info("system package cache is older then WEB-INF/lib, rescan packages"); } return !stale; } catch (IOException e) { logger.warn("unable to read WEB-INF/lib directory, keeping package cache"); return true; } } /* Dependencies */ @Override public void setServletContext(final ServletContext servletContext) { this.servletContext = servletContext; resourcePatternResolver = new ServletContextResourcePatternResolver(servletContext); } public void setDescriptorService(final DescriptorService descriptorService) { Assert.notNull(descriptorService); this.descriptorService = descriptorService; } }