/* * 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.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.zip.ZipOutputStream; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.eclipse.core.runtime.content.IContentType; import com.google.common.io.Files; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.core.HalePlatform; 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.ResourceAdvisor; import eu.esdihumboldt.hale.common.core.io.Value; import eu.esdihumboldt.hale.common.core.io.extension.ResourceAdvisorExtension; import eu.esdihumboldt.hale.common.core.io.impl.SubtaskProgressIndicator; 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.ProjectFileInfo; import eu.esdihumboldt.hale.common.core.io.project.util.LocationUpdater; import eu.esdihumboldt.hale.common.core.io.project.util.XMLAlignmentUpdater; 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.DefaultInputSupplier; import eu.esdihumboldt.hale.common.core.io.supplier.FileIOSupplier; import eu.esdihumboldt.hale.common.core.io.supplier.LocatableInputSupplier; import eu.esdihumboldt.hale.common.core.io.supplier.LocatableOutputSupplier; import eu.esdihumboldt.hale.common.core.service.cleanup.CleanupContext; import eu.esdihumboldt.hale.common.core.service.cleanup.CleanupService; import eu.esdihumboldt.util.io.IOUtils; /** * Save projects (including all related resources) as an archive (zip) * * @author Patrick Lieb */ public class ArchiveProjectWriter extends AbstractProjectWriter { private static final ALogger log = ALoggerFactory.getLogger(ArchiveProjectWriter.class); /** * The provider ID as registered in the extension point. */ public static final String ID = "eu.esdihumboldt.hale.io.project.hale25.zip.writer"; /** * Parameter for including or excluding web resources */ public static final String INCLUDE_WEB_RESOURCES = "includeweb"; /** * Parameter for including or excluding data files */ public static final String EXLUDE_DATA_FILES = "excludedata"; @Override protected IOReport execute(ProgressIndicator progress, IOReporter reporter) throws IOProviderConfigurationException, IOException { return createProjectArchive(getTarget().getOutput(), reporter, progress); } /** * Creates the project archive. * * @param target {@link OutputStream} to write the archive to * @param reporter the reporter to use for the execution report * @param progress the progress indicator * @return the execution report * @throws IOException if an I/O operation fails * @throws IOProviderConfigurationException if the I/O provider was not * configured properly */ public IOReport createProjectArchive(OutputStream target, IOReporter reporter, ProgressIndicator progress) throws IOException, IOProviderConfigurationException { ZipOutputStream zip = new ZipOutputStream(target); // all files related to the project are copied into a temporary // directory first and then packed into a zip file // create temporary directory and project file File tempDir = Files.createTempDir(); File baseFile = new File(tempDir, "project.halex"); // mark the temporary directory for clean-up if the project is closed CleanupService clean = HalePlatform.getService(CleanupService.class); if (clean != null) { clean.addTemporaryFiles(CleanupContext.PROJECT, tempDir); } LocatableOutputSupplier<OutputStream> out = new FileIOSupplier(baseFile); // false is correct if getParameter is null because false is default boolean includeWebresources = getParameter(INCLUDE_WEB_RESOURCES).as(Boolean.class, false); SubtaskProgressIndicator subtask = new SubtaskProgressIndicator(progress); // save old IO configurations List<IOConfiguration> oldResources = new ArrayList<IOConfiguration>(); for (int i = 0; i < getProject().getResources().size(); i++) { // clone all IO configurations to work on different objects oldResources.add(getProject().getResources().get(i).clone()); } IOConfiguration config = getProject().getSaveConfiguration(); if (config == null) { config = new IOConfiguration(); } IOConfiguration oldSaveConfig = config.clone(); // copy resources to the temp directory and update xml schemas updateResources(tempDir, includeWebresources, subtask, reporter); // update target save configuration of the project config.getProviderConfiguration().put(PARAM_TARGET, Value.of(baseFile.toURI().toString())); // write project file via XMLProjectWriter XMLProjectWriter writer = new XMLProjectWriter(); writer.setTarget(out); writer.setProject(getProject()); writer.setProjectFiles(getProjectFiles()); IOReport report = writer.execute(progress, reporter); // now after the project with its project files is written, look for the // alignment file and update it ProjectFileInfo newAlignmentInfo = getAlignmentFile(getProject()); if (newAlignmentInfo != null) { URI newAlignment = tempDir.toURI().resolve(newAlignmentInfo.getLocation()); XMLAlignmentUpdater.update(new File(newAlignment), newAlignment, includeWebresources, reporter); } // put the complete temp directory into a zip file IOUtils.zipDirectory(tempDir, zip); zip.close(); // the files may not be deleted now as they will be needed if the // project is saved again w/o loading it first // update the relative resource locations LocationUpdater updater = new LocationUpdater(getProject(), out.getLocation()); // resources are made absolute (else they can't be found afterwards), // e.g. when saving the project again before loading it updater.updateProject(false); // reset the save configurations that has been overridden by the XML // project writer getProject().setSaveConfiguration(oldSaveConfig); if (clean == null) { // if no clean service is available, assume the directory is not // needed anymore FileUtils.deleteDirectory(tempDir); } return report; } /** * Get a project's alignment file * * @param project the project * @return info object for the alignment file */ protected ProjectFileInfo getAlignmentFile(Project project) { for (ProjectFileInfo pfi : project.getProjectFiles()) if (pfi.getName().equals("alignment.xml")) { return pfi; } return null; } /** * Update the resources and copy them into the target directory * * @param targetDirectory target directory * @param includeWebResources whether to include web resources in the copy * @param progress the progress indicator * @param reporter the reporter to use for the execution report * @throws IOException if an I/O operation fails */ protected void updateResources(File targetDirectory, boolean includeWebResources, ProgressIndicator progress, IOReporter reporter) throws IOException { progress.begin("Copy resources", ProgressIndicator.UNKNOWN); try { List<IOConfiguration> resources = getProject().getResources(); // every resource needs his own directory int count = 1; // true if excluded files should be skipped; false is default boolean excludeDataFiles = getParameter(EXLUDE_DATA_FILES).as(Boolean.class, false); // resource locations mapped to new resource path Map<URI, String> handledResources = new HashMap<>(); Iterator<IOConfiguration> iter = resources.iterator(); while (iter.hasNext()) { IOConfiguration resource = iter.next(); // check if ActionId is equal to // eu.esdihumboldt.hale.common.instance.io.InstanceIO.ACTION_LOAD_SOURCE_DATA // import not possible due to cycle errors if (excludeDataFiles && resource.getActionId() .equals("eu.esdihumboldt.hale.io.instance.read.source")) { // delete reference in project file iter.remove(); continue; } // get resource path Map<String, Value> providerConfig = resource.getProviderConfiguration(); String path = providerConfig.get(ImportProvider.PARAM_SOURCE).toString(); URI pathUri; try { pathUri = new URI(path); } catch (URISyntaxException e1) { reporter.error(new IOMessageImpl( "Skipped resource because of invalid URI: " + path, e1)); continue; } if (!pathUri.isAbsolute()) { if (getPreviousTarget() != null) { pathUri = getPreviousTarget().resolve(pathUri); } else { log.warn("Could not resolve relative path " + pathUri.toString()); } } // check if path was already handled if (handledResources.containsKey(pathUri)) { providerConfig.put(ImportProvider.PARAM_SOURCE, Value.of(handledResources.get(pathUri))); // skip copying the resource continue; } String scheme = pathUri.getScheme(); LocatableInputSupplier<? extends InputStream> input = null; if (scheme != null) { if (scheme.equals("http") || scheme.equals("https")) { // web resource if (includeWebResources) { input = new DefaultInputSupplier(pathUri); } else { // web resource that should not be included this // time // but the resolved URI should be stored // nevertheless // otherwise the URI may be invalid if it was // relative providerConfig.put(ImportProvider.PARAM_SOURCE, Value.of(pathUri.toASCIIString())); continue; } } else if (scheme.equals("file") || scheme.equals("platform") || scheme.equals("bundle") || scheme.equals("jar")) { // files need always to be included // platform resources (or other internal resources) // should be included as well input = new DefaultInputSupplier(pathUri); } else { // other type of URI, e.g. JDBC // not to be included providerConfig.put(ImportProvider.PARAM_SOURCE, Value.of(pathUri.toASCIIString())); continue; } } else { // now can't open that, can we? reporter.error( new IOMessageImpl("Skipped resource because it cannot be loaded from " + pathUri.toString(), null)); continue; } progress.setCurrentTask("Copying resource at " + path); // every resource file is copied into an own resource // directory in the target directory String resourceFolder = "resource" + count; File newDirectory = new File(targetDirectory, resourceFolder); try { newDirectory.mkdir(); } catch (SecurityException e) { throw new IOException("Can not create directory " + newDirectory.toString(), e); } // Extract the file name from pathUri.getPath(). // This will produce a non-URL-encoded file name to be used in // the File(File parent, String child) constructor below String fileName = FilenameUtils.getName(pathUri.getPath().toString()); if (path.isEmpty()) { fileName = "file"; } File newFile = new File(newDirectory, fileName); Path target = newFile.toPath(); // retrieve the resource advisor Value ct = providerConfig.get(ImportProvider.PARAM_CONTENT_TYPE); IContentType contentType = null; if (ct != null) { contentType = HalePlatform.getContentTypeManager() .getContentType(ct.as(String.class)); } ResourceAdvisor ra = ResourceAdvisorExtension.getInstance().getAdvisor(contentType); // copy the resource progress.setCurrentTask("Copying resource at " + path); ra.copyResource(input, target, contentType, includeWebResources, reporter); // Extract the URL-encoded file name of the copied resource and // build the new relative resource path String resourceName = FilenameUtils.getName(target.toUri().toString()); String newPath = resourceFolder + "/" + resourceName; // store new path for resource handledResources.put(pathUri, newPath); // update the provider configuration providerConfig.put(ImportProvider.PARAM_SOURCE, Value.of(newPath)); count++; } } finally { progress.end(); } } }