| /*=============================================================================# |
| # Copyright (c) 2006, 2021 Stephan Wahlbrink and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 |
| # which is available at https://www.apache.org/licenses/LICENSE-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 |
| # |
| # Contributors: |
| # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.ecommons.io; |
| |
| import java.io.BufferedReader; |
| import java.io.ByteArrayInputStream; |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStream; |
| import java.io.Reader; |
| import java.io.SequenceInputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| |
| import org.eclipse.core.filesystem.EFS; |
| import org.eclipse.core.filesystem.IFileStore; |
| import org.eclipse.core.filesystem.IFileSystem; |
| import org.eclipse.core.filesystem.URIUtil; |
| import org.eclipse.core.resources.IContainer; |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IWorkspace; |
| import org.eclipse.core.resources.IWorkspaceRoot; |
| import org.eclipse.core.resources.IWorkspaceRunnable; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.core.runtime.Path; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.SubProgressMonitor; |
| import org.eclipse.core.runtime.jobs.ISchedulingRule; |
| import org.eclipse.core.variables.IStringVariableManager; |
| import org.eclipse.core.variables.VariablesPlugin; |
| |
| import org.eclipse.statet.ecommons.io.internal.EFSUtilImpl; |
| import org.eclipse.statet.ecommons.io.internal.WorkspaceUtilImpl; |
| import org.eclipse.statet.internal.ecommons.coreutils.CoreMiscellanyPlugin; |
| |
| |
| /** |
| * Utilities to work with files. |
| * |
| * TODO: add write action (text stream) |
| */ |
| public abstract class FileUtil { |
| |
| |
| public static final String UTF_8 = "UTF-8"; //$NON-NLS-1$ |
| |
| public static final String UTF_16_BE = "UTF-16BE"; //$NON-NLS-1$ |
| |
| public static final String UTF_16_LE = "UTF-16LE"; //$NON-NLS-1$ |
| |
| /** |
| * Constant that identifies the Byte-Order-Mark for contents encoded with |
| * the UTF-8 character encoding scheme. |
| */ |
| public final static byte[] BOM_UTF_8 = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; |
| |
| /** |
| * Constant that identifies the Byte-Order-Mark for contents encoded with |
| * the UTF-16 Big Endian character encoding scheme. |
| */ |
| public final static byte[] BOM_UTF_16BE = {(byte) 0xFE, (byte) 0xFF}; |
| |
| /** |
| * Constant that identifies the Byte-Order-Mark for contents encoded with |
| * the UTF-16 Little Endian character encoding scheme. |
| */ |
| public final static byte[] BOM_UTF_16LE = {(byte) 0xFF, (byte) 0xFE}; |
| |
| |
| /*-- Local files --*/ |
| public static IFileStore getLocalFileStore(final String s) throws CoreException { |
| return getLocalFileStore(s, (IFileStore) null); |
| } |
| |
| /** |
| * Resolves a string presentation of a path to an IFileStore in the local file system, |
| * if possible. |
| * |
| * As usual the IFileStore is a resource handle only and the resource must not exists. |
| * |
| * @param s string representation of a path |
| * @param relativeParent optional folder used as parent if path is relative |
| * @return an IFileStore in the local file system |
| * @throws CoreException if path is not a valid local path |
| */ |
| public static IFileStore getLocalFileStore(final String s, final IFileStore relativeParent) throws CoreException { |
| if (s.length() > 0) { |
| final IFileSystem localFS = EFS.getLocalFileSystem(); |
| if (s.startsWith(EFS.SCHEME_FILE)) { |
| try { |
| return localFS.getStore(new URI(s).normalize()); |
| } |
| catch (final URISyntaxException e) { |
| throw new CoreException(new Status(IStatus.ERROR, CoreMiscellanyPlugin.BUNDLE_ID, e.getReason())); |
| } |
| } |
| final IPath path = Path.fromOSString(s); |
| if (path.isUNC()) { |
| final URI uri = URIUtil.toURI(path); |
| if (uri != null) { |
| return localFS.getStore(uri); |
| } |
| } |
| if (path.isAbsolute()) { |
| final String device = path.getDevice(); |
| if (device == null || device.length() <= 2) { |
| // return localFS.getStore(URIUtil.toURI(path)); |
| final URI uri = URIUtil.toURI(s); |
| if (uri != null) { |
| return localFS.getStore(uri); |
| } |
| } |
| } |
| else if (relativeParent != null && // !path.isAbsolute() && |
| path.getDevice() == null) { |
| return relativeParent.getFileStore(path); |
| } |
| } |
| else if (relativeParent != null) { // && s.length() == 0 |
| return relativeParent; |
| } |
| throw new CoreException(new Status(IStatus.ERROR, CoreMiscellanyPlugin.BUNDLE_ID, "No local filesystem resource.")); |
| } |
| |
| /** |
| * Resolves a string presentation of a path to an IFileStore in the local file system, |
| * if possible. |
| * |
| * As usual the IFileStore is a resource handle only and the resource must not exists. |
| * |
| * @param s string representation of a path |
| * @param relativeParent optional folder used as parent if path is relative |
| * @return an IFileStore in the local file system |
| * @throws CoreException if path is not a valid local path |
| */ |
| public static IFileStore getLocalFileStore(final String s, final IContainer relativeParent) throws CoreException { |
| if (s.length() > 0) { |
| final IFileSystem localFS = EFS.getLocalFileSystem(); |
| if (s.startsWith(EFS.SCHEME_FILE)) { |
| try { |
| return localFS.getStore(new URI(s).normalize()); |
| } |
| catch (final URISyntaxException e) { |
| throw new CoreException(new Status(IStatus.ERROR, CoreMiscellanyPlugin.BUNDLE_ID, e.getReason())); |
| } |
| } |
| final IPath path = Path.fromOSString(s); |
| if (path.isUNC()) { |
| final URI uri = URIUtil.toURI(path); |
| if (uri != null) { |
| return localFS.getStore(uri); |
| } |
| } |
| if (path.isAbsolute()) { |
| final String device = path.getDevice(); |
| if (device == null || device.length() <= 2) { |
| // return localFS.getStore(URIUtil.toURI(path)); |
| final URI uri = URIUtil.toURI(s); |
| if (uri != null) { |
| return localFS.getStore(uri); |
| } |
| } |
| } |
| else if (relativeParent != null && // !path.isAbsolute() && |
| path.getDevice() == null) { |
| return EFS.getStore(relativeParent.getFile(path).getLocationURI().normalize()); |
| } |
| } |
| else if (relativeParent != null) { // && s.length() == 0 |
| return EFS.getStore(relativeParent.getLocationURI().normalize()); |
| } |
| throw new CoreException(new Status(IStatus.ERROR, CoreMiscellanyPlugin.BUNDLE_ID, "No local filesystem resource.")); |
| } |
| |
| /** |
| * Resolves a string presentation of a path to an IFileStore in the local file system, |
| * if possible. |
| * |
| * As usual the IFileStore is a resource handle only and the resource must not exists. |
| * |
| * @param s string representation of a path |
| * @return an IFileStore in the local file system |
| * @throws CoreException if path is not a valid local path |
| */ |
| public static IFileStore getFileStore(final String location) throws CoreException { |
| return getFileStore(location, null); |
| } |
| |
| /** |
| * Resolves a string presentation of a path to an IFileStore, if possible. |
| * |
| * The resource must not be in the local file system (URI with scheme). |
| * As usual the IFileStore is a resource handle only and the resource must not exists. |
| * |
| * @param s string representation of a path |
| * @param relativeParent optional folder used as parent if path is relative |
| * @return an IFileStore in the local file system |
| * @throws CoreException if path is not a valid local path |
| */ |
| public static IFileStore getFileStore(final String location, final IFileStore relativeParent) throws CoreException { |
| try { |
| return FileUtil.getLocalFileStore(location, relativeParent); |
| } |
| catch (final CoreException e) { |
| } |
| |
| final int p = location.indexOf(':'); |
| if (p > 1) { |
| try { |
| final URI uri = new URI(location); |
| if (uri.getScheme() != null) { |
| return EFS.getStore(uri); |
| } |
| } |
| catch (final URISyntaxException e) { |
| // use uri error message only if we find valid scheme at the beginning |
| try { |
| new URI(location.substring(0, p), "a", null); //$NON-NLS-1$ |
| throw new CoreException(new Status(IStatus.ERROR, CoreMiscellanyPlugin.BUNDLE_ID, e.getReason())); |
| } |
| catch (final URISyntaxException invalidScheme) { |
| } |
| } |
| } |
| |
| throw new CoreException(new Status(IStatus.ERROR, CoreMiscellanyPlugin.BUNDLE_ID, "No supported filesystem resource.")); |
| } |
| |
| /** |
| * Returns a string for the given file store. |
| * The string can be converted back to a file store by the methods |
| * {@link #getFileStore(String)} etc. of this class. |
| * |
| * @param fileStore the file store or <code>null</code> |
| * @return the string for the file store or <code>null</code>, if the file store was <code>null</code> |
| */ |
| public static String toString(final IFileStore fileStore) { |
| if (fileStore == null) { |
| return null; |
| } |
| if (fileStore.getFileSystem().getScheme().equals(EFS.SCHEME_FILE)) { |
| return fileStore.toString(); |
| } |
| else { |
| return fileStore.toURI().toString(); |
| } |
| } |
| |
| |
| /** |
| * Tries to resolves string to local file store handler. |
| * - Performs Variable substitution |
| * |
| * @param location path as string of the location |
| * @param parent optional parent directory, used if <code>location</code> is relative |
| * @param child optional child, appended to location |
| * @return the file store handler |
| * @throws CoreException |
| */ |
| public static IFileStore expandToLocalFileStore(final String location, final IFileStore parent, final String child) throws CoreException { |
| final IStringVariableManager variables = VariablesPlugin.getDefault().getStringVariableManager(); |
| final String expanded = variables.performStringSubstitution(location); |
| final IFileStore localFileStore = getLocalFileStore(expanded, parent); |
| if (child != null) { |
| return localFileStore.getChild(child); |
| } |
| return localFileStore; |
| } |
| |
| public static IFile getAsWorkspaceFile(final URI uri) { |
| final IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); |
| IPath path = URIUtil.toPath(uri); |
| if (path != null) { |
| path = path.makeAbsolute(); |
| final IFile file = root.getFileForLocation(path); |
| if (file != null) { |
| return file; |
| } |
| } |
| final IFile[] files = root.findFilesForLocationURI(uri); |
| if (files.length > 0) { |
| return files[0]; |
| } |
| return null; |
| } |
| |
| public static void prepareTextOutput(final OutputStream outputStream, final String encoding) throws IOException { |
| if (encoding == null) { |
| return; |
| } |
| final byte[] prefix; |
| if (encoding.equals(FileUtil.UTF_8)) { |
| prefix = FileUtil.BOM_UTF_8; |
| } |
| else if (encoding.equals(FileUtil.UTF_16_BE)) { |
| prefix = FileUtil.BOM_UTF_16BE; |
| } |
| else if (encoding.equals(FileUtil.UTF_16_LE)) { |
| prefix = FileUtil.BOM_UTF_16LE; |
| } |
| else { |
| return; |
| } |
| for (int i = 0; i < prefix.length; i++) { |
| outputStream.write(prefix[i]); |
| } |
| } |
| |
| |
| /*-- File Operations --*/ |
| |
| public static abstract class AbstractFileOperation { |
| |
| protected int fMode = EFS.NONE; |
| |
| protected String fCharset = UTF_8; |
| protected boolean fForceCharset = false; |
| |
| protected AbstractFileOperation() { |
| } |
| |
| public void setFileOperationMode(final int mode) { |
| fMode = mode; |
| } |
| |
| public void setCharset(final String charset, final boolean forceCharset) { |
| fCharset = charset; |
| fForceCharset = forceCharset; |
| } |
| |
| |
| public void doOperation(final IProgressMonitor monitor) throws CoreException, OperationCanceledException { |
| runInEnv(monitor); |
| } |
| |
| protected abstract void runInEnv(IProgressMonitor monitor) throws CoreException, OperationCanceledException; |
| |
| protected void runAsWorkspaceRunnable(final IProgressMonitor monitor, final ISchedulingRule rule) throws CoreException, OperationCanceledException { |
| final IWorkspace workspace = ResourcesPlugin.getWorkspace(); |
| final IWorkspaceRunnable workspaceRunner = new IWorkspaceRunnable() { |
| @Override |
| public void run(final IProgressMonitor monitor) throws CoreException { |
| runInEnv(monitor); |
| } |
| }; |
| workspace.run(workspaceRunner, rule, IWorkspace.AVOID_UPDATE, monitor); |
| } |
| } |
| |
| public abstract class WriteTextFileOperation extends AbstractFileOperation { |
| |
| protected WriteTextFileOperation() { |
| super(); |
| } |
| |
| protected abstract void writeImpl(IProgressMonitor monitor) throws CoreException, UnsupportedEncodingException, IOException; |
| |
| @Override |
| protected void runInEnv(final IProgressMonitor monitor) throws CoreException, OperationCanceledException { |
| try { |
| monitor.beginTask("Writing to "+getLabel(), 100); |
| if (monitor.isCanceled()) { |
| throw new OperationCanceledException(); |
| } |
| |
| writeImpl(monitor); |
| } |
| catch (final UnsupportedEncodingException e) { |
| throw new CoreException(new Status(IStatus.ERROR, CoreMiscellanyPlugin.BUNDLE_ID, 0, |
| "The selected charset is unsupported on your system.", e)); |
| } |
| catch (final IOException e) { |
| throw new CoreException(new Status(IStatus.ERROR, CoreMiscellanyPlugin.BUNDLE_ID, 0, |
| "Error while writing to file.", e)); |
| } |
| finally { |
| monitor.done(); |
| } |
| } |
| } |
| |
| |
| public static interface ReaderAction { |
| |
| void run(BufferedReader reader, IProgressMonitor monitor) throws IOException, CoreException; |
| |
| } |
| |
| protected static class FileInput implements Closeable { |
| |
| private String fEncoding; |
| private String fDefaultEncoding; |
| private InputStream fStream; |
| |
| public FileInput(final InputStream input, final String expliciteCharsetHint) throws IOException, CoreException { |
| fStream = input; |
| if (expliciteCharsetHint != null) { |
| if (expliciteCharsetHint.equals(UTF_8) |
| || expliciteCharsetHint.equals(UTF_16_BE) |
| || expliciteCharsetHint.equals(UTF_16_LE)) { |
| read(input); |
| } |
| fDefaultEncoding = expliciteCharsetHint; |
| } |
| else { |
| read(input); |
| } |
| fEncoding = (fDefaultEncoding != null) ? fDefaultEncoding : FileUtil.UTF_8; |
| } |
| |
| void read(final InputStream input) throws IOException { |
| try { |
| final int n = 3; |
| final byte[] bytes = new byte[n]; |
| final int readed = input.read(bytes, 0, n); |
| if (readed == 0) { |
| return; |
| } |
| int next = 0; |
| if (startsWith(bytes, BOM_UTF_8)) { |
| next = BOM_UTF_8.length; |
| fDefaultEncoding = FileUtil.UTF_8; |
| } |
| else if (startsWith(bytes, BOM_UTF_16BE)) { |
| next = BOM_UTF_16BE.length; |
| fDefaultEncoding = FileUtil.UTF_16_BE; |
| } |
| else if (startsWith(bytes, BOM_UTF_16LE)) { |
| next = BOM_UTF_16LE.length; |
| fDefaultEncoding = FileUtil.UTF_16_LE; |
| } |
| if (readed-next > 0) { |
| fStream = new SequenceInputStream(new ByteArrayInputStream( |
| bytes, next, readed-next), input); |
| } |
| } |
| catch (final IOException e) { |
| saveClose(input); |
| throw e; |
| } |
| } |
| |
| private boolean startsWith(final byte[] array, final byte[] start) { |
| for (int i = 0; i < start.length; i++) { |
| if (array[i] != start[i]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public void setEncoding(final String encoding, final boolean force) { |
| if (encoding == null && fDefaultEncoding != null) { |
| fEncoding = fDefaultEncoding; |
| } |
| if (force || fDefaultEncoding == null) { |
| fEncoding = encoding; |
| } |
| } |
| |
| @Override |
| public void close() throws IOException { |
| if (fStream != null) { |
| fStream.close(); |
| } |
| } |
| |
| public String getDefaultCharset() { |
| return fDefaultEncoding; |
| } |
| |
| public Reader getReader() throws UnsupportedEncodingException { |
| return new InputStreamReader(fStream, fEncoding); |
| } |
| |
| } |
| |
| public abstract class ReadTextFileOperation extends AbstractFileOperation { |
| |
| protected abstract FileInput getInput(IProgressMonitor monitor) throws CoreException, IOException; |
| protected abstract ReaderAction getAction(); |
| |
| public ReadTextFileOperation() { |
| super(); |
| } |
| |
| @Override |
| protected void runInEnv(final IProgressMonitor monitor) throws CoreException { |
| FileInput fi = null; |
| BufferedReader reader = null; |
| try { |
| monitor.beginTask(null, 100); |
| final String fileLabel = getLabel(); |
| monitor.subTask("Opening "+fileLabel+"..."); |
| if (monitor.isCanceled()) { |
| throw new OperationCanceledException(); |
| } |
| |
| fi = getInput(new SubProgressMonitor(monitor, 10)); |
| fi.setEncoding(fCharset, fForceCharset); |
| |
| reader = new BufferedReader(fi.getReader()); |
| monitor.worked(5); |
| monitor.subTask("Reading "+fileLabel+"..."); |
| getAction().run(reader, new SubProgressMonitor(monitor, 80)); |
| } |
| catch (final UnsupportedEncodingException e) { |
| throw new CoreException(new Status(IStatus.ERROR, CoreMiscellanyPlugin.BUNDLE_ID, 0, |
| "The selected charset is unsupported on your system.", e)); |
| } |
| catch (final IOException e) { |
| throw new CoreException(new Status(IStatus.ERROR, CoreMiscellanyPlugin.BUNDLE_ID, 0, |
| "Error while reading the file.", e)); |
| } |
| finally { |
| saveClose(reader); |
| saveClose(fi); |
| monitor.done(); |
| } |
| } |
| } |
| |
| public static FileUtil getFileUtil(final Object file) { |
| if (file == null) { |
| throw new NullPointerException("file"); |
| } |
| if (file instanceof IFile) { |
| return new WorkspaceUtilImpl((IFile) file); |
| } |
| else if (file instanceof IFileStore) { |
| final IFileStore efsFile = (IFileStore) file; |
| final IFile iFile = getAsWorkspaceFile(efsFile.toURI()); |
| if (iFile != null) { |
| return new WorkspaceUtilImpl(iFile); |
| } |
| return new EFSUtilImpl(efsFile); |
| } |
| throw new IllegalArgumentException("Unknown file object: " + file.getClass()); |
| } |
| |
| |
| protected static void saveClose(final Closeable stream) { |
| if (stream != null) { |
| try { |
| stream.close(); |
| } catch (final IOException e) {} |
| } |
| } |
| |
| |
| /** |
| * Return the time stamp of the file |
| * |
| * @param monitor a progress monitor for progress and cancellation report |
| * @return the time stamp |
| * @throws CoreException |
| */ |
| public abstract long getTimeStamp(IProgressMonitor monitor) throws CoreException; |
| |
| /** |
| * Return a label for the file. It can be used to show it in the UI, but not to access or |
| * identify the file programatically. |
| * |
| * @param monitor a progress monitor for progress and cancellation report |
| * @return the label |
| * @throws CoreException |
| */ |
| public abstract String getLabel(); |
| |
| /** |
| * Return the URI of the file. |
| * |
| * @return the URI or <code>null</code> if the file doesn't have an URI |
| * @throws CoreException |
| */ |
| public abstract URI getURI(); |
| |
| |
| public abstract ReadTextFileOperation createReadTextFileOp(ReaderAction action); |
| public abstract WriteTextFileOperation createWriteTextFileOp(String content); |
| |
| } |