/******************************************************************************* * Copyright (c) 2017 the TeXlipse team and others. * 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 * * Contributors: * The TeXlipse team - initial API and implementation *******************************************************************************/ package org.eclipse.texlipse.builder; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspaceRunnable; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.texlipse.TexlipsePlugin; import org.eclipse.texlipse.properties.TexlipseProperties; /** * Performs actions on output files. This includes moving, renaming, or setting * derived file flags, as defined in the preferences. * * @author Matthias Erll * */ public class OutputFileManager { private final IProject project; private final ProjectFileTracking tracking; private IContainer sourceDir; private IFolder outputDir; private IFolder tempDir; private String format; private IFile sourceFile; private IFile currentSourceFile; private Set<IPath> movedFiles; /** * Moves a file to the output directory with a new name. * * @param project the current project * @param sourceFile output file to be moved * @param destDir the destination directory of the file * @param destName the new name of the file * @param monitor progress monitor * @throws CoreException if an error occurs * @return file in the new location */ private static IFile moveFile(IProject project, IFile sourceFile, IContainer destContainer, String destName, IProgressMonitor monitor) throws CoreException { if (sourceFile != null && sourceFile.exists() && destName != null) { final IPath destRelPath = new Path(destName); final IFile dest = destContainer.getFile(destRelPath); if (dest.exists()) { File outFile = new File(sourceFile.getLocationURI()); File destFile = new File(dest.getLocationURI()); try { // Try to move the content instead of deleting the old file // and replace it by the new one. This is better for some // viewers like Sumatrapdf FileOutputStream out = new FileOutputStream(destFile); out.getChannel().tryLock(); BufferedInputStream in = new BufferedInputStream(new FileInputStream(outFile)); byte[] buf = new byte[4096]; int l; while ((l = in.read(buf)) != -1) { out.write(buf, 0, l); } in.close(); out.close(); sourceFile.delete(true, monitor); } catch (IOException e) { // try to delete and move the file dest.delete(true, monitor); sourceFile.move(dest.getFullPath(), true, monitor); } } else { // move the file sourceFile.move(dest.getFullPath(), true, monitor); } monitor.worked(1); return dest; } else { return null; } } /** * Retrieves an IFile object of the currently selected output file, * no matter if the file actually exists. * This method is used to respect partial builds. * * @return current output file, or null if not selected */ private IFile getSelectedOutputFile() { String fileName = TexlipseProperties.getOutputFileName(project); if (fileName != null) { if (outputDir == null) { return project.getFile(fileName); } else { return outputDir.getFile(fileName); } } else { return null; } } /** * Retrieves, and if necessary creates, the currently selected output * folder. If the output folder is not available, the project root is * used instead. * This method is used to respect partial builds. * * @param markAsDerived if set to true, the "derived" flag will be set * for the folder * @param monitor progress monitor * @return output container * @throws CoreException if an error occurs */ private IContainer getSelectedOutputContainer(boolean markAsDerived, IProgressMonitor monitor) throws CoreException { if (outputDir != null) { if (!outputDir.exists()) { outputDir.create(true, true, monitor); } if (markAsDerived) { outputDir.setDerived(true); } return outputDir; } else { // if not set, assume project directory return project; } } /** * Retrieves the IFile object of the actually used source file, no matter * if it actually exists. * This method is used to respect partial builds. * * @return actually selected source file, or null if no source file has * been set */ private IFile getActualSourceFile() { if (currentSourceFile == null) { return sourceFile; } else { return currentSourceFile; } } /** * Retrieves the IContainer object of the actually selected source file, * no matter if it actually exists. * This method is used to respect partial builds. * * @return current source file container, or null of no source file * has been set */ private IContainer getActualSourceContainer() { if (currentSourceFile == null) { if (sourceFile != null) { return sourceFile.getParent(); } else { return null; } } else { return currentSourceFile.getParent(); } } /** * Find the output file and get the local time stamp. * * @return the "last modified" -timestamp of the project output file, * or -1 if file does not exist */ private long getOutputFileDate() { IFile of = getSelectedOutputFile(); if (of != null && of.exists()) { return of.getLocalTimeStamp(); } else { return -1; } } /** * Moves a set of files from the source container to the destination. All * files need to be inside the source container or any of its * subfolders. Existing folders are not removed, if left empty. If source * and destination container are identical, files are if applicable only * marked as derived. * * @param source source container * @param dest destination folder (can be null, for only marking files as * derived) * @param nameSet set of file paths to move * @param markAsDerived mark files as derived after moving * @param force overwrite exiting files and create subfolders in destination * folder, if necessary * @param monitor progress monitor * @return a new set of file paths in their new location. This only includes * files which have actually been moved. * @throws CoreException if an error occurs */ private Set<IPath> moveFiles(final IContainer source, final IContainer dest, final Set<IPath> nameSet, boolean markAsDerived, boolean force, IProgressMonitor monitor) throws CoreException { Set<IPath> newNames = new HashSet<IPath>(); if (nameSet != null && !nameSet.isEmpty()) { IPath sourcePath = source.getProjectRelativePath(); IPath destPath; if (dest != null) { destPath = dest.getProjectRelativePath(); } else { destPath = sourcePath; } boolean moveFiles = !sourcePath.equals(destPath); int sourceSeg = sourcePath.segmentCount(); // Sort paths for running through file system structure incrementally IPath[] sortedNames = nameSet.toArray(new IPath[0]); Arrays.sort(sortedNames, new Comparator<IPath>() { public int compare(IPath o1, IPath o2) { return o1.toString().compareTo(o2.toString()); } }); for (IPath filePath : sortedNames) { if (sourcePath.isPrefixOf(filePath)) { IFile currentFile = project.getFile(filePath); if (moveFiles) { // Generate new path IPath destFilePath = destPath.append(filePath.removeFirstSegments(sourceSeg)); IFile destFile = project.getFile(destFilePath); if (currentFile.exists() && (force || !destFile.exists())) { // Retrieve destination parent folder IContainer destFolder = destFile.getParent(); if (destFolder instanceof IFolder && !destFolder.exists() && force) { // Create destination folder if necessary ((IFolder) destFolder).create(true, true, monitor); if (markAsDerived) { destFolder.setDerived(true); } } if (destFolder.exists()) { // Move file if (destFile.exists() && force) { destFile.delete(true, monitor); } currentFile.move(destFile.getFullPath(), true, monitor); if (markAsDerived && destFile.exists()) { destFile.setDerived(true); } // Store path for later reversal newNames.add(destFilePath); } } } else { if (markAsDerived && currentFile.exists()) { currentFile.setDerived(true); } } monitor.worked(1); } } } return newNames; } /** * Deletes a set of files from the file system, and also their parent * folders if those become empty during this process. * * @param nameSet set of file paths * @param monitor progress monitor * @throws CoreException if an error occurs */ private void deleteFiles(final Set<IPath> nameSet, IProgressMonitor monitor) throws CoreException { if (nameSet == null || nameSet.isEmpty()) { return; } Set<IContainer> subFolders = new HashSet<IContainer>(); for (IPath filePath : nameSet) { // Generate new path IFile currentFile = project.getFile(filePath); if (currentFile.exists()) { // Retrieve parent folder and store for deletion IContainer folder = currentFile.getParent(); subFolders.add(folder); currentFile.delete(true, monitor); } monitor.worked(1); } // Delete parent folders, if they are empty for (IContainer folder : subFolders) { if (folder.exists() && folder.members().length == 0) { folder.delete(true, monitor); } monitor.worked(1); } } /** * Renames output files and/or moves them if necessary. A file is * considered an output file, if * <ul> * <li>it is the current output file (which can also be from a temporary * build)</li> * <p><b>or</b></p> * <li>it has the same file name as the current input file, apart from its * file extension, and one of the derived file extensions as specified in * the preferences</li> * </ul> * * @param monitor progress monitor * @return set of paths to the (possibly moved) files * @throws CoreException if an error occurs */ private Set<IPath> moveOutputFiles(IProgressMonitor monitor) throws CoreException { final boolean markAsDerived = "true".equals( TexlipseProperties.getProjectProperty(project, TexlipseProperties.MARK_OUTPUT_DERIVED_PROPERTY)); final String[] derivedExts = TexlipsePlugin.getPreferenceArray( TexlipseProperties.DERIVED_FILES); final IFile aSourceFile = getActualSourceFile(); final IContainer aSourceContainer = getActualSourceContainer(); final IFile sOutputFile = getSelectedOutputFile(); final IContainer sOutputContainer = getSelectedOutputContainer(markAsDerived, monitor); if (aSourceFile == null || aSourceContainer == null || sOutputFile == null || sOutputContainer == null) { // Something is wrong with the settings return null; } // Get name without extension from main files for renaming final String dotFormat = '.' + format; final String sourceBaseName = stripFileExt(aSourceFile.getName(), null); final String outputBaseName = stripFileExt(sOutputFile.getName(), dotFormat); // Check if files are to be moved or renamed final boolean moveFiles = !sourceBaseName.equals(outputBaseName) || !sOutputContainer.equals(aSourceContainer); // Retrieve output and other derived files along with their extensions final Map<IPath, String> outputFiles = ProjectFileTracking.getOutputNames(aSourceContainer, sourceBaseName, derivedExts, format, monitor); // Check if there is anything to do if ((moveFiles || markAsDerived) && !outputFiles.isEmpty()) { final Set<IPath> movedFiles = new HashSet<IPath>(outputFiles.size()); project.getWorkspace().run(new IWorkspaceRunnable() { public void run(IProgressMonitor monitor) throws CoreException { // Move files to destination folder and rename for (Entry<IPath, String> entry : outputFiles.entrySet()) { IFile currentFile = project.getFile(entry.getKey()); if (moveFiles) { // Determine new file name String destName = outputBaseName + entry.getValue(); // Move file IFile dest = moveFile(project, currentFile, sOutputContainer, destName, monitor); if (dest != null && markAsDerived) { dest.setDerived(true); } movedFiles.add(dest.getProjectRelativePath()); } else { // Possibly mark as derived if (markAsDerived) { currentFile.setDerived(true); } movedFiles.add(entry.getKey()); } } } }, monitor); return movedFiles; } else { return outputFiles.keySet(); } } /** * Moves temporary files out of the build directory, if applicable. A file * is considered a temporary file, if * <ul> * <li>it had been in the temporary files folder before the build * process</li> * <p><b>or</b></p> * <li>it was created or modified during the build process, and has a * temporary file extension as specified in the preferences</li> * </ul> * * @param excludes set of paths to exclude from moving, e.g. because they * are the main output files * @param monitor progress monitor * @throws CoreException if an error occurs */ private void moveTempFiles(final Set<IPath> excludes, IProgressMonitor monitor) throws CoreException { final IContainer aSourceContainer = getActualSourceContainer(); if (tracking.isInitial() || aSourceContainer == null || !aSourceContainer.exists()) { return; } final boolean markAsDerived = "true".equals( TexlipseProperties.getProjectProperty(project, TexlipseProperties.MARK_TEMP_DERIVED_PROPERTY)); final String[] tempExts = TexlipsePlugin.getPreferenceArray( TexlipseProperties.TEMP_FILE_EXTS); // Check if there is anything to do if (markAsDerived || tempDir != null) { // First move temporary files, which had been placed into the source folder // just prior to the build; // then check for new temporary files, which need to be moved project.getWorkspace().run(new IWorkspaceRunnable() { public void run(IProgressMonitor monitor) throws CoreException { if (movedFiles != null) { if (excludes != null) { movedFiles.removeAll(excludes); } moveFiles(sourceDir, tempDir, movedFiles, markAsDerived, true, monitor); } final Set<IPath> newTempNames = tracking.getNewTempNames(aSourceContainer, tempExts, format, monitor); if (excludes != null) { newTempNames.removeAll(excludes); } moveFiles(sourceDir, tempDir, newTempNames, markAsDerived, true, monitor); } }, monitor); } } /** * Moves all files currently located in the temporary files folder into the * build directory * * @param monitor progress monitor * @throws CoreException if an error occurs */ private void restoreTempFiles(IProgressMonitor monitor) throws CoreException { final Set<IPath> tempNames = tracking.getTempFiles(); if (tempDir == null || tempNames.isEmpty()) { movedFiles = new HashSet<IPath>(); return; } // Move files and store new paths for later reversal project.getWorkspace().run(new IWorkspaceRunnable() { public void run(IProgressMonitor monitor) throws CoreException { movedFiles = moveFiles(tempDir, sourceDir, tracking.getTempFiles(), false, false, monitor); } }, monitor); } /** * Utility method for refreshing the current view on all relevant input and * output folders. This makes sure, that methods determining and moving files * get the current workspace contents. * * @param monitor progress monitor * @throws CoreException if an error occurs */ private void refreshView(IProgressMonitor monitor) throws CoreException { sourceDir.refreshLocal(IProject.DEPTH_INFINITE, monitor); if (outputDir != null && !sourceDir.getProjectRelativePath().isPrefixOf(outputDir.getProjectRelativePath())) { outputDir.refreshLocal(IProject.DEPTH_ONE, monitor); } if (tempDir != null && !sourceDir.getProjectRelativePath().isPrefixOf(tempDir.getProjectRelativePath())) { tempDir.refreshLocal(IProject.DEPTH_INFINITE, monitor); } if (!sourceDir.getLocation().equals(project.getLocation())) { project.refreshLocal(IProject.DEPTH_ONE, monitor); } } /** * Returns the given file name without the extension. The file extension * can be provided in <code>ext</code> or be determined automatically. If * the former applies, the extension(s) should start with a dot. The method * does not check for that condition. If determined automatically, the last * dot marks the beginning of the file extension (similar to * <code>getFileExtension()</code> of IFile). * * @param name file name * @param ext suggested file extension (can be null) * @return the given file name without the extension */ public static String stripFileExt(String name, String ext) { if (name != null) { if (ext == null) { int idx = name.lastIndexOf('.'); if (idx > 0) { return name.substring(0, idx); } else { return name; } } else { int nameLen = name.length(); int extLen = ext.length(); if (nameLen > extLen) { return name.substring(0, nameLen - extLen); } else { return ""; } } } else { return name; } } /** * Constructor. * * @param project current project * @param tracking file tracking for the project */ public OutputFileManager(final IProject project, final ProjectFileTracking tracking) { this.project = project; this.tracking = tracking; this.init(); } /** * Initializes variables, which are often reused. This should be called * every time there is a chance that project settings have been changed. */ public void init() { sourceDir = TexlipseProperties.getProjectSourceDir(project); outputDir = TexlipseProperties.getProjectOutputDir(project); tempDir = TexlipseProperties.getProjectTempDir(project); format = TexlipseProperties.getProjectProperty(project, TexlipseProperties.OUTPUT_FORMAT); sourceFile = TexlipseProperties.getProjectSourceFile(project); } /** * Performs actions before a LaTeX document is built; namely: * <ul> * <li>memorizing which files are present in the temporary and build source * folder, and</li> * <li>moving temporary files from their folder into the build folder, so * the build process has access to them.</li> * </ul> * * @param monitor progress monitor * @throws CoreException if an error occurs */ public void performBeforeBuild(IProgressMonitor monitor) throws CoreException { // capture current state of build and temp folder tracking.refreshSnapshots(sourceDir, monitor); // use temp files from previous build restoreTempFiles(monitor); } /** * Performs actions after the LaTeX builder has finished building a document * for the current source; namely: * <ul> * <li>renaming and/or moving output (and other derived) files out of the * build directory into the output folder</li> * <li>moving old and new temporary files out of the build directory into * the temporary files folder</li> * </ul> * * @param inputFile name of the input file; this can be <code>null</code>, * if the current main document has just been built, but should be set * after partial builds * @param monitor progress monitor * @throws CoreException if an error occurs */ public void performAfterBuild(IProgressMonitor monitor) throws CoreException { // keeping first exception, which occurs when moving files; however, attempts // to still perform following steps CoreException ex = null; // make sure this has access to all files (if this fails, it means trouble to // all following steps) refreshView(monitor); Set<IPath> outputFiles = null; try { // possibly move output files away from the source dir and mark as derived outputFiles = moveOutputFiles(monitor); } catch (CoreException e) { // store exception for throwing it later ex = new BuilderCoreException(TexlipsePlugin.stat( TexlipsePlugin.getResourceString("builderCoreErrorOutputBlock"))); } try { // move temp files out of this folder and mark as derived moveTempFiles(outputFiles, monitor); } catch (CoreException e) { // we only worry about this one, if the build was okay if (ex == null) { ex = new BuilderCoreException(TexlipsePlugin.stat( TexlipsePlugin.getResourceString("builderCoreErrorTempBlock"))); } } try { refreshView(monitor); } catch (CoreException e) { // this is not irrelevant, but not as severe as the others if (ex == null) { ex = e; } } tracking.clearSnapshots(); // now throw any pending exception, after cleaning up if (ex != null) { throw ex; } } /** * Deletes the output file. * * @param monitor progress monitor * @throws CoreException if an error occurs */ public void cleanOutputFile(IProgressMonitor monitor) throws CoreException { monitor.subTask(TexlipsePlugin.getResourceString("builderSubTaskCleanOutput")); IFile outputFile = getSelectedOutputFile(); if (outputFile != null && outputFile.exists()) { outputFile.delete(true, monitor); } monitor.worked(1); } /** * Deletes the contents of the temporary files folder, including subfolders * * @param monitor progress monitor * @throws CoreException if an error occurs */ public void cleanTempFiles(IProgressMonitor monitor) throws CoreException { if (tempDir != null && tempDir.exists()) { monitor.beginTask(TexlipsePlugin.getResourceString("builderSubTaskClean"), tempDir.members().length); monitor.subTask(TexlipsePlugin.getResourceString("builderSubTaskCleanTemp")); // Retrieve current temp folder content final Set<IPath> currentTmpFiles = tracking.getTempFolderNames(monitor); // Perform deletion deleteFiles(currentTmpFiles, monitor); } tracking.clearSnapshots(); } /** * Determines, if the current output file is up to date (i.e. all source * files are older). This method is aware of partial builds. * * @return true, if the output file does not need to be rebuilt; false * if it should */ public boolean isUpToDate() { long lastBuildStamp = getOutputFileDate(); IResource[] files = TexlipseProperties.getAllProjectFiles(project); for (int i = 0; i < files.length; i++) { long stamp = files[i].getLocalTimeStamp(); if (stamp > lastBuildStamp) { return false; } } return true; } /** * Retrieves the currently set source file. * * @return source file */ public IFile getCurrentSourceFile() { return currentSourceFile; } /** * Sets the current main source file for the project. This method does not * check, if it exists. If <code>null</code> is passed, the project's main * input file is assumed. Therefore, it should be set to the temporary * input for partial builds. * * @param sourceFile source file */ public void setCurrentSourceFile(IFile sourceFile) { this.currentSourceFile = sourceFile; } }