/*
* Copyright (c) 2012 Data Harmonisation Panel
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution. If not, see <http://www.gnu.org/licenses/>.
*
* Contributors:
* HUMBOLDT EU Integrated Project #030962
* Data Harmonisation Panel <http://www.dhpanel.eu>
*/
package eu.esdihumboldt.hale.common.core.io.project.impl;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import eu.esdihumboldt.hale.common.core.io.IOProviderConfigurationException;
import eu.esdihumboldt.hale.common.core.io.ImportProvider;
import eu.esdihumboldt.hale.common.core.io.ProgressIndicator;
import eu.esdihumboldt.hale.common.core.io.Value;
import eu.esdihumboldt.hale.common.core.io.impl.AbstractExportProvider;
import eu.esdihumboldt.hale.common.core.io.impl.AbstractIOProvider;
import eu.esdihumboldt.hale.common.core.io.project.ProjectIO;
import eu.esdihumboldt.hale.common.core.io.project.model.IOConfiguration;
import eu.esdihumboldt.hale.common.core.io.project.model.Project;
import eu.esdihumboldt.hale.common.core.io.project.model.ProjectFile;
import eu.esdihumboldt.hale.common.core.io.project.model.ProjectFileInfo;
import eu.esdihumboldt.hale.common.core.io.report.IOReport;
import eu.esdihumboldt.hale.common.core.io.report.IOReporter;
import eu.esdihumboldt.hale.common.core.io.report.impl.IOMessageImpl;
import eu.esdihumboldt.hale.common.core.io.supplier.LocatableOutputSupplier;
import eu.esdihumboldt.util.io.IOUtils;
import eu.esdihumboldt.util.io.OutputStreamDecorator;
/**
* Writes a project file
*
* @author Simon Templer
*/
public class DefaultProjectWriter extends AbstractProjectWriter {
/**
* Output stream for a ZIP entry
*/
private static class EntryOutputStream extends OutputStreamDecorator {
private final ZipOutputStream zip;
/**
* Create an output stream for a ZIP entry
*
* @param zip the ZIP output stream
*/
public EntryOutputStream(ZipOutputStream zip) {
super(zip);
this.zip = zip;
}
/**
* @see OutputStreamDecorator#close()
*/
@Override
public void close() throws IOException {
// instead of closing the stream close the entry
zip.closeEntry();
}
}
/**
* The configuration parameter name for detailing if project files are to be
* placed outside the project archive
*/
public static final String PARAM_SEPARATE_FILES = "projectFiles.separate";
/**
* If project files are to be placed outside the archive. Only has effect if
* {@link #archive} is <code>true</code>
*/
private boolean useSeparateFiles = false;
/**
* If the project shall be saved to a ZIP archive
*/
private final boolean archive;
/**
* @param archive if the project shall be saved to a ZIP archive
*/
public DefaultProjectWriter(boolean archive) {
super();
this.archive = archive;
}
/**
* @see AbstractExportProvider#validate()
*/
@Override
public void validate() throws IOProviderConfigurationException {
super.validate();
if (getProject() == null) {
fail("The main project file has not been set");
}
}
/**
* @see AbstractExportProvider#storeConfiguration(Map)
*/
@Override
public void storeConfiguration(Map<String, Value> configuration) {
// store if separate files are to be used
configuration.put(PARAM_SEPARATE_FILES, Value.of(useSeparateFiles));
super.storeConfiguration(configuration);
}
@Override
public void setParameter(String name, Value value) {
if (name.equals(PARAM_SEPARATE_FILES)) {
setUseSeparateFiles(value.as(Boolean.class));
}
else {
super.setParameter(name, value);
}
}
/**
* @see AbstractIOProvider#execute(ProgressIndicator, IOReporter)
*/
@Override
protected IOReport execute(ProgressIndicator progress, IOReporter reporter)
throws IOProviderConfigurationException, IOException {
boolean separateProjectFiles = !archive || isUseSeparateFiles();
URI targetLocation = getTarget().getLocation();
File targetFile;
try {
targetFile = new File(targetLocation);
} catch (Exception e) {
if (!archive) {
// cannot save as XML if it's not a file
reporter.error(new IOMessageImpl("Could not determine project file path.", e));
reporter.setSuccess(false);
return reporter;
}
targetFile = null;
// if it's not a file, we must save the project files inside the zip
// stream
separateProjectFiles = false;
}
int entries = 1;
if (getProjectFiles() != null) {
entries += getProjectFiles().size();
}
progress.begin("Save project", entries);
// clear project file information that may already be contained in the
// project
getProject().getProjectFiles().clear();
// write additional project files if they are to be placed in separate
// files
if (separateProjectFiles && targetFile != null) {
for (Entry<String, ProjectFile> entry : getProjectFiles().entrySet()) {
String name = entry.getKey();
// determine target file for project file
String projectFileName = targetFile.getName() + "." + name;
final File pfile = new File(targetFile.getParentFile(), projectFileName);
// the following line is basically
// URI.create(escape(projectFileName))
URI relativeProjectFile = targetFile.getParentFile().toURI()
.relativize(pfile.toURI());
// add project file information to project
getProject().getProjectFiles().add(new ProjectFileInfo(name, relativeProjectFile));
// write entry
ProjectFile file = entry.getValue();
try {
LocatableOutputSupplier<OutputStream> target = new LocatableOutputSupplier<OutputStream>() {
@Override
public OutputStream getOutput() throws IOException {
return new BufferedOutputStream(new FileOutputStream(pfile));
}
@Override
public URI getLocation() {
return pfile.toURI();
}
};
file.store(target);
} catch (Exception e) {
reporter.error(new IOMessageImpl("Error saving a project file.", e));
reporter.setSuccess(false);
return reporter;
}
progress.advance(1);
}
}
updateRelativeResourcePaths(getProject().getResources(), getPreviousTarget(),
targetLocation);
if (archive) {
// save to archive
final ZipOutputStream zip = new ZipOutputStream(new BufferedOutputStream(getTarget()
.getOutput()));
try {
// write main entry
zip.putNextEntry(new ZipEntry(ProjectIO.PROJECT_FILE));
try {
Project.save(getProject(), new EntryOutputStream(zip));
} catch (Exception e) {
reporter.error(new IOMessageImpl("Could not save main project configuration.",
e));
reporter.setSuccess(false);
return reporter;
}
zip.closeEntry();
progress.advance(1);
// write additional project files to zip stream
if (getProjectFiles() != null && !separateProjectFiles) {
for (Entry<String, ProjectFile> entry : getProjectFiles().entrySet()) {
String name = entry.getKey();
if (name.equalsIgnoreCase(ProjectIO.PROJECT_FILE)) {
reporter.error(new IOMessageImpl(
"Invalid file name {0}. File name may not match the name of the main project configuration.",
null, -1, -1, name));
}
else {
// write entry
zip.putNextEntry(new ZipEntry(name));
ProjectFile file = entry.getValue();
try {
LocatableOutputSupplier<OutputStream> target = new LocatableOutputSupplier<OutputStream>() {
private boolean first = true;
@Override
public OutputStream getOutput() throws IOException {
if (first) {
first = false;
return new EntryOutputStream(zip);
}
throw new IllegalStateException(
"Output stream only available once");
}
@Override
public URI getLocation() {
return getTarget().getLocation();
}
};
file.store(target);
} catch (Exception e) {
reporter.error(new IOMessageImpl("Error saving a project file.", e));
reporter.setSuccess(false);
return reporter;
}
zip.closeEntry();
}
progress.advance(1);
}
}
} finally {
zip.close();
}
}
else {
// save project file to XML
OutputStream out = getTarget().getOutput();
try {
Project.save(getProject(), out);
} catch (Exception e) {
reporter.error(new IOMessageImpl("Could not save main project file.", e));
reporter.setSuccess(false);
return reporter;
} finally {
out.close();
}
progress.advance(1);
}
reporter.setSuccess(true);
return reporter;
}
/**
* @return the useSeparateFiles
*/
public boolean isUseSeparateFiles() {
return useSeparateFiles;
}
/**
* @param useSeparateFiles the useSeparateFiles to set
*/
public void setUseSeparateFiles(boolean useSeparateFiles) {
this.useSeparateFiles = useSeparateFiles;
}
private void updateRelativeResourcePaths(Iterable<IOConfiguration> resources,
URI previousTarget, URI newTarget) {
// if the previous target is null, there cannot be relative paths
if (previousTarget == null)
return;
for (IOConfiguration resource : resources) {
Map<String, Value> providerConfig = resource.getProviderConfiguration();
URI pathUri = URI.create(providerConfig.get(ImportProvider.PARAM_SOURCE).as(
String.class));
// update relative URIs
if (!pathUri.isAbsolute()) {
// resolve the resource's URI
pathUri = previousTarget.resolve(pathUri);
// try to get a relative path from the new project to the
// resource
URI relative = IOUtils.getRelativePath(pathUri, newTarget);
providerConfig.put(ImportProvider.PARAM_SOURCE, Value.of(relative.toString()));
}
}
}
}