// Copyright (c) 2009 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.debug.core.util; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import org.chromium.debug.core.ChromiumDebugPlugin; import org.chromium.debug.core.efs.ChromiumScriptFileSystem; import org.chromium.sdk.Breakpoint; import org.chromium.sdk.BreakpointTypeExtension; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.filesystem.IFileStore; 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.IProjectDescription; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceStatus; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.ResourceAttributes; 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.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; /** * A utility for interaction with the Eclipse workspace. */ public class ChromiumDebugPluginUtil { private static final String CHROMIUM_EXTENSION = "chromium"; //$NON-NLS-1$ public static final Set<String> SUPPORTED_EXTENSIONS = new HashSet<String>(Arrays.asList(CHROMIUM_EXTENSION, "js", //$NON-NLS-1$ "html", "htm")); //$NON-NLS-1$ //$NON-NLS-2$ public static final List<String> SUPPORTED_EXTENSIONS_SUFFIX_LIST; static { SUPPORTED_EXTENSIONS_SUFFIX_LIST = new ArrayList<String>(SUPPORTED_EXTENSIONS.size()); for (String extension : SUPPORTED_EXTENSIONS) { SUPPORTED_EXTENSIONS_SUFFIX_LIST.add("." + extension); //$NON-NLS-1$ } } public static final String JS_DEBUG_PROJECT_NATURE = "org.chromium.debug.core.jsnature"; //$NON-NLS-1$ public static final String CHROMIUM_EXTENSION_SUFFIX = "." + CHROMIUM_EXTENSION; //$NON-NLS-1$ private static final String PROJECT_EXPLORER_ID = "org.eclipse.ui.navigator.ProjectExplorer"; //$NON-NLS-1$ /** * Brings up the "Project Explorer" view in the active workbench window. */ public static void openProjectExplorerView() { Display.getDefault().asyncExec(new Runnable() { public void run() { IWorkbench workbench = PlatformUI.getWorkbench(); IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); if (window == null) { if (workbench.getWorkbenchWindowCount() == 1) { window = workbench.getWorkbenchWindows()[0]; } } if (window != null) { try { window.getActivePage().showView(PROJECT_EXPLORER_ID); } catch (PartInitException e) { // ignore } } } }); } /** * Creates an empty workspace project with the name starting with the given projectNameBase. * Created project is guaranteed to be new in EFS, but workspace may happen to * already have project with such url (left uncleaned from previous runs). Such project * silently gets deleted. * @param projectNameBase project name template * @return the newly created project, or {@code null} if the creation failed */ public static IProject createEmptyProject(String projectNameBase) { ProjectCheckData projectProject; try { for (int uniqueNumber = 0; ; uniqueNumber++) { String projectNameTry; if (uniqueNumber == 0) { projectNameTry = projectNameBase; } else { projectNameTry = projectNameBase + " (" + uniqueNumber + ")"; //$NON-NLS-1$ //$NON-NLS-2$ } projectProject = checkProjectName(projectNameTry); if (projectProject != null) { break; } } } catch (CoreException e) { ChromiumDebugPlugin.log(e); return null; } IProject project = projectProject.getProject(); if (project.exists()) { try { project.delete(true, null); } catch (CoreException e) { ChromiumDebugPlugin.log(e); return null; } } IProjectDescription description = ResourcesPlugin.getWorkspace().newProjectDescription(project.getName()); description.setLocationURI(projectProject.getProjectUri()); description.setNatureIds(new String[] { JS_DEBUG_PROJECT_NATURE }); try { project.create(description, null); project.open(null); return project; } catch (CoreException e) { ChromiumDebugPlugin.log(e); return null; } } private interface ProjectCheckData { IProject getProject(); URI getProjectUri(); } /** * Checks whether debug virtual project can be created. * @param projectNameTry desired project name * @return project project with parameters data or null if project with desired name cannot be * created */ private static ProjectCheckData checkProjectName(String projectNameTry) throws CoreException { final IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectNameTry); if (project.exists()) { URI projectURI = project.getLocationURI(); if (!ChromiumScriptFileSystem.isChromiumDebugURI(projectURI)) { // This is not our project. Do not touch it. return null; } } IPath newPath = project.getFullPath(); final URI projectUriTry = ChromiumScriptFileSystem.getFileStoreUri(newPath); IFileStore projectStore = EFS.getStore(projectUriTry); if (projectStore.fetchInfo().exists()) { return null; } return new ProjectCheckData() { public IProject getProject() { return project; } public URI getProjectUri() { return projectUriTry; } }; } /** * Removes virtual project which was created for debug session. Does its job * asynchronously. */ public static void deleteVirtualProjectAsync(final IProject debugProject) { Job job = new Job("Remove virtual project") { @Override protected IStatus run(IProgressMonitor monitor) { URI projectUri = debugProject.getLocationURI(); try { IFileStore projectStore = EFS.getStore(projectUri); if (projectStore.fetchInfo().exists()) { projectStore.delete(EFS.NONE, null); } debugProject.delete(true, null); } catch (CoreException e) { ChromiumDebugPlugin.log(e); return new Status(IStatus.ERROR, ChromiumDebugPlugin.PLUGIN_ID, "Failed to delete virtual project"); } return Status.OK_STATUS; } }; job.schedule(); } /** * @param projectName to check for existence * @return whether the project named projectName exists. */ public static boolean projectExists(String projectName) { IWorkspace ws = ResourcesPlugin.getWorkspace(); IProject proj = ws.getRoot().getProject(projectName); return proj.exists(); } /** * Creates an empty file with the given filename in the given project. * * @param container to create the file in * @param filename the base short file name to create (will be sanitized for * illegal chars and, in the case of a name clash, suffixed with "(N)") * @return the result of IFile.getName(), or {@code null} if the creation * has failed */ public static IFile createFile(IProject container, String filename) { return createFile(container, FileContainerHandler.FOR_PROJECT, filename); } /** * Creates an empty file with the given filename in the given project. * * @param container to create the file in * @param filename the base short file name to create (will be sanitized for * illegal chars and, in the case of a name clash, suffixed with "(N)") * @return the result of IFile.getName(), or {@code null} if the creation * has failed */ public static IFile createFile(IFolder container, String filename) { return createFile(container, FileContainerHandler.FOR_FOLDER, filename); } private static <C> IFile createFile(final C container, final FileContainerHandler<C> handler, String filename) { String patchedName = FILE_NAME_BAD_CHARS.matcher(filename).replaceAll("_"); // In Linux and Mac it should be enough. However, lets check more. if (!ResourcesPlugin.getWorkspace().validateName(patchedName, IResource.FILE).isOK()) { patchedName = FILE_NAME_BAD_CHARS_FALLBACK.matcher(filename).replaceAll("_"); } UniqueKeyGenerator.Factory<IFile> factory = new UniqueKeyGenerator.Factory<IFile>() { public IFile tryCreate(String uniqueName) { String filePathname = uniqueName + CHROMIUM_EXTENSION_SUFFIX; IFile file = handler.getFile(container, filePathname); if (file.exists()) { return null; } try { file.create(new ByteArrayInputStream("".getBytes()), false, null); //$NON-NLS-1$ } catch (CoreException e) { IStatus status = e.getStatus(); if (status instanceof IResourceStatus) { if (status.getCode() == IResourceStatus.CASE_VARIANT_EXISTS) { // File with different name case already exists. // For our case it's the same as "file.exists()". return null; } } throw new RuntimeException(e); } return file; } }; // Can we have 1000 same-named files? return UniqueKeyGenerator.createUniqueKey(patchedName, 1000, factory); } private static final Pattern FILE_NAME_BAD_CHARS = Pattern.compile("[/\\x00]"); private static final Pattern FILE_NAME_BAD_CHARS_FALLBACK = Pattern.compile("[^\\w\\._-]"); /** * Helper class that provides polymorphic access to {@link IProject} and {@link IContainer}. * Used for uniform access to "getFile" methods from both interfaces. */ private static abstract class FileContainerHandler<T> { abstract IFile getFile(T container, String name); static final FileContainerHandler<IProject> FOR_PROJECT = new FileContainerHandler<IProject>() { @Override IFile getFile(IProject project, String name) { return project.getFile(name); } }; static final FileContainerHandler<IFolder> FOR_FOLDER = new FileContainerHandler<IFolder>() { @Override IFile getFile(IFolder folder, String name) { return folder.getFile(name); } }; } /** * Writes data into a resource with the given resourceName residing in the * source folder of the given project. The previous file content is lost. * Temporarily resets the "read-only" file attribute if one is present. * * @param file to set contents for * @param data to write into the file * @throws CoreException */ public static void writeFile(IFile file, String data) throws CoreException { if (file != null && file.exists()) { ResourceAttributes resourceAttributes = file.getResourceAttributes(); if (resourceAttributes.isReadOnly()) { resourceAttributes.setReadOnly(false); file.setResourceAttributes(resourceAttributes); } file.setContents(new ByteArrayInputStream(data.getBytes()), IFile.FORCE, null); resourceAttributes.setReadOnly(true); file.setResourceAttributes(resourceAttributes); } } public static boolean isInteger(String value) { try { Integer.parseInt(value); return true; } catch (NumberFormatException e) { return false; } } /** * The container where the script sources should be put. * * @param project where the launch configuration stores the scripts * @return the script source container */ public static IContainer getSourceContainer(IProject project) { return project; } public static byte[] readFileContents(IFile file) throws IOException, CoreException { InputStream inputStream = file.getContents(); try { return readBytes(inputStream); } finally { inputStream.close(); } } public static byte[] readBytes(InputStream inputStream) throws IOException { BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] array = new byte[1024]; while (true) { int len = bufferedInputStream.read(array); if (len == -1) { break; } output.write(array, 0, len); } return output.toByteArray(); } public static final Breakpoint.Target.Visitor<String> BREAKPOINT_TARGET_TO_STRING = new BreakpointTypeExtension.ScriptRegExpSupport.Visitor<String>() { @Override public String visitScriptName(String scriptName) { return "script_name=" + scriptName; } @Override public String visitScriptId(Object scriptId) { return "script_id=" + scriptId; } @Override public String visitRegExp(String regExp) { return "RegExp=" + regExp; } @Override public String visitUnknown(Breakpoint.Target target) { return "Unknown target: + " + target; } }; public static <T> T throwUnsupported() { throw new UnsupportedOperationException(); } private ChromiumDebugPluginUtil() { // not instantiable } }