/*
* 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.
*/
/*
* CarBuilder.java
* Creation date: Jan 18, 2006.
* By: Joseph Wong
*/
package org.openquark.cal.services;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarOutputStream;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import org.openquark.cal.compiler.ModuleName;
import org.openquark.cal.machine.ProgramResourceLocator;
import org.openquark.cal.machine.ProgramResourceRepository;
import org.openquark.util.FileSystemHelper;
import org.openquark.util.Messages;
import org.openquark.util.Pair;
import org.openquark.util.TextEncodingUtilities;
/**
* This class implements a CAL Archive (Car) builder that will go through the
* resources in a supplied workspace and add those resources to a new Car.
*
* This class is not meant to be instantiated or subclassed.
*
* @author Joseph Wong
*/
public final class CarBuilder {
private static final String DOT_CWS = "." + WorkspaceDeclarationPathMapper.INSTANCE.getFileExtension();
private static final String DOT_CAR = "." + CarPathMapper.INSTANCE.getFileExtension();
private static final String DOT_JAR = ".jar";
public static final String DOT_CAR_DOT_JAR = DOT_CAR + DOT_JAR;
private static final String WORKSPACE_DECLARATIONS_FOLDER_NAME = WorkspaceDeclarationPathMapper.INSTANCE.getBaseResourceFolder().getPathStringMinusSlash();
private static final String CAR_FOLDER_NAME = CarPathMapper.INSTANCE.getBaseResourceFolder().getPathStringMinusSlash();
/**
* The default name to use for the workspace spec file that contains all the modules in the Car.
*/
private static final String DEFAULT_MAIN_SPEC_NAME = "main.carspec";
/**
* The singleton instance for accessing the message bundle.
*/
private static final Messages MESSAGES = new Messages(CarBuilder.class, "carbuilder");
/**
* The marker file to be added to all Cars that identifies a file as a Car.
*/
public static final String CAR_MARKER_NAME = "Car.marker";
/**
* The name of the special file that contains a list of the additional files in the Car.
*/
public static final String CAR_ADDITIONAL_FILES_NAME = "Car.additionalFiles";
/**
* This class encapsulates the configuration options for the CarBuilder.
*
* @author Joseph Wong
*/
public final static class Configuration {
/**
* The workspace manager that will provide the resources to be added to the Car.
*/
private final WorkspaceManager workspaceManager;
/**
* The names of the modules to be included in the Car.
*/
private final ModuleName[] moduleNames;
/**
* A Map mapping a Car Workspace Spec file name to the corresponding CarSpec instance.
*/
private final Map<String, CarSpec> carSpecs;
/**
* The name of the main CarSpec.
*/
private final String mainSpecName;
/**
* The monitor to be used for monitoring the progress of the Car building operation.
*/
private Monitor monitor;
/**
* Whether to build the Car with sourceless modules (i.e. the CAL files will be empty stubs).
*/
private boolean buildSourcelessModules;
/**
* A map from relative paths to the contents of additional files to be included in the Car.
*/
private final Map<String, byte[]> additionalFiles;
/**
* Constructs an instance of CarBuilder.Configuration where all the modules in the workspace manager
* will be added to the Car.
*
* @param workspaceManager the workspace manager that will provide the resources to be added to the Car.
*/
public Configuration(WorkspaceManager workspaceManager) {
this(workspaceManager, workspaceManager.getWorkspace().getModuleNames());
}
/**
* Factory method for constructing an instance of CarBuilder.Configuration where modules that are
* imported from existing Cars can optionally be excluded.
*
* @param workspaceManager the workspace manager that will provide the resources to be added to the Car.
* @param options configuration options for the Car builder.
* @return an instance of this class.
*/
public static Configuration makeConfigOptionallySkippingModulesAlreadyInCars(WorkspaceManager workspaceManager, BuilderOptions options) {
Configuration config;
if (!options.shouldSkipModulesAlreadyInCars) {
config = new Configuration(workspaceManager);
} else {
// modules in Cars should be skipped...
List<ModuleName> nonCarModulesList = new ArrayList<ModuleName>();
CALWorkspace workspace = workspaceManager.getWorkspace();
ModuleName[] allModuleNames = workspace.getModuleNames();
for (final ModuleName moduleName : allModuleNames) {
VaultElementInfo vaultInfo = workspace.getVaultInfo(moduleName);
if (!isVaultElementFromCar(vaultInfo)) {
nonCarModulesList.add(moduleName);
}
}
ModuleName[] nonCarModules = nonCarModulesList.toArray(new ModuleName[nonCarModulesList.size()]);
config = new Configuration(workspaceManager, nonCarModules);
}
config.setBuildSourcelessModule(options.buildSourcelessModules);
return config;
}
/**
* Constructs an instance of CarBuilder.Configuration where only the named modules will be added to the Car.
*
* @param workspaceManager the workspace manager that will provide the resources to be added to the Car.
* @param moduleNames the names of the modules to be included in the Car.
*/
public Configuration(WorkspaceManager workspaceManager, ModuleName[] moduleNames) {
this(workspaceManager, moduleNames, DEFAULT_MAIN_SPEC_NAME);
}
/**
* Constructs an instance of CarBuilder.Configuration where only the named modules will be added to the Car.
*
* @param workspaceManager the workspace manager that will provide the resources to be added to the Car.
* @param moduleNames the names of the modules to be included in the Car.
* @param mainSpecName the name to use for the workspace spec file that contains all the modules in the Car.
*/
public Configuration(WorkspaceManager workspaceManager, ModuleName[] moduleNames, String mainSpecName) {
if (workspaceManager == null || moduleNames == null || mainSpecName == null) {
throw new NullPointerException();
}
this.workspaceManager = workspaceManager;
this.moduleNames = moduleNames.clone();
this.carSpecs = new HashMap<String, CarSpec>();
this.mainSpecName = mainSpecName;
this.monitor = DefaultMonitor.INSTANCE;
this.buildSourcelessModules = false;
this.additionalFiles = new HashMap<String, byte[]>();
// sort the module names in alphabetical order
Arrays.sort(this.moduleNames);
// add the main spec to the car specs map
carSpecs.put(mainSpecName, new CarSpec.ModuleList(mainSpecName, this.moduleNames));
}
/**
* Specifies the monitor to be used for monitoring the progress of the Car building operation.
*
* @param monitor the monitor to be used by the CarBuilder. If this argument is null, then a {@link
* CarBuilder.DefaultMonitor DefaultMonitor} will be used as the monitor.
*/
public void setMonitor(Monitor monitor) {
if (monitor == null) {
this.monitor = DefaultMonitor.INSTANCE;
} else {
this.monitor = monitor;
}
}
/**
* Specifies whether to build the Car with sourceless modules (i.e. the CAL files will be empty stubs).
* @param buildSourcelessModules the setting.
*/
public void setBuildSourcelessModule(boolean buildSourcelessModules) {
this.buildSourcelessModules = buildSourcelessModules;
}
/**
* Adds a Car Workspace Spec file to the Car.
* @param specName the name of the spec file.
* @param modulesInSpec the names of the modules to be included in the spec file.
*/
public void addCarSpec(String specName, ModuleName[] modulesInSpec) {
if (specName == null || modulesInSpec == null) {
throw new NullPointerException();
}
carSpecs.put(specName, new CarSpec.ModuleList(specName, modulesInSpec));
}
/**
* Adds a preexisting Car Workspace Spec file from the file system to the Car.
* @param specName the name of the spec file when added to the Car.
* @param specFile the preexisting spec file.
*/
public void addFileBasedCarSpec(String specName, File specFile) {
if (specName == null || specFile == null) {
throw new NullPointerException();
}
carSpecs.put(specName, new CarSpec.FileBased(specName, specFile));
}
/**
* Adds an additional file to be included with the Car.
* @param relativePath the relative path of the file.
* @param contents the contents of the file.
*/
public void addAdditionalFile(String relativePath, byte[] contents) {
additionalFiles.put(relativePath, contents);
}
/**
* Returns the import declaration corresponding to the Car being built, if the Car were to be
* found in the StandardVault.
*
* @param carName the name of the Car.
*/
private String getWorkspaceDeclarationImportLineForStandardVaultCar(String carName) {
return "import car StandardVault " + carName + " " + mainSpecName;
}
}
/**
* This abstract class represents the contents of a Car Workspace Spec file.
*
* @author Joseph Wong
*/
private static abstract class CarSpec {
/**
* The name of the spec file.
*/
private final String specName;
/**
* This class represents the contents of a spec file as an array of module names to be included in the file.
*
* @author Joseph Wong
*/
private static final class ModuleList extends CarSpec {
/**
* The names of the modules to be included in the spec file.
*/
private final ModuleName[] modulesInSpec;
/**
* Constructs a CarSpec.ModuleList instance.
* @param specName the name of the spec file.
* @param modulesInSpec the names of the modules to be included in the spec file.
*/
private ModuleList(String specName, ModuleName[] modulesInSpec) {
super(specName);
if (modulesInSpec == null) {
throw new NullPointerException();
}
this.modulesInSpec = modulesInSpec.clone();
}
/**
* {@inheritDoc}
*/
@Override
void writeTo(OutputStream outputStream) throws IOException {
String spec = getWorkspaceSpec(getName(), modulesInSpec);
writeStringToStream(outputStream, spec);
}
/**
* Returns the string contents of the spec file given the file name and the array of module names.
* @param specName the name of the spec file.
* @param moduleNames the array of module names to be included in the file.
* @return the string contents of the spec file.
*/
private static String getWorkspaceSpec(String specName, ModuleName[] moduleNames) {
StringBuilder builder = new StringBuilder();
builder.append("// ").append(specName).append("\n// Created at " + new Date() + "\n\n");
for (final ModuleName element : moduleNames) {
builder.append(element).append("\n");
}
return builder.toString();
}
}
/**
* This class represents the contents of a spec file as a preexisting file on the file system.
*
* @author Joseph Wong
*/
private static final class FileBased extends CarSpec {
/**
* The preexisting spec file on the file system.
*/
private final File specFile;
/**
* Constructs an instance of CarSpec.FileBased.
* @param specName the name of the spec file.
* @param specFile the preexisting spec file on the file system.
*/
private FileBased(String specName, File specFile) {
super(specName);
if (specFile == null) {
throw new NullPointerException();
}
this.specFile = specFile;
}
/**
* {@inheritDoc}
*/
@Override
void writeTo(OutputStream outputStream) throws IOException {
FileInputStream fis = new FileInputStream(specFile);
try {
FileSystemResourceHelper.transferData(fis, outputStream);
} finally {
fis.close();
}
}
}
/**
* Private constructor for subclasses.
* @param specName the name of the spec file.
*/
private CarSpec(String specName) {
if (specName == null) {
throw new NullPointerException();
}
this.specName = specName;
}
/**
* @return the name of the spec file.
*/
String getName() {
return specName;
}
/**
* Writes the string contents of the spec file to the specified output stream.
* @param outputStream the stream to be written to.
* @throws IOException
*/
abstract void writeTo(OutputStream outputStream) throws IOException;
}
/**
* This interface specifies a progress monitor to be used with a {@link CarBuilder} for
* monitoring the progress of the Car building operation.
*
* @author Joseph Wong
*/
public static interface Monitor {
/**
* Returns whether the operation has been canceled by the user. This method is
* meant to be queried by the {@link CarBuilder} in determining whether to abort the operation.
*
* @return true if the operation has been canceled, false otherwise.
*/
public boolean isCanceled();
/**
* Displays the specified messages to the user (if appropriate).
* @param messages the messages to be shown.
*/
public void showMessages(String[] messages);
/**
* Invoked when the CarBuilder has started its operation.
* @param nCars the number of Cars to be built.
* @param nTotalModules the total number of modules across all Cars.
*/
public void operationStarted(int nCars, int nTotalModules);
/**
* Invoked when the CarBuilder has started processing the modules and adding them to a Car.
* @param carName the name of the Car being built.
* @param nModules the number of modules that will be added to the Car.
*/
public void carBuildingStarted(String carName, int nModules);
/**
* Invoked when the CarBuilder has started processing the named module and adding it to the current Car.
* @param carName the name of the Car being built.
* @param moduleName the name of the module being processed by the CarBuilder.
*/
public void processingModule(String carName, ModuleName moduleName);
/**
* Invoked when the CarBuilder has finished building a Car.
* @param carName the name of the Car just built.
*/
public void carBuildingDone(String carName);
/**
* Invoked when the CarBuilder has finished its operation.
*/
public void operationDone();
}
/**
* This is a default implementation of the Monitor interface which does nothing, and which always returns true
* for isCanceled().
*
* This class is meant to be used through its singleton instance.
*
* @author Joseph Wong
*/
public static final class DefaultMonitor implements Monitor {
/** Private constructor. */
private DefaultMonitor() {}
// default implementation
public boolean isCanceled() {
return false;
}
public void showMessages(String[] messages) {}
public void operationStarted(int nCars, int nTotalModules) {}
public void carBuildingStarted(String carName, int nModules) {}
public void processingModule(String carName, ModuleName moduleName) {}
public void carBuildingDone(String carName) {}
public void operationDone() {}
/** Singleton instance. */
public static final DefaultMonitor INSTANCE = new DefaultMonitor();
}
/**
* Encapsulates the options for the Car builder. Note that this is different from
* the per-Car configuration encapsulated by the {@link CarBuilder.Configuration} class.
*
* @author Joseph Wong
*/
public static final class BuilderOptions {
/**
* Specifies whether modules already in Cars should be skipped over.
*/
private final boolean shouldSkipModulesAlreadyInCars;
/**
* Specifies whether a corresponding workspace declaration should be generated also.
*/
private final boolean shouldGenerateCorrespWorkspaceDecl;
/**
* Specifies whether the generated workspace declaration should end simply with '.cws' instead of '.car.cws'.
*/
private final boolean noCarSuffixInWorkspaceDeclName;
/**
* Specifies whether to build the Car with sourceless modules (i.e. the CAL files will be empty stubs).
*/
private final boolean buildSourcelessModules;
/**
* Specifies the names of Cars to be explicitly excluded.
*/
private final Set<String> carsToExclude;
/**
* Specifies whether to generate Cars with the suffix .car.jar.
*/
private final boolean shouldGenerateCarJarSuffix;
/**
* Constructs an instance of BuilderOptions.
* @param shouldSkipModulesAlreadyInCars whether modules already in Cars should be skipped over.
* @param shouldGenerateCorrespWorkspaceDecl whether a corresponding workspace declaration should be generated also.
* @param noCarSuffixInWorkspaceDeclName whether the generated workspace declaration should end simply with '.cws' instead of '.car.cws'.
* @param buildSourcelessModules whether to build the Car with sourceless modules (i.e. the CAL files will be empty stubs).
* @param carsToExclude the names of Cars to be explicitly excluded.
* @param shouldGenerateCarJarSuffix whether to generate Cars with the suffix .car.jar.
*/
public BuilderOptions(
boolean shouldSkipModulesAlreadyInCars,
boolean shouldGenerateCorrespWorkspaceDecl,
boolean noCarSuffixInWorkspaceDeclName,
boolean buildSourcelessModules,
Collection<String> carsToExclude,
boolean shouldGenerateCarJarSuffix) {
this.shouldSkipModulesAlreadyInCars = shouldSkipModulesAlreadyInCars;
this.shouldGenerateCorrespWorkspaceDecl = shouldGenerateCorrespWorkspaceDecl;
this.noCarSuffixInWorkspaceDeclName = noCarSuffixInWorkspaceDeclName;
this.buildSourcelessModules = buildSourcelessModules;
this.carsToExclude = Collections.unmodifiableSet(new HashSet<String>(carsToExclude));
this.shouldGenerateCarJarSuffix = shouldGenerateCarJarSuffix;
}
}
/**
* Encapsulates the configuration for building a Car and saving it to the file system,
* including in particular the {@link CarBuilder.Configuration} for the Car.
*
* @author Joseph Wong
*/
public static final class FileOutputConfiguration {
/**
* The basic Car building configuration.
*/
private final Configuration config;
/**
* The location of the output Car file.
*/
private final File carFile;
/**
* The name of the output Car. Can be different from the name of the Car file, e.g. when the Car file ends with a
* .car.jar suffix.
*/
private final String carName;
/**
* The location of the corresponding workspace declaration file. Can be null if none is to be generated.
*/
private final File cwsFile;
/**
* A Collection containing the names of additional declarations to be added as imports to the workspace declaration,
* if it is to be generated.
*/
private Collection<String> additionalImportsForWorkspaceDecl;
/**
* Constructs an instance of FileOutputConfiguration.
* @param config the basic Car building configuration.
* @param carFile the location of the output Car file.
* @param carName the name of the output Car.
* @param cwsFile the corresponding workspace declaration file. Can be null if none is to be generated.
*/
public FileOutputConfiguration(Configuration config, File carFile, String carName, File cwsFile) {
if (config == null || carFile == null || carName == null) {
throw new NullPointerException();
}
this.carFile = carFile;
this.carName = carName;
this.config = config;
this.cwsFile = cwsFile; // can be null
this.additionalImportsForWorkspaceDecl = Collections.emptySet();
}
/**
* @return the basic Car building configuration.
*/
public Configuration getConfig() {
return config;
}
/**
* @return the location of the output Car file.
*/
public File getCarFile() {
return carFile;
}
/**
* @return the name of the output Car.
*/
public String getCarName() {
return carName;
}
/**
* @return the location of the corresponding workspace declaration file, or null if none is to be generated.
*/
public File getWorkspaceDeclarationFile() {
return cwsFile;
}
/**
* Sets the Collection containing the names of additional declarations to be added as imports to the workspace declaration,
* if it is to be generated.
* @param additionalImportsForWorkspaceDecl
*/
private void setAdditionalImportsForWorkspaceDecl(Collection<String> additionalImportsForWorkspaceDecl) {
this.additionalImportsForWorkspaceDecl = Collections.unmodifiableCollection(additionalImportsForWorkspaceDecl);
}
/**
* @return the Collection containing the names of additional declarations to be added as imports to the workspace declaration,
* if it is to be generated.
*/
public Collection<String> getAdditionalImportsForWorkspaceDecl() {
return additionalImportsForWorkspaceDecl;
}
/**
* @return whether to generate Cars with the suffix .car.jar.
*/
public boolean shouldGenerateCarJarSuffix() {
return carFile.getName().endsWith(DOT_JAR);
}
}
/**
* Encapsulates a set of {@link CarBuilder.Configuration}s that are constructed for building one
* Car per workspace declaration.
*
* @author Joseph Wong
*/
public static final class ConfigurationsForOneCarPerWorkspaceDeclaration {
/**
* The map (workspace decl name -> {@link CarBuilder.FileOutputConfiguration}) that contains all the configurations.
*/
private final Map<String, FileOutputConfiguration> workspaceDeclToConfigMap;
/**
* The map (module name -> LinkedHashSet of (Pair of (workspace decl name, VaultElementInfo))) that contains
* information about conflicting entries in workspace declaration files:
*
* A module may appear in more than one declaration, and these declaration entries may be for different vaults/revisions.
* Only one declaration entry correspond to the resources in the workspace. The remainder are conflicts. So we
* store information about each conflicting entry (a VaultElementInfo) and its corresponding workspace declaration file name.
*/
private final Map<ModuleName, LinkedHashSet<Pair<String, VaultElementInfo>>> moduleNameToConflictingWorkspaceEntriesMap;
/**
* A map containing the names of the modules included by the workspace declaration and
* their associated entries in all the workspace declarations in the tree.
*/
private final Map<String, LinkedHashSet<String>> importsMap;
/**
* Private constructor. Intended only to be called by {@link CarBuilder#makeConfigurationsForOneCarPerWorkspaceDeclaration}.
* @param workspaceDeclToConfigMap
* @param moduleNameToConflictingWorkspaceEntriesMap
*/
private ConfigurationsForOneCarPerWorkspaceDeclaration(Map<String, FileOutputConfiguration> workspaceDeclToConfigMap, Map<ModuleName, LinkedHashSet<Pair<String, VaultElementInfo>>> moduleNameToConflictingWorkspaceEntriesMap, Map<String, LinkedHashSet<String>> importsMap) {
if (moduleNameToConflictingWorkspaceEntriesMap == null || workspaceDeclToConfigMap == null || importsMap == null) {
throw new NullPointerException();
}
// wrap the maps with unmodifiable wrappers since they're returned directly by accessors
this.moduleNameToConflictingWorkspaceEntriesMap = Collections.unmodifiableMap(moduleNameToConflictingWorkspaceEntriesMap);
this.workspaceDeclToConfigMap = Collections.unmodifiableMap(workspaceDeclToConfigMap);
this.importsMap = Collections.unmodifiableMap(importsMap);
}
/**
* @return the map (workspace decl name -> {@link CarBuilder.FileOutputConfiguration}) that contains all the configurations.
*/
public Map<String, FileOutputConfiguration> getWorkspaceDeclToConfigMap() {
return workspaceDeclToConfigMap;
}
/**
* @return the map (module name -> LinkedHashSet of (Pair of (workspace decl name, VaultElementInfo))) that contains
* information about conflicting entries in workspace declaration files.
*/
public Map<ModuleName, LinkedHashSet<Pair<String, VaultElementInfo>>> getModuleNameToConflictingWorkspaceEntriesMap() {
return moduleNameToConflictingWorkspaceEntriesMap;
}
/**
* @return a map containing the names of the modules included by the workspace declaration and
* their associated entries in all the workspace declarations in the tree.
*/
public Map<String, LinkedHashSet<String>> getImportsMap() {
return importsMap;
}
}
/** Private constructor. This class is not meant to be instantiated. */
private CarBuilder() {}
/**
* Constructs a set of {@link CarBuilder.FileOutputConfiguration}s that are constructed for building one
* Car per workspace declaration.
*
* @param workspaceManager the workspace manager that will provide the resources to be added to the Cars.
* @param monitor the {@link CarBuilder.Monitor} to be used.
* @param outputDirectory the output directory to which the Car files will be written.
* @param options configuration options for the builder.
* @return a {@link CarBuilder.ConfigurationsForOneCarPerWorkspaceDeclaration} encapsulating the configurations and information on
* any identified conflicts.
*/
public static ConfigurationsForOneCarPerWorkspaceDeclaration makeConfigurationsForOneCarPerWorkspaceDeclaration(WorkspaceManager workspaceManager, Monitor monitor, File outputDirectory, BuilderOptions options) {
WorkspaceDeclaration.StreamProvider workspaceDeclarationProvider = workspaceManager.getInitialWorkspaceDeclarationProvider();
CALWorkspace workspace = workspaceManager.getWorkspace();
Status status = new Status("Reading workspace declarations");
// fetch information about all imported workspace declaration files using the WorkspaceLoader
Map<ModuleName, LinkedHashSet<Pair<String, VaultElementInfo>>> moduleNameToWorkspaceDeclInfoMap = WorkspaceLoader.getStoredModuleNameToWorkspaceDeclNamesMap(workspaceDeclarationProvider, workspace, status);
Map<ModuleName, LinkedHashSet<Pair<String, VaultElementInfo>>> moduleNameToConflictingWorkspaceEntriesMap = new HashMap<ModuleName, LinkedHashSet<Pair<String, VaultElementInfo>>>();
////
/// Invert the moduleNameToWorkspaceDeclNamesMap to (workspace decl name -> Set of module names), and build up
/// the map containing conflict information
//
Map<String, Set<ModuleName>> workspaceDeclToModuleNamesMap = new HashMap<String, Set<ModuleName>>();
for (final Map.Entry<ModuleName, LinkedHashSet<Pair<String, VaultElementInfo>>> entry : moduleNameToWorkspaceDeclInfoMap.entrySet()) {
ModuleName moduleName = entry.getKey();
LinkedHashSet<Pair<String, VaultElementInfo>> workspaceDeclInfo = entry.getValue();
VaultElementInfo actualVaultInfo = null;
for (final Pair<String, VaultElementInfo> workspaceDeclNameAndVaultElementInfoPair : workspaceDeclInfo) {
String workspaceDeclName = workspaceDeclNameAndVaultElementInfoPair.fst();
VaultElementInfo vaultInfo = workspaceDeclNameAndVaultElementInfoPair.snd();
// we know that the workspaceDeclInfo is ordered in such a way that the first entry
// correspond to the vault info that is actually used by the workspace, and that the subsequent entries
// correspond to "shadowed" declaration entries that may in fact conflict with the "actualVaultInfo"
// detect if there were conflicting entries for the same module in different workspace declaration files
if (actualVaultInfo == null) {
actualVaultInfo = vaultInfo;
// if the module comes from a Car, then skip it if shouldSkipModulesAlreadyInCars is specified
if (options.shouldSkipModulesAlreadyInCars && isVaultElementFromCar(actualVaultInfo)) {
break; // break out of the inner loop
}
} else {
if (!actualVaultInfo.equals(vaultInfo)) {
// insert the conflicting vault info into the conflict map under the module name
LinkedHashSet<Pair<String, VaultElementInfo>> conflictingWorkspaceEntries = moduleNameToConflictingWorkspaceEntriesMap.get(moduleName);
if (conflictingWorkspaceEntries == null) {
conflictingWorkspaceEntries = new LinkedHashSet<Pair<String, VaultElementInfo>>();
moduleNameToConflictingWorkspaceEntriesMap.put(moduleName, conflictingWorkspaceEntries);
}
conflictingWorkspaceEntries.add(new Pair<String, VaultElementInfo>(workspaceDeclName, vaultInfo));
}
}
Set<ModuleName> modulesInWorkspaceDecl = workspaceDeclToModuleNamesMap.get(workspaceDeclName);
if (modulesInWorkspaceDecl == null) {
modulesInWorkspaceDecl = new HashSet<ModuleName>();
workspaceDeclToModuleNamesMap.put(workspaceDeclName, modulesInWorkspaceDecl);
}
modulesInWorkspaceDecl.add(moduleName);
}
}
// fetch information about the tree of imports using the WorkspaceLoader
Map<String, LinkedHashSet<String>> importsMap = WorkspaceLoader.getWorkspaceDeclarationImportsMap(workspaceDeclarationProvider, workspace, status);
////
/// Build the result map (workspace decl name -> Configuration) of the Configuration objects
///
/// NOTE: We produce empty Cars too (so that their cws files also get generated, in case any client code refers to them explicitly)
//
Map<String, FileOutputConfiguration> workspaceDeclToConfigMap = new HashMap<String, FileOutputConfiguration>();
// We first gather the names of the all the workspace declarations involved.
Set<String> allWorkspaceDeclsInvolved = importsMap.keySet();
// For each workspace declaration, we construct a Configuration object and add it to the result map
for (final String workspaceDeclName : allWorkspaceDeclsInvolved) {
Set<ModuleName> modulesInWorkspaceDecl = workspaceDeclToModuleNamesMap.get(workspaceDeclName);
final int nModules;
if (modulesInWorkspaceDecl == null) {
modulesInWorkspaceDecl = Collections.emptySet();
nModules = 0;
} else {
nModules = modulesInWorkspaceDecl.size();
}
Configuration config = new Configuration(workspaceManager, modulesInWorkspaceDecl.toArray(new ModuleName[nModules]));
config.setMonitor(monitor);
config.setBuildSourcelessModule(options.buildSourcelessModules);
String carName = makeCarNameFromSourceWorkspaceDeclName(workspaceDeclName);
File carFile = makeOutputCarFile(outputDirectory, carName, options);
File cwsFile;
if (options.shouldGenerateCorrespWorkspaceDecl) {
cwsFile = makeOutputCorrespWorkspaceDeclFile(outputDirectory, carName, options.noCarSuffixInWorkspaceDeclName);
} else {
cwsFile = null;
}
FileOutputConfiguration fileOutputConfig = new FileOutputConfiguration(config, carFile, carName, cwsFile);
workspaceDeclToConfigMap.put(workspaceDeclName, fileOutputConfig);
}
ConfigurationsForOneCarPerWorkspaceDeclaration configs = new ConfigurationsForOneCarPerWorkspaceDeclaration(workspaceDeclToConfigMap, moduleNameToConflictingWorkspaceEntriesMap, importsMap);
addImportsToConfigurationsForOneCarPerWorkspaceDeclaration(configs);
return configs;
}
/**
* Modify the given configurations with additional import declarations for the workspace declaration files that are to be generated.
* @param configs the configurations to be modified.
*/
private static void addImportsToConfigurationsForOneCarPerWorkspaceDeclaration(ConfigurationsForOneCarPerWorkspaceDeclaration configs) {
final Map<String, FileOutputConfiguration> workspaceDeclToConfigMap = configs.workspaceDeclToConfigMap;
for (final Map.Entry<String,FileOutputConfiguration> entry : workspaceDeclToConfigMap.entrySet()) {
String workspaceDeclName = entry.getKey();
FileOutputConfiguration fileOutputConfig = entry.getValue();
LinkedHashSet<String> imports = getListOfImports(configs, workspaceDeclName);
fileOutputConfig.setAdditionalImportsForWorkspaceDecl(imports);
}
}
/**
* Returns whether the specified vault element info refers to a vault element stored in a Car.
* @param vaultInfo the vault element info.
* @return true if the vault element is stored in a Car.
*/
private static boolean isVaultElementFromCar(VaultElementInfo vaultInfo) {
return vaultInfo instanceof VaultElementInfo.Nested &&
vaultInfo.getVaultDescriptor().equals(CarVault.getVaultClassDescriptor());
}
/**
* Builds one Car file per workspace declaration file that constitutes the initial declaration of the CAL workspace.
* @param workspaceManager the workspace manager that will provide the resources to be added to the Cars.
* @param monitor the {@link CarBuilder.Monitor} to be used.
* @param outputDirectory the output directory to which the Car files will be written.
* @param options configuration options for the builder.
* @return an array of the names of the Car files built.
* @throws FileNotFoundException if the output file is not a valid location.
* @throws IOException
*/
public static String[] buildOneCarPerWorkspaceDeclaration(WorkspaceManager workspaceManager, Monitor monitor, File outputDirectory, BuilderOptions options) throws FileNotFoundException, IOException {
boolean directoryExists = FileSystemHelper.ensureDirectoryExists(outputDirectory);
if (!directoryExists) {
throw new FileNotFoundException("Output directory does not exist: " + outputDirectory);
}
ConfigurationsForOneCarPerWorkspaceDeclaration configs = makeConfigurationsForOneCarPerWorkspaceDeclaration(workspaceManager, monitor, outputDirectory, options);
final Map<String, FileOutputConfiguration> workspaceDeclToConfigMap = configs.workspaceDeclToConfigMap;
final Map<ModuleName, LinkedHashSet<Pair<String, VaultElementInfo>>> moduleNameToConflictingWorkspaceEntriesMap = configs.moduleNameToConflictingWorkspaceEntriesMap;
////
/// If there are conflicting workspace declaration entries for any module, display a warning
//
if (!moduleNameToConflictingWorkspaceEntriesMap.isEmpty()) {
showMessagesAboutConflictingWorkspaceEntries(monitor, moduleNameToConflictingWorkspaceEntriesMap);
}
////
/// Now proceed to build the Cars, one at a time using the supplied configurations
//
// identify the total number of Cars to be built, as well as the total number of modules across all Cars
int nCars = workspaceDeclToConfigMap.size();
int nTotalModules = 0;
for (final FileOutputConfiguration fileOutputConfig : workspaceDeclToConfigMap.values()) {
nTotalModules += fileOutputConfig.getConfig().moduleNames.length;
}
// run the build helper, one per Car
monitor.operationStarted(nCars, nTotalModules);
try {
List<String> carsBuilt = new ArrayList<String>();
for (final Map.Entry<String, FileOutputConfiguration> entry : workspaceDeclToConfigMap.entrySet()) {
// break out of the loop and quit if the user canceled the operation
if (monitor.isCanceled()) {
break;
}
FileOutputConfiguration fileOutputConfig = entry.getValue();
String carName = fileOutputConfig.getCarName();
if (!options.carsToExclude.contains(carName)) {
boolean notCanceled = buildCarHelper(fileOutputConfig);
if (notCanceled) {
carsBuilt.add(carName);
}
} else {
// skip the module which is specified to be excluded
monitor.carBuildingStarted(carName, 0);
monitor.carBuildingDone(carName);
}
}
return carsBuilt.toArray(new String[carsBuilt.size()]);
} finally {
monitor.operationDone();
}
}
/**
* Show messages via the given monitor about conflicting workspace entries.
* @param monitor the monitor through which the messages will be shown.
* @param moduleNameToConflictingWorkspaceEntriesMap
*
* a map (module name -> LinkedHashSet of (Pair of (workspace decl name, VaultElementInfo))) that contains
* information about conflicting entries in workspace declaration files:
*
* A module may appear in more than one declaration, and these declaration entries may be for different vaults/revisions.
* Only one declaration entry correspond to the resources in the workspace. The remainder are conflicts. So we
* store information about each conflicting entry (a VaultElementInfo) and its corresponding workspace declaration file name.
*/
private static void showMessagesAboutConflictingWorkspaceEntries(Monitor monitor, Map<ModuleName, LinkedHashSet<Pair<String, VaultElementInfo>>> moduleNameToConflictingWorkspaceEntriesMap) {
List<String> messages = new ArrayList<String>();
for (final Map.Entry<ModuleName, LinkedHashSet<Pair<String, VaultElementInfo>>> entry : moduleNameToConflictingWorkspaceEntriesMap.entrySet()) {
ModuleName moduleName = entry.getKey();
LinkedHashSet<Pair<String, VaultElementInfo>> conflictingWorkspaceEntries = entry.getValue();
messages.add(MESSAGES.getString("moduleHasVaultInfoConflictsHeader", moduleName));
for (final Pair<String, VaultElementInfo> workspaceDeclNameAndVaultElementPair : conflictingWorkspaceEntries) {
String workspaceDeclName = workspaceDeclNameAndVaultElementPair.fst();
VaultElementInfo vaultInfo = workspaceDeclNameAndVaultElementPair.snd();
StringBuilder vaultInfoString = new StringBuilder(vaultInfo.getVaultDescriptor());
String locationString = vaultInfo.getLocationString();
if (locationString != null) {
vaultInfoString.append(' ').append(locationString);
}
vaultInfoString.append(' ').append(MESSAGES.getString("revisionNumber", Integer.valueOf(vaultInfo.getRevision())));
messages.add(MESSAGES.getString("conflictingVaultInfo", workspaceDeclName, vaultInfoString.toString()));
}
}
monitor.showMessages(messages.toArray(new String[0]));
}
/**
* Calculates the workspace declarations that should be imported by the given workspace declaration.
* Note that we elide any declaration that does not have a corresponding Car generated.
*
* @param configs the set of configurations for the various Cars being generated.
* @param workspaceDeclName the name of the workspace declaration to look up.
* @return the LinkedHashSet of declarations to be imported.
*/
private static LinkedHashSet<String> getListOfImports(ConfigurationsForOneCarPerWorkspaceDeclaration configs, String workspaceDeclName) {
LinkedHashSet<String> imports = new LinkedHashSet<String>();
Set<String> alreadyVisitedDecls = new HashSet<String>();
addToListOfImports(imports, alreadyVisitedDecls, configs, workspaceDeclName);
return imports;
}
/**
* Calculates the workspace declarations that should be imported by the given workspace declaration.
* Note that we elide any declaration that does not have a corresponding Car generated.
*
* @param imports the LinkedHashSet to be added to.
* @param alreadyVisitedDecls the workspace declarations that have already been visited.
* @param configs the set of configurations for the various Cars being generated.
* @param workspaceDeclName the name of the workspace declaration to look up.
*/
private static void addToListOfImports(Set<String> imports, Set<String> alreadyVisitedDecls, ConfigurationsForOneCarPerWorkspaceDeclaration configs, String workspaceDeclName) {
if (alreadyVisitedDecls.contains(workspaceDeclName)) {
return;
}
alreadyVisitedDecls.add(workspaceDeclName);
LinkedHashSet<String> origImports = configs.importsMap.get(workspaceDeclName);
if (origImports == null) {
return;
}
for (final String importedDecl : origImports) {
FileOutputConfiguration fileOutputConfigForImportedDecl = configs.workspaceDeclToConfigMap.get(importedDecl);
if (fileOutputConfigForImportedDecl != null) {
File cwsFile = fileOutputConfigForImportedDecl.cwsFile;
if (cwsFile != null) {
String correspWorkspaceDecl = cwsFile.getName();
if (correspWorkspaceDecl != null) {
imports.add(correspWorkspaceDecl);
}
}
} else {
// recursive call to extract the imports of this declaration which does not have a corresponding
// Car being generated.
addToListOfImports(imports, alreadyVisitedDecls, configs, importedDecl);
}
}
}
/**
* Strips the trailing '.cws' from the given string.
* @param workspaceDeclName the workspace declaration name.
* @return the given string with the trailing '.cws' stripped.
*/
private static String stripTrailingDotCWS(String workspaceDeclName) {
return workspaceDeclName.replaceAll("\\" + DOT_CWS + "$", "");
}
/**
* Strips the trailing '.car' from the given string.
* @param carName the Car name.
* @return the given string with the trailing '.car' stripped.
*/
private static String stripTrailingDotCAR(String carName) {
return carName.replaceAll("\\" + DOT_CAR + "$", "");
}
/**
* Builds a Car according to the given configuration and writes it out to the specified output file.
* @param config the configuration for building the Car.
* @param outputDir the output directory to be written to.
* @param options configuration options for the Car builder.
* @throws FileNotFoundException if the output file is not a valid location.
* @return true if, at the end, the operation is not canceled by the user.
* @throws IOException
*/
public static boolean buildCar(Configuration config, File outputDir, String carName, BuilderOptions options) throws FileNotFoundException, IOException {
File cwsFile;
if (options.shouldGenerateCorrespWorkspaceDecl) {
cwsFile = makeOutputCorrespWorkspaceDeclFile(outputDir, carName, options.noCarSuffixInWorkspaceDeclName);
} else {
cwsFile = null;
}
FileOutputConfiguration fileOutputConfig = new FileOutputConfiguration(config, makeOutputCarFile(outputDir, carName, options), carName, cwsFile);
return buildCar(fileOutputConfig);
}
/**
* Constructs a Car name from a source workspace declaration name. For example
* everything.default.cws -> everything.default.car
*
* @param sourceWorkspaceDeclName the source workspace declaration name.
* @return the corresponding Car name.
*/
public static String makeCarNameFromSourceWorkspaceDeclName(String sourceWorkspaceDeclName) {
return stripTrailingDotCWS(sourceWorkspaceDeclName) + DOT_CAR;
}
/**
* Constructs a {@link File} for the named Car to be generated.
* @param rootOutputDir the root output directory.
* @param carName the name of the Car.
* @param options the associated BuilderOptions object for this Car building operation.
* @return the corresponding File object.
*/
private static File makeOutputCarFile(File rootOutputDir, String carName, BuilderOptions options) {
if (options.shouldGenerateCarJarSuffix) {
return new File(rootOutputDir, carName + DOT_JAR);
} else {
File carFolder = new File(rootOutputDir, CAR_FOLDER_NAME);
return new File(carFolder, carName);
}
}
/**
* Constructs a {@link File} for the corresponding workspace declaration to be generated for the given Car.
* @param rootOutputDir the root output directory.
* @param carName the name of the Car.
* @param noCarSuffixInWorkspaceDeclName whether the generated workspace declaration should end simply with '.cws' instead of '.car.cws'.
* @return the corresponding File object.
*/
private static File makeOutputCorrespWorkspaceDeclFile(File rootOutputDir, String carName, boolean noCarSuffixInWorkspaceDeclName) {
String cwsName = makeOutputCorrespWorkspaceDeclName(carName, noCarSuffixInWorkspaceDeclName);
File workspaceDeclFolder = new File(rootOutputDir, WORKSPACE_DECLARATIONS_FOLDER_NAME);
return new File(workspaceDeclFolder, cwsName);
}
/**
* Constructs a name for the corresponding workspace declaration to be generated for the given Car. For example
* everything.default.car -> everything.default.car.cws (if noCarSuffixInWorkspaceDeclName is false), OR
* everything.default.car -> everything.default.cws (if noCarSuffixInWorkspaceDeclName is true)
*
* @param carName the name of the Car.
* @param noCarSuffixInWorkspaceDeclName whether the generated workspace declaration should end simply with '.cws' instead of '.car.cws'.
* @return the name of the workspace declaration file.
*/
public static String makeOutputCorrespWorkspaceDeclName(String carName, boolean noCarSuffixInWorkspaceDeclName) {
if (noCarSuffixInWorkspaceDeclName) {
return stripTrailingDotCAR(carName) + DOT_CWS;
} else {
return carName + DOT_CWS;
}
}
/**
* Builds a Car according to the given configuration and writes it out to the specified output file.
* @param fileOutputConfig the configuration for building the Car.
* @throws FileNotFoundException if the output file is not a valid location.
* @return true if, at the end, the operation is not canceled by the user.
* @throws IOException
*/
public static boolean buildCar(FileOutputConfiguration fileOutputConfig) throws FileNotFoundException, IOException {
final Configuration config = fileOutputConfig.getConfig();
config.monitor.operationStarted(1, config.moduleNames.length);
try {
return buildCarHelper(fileOutputConfig);
} finally {
config.monitor.operationDone();
}
}
/**
* Builds a Car according to the given configuration and writes it out to the specified output file.
* @param fileOutputConfig the configuration for building the Car.
* @throws FileNotFoundException if the output file is not a valid location.
* @return true if, at the end, the operation is not canceled by the user.
* @throws IOException
*/
private static boolean buildCarHelper(FileOutputConfiguration fileOutputConfig) throws FileNotFoundException, IOException {
final Configuration config = fileOutputConfig.getConfig();
final File outputFile = fileOutputConfig.getCarFile();
final String carName = fileOutputConfig.getCarName();
final boolean shouldGenerateCarJarSuffix = fileOutputConfig.shouldGenerateCarJarSuffix();
// generate the corresponding cws file if requested
final File cwsFile = fileOutputConfig.getWorkspaceDeclarationFile();
if (cwsFile != null) {
final String cwsName = cwsFile.getName();
final Collection<String> additionalImportsForWorkspaceDecl = fileOutputConfig.getAdditionalImportsForWorkspaceDecl();
if (shouldGenerateCarJarSuffix) {
StringWriter writer = new StringWriter();
writeCorrespondingWorkspaceDeclaration(config, carName, additionalImportsForWorkspaceDecl, writer, cwsName);
byte[] cwsContents = TextEncodingUtilities.getUTF8Bytes(writer.toString());
config.addAdditionalFile(WorkspaceDeclarationNullaryStore.WORKSPACE_DECLARATION_IN_JAR_BASE_FOLDER.extendFile(cwsName).getPathStringMinusSlash(), cwsContents);
} else {
FileSystemHelper.ensureDirectoryExists(cwsFile.getParentFile());
Writer writer = new FileWriter(cwsFile);
writeCorrespondingWorkspaceDeclaration(config, carName, additionalImportsForWorkspaceDecl, writer, cwsName);
}
}
return buildCarHelper(config, carName, outputFile);
}
/**
* Writes the corresponding workspace declaration that references the Car being built to the specified writer.
* @param config the Car's configuration.
* @param carName the name of the Car.
* @param additionalImportsForWorkspaceDecl additional imports to be added to the workspace declaration.
* @param writer the Writer to write to.
* @param cwsName the name of the workspace declaration file to generate.
* @throws IOException
*/
private static void writeCorrespondingWorkspaceDeclaration(Configuration config, String carName, Collection<String> additionalImportsForWorkspaceDecl, Writer writer, String cwsName) throws IOException {
try {
PrintWriter printWriter = new PrintWriter(writer);
String comment = MESSAGES.getString("generatedCWSFileComment", cwsName, new Date());
String[] commentLines = comment.split("\n");
for (final String element : commentLines) {
printWriter.println(element);
}
printWriter.println();
printWriter.println(config.getWorkspaceDeclarationImportLineForStandardVaultCar(carName));
for (final String additionalImport : additionalImportsForWorkspaceDecl) {
printWriter.println("import StandardVault " + additionalImport);
}
printWriter.flush();
} finally {
writer.close();
}
}
////===========================================================================================
/// Fundamental routines for building a single Car.
//
/**
* Builds a Car according to the given configuration and writes it out to the specified output file.
* @param config the configuration for building the Car.
* @param carName the canonical name of the Car to be built.
* @param outputFile the output file to be written to.
* @throws FileNotFoundException if the output file is not a valid location.
* @return true if, at the end, the operation is not canceled by the user.
* @throws IOException
*/
public static boolean buildCar(Configuration config, String carName, File outputFile) throws FileNotFoundException, IOException {
config.monitor.operationStarted(1, config.moduleNames.length);
try {
return buildCarHelper(config, carName, outputFile);
} finally {
config.monitor.operationDone();
}
}
/**
* Builds a Car according to the given configuration and writes it out to the specified output file.
* @param config the configuration for building the Car.
* @param carName the canonical name of the Car to be built.
* @param outputFile the output file to be written to.
* @throws FileNotFoundException if the output file is not a valid location.
* @return true if, at the end, the operation is not canceled by the user.
* @throws IOException
*/
private static boolean buildCarHelper(Configuration config, String carName, File outputFile) throws FileNotFoundException, IOException {
FileSystemHelper.ensureDirectoryExists(outputFile.getParentFile());
FileOutputStream fos = new FileOutputStream(outputFile);
boolean notCanceled = false;
try {
notCanceled = buildCarHelper(config, carName, outputFile.getName(), fos);
return notCanceled;
} finally {
fos.close();
// delete the incomplete Car if the operation was canceled or the operation failed with an exception
if (!notCanceled) {
outputFile.delete();
}
}
}
/**
* Builds a Car according to the given configuration and writes it out to the specified output stream.
* @param config the configuration for building the Car.
* @param carName the name of the Car to be built.
* @param outputStream the output stream to be written to.
* @return true if, at the end, the operation is not canceled by the user.
* @throws IOException
*/
public static boolean buildCar(Configuration config, String carName, OutputStream outputStream) throws IOException {
config.monitor.operationStarted(1, config.moduleNames.length);
try {
return buildCarHelper(config, carName, carName, outputStream);
} finally {
config.monitor.operationDone();
}
}
/**
* Builds a Car according to the given configuration and writes it out to the specified output stream.
* @param config the configuration for building the Car.
* @param carName the canonical name of the Car to be built.
* @param fileName the file name of the Car to be built.
* @param outputStream the output stream to be written to.
* @return true if, at the end, the operation is not canceled by the user.
* @throws IOException
*/
private static boolean buildCarHelper(Configuration config, String carName, String fileName, OutputStream outputStream) throws IOException {
// the configuration came with a canceled monitor, so we shouldn't do anything
if (config.monitor.isCanceled()) {
return false;
}
WorkspaceManager workspaceManager = config.workspaceManager;
ModuleName[] moduleNames = config.moduleNames;
Monitor monitor = config.monitor;
CALWorkspace workspace = workspaceManager.getWorkspace();
ProgramResourcePathMapper programResourcePathMapper = new ProgramResourcePathMapper(workspaceManager.getMachineType());
BufferedOutputStream bos = new BufferedOutputStream(outputStream, 1024);
JarOutputStream jos = new JarOutputStream(bos);
try {
// Write out a minimal manifest.
ModulePackager.writeMinimalManifestToCar(jos, carName);
// Set compression level.
jos.setLevel(Deflater.DEFAULT_COMPRESSION);
// Write out the Car marker
ZipEntry markerEntry = new ZipEntry(CAR_MARKER_NAME);
jos.putNextEntry(markerEntry);
jos.closeEntry();
// Write out the spec files
for (final Map.Entry<String,CarSpec> entry : config.carSpecs.entrySet()) {
String specName = entry.getKey();
CarSpec carSpec = entry.getValue();
ZipEntry zipEntry = new ZipEntry(Car.CAR_WORKSPACE_SPEC_FOLDER.extendFile(specName).getPathStringMinusSlash());
jos.putNextEntry(zipEntry);
try {
carSpec.writeTo(jos);
} finally {
jos.closeEntry();
}
}
// Add each module and all their resources to the Car
monitor.carBuildingStarted(fileName, moduleNames.length);
for (int i = 0; i < moduleNames.length && !monitor.isCanceled(); i++) {
ModuleName moduleName = moduleNames[i];
monitor.processingModule(fileName, moduleName);
ModulePackager.writeModuleToJar(workspace, moduleName, jos, config.buildSourcelessModules);
writeModuleProgramResourcesToCar(workspaceManager, moduleName, jos, programResourcePathMapper);
}
// Add the additional files to the Car.
List<String> additionalFileRelativePaths = new ArrayList<String>();
for (final Map.Entry<String, byte[]> entry : config.additionalFiles.entrySet()) {
if (monitor.isCanceled()) {
break;
}
String relativePath = entry.getKey();
byte[] bytes = entry.getValue();
ZipEntry zipEntry = new ZipEntry(relativePath);
jos.putNextEntry(zipEntry);
try {
jos.write(bytes);
} finally {
jos.closeEntry();
}
additionalFileRelativePaths.add(relativePath);
}
// Write out the list of additional files.
ZipEntry additionalFileListEntry = new ZipEntry(CAR_ADDITIONAL_FILES_NAME);
jos.putNextEntry(additionalFileListEntry);
try {
StringBuilder builder = new StringBuilder();
for (int k = 0, n = additionalFileRelativePaths.size(); k < n; k++) {
if (k > 0) {
builder.append('\n');
}
builder.append(additionalFileRelativePaths.get(k));
}
writeStringToStream(jos, builder.toString());
} finally {
jos.closeEntry();
}
return !config.monitor.isCanceled();
} finally {
monitor.carBuildingDone(fileName);
jos.flush();
bos.flush();
jos.close();
bos.close();
}
}
/**
* Writes the program resources (e.g. cmi and lc files) of the specified module to the Car through the given JarOutputStream.
*
* @param workspaceManager the workspace manager providing the resources.
* @param moduleName the name of the module.
* @param jos the JarOutputStream for the Car.
* @param programResourcePathMapper the path mapper for program resources.
* @throws IOException
*/
private static void writeModuleProgramResourcesToCar(WorkspaceManager workspaceManager, ModuleName moduleName, JarOutputStream jos, ProgramResourcePathMapper programResourcePathMapper) throws IOException {
ResourcePath.Folder moduleFolderPath = (ResourcePath.Folder)programResourcePathMapper.getModuleResourcePath(moduleName);
ProgramResourceLocator.Folder folderLocator = new ProgramResourceLocator.Folder(moduleName, ResourcePath.EMPTY_PATH);
ProgramResourceRepository programResourceRepository = workspaceManager.getRepository();
ProgramResourceLocator[] resourceLocators = programResourceRepository.getMembers(folderLocator);
for (final ProgramResourceLocator resourceLocator : resourceLocators) {
if (resourceLocator instanceof ProgramResourceLocator.File) {
ProgramResourceLocator.File fileLocator = (ProgramResourceLocator.File)resourceLocator;
String relativePath = moduleFolderPath.extendFile(resourceLocator.getName()).getPathStringMinusSlash();
ZipEntry entry = new ZipEntry(relativePath);
jos.putNextEntry(entry);
InputStream inputStream = programResourceRepository.getContents(fileLocator); // this call throws IOException, and should not return null on error
try {
FileSystemResourceHelper.transferData(inputStream, jos);
} finally {
// We need to close the input stream explicitly or else we will be leaking file handles
inputStream.close();
jos.closeEntry();
}
}
}
}
/**
* Writes the given string to the specified stream.
* @param outputStream
* @param string
* @throws IOException
*/
private static void writeStringToStream(OutputStream outputStream, String string) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(baos);
osw.write(string);
osw.flush();
baos.flush();
byte[] bytes = baos.toByteArray();
outputStream.write(bytes);
}
}