/******************************************************************************* * Copyright (c) 2012, 2016 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() *******************************************************************************/ 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.util.URI; import org.eclipse.emf.common.util.WrappedException; import org.eclipse.emf.compare.ide.EMFCompareIDEPlugin; 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); try (InputStream stream = storage.getContents()) { resource.load(stream, options); } return resource; } catch (IOException | CoreException | WrappedException e) { // return null } return null; } /** * 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); } }