/* * Copyright 2010-2011 Research In Motion Limited. * * Licensed 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 WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.rim.tumbler.config; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashSet; import java.util.Hashtable; import java.util.Set; import java.util.Vector; import java.util.Map.Entry; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import net.rim.tumbler.exception.PackageException; import net.rim.tumbler.file.ExtensionDependencyManager; import net.rim.tumbler.file.FileManager; import net.rim.tumbler.file.Library; import net.rim.tumbler.file.Library.Configuration; import net.rim.tumbler.file.Library.Extension; import net.rim.tumbler.file.Library.Jar; import net.rim.tumbler.file.Library.Platform; import net.rim.tumbler.file.Library.Src; import net.rim.tumbler.file.Library.Target; import net.rim.tumbler.log.LogType; import net.rim.tumbler.log.Logger; import net.rim.tumbler.session.BBWPProperties; import net.rim.tumbler.session.SessionManager; import net.rim.tumbler.xml.LibraryXMLParser; public class FeatureManager { // TODO Hardcode platform="JAVA" private String _platform = "JAVA"; // TODO Hardcode target="default" until target becomes one of the command // line params in bbwp.exe private String _targetVersion = "default"; private HashSet< String > _requiredFeatures; // {featureId, paths & root library.xml info} private Hashtable< String, ExtensionInfo > _repositoryFeatures; // {extensionId, paths & root library.xml info} private Hashtable< String, ExtensionInfo > _extensionLookupTable; private HashSet< String > _resolvedPaths; // By default, "ext" under Tumbler's home, configurable in bbwp.properties private String _repositoryDir; // Common folder contains shared common APIs, folder under _repositoryDir // for storing common API private String _commonAPIDir = "common"; private HashSet< String > _extensionClasses; private HashSet< String > _sharedGlobalJSFiles; private HashSet< String > _extensionJSFiles; // List of compiled JARs that the extensions might depend on private HashSet< String > _compiledJARDependencies; private HashSet< String > _curPaths = new HashSet< String >(); private HashSet< String > _commonAPIPaths = new HashSet< String >(); private File _temporaryDirectory; // TODO Hardcode features that are not found in extension repository, // temporary workaround until widgetcache extension is moved out of // framework private static HashSet< String > FRAMEWORK_FEATURES = new HashSet< String >(); private static final String TEMPORARY_DIRECTORY = "~temporaryextensionsource"; private static final String EXTENSION_DIRECTORY = "extension"; private static final String JS_DIRECTORY = "WebWorksApplicationSharedJsRepository0"; private static final String COMMON_JS_DIRECTORY = "sharedglobal"; private static final String LIBRARY_XML = "library.xml"; /** * Stores the extension's library.xml information and the paths to source * code in repository. */ public static class ExtensionInfo { private Library _lib; private HashSet<String> _javaPaths; private HashSet<String> _jarPaths; private HashSet<String> _jsPaths; private File _extensionFolder; public ExtensionInfo(Library lib, HashSet<String> javaPaths, HashSet<String> jsPaths) { _lib = lib; _javaPaths = javaPaths; _jsPaths = jsPaths; } public Library getLibrary() { return _lib; } /** * @return the extension id found in library.xml, or null if it is not * defined */ public String getExtensionId() { if (_lib != null && _lib.getExtension() != null) { return _lib.getExtension().getId(); } return null; } public HashSet<String> getRepositoryJavaPaths() { return _javaPaths; } public HashSet<String> getRepositoryJavaScriptPaths() { return _jsPaths; } public HashSet<String> getCompiledJARPaths() { return _jarPaths; } public void addCompiledJARPath(String path) { if (_jarPaths == null) { _jarPaths = new HashSet<String>(); } _jarPaths.add(path); } public void setExtensionFolder(File extFolder) { _extensionFolder = extFolder; } public File getExtensionFolder() { return _extensionFolder; } @Override public String toString() { StringBuffer buf = new StringBuffer(); buf.append("{lib: "); buf.append(_lib); buf.append(", javaPaths: "); buf.append(_javaPaths); buf.append(", jsPaths: "); buf.append(_jsPaths); buf.append("}"); return buf.toString(); } } public FeatureManager(BBWPProperties bbwpProperties, Hashtable<WidgetAccess, Vector<WidgetFeature>> accessTable) { _repositoryFeatures = new Hashtable<String, ExtensionInfo>(); _extensionLookupTable = new Hashtable<String, ExtensionInfo>(); _resolvedPaths = new HashSet<String>(); _repositoryDir = bbwpProperties.getRepositoryDir(); _requiredFeatures = getRequiredFeatures(accessTable); _extensionClasses = new HashSet<String>(); _compiledJARDependencies = new HashSet<String>(); _sharedGlobalJSFiles = new HashSet<String>(); _extensionJSFiles = new HashSet<String>(); // TODO temp workaround, treat widgetcache features as exceptions // will not throw error when the code fails to find them in extension // repository if (FRAMEWORK_FEATURES.isEmpty()) { FRAMEWORK_FEATURES.add("blackberry.widgetcache"); FRAMEWORK_FEATURES.add("blackberry.widgetcache.CacheInformation"); } } private static HashSet<String> getRequiredFeatures( Hashtable<WidgetAccess, Vector<WidgetFeature>> accessTable) { Set<WidgetAccess> keys = accessTable.keySet(); HashSet<String> requiredFeatures = new HashSet<String>(); for (Object accessKey : keys) { Vector<WidgetFeature> features = (Vector<WidgetFeature>) accessTable .get(accessKey); for (Object featureObject : features) { WidgetFeature feature = (WidgetFeature) featureObject; requiredFeatures.add(feature.getID()); } } return requiredFeatures; } /** * Given a WebWorks application archive, look at the features specified in * the white list and figure out all the extensions that need to be built. * * @param widgetArchive * WebWorks application archive * @param extJars * zip entries of JAR files found in the archive's "ext" folder * @return a set of file paths of the source code of all extensions that * need to be built * @throws Exception * if problems encountered while resolving extension * dependencies */ public HashSet<String> resolveFeatures(ZipFile widgetArchive, HashSet<ZipEntry> extJars) throws Exception { // step 1: resolve features from widget archive ext folder // if JAR files contribute to features from core API, JAR wins if (extJars != null && !extJars.isEmpty()) { resolveFeaturesFromExtJars(widgetArchive, extJars); } // step 2: resolve features from repository folder resolveFeaturesFromRepository(); // step 3: copy common API files to extension folder for code to compile copyCommonAPI(); return _resolvedPaths; } public HashSet<String> getExtensionClasses() { return _extensionClasses; } public HashSet<String> getExtensionJSFiles() { return _extensionJSFiles; } public HashSet<String> getSharedGlobalJSFiles() { return _sharedGlobalJSFiles; } /** * @return folders that need to be copied for common APIs */ public HashSet<String> getCommonAPIPaths() { return _commonAPIPaths; } /** * @return */ public HashSet<String> getCompiledJARDependencies() { return _compiledJARDependencies; } /** * Check JARs in widget archive's ext folder <br> * If required features are found in JARs, extract them to source folder <br> * Add paths in HashSet when done */ private void resolveFeaturesFromExtJars(ZipFile widgetArchive, HashSet<ZipEntry> extJars) { if (_temporaryDirectory != null) { if (_temporaryDirectory.delete() == false) { Logger.logMessage(LogType.WARNING, "EXCEPTION_DELETING_DIRECTORY", _temporaryDirectory.toString()); } } _temporaryDirectory = new File(getTempExtensionPath()); for (ZipEntry jarFile : extJars) { try { // uncompress the jar into folders File sourceExtension = unzipJarToTempDir(widgetArchive, jarFile); if (sourceExtension == null) { Logger.logMessage(LogType.WARNING, "EXCEPTION_FAILING_DECOMPRESS_JAR", jarFile.getName()); return; } // parse features offered from ext JARs Hashtable<String, ExtensionInfo> eligibleFeatures = getEligibleFeatures(sourceExtension); // if any eligible ids exists, move the content of the temp dir // into a permanent location finalizeEligibleIDs(eligibleFeatures); } catch (Exception e) { e.printStackTrace(); } finally { File[] tempExten = _temporaryDirectory.listFiles(); for (File f : tempExten) { FileManager.deleteDirectory(f); } } } FileManager.deleteDirectory(_temporaryDirectory); } /* * Takes the jar, uncompress it to temp folder, then return its handle in * the temp folder */ private File unzipJarToTempDir(ZipFile widgetArchive, ZipEntry jarFile) throws IOException { String extensionName = getExtensionName(jarFile.getName()); File sourceExtensionJar = null; ZipFile zipFile = null; try { // first copy the zip entry into the extension folder and identify // it as a jar sourceExtensionJar = FileManager.copyZipEntry(widgetArchive, jarFile, getExtensionPath() + extensionName + ".jar"); zipFile = new ZipFile(sourceExtensionJar); Enumeration<?> enu = zipFile.entries(); // go through the jar, copy each entry into the temporary folder while (enu.hasMoreElements()) { ZipEntry zipEntry = (ZipEntry) enu.nextElement(); if (zipEntry.isDirectory()) { continue; } FileManager.copyZipEntry(zipFile, zipEntry, getTempExtensionPath() + extensionName + File.separator); } } finally { if (zipFile != null) { zipFile.close(); } if (sourceExtensionJar != null) { if (sourceExtensionJar.delete() == false) { Logger.logMessage(LogType.WARNING, "EXCEPTION_DELETING_FILE", sourceExtensionJar.toString()); } } } File[] fileList = _temporaryDirectory.listFiles(); for (File f : fileList) { if (extensionName.equals(f.getName())) { return f; } } return null; } private static String getExtensionName(String extensionName) { if (extensionName == null) { return null; } int fileTypePosition = extensionName.lastIndexOf(".jar"); // the zip entry might look like "ext/xxx.jar" or "ext\xxx.jar" // if checking index of "/" gives -1, should check the index of "\" int fileSepPosition = extensionName.lastIndexOf("/"); if (fileSepPosition < 0) { fileSepPosition = extensionName.lastIndexOf("\\"); } if (fileTypePosition < 0) { fileTypePosition = extensionName.length(); } return extensionName.substring(fileSepPosition + 1, fileTypePosition); } private Hashtable<String, ExtensionInfo> getEligibleFeatures( File sourceExtension) { Hashtable<String, ExtensionInfo> eligibleFeatures = new Hashtable<String, ExtensionInfo>(); File[] files = sourceExtension.listFiles(); Hashtable<String, ExtensionInfo> availableFeatures = null; // library.xml should at top level for (File f : files) { if (f.getName().equalsIgnoreCase(LIBRARY_XML)) { try { availableFeatures = parseFeaturesInLibraryXML(f, true); } catch (Exception e) { return eligibleFeatures; } if (availableFeatures != null) { for (Entry<String, ExtensionInfo> entry : availableFeatures .entrySet()) { eligibleFeatures.put(entry.getKey(), entry.getValue()); } } } } return eligibleFeatures; } @SuppressWarnings("unchecked") private void finalizeEligibleIDs(Hashtable<String, ExtensionInfo> eligibleFeatures) { for (Entry<String, ExtensionInfo> entry : eligibleFeatures.entrySet()) { String key = entry.getKey(); ExtensionInfo value = entry.getValue(); if (_requiredFeatures.contains(key)) { copyExtensionPathsToSourceDir(value.getRepositoryJavaPaths(), getExtensionPath(), null); _resolvedPaths.addAll((HashSet<String>) _curPaths.clone()); _extensionClasses.add(value.getLibrary().getEntryClass()); // remove features from the list, so that even if the feature is // available in repository, the repository version will not be used _requiredFeatures.remove(key); } } } private static HashSet<String> getJSRelativePaths(HashSet<String> fullPaths) { String relPath = null; HashSet<String> relativePaths = new HashSet<String>(); for (String path : fullPaths) { relPath = new File(SessionManager.getInstance().getSourceFolder()).toURI().relativize(new File(path).toURI()).getPath(); relativePaths.add(relPath); } return relativePaths; } /** * Resolve features from extension repository */ @SuppressWarnings("unchecked") private void resolveFeaturesFromRepository() throws Exception { parseRepository(); HashSet<String> extensions = new HashSet<String>(); // derive extensions to be built based on features on white list for (String featureId : _requiredFeatures) { ExtensionInfo info = _repositoryFeatures.get(featureId); if (info != null) { String extensionId = info.getExtensionId(); // unable to build app that uses feature from an extension that // does not have an id // because it is not possible to resolve dependencies if (extensionId != null && !extensionId.isEmpty()) { extensions.add(extensionId); // if the extension has any JAR dependencies, add it to the // list so that it gets added to rapc classpath if (info.getCompiledJARPaths() != null) { for (String jarPath : info.getCompiledJARPaths()) { File jarFile = new File(jarPath); _compiledJARDependencies.add(jarFile .getAbsolutePath()); } } } else { throw new PackageException( "EXCEPTION_NEED_FEATURE_FROM_UNIDENTIFIED_EXTENSION", featureId); } } else { // TODO temp workaround to not throw error when widgetcache // features cannot be found in repository if (!FRAMEWORK_FEATURES.contains(featureId)) { throw new PackageException("EXCEPTION_FEATURE_NOT_FOUND", featureId); } } } // find all extensions that need to be built with dependencies taken // into account ExtensionDependencyManager edm = new ExtensionDependencyManager( _extensionLookupTable); extensions = edm.resolveExtensions(extensions); for (String extensionId : extensions) { ExtensionInfo info = _extensionLookupTable.get(extensionId); HashSet<String> javaPaths = info.getRepositoryJavaPaths(); copyExtensionPathsToSourceDir(javaPaths, getExtensionPath(), info.getExtensionFolder()); _resolvedPaths.addAll((HashSet<String>) _curPaths.clone()); HashSet<String> jsPaths = info.getRepositoryJavaScriptPaths(); copyExtensionPathsToSourceDir(jsPaths, getJavaScriptPath() + File.separator + getEscapedEntryClass(info.getLibrary().getEntryClass()) + File.separator, info.getExtensionFolder()); _resolvedPaths.addAll((HashSet<String>) _curPaths.clone()); _extensionJSFiles.addAll(getJSRelativePaths(_curPaths)); // extension can be found in lookup table for sure, otherwise // exception would have been thrown in ExtensionDependencyManager.resolve Library lib = _extensionLookupTable.get(extensionId).getLibrary(); _extensionClasses.add(lib.getEntryClass()); } } @SuppressWarnings("unchecked") private void copyCommonAPI() { File dir = new File(_repositoryDir + File.separator + _commonAPIDir); File[] files = dir.listFiles(); boolean sharedGlobalJS = false; for (File f : files) { if (f.exists()) { try { File destFile = null; if (f.isDirectory() && f.getName().equals(COMMON_JS_DIRECTORY)) { destFile = new File(getJavaScriptPath() + f.getName()); sharedGlobalJS = true; } else { destFile = new File(getExtensionPath() + f.getName()); } _curPaths.clear(); copyFiles(f, destFile); _commonAPIPaths .addAll((HashSet<String>) _curPaths.clone()); if (sharedGlobalJS) { _sharedGlobalJSFiles = getJSRelativePaths(_curPaths); } } catch (IOException e) { Logger.logMessage(LogType.ERROR, "EXCEPTION_IO_COPY_FILES", new String[] {f.getAbsolutePath(), e.getMessage()}); } } } } /** * For each folder in the extension repository, look for its library.xml and * parse it to resolve source file paths */ private void parseRepository() { File dir = new File(_repositoryDir); if (dir.exists() && dir.isDirectory()) { File[] files = dir.listFiles(); for (File f : files) { if (f.isDirectory()) { File[] extFiles = f.listFiles(); for (File g : extFiles) { try { if (g.getName().equalsIgnoreCase(LIBRARY_XML)) { Hashtable<String, ExtensionInfo> features = parseFeaturesInLibraryXML( g, false); if (features != null) { _repositoryFeatures.putAll(features); } } } catch (Exception e) { // TODO handle error } } } } } } /** * Given a library file, find the configuration element that matches the * current platform and target * * @param lib * @return the configuration that matches the current platform and target */ private Configuration getTargetConfiguration(Library lib) { String matchedConfigName = null; // check whether platform and target match what we support ArrayList<Platform> platforms = lib.getPlatforms(); if (platforms != null) { for (Platform p : platforms) { if (p.getValue().equals(_platform)) { ArrayList<Target> targets = p.getTargets(); if (targets != null) { for (Target t : targets) { if (t.getVersion().equals(_targetVersion)) { matchedConfigName = t.getConfigName(); } } } } } } // make sure the config is defined if (matchedConfigName != null) { ArrayList<Configuration> configurations = lib.getConfigurations(); for (Configuration config : configurations) { if (config.getName().equals(matchedConfigName)) { return config; } } } return null; } /** * Helper method for parsing out library.xml into an object * * @param libraryXML * @throws Exception */ private static Library parseLibraryXML(File libraryXML) throws Exception { LibraryXMLParser parser = new LibraryXMLParser(); return parser.parseXML(libraryXML.getAbsolutePath()); } /** * Parse a given a library.xml to find out the features it offers and store * the proper set of source file paths for the current platform and target * * @param libraryXML * library.xml file, cannot be null * @param allowBackwardCompatibility * true if it's parsing library.xml from an extension JAR * @return hashtable that contains feature id's and the paths for the source * files, or null if (1) library.xml is malformed, (2) if * allowBackwardCompatibility is false, and <target>, * <configuration>, <platform> or <src> is not specified correctly * @throws Exception */ private Hashtable<String, ExtensionInfo> parseFeaturesInLibraryXML( File libraryXML, boolean allowBackwardCompatibility) throws Exception { Library lib = parseLibraryXML(libraryXML); if (lib == null) { return null; } ArrayList<WidgetFeature> features = lib.getFeatures(); HashSet<String> javaPaths = new HashSet<String>(); HashSet<String> jsPaths = new HashSet<String>(); Hashtable<String, ExtensionInfo> availableFeatures = new Hashtable<String, ExtensionInfo>(); if (allowBackwardCompatibility) { // have to work for library.xml that doesn't contain configuration and platform elements File[] files = _temporaryDirectory.listFiles(); for (File f : files) { javaPaths.add(f.getAbsolutePath()); } } else { ExtensionInfo info = new ExtensionInfo(lib, javaPaths, jsPaths); boolean extensionIdFound = false; if (lib.getExtension() != null) { Extension extension = lib.getExtension(); String id = extension.getId(); if (id != null && !id.isEmpty()) { if (_extensionLookupTable.contains(id)) { // more than one library.xml contain the same extension id Logger.logMessage(LogType.WARNING, "VALIDATION_EXTENSION_DEFINED_MORE_THAN_ONCE", new String[] { id }); } _extensionLookupTable.put(id, info); info.setExtensionFolder(libraryXML.getParentFile()); extensionIdFound = true; } } if (!extensionIdFound) { // not considered an error, this extension might not be used // by the app being compiled Logger.logMessage(LogType.WARNING, "VALIDATION_LIBRARYXML_EXTENSION_ID_NOT_DEFINED", new String[] { libraryXML.getAbsolutePath() }); } Configuration config = getTargetConfiguration(lib); if (config == null) { Logger.logMessage(LogType.WARNING, "VALIDATION_LIBRARYXML_NO_CONFIG", new String[] { libraryXML.getAbsolutePath() }); return null; } ArrayList<Src> src = config.getSrc(); if (src == null || src.isEmpty()) { Logger.logMessage(LogType.WARNING, "VALIDATION_LIBRARYXML_NO_SRC", new String[] { libraryXML.getAbsolutePath() }); return null; } File extensionDir = libraryXML.getParentFile(); for (Src s : src) { String path = s.getPath(); if (s.getType().equalsIgnoreCase("text/java")) { javaPaths.add(extensionDir.getAbsolutePath() + File.separator + path); } else if (s.getType().equalsIgnoreCase("text/javascript")) { jsPaths.add(extensionDir.getAbsolutePath() + File.separator + path); } } } ExtensionInfo info = new ExtensionInfo(lib, javaPaths, jsPaths); for (WidgetFeature feature : features) { availableFeatures.put(feature.getID(), info); } if (lib.getCompiledJARDependencies() != null) { for (Jar j : lib.getCompiledJARDependencies()) { String path = j.getPath(); File temp = new File(path); if (temp.isAbsolute()) { info.addCompiledJARPath(path); } else { info.addCompiledJARPath(libraryXML.getParentFile() .getAbsolutePath() + File.separator + path); } } } return availableFeatures; } private static String getExtensionPath() { return SessionManager.getInstance().getSourceFolder() + File.separator + EXTENSION_DIRECTORY + File.separator; } private static String getJavaScriptPath() { return SessionManager.getInstance().getSourceFolder() + File.separator + JS_DIRECTORY + File.separator; } private static String getTempExtensionPath() { return SessionManager.getInstance().getSourceFolder() + TEMPORARY_DIRECTORY + File.separator; } // simply replace all occurrences of "." with "_" private static String getEscapedEntryClass(String entryClass) { return entryClass.replace(".", "_"); } private void copyExtensionPathsToSourceDir(HashSet<String> paths, String destDir, File extensionFolder) { _curPaths.clear(); for (String path : paths) { File file = new File(path); String relativePath = ""; if (extensionFolder != null) { relativePath = extensionFolder.toURI().relativize(file.getParentFile().toURI()).getPath(); relativePath += File.separator; } if (file.exists()) { try { copyFiles(file, new File( destDir + relativePath + file.getName())); } catch (IOException e) { Logger.logMessage(LogType.ERROR, "EXCEPTION_IO_COPY_FILES", new String[] {file.getAbsolutePath(), e.getMessage()}); } } } } /** * This method copies file recursively from src to dest */ private void copyFiles(File src, File dest) throws IOException { // Check to ensure that the source is valid if (!src.exists()) { throw new IOException("copyFiles: Cannot find source: " + src.getAbsolutePath() + "."); } else if (!src.canRead()) { // check to ensure we have rights to the source throw new IOException("copyFiles: No right to read source: " + src.getAbsolutePath() + "."); } // is this a directory copy? if (src.isDirectory()) { if (!dest.exists()) { // does the destination already exist? // if not we need to make it exist if possible (note this is // mkdirs not mkdir) if (!dest.mkdirs()) { throw new IOException( "copyFiles: Could not create direcotry: " + dest.getAbsolutePath() + "."); } } String list[] = src.list(); // copy all the files in the list. for (String path : list) { File dest1 = new File(dest, path); File src1 = new File(src, path); copyFiles(src1, dest1); } } else { // This was not a directory, just copy the file try { if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } // open the files for input and output FileManager.copyFile(src, dest); _curPaths.add(dest.getAbsolutePath()); } catch (IOException e) { // Error copying file IOException wrapper = new IOException( "copyFiles: Unable to copy file: " + src.getAbsolutePath() + " to " + dest.getAbsolutePath() + "."); wrapper.initCause(e); wrapper.setStackTrace(e.getStackTrace()); throw wrapper; } } } }