blob: 06c5da046d72690d5f4a18bae1a3d7ba6bd1e3db [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2017 Obeo 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:
* Obeo - initial API and implementation
* Michael Borkowski - bug 467677
* Philip Langer - optimize use of StorageTraversal.getStorages(), 508526
*******************************************************************************/
package org.eclipse.emf.compare.ide.utils;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.core.runtime.content.IContentTypeManager;
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.common.util.WrappedException;
import org.eclipse.emf.compare.ide.EMFCompareIDEPlugin;
import org.eclipse.emf.compare.ide.internal.utils.StoragePathAdapter;
import org.eclipse.emf.compare.ide.internal.utils.URIStorage;
import org.eclipse.emf.compare.merge.ResourceChangeAdapter;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.EcoreUtil;
/**
* This class will be used to provide various utilities aimed at IResource manipulation.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
*/
public final class ResourceUtil {
/** Content types of the files to consider as potential models. */
private static final String[] MODEL_CONTENT_TYPES = new String[] {"org.eclipse.emf.compare.content.type", //$NON-NLS-1$
"org.eclipse.emf.ecore", //$NON-NLS-1$
"org.eclipse.emf.ecore.xmi", }; //$NON-NLS-1$
/**
* This can be used in order to convert an Iterable of IStorages to an Iterable over the storage's URIs.
*/
private static final Function<IStorage, URI> AS_URI = new Function<IStorage, URI>() {
public URI apply(IStorage input) {
if (input != null) {
return createURIFor(input);
}
return null;
}
};
/**
* This does not need to be instantiated.
*/
private ResourceUtil() {
// hides default constructor
}
/**
* Provides a {@link Function} that converts an {@link IStorage} into a {@link URI}.
*
* @return A {@link Function} that converts an {@link IStorage} into a {@link URI}. This function
* transforms a {@code null} storage into a {@code null} URI.
* @since 3.2
*/
public static Function<IStorage, URI> asURI() {
return AS_URI;
}
/**
* This will try and load the given file as an EMF model, and return the corresponding {@link Resource} if
* at all possible.
*
* @param storage
* The file we need to try and load as a model.
* @param resourceSet
* The resource set in which to load this Resource.
* @param options
* The options to pass to {@link Resource#load(java.util.Map)}.
* @return The loaded EMF Resource if {@code file} was a model, {@code null} otherwise.
*/
public static Resource loadResource(IStorage storage, ResourceSet resourceSet, Map<?, ?> options) {
final URI uri = createURIFor(storage);
try {
Resource resource = resourceSet.createResource(uri);
setAssociatedStorage(resource, storage);
try (InputStream stream = storage.getContents()) {
resource.load(stream, options);
}
return resource;
} catch (IOException | CoreException | WrappedException e) {
// return null
}
return null;
}
/**
* Returns the storage {@link #setAssociatedStorage(Resource, IStorage) associated} with the resource.
*
* @param resource
* the resource.
* @return the associated storage or <code>null</code> if there isn't one.
* @see #setAssociatedStorage(Resource, IStorage)
*/
public static IStorage getAssociatedStorage(Resource resource) {
StorageProvider storageProvider = (StorageProvider)EcoreUtil.getExistingAdapter(resource,
StorageProvider.class);
if (storageProvider != null) {
return storageProvider.getStorage();
} else {
return null;
}
}
/**
* Associates the storage with the resource such that {@link #getAssociatedStorage(Resource)} will return
* this storage for the resource.
*
* @param resource
* the resource.
* @param storage
* the associated storage.
*/
public static void setAssociatedStorage(Resource resource, IStorage storage) {
final String fullPath = storage.getFullPath().toString();
boolean isLocal = storage instanceof IFile;
EList<Adapter> eAdapters = resource.eAdapters();
if (storage instanceof IStoragePathAdapterProvider) {
eAdapters.add(((IStoragePathAdapterProvider)storage).createStoragePathAdapter(fullPath, isLocal));
} else {
eAdapters.add(new StoragePathAdapter(fullPath, isLocal));
}
eAdapters.add(new StorageProvider(storage));
}
/**
* Used by {@link ResourceUtil#getAssociatedStorage(Resource)} and
* {@link ResourceUtil#setAssociatedStorage(Resource, IStorage)} to map a resource to its associated
* storage.
*/
private static final class StorageProvider extends AdapterImpl {
/**
* The storage.
*/
private final IStorage storage;
/**
* Creates an instance for the storage.
*
* @param storage
* the storage.
*/
StorageProvider(IStorage storage) {
this.storage = storage;
}
@Override
public boolean isAdapterForType(Object type) {
return type == StorageProvider.class;
}
public IStorage getStorage() {
return storage;
}
}
/**
* Checks whether the two given storages point to binary identical data.
*
* @param left
* First of the two storages which content we are testing.
* @param right
* Second of the two storages which content we are testing.
* @return <code>true</code> if {@code left} and {@code right} are binary identical.
*/
public static boolean binaryIdentical(IStorage left, IStorage right) {
final int maxBufferSize = 8192;
final byte[] buffer = new byte[maxBufferSize];
try (BufferedInputStream leftStream = new BufferedInputStream(left.getContents(), maxBufferSize);
BufferedInputStream rightStream = new BufferedInputStream(right.getContents(),
maxBufferSize);) {
int readLeft;
boolean identical = true;
do {
readLeft = leftStream.read(buffer, 0, buffer.length);
if (readLeft == -1) {
// check if there is anything left to read on right
identical = rightStream.read() == -1;
break;
}
if (!verifyNextBytes(rightStream, buffer, 0, readLeft)) {
identical = false;
break;
}
} while (readLeft > 0);
return identical;
} catch (CoreException | IOException e) {
logError(e);
}
return false;
}
/**
* Checks whether the three given storages point to binary identical data. This could be done by calling
* {@link #binaryIdentical(IStorage, IStorage)} twice, though this implementation allows us to shortcut
* whenever one byte differs... and will read one less file from its input stream.
*
* @param left
* First of the three storages which content we are testing.
* @param right
* Second of the three storages which content we are testing.
* @param origin
* Third of the three storages which content we are testing.
* @return <code>true</code> if {@code left}, {@code right} and {@code origin} are binary identical.
*/
public static boolean binaryIdentical(IStorage left, IStorage right, IStorage origin) {
final int maxBufferSize = 8192;
final byte[] buffer = new byte[maxBufferSize];
try (InputStream leftStream = new BufferedInputStream(left.getContents(), maxBufferSize);
InputStream rightStream = new BufferedInputStream(right.getContents(), maxBufferSize);
InputStream originStream = new BufferedInputStream(origin.getContents(), maxBufferSize);) {
int readLeft;
boolean identical = true;
do {
readLeft = leftStream.read(buffer, 0, buffer.length);
if (readLeft == -1) {
// check if there is anything left to read on right or origin
identical = rightStream.read() == -1 && originStream.read() == -1;
break;
}
if (!verifyNextBytes(rightStream, buffer, 0, readLeft)
|| !verifyNextBytes(originStream, buffer, 0, readLeft)) {
identical = false;
break;
}
} while (readLeft > 0);
return identical;
} catch (CoreException | IOException e) {
logError(e);
}
return false;
}
/**
* Verifies whether the next <code>length</code> bytes coming from <code>stream</code> equal
* <code>bytes</code> at offset <code>offset</code>.
*
* @param stream
* The stream to read bytes from
* @param bytes
* The array of bytes to compare to (from offset of <code>offset</code> bytes)
* @param offset
* The offset in the byte array to use
* @param length
* The amount of bytes to verify
* @return <code>true</code> if there are at least <code>length</code> bytes in the stream and they equal
* the provided bytes
* @throws IOException
* If an I/O problem occurs
*/
private static boolean verifyNextBytes(InputStream stream, byte[] bytes, int offset, int length)
throws IOException {
int done = 0;
byte[] buffer = new byte[offset + length];
while (done < length) {
int read = stream.read(buffer, offset + done, length - done);
if (read == -1 || !equalArrays(offset + done, read, bytes, buffer)) {
return false;
}
done += read;
}
return true;
}
/**
* Create the URI with which we'll load the given IFile as an EMF resource.
*
* @param file
* The file for which we need an EMF URI.
* @return The created URI.
* @since 3.1
*/
public static URI createURIFor(IFile file) {
// whether it exists or not (no longer), use platform:/resource
return URI.createPlatformResourceURI(file.getFullPath().toString(), true);
}
/**
* Create the URI with which we'll load the given IStorage as an EMF resource.
*
* @param storage
* The storage for which we need an EMF URI.
* @return The created URI.
*/
public static URI createURIFor(IStorage storage) {
URI shortcut = null;
if (storage instanceof IFile) {
shortcut = createURIFor((IFile)storage);
} else if (storage instanceof URIStorage) {
shortcut = ((URIStorage)storage).getURI();
}
if (shortcut != null) {
return shortcut;
}
String path = getFixedPath(storage).toString();
// Given the two paths
// "g:/ws/project/test.ecore"
// "/project/test.ecore"
// We have no way to determine which is absolute and which should be platform:/resource
URI uri;
if (path.startsWith("platform:/plugin/")) { //$NON-NLS-1$
uri = URI.createURI(path);
} else if (path.startsWith("file:/")) { //$NON-NLS-1$
uri = URI.createURI(path);
} else if (hasStoragePathProvider(storage)) {
uri = URI.createPlatformResourceURI(path, true);
} else {
uri = URI.createURI(path, true);
}
final IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
final IPath iPath = new Path(path);
if (root != null && iPath.segmentCount() >= 2 && root.getFile(iPath).exists()) {
uri = URI.createPlatformResourceURI(path, true);
}
return uri;
}
/**
* Tries and retrieve the {@link IResource} associated with the given {@link URI}. This returns a file
* handle, which might point to a non-existing IResource.
*
* @param uri
* the URI for which we want the {@link IResource}.
* @return the {@link IResource} if found, null otherwise.
* @since 3.2
*/
public static IResource getResourceFromURI(final URI uri) {
final IResource targetFile;
if (uri.isPlatform()) {
IPath platformString = new Path(uri.trimFragment().toPlatformString(true));
targetFile = ResourcesPlugin.getWorkspace().getRoot().getFile(platformString);
} else {
/*
* FIXME Deresolve the URI against the workspace root, if it cannot be done, delegate to
* super.createInputStream()
*/
targetFile = ResourcesPlugin.getWorkspace().getRoot()
.getFile(new Path(uri.trimFragment().toString()));
}
return targetFile;
}
/**
* Returns a path for this storage after fixing from an {@link IStoragePathProvider} if one exists.
*
* @param storage
* The storage for which we need a fixed full path.
* @return The full path to this storage, fixed if need be.
* @since 3.2
*/
public static IPath getFixedPath(IStorage storage) {
final Object adapter = Platform.getAdapterManager().loadAdapter(storage,
IStoragePathProvider.class.getName());
if (adapter instanceof IStoragePathProvider) {
return ((IStoragePathProvider)adapter).computeFixedPath(storage);
}
return storage.getFullPath();
}
/**
* Returns an absolute path for this storage if one exists. If the storage can be adapted to
* {@link IStoragePathProvider2}, it will call computeAbsolutePath from this interface. If the storage is
* a File, a {@link Path} will be created and then getAbsolutePath will be called. In other cases, the
* method will return the full path of the storage.
*
* @param storage
* The storage for which we need an absolute path.
* @return The absolute path to this storage.
* @since 3.3
*/
public static IPath getAbsolutePath(IStorage storage) {
final IPath absolutePath;
final Object adapter = Platform.getAdapterManager().loadAdapter(storage,
IStoragePathProvider.class.getName());
if (adapter instanceof IStoragePathProvider2) {
absolutePath = ((IStoragePathProvider2)adapter).computeAbsolutePath(storage);
} else if (storage instanceof File) {
absolutePath = new Path(((File)storage).getAbsolutePath());
} else {
absolutePath = storage.getFullPath();
}
return absolutePath;
}
/**
* Checks if an {@link IStoragePathProvider} exists for the given storage.
*
* @param storage
* the given storage.
* @return true if exists, false otherwise.
*/
private static boolean hasStoragePathProvider(IStorage storage) {
final boolean hasProvider;
final Object adapter = Platform.getAdapterManager().loadAdapter(storage,
IStoragePathProvider.class.getName());
if (adapter instanceof IStoragePathProvider) {
hasProvider = true;
} else {
hasProvider = false;
}
return hasProvider;
}
/**
* This can be called to save all resources contained by the resource set. This will not try and save
* resources that do not support output.
*
* @param resourceSet
* The resource set to save.
* @param options
* The options we are to pass on to {@link Resource#save(Map)}.
*/
public static void saveAllResources(ResourceSet resourceSet, Map<?, ?> options) {
List<Resource> resources = Lists.newArrayList(resourceSet.getResources());
for (Resource resource : resources) {
saveResource(resource, options);
}
}
/**
* This can be called to save all resources contained by the resource set. This will not try and save
* resources that do not support output.
*
* @param resourceSet
* The resource set to save.
* @param options
* The options we are to pass on to {@link Resource#save(Map)}.
* @param leftTraversal
* The traversal corresponding to the left side.
* @param rightTraversal
* The traversal corresponding to the right side.
* @param originTraversal
* The traversal corresponding to the common ancestor of both other side. Can be
* <code>null</code>.
* @since 3.3
*/
public static void saveAllResources(ResourceSet resourceSet, Map<?, ?> options,
StorageTraversal leftTraversal, StorageTraversal rightTraversal,
StorageTraversal originTraversal) {
// filter out the resources that don't support output
List<Resource> resources = Lists
.newArrayList(Iterables.filter(resourceSet.getResources(), new Predicate<Resource>() {
public boolean apply(Resource input) {
return supportsOutput(input);
}
}));
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
final Set<Resource> wsResources = Sets.newHashSet();
final Set<Resource> nonWsResources = Sets.newHashSet();
for (Resource resource : resources) {
String projectName = new Path(resource.getURI().toPlatformString(true)).segment(0);
IProject project = root.getProject(projectName);
if (project != null && project.isAccessible()) {
wsResources.add(resource);
} else {
nonWsResources.add(resource);
}
}
// Save workspace resources first
for (Resource resource : wsResources) {
saveResource(resource, options);
}
// Delete workspace resources from ResourceSet
// Is it really useful ?
resources.removeAll(wsResources);
// Change "platform:/resource/relativePath" URIs of non-workspace resources into "file:/absolutePath"
// URIs
final Set<? extends IStorage> leftStorages = leftTraversal.getStorages();
final Set<? extends IStorage> rightStorages = rightTraversal.getStorages();
final Set<? extends IStorage> originStorages;
if (originTraversal != null) {
originStorages = originTraversal.getStorages();
} else {
originStorages = null;
}
for (Resource resource : nonWsResources) {
String absolutePath = getAbsolutePath(resource, leftStorages, rightStorages, originStorages);
URI fileURI = URI.createFileURI(absolutePath);
resource.setURI(fileURI);
}
// Save non-workspace resources
for (Resource resource : nonWsResources) {
saveResource(resource, options);
}
}
/**
* Get the absolute path of the given resource.
*
* @param resource
* The resource for which we seek an absolute path.
* @param leftStorages
* The storages of the left traversal.
* @param rightStorages
* The storages of the right traversal.
* @param originStorages
* The storages of the common ancestor traversal. Can be <code>null</code>.
* @return the absolute path of the given resource if found, null otherwise.
*/
private static String getAbsolutePath(Resource resource, Set<? extends IStorage> leftStorages,
Set<? extends IStorage> rightStorages, Set<? extends IStorage> originStorages) {
URI uri = resource.getURI();
String absolutePath = getAbsolutePath(uri, leftStorages);
if (absolutePath == null) {
absolutePath = getAbsolutePath(uri, rightStorages);
}
if (absolutePath == null && originStorages != null) {
absolutePath = getAbsolutePath(uri, originStorages);
}
return absolutePath;
}
/**
* Get the absolute path of the given URI that corresponds to one of the given storages.
*
* @param uri
* The URI for which we seek an absolute path.
* @param storages
* The given storages.
* @return the absolute path of the given URI if found, null otherwise.
*/
private static String getAbsolutePath(URI uri, Set<? extends IStorage> storages) {
for (IStorage storage : storages) {
IPath storagePath = getFixedPath(storage);
if (storagePath.makeAbsolute().toString().equals(uri.toPlatformString(true))) {
IPath absolutePath = getAbsolutePath(storage);
if (absolutePath != null) {
return absolutePath.toString();
}
}
}
return null;
}
/**
* This can be called to save the given resource. This will not try and save a resource that does not
* support output.
*
* @param resource
* The resource to save.
* @param options
* The options we are to pass on to {@link Resource#save(Map)}.
* @since 3.1
*/
public static void saveResource(Resource resource, Map<?, ?> options) {
if (supportsOutput(resource)) {
try {
if (mustDelete(resource)) {
deleteResource(resource);
} else {
resource.save(options);
}
} catch (IOException e) {
logError(e);
}
}
}
/**
* Check if the given resource must be deleted.
*
* @param resource
* The resource to delete, must not be null.
* @return true if the given resource must be deleted, false otherwise.
* @since 3.4
*/
protected static boolean mustDelete(Resource resource) {
Adapter adapter = EcoreUtil.getAdapter(resource.eAdapters(), ResourceChangeAdapter.class);
if (adapter instanceof ResourceChangeAdapter) {
return ((ResourceChangeAdapter)adapter).mustDelete(resource);
}
return false;
}
/**
* Delete the given resource.
*
* @param resource
* The resource to delete, must not be null.
* @since 3.4
*/
protected static void deleteResource(final Resource resource) {
try {
resource.delete(Collections.emptyMap());
} catch (IOException e) {
logError(e);
}
}
/**
* This will return <code>true</code> if the given <em>contentTypeId</em> represents a content-type
* contained in the given array.
*
* @param contentTypeId
* Fully qualified identifier of the content type we seek.
* @param contentTypes
* The array of content-types to compare against.
* @return <code>true</code> if the given array contains a content-type with this id.
* @since 3.1
*/
public static boolean hasContentType(String contentTypeId, List<IContentType> contentTypes) {
IContentTypeManager ctManager = Platform.getContentTypeManager();
IContentType expected = ctManager.getContentType(contentTypeId);
if (expected == null) {
return false;
}
boolean hasContentType = false;
for (int i = 0; i < contentTypes.size() && !hasContentType; i++) {
if (contentTypes.get(i).isKindOf(expected)) {
hasContentType = true;
}
}
return hasContentType;
}
/**
* Checks whether the given file has one of the content types described in {@link #MODEL_CONTENT_TYPES}.
*
* @param file
* The file which contents are to be checked.
* @return <code>true</code> if this file has one of the "model" content types.
* @since 3.1
*/
public static boolean hasModelType(IFile file) {
boolean isModel = false;
// Try a first pass without the file contents, since some content type parsers can be very sluggish
// (EMF uses a sax parser to describe its content)
final IContentTypeManager ctManager = Platform.getContentTypeManager();
final List<IContentType> fileNameTypes = Lists
.newArrayList(ctManager.findContentTypesFor(file.getName()));
for (int i = 0; i < MODEL_CONTENT_TYPES.length && !isModel; i++) {
isModel = hasContentType(MODEL_CONTENT_TYPES[i], fileNameTypes);
}
if (isModel) {
return true;
}
// Fall back to the slower test
final List<IContentType> contentTypes = Lists.newArrayList(getContentTypes(file));
contentTypes.removeAll(fileNameTypes);
for (int i = 0; i < MODEL_CONTENT_TYPES.length && !isModel; i++) {
isModel = hasContentType(MODEL_CONTENT_TYPES[i], contentTypes);
}
return isModel;
}
/**
* Returns the whole list of content types of the given IFile, or an empty array if none.
*
* @param file
* The file we need the content types of.
* @return All content types associated with the given file, an empty array if none.
* @since 3.1
*/
public static IContentType[] getContentTypes(IFile file) {
final IContentTypeManager ctManager = Platform.getContentTypeManager();
IContentType[] contentTypes = new IContentType[0];
try (InputStream resourceContent = file.getContents()) {
contentTypes = ctManager.findContentTypesFor(resourceContent, file.getName());
} catch (CoreException | IOException e) {
ctManager.findContentTypesFor(file.getName());
}
return contentTypes;
}
/**
* Disable saving for resources that cannot support it.
*
* @param resource
* The resource we are to check.
* @return <code>true</code> if we can save this <code>resource</code>, <code>false</code> otherwise.
*/
private static boolean supportsOutput(Resource resource) {
final URI uri = resource.getURI();
if (uri.isPlatformResource() || uri.isRelative() || uri.isFile()) {
return true;
}
return false;
}
/**
* Checks whether the two arrays contain identical data in the {@code [0:length]} range.
*
* @param offset
* The offset at which to start comparing
* @param length
* Length of the data range to check within the arrays.
* @param array1
* First of the two arrays which content we need to check.
* @param array2
* Second of the two arrays which content we need to check.
* @return <code>true</code> if the two given arrays contain identical data in the
* {@code [offset..offset+length]} range.
*/
private static boolean equalArrays(int offset, int length, byte[] array1, byte[] array2) {
boolean result = true;
if (array1 == array2) {
result = true;
} else if (array1 == null || array2 == null) {
result = false;
} else {
for (int i = offset; result && i < offset + length; i++) {
result = array1[i] == array2[i];
}
}
return result;
}
/**
* Logs the given exception as an error.
*
* @param e
* The exception we need to log.
*/
private static void logError(Exception e) {
final IStatus status = new Status(IStatus.ERROR, EMFCompareIDEPlugin.PLUGIN_ID, e.getMessage(), e);
EMFCompareIDEPlugin.getDefault().getLog().log(status);
}
}