aboutsummaryrefslogtreecommitdiffstats
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src
diff options
context:
space:
mode:
authorTor Norbye <tnorbye@google.com>2010-12-09 15:22:29 -0800
committerAndroid Code Review <code-review@android.com>2010-12-09 15:22:29 -0800
commiteadfb94154126f670fa0ada62d36fec5f6188bd9 (patch)
tree9f9b6e4ffdd16c9894b14ef7a09827c75ba254fd /eclipse/plugins/com.android.ide.eclipse.adt/src
parentf7bb5e5fe9c3e6cda5f6d2c4a1db4673a2f8f02d (diff)
parent2a58932d3c4e2642cbdbfc161b4f7b884b3d7ea6 (diff)
downloadsdk-eadfb94154126f670fa0ada62d36fec5f6188bd9.zip
sdk-eadfb94154126f670fa0ada62d36fec5f6188bd9.tar.gz
sdk-eadfb94154126f670fa0ada62d36fec5f6188bd9.tar.bz2
Merge "Add a hyperlink resolved for Android XML files"
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java238
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidSourceViewerConfig.java24
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java16
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java85
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/XmlHyperlinkResolver.java714
5 files changed, 987 insertions, 90 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
index 5050afe..4ce6675 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
@@ -34,6 +34,7 @@ import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile;
import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolder;
import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType;
import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
@@ -44,6 +45,7 @@ import com.android.ide.eclipse.adt.internal.ui.EclipseUiHelper;
import com.android.ide.eclipse.ddms.DdmsPlugin;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkConstants;
+import com.android.sdklib.io.StreamException;
import com.android.sdkstats.SdkStatsService;
import org.eclipse.core.resources.IFile;
@@ -98,6 +100,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
@@ -402,6 +406,240 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger {
}
/**
+ * Reads the contents of an {@link IFile} and return it as a String
+ *
+ * @param file the file to be read
+ * @return the String read from the file, or null if there was an error
+ */
+ public static String readFile(IFile file) {
+ InputStream contents = null;
+ try {
+ contents = file.getContents();
+ String charset = file.getCharset();
+ return readFile(new InputStreamReader(contents, charset));
+ } catch (CoreException e) {
+ // pass -- ignore files we can't read
+ } catch (UnsupportedEncodingException e) {
+ // pass -- ignore files we can't read
+ } finally {
+ try {
+ if (contents != null) {
+ contents.close();
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Can't read file %1$s", file); //NON-NLS-1$
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns true iff the given file contains the given String.
+ *
+ * @param file the file to look for the string in
+ * @param string the string to be searched for
+ * @return true if the file is found and contains the given string anywhere within it
+ */
+ public static boolean fileContains(IFile file, String string) {
+ InputStream contents = null;
+ try {
+ contents = file.getContents();
+ String charset = file.getCharset();
+ return streamContains(new InputStreamReader(contents, charset), string);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Can't read file %1$s", file); //NON-NLS-1$
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true iff the given input stream contains the given String.
+ *
+ * @param r the stream to look for the string in
+ * @param string the string to be searched for
+ * @return true if the file is found and contains the given string anywhere within it
+ */
+ public static boolean streamContains(Reader r, String string) {
+ if (string.length() == 0) {
+ return true;
+ }
+
+ PushbackReader reader = null;
+ try {
+ reader = new PushbackReader(r, string.length());
+ char first = string.charAt(0);
+ while (true) {
+ int c = reader.read();
+ if (c == -1) {
+ return false;
+ } else if (c == first) {
+ boolean matches = true;
+ for (int i = 1; i < string.length(); i++) {
+ c = reader.read();
+ if (c == -1) {
+ return false;
+ } else if (string.charAt(i) != (char)c) {
+ matches = false;
+ // Back up the characters that did not match
+ reader.backup(i-1);
+ break;
+ }
+ }
+ if (matches) {
+ return true;
+ }
+ }
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Can't read stream"); //NON-NLS-1$
+ } finally {
+ try {
+ if (reader != null) {
+ reader.close();
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Can't read stream"); //NON-NLS-1$
+ }
+ }
+
+ return false;
+
+ }
+
+ /**
+ * A special reader that allows backing up in the input (up to a predefined maximum
+ * number of characters)
+ * <p>
+ * NOTE: This class ONLY works with the {@link #read()} method!!
+ */
+ private static class PushbackReader extends BufferedReader {
+ /**
+ * Rolling/circular buffer. Can be a char rather than int since we never store EOF
+ * in it.
+ */
+ private char[] mStorage;
+
+ /** Points to the head of the queue. When equal to the tail, the queue is empty. */
+ private int mHead;
+
+ /**
+ * Points to the tail of the queue. This will move with each read of the actual
+ * wrapped reader, and the characters previous to it in the circular buffer are
+ * the most recently read characters.
+ */
+ private int mTail;
+
+ /**
+ * Creates a new reader with a given maximum number of backup characters
+ *
+ * @param reader the reader to wrap
+ * @param max the maximum number of characters to allow rollback for
+ */
+ public PushbackReader(Reader reader, int max) {
+ super(reader);
+ mStorage = new char[max + 1];
+ }
+
+ @Override
+ public int read() throws IOException {
+ // Have we backed up? If so we should serve characters
+ // from the storage
+ if (mHead != mTail) {
+ char c = mStorage[mHead];
+ mHead = (mHead + 1) % mStorage.length;
+ return c;
+ }
+ assert mHead == mTail;
+
+ // No backup -- read the next character, but stash it into storage
+ // as well such that we can retrieve it if we must.
+ int c = super.read();
+ mStorage[mHead] = (char) c;
+ mHead = mTail = (mHead + 1) % mStorage.length;
+ return c;
+ }
+
+ /**
+ * Backs up the reader a given number of characters. The next N reads will yield
+ * the N most recently read characters prior to this backup.
+ *
+ * @param n the number of characters to be backed up
+ */
+ public void backup(int n) {
+ if (n >= mStorage.length) {
+ throw new IllegalArgumentException("Exceeded backup limit");
+ }
+ assert n < mStorage.length;
+ mHead -= n;
+ if (mHead < 0) {
+ mHead += mStorage.length;
+ }
+ }
+ }
+
+ /**
+ * Reads the contents of a {@link ResourceFile} and returns it as a String
+ *
+ * @param file the file to be read
+ * @return the contents as a String, or null if reading failed
+ */
+ public static String readFile(ResourceFile file) {
+ InputStream contents = null;
+ try {
+ contents = file.getFile().getContents();
+ return readFile(new InputStreamReader(contents));
+ } catch (StreamException e) {
+ // pass -- ignore files we can't read
+ } finally {
+ try {
+ if (contents != null) {
+ contents.close();
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Can't read layout file"); //NON-NLS-1$
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Reads the contents of an {@link InputStreamReader} and return it as a String
+ *
+ * @param inputStream the input stream to be read from
+ * @return the String read from the stream, or null if there was an error
+ */
+ public static String readFile(Reader inputStream) {
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(inputStream);
+ StringBuilder sb = new StringBuilder(2000);
+ while (true) {
+ int c = reader.read();
+ if (c == -1) {
+ return sb.toString();
+ } else {
+ sb.append((char)c);
+ }
+ }
+ } catch (IOException e) {
+ // pass -- ignore files we can't read
+ } finally {
+ try {
+ if (reader != null) {
+ reader.close();
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Can't read input stream"); //NON-NLS-1$
+ }
+ }
+
+ return null;
+ }
+
+ /**
* Reads and returns the content of a text file embedded in the plugin jar
* file.
* @param filepath the file path to the text file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidSourceViewerConfig.java
index 3b41252..38d8844 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidSourceViewerConfig.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidSourceViewerConfig.java
@@ -30,6 +30,7 @@ import org.eclipse.wst.xml.core.text.IXMLPartitions;
import org.eclipse.wst.xml.ui.StructuredTextViewerConfigurationXML;
import java.util.ArrayList;
+import java.util.Map;
/**
* Base Source Viewer Configuration for Android resources.
@@ -43,7 +44,7 @@ public class AndroidSourceViewerConfig extends StructuredTextViewerConfiguration
super();
mProcessor = processor;
}
-
+
@Override
public IContentAssistant getContentAssistant(ISourceViewer sourceViewer) {
return super.getContentAssistant(sourceViewer);
@@ -52,7 +53,7 @@ public class AndroidSourceViewerConfig extends StructuredTextViewerConfiguration
/**
* Returns the content assist processors that will be used for content
* assist in the given source viewer and for the given partition type.
- *
+ *
* @param sourceViewer the source viewer to be configured by this
* configuration
* @param partitionType the partition type for which the content assist
@@ -76,7 +77,7 @@ public class AndroidSourceViewerConfig extends StructuredTextViewerConfiguration
IDocument doc = sourceViewer.getDocument();
if (doc != null)
doc.toString();
-
+
processors.add(mProcessor);
}
@@ -87,14 +88,14 @@ public class AndroidSourceViewerConfig extends StructuredTextViewerConfiguration
processors.add(p);
}
}
-
+
if (processors.size() > 0) {
return processors.toArray(new IContentAssistProcessor[processors.size()]);
} else {
return null;
}
}
-
+
@Override
public ITextHover getTextHover(ISourceViewer sourceViewer, String contentType) {
// TODO text hover for android xml
@@ -107,10 +108,21 @@ public class AndroidSourceViewerConfig extends StructuredTextViewerConfiguration
// TODO auto edit strategies for android xml
return super.getAutoEditStrategies(sourceViewer, contentType);
}
-
+
@Override
public IContentFormatter getContentFormatter(ISourceViewer sourceViewer) {
// TODO content formatter for android xml
return super.getContentFormatter(sourceViewer);
}
+
+ @Override
+ protected Map<String, ?> getHyperlinkDetectorTargets(final ISourceViewer sourceViewer) {
+ @SuppressWarnings("unchecked")
+ Map<String, ?> targets = super.getHyperlinkDetectorTargets(sourceViewer);
+ // If we want to look up more context in our HyperlinkDetector via the
+ // getAdapter method, we should place an IAdaptable object into the map here.
+ targets.put("com.android.ide.eclipse.xmlCode", null); //$NON-NLS-1$
+ return targets;
+ }
+
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java
index 5ffc17e..0f02aae 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java
@@ -923,7 +923,6 @@ public abstract class AndroidXmlEditor extends FormEditor implements IResourceCh
* tree.
* @return True if the node was shown.
*/
- @SuppressWarnings("restriction") // Yes, this method relies a lot on restricted APIs
public boolean show(Node xmlNode) {
if (xmlNode instanceof IndexedRegion) {
IndexedRegion region = (IndexedRegion)xmlNode;
@@ -946,6 +945,21 @@ public abstract class AndroidXmlEditor extends FormEditor implements IResourceCh
}
/**
+ * Selects and reveals the given range in the text editor
+ *
+ * @param start the beginning offset
+ * @param length the length of the region to show
+ */
+ public void show(int start, int length) {
+ IEditorPart textPage = getEditor(mTextPageIndex);
+ if (textPage instanceof StructuredTextEditor) {
+ StructuredTextEditor editor = (StructuredTextEditor) textPage;
+ setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID);
+ editor.selectAndReveal(start, length);
+ }
+ }
+
+ /**
* Get the XML text directly from the editor.
*
* @param xmlNode The node whose XML text we want to obtain.
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java
index f33c296..0581cc0 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java
@@ -37,7 +37,6 @@ import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager.IR
import com.android.ide.eclipse.adt.io.IFileWrapper;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.annotations.VisibleForTesting;
-import com.android.sdklib.io.StreamException;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
@@ -57,12 +56,8 @@ import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
-import java.io.BufferedReader;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
import java.io.StringReader;
-import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -438,11 +433,11 @@ public class IncludeFinder {
// If no XML model we have to read the XML contents and (possibly)
// parse it
if (!hadXmlModel) {
- String xml = readFile(file);
+ String xml = AdtPlugin.readFile(file);
includes = findIncludes(xml);
}
} else {
- String xml = readFile(resourceFile);
+ String xml = AdtPlugin.readFile(resourceFile);
includes = findIncludes(xml);
}
@@ -638,82 +633,6 @@ public class IncludeFinder {
}
}
- // ----- I/O Utilities -----
-
- /** Reads the contents of an {@link IFile} and return it as a String */
- private static String readFile(IFile file) {
- InputStream contents = null;
- try {
- contents = file.getContents();
- String charset = file.getCharset();
- return readFile(new InputStreamReader(contents, charset));
- } catch (CoreException e) {
- // pass -- ignore files we can't read
- } catch (UnsupportedEncodingException e) {
- // pass -- ignore files we can't read
- } finally {
- try {
- if (contents != null) {
- contents.close();
- }
- } catch (IOException e) {
- AdtPlugin.log(e, "Can't read layout file"); //NON-NLS-1$
- }
- }
-
- return null;
- }
-
- /** Reads the contents of a {@link ResourceFile} and returns it as a String */
- private static String readFile(ResourceFile file) {
- InputStream contents = null;
- try {
- contents = file.getFile().getContents();
- return readFile(new InputStreamReader(contents));
- } catch (StreamException e) {
- // pass -- ignore files we can't read
- } finally {
- try {
- if (contents != null) {
- contents.close();
- }
- } catch (IOException e) {
- AdtPlugin.log(e, "Can't read layout file"); //NON-NLS-1$
- }
- }
-
- return null;
- }
-
- /** Reads the contents of an {@link InputStreamReader} and return it as a String */
- private static String readFile(InputStreamReader is) {
- BufferedReader reader = null;
- try {
- reader = new BufferedReader(is);
- StringBuilder sb = new StringBuilder(2000);
- while (true) {
- int c = reader.read();
- if (c == -1) {
- return sb.toString();
- } else {
- sb.append((char) c);
- }
- }
- } catch (IOException e) {
- // pass -- ignore files we can't read
- } finally {
- try {
- if (reader != null) {
- reader.close();
- }
- } catch (IOException e) {
- AdtPlugin.log(e, "Can't read layout file"); //NON-NLS-1$
- }
- }
-
- return null;
- }
-
// ----- Cycle detection -----
private void detectCycles(String from) {
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/XmlHyperlinkResolver.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/XmlHyperlinkResolver.java
new file mode 100644
index 0000000..24f5d33
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/XmlHyperlinkResolver.java
@@ -0,0 +1,714 @@
+/*
+ * Copyright (C) 2010 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.internal.editors.xml;
+
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS;
+import static com.android.ide.common.layout.LayoutConstants.VIEW;
+import static com.android.ide.eclipse.adt.AndroidConstants.EXT_XML;
+import static com.android.ide.eclipse.adt.AndroidConstants.WS_RESOURCES;
+import static com.android.ide.eclipse.adt.AndroidConstants.WS_SEP;
+import static com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors.NAME_ATTR;
+import static com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors.ROOT_ELEMENT;
+import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_NAME;
+import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_PACKAGE;
+import static com.android.sdklib.xml.AndroidManifest.NODE_ACTIVITY;
+import static com.android.sdklib.xml.AndroidManifest.NODE_SERVICE;
+
+import com.android.ide.common.layout.Pair;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AndroidConstants;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.resources.ResourceType;
+import com.android.ide.eclipse.adt.internal.resources.manager.FolderTypeRelationship;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolder;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.io.IFolderWrapper;
+import com.android.sdklib.SdkConstants;
+import com.android.sdklib.annotations.VisibleForTesting;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.jface.action.IStatusLineManager;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.Region;
+import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
+import org.eclipse.jface.text.hyperlink.IHyperlink;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.ide.IDE;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.encoding.util.Logger;
+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.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Resolver to create hyperlinks in Android XML files to jump to associated resources --
+ * activities, layouts, strings, etc
+ */
+@SuppressWarnings("restriction")
+public class XmlHyperlinkResolver extends AbstractHyperlinkDetector {
+
+ /** Regular expression matching a FQCN for a view class */
+ @VisibleForTesting
+ /* package */ static final Pattern CLASS_PATTERN = Pattern.compile(
+ "(([a-zA-Z_\\$][a-zA-Z0-9_\\$]*)+\\.)+[a-zA-Z_\\$][a-zA-Z0-9_\\$]*"); // NON-NLS-1$
+
+ /**
+ * Determines whether the given node/attribute corresponds to a target we can link to.
+ *
+ * @param node the node surrounding the cursor
+ * @param attribute the attribute surrounding the cursor
+ * @return true if the given node/attribute pair is a link target
+ */
+ private boolean isLinkable(Node node, Attr attribute) {
+ if (isClassReference(node, attribute)) {
+ return true;
+ }
+
+ // Everything else here is attribute based
+ if (attribute == null) {
+ return false;
+ }
+
+ if (isActivity(node, attribute) || isService(node, attribute)) {
+ return true;
+ }
+
+ Pair<ResourceType,String> resource = getResource(attribute.getValue());
+ if (resource != null) {
+ ResourceType type = resource.getFirst();
+ if (type != null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if this node/attribute pair corresponds to a manifest reference to
+ * an activity.
+ */
+ private boolean isActivity(Node node, Attr attribute) {
+ // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump
+ // to it
+ String nodeName = node.getNodeName();
+ if (NODE_ACTIVITY.equals(nodeName) && ATTRIBUTE_NAME.equals(attribute.getLocalName())
+ && ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean isClassReference(Node node, Attr attribute) {
+ String tag = node.getNodeName();
+ if (attribute != null && ATTR_CLASS.equals(attribute.getLocalName()) && VIEW.equals(tag)) {
+ return true;
+ }
+
+ // If the element looks like a fully qualified class name (e.g. it's a custom view
+ // element) offer it as a link
+ if (tag.indexOf('.') != -1 && CLASS_PATTERN.matcher(tag).matches()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private String getClassFqcn(Node node, Attr attribute) {
+ String tag = node.getNodeName();
+ if (attribute != null && ATTR_CLASS.equals(attribute.getLocalName()) && VIEW.equals(tag)) {
+ return attribute.getValue();
+ }
+
+ if (tag.indexOf('.') != -1 && CLASS_PATTERN.matcher(tag).matches()) {
+ return tag;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns true if this node/attribute pair corresponds to a manifest reference to
+ * an service.
+ */
+ private boolean isService(Node node, Attr attribute) {
+ // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump to it
+ String nodeName = node.getNodeName();
+ if (NODE_SERVICE.equals(nodeName) && ATTRIBUTE_NAME.equals(attribute.getLocalName())
+ && ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the fully qualified class name of an activity referenced by the given
+ * AndroidManifest.xml node
+ */
+ private String getActivityClassFqcn(Node node, Attr attribute) {
+ StringBuilder sb = new StringBuilder();
+ Element root = node.getOwnerDocument().getDocumentElement();
+ String pkg = root.getAttribute(ATTRIBUTE_PACKAGE);
+ sb.append(pkg);
+ String className = attribute.getValue();
+ sb.append(className);
+ return sb.toString();
+ }
+
+ /**
+ * Returns the fully qualified class name of a service referenced by the given
+ * AndroidManifest.xml node
+ */
+ private String getServiceClassFqcn(Node node, Attr attribute) {
+ // Same logic
+ return getActivityClassFqcn(node, attribute);
+ }
+
+ /**
+ * Is this a resource that can be defined in any file within the "values" folder?
+ */
+ private boolean isValueResource(ResourceType type) {
+ ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type);
+ for (ResourceFolderType folderType : folderTypes) {
+ if (folderType == ResourceFolderType.VALUES) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Is this a resource that resides in a file whose name is determined by the
+ * resource name?
+ */
+ private boolean isFileResource(ResourceType type) {
+ ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type);
+ for (ResourceFolderType folderType : folderTypes) {
+ if (folderType != ResourceFolderType.VALUES) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Computes the actual exact location to jump to for a given node+attribute
+ * combination. Can optionally return an offset range within a file to highlight.
+ *
+ * @param node surrounding node
+ * @param attribute surrounding attribute
+ * @return a pair of a file location and an optional (or null) region within that file
+ */
+ private boolean open(Node node, Attr attribute) {
+ IProject project = getProject();
+ if (project == null) {
+ return false;
+ }
+
+ if (isActivity(node, attribute)) {
+ String fqcn = getActivityClassFqcn(node, attribute);
+ return openJavaClass(project, fqcn);
+ } else if (isService(node, attribute)) {
+ String fqcn = getServiceClassFqcn(node, attribute);
+ return openJavaClass(project, fqcn);
+ } else if (isClassReference(node, attribute)) {
+ return openJavaClass(project, getClassFqcn(node, attribute));
+ } else {
+ Pair<ResourceType,String> resource = getResource(attribute.getValue());
+ if (resource != null) {
+ ResourceType type = resource.getFirst();
+ if (type != null) {
+ String name = resource.getSecond();
+ IResource member = null;
+ IRegion region = null;
+
+ // Is this something found in a values/ folder?
+ if (isValueResource(type)) {
+ Pair<IFile,IRegion> def = findValueDefinition(project, type, name);
+ if (def != null) {
+ member = def.getFirst();
+ region = def.getSecond();
+ }
+ }
+
+ // Is this something found in a file identified by the name?
+ // (Some URLs can be both -- for example, a color can be both
+ // listed in an xml files in values/ as well as under /res/color/).
+ if (member == null && isFileResource(type)) {
+ // It's a single file resource, like @layout/foo; open
+ // layout/foo.xml
+ member = findMember(project, type, name);
+ }
+
+ try {
+ if (member != null && member instanceof IFile) {
+ IFile file = (IFile) member;
+ IEditorPart sourceEditor = getEditor();
+ IWorkbenchPage page = sourceEditor.getEditorSite().getPage();
+ IEditorPart targetEditor = IDE.openEditor(page, file, true);
+ if ((region != null) && (targetEditor instanceof AndroidXmlEditor)) {
+ ((AndroidXmlEditor) targetEditor).show(region.getOffset(),
+ region.getLength());
+ }
+
+ return true;
+ }
+ } catch (PartInitException pie) {
+ Logger.log(Logger.WARNING_DEBUG, pie.getMessage(), pie);
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /** Opens a Java class for the given fully qualified class name */
+ private boolean openJavaClass(IProject project, String fqcn) {
+ if (fqcn == null) {
+ return false;
+ }
+
+ // Handle inner classes
+ if (fqcn.indexOf('$') != -1) {
+ fqcn = fqcn.replaceAll("\\$", "."); //NON-NLS-1$ //NON-NLS-2$
+ }
+
+ try {
+ if (project.hasNature(JavaCore.NATURE_ID)) {
+ IJavaProject javaProject = JavaCore.create(project);
+ IJavaElement result = javaProject.findType(fqcn);
+ if (result != null) {
+ return JavaUI.openInEditor(result) != null;
+ }
+ }
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Can't open class %1$s", fqcn); // NON-NLS-1$
+ } catch (JavaModelException e) {
+ AdtPlugin.log(e, "Can't open class %1$s", fqcn); // NON-NLS-1$
+ Display.getCurrent().beep();
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't open class %1$s", fqcn); // NON-NLS-1$
+ }
+
+ return false;
+ }
+
+ /** Looks up the project member of the given type and the given name */
+ private IResource findMember(IProject project, ResourceType type, String name) {
+ String relativePath;
+ IResource member;
+ String folder = WS_RESOURCES + WS_SEP + type.getName();
+ relativePath = folder + WS_SEP + name + '.' + EXT_XML;
+ member = project.findMember(relativePath);
+ if (member == null) {
+ // Search for any file in the directory with the given basename;
+ // this is necessary for files like drawables that don't have
+ // .xml files. It's an error to have conflicts in basenames for
+ // these resources types so this is safe.
+ IResource d = project.findMember(folder);
+ if (d instanceof IFolder) {
+ IFolder dir = (IFolder) d;
+ member = findInFolder(name, dir);
+ }
+
+ if (member == null) {
+ // Still couldn't find the member; it must not be defined in a "base" directory
+ // like "layout"; look in various variations
+ ResourceManager manager = ResourceManager.getInstance();
+ ProjectResources resources = manager.getProjectResources(project);
+
+ ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type);
+ for (ResourceFolderType folderType : folderTypes) {
+ if (folderType != ResourceFolderType.VALUES) {
+ List<ResourceFolder> folders = resources.getFolders(folderType);
+ for (ResourceFolder resourceFolder : folders) {
+ if (resourceFolder.getFolder() instanceof IFolderWrapper) {
+ IFolderWrapper wrapper =
+ (IFolderWrapper) resourceFolder.getFolder();
+ IFolder iFolder = wrapper.getIFolder();
+ member = findInFolder(name, iFolder);
+ if (member != null) {
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return member;
+ }
+
+ /**
+ * Finds a resource of the given name in the given folder, searching for possible file
+ * extensions
+ */
+ private IResource findInFolder(String name, IFolder dir) {
+ try {
+ for (IResource child : dir.members()) {
+ String fileName = child.getName();
+ int index = fileName.indexOf('.');
+ if (index != -1) {
+ fileName = fileName.substring(0, index);
+ }
+ if (fileName.equals(name)) {
+ return child;
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, ""); // NON-NLS-1$
+ }
+
+ return null;
+ }
+
+ /**
+ * Search for a resource of a "multi-file" type (like @string) where the value can be
+ * found in any file within the folder containing resource of that type (in the case
+ * of @string, "values", and in the case of @color, "colors", etc).
+ */
+ private Pair<IFile, IRegion> findValueDefinition(IProject project, ResourceType type,
+ String name) {
+ // Search within the files in the values folder and find the value which defines
+ // the given resource. To be efficient, we will only parse XML files that contain
+ // a string match of the given token name.
+
+ String values = AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP
+ + SdkConstants.FD_VALUES;
+ IFolder f = project.getFolder(values);
+ if (f.exists()) {
+ try {
+ // Check XML files in values/
+ for (IResource resource : f.members()) {
+ if (resource.exists() && !resource.isDerived() && resource instanceof IFile) {
+ IFile file = (IFile) resource;
+ // Must have an XML extension
+ if (EXT_XML.equals(file.getFileExtension())) {
+ Pair<IFile, IRegion> target = findInXml(type, name, file);
+ if (target != null) {
+ return target;
+ }
+ }
+ }
+ }
+ } catch (CoreException e) {
+ // pass
+ }
+ }
+ return null;
+ }
+
+ /** Parses the given file and locates a definition of the given resource */
+ private Pair<IFile, IRegion> findInXml(ResourceType type, String name, IFile file) {
+ IStructuredModel model = null;
+ try {
+ model = StructuredModelManager.getModelManager().getExistingModelForRead(file);
+ if (model == null) {
+ // There is no open or cached model for the file; see if the file looks
+ // like it's interesting (content contains the String name we are looking for)
+ if (AdtPlugin.fileContains(file, name)) {
+ // Yes, so parse content
+ model = StructuredModelManager.getModelManager().getModelForRead(file);
+ }
+ }
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ Document document = domModel.getDocument();
+ return findInDocument(type, name, file, document);
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Can't parse %1$s", file); // NON-NLS-1$
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't parse %1$s", file); // NON-NLS-1$
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ return null;
+ }
+
+ /** Looks within an XML DOM document for the given resource name and returns it */
+ private Pair<IFile, IRegion> findInDocument(ResourceType type, String name, IFile file,
+ Document document) {
+ Element root = document.getDocumentElement();
+ if (root.getTagName().equals(ROOT_ELEMENT)) {
+ NodeList children = root.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element)child;
+ if (element.getTagName().equals(type.getName())) {
+ String elementName = element.getAttribute(NAME_ATTR);
+ if (elementName.equals(name)) {
+ IRegion region = null;
+ if (element instanceof IndexedRegion) {
+ IndexedRegion r = (IndexedRegion) element;
+ // IndexedRegion.getLength() returns bogus values
+ int length = r.getEndOffset() - r.getStartOffset();
+ region = new Region(r.getStartOffset(), length);
+ }
+
+ return Pair.of(file, region);
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** Return the resource type of the given url, and the resource name */
+ private Pair<ResourceType,String> getResource(String url) {
+ if (!url.startsWith("@")) { //$NON-NLS-1$
+ return null;
+ }
+ int typeEnd = url.indexOf('/', 1);
+ if (typeEnd == -1) {
+ return null;
+ }
+ int nameBegin = typeEnd + 1;
+
+ // Skip @ and @+
+ int typeBegin = url.startsWith("@+") ? 2 : 1; // NON-NLS-1$
+
+ int colon = url.lastIndexOf(':', typeEnd);
+ if (colon != -1) {
+ typeBegin = colon + 1;
+ }
+ String typeName = url.substring(typeBegin, typeEnd);
+ ResourceType type = ResourceType.getEnum(typeName);
+ if (type == null) {
+ return null;
+ }
+ String name = url.substring(nameBegin);
+
+ return Pair.of(type, name);
+ }
+
+ // ----- Implements IHyperlinkDetector -----
+
+ public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region,
+ boolean canShowMultipleHyperlinks) {
+
+ if (region == null || textViewer == null) {
+ return null;
+ }
+
+ IDocument document = textViewer.getDocument();
+ Node currentNode = getCurrentNode(document, region.getOffset());
+ if (currentNode == null || currentNode.getNodeType() != Node.ELEMENT_NODE) {
+ return null;
+ }
+
+ Attr currentAttr = getCurrentAttrNode(currentNode, region.getOffset());
+ IRegion range = null;
+ if (currentAttr == null) {
+ if (currentNode instanceof IndexedRegion) {
+ IndexedRegion r = (IndexedRegion) currentNode;
+ range = new Region(r.getStartOffset() + 1, currentNode.getNodeName().length());
+ }
+ } else if (currentAttr instanceof IndexedRegion) {
+ IndexedRegion r = (IndexedRegion) currentAttr;
+ range = new Region(r.getStartOffset(), r.getLength());
+ }
+
+ if (isLinkable(currentNode, currentAttr) && range != null) {
+ IHyperlink hyperlink = new DeferredResolutionLink(currentNode, currentAttr, range);
+ if (hyperlink != null) {
+ return new IHyperlink[] {
+ hyperlink
+ };
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the editor applicable to this hyperlink detection */
+ private IEditorPart getEditor() {
+ // I would like to be able to find this via getAdapter(TextEditor.class) but
+ // couldn't find a way to initialize the editor context from
+ // AndroidSourceViewerConfig#getHyperlinkDetectorTargets (which only has
+ // a TextViewer, not a TextEditor, instance).
+ //
+ // Therefore, for now, use a hack. This hack is reasonable because hyperlink
+ // resolvers are only run for the front-most visible window in the active
+ // workbench.
+ IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (window != null) {
+ IWorkbenchPage page = window.getActivePage();
+ if (page != null) {
+ return page.getActiveEditor();
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the project applicable to this hyperlink detection */
+ private IProject getProject() {
+ IEditorPart editor = getEditor();
+ if (editor != null) {
+ IEditorInput input = editor.getEditorInput();
+ if (input instanceof IFileEditorInput) {
+ IFileEditorInput fileInput = (IFileEditorInput) input;
+ return fileInput.getFile().getProject();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Hyperlink implementation which delays computing the actual file and offset target
+ * until it is asked to open the hyperlink
+ */
+ class DeferredResolutionLink implements IHyperlink {
+ private Node mNode;
+ private Attr mAttribute;
+ private IRegion mRegion;
+
+ public DeferredResolutionLink(Node mNode, Attr mAttribute, IRegion mRegion) {
+ super();
+ this.mNode = mNode;
+ this.mAttribute = mAttribute;
+ this.mRegion = mRegion;
+ }
+
+ public IRegion getHyperlinkRegion() {
+ return mRegion;
+ }
+
+ public String getHyperlinkText() {
+ return null;
+ }
+
+ public String getTypeLabel() {
+ return null;
+ }
+
+ public void open() {
+ // Lazily compute the location to open
+ if (!XmlHyperlinkResolver.this.open(mNode, mAttribute)) {
+ // Failed: display message to the user
+ String message = String.format("Could not open %1$s", mAttribute.getValue());
+ IEditorSite editorSite = getEditor().getEditorSite();
+ IStatusLineManager status = editorSite.getActionBars().getStatusLineManager();
+ status.setErrorMessage(message);
+ }
+ }
+ }
+
+ // The below code are private utility methods copied from the XMLHyperlinkDetector
+ // in the Eclipse WTP.
+
+ /**
+ * Returns the attribute node within node at offset.
+ * <p>
+ * Copy of Eclipse's XMLHyperlinkDetector#getCurrentAttrNode
+ */
+ private Attr getCurrentAttrNode(Node node, int offset) {
+ if ((node instanceof IndexedRegion) && ((IndexedRegion) node).contains(offset)
+ && (node.hasAttributes())) {
+ NamedNodeMap attrs = node.getAttributes();
+ // go through each attribute in node and if attribute contains
+ // offset, return that attribute
+ for (int i = 0; i < attrs.getLength(); ++i) {
+ // assumption that if parent node is of type IndexedRegion,
+ // then its attributes will also be of type IndexedRegion
+ IndexedRegion attRegion = (IndexedRegion) attrs.item(i);
+ if (attRegion.contains(offset)) {
+ return (Attr) attrs.item(i);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the node the cursor is currently on in the document. null if no node is
+ * selected
+ * <p>
+ * Copy of Eclipse's XMLHyperlinkDetector#getCurrentNode
+ */
+ private Node getCurrentNode(IDocument document, int offset) {
+ // get the current node at the offset (returns either: element,
+ // doc type, text)
+ IndexedRegion inode = null;
+ IStructuredModel sModel = null;
+ try {
+ sModel = StructuredModelManager.getModelManager().getExistingModelForRead(document);
+ if (sModel != null) {
+ inode = sModel.getIndexedRegion(offset);
+ if (inode == null) {
+ inode = sModel.getIndexedRegion(offset - 1);
+ }
+ }
+ } finally {
+ if (sModel != null) {
+ sModel.releaseFromRead();
+ }
+ }
+
+ if (inode instanceof Node) {
+ return (Node) inode;
+ }
+ return null;
+ }
+}