/******************************************************************************* * Copyright (c) 2011 Wind River Systems, Inc. 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: * Wind River Systems - initial API and implementation * William Chen (Wind River)- [345387] Open the remote files with a proper editor * William Chen (Wind River)- [345552] Edit the remote files with a proper editor *******************************************************************************/ package org.eclipse.tm.te.tcf.filesystem.internal.handlers; 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.io.InputStream; import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.text.DecimalFormat; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.widgets.Shell; import org.eclipse.tm.te.tcf.filesystem.activator.UIPlugin; import org.eclipse.tm.te.tcf.filesystem.internal.exceptions.TCFException; import org.eclipse.tm.te.tcf.filesystem.internal.nls.Messages; import org.eclipse.tm.te.tcf.filesystem.internal.url.TcfURLConnection; import org.eclipse.tm.te.tcf.filesystem.model.FSTreeNode; import org.eclipse.ui.PlatformUI; /** * The local file system cache used to manage the temporary files downloaded * from a remote file system. */ public class CacheManager { // The agent directory's prefixed name. private static final String WS_AGENT_DIR_PREFIX = "agent_"; //$NON-NLS-1$ // The default chunk size of the buffer used during downloading files. private static final int DEFAULT_CHUNK_SIZE = 5 * 1024; // The formatter used to format the size displayed while downloading. private static final DecimalFormat SIZE_FORMAT = new DecimalFormat("#,##0.##"); //$NON-NLS-1$ // The singleton instance. private static CacheManager instance; /** * Get the singleton cache manager. * * @return The singleton cache manager. */ public static CacheManager getInstance() { if (instance == null) { instance = new CacheManager(); } return instance; } /** * Create a cache manager. */ private CacheManager() { } /** * Get the local path of a node's cached file. * <p> * The preferred location is within the plugin's state location, in * example <code><state location>agent_<hashcode_of_peerId>/remote/path/to/the/file...</code>. * <p> * If the plug-in is loaded in a RCP workspace-less environment, the * fall back strategy is to use the users home directory. * * @param node * The file/folder node. * @return The local path of the node's cached file. */ public IPath getCachePath(FSTreeNode node) { File location = getCacheRoot(); String agentId = node.peerNode.getPeer().getID(); // Use Math.abs to avoid negative hash value. String agent = WS_AGENT_DIR_PREFIX + Math.abs(agentId.hashCode()); IPath agentDir = new Path(location.getAbsolutePath()).append(agent); File agentDirFile = agentDir.toFile(); if (!agentDirFile.exists()) { agentDirFile.mkdir(); } return appendNodePath(agentDir, node); } /** * Get the local file of the specified node. * * <p> * The preferred location is within the plugin's state location, in * example <code><state location>agent_<hashcode_of_peerId>/remote/path/to/the/file...</code>. * <p> * If the plug-in is loaded in a RCP workspace-less environment, the * fall back strategy is to use the users home directory. * * @param node * The file/folder node. * @return The file object of the node's local cache. */ public File getCacheFile(FSTreeNode node){ return getCachePath(node).toFile(); } /** * Get the cache file system's root directory on the local host's * file system. * * @return The root folder's location of the cache file system. */ public File getCacheRoot() { File location; try { location = UIPlugin.getDefault().getStateLocation().toFile(); }catch (IllegalStateException e) { // An RCP workspace-less environment (-data @none) location = new File(System.getProperty("user.home"), ".tcf"); //$NON-NLS-1$ //$NON-NLS-2$ location = new File(location, "fs"); //$NON-NLS-1$ } // Create the location if it not exist if (!location.exists()) location.mkdir(); return location; } /** * Append the path with the specified node's context path. * * @param path * The path to be appended. * @param node * The file/folder node. * @return The path to the node. */ private IPath appendNodePath(IPath path, FSTreeNode node) { if (!node.isRoot() && node.parent!=null) { path = appendNodePath(path, node.parent); return appendPathSegment(node, path, node.name); } if (node.isWindowsNode()) { String name = node.name.substring(0, 1); return appendPathSegment(node, path, name); } return path; } /** * Append the path with the segment "name". Create a directory * if the node is a directory which does not yet exist. * * @param node The file/folder node. * @param path The path to appended. * @param name The segment's name. * @return The path with the segment "name" appended. */ private IPath appendPathSegment(FSTreeNode node, IPath path, String name) { IPath newPath = path.append(name); File newFile = newPath.toFile(); if (node.isDirectory() && !newFile.exists()) { newFile.mkdir(); } return newPath; } /** * Download the data of the file from the remote file system. * Must be called within a UI thread. * @param node * The file node. * * @return true if it is successful, false there're errors or it is * canceled. */ public boolean download(final FSTreeNode node) { IRunnableWithProgress runnable = new IRunnableWithProgress() { @Override public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { monitor.beginTask(NLS.bind(Messages.CacheManager_DowloadingFile, node.name), 100); OutputStream output = null; try { // Write the data to its local cache file. File file = getCachePath(node).toFile(); if(file.exists() && !file.canWrite()){ // If the file exists and is read-only, delete it. file.delete(); } output = new BufferedOutputStream(new FileOutputStream(file)); download2OutputStream(node, output, monitor); if (monitor.isCanceled()) throw new InterruptedException(); } catch (IOException e) { throw new InvocationTargetException(e); } finally { if (output != null) { try { output.close(); } catch (Exception e) { } } monitor.done(); } } }; Shell parent = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); TimeTriggeredProgressMonitorDialog dialog = new TimeTriggeredProgressMonitorDialog( parent, 250); dialog.setCancelable(true); File file = getCachePath(node).toFile(); try { dialog.run(true, true, runnable); // If downloading is successful, update the attributes of the file and // set the last modified time to that of its corresponding file. StateManager.getInstance().updateState(node); // If the node is read-only, make the cache file read-only. if(!node.isWritable()) file.setReadOnly(); return true; } catch(TCFException e) { MessageDialog.openError(parent, Messages.StateManager_UpdateFailureTitle, e.getLocalizedMessage()); } catch (InvocationTargetException e) { // Something's gone wrong. Roll back the downloading and display the // error. file.delete(); PersistenceManager.getInstance().removeBaseTimestamp(node.getLocationURL()); displayError(parent, e); } catch (InterruptedException e) { // It is canceled. Just roll back the downloading result. file.delete(); PersistenceManager.getInstance().removeBaseTimestamp(node.getLocationURL()); } return false; } /** * Upload the local files to the remote file system. * Must be called within UI thread. * @param nodes * The files' location. Not null. * * @return true if it is successful, false there're errors or it is * canceled. */ public boolean upload(final FSTreeNode... nodes) { Shell parent = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); try { IRunnableWithProgress runnable = new IRunnableWithProgress() { @Override public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { try { String message; if(nodes.length==1) message = NLS.bind(Messages.CacheManager_UploadSingleFile, nodes[0].name); else message = NLS.bind(Messages.CacheManager_UploadNFiles, Long.valueOf(nodes.length)); monitor.beginTask(message, 100); boolean canceled = uploadFiles(monitor, nodes); if (canceled) throw new InterruptedException(); } catch (Exception e) { throw new InvocationTargetException(e); } finally { monitor.done(); } } }; TimeTriggeredProgressMonitorDialog dialog = new TimeTriggeredProgressMonitorDialog(parent, 250); dialog.setCancelable(true); dialog.run(true, true, runnable); return true; } catch (InvocationTargetException e) { // Something's gone wrong. Roll back the downloading and display the // error. displayError(parent, e); } catch (InterruptedException e) { // It is canceled. Just roll back the downloading result. } return false; } /** * Display the error in an error dialog. * * @param node * the file node. * @param parent * the parent shell. * @param e * The error exception. */ private void displayError(Shell parent, InvocationTargetException e) { Throwable target = e.getTargetException(); Throwable cause = target.getCause() != null ? target.getCause() : target; MessageDialog.openError(parent, Messages.CacheManager_DownloadingError, cause.getLocalizedMessage()); } /** * Upload the specified files using the monitor to report the progress. * * @param peers * The local files' peer files. * @param locals * The local files to be uploaded. * @param monitor * The monitor used to report the progress. * @return true if it is canceled or else false. * @throws Exception * an Exception thrown during downloading and storing data. */ public boolean uploadFiles(IProgressMonitor monitor, FSTreeNode... nodes) throws IOException { BufferedInputStream input = null; BufferedOutputStream output = null; // The buffer used to download the file. byte[] data = new byte[DEFAULT_CHUNK_SIZE]; // Calculate the total size. long totalSize = 0; for (FSTreeNode node : nodes) { File file = getCachePath(node).toFile(); totalSize += file.length(); } // Calculate the chunk size of one percent. int chunk_size = (int) totalSize / 100; // The current reading percentage. int percentRead = 0; // The current length of read bytes. long bytesRead = 0; for (int i = 0; i < nodes.length && !monitor.isCanceled(); i++) { File file = getCachePath(nodes[i]).toFile(); try { URL url = nodes[i].getLocationURL(); TcfURLConnection connection = (TcfURLConnection) url.openConnection(); connection.setDoInput(false); connection.setDoOutput(true); input = new BufferedInputStream(new FileInputStream(file)); output = new BufferedOutputStream(connection.getOutputStream()); // Total size displayed on the progress dialog. String fileLength = formatSize(file.length()); int length; while ((length = input.read(data)) >= 0 && !monitor.isCanceled()) { output.write(data, 0, length); output.flush(); bytesRead += length; if (chunk_size != 0) { int percent = (int) bytesRead / chunk_size; if (percent != percentRead) { // Update the progress. monitor.worked(percent - percentRead); percentRead = percent; // Remember the percentage. // Report the progress. monitor.subTask(NLS.bind(Messages.CacheManager_UploadingProgress, new Object[]{file.getName(), formatSize(bytesRead), fileLength})); } } } } finally { if (output != null) { try { output.close(); } catch (Exception e) { } } if (input != null) { try { input.close(); } catch (Exception e) { } } if(!monitor.isCanceled()){ // Once upload is successful, synchronize the modified time. try { StateManager.getInstance().commitState(nodes[i]); } catch (TCFException tcfe) { throw new IOException(tcfe.getLocalizedMessage()); } } } } return monitor.isCanceled(); } /** * Download the specified file into an output stream using the monitor to report the progress. * * @param node * The file to be downloaded. * @param output * The output stream. * @param monitor * The monitor used to report the progress. * @throws IOException * an IOException thrown during downloading and storing data. */ public void download2OutputStream(FSTreeNode node, OutputStream output, IProgressMonitor monitor) throws IOException { InputStream input = null; // Open the input stream of the node using the tcf stream protocol. try{ URL url = node.getLocationURL(); InputStream in = url.openStream(); input = new BufferedInputStream(in); // The buffer used to download the file. byte[] data = new byte[DEFAULT_CHUNK_SIZE]; // Calculate the chunk size of one percent. int chunk_size = (int) node.attr.size / 100; // Total size displayed on the progress dialog. String total_size = formatSize(node.attr.size); int percentRead = 0; long bytesRead = 0; int length; while ((length = input.read(data)) >= 0 && !monitor.isCanceled()) { output.write(data, 0, length); output.flush(); bytesRead += length; if (chunk_size != 0) { int percent = (int) bytesRead / chunk_size; if (percent != percentRead) { // Update the progress. monitor.worked(percent - percentRead); percentRead = percent; // Remember the percentage. // Report the progress. monitor.subTask(NLS.bind(Messages.CacheManager_DownloadingProgress, formatSize(bytesRead), total_size)); } } } }finally{ if (input != null) { try { input.close(); } catch (Exception e) { } } } } /** * Use the SIZE_FORMAT to format the file's size. The rule is: 1. If the * size is less than 1024 bytes, then show it as "####" bytes. 2. If the * size is less than 1024 KBs, while more than 1 KB, then show it as * "####.##" KBs. 3. If the size is more than 1 MB, then show it as * "####.##" MBs. * * @param size * The file size to be displayed. * @return The string representation of the size. */ private String formatSize(long size) { double kbSize = size / 1024.0; if (kbSize < 1.0) { return SIZE_FORMAT.format(size) + Messages.CacheManager_Bytes; } double mbSize = kbSize / 1024.0; if (mbSize < 1.0) return SIZE_FORMAT.format(kbSize) + Messages.CacheManager_KBs; return SIZE_FORMAT.format(mbSize) + Messages.CacheManager_MBs; } }