/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* ModulePackager.java
* Creation date: Sep 13, 2004.
* By: Edward Lam
*/
package org.openquark.cal.services;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import org.openquark.cal.compiler.ModuleName;
import org.openquark.cal.services.ResourcePath.Folder;
/**
* This class provides utility methods to aid in persistence and navigation of module resources in .packaged form.
* @author Edward Lam
*/
public class ModulePackager {
/*
TODOEL: add more properties to the manifest. Examples:
BCEL manifest:
Manifest-Version: 1.0
Created-By: Apache Jakarta Maven
Built-By: dahm
Package: org.apache.bcel
Build-Jdk: 1.4.1_01
Name: org.apache.bcel
Specification-Title: bcel
Specification-Version: 5.1
Specification-Vendor: Apache Software Foundation
Implementation-Title: org.apache.bcel
Implementation-Version: 5.1
Implementation-Vendor: Apache Software Foundation
cecore.mf:
Manifest-Version: 1.0
Product-Name: Crystal Enterprise Java SDK (C)
Implementation-Title: Crystal Enterprise SDK core
Implementation-Version: 11.0.0.700
Implementation-Vendor: Business Objects, Inc.(C)
*/
/** The name of the manifest attribute for a Car's canonical name. */
static final String CAR_NAME_MANIFEST_ATTRIBUTE = "Car-Name";
/**
* Test target.
* Creates a test .jar.
*/
public static void main(String[] args) throws IOException {
// JarFile jarFile = new JarFile("C:\\Prelude.jar");
// for (Enumeration enum = jarFile.entries(); enum.hasMoreElements(); ) {
// JarEntry jarEntry = (JarEntry)enum.nextElement();
// System.out.println(jarEntry.getName());
// }
// Create the manifest
Manifest manifest = new Manifest();
// Add some attributes.
Attributes mainAttributes = manifest.getMainAttributes();
String manifestVersionString = Attributes.Name.MANIFEST_VERSION.toString();
mainAttributes.putValue(manifestVersionString, "1.0");
String mainClassString = Attributes.Name.MAIN_CLASS.toString();
mainAttributes.putValue(mainClassString, "foo.bar.Baz");
// JarOutputStream jos = new JarOutputStream(new BufferedOutputStream(new FileOutputStream("C:\\temp\\foo.jar")), manifest);
JarOutputStream jos = new JarOutputStream(new BufferedOutputStream(new FileOutputStream("C:\\foo.jar")), manifest);
// Set comment and compression level.
jos.setComment("This is the jar file comment.");
jos.setLevel(Deflater.DEFAULT_COMPRESSION);
// Add an entry.
JarEntry jarEntry = new JarEntry("entryName");
// jarEntry.getAttributes().putValue("someAttributeName", "someValue");
jos.putNextEntry(jarEntry);
jos.write(new byte[]{1, 2, 3});
// Add a second entry.
jarEntry = new JarEntry("foo/bar.baz");
jos.putNextEntry(jarEntry);
jos.write(new byte[]{49, 105, 96, 97, 98});
// Add a third entry.
jarEntry = new JarEntry("/foo/bar/Baz.class");
jos.putNextEntry(jarEntry);
jos.write(new byte[]{49, 105, 96, 97, 98});
jos.flush();
jos.close();
}
/**
* Write a module from the workspace to a file.
* @param workspace the workspace containing the module to write.
* @param moduleName the name of the module to write.
* @param packagedModuleFile the file which will contain the packaged module. This will be created if it doesn't already exist.
* @throws IOException
*/
public static void writePackagedModule(CALWorkspace workspace, ModuleName moduleName, File packagedModuleFile) throws IOException {
writeModuleToJar(workspace, moduleName, new FileOutputStream(packagedModuleFile), true);
}
/**
* Write the workspace resources for a metamodule to a jar.
* @param workspace the workspace with the module to persist.
* @param moduleName the name of a module to persist.
* @param outputStream the output stream to which the jar will be written.
* @throws IOException
*/
public static void writeModuleToJar(CALWorkspace workspace, ModuleName moduleName, OutputStream outputStream) throws IOException {
writeModuleToJar(workspace, moduleName, outputStream, false);
}
/**
* Write the workspace resources for a metamodule to a jar.
* @param workspace the workspace with the module to persist.
* @param moduleName the name of a module to persist.
* @param outputStream the output stream to which the jar will be written.
* @param encrypt if true, the resource files will be encrypted in a simple way.
* @throws IOException
*/
private static void writeModuleToJar(CALWorkspace workspace, ModuleName moduleName,
OutputStream outputStream, boolean encrypt) throws IOException {
// JarOutputStream jos = new JarOutputStream(new BufferedOutputStream(outputStream), manifest);
JarOutputStream jos = new JarOutputStream(new BufferedOutputStream(outputStream));
// Write out a minimal manifest.
writeMinimalManifestToJar(jos);
// Set compression level.
jos.setLevel(Deflater.DEFAULT_COMPRESSION);
writeModuleToJar(workspace, moduleName, jos, encrypt, false);
// For some reason, closing the enclosing output stream isn't sufficient.
jos.close();
}
/**
* Writes a minimal manifest file to the jar.
* @param jos the jar output stream.
* @throws IOException
*/
static void writeMinimalManifestToJar(JarOutputStream jos) throws IOException {
// Create the manifest
Manifest manifest = new Manifest();
// Add some attributes.
Attributes mainAttributes = manifest.getMainAttributes();
String manifestVersionString = Attributes.Name.MANIFEST_VERSION.toString();
mainAttributes.putValue(manifestVersionString, "1.0");
writeManifestToJar(jos, manifest);
}
/**
* Writes a minimal manifest file to the Car or Car-jar.
* @param jos the jar output stream.
* @param carName the canonical name of the car.
* @throws IOException
*/
static void writeMinimalManifestToCar(JarOutputStream jos, String carName) throws IOException {
// Create the manifest
Manifest manifest = new Manifest();
// Add some attributes.
Attributes mainAttributes = manifest.getMainAttributes();
String manifestVersionString = Attributes.Name.MANIFEST_VERSION.toString();
mainAttributes.putValue(manifestVersionString, "1.0");
mainAttributes.putValue(CAR_NAME_MANIFEST_ATTRIBUTE, carName);
writeManifestToJar(jos, manifest);
}
/**
* Writes a manifest file to the jar.
* @param jos the jar output stream.
* @param manifest the manifest to be written out.
* @throws IOException
*/
private static void writeManifestToJar(JarOutputStream jos, Manifest manifest) throws IOException {
// Manually add the manifest, so that we can set the time for its entry.
ZipEntry e = new ZipEntry(JarFile.MANIFEST_NAME);
// TODOEL: Find a sensible time to put here.
// If this time changes for every output, the .jar file which is created will have a different timestamp for this entry.
// This will cause the digest on the jar "packaged module" in CE to be different.
//
// Possibilities:
// The latest time for any of the added resources.
// The time of the first resource added after Manifest.mf.
// Use a pre-written Manifest.mf.
//
e.setTime(0);
jos.putNextEntry(e);
manifest.write(new BufferedOutputStream(jos));
jos.closeEntry();
}
/**
* Write the workspace resources for a module to a jar.
* @param workspace the workspace with the module to persist.
* @param moduleName the name of a module to persist.
* @param jos the JarOutputStream to be written to.
* @param buildSourcelessModules whether to build the jar with sourceless modules (i.e. the CAL files will be empty stubs).
* @throws IOException
*/
static void writeModuleToJar(CALWorkspace workspace, ModuleName moduleName, JarOutputStream jos, boolean buildSourcelessModules) throws IOException {
writeModuleToJar(workspace, moduleName, jos, false, buildSourcelessModules);
}
/**
* Write the workspace resources for a module to a jar.
* @param workspace the workspace with the module to persist.
* @param moduleName the name of a module to persist.
* @param jos the JarOutputStream to be written to.
* @param encrypt if true, the resource files will be encrypted in a simple way.
* @param buildSourcelessModules whether to build the jar with sourceless modules (i.e. the CAL files will be empty stubs).
* @throws IOException
*/
private static void writeModuleToJar(CALWorkspace workspace, ModuleName moduleName, JarOutputStream jos, boolean encrypt, boolean buildSourcelessModules) throws IOException {
// Iterate over the resource managers.
for (final ResourceManager resourceManager : workspace.getResourceManagersForModule(moduleName)) {
if (resourceManager instanceof ModuleResourceManager) {
writeResourcesToJar(workspace, (ModuleResourceManager)resourceManager, moduleName, jos, encrypt, buildSourcelessModules);
}
}
}
/**
* Writes the user resources associated with a module into a jar.
* @param workspace the workspace with the module.
* @param moduleName the name of the module whose user resources are to be written out.
* @param jos the JarOutputStream to be written to.
* @throws IOException
*/
public static void writeUserResourcesToJar(final CALWorkspace workspace, final ModuleName moduleName, final JarOutputStream jos) throws IOException {
final UserResourceManager userResourceManager = workspace.getUserResourceManager(moduleName);
writeResourcesToJar(workspace, userResourceManager, moduleName, jos, false, false);
}
/**
* Writes the resources from a resource manager into a jar.
* @param workspace the workspace with the module to persist.
* @param resourceManager the resource manager.
* @param moduleName the name of a module to persist.
* @param jos the JarOutputStream to be written to.
* @param encrypt if true, the resource files will be encrypted in a simple way.
* @param buildSourcelessModules whether to build the jar with sourceless modules (i.e. the CAL files will be empty stubs).
* @throws IOException
*/
private static void writeResourcesToJar(final CALWorkspace workspace, final ModuleResourceManager resourceManager, final ModuleName moduleName, final JarOutputStream jos, boolean encrypt, final boolean buildSourcelessModules) throws IOException {
// The byte buffer and array used for copying resources.
byte[] bufArray = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(bufArray);
boolean isSourceManager = resourceManager instanceof CALSourceManager;
// Iterate over the resources in the manager.
for (Iterator<WorkspaceResource> it = resourceManager.getResourceIterator(moduleName); it.hasNext(); ) {
WorkspaceResource moduleResource = it.next();
// Get the name of the resource, and the path to that resource.
ResourceIdentifier resourceIdentifier = moduleResource.getIdentifier();
ResourcePath.FilePath filePath = workspace.getResourcePath(resourceIdentifier);
// Add a jar entry.
JarEntry jarEntry = new JarEntry(filePath.getPathStringMinusSlash());
jarEntry.setTime(moduleResource.getTimeStamp());
jos.putNextEntry(jarEntry);
if (!isSourceManager || !buildSourcelessModules) {
// We omit the content of the entry if the resource is a module source and buildSourcelessModules is true.
// Write the bytes for that entry.
InputStream moduleResourceStream = null;
try {
moduleResourceStream = moduleResource.getInputStream(new Status("Input Status")); // shouldn't normally be null..
ReadableByteChannel byteChannel = Channels.newChannel(moduleResourceStream);
while (true) {
int bytesRead = byteChannel.read(buffer);
if (bytesRead < 0) {
// end of file.
break;
}
// Write the data.
jos.write(bufArray, 0, bytesRead);
buffer.rewind(); // rewind the buffer so that the next read starts at position 0 again
}
} finally {
// Don't forget to close the input stream..
if (moduleResourceStream != null) {
try {
moduleResourceStream.close();
} catch (IOException ioe) {
}
}
}
}
}
}
/**
* Get the names representing the resources in a folder.
*
* @param jarFileManager the jar file to search
* @param folder the folder to search.
* @param pathMapper the path mapper to use to map paths back to resource names
* @return the names representing the resources in the given directory.
* Never null, but may be empty if the given file is not a directory.
*/
public static List<ResourceName> getFolderResourceNames(JarFileManager jarFileManager, ResourcePath.Folder folder, ResourcePathMapper pathMapper) {
return getFilteredFolderResourceNames(jarFileManager, folder, pathMapper, null);
}
/**
* Get a filtered list of the names representing the resources in a folder.
*
* @param jarFileManager the jar file to search
* @param folder the folder to search.
* @param pathMapper the path mapper to use to map paths back to resource names
* @param filter the filter to use for determining which resource names to keep in the returned list.
* @return the names representing the resources in the given directory.
* Never null, but may be empty if the given file is not a directory.
*/
public static List<ResourceName> getFilteredFolderResourceNames(JarFileManager jarFileManager, ResourcePath.Folder folder, ResourcePathMapper pathMapper, ResourceName.Filter filter) {
String relativePathOfFolder = folder.getPathStringMinusSlash();
Set<ResourceName> filteredSet = new TreeSet<ResourceName>(); // sort alphabetically..
// Iterate over the entries in the jar file in the folder.
Set<String> fileNamesInFolder = jarFileManager.getFileNamesInFolder(relativePathOfFolder);
if (fileNamesInFolder != null) {
for (final String fileName : fileNamesInFolder) {
// It passes if the path mapper returns a non-null resource name (e.g. it has the right file extension).
ResourceName resourceName = pathMapper.getResourceNameFromFolderAndFileName(folder, fileName);
if (resourceName != null) {
// If the filter is not null, check the path to make sure it's acceptable first
if (filter == null || filter.accept(resourceName)) {
filteredSet.add(resourceName);
}
}
}
}
return new ArrayList<ResourceName>(filteredSet);
}
/**
* Get the names representing the resources in the subtree rooted at the folder.
*
* @param jarFileManager the jar file to search
* @param folder the folder to search.
* @param pathMapper the path mapper to use to map paths back to resource names
* @return the names representing the resources in the given directory.
* Never null, but may be empty if the given file is not a directory.
*/
public static List<ResourceName> getResourceNamesInSubtreeRootedAtFolder(JarFileManager jarFileManager, ResourcePath.Folder folder, ResourcePathMapper pathMapper) {
String relativePathOfFolder = folder.getPathStringMinusSlash();
Set<ResourceName> filteredSet = new TreeSet<ResourceName>(); // sort alphabetically..
// Iterate over the entries in the jar file in all the subfolders and the given folder.
Set<String> foldersToCheck = new HashSet<String>();
foldersToCheck.add(relativePathOfFolder);
foldersToCheck.addAll(jarFileManager.getAllDescendentFolderPathsInFolder(relativePathOfFolder));
for (final String relativePathOfFolderToCheck : foldersToCheck) {
ResourcePath.Folder resourceFolderToCheck = ResourcePath.Folder.makeFromPathStringMinusSlash(relativePathOfFolderToCheck);
Set<String> fileNamesInFolder = jarFileManager.getFileNamesInFolder(relativePathOfFolderToCheck);
if (fileNamesInFolder != null) {
for (final String fileName : fileNamesInFolder) {
// It passes if the path mapper returns a non-null resource name (e.g. it has the right file extension).
ResourceName resourceName = pathMapper.getResourceNameFromFolderAndFileName(resourceFolderToCheck, fileName);
if (resourceName != null) {
filteredSet.add(resourceName);
}
}
}
}
return new ArrayList<ResourceName>(filteredSet);
}
/**
* Get the subfolders of a given folder which contain any files with a given extension.
* @param parentFolder the folder whose subfolders will be searched. The names of these subfolders
* are eligible to be returned.
* @param fileExtension the file extension to search for. If null, all files will be returned.
* @param convertFromFileSystemName whether the folder names in the jar file are encoded with getFileSystemName(), and should be decoded.
* @return the names of folders for which files with the given extension exist.
*/
public static Set<String> getFolderNamesWithFileExtension(JarFileManager jarFileManager, ResourcePath.Folder parentFolder,
String fileExtension, boolean convertFromFileSystemName) {
String relativePathOfParentFolder = parentFolder.getPathStringMinusSlash();
Set<String> matchingFolderNameSet = new HashSet<String>();
// Get the components of the parent folder.
String[] parentFolderElements = parentFolder.getPathElements();
int nParentFolderElements = parentFolderElements.length;
// Iterate over the entries in the jar file in the folder.
Set<String> subfolderPaths = jarFileManager.getAllDescendentFolderPathsInFolder(relativePathOfParentFolder);
if (subfolderPaths != null) {
for (final String subfolderPath : subfolderPaths) {
Set<String> folderContents = jarFileManager.getFileNamesInFolder(subfolderPath);
if (folderContents != null) {
for (final String fileName : folderContents) {
// Get the name of the entry, minus the leading slash.
String entryName = subfolderPath + "/" + fileName;
// Split into its components.
String[] entryNameComponents = (entryName.charAt(0) == '/') ? entryName.substring(1).split("/") : entryName.split("/");
int nEntryNameComponents = entryNameComponents.length;
// // Convert from file system name if necessary.
// if (convertFromFileSystemName) {
// for (int i = 0; i < nEntryNameComponents; i++) {
// entryNameComponents[i] = FileSystemResourceHelper.fromFileSystemName(entryNameComponents[i]);
// }
// }
// Check that there are enough components in the entry.
if (nEntryNameComponents <= nParentFolderElements) {
continue;
}
// Check that the components of the entry name match with the parent folder.
for (int i = 0; i < nParentFolderElements; i++) {
if (!parentFolderElements[i].equals(entryNameComponents[i])) {
continue;
}
}
// Check that the file extension matches.
if (fileExtension != null && !entryNameComponents[nEntryNameComponents - 1].endsWith("." + fileExtension)) {
continue;
}
// If we're here, the folder matches.
String matchingFolderName = entryNameComponents[nParentFolderElements];
if (convertFromFileSystemName) {
matchingFolderName = FileSystemResourceHelper.fromFileSystemName(matchingFolderName);
}
matchingFolderNameSet.add(matchingFolderName);
}
}
}
}
return matchingFolderNameSet;
}
/**
* Returns a set of folder resource paths of all non-empty subfolders in the given folder.
* @param jarFileManager the jar file manager containing the jar to be processed.
* @param parentFolder the folder resource path of the root of the subtree to process.
* @return a set of folder resource paths of all non-empty subfolders in the given folder.
*/
public static Set<ResourcePath.Folder> getResourcePathsOfAllNonEmptySubfolders(JarFileManager jarFileManager, ResourcePath.Folder parentFolder) {
String relativePathOfParentFolder = parentFolder.getPathStringMinusSlash();
Set<ResourcePath.Folder> result = new HashSet<Folder>();
// Iterate over the entries in the jar file in the folder.
Set<String> subfolderPaths = jarFileManager.getAllDescendentFolderPathsInFolder(relativePathOfParentFolder);
for (final String subfolderPath : subfolderPaths) {
if (!jarFileManager.getFileNamesInFolder(subfolderPath).isEmpty()) {
result.add(ResourcePath.Folder.makeFromPathStringMinusSlash(subfolderPath));
}
}
return result;
}
}