/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.eclipse.org/org/documents/epl-v10.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ide.eclipse.adt;
import static com.android.SdkConstants.TOOLS_PREFIX;
import static com.android.SdkConstants.TOOLS_URI;
import static org.eclipse.ui.IWorkbenchPage.MATCH_INPUT;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.sdklib.SdkVersionInfo;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper.IProjectFilter;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.repository.PkgProps;
import com.android.utils.XmlUtils;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables;
import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IURIEditorInput;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.editors.text.TextFileDocumentProvider;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.io.File;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
/** Utility methods for ADT */
@SuppressWarnings("restriction") // WST API
public class AdtUtils {
/**
* Creates a Java class name out of the given string, if possible. For
* example, "My Project" becomes "MyProject", "hello" becomes "Hello",
* "Java's" becomes "Java", and so on.
*
* @param string the string to be massaged into a Java class
* @return the string as a Java class, or null if a class name could not be
* extracted
*/
@Nullable
public static String extractClassName(@NonNull String string) {
StringBuilder sb = new StringBuilder(string.length());
int n = string.length();
int i = 0;
for (; i < n; i++) {
char c = Character.toUpperCase(string.charAt(i));
if (Character.isJavaIdentifierStart(c)) {
sb.append(c);
i++;
break;
}
}
if (sb.length() > 0) {
for (; i < n; i++) {
char c = string.charAt(i);
if (Character.isJavaIdentifierPart(c)) {
sb.append(c);
}
}
return sb.toString();
}
return null;
}
/**
* Strips off the last file extension from the given filename, e.g.
* "foo.backup.diff" will be turned into "foo.backup".
*
* Note that dot files (e.g. ".profile") will be left alone.
*
* @param filename the filename to be stripped
* @return the filename without the last file extension.
*/
public static String stripLastExtension(String filename) {
int dotIndex = filename.lastIndexOf('.');
if (dotIndex > 0) { // > 0 instead of != -1: Treat dot files (e.g. .profile) differently
return filename.substring(0, dotIndex);
} else {
return filename;
}
}
/**
* Strips off all extensions from the given filename, e.g. "foo.9.png" will
* be turned into "foo".
*
* Note that dot files (e.g. ".profile") will be left alone.
*
* @param filename the filename to be stripped
* @return the filename without any file extensions
*/
public static String stripAllExtensions(String filename) {
int dotIndex = filename.indexOf('.');
if (dotIndex > 0) { // > 0 instead of != -1: Treat dot files (e.g. .profile) differently
return filename.substring(0, dotIndex);
} else {
return filename;
}
}
/**
* Strips the given suffix from the given string, provided that the string ends with
* the suffix.
*
* @param string the full string to strip from
* @param suffix the suffix to strip out
* @return the string without the suffix at the end
*/
public static String stripSuffix(@NonNull String string, @NonNull String suffix) {
if (string.endsWith(suffix)) {
return string.substring(0, string.length() - suffix.length());
}
return string;
}
/**
* Capitalizes the string, i.e. transforms the initial [a-z] into [A-Z].
* Returns the string unmodified if the first character is not [a-z].
*
* @param str The string to capitalize.
* @return The capitalized string
*/
public static String capitalize(String str) {
if (str == null || str.length() < 1 || Character.isUpperCase(str.charAt(0))) {
return str;
}
StringBuilder sb = new StringBuilder();
sb.append(Character.toUpperCase(str.charAt(0)));
sb.append(str.substring(1));
return sb.toString();
}
/**
* Converts a CamelCase word into an underlined_word
*
* @param string the CamelCase version of the word
* @return the underlined version of the word
*/
public static String camelCaseToUnderlines(String string) {
if (string.isEmpty()) {
return string;
}
StringBuilder sb = new StringBuilder(2 * string.length());
int n = string.length();
boolean lastWasUpperCase = Character.isUpperCase(string.charAt(0));
for (int i = 0; i < n; i++) {
char c = string.charAt(i);
boolean isUpperCase = Character.isUpperCase(c);
if (isUpperCase && !lastWasUpperCase) {
sb.append('_');
}
lastWasUpperCase = isUpperCase;
c = Character.toLowerCase(c);
sb.append(c);
}
return sb.toString();
}
/**
* Converts an underlined_word into a CamelCase word
*
* @param string the underlined word to convert
* @return the CamelCase version of the word
*/
public static String underlinesToCamelCase(String string) {
StringBuilder sb = new StringBuilder(string.length());
int n = string.length();
int i = 0;
boolean upcaseNext = true;
for (; i < n; i++) {
char c = string.charAt(i);
if (c == '_') {
upcaseNext = true;
} else {
if (upcaseNext) {
c = Character.toUpperCase(c);
}
upcaseNext = false;
sb.append(c);
}
}
return sb.toString();
}
/**
* Returns the current editor (the currently visible and active editor), or null if
* not found
*
* @return the current editor, or null
*/
public static IEditorPart getActiveEditor() {
IWorkbenchWindow window = getActiveWorkbenchWindow();
if (window != null) {
IWorkbenchPage page = window.getActivePage();
if (page != null) {
return page.getActiveEditor();
}
}
return null;
}
/**
* Returns the current active workbench, or null if not found
*
* @return the current window, or null
*/
@Nullable
public static IWorkbenchWindow getActiveWorkbenchWindow() {
IWorkbench workbench = PlatformUI.getWorkbench();
IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
if (window == null) {
IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
if (windows.length > 0) {
window = windows[0];
}
}
return window;
}
/**
* Returns the current active workbench page, or null if not found
*
* @return the current page, or null
*/
@Nullable
public static IWorkbenchPage getActiveWorkbenchPage() {
IWorkbenchWindow window = getActiveWorkbenchWindow();
if (window != null) {
IWorkbenchPage page = window.getActivePage();
if (page == null) {
IWorkbenchPage[] pages = window.getPages();
if (pages.length > 0) {
page = pages[0];
}
}
return page;
}
return null;
}
/**
* Returns the current active workbench part, or null if not found
*
* @return the current active workbench part, or null
*/
@Nullable
public static IWorkbenchPart getActivePart() {
IWorkbenchWindow window = getActiveWorkbenchWindow();
if (window != null) {
IWorkbenchPage activePage = window.getActivePage();
if (activePage != null) {
return activePage.getActivePart();
}
}
return null;
}
/**
* Returns the current text editor (the currently visible and active editor), or null
* if not found.
*
* @return the current text editor, or null
*/
public static ITextEditor getActiveTextEditor() {
IEditorPart editor = getActiveEditor();
if (editor != null) {
if (editor instanceof ITextEditor) {
return (ITextEditor) editor;
} else {
return (ITextEditor) editor.getAdapter(ITextEditor.class);
}
}
return null;
}
/**
* Looks through the open editors and returns the editors that have the
* given file as input.
*
* @param file the file to search for
* @param restore whether editors should be restored (if they have an open
* tab, but the editor hasn't been restored since the most recent
* IDE start yet
* @return a collection of editors
*/
@NonNull
public static Collection findEditorsFor(@NonNull IFile file, boolean restore) {
FileEditorInput input = new FileEditorInput(file);
List result = null;
IWorkbench workbench = PlatformUI.getWorkbench();
IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
for (IWorkbenchWindow window : windows) {
IWorkbenchPage[] pages = window.getPages();
for (IWorkbenchPage page : pages) {
IEditorReference[] editors = page.findEditors(input, null, MATCH_INPUT);
if (editors != null) {
for (IEditorReference reference : editors) {
IEditorPart editor = reference.getEditor(restore);
if (editor != null) {
if (result == null) {
result = new ArrayList();
}
result.add(editor);
}
}
}
}
}
if (result == null) {
return Collections.emptyList();
}
return result;
}
/**
* Attempts to convert the given {@link URL} into a {@link File}.
*
* @param url the {@link URL} to be converted
* @return the corresponding {@link File}, which may not exist
*/
@NonNull
public static File getFile(@NonNull URL url) {
try {
// First try URL.toURI(): this will work for URLs that contain %20 for spaces etc.
// Unfortunately, it *doesn't* work for "broken" URLs where the URL contains
// spaces, which is often the case.
return new File(url.toURI());
} catch (URISyntaxException e) {
// ...so as a fallback, go to the old url.getPath() method, which handles space paths.
return new File(url.getPath());
}
}
/**
* Returns the file for the current editor, if any.
*
* @return the file for the current editor, or null if none
*/
public static IFile getActiveFile() {
IEditorPart editor = getActiveEditor();
if (editor != null) {
IEditorInput input = editor.getEditorInput();
if (input instanceof IFileEditorInput) {
IFileEditorInput fileInput = (IFileEditorInput) input;
return fileInput.getFile();
}
}
return null;
}
/**
* Returns an absolute path to the given resource
*
* @param resource the resource to look up a path for
* @return an absolute file system path to the resource
*/
@NonNull
public static IPath getAbsolutePath(@NonNull IResource resource) {
IPath location = resource.getRawLocation();
if (location != null) {
return location.makeAbsolute();
} else {
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IWorkspaceRoot root = workspace.getRoot();
IPath workspacePath = root.getLocation();
return workspacePath.append(resource.getFullPath());
}
}
/**
* Converts a workspace-relative path to an absolute file path
*
* @param path the workspace-relative path to convert
* @return the corresponding absolute file in the file system
*/
@NonNull
public static File workspacePathToFile(@NonNull IPath path) {
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
IResource res = root.findMember(path);
if (res != null) {
IPath location = res.getLocation();
if (location != null) {
return location.toFile();
}
return root.getLocation().append(path).toFile();
}
return path.toFile();
}
/**
* Converts a {@link File} to an {@link IFile}, if possible.
*
* @param file a file to be converted
* @return the corresponding {@link IFile}, or null
*/
public static IFile fileToIFile(File file) {
if (!file.isAbsolute()) {
file = file.getAbsoluteFile();
}
IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
IFile[] files = workspace.findFilesForLocationURI(file.toURI());
if (files.length > 0) {
return files[0];
}
IPath filePath = new Path(file.getPath());
return pathToIFile(filePath);
}
/**
* Converts a {@link File} to an {@link IResource}, if possible.
*
* @param file a file to be converted
* @return the corresponding {@link IResource}, or null
*/
public static IResource fileToResource(File file) {
if (!file.isAbsolute()) {
file = file.getAbsoluteFile();
}
IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
IFile[] files = workspace.findFilesForLocationURI(file.toURI());
if (files.length > 0) {
return files[0];
}
IPath filePath = new Path(file.getPath());
return pathToResource(filePath);
}
/**
* Converts a {@link IPath} to an {@link IFile}, if possible.
*
* @param path a path to be converted
* @return the corresponding {@link IFile}, or null
*/
public static IFile pathToIFile(IPath path) {
IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
IFile[] files = workspace.findFilesForLocationURI(URIUtil.toURI(path.makeAbsolute()));
if (files.length > 0) {
return files[0];
}
IPath workspacePath = workspace.getLocation();
if (workspacePath.isPrefixOf(path)) {
IPath relativePath = path.makeRelativeTo(workspacePath);
IResource member = workspace.findMember(relativePath);
if (member instanceof IFile) {
return (IFile) member;
}
} else if (path.isAbsolute()) {
return workspace.getFileForLocation(path);
}
return null;
}
/**
* Converts a {@link IPath} to an {@link IResource}, if possible.
*
* @param path a path to be converted
* @return the corresponding {@link IResource}, or null
*/
public static IResource pathToResource(IPath path) {
IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
IFile[] files = workspace.findFilesForLocationURI(URIUtil.toURI(path.makeAbsolute()));
if (files.length > 0) {
return files[0];
}
IPath workspacePath = workspace.getLocation();
if (workspacePath.isPrefixOf(path)) {
IPath relativePath = path.makeRelativeTo(workspacePath);
return workspace.findMember(relativePath);
} else if (path.isAbsolute()) {
return workspace.getFileForLocation(path);
}
return null;
}
/**
* Returns all markers in a file/document that fit on the same line as the given offset
*
* @param markerType the marker type
* @param file the file containing the markers
* @param document the document showing the markers
* @param offset the offset to be checked
* @return a list (possibly empty but never null) of matching markers
*/
@NonNull
public static List findMarkersOnLine(
@NonNull String markerType,
@NonNull IResource file,
@NonNull IDocument document,
int offset) {
List matchingMarkers = new ArrayList(2);
try {
IMarker[] markers = file.findMarkers(markerType, true, IResource.DEPTH_ZERO);
// Look for a match on the same line as the caret.
IRegion lineInfo = document.getLineInformationOfOffset(offset);
int lineStart = lineInfo.getOffset();
int lineEnd = lineStart + lineInfo.getLength();
int offsetLine = document.getLineOfOffset(offset);
for (IMarker marker : markers) {
int start = marker.getAttribute(IMarker.CHAR_START, -1);
int end = marker.getAttribute(IMarker.CHAR_END, -1);
if (start >= lineStart && start <= lineEnd && end > start) {
matchingMarkers.add(marker);
} else if (start == -1 && end == -1) {
// Some markers don't set character range, they only set the line
int line = marker.getAttribute(IMarker.LINE_NUMBER, -1);
if (line == offsetLine + 1) {
matchingMarkers.add(marker);
}
}
}
} catch (CoreException e) {
AdtPlugin.log(e, null);
} catch (BadLocationException e) {
AdtPlugin.log(e, null);
}
return matchingMarkers;
}
/**
* Returns the available and open Android projects
*
* @return the available and open Android projects, never null
*/
@NonNull
public static IJavaProject[] getOpenAndroidProjects() {
return BaseProjectHelper.getAndroidProjects(new IProjectFilter() {
@Override
public boolean accept(IProject project) {
return project.isAccessible();
}
});
}
/**
* Returns a unique project name, based on the given {@code base} file name
* possibly with a {@code conjunction} and a new number behind it to ensure
* that the project name is unique. For example,
* {@code getUniqueProjectName("project", "_")} will return
* {@code "project"} if that name does not already exist, and if it does, it
* will return {@code "project_2"}.
*
* @param base the base name to use, such as "foo"
* @param conjunction a string to insert between the base name and the
* number.
* @return a unique project name based on the given base and conjunction
*/
public static String getUniqueProjectName(String base, String conjunction) {
// We're using all workspace projects here rather than just open Android project
// via getOpenAndroidProjects because the name cannot conflict with non-Android
// or closed projects either
IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
IProject[] projects = workspaceRoot.getProjects();
for (int i = 1; i < 1000; i++) {
String name = i == 1 ? base : base + conjunction + Integer.toString(i);
boolean found = false;
for (IProject project : projects) {
// Need to make case insensitive comparison, since otherwise we can hit
// org.eclipse.core.internal.resources.ResourceException:
// A resource exists with a different case: '/test'.
if (project.getName().equalsIgnoreCase(name)) {
found = true;
break;
}
}
if (!found) {
return name;
}
}
return base;
}
/**
* Returns the name of the parent folder for the given editor input
*
* @param editorInput the editor input to check
* @return the parent folder, which is never null but may be ""
*/
@NonNull
public static String getParentFolderName(@Nullable IEditorInput editorInput) {
if (editorInput instanceof IFileEditorInput) {
IFile file = ((IFileEditorInput) editorInput).getFile();
return file.getParent().getName();
}
if (editorInput instanceof IURIEditorInput) {
IURIEditorInput urlEditorInput = (IURIEditorInput) editorInput;
String path = urlEditorInput.getURI().toString();
int lastIndex = path.lastIndexOf('/');
if (lastIndex != -1) {
int lastLastIndex = path.lastIndexOf('/', lastIndex - 1);
if (lastLastIndex != -1) {
return path.substring(lastLastIndex + 1, lastIndex);
}
}
}
return "";
}
/**
* Returns the XML editor for the given editor part
*
* @param part the editor part to look up the editor for
* @return the editor or null if this part is not an XML editor
*/
@Nullable
public static AndroidXmlEditor getXmlEditor(@NonNull IEditorPart part) {
if (part instanceof AndroidXmlEditor) {
return (AndroidXmlEditor) part;
} else if (part instanceof GraphicalEditorPart) {
((GraphicalEditorPart) part).getEditorDelegate().getEditor();
}
return null;
}
/**
* Sets the given tools: attribute in the given XML editor document, adding
* the tools name space declaration if necessary, formatting the affected
* document region, and optionally comma-appending to an existing value and
* optionally opening and revealing the attribute.
*
* @param editor the associated editor
* @param element the associated element
* @param description the description of the attribute (shown in the undo
* event)
* @param name the name of the attribute
* @param value the attribute value
* @param reveal if true, open the editor and select the given attribute
* node
* @param appendValue if true, add this value as a comma separated value to
* the existing attribute value, if any
*/
public static void setToolsAttribute(
@NonNull final AndroidXmlEditor editor,
@NonNull final Element element,
@NonNull final String description,
@NonNull final String name,
@Nullable final String value,
final boolean reveal,
final boolean appendValue) {
editor.wrapUndoEditXmlModel(description, new Runnable() {
@Override
public void run() {
String prefix = XmlUtils.lookupNamespacePrefix(element, TOOLS_URI, null, true);
if (prefix == null) {
// Add in new prefix...
prefix = XmlUtils.lookupNamespacePrefix(element,
TOOLS_URI, TOOLS_PREFIX, true /*create*/);
if (value != null) {
// ...and ensure that the header is formatted such that
// the XML namespace declaration is placed in the right
// position and wrapping is applied etc.
editor.scheduleNodeReformat(editor.getUiRootNode(),
true /*attributesOnly*/);
}
}
String v = value;
if (appendValue && v != null) {
String prev = element.getAttributeNS(TOOLS_URI, name);
if (prev.length() > 0) {
v = prev + ',' + value;
}
}
// Use the non-namespace form of set attribute since we can't
// reference the namespace until the model has been reloaded
if (v != null) {
element.setAttribute(prefix + ':' + name, v);
} else {
element.removeAttribute(prefix + ':' + name);
}
UiElementNode rootUiNode = editor.getUiRootNode();
if (rootUiNode != null && v != null) {
final UiElementNode uiNode = rootUiNode.findXmlNode(element);
if (uiNode != null) {
editor.scheduleNodeReformat(uiNode, true /*attributesOnly*/);
if (reveal) {
// Update editor selection after format
Display display = AdtPlugin.getDisplay();
if (display != null) {
display.asyncExec(new Runnable() {
@Override
public void run() {
Node xmlNode = uiNode.getXmlNode();
Attr attribute = ((Element) xmlNode).getAttributeNodeNS(
TOOLS_URI, name);
if (attribute instanceof IndexedRegion) {
IndexedRegion region = (IndexedRegion) attribute;
editor.getStructuredTextEditor().selectAndReveal(
region.getStartOffset(), region.getLength());
}
}
});
}
}
}
}
}
});
}
/**
* Returns a string label for the given target, of the form
* "API 16: Android 4.1 (Jelly Bean)".
*
* @param target the target to generate a string from
* @return a suitable display string
*/
@NonNull
public static String getTargetLabel(@NonNull IAndroidTarget target) {
if (target.isPlatform()) {
AndroidVersion version = target.getVersion();
String codename = target.getProperty(PkgProps.PLATFORM_CODENAME);
String release = target.getProperty("ro.build.version.release"); //$NON-NLS-1$
if (codename != null) {
return String.format("API %1$d: Android %2$s (%3$s)",
version.getApiLevel(),
release,
codename);
}
return String.format("API %1$d: Android %2$s", version.getApiLevel(),
release);
}
return String.format("%1$s (API %2$s)", target.getFullName(),
target.getVersion().getApiString());
}
/**
* Sets the given tools: attribute in the given XML editor document, adding
* the tools name space declaration if necessary, and optionally
* comma-appending to an existing value.
*
* @param file the file associated with the element
* @param element the associated element
* @param description the description of the attribute (shown in the undo
* event)
* @param name the name of the attribute
* @param value the attribute value
* @param appendValue if true, add this value as a comma separated value to
* the existing attribute value, if any
*/
public static void setToolsAttribute(
@NonNull final IFile file,
@NonNull final Element element,
@NonNull final String description,
@NonNull final String name,
@Nullable final String value,
final boolean appendValue) {
IModelManager modelManager = StructuredModelManager.getModelManager();
if (modelManager == null) {
return;
}
try {
IStructuredModel model = null;
if (model == null) {
model = modelManager.getModelForEdit(file);
}
if (model != null) {
try {
model.aboutToChangeModel();
if (model instanceof IDOMModel) {
IDOMModel domModel = (IDOMModel) model;
Document doc = domModel.getDocument();
if (doc != null && element.getOwnerDocument() == doc) {
String prefix = XmlUtils.lookupNamespacePrefix(element, TOOLS_URI,
null, true);
if (prefix == null) {
// Add in new prefix...
prefix = XmlUtils.lookupNamespacePrefix(element,
TOOLS_URI, TOOLS_PREFIX, true);
}
String v = value;
if (appendValue && v != null) {
String prev = element.getAttributeNS(TOOLS_URI, name);
if (prev.length() > 0) {
v = prev + ',' + value;
}
}
// Use the non-namespace form of set attribute since we can't
// reference the namespace until the model has been reloaded
if (v != null) {
element.setAttribute(prefix + ':' + name, v);
} else {
element.removeAttribute(prefix + ':' + name);
}
}
}
} finally {
model.changedModel();
String updated = model.getStructuredDocument().get();
model.releaseFromEdit();
model.save(file);
// Must also force a save on disk since the above model.save(file) often
// (always?) has no effect.
ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager();
NullProgressMonitor monitor = new NullProgressMonitor();
IPath path = file.getFullPath();
manager.connect(path, LocationKind.IFILE, monitor);
try {
ITextFileBuffer buffer = manager.getTextFileBuffer(path,
LocationKind.IFILE);
IDocument currentDocument = buffer.getDocument();
currentDocument.set(updated);
buffer.commit(monitor, true);
} finally {
manager.disconnect(path, LocationKind.IFILE, monitor);
}
}
}
} catch (Exception e) {
AdtPlugin.log(e, null);
}
}
/**
* Returns the Android version and code name of the given API level
*
* @param api the api level
* @return a suitable version display name
*/
public static String getAndroidName(int api) {
if (api <= SdkVersionInfo.HIGHEST_KNOWN_API) {
return SdkVersionInfo.getAndroidName(api);
}
// Consult SDK manager to see if we know any more (later) names,
// installed by user
Sdk sdk = Sdk.getCurrent();
if (sdk != null) {
for (IAndroidTarget target : sdk.getTargets()) {
if (target.isPlatform()) {
AndroidVersion version = target.getVersion();
if (version.getApiLevel() == api) {
return getTargetLabel(target);
}
}
}
}
return "API " + api;
}
/**
* Returns the highest known API level to this version of ADT. The
* {@link #getAndroidName(int)} method will return real names up to and
* including this number.
*
* @return the highest known API number
*/
public static int getHighestKnownApiLevel() {
return SdkVersionInfo.HIGHEST_KNOWN_API;
}
/**
* Returns a list of known API names
*
* @return a list of string API names, starting from 1 and up through the
* maximum known versions (with no gaps)
*/
public static String[] getKnownVersions() {
int max = getHighestKnownApiLevel();
Sdk sdk = Sdk.getCurrent();
if (sdk != null) {
for (IAndroidTarget target : sdk.getTargets()) {
if (target.isPlatform()) {
AndroidVersion version = target.getVersion();
if (!version.isPreview()) {
max = Math.max(max, version.getApiLevel());
}
}
}
}
String[] versions = new String[max];
for (int api = 1; api <= max; api++) {
versions[api-1] = getAndroidName(api);
}
return versions;
}
/**
* Returns the Android project(s) that are selected or active, if any. This
* considers the selection, the active editor, etc.
*
* @param selection the current selection
* @return a list of projects, possibly empty (but never null)
*/
@NonNull
public static List getSelectedProjects(@Nullable ISelection selection) {
List projects = new ArrayList();
if (selection instanceof IStructuredSelection) {
IStructuredSelection structuredSelection = (IStructuredSelection) selection;
// get the unique selected item.
Iterator> iterator = structuredSelection.iterator();
while (iterator.hasNext()) {
Object element = iterator.next();
// First look up the resource (since some adaptables
// provide an IResource but not an IProject, and we can
// always go from IResource to IProject)
IResource resource = null;
if (element instanceof IResource) { // may include IProject
resource = (IResource) element;
} else if (element instanceof IAdaptable) {
IAdaptable adaptable = (IAdaptable)element;
Object adapter = adaptable.getAdapter(IResource.class);
resource = (IResource) adapter;
}
// get the project object from it.
IProject project = null;
if (resource != null) {
project = resource.getProject();
} else if (element instanceof IAdaptable) {
project = (IProject) ((IAdaptable) element).getAdapter(IProject.class);
}
if (project != null && !projects.contains(project)) {
projects.add(project);
}
}
}
if (projects.isEmpty()) {
// Try to look at the active editor instead
IFile file = AdtUtils.getActiveFile();
if (file != null) {
projects.add(file.getProject());
}
}
if (projects.isEmpty()) {
// If we didn't find a default project based on the selection, check how many
// open Android projects we can find in the current workspace. If there's only
// one, we'll just select it by default.
IJavaProject[] open = AdtUtils.getOpenAndroidProjects();
for (IJavaProject project : open) {
projects.add(project.getProject());
}
return projects;
} else {
// Make sure all the projects are Android projects
List androidProjects = new ArrayList(projects.size());
for (IProject project : projects) {
if (BaseProjectHelper.isAndroidProject(project)) {
androidProjects.add(project);
}
}
return androidProjects;
}
}
private static Boolean sEclipse4;
/**
* Returns true if the running Eclipse is version 4.x or later
*
* @return true if the current Eclipse version is 4.x or later, false
* otherwise
*/
public static boolean isEclipse4() {
if (sEclipse4 == null) {
sEclipse4 = Platform.getBundle("org.eclipse.e4.ui.model.workbench") != null; //$NON-NLS-1$
}
return sEclipse4;
}
/**
* Reads the contents of an {@link IFile} and return it as a byte array
*
* @param file the file to be read
* @return the String read from the file, or null if there was an error
*/
@SuppressWarnings("resource") // Eclipse doesn't understand Closeables.closeQuietly yet
@Nullable
public static byte[] readData(@NonNull IFile file) {
InputStream contents = null;
try {
contents = file.getContents();
return ByteStreams.toByteArray(contents);
} catch (Exception e) {
// Pass -- just return null
} finally {
Closeables.closeQuietly(contents);
}
return null;
}
/**
* Ensure that a given folder (and all its parents) are created. This implements
* the equivalent of {@link File#mkdirs()} for {@link IContainer} folders.
*
* @param container the container to ensure exists
* @throws CoreException if an error occurs
*/
public static void ensureExists(@Nullable IContainer container) throws CoreException {
if (container == null || container.exists()) {
return;
}
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
IFolder folder = root.getFolder(container.getFullPath());
ensureExists(folder);
}
private static void ensureExists(IFolder folder) throws CoreException {
if (folder != null && !folder.exists()) {
IContainer parent = folder.getParent();
if (parent instanceof IFolder) {
ensureExists((IFolder) parent);
}
folder.create(false, false, null);
}
}
/**
* Format the given floating value into an XML string, omitting decimals if
* 0
*
* @param value the value to be formatted
* @return the corresponding XML string for the value
*/
public static String formatFloatAttribute(float value) {
if (value != (int) value) {
// Run String.format without a locale, because we don't want locale-specific
// conversions here like separating the decimal part with a comma instead of a dot!
return String.format((Locale) null, "%.2f", value); //$NON-NLS-1$
} else {
return Integer.toString((int) value);
}
}
/**
* Creates all the directories required for the given path.
*
* @param wsPath the path to create all the parent directories for
* @return true if all the parent directories were created
*/
public static boolean createWsParentDirectory(IContainer wsPath) {
if (wsPath.getType() == IResource.FOLDER) {
if (wsPath.exists()) {
return true;
}
IFolder folder = (IFolder) wsPath;
try {
if (createWsParentDirectory(wsPath.getParent())) {
folder.create(true /* force */, true /* local */, null /* monitor */);
return true;
}
} catch (CoreException e) {
e.printStackTrace();
}
}
return false;
}
/**
* Lists the files of the given directory and returns them as an array which
* is never null. This simplifies processing file listings from for each
* loops since {@link File#listFiles} can return null. This method simply
* wraps it and makes sure it returns an empty array instead if necessary.
*
* @param dir the directory to list
* @return the children, or empty if it has no children, is not a directory,
* etc.
*/
@NonNull
public static File[] listFiles(File dir) {
File[] files = dir.listFiles();
if (files != null) {
return files;
} else {
return new File[0];
}
}
/**
* Closes all open editors that are showing a file for the given project. This method
* should be called when a project is closed or deleted.
*
* This method can be called from any thread, but if it is not called on the GUI thread
* the editor will be closed asynchronously.
*
* @param project the project to close all editors for
* @param save whether unsaved editors should be saved first
*/
public static void closeEditors(@NonNull final IProject project, final boolean save) {
final Display display = AdtPlugin.getDisplay();
if (display == null || display.isDisposed()) {
return;
}
if (display.getThread() != Thread.currentThread()) {
display.asyncExec(new Runnable() {
@Override
public void run() {
closeEditors(project, save);
}
});
return;
}
// Close editors for removed files
IWorkbench workbench = PlatformUI.getWorkbench();
for (IWorkbenchWindow window : workbench.getWorkbenchWindows()) {
for (IWorkbenchPage page : window.getPages()) {
List matching = null;
for (IEditorReference ref : page.getEditorReferences()) {
boolean close = false;
try {
IEditorInput input = ref.getEditorInput();
if (input instanceof IFileEditorInput) {
IFileEditorInput fileInput = (IFileEditorInput) input;
if (project.equals(fileInput.getFile().getProject())) {
close = true;
}
}
} catch (PartInitException ex) {
close = true;
}
if (close) {
if (matching == null) {
matching = new ArrayList(2);
}
matching.add(ref);
}
}
if (matching != null) {
IEditorReference[] refs = new IEditorReference[matching.size()];
page.closeEditors(matching.toArray(refs), save);
}
}
}
}
/**
* Closes all open editors for the given file. Note that a file can be open in
* more than one editor, for example by using Open With on the file to choose different
* editors.
*
* This method can be called from any thread, but if it is not called on the GUI thread
* the editor will be closed asynchronously.
*
* @param file the file whose editors should be closed.
* @param save whether unsaved editors should be saved first
*/
public static void closeEditors(@NonNull final IFile file, final boolean save) {
final Display display = AdtPlugin.getDisplay();
if (display == null || display.isDisposed()) {
return;
}
if (display.getThread() != Thread.currentThread()) {
display.asyncExec(new Runnable() {
@Override
public void run() {
closeEditors(file, save);
}
});
return;
}
// Close editors for removed files
IWorkbench workbench = PlatformUI.getWorkbench();
for (IWorkbenchWindow window : workbench.getWorkbenchWindows()) {
for (IWorkbenchPage page : window.getPages()) {
List matching = null;
for (IEditorReference ref : page.getEditorReferences()) {
boolean close = false;
try {
IEditorInput input = ref.getEditorInput();
if (input instanceof IFileEditorInput) {
IFileEditorInput fileInput = (IFileEditorInput) input;
if (file.equals(fileInput.getFile())) {
close = true;
}
}
} catch (PartInitException ex) {
close = true;
}
if (close) {
// Found
if (matching == null) {
matching = new ArrayList(2);
}
matching.add(ref);
// We don't break here in case the file is
// opened multiple times with different editors.
}
}
if (matching != null) {
IEditorReference[] refs = new IEditorReference[matching.size()];
page.closeEditors(matching.toArray(refs), save);
}
}
}
}
/**
* Returns the offset region of the given 0-based line number in the given
* file
*
* @param file the file to look up the line number in
* @param line the line number (0-based, meaning that the first line is line
* 0)
* @return the corresponding offset range, or null
*/
@Nullable
public static IRegion getRegionOfLine(@NonNull IFile file, int line) {
IDocumentProvider provider = new TextFileDocumentProvider();
try {
provider.connect(file);
IDocument document = provider.getDocument(file);
if (document != null) {
return document.getLineInformation(line);
}
} catch (Exception e) {
AdtPlugin.log(e, "Can't find range information for %1$s", file.getName());
} finally {
provider.disconnect(file);
}
return null;
}
/**
* Returns all resource variations for the given file
*
* @param file resource file, which should be an XML file in one of the
* various resource folders, e.g. res/layout, res/values-xlarge, etc.
* @param includeSelf if true, include the file itself in the list,
* otherwise exclude it
* @return a list of all the resource variations
*/
public static List getResourceVariations(@Nullable IFile file, boolean includeSelf) {
if (file == null) {
return Collections.emptyList();
}
// Compute the set of layout files defining this layout resource
List variations = new ArrayList();
String name = file.getName();
IContainer parent = file.getParent();
if (parent != null) {
IContainer resFolder = parent.getParent();
if (resFolder != null) {
String parentName = parent.getName();
String prefix = parentName;
int qualifiers = prefix.indexOf('-');
if (qualifiers != -1) {
parentName = prefix.substring(0, qualifiers);
prefix = prefix.substring(0, qualifiers + 1);
} else {
prefix = prefix + '-';
}
try {
for (IResource resource : resFolder.members()) {
String n = resource.getName();
if ((n.startsWith(prefix) || n.equals(parentName))
&& resource instanceof IContainer) {
IContainer layoutFolder = (IContainer) resource;
IResource r = layoutFolder.findMember(name);
if (r instanceof IFile) {
IFile variation = (IFile) r;
if (!includeSelf && file.equals(variation)) {
continue;
}
variations.add(variation);
}
}
}
} catch (CoreException e) {
AdtPlugin.log(e, null);
}
}
}
return variations;
}
/**
* Returns whether the current thread is the UI thread
*
* @return true if the current thread is the UI thread
*/
public static boolean isUiThread() {
return AdtPlugin.getDisplay() != null
&& AdtPlugin.getDisplay().getThread() == Thread.currentThread();
}
/**
* Replaces any {@code \\uNNNN} references in the given string with the corresponding
* unicode characters.
*
* @param s the string to perform replacements in
* @return the string with unicode escapes replaced with actual characters
*/
@NonNull
public static String replaceUnicodeEscapes(@NonNull String s) {
// Handle unicode escapes
if (s.indexOf("\\u") != -1) { //$NON-NLS-1$
StringBuilder sb = new StringBuilder(s.length());
for (int i = 0, n = s.length(); i < n; i++) {
char c = s.charAt(i);
if (c == '\\' && i < n - 1) {
char next = s.charAt(i + 1);
if (next == 'u' && i < n - 5) { // case sensitive
String hex = s.substring(i + 2, i + 6);
try {
int unicodeValue = Integer.parseInt(hex, 16);
sb.append((char) unicodeValue);
i += 5;
continue;
} catch (NumberFormatException nufe) {
// Invalid escape: Just proceed to literally transcribe it
sb.append(c);
}
} else {
sb.append(c);
sb.append(next);
i++;
continue;
}
} else {
sb.append(c);
}
}
s = sb.toString();
}
return s;
}
/**
* Looks up the {@link ResourceFolderType} corresponding to a given
* {@link ResourceType}: the folder where those resources can be found.
*
* Note that {@link ResourceType#ID} is a special case: it can not just
* be defined in {@link ResourceFolderType#VALUES}, but it can also be
* defined inline via {@code @+id} in {@link ResourceFolderType#LAYOUT} and
* {@link ResourceFolderType#MENU} folders.
*
* @param type the resource type
* @return the corresponding resource folder type
*/
@NonNull
public static ResourceFolderType getFolderTypeFor(@NonNull ResourceType type) {
switch (type) {
case ANIM:
return ResourceFolderType.ANIM;
case ANIMATOR:
return ResourceFolderType.ANIMATOR;
case ARRAY:
return ResourceFolderType.VALUES;
case COLOR:
return ResourceFolderType.COLOR;
case DRAWABLE:
return ResourceFolderType.DRAWABLE;
case INTERPOLATOR:
return ResourceFolderType.INTERPOLATOR;
case LAYOUT:
return ResourceFolderType.LAYOUT;
case MENU:
return ResourceFolderType.MENU;
case MIPMAP:
return ResourceFolderType.MIPMAP;
case RAW:
return ResourceFolderType.RAW;
case XML:
return ResourceFolderType.XML;
case ATTR:
case BOOL:
case DECLARE_STYLEABLE:
case DIMEN:
case FRACTION:
case ID:
case INTEGER:
case PLURALS:
case PUBLIC:
case STRING:
case STYLE:
case STYLEABLE:
return ResourceFolderType.VALUES;
default:
assert false : type;
return ResourceFolderType.VALUES;
}
}
/**
* Looks up the {@link ResourceType} defined in a given {@link ResourceFolderType}.
*
* Note that for {@link ResourceFolderType#VALUES} there are many, many
* different types of resources that can be defined, so this method returns
* {@code null} for that scenario.
*
* Note also that {@link ResourceType#ID} is a special case: it can not just
* be defined in {@link ResourceFolderType#VALUES}, but it can also be
* defined inline via {@code @+id} in {@link ResourceFolderType#LAYOUT} and
* {@link ResourceFolderType#MENU} folders.
*
* @param folderType the resource folder type
* @return the corresponding resource type, or null if {@code folderType} is
* {@link ResourceFolderType#VALUES}
*/
@Nullable
public static ResourceType getResourceTypeFor(@NonNull ResourceFolderType folderType) {
switch (folderType) {
case ANIM:
return ResourceType.ANIM;
case ANIMATOR:
return ResourceType.ANIMATOR;
case COLOR:
return ResourceType.COLOR;
case DRAWABLE:
return ResourceType.DRAWABLE;
case INTERPOLATOR:
return ResourceType.INTERPOLATOR;
case LAYOUT:
return ResourceType.LAYOUT;
case MENU:
return ResourceType.MENU;
case MIPMAP:
return ResourceType.MIPMAP;
case RAW:
return ResourceType.RAW;
case XML:
return ResourceType.XML;
case VALUES:
return null;
default:
assert false : folderType;
return null;
}
}
}