/*******************************************************************************
* Copyright (c) 2011, 2014 Zend Technologies Ltd.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package org.zend.sdklib.application;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.xml.bind.JAXBException;
import org.zend.sdklib.SdkException;
import org.zend.sdklib.descriptor.pkg.Package;
import org.zend.sdklib.descriptor.pkg.Version;
import org.zend.sdklib.internal.library.AbstractChangeNotifier;
import org.zend.sdklib.internal.mapping.LibraryMapping;
import org.zend.sdklib.internal.project.ProjectResourcesWriter;
import org.zend.sdklib.internal.utils.JaxbHelper;
import org.zend.sdklib.mapping.IMapping;
import org.zend.sdklib.mapping.IMappingEntry;
import org.zend.sdklib.mapping.IMappingEntry.Type;
import org.zend.sdklib.mapping.IMappingLoader;
import org.zend.sdklib.mapping.IMappingModel;
import org.zend.sdklib.mapping.IVariableResolver;
import org.zend.sdklib.mapping.MappingModelFactory;
import org.zend.sdklib.project.DeploymentScriptTypes;
import org.zend.webapi.core.progress.BasicStatus;
import org.zend.webapi.core.progress.IChangeNotifier;
import org.zend.webapi.core.progress.StatusCode;
/**
* Provides ability to create ZPK application package.
*
* @author Wojciech Galanciak, 2011
* @author Kaloyan Raev, 2014
*/
public class PackageBuilder extends AbstractChangeNotifier {
public static final String EXTENSION = ".zpk";
private static final int BUFFER = 1024;
private static final int STEPS = 10;
protected File container;
protected File configLocation;
protected IMappingModel model;
private ZipOutputStream out;
private Set<String> addedPaths;
private IVariableResolver variableResolver;
private int resolution;
private int progress;
public PackageBuilder(File container, File configLocation,
IMappingLoader loader, IChangeNotifier notifier) {
super(notifier);
this.container = container;
this.configLocation = configLocation;
this.model = loader != null ? MappingModelFactory.createModel(loader,
configLocation) : MappingModelFactory
.createDefaultModel(configLocation);
this.addedPaths = new HashSet<String>();
}
public PackageBuilder(File container, File configLocation,
IChangeNotifier notifier) {
this(container, configLocation, null, notifier);
}
public PackageBuilder(File container, File configLocation,
IMappingLoader loader) {
super();
this.container = container;
this.configLocation = configLocation;
this.model = loader != null ? MappingModelFactory.createModel(loader,
configLocation) : MappingModelFactory
.createDefaultModel(configLocation);
this.addedPaths = new HashSet<String>();
}
public PackageBuilder(File container, File configLocation) {
this(container, configLocation, (IMappingLoader) null);
}
public PackageBuilder(File container, IMappingLoader loader,
IChangeNotifier notifier) {
this(container, container, loader, notifier);
}
public PackageBuilder(File container, IChangeNotifier notifier) {
this(container, container, notifier);
}
public PackageBuilder(File container, IMappingLoader loader) {
this(container, container, loader);
}
public PackageBuilder(File container) {
this(container, (IMappingLoader) null);
}
public void setVariableResolver(IVariableResolver variableResolver) {
this.variableResolver = variableResolver;
}
/**
* @param directory
* @return the file name to be created when deployment package file is
* created
* @throws IOException
*/
public File getDeploymentPackageFile(File directory) throws IOException {
if (directory == null || !directory.isDirectory()) {
log.error(new IllegalArgumentException(
"Location cannot be null or non-existing directory"));
return null;
}
configLocation = configLocation.getCanonicalFile();
String name = getPackageName(configLocation);
if (name == null) {
return null;
}
return new File(directory, name + EXTENSION);
}
/**
* Creates compressed package file in the given folder.
*
* @param path
* - location where package should be created
* @return
*/
public File createDeploymentPackage(File directory) {
try {
container = container.getCanonicalFile();
File result = getDeploymentPackageFile(directory);
if (result == null) {
return null;
}
prepareOutputFile(result);
if (!model.isLoaded()) {
createDefaultModel();
}
int totalWork = calculateTotalWork();
resolution = (int) totalWork / STEPS;
// extra step reserved for finishing output file
notifier.statusChanged(new BasicStatus(StatusCode.STARTING, "Package creation",
"Creating " + result.getName() + " deployment package...", STEPS + 1));
File descriptorFile = new File(configLocation,
ProjectResourcesWriter.DESCRIPTOR);
addFileToPackage(descriptorFile, null, null, null, false);
resolveIconAndLicence();
resolveMappings();
finishOutputFile(result);
notifier.statusChanged(
new BasicStatus(StatusCode.PROCESSING, "Package creation", "Creating deployment package...", 1));
notifier.statusChanged(new BasicStatus(StatusCode.STOPPING,
"Package creation",
"Deployment package created successfully."));
return result;
} catch (IOException e) {
notifier.statusChanged(new BasicStatus(StatusCode.ERROR,
"Package creation",
"Error during building deployment package", e));
log.error("Error during building deployment package");
log.error(e);
}
return null;
}
/**
* Creates compressed package file in the given location.
*
* @param destination
* - location where package should be created
* @return
*/
public File createDeploymentPackage(String destination) {
return createDeploymentPackage(new File(destination));
}
/**
* Creates compressed package file in the current location.
*
* @param path
* - location where package should be created
* @return
*/
public File createDeploymentPackage() {
return createDeploymentPackage(new File("."));
}
/**
* Prepares the output ZPK file that will contain the exported project.
*
* <p>
* This method is called before any mapping is resolved yet. Subclasses may
* override this method to provide alternative way of creating the ZPK file.
* </p>
*
* @param zpkFile
* the ZPK file
*
* @throws IOException
* if an error occurs
*/
protected void prepareOutputFile(File zpkFile) throws IOException {
out = new ZipOutputStream(new BufferedOutputStream(
new FileOutputStream(zpkFile)));
}
/**
* Finishes the output ZPK file that will contain the exported project.
*
* <p>
* This method is called after all mappings are resolved. Subclasses must
* override this method if they have already overridden
* {@link #prepareOutputFile(File)}.
* </p>
*
* @param zpkFile
* the ZPK file
*
* @throws IOException
* if an error occurs
*/
protected void finishOutputFile(File zpkFile) throws IOException {
out.close();
}
/**
* Adds a file from the project being exported to the output ZPK file.
*
* <p>
* This method is called for each mapping being resolved. Subclasses must
* override this method if they have already overridden
* {@link #prepareOutputFile(File)}.
* </p>
*
* @param file
* file to add
* @param relativePath
* relative path of the file to the project root
*
* @throws IOException
* if an error occurs
*/
protected void addFileToOutput(File file, String relativePath)
throws IOException {
ZipEntry entry = new ZipEntry(relativePath);
out.putNextEntry(entry);
int count;
byte data[] = new byte[BUFFER];
BufferedInputStream in = new BufferedInputStream(new FileInputStream(
file), BUFFER);
while ((count = in.read(data, 0, BUFFER)) != -1) {
out.write(data, 0, count);
}
in.close();
}
/**
* Adds an empty directory to the output ZPK file.
*
* <p>
* This method may be called while the mapping are being resolved.
* Subclasses must override this method if they have already overridden
* {@link #prepareOutputFile(File)}.
* </p>
*
* @param directory
* directory to add
*
* @throws IOException
* if an error occurs
*/
protected void addEmptyDirectoryToOutput(File directory) throws IOException {
String location = directory.getCanonicalPath();
String path = getContainerRelativePath(location) + "/";
ZipEntry entry = new ZipEntry(path.replaceAll("\\\\", "/"));
out.putNextEntry(entry);
}
private void resolveIconAndLicence() {
String icon = getIconName(configLocation);
if (icon != null) {
try {
addFileToPackage(new File(container, icon), null, null, null,
false);
} catch (IOException e) {
// do nothing, it means that descriptor has entries which are
// not valid
}
}
String license = getLicenseName(configLocation);
if (license != null) {
try {
addFileToPackage(new File(container, license), null, null,
null, false);
} catch (IOException e) {
// do nothing, it means that descriptor has entries which are
// not valid
}
}
}
private void resolveMappings() throws IOException {
String appdir = getAppdirName(configLocation);
String scriptsdir = getScriptsdirName(configLocation);
if (appdir != null) {
if (!appdir.isEmpty()) {
addEmptyDirectoryToOutput(new File(container, appdir));
}
resolveMapping(IMappingModel.APPDIR, appdir, false);
resolveLibraryMapping(appdir, false);
}
if (scriptsdir != null && !scriptsdir.isEmpty()) {
addEmptyDirectoryToOutput(new File(container, scriptsdir));
resolveMapping(IMappingModel.SCRIPTSDIR, scriptsdir, true);
}
}
private void resolveLibraryMapping(String folderName, boolean b)
throws IOException {
List<IMappingEntry> entries = model.getEnties(Type.INCLUDE, "library");
for (IMappingEntry entry : entries) {
List<IMapping> mappings = entry.getMappings();
for (IMapping mapping : mappings) {
LibraryMapping libraryMapping = LibraryMapping.create(
entry.getFolder(), mapping.getPath());
if (libraryMapping != null) {
String mappingFolder = folderName;
if (!libraryMapping.getFolder().isEmpty()) {
mappingFolder += File.separator
+ libraryMapping.getFolder();
}
String library = resolveVariables(libraryMapping
.getLibraryPath());
File libraryFile = new File(library);
if (!libraryFile.isAbsolute()) {
libraryFile = new File(container, library);
}
File resource = new File(libraryFile.getCanonicalPath());
if (resource.exists()) {
addFileToPackage(resource, mappingFolder, library,
"library", false);
}
}
}
}
}
private String resolveVariables(String libraryPath) throws IOException {
try {
return variableResolver != null ? variableResolver
.resolve(libraryPath) : libraryPath;
} catch (SdkException e) {
throw new IOException(e);
}
}
private void resolveMapping(String tag, String folderName, boolean allowFlat)
throws IOException {
List<IMappingEntry> entries = model.getEnties(Type.INCLUDE, tag);
for (IMappingEntry entry : entries) {
List<IMapping> mappings = entry.getMappings();
for (IMapping mapping : mappings) {
File resource = new File(container, mapping.getPath());
if (resource.exists()) {
allowFlat &= resource.isDirectory() && entries.size() == 1
&& mappings.size() == 1;
addFileToPackage(resource, folderName, mapping.getPath(),
tag, allowFlat);
}
}
}
}
private void addFileToPackage(File root, String mappingFolder,
String mappingPath, String tag, boolean allowFlat)
throws IOException {
if (!model.isExcluded(tag, root.getCanonicalPath())) {
if (root.isDirectory() && !isExcludeAllChildren(tag, root)) {
File[] children = root.listFiles();
for (File child : children) {
addFileToPackage(child, mappingFolder, mappingPath, tag,
allowFlat);
}
} else {
String location = root.getCanonicalPath();
String path = getContainerRelativePath(location);
if (mappingPath != null) {
path = root.getCanonicalPath();
File file = new File(mappingPath);
if (!file.isAbsolute()) {
file = new File(container, mappingPath);
}
String fullMapping = file.getCanonicalPath();
String destFolder = path.substring((allowFlat ? path
: fullMapping).lastIndexOf(File.separator));
path = mappingFolder + destFolder;
}
if (root.isDirectory()) {
path += "/";
}
path = path.replaceAll("\\\\", "/");
if (path.startsWith("/")) {
path = path.substring(1);
}
if (addedPaths.add(path)) {
if (!root.isDirectory()) {
addFileToOutput(root, path);
}
progress++;
if (progress >= resolution) {
notifier.statusChanged(new BasicStatus(
StatusCode.PROCESSING, "Package creation",
"Creating deployment package...", 1));
progress = 0;
}
}
}
}
}
private boolean isExcludeAllChildren(String tag, File root)
throws IOException {
File[] children = root.listFiles();
for (File file : children) {
if (!model.isExcluded(tag, file.getCanonicalPath())) {
return false;
}
}
return true;
}
private String getContainerRelativePath(String path) {
String containerPath = container.getAbsolutePath();
int position = containerPath.length() + 1;
if (!path.startsWith(containerPath) || position >= path.length()) {
containerPath = configLocation.getAbsolutePath();
position = containerPath.length() + 1;
if (!path.startsWith(containerPath) || position >= path.length()) {
return path;
}
}
return path.substring(position);
}
private String getPackageName(File container) {
String result = null;
Package p = getPackage(container);
if (p != null) {
String name = p.getName();
final Version version2 = p.getVersion();
if (version2 == null) {
throw new IllegalStateException(
"Error, missing <version> element in deployment descriptor");
}
String version = version2.getRelease();
if (name != null && version != null) {
result = name + "-" + version;
}
}
return result;
}
protected String getAppdirName(File container) {
String result = null;
Package p = getPackage(container);
if (p != null) {
if ("library".equals(p.getType())) {
result = p.getLibdir();
} else {
result = p.getAppdir();
}
}
if (result == null) {
result = "";
}
return result;
}
private String getScriptsdirName(File container) {
String result = null;
Package p = getPackage(container);
if (p != null) {
result = p.getScriptsdir();
}
return result;
}
private String getIconName(File container) {
String result = null;
Package p = getPackage(container);
if (p != null) {
result = p.getIcon();
}
return result;
}
private String getLicenseName(File container) {
String result = null;
Package p = getPackage(container);
if (p != null) {
result = p.getEula();
}
return result;
}
private Package getPackage(File container) {
File descriptorFile = new File(container,
ProjectResourcesWriter.DESCRIPTOR);
if (!descriptorFile.exists()) {
log.error(descriptorFile.getAbsoluteFile() + " does not exist.");
return null;
}
FileInputStream pkgStream = null;
Package p = null;
try {
pkgStream = new FileInputStream(descriptorFile);
p = JaxbHelper.unmarshalPackage(pkgStream);
} catch (IOException e) {
throw new IllegalStateException(e);
} catch (JAXBException e) {
throw new IllegalStateException(e);
} finally {
try {
pkgStream.close();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
return p;
}
private void createDefaultModel() throws IOException {
if (container.isDirectory()) {
String scriptdir = getScriptsdirName(configLocation);
File[] files = container.listFiles();
for (File file : files) {
String name = file.getName();
if (!model.isExcluded(null, name) && !shoudBeExcluded(name)) {
if (name.equals(scriptdir) && file.isDirectory()) {
String[] scripts = file.list();
for (String script : scripts) {
if (DeploymentScriptTypes.byName(script) != null) {
String path = name + "/" + script;
model.addMapping(IMappingModel.SCRIPTSDIR,
Type.INCLUDE, path, false);
}
}
} else {
model.addMapping(IMappingModel.APPDIR, Type.INCLUDE,
name, false);
}
}
}
if (scriptdir != null
&& model.getEntry(IMappingModel.SCRIPTSDIR, Type.INCLUDE)
.getMappings().size() == 0) {
notifier.statusChanged(new BasicStatus(StatusCode.WARNING,
"Package creation",
"Scriptsdir declared in descriptor file does not exist in the project"));
log.warning("Scriptsdir declared in descriptor file does not exist in the project");
}
}
}
private boolean shoudBeExcluded(String name) {
return ProjectResourcesWriter.DESCRIPTOR.equals(name)
|| name.toLowerCase().contains("test") || name.startsWith(".");
}
private int calculateTotalWork() throws IOException {
// is 1 because of deployment.xml file which is always added to the
// package
int totalWork = 1;
List<String> folders = model.getFolders();
for (String folder : folders) {
IMappingEntry entry = model.getEntry(folder, Type.INCLUDE);
if (entry != null) {
List<IMapping> includes = entry.getMappings();
for (IMapping mapping : includes) {
String path = mapping.getPath();
LibraryMapping libraryMapping = LibraryMapping.create(
entry.getFolder(), path);
if (libraryMapping != null) {
path = libraryMapping.getLibraryPath();
}
File file = new File(path);
if (!file.isAbsolute()) {
file = new File(container, mapping.getPath());
}
if (file.exists()) {
totalWork += countFiles(file, folder);
}
}
}
}
return totalWork;
}
private int countFiles(File file, String folder) throws IOException {
int counter = 0;
if (!model.isExcluded(folder, file.getCanonicalPath())) {
if (file.isDirectory()) {
File[] children = file.listFiles();
for (File child : children) {
counter += countFiles(child, folder);
}
} else {
counter++;
}
}
return counter;
}
}