• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ide.eclipse.adt.internal.editors;
18 
19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
20 import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS;
21 import static com.android.ide.common.layout.LayoutConstants.ATTR_NAME;
22 import static com.android.ide.common.layout.LayoutConstants.ATTR_ON_CLICK;
23 import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
24 import static com.android.ide.common.layout.LayoutConstants.VIEW;
25 import static com.android.ide.common.resources.ResourceResolver.PREFIX_ANDROID_RESOURCE_REF;
26 import static com.android.ide.common.resources.ResourceResolver.PREFIX_RESOURCE_REF;
27 import static com.android.ide.eclipse.adt.AdtConstants.ANDROID_PKG;
28 import static com.android.ide.eclipse.adt.AdtConstants.EXT_XML;
29 import static com.android.ide.eclipse.adt.AdtConstants.FN_RESOURCE_BASE;
30 import static com.android.ide.eclipse.adt.AdtConstants.FN_RESOURCE_CLASS;
31 import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_FRAGMENT;
32 import static com.android.ide.eclipse.adt.internal.editors.values.descriptors.ValuesDescriptors.NAME_ATTR;
33 import static com.android.ide.eclipse.adt.internal.editors.values.descriptors.ValuesDescriptors.ROOT_ELEMENT;
34 import static com.android.ide.eclipse.adt.internal.editors.values.descriptors.ValuesDescriptors.STYLE_ELEMENT;
35 import static com.android.sdklib.SdkConstants.FD_DOCS;
36 import static com.android.sdklib.SdkConstants.FD_DOCS_REFERENCE;
37 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_NAME;
38 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_PACKAGE;
39 import static com.android.sdklib.xml.AndroidManifest.NODE_ACTIVITY;
40 import static com.android.sdklib.xml.AndroidManifest.NODE_SERVICE;
41 
42 import com.android.annotations.VisibleForTesting;
43 import com.android.ide.common.resources.ResourceFile;
44 import com.android.ide.common.resources.ResourceFolder;
45 import com.android.ide.common.resources.ResourceRepository;
46 import com.android.ide.common.resources.configuration.FolderConfiguration;
47 import com.android.ide.eclipse.adt.AdtPlugin;
48 import com.android.ide.eclipse.adt.AdtUtils;
49 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
50 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
51 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor;
52 import com.android.ide.eclipse.adt.internal.editors.values.descriptors.ValuesDescriptors;
53 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
54 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
55 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
56 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
57 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
58 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
59 import com.android.ide.eclipse.adt.io.IFileWrapper;
60 import com.android.ide.eclipse.adt.io.IFolderWrapper;
61 import com.android.io.FileWrapper;
62 import com.android.io.IAbstractFile;
63 import com.android.io.IAbstractFolder;
64 import com.android.resources.ResourceFolderType;
65 import com.android.resources.ResourceType;
66 import com.android.sdklib.IAndroidTarget;
67 import com.android.sdklib.SdkConstants;
68 import com.android.util.Pair;
69 
70 import org.apache.xerces.parsers.DOMParser;
71 import org.apache.xerces.xni.Augmentations;
72 import org.apache.xerces.xni.NamespaceContext;
73 import org.apache.xerces.xni.QName;
74 import org.apache.xerces.xni.XMLAttributes;
75 import org.apache.xerces.xni.XMLLocator;
76 import org.apache.xerces.xni.XNIException;
77 import org.eclipse.core.filesystem.EFS;
78 import org.eclipse.core.filesystem.IFileStore;
79 import org.eclipse.core.resources.IContainer;
80 import org.eclipse.core.resources.IFile;
81 import org.eclipse.core.resources.IFolder;
82 import org.eclipse.core.resources.IProject;
83 import org.eclipse.core.resources.IResource;
84 import org.eclipse.core.runtime.CoreException;
85 import org.eclipse.core.runtime.IPath;
86 import org.eclipse.core.runtime.NullProgressMonitor;
87 import org.eclipse.core.runtime.Path;
88 import org.eclipse.jdt.core.Flags;
89 import org.eclipse.jdt.core.ICodeAssist;
90 import org.eclipse.jdt.core.IJavaElement;
91 import org.eclipse.jdt.core.IJavaProject;
92 import org.eclipse.jdt.core.IMethod;
93 import org.eclipse.jdt.core.IType;
94 import org.eclipse.jdt.core.JavaModelException;
95 import org.eclipse.jdt.core.search.IJavaSearchConstants;
96 import org.eclipse.jdt.core.search.IJavaSearchScope;
97 import org.eclipse.jdt.core.search.SearchEngine;
98 import org.eclipse.jdt.core.search.SearchMatch;
99 import org.eclipse.jdt.core.search.SearchParticipant;
100 import org.eclipse.jdt.core.search.SearchPattern;
101 import org.eclipse.jdt.core.search.SearchRequestor;
102 import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
103 import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;
104 import org.eclipse.jdt.internal.ui.text.JavaWordFinder;
105 import org.eclipse.jdt.ui.JavaUI;
106 import org.eclipse.jdt.ui.actions.SelectionDispatchAction;
107 import org.eclipse.jface.action.IAction;
108 import org.eclipse.jface.action.IStatusLineManager;
109 import org.eclipse.jface.text.BadLocationException;
110 import org.eclipse.jface.text.IDocument;
111 import org.eclipse.jface.text.IRegion;
112 import org.eclipse.jface.text.ITextViewer;
113 import org.eclipse.jface.text.Region;
114 import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
115 import org.eclipse.jface.text.hyperlink.IHyperlink;
116 import org.eclipse.ui.IEditorInput;
117 import org.eclipse.ui.IEditorPart;
118 import org.eclipse.ui.IEditorReference;
119 import org.eclipse.ui.IEditorSite;
120 import org.eclipse.ui.IWorkbenchPage;
121 import org.eclipse.ui.PartInitException;
122 import org.eclipse.ui.ide.IDE;
123 import org.eclipse.ui.part.FileEditorInput;
124 import org.eclipse.ui.part.MultiPageEditorPart;
125 import org.eclipse.ui.texteditor.ITextEditor;
126 import org.eclipse.wst.sse.core.StructuredModelManager;
127 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
128 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
129 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
130 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
131 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
132 import org.eclipse.wst.sse.ui.StructuredTextEditor;
133 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
134 import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
135 import org.w3c.dom.Attr;
136 import org.w3c.dom.Document;
137 import org.w3c.dom.Element;
138 import org.w3c.dom.NamedNodeMap;
139 import org.w3c.dom.Node;
140 import org.w3c.dom.NodeList;
141 import org.xml.sax.InputSource;
142 import org.xml.sax.SAXException;
143 
144 import java.io.File;
145 import java.io.FileInputStream;
146 import java.io.IOException;
147 import java.net.MalformedURLException;
148 import java.net.URL;
149 import java.util.ArrayList;
150 import java.util.Collections;
151 import java.util.Comparator;
152 import java.util.List;
153 import java.util.concurrent.atomic.AtomicBoolean;
154 import java.util.regex.Pattern;
155 
156 /**
157  * Class containing hyperlink resolvers for XML and Java files to jump to associated
158  * resources -- Java Activity and Service classes, XML layout and string declarations,
159  * image drawables, etc.
160  */
161 @SuppressWarnings("restriction")
162 public class Hyperlinks {
163     private static final String CATEGORY = "category";                            //$NON-NLS-1$
164     private static final String ACTION = "action";                                //$NON-NLS-1$
165     private static final String PERMISSION = "permission";                        //$NON-NLS-1$
166     private static final String USES_PERMISSION = "uses-permission";              //$NON-NLS-1$
167     private static final String CATEGORY_PKG_PREFIX = "android.intent.category."; //$NON-NLS-1$
168     private static final String ACTION_PKG_PREFIX = "android.intent.action.";     //$NON-NLS-1$
169     private static final String PERMISSION_PKG_PREFIX = "android.permission.";    //$NON-NLS-1$
170 
Hyperlinks()171     private Hyperlinks() {
172         // Not instantiatable. This is a container class containing shared code
173         // for the various inner classes that are actual hyperlink resolvers.
174     }
175 
176     /** Regular expression matching a FQCN for a view class */
177     @VisibleForTesting
178     /* package */ static final Pattern CLASS_PATTERN = Pattern.compile(
179         "(([a-zA-Z_\\$][a-zA-Z0-9_\\$]*)+\\.)+[a-zA-Z_\\$][a-zA-Z0-9_\\$]*"); //$NON-NLS-1$
180 
181     /** Determines whether the given attribute <b>name</b> is linkable */
isAttributeNameLink(XmlContext context)182     private static boolean isAttributeNameLink(XmlContext context) {
183         // We could potentially allow you to link to builtin Android properties:
184         //   ANDROID_URI.equals(attribute.getNamespaceURI())
185         // and then jump into the res/values/attrs.xml document that is available
186         // in the SDK data directory (path found via
187         // IAndroidTarget.getPath(IAndroidTarget.ATTRIBUTES)).
188         //
189         // For now, we're not doing that.
190         //
191         // We could also allow to jump into custom attributes in custom view
192         // classes. Not yet implemented.
193 
194         return false;
195     }
196 
197     /** Determines whether the given attribute <b>value</b> is linkable */
isAttributeValueLink(XmlContext context)198     private static boolean isAttributeValueLink(XmlContext context) {
199         // Everything else here is attribute based
200         Attr attribute = context.getAttribute();
201         if (attribute == null) {
202             return false;
203         }
204 
205         if (isClassAttribute(context) || isOnClickAttribute(context)
206                 || isManifestName(context) || isStyleAttribute(context)) {
207             return true;
208         }
209 
210         String value = attribute.getValue();
211         if (value.startsWith("@+")) { //$NON-NLS-1$
212             // It's a value -declaration-, nowhere else to jump
213             // (though we could consider jumping to the R-file; would that
214             // be helpful?)
215             return false;
216         }
217 
218         Pair<ResourceType,String> resource = ResourceHelper.parseResource(value);
219         if (resource != null) {
220             ResourceType type = resource.getFirst();
221             if (type != null) {
222                 return true;
223             }
224         }
225 
226         return false;
227     }
228 
229     /** Determines whether the given element <b>name</b> is linkable */
isElementNameLink(XmlContext context)230     private static boolean isElementNameLink(XmlContext context) {
231         if (isClassElement(context)) {
232             return true;
233         }
234 
235         return false;
236     }
237 
238     /**
239      * Returns true if this node/attribute pair corresponds to a manifest reference to
240      * an activity.
241      */
isActivity(XmlContext context)242     private static boolean isActivity(XmlContext context) {
243         // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump
244         // to it
245         Attr attribute = context.getAttribute();
246         String tagName = context.getElement().getTagName();
247         if (NODE_ACTIVITY.equals(tagName) && ATTRIBUTE_NAME.equals(attribute.getLocalName())
248                 && ANDROID_URI.equals(attribute.getNamespaceURI())) {
249             return true;
250         }
251 
252         return false;
253     }
254 
255     /**
256      * Returns true if this node/attribute pair corresponds to a manifest android:name reference
257      */
isManifestName(XmlContext context)258     private static boolean isManifestName(XmlContext context) {
259         Attr attribute = context.getAttribute();
260         if (attribute != null && ATTRIBUTE_NAME.equals(attribute.getLocalName())
261                 && ANDROID_URI.equals(attribute.getNamespaceURI())) {
262             if (getEditor() instanceof ManifestEditor) {
263                 return true;
264             }
265         }
266 
267         return false;
268     }
269 
270     /**
271      * Opens the declaration corresponding to an android:name reference in the
272      * AndroidManifest.xml file
273      */
openManifestName(IProject project, XmlContext context)274     private static boolean openManifestName(IProject project, XmlContext context) {
275         if (isActivity(context)) {
276             String fqcn = getActivityClassFqcn(context);
277             return AdtPlugin.openJavaClass(project, fqcn);
278         } else if (isService(context)) {
279             String fqcn = getServiceClassFqcn(context);
280             return AdtPlugin.openJavaClass(project, fqcn);
281         } else if (isBuiltinPermission(context)) {
282             String permission = context.getAttribute().getValue();
283             // Mutate something like android.permission.ACCESS_CHECKIN_PROPERTIES
284             // into relative doc url android/Manifest.permission.html#ACCESS_CHECKIN_PROPERTIES
285             assert permission.startsWith(PERMISSION_PKG_PREFIX);
286             String relative = "android/Manifest.permission.html#" //$NON-NLS-1$
287                     + permission.substring(PERMISSION_PKG_PREFIX.length());
288 
289             URL url = getDocUrl(relative);
290             if (url != null) {
291                 AdtPlugin.openUrl(url);
292                 return true;
293             } else {
294                 return false;
295             }
296         } else if (isBuiltinIntent(context)) {
297             String intent = context.getAttribute().getValue();
298             // Mutate something like android.intent.action.MAIN into
299             // into relative doc url android/content/Intent.html#ACTION_MAIN
300             String relative;
301             if (intent.startsWith(ACTION_PKG_PREFIX)) {
302                 relative = "android/content/Intent.html#ACTION_" //$NON-NLS-1$
303                         + intent.substring(ACTION_PKG_PREFIX.length());
304             } else if (intent.startsWith(CATEGORY_PKG_PREFIX)) {
305                 relative = "android/content/Intent.html#CATEGORY_" //$NON-NLS-1$
306                         + intent.substring(CATEGORY_PKG_PREFIX.length());
307             } else {
308                 return false;
309             }
310             URL url = getDocUrl(relative);
311             if (url != null) {
312                 AdtPlugin.openUrl(url);
313                 return true;
314             } else {
315                 return false;
316             }
317         }
318 
319         return false;
320     }
321 
322     /** Returns true if this represents a style attribute */
isStyleAttribute(XmlContext context)323     private static boolean isStyleAttribute(XmlContext context) {
324         String tag = context.getElement().getTagName();
325         return STYLE_ELEMENT.equals(tag);
326     }
327 
328     /**
329      * Returns true if this represents a {@code <view class="foo.bar.Baz">} class
330      * attribute, or a {@code <fragment android:name="foo.bar.Baz">} class attribute
331      */
isClassAttribute(XmlContext context)332     private static boolean isClassAttribute(XmlContext context) {
333         Attr attribute = context.getAttribute();
334         if (attribute == null) {
335             return false;
336         }
337         String tag = context.getElement().getTagName();
338         String attributeName = attribute.getLocalName();
339         return ATTR_CLASS.equals(attributeName) && (VIEW.equals(tag) || VIEW_FRAGMENT.equals(tag))
340                 || ATTR_NAME.equals(attributeName) && VIEW_FRAGMENT.equals(tag);
341     }
342 
343     /** Returns true if this represents an onClick attribute specifying a method handler */
isOnClickAttribute(XmlContext context)344     private static boolean isOnClickAttribute(XmlContext context) {
345         Attr attribute = context.getAttribute();
346         if (attribute == null) {
347             return false;
348         }
349         return ATTR_ON_CLICK.equals(attribute.getLocalName()) && attribute.getValue().length() > 0;
350     }
351 
352     /** Returns true if this represents a {@code <foo.bar.Baz>} custom view class element */
isClassElement(XmlContext context)353     private static boolean isClassElement(XmlContext context) {
354         if (context.getAttribute() != null) {
355             // Don't match the outer element if the user is hovering over a specific attribute
356             return false;
357         }
358         // If the element looks like a fully qualified class name (e.g. it's a custom view
359         // element) offer it as a link
360         String tag = context.getElement().getTagName();
361         return (tag.indexOf('.') != -1 && CLASS_PATTERN.matcher(tag).matches());
362     }
363 
364     /** Returns the FQCN for a class declaration at the given context */
getClassFqcn(XmlContext context)365     private static String getClassFqcn(XmlContext context) {
366         if (isClassAttribute(context)) {
367             return context.getAttribute().getValue();
368         } else if (isClassElement(context)) {
369             return context.getElement().getTagName();
370         }
371 
372         return null;
373     }
374 
375     /**
376      * Returns true if this node/attribute pair corresponds to a manifest reference to
377      * an service.
378      */
isService(XmlContext context)379     private static boolean isService(XmlContext context) {
380         Attr attribute = context.getAttribute();
381         Element node = context.getElement();
382 
383         // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump to it
384         String nodeName = node.getNodeName();
385         if (NODE_SERVICE.equals(nodeName) && ATTRIBUTE_NAME.equals(attribute.getLocalName())
386                 && ANDROID_URI.equals(attribute.getNamespaceURI())) {
387             return true;
388         }
389 
390         return false;
391     }
392 
393     /**
394      * Returns a URL pointing to the Android reference documentation, either installed
395      * locally or the one on android.com
396      *
397      * @param relative a relative url to append to the root url
398      * @return a URL pointing to the documentation
399      */
getDocUrl(String relative)400     private static URL getDocUrl(String relative) {
401         // First try to find locally installed documentation
402         File sdkLocation = new File(Sdk.getCurrent().getSdkLocation());
403         File docs = new File(sdkLocation, FD_DOCS + File.separator + FD_DOCS_REFERENCE);
404         try {
405             if (docs.exists()) {
406                 String s = docs.toURI().toURL().toExternalForm();
407                 if (!s.endsWith("/")) { //$NON-NLS-1$
408                     s += "/";           //$NON-NLS-1$
409                 }
410                 return new URL(s + relative);
411             }
412             // If not, fallback to the online documentation
413             return new URL("http://developer.android.com/reference/" + relative); //$NON-NLS-1$
414         } catch (MalformedURLException e) {
415             AdtPlugin.log(e, "Can't create URL for %1$s", docs);
416             return null;
417         }
418     }
419 
420     /** Returns true if the context is pointing to a permission name reference */
isBuiltinPermission(XmlContext context)421     private static boolean isBuiltinPermission(XmlContext context) {
422         Attr attribute = context.getAttribute();
423         Element node = context.getElement();
424 
425         // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump to it
426         String nodeName = node.getNodeName();
427         if ((USES_PERMISSION.equals(nodeName) || PERMISSION.equals(nodeName))
428                 && ATTRIBUTE_NAME.equals(attribute.getLocalName())
429                 && ANDROID_URI.equals(attribute.getNamespaceURI())) {
430             String value = attribute.getValue();
431             if (value.startsWith(PERMISSION_PKG_PREFIX)) {
432                 return true;
433             }
434         }
435 
436         return false;
437     }
438 
439     /** Returns true if the context is pointing to an intent reference */
isBuiltinIntent(XmlContext context)440     private static boolean isBuiltinIntent(XmlContext context) {
441         Attr attribute = context.getAttribute();
442         Element node = context.getElement();
443 
444         // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump to it
445         String nodeName = node.getNodeName();
446         if ((ACTION.equals(nodeName) || CATEGORY.equals(nodeName))
447                 && ATTRIBUTE_NAME.equals(attribute.getLocalName())
448                 && ANDROID_URI.equals(attribute.getNamespaceURI())) {
449             String value = attribute.getValue();
450             if (value.startsWith(ACTION_PKG_PREFIX) || value.startsWith(CATEGORY_PKG_PREFIX)) {
451                 return true;
452             }
453         }
454 
455         return false;
456     }
457 
458 
459     /**
460      * Returns the fully qualified class name of an activity referenced by the given
461      * AndroidManifest.xml node
462      */
getActivityClassFqcn(XmlContext context)463     private static String getActivityClassFqcn(XmlContext context) {
464         Attr attribute = context.getAttribute();
465         Element node = context.getElement();
466         StringBuilder sb = new StringBuilder();
467         Element root = node.getOwnerDocument().getDocumentElement();
468         String pkg = root.getAttribute(ATTRIBUTE_PACKAGE);
469         String className = attribute.getValue();
470         if (className.startsWith(".")) { //$NON-NLS-1$
471             sb.append(pkg);
472         } else if (className.indexOf('.') == -1) {
473             // According to the <activity> manifest element documentation, this is not
474             // valid ( http://developer.android.com/guide/topics/manifest/activity-element.html )
475             // but it appears in manifest files and appears to be supported by the runtime
476             // so handle this in code as well:
477             sb.append(pkg);
478             sb.append('.');
479         } // else: the class name is already a fully qualified class name
480         sb.append(className);
481         return sb.toString();
482     }
483 
484     /**
485      * Returns the fully qualified class name of a service referenced by the given
486      * AndroidManifest.xml node
487      */
getServiceClassFqcn(XmlContext context)488     private static String getServiceClassFqcn(XmlContext context) {
489         // Same logic
490         return getActivityClassFqcn(context);
491     }
492 
493     /**
494      * Returns the XML tag containing an element description for value items of the given
495      * resource type
496      *
497      * @param type the resource type to query the XML tag name for
498      * @return the tag name used for value declarations in XML of resources of the given
499      *         type
500      */
getTagName(ResourceType type)501     public static String getTagName(ResourceType type) {
502         if (type == ResourceType.ID) {
503             // Ids are recorded in <item> tags instead of <id> tags
504             return ValuesDescriptors.ITEM_TAG;
505         }
506 
507         return type.getName();
508     }
509 
510     /**
511      * Computes the actual exact location to jump to for a given XML context.
512      *
513      * @param context the XML context to be opened
514      * @return true if the request was handled successfully
515      */
open(XmlContext context)516     private static boolean open(XmlContext context) {
517         IProject project = getProject();
518         if (project == null) {
519             return false;
520         }
521 
522         if (isManifestName(context)) {
523             return openManifestName(project, context);
524         } else if (isClassElement(context) || isClassAttribute(context)) {
525             return AdtPlugin.openJavaClass(project, getClassFqcn(context));
526         } else if (isOnClickAttribute(context)) {
527             return openOnClickMethod(project, context.getAttribute().getValue());
528         } else {
529             return false;
530         }
531     }
532 
533     /** Opens a path (which may not be in the workspace) */
openPath(IPath filePath, IRegion region, int offset)534     private static void openPath(IPath filePath, IRegion region, int offset) {
535         IEditorPart sourceEditor = getEditor();
536         IWorkbenchPage page = sourceEditor.getEditorSite().getPage();
537 
538         IFile file = AdtUtils.pathToIFile(filePath);
539         if (file != null && file.exists()) {
540             try {
541                 AdtPlugin.openFile(file, region);
542                 return;
543             } catch (PartInitException ex) {
544                 AdtPlugin.log(ex, "Can't open %$1s", filePath); //$NON-NLS-1$
545             }
546         } else {
547             // It's not a path in the workspace; look externally
548             // (this is probably an @android: path)
549             if (filePath.isAbsolute()) {
550                 IFileStore fileStore = EFS.getLocalFileSystem().getStore(filePath);
551                 if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) {
552                     try {
553                         IEditorPart target = IDE.openEditorOnFileStore(page, fileStore);
554                         if (target instanceof MultiPageEditorPart) {
555                             MultiPageEditorPart part = (MultiPageEditorPart) target;
556                             IEditorPart[] editors = part.findEditors(target.getEditorInput());
557                             if (editors != null) {
558                                 for (IEditorPart editor : editors) {
559                                     if (editor instanceof StructuredTextEditor) {
560                                         StructuredTextEditor ste = (StructuredTextEditor) editor;
561                                         part.setActiveEditor(editor);
562                                         ste.selectAndReveal(offset, 0);
563                                         break;
564                                     }
565                                 }
566                             }
567                         }
568 
569                         return;
570                     } catch (PartInitException ex) {
571                         AdtPlugin.log(ex, "Can't open %$1s", filePath); //$NON-NLS-1$
572                     }
573                 }
574             }
575         }
576 
577         // Failed: display message to the user
578         displayError(String.format("Could not find resource %1$s", filePath));
579     }
580 
displayError(String message)581     private static void displayError(String message) {
582         // Failed: display message to the user
583         IEditorSite editorSite = getEditor().getEditorSite();
584         IStatusLineManager status = editorSite.getActionBars().getStatusLineManager();
585         status.setErrorMessage(message);
586     }
587 
588     /**
589      * Opens a Java method referenced by the given on click attribute method name
590      *
591      * @param project the project containing the click handler
592      * @param method the method name of the on click handler
593      * @return true if the method was opened, false otherwise
594      */
openOnClickMethod(IProject project, String method)595     public static boolean openOnClickMethod(IProject project, String method) {
596         // Search for the method in the Java index, filtering by the required click handler
597         // method signature (public and has a single View parameter), and narrowing the scope
598         // first to Activity classes, then to the whole workspace.
599         final AtomicBoolean success = new AtomicBoolean(false);
600         SearchRequestor requestor = new SearchRequestor() {
601             @Override
602             public void acceptSearchMatch(SearchMatch match) throws CoreException {
603                 Object element = match.getElement();
604                 if (element instanceof IMethod) {
605                     IMethod methodElement = (IMethod) element;
606                     String[] parameterTypes = methodElement.getParameterTypes();
607                     if (parameterTypes != null
608                             && parameterTypes.length == 1
609                             && ("Qandroid.view.View;".equals(parameterTypes[0]) //$NON-NLS-1$
610                                     || "QView;".equals(parameterTypes[0]))) {   //$NON-NLS-1$
611                         // Check that it's public
612                         if (Flags.isPublic(methodElement.getFlags())) {
613                             JavaUI.openInEditor(methodElement);
614                             success.getAndSet(true);
615                         }
616                     }
617                 }
618             }
619         };
620         try {
621             IJavaSearchScope scope = null;
622             IType activityType = null;
623             IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
624             if (javaProject != null) {
625                 activityType = javaProject.findType(SdkConstants.CLASS_ACTIVITY);
626                 if (activityType != null) {
627                     scope = SearchEngine.createHierarchyScope(activityType);
628                 }
629             }
630             if (scope == null) {
631                 scope = SearchEngine.createWorkspaceScope();
632             }
633 
634             SearchParticipant[] participants = new SearchParticipant[] {
635                 SearchEngine.getDefaultSearchParticipant()
636             };
637             int matchRule = SearchPattern.R_PATTERN_MATCH | SearchPattern.R_CASE_SENSITIVE;
638             SearchPattern pattern = SearchPattern.createPattern("*." + method,
639                     IJavaSearchConstants.METHOD, IJavaSearchConstants.DECLARATIONS, matchRule);
640             SearchEngine engine = new SearchEngine();
641             engine.search(pattern, participants, scope, requestor, new NullProgressMonitor());
642 
643             boolean ok = success.get();
644             if (!ok && activityType != null) {
645                 // TODO: Create a project+dependencies scope and search only that scope
646 
647                 // Try searching again with a complete workspace scope this time
648                 scope = SearchEngine.createWorkspaceScope();
649                 engine.search(pattern, participants, scope, requestor, new NullProgressMonitor());
650 
651                 // TODO: There could be more than one match; add code to consider them all
652                 // and pick the most likely candidate and open only that one.
653 
654                 ok = success.get();
655             }
656             return ok;
657         } catch (CoreException e) {
658             AdtPlugin.log(e, null);
659         }
660         return false;
661     }
662 
663     /**
664      * Returns the current configuration, if the associated UI editor has been initialized
665      * and has an associated configuration
666      *
667      * @return the configuration for this file, or null
668      */
getConfiguration()669     private static FolderConfiguration getConfiguration() {
670         IEditorPart editor = getEditor();
671         if (editor != null) {
672             LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor);
673             GraphicalEditorPart graphicalEditor =
674                 delegate == null ? null : delegate.getGraphicalEditor();
675 
676             if (graphicalEditor != null) {
677                 return graphicalEditor.getConfiguration();
678             } else {
679                 // TODO: Could try a few more things to get the configuration:
680                 // (1) try to look at the file.getPersistentProperty(NAME_CONFIG_STATE)
681                 //    which will return previously saved state. This isn't necessary today
682                 //    since no editors seem to be lazily initialized.
683                 // (2) attempt to use the configuration from any of the other open
684                 //    files, especially files in the same directory as this one.
685             }
686 
687             // Create a configuration from the current file
688             IProject project = null;
689             IEditorInput editorInput = editor.getEditorInput();
690             if (editorInput instanceof FileEditorInput) {
691                 IFile file = ((FileEditorInput) editorInput).getFile();
692                 project = file.getProject();
693                 ProjectResources pr = ResourceManager.getInstance().getProjectResources(project);
694                 IContainer parent = file.getParent();
695                 if (parent instanceof IFolder) {
696                     ResourceFolder resFolder = pr.getResourceFolder((IFolder) parent);
697                     if (resFolder != null) {
698                         return resFolder.getConfiguration();
699                     }
700                 }
701             }
702 
703             // Might be editing a Java file, where there is no configuration context.
704             // Instead look at surrounding files in the workspace and obtain one valid
705             // configuration.
706             for (IEditorReference reference : editor.getSite().getPage().getEditorReferences()) {
707                 IEditorPart part = reference.getEditor(false /*restore*/);
708 
709                 LayoutEditorDelegate refDelegate = LayoutEditorDelegate.fromEditor(part);
710                 if (refDelegate != null) {
711                     IProject refProject = refDelegate.getEditor().getProject();
712                     if (project == null || project == refProject) {
713                         GraphicalEditorPart refGraphicalEditor = refDelegate.getGraphicalEditor();
714                         if (refGraphicalEditor != null) {
715                             return refGraphicalEditor.getConfiguration();
716                         }
717                     }
718                 }
719             }
720         }
721 
722         return null;
723     }
724 
725     /** Returns the {@link IAndroidTarget} to be used for looking up system resources */
getTarget(IProject project)726     private static IAndroidTarget getTarget(IProject project) {
727         IEditorPart editor = getEditor();
728         LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor);
729         if (delegate != null) {
730             GraphicalEditorPart graphicalEditor = delegate.getGraphicalEditor();
731             if (graphicalEditor != null) {
732                 return graphicalEditor.getRenderingTarget();
733             }
734         }
735 
736         Sdk currentSdk = Sdk.getCurrent();
737         if (currentSdk == null) {
738             return null;
739         }
740 
741         return currentSdk.getTarget(project);
742     }
743 
744     /** Return either the project resources or the framework resources (or null) */
getResources(IProject project, boolean framework)745     private static ResourceRepository getResources(IProject project, boolean framework) {
746         if (framework) {
747             IAndroidTarget target = getTarget(project);
748 
749             if (target == null && project == null && framework) {
750                 // No current project: probably jumped into some of the framework XML resource
751                 // files and attempting to jump around. Attempt to figure out which target
752                 // we're dealing with and continue looking within the same framework.
753                 IEditorPart editor = getEditor();
754                 Sdk sdk = Sdk.getCurrent();
755                 if (sdk != null && editor instanceof AndroidXmlEditor) {
756                     AndroidTargetData data = ((AndroidXmlEditor) editor).getTargetData();
757                     if (data != null) {
758                         return data.getFrameworkResources();
759                     }
760                 }
761             }
762 
763             if (target == null) {
764                 return null;
765             }
766             AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
767             if (data == null) {
768                 return null;
769             }
770             return data.getFrameworkResources();
771         } else {
772             return ResourceManager.getInstance().getProjectResources(project);
773         }
774     }
775 
776     /**
777      * Finds a definition of an id attribute in layouts. (Ids can also be defined as
778      * resources; use {@link #findValueInXml} or {@link #findValueInDocument} to locate it there.)
779      */
findIdDefinition(IProject project, String id)780     private static Pair<IFile, IRegion> findIdDefinition(IProject project, String id) {
781         // FIRST look in the same file as the originating request, that's where you usually
782         // want to jump
783         IFile self = AdtUtils.getActiveFile();
784         if (self != null && EXT_XML.equals(self.getFileExtension())) {
785             Pair<IFile, IRegion> target = findIdInXml(id, self);
786             if (target != null) {
787                 return target;
788             }
789         }
790 
791         // Look in the configuration folder: Search compatible configurations
792         ResourceRepository resources = getResources(project, false /* isFramework */);
793         FolderConfiguration configuration = getConfiguration();
794         if (configuration != null) { // Not the case when searching from Java files for example
795             List<ResourceFolder> folders = resources.getFolders(ResourceFolderType.LAYOUT);
796             if (folders != null) {
797                 for (ResourceFolder folder : folders) {
798                     if (folder.getConfiguration().isMatchFor(configuration)) {
799                         IAbstractFolder wrapper = folder.getFolder();
800                         if (wrapper instanceof IFolderWrapper) {
801                             IFolder iFolder = ((IFolderWrapper) wrapper).getIFolder();
802                             Pair<IFile, IRegion> target = findIdInFolder(iFolder, id);
803                             if (target != null) {
804                                 return target;
805                             }
806                         }
807                     }
808                 }
809                 return null;
810             }
811         }
812 
813         // Ugh. Search ALL layout files in the project!
814         List<ResourceFolder> folders = resources.getFolders(ResourceFolderType.LAYOUT);
815         if (folders != null) {
816             for (ResourceFolder folder : folders) {
817                 IAbstractFolder wrapper = folder.getFolder();
818                 if (wrapper instanceof IFolderWrapper) {
819                     IFolder iFolder = ((IFolderWrapper) wrapper).getIFolder();
820                     Pair<IFile, IRegion> target = findIdInFolder(iFolder, id);
821                     if (target != null) {
822                         return target;
823                     }
824                 }
825             }
826         }
827 
828         return null;
829     }
830 
831     /**
832      * Finds a definition of an id attribute in a particular layout folder.
833      */
findIdInFolder(IContainer f, String id)834     private static Pair<IFile, IRegion> findIdInFolder(IContainer f, String id) {
835         try {
836             // Check XML files in values/
837             for (IResource resource : f.members()) {
838                 if (resource.exists() && !resource.isDerived() && resource instanceof IFile) {
839                     IFile file = (IFile) resource;
840                     // Must have an XML extension
841                     if (EXT_XML.equals(file.getFileExtension())) {
842                         Pair<IFile, IRegion> target = findIdInXml(id, file);
843                         if (target != null) {
844                             return target;
845                         }
846                     }
847                 }
848             }
849         } catch (CoreException e) {
850             AdtPlugin.log(e, ""); //$NON-NLS-1$
851         }
852 
853         return null;
854     }
855 
856     /** Parses the given file and locates a definition of the given resource */
findValueInXml( ResourceType type, String name, IFile file)857     private static Pair<IFile, IRegion> findValueInXml(
858             ResourceType type, String name, IFile file) {
859         IStructuredModel model = null;
860         try {
861             model = StructuredModelManager.getModelManager().getExistingModelForRead(file);
862             if (model == null) {
863                 // There is no open or cached model for the file; see if the file looks
864                 // like it's interesting (content contains the String name we are looking for)
865                 if (AdtPlugin.fileContains(file, name)) {
866                     // Yes, so parse content
867                     model = StructuredModelManager.getModelManager().getModelForRead(file);
868                 }
869             }
870             if (model instanceof IDOMModel) {
871                 IDOMModel domModel = (IDOMModel) model;
872                 Document document = domModel.getDocument();
873                 return findValueInDocument(type, name, file, document);
874             }
875         } catch (IOException e) {
876             AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$
877         } catch (CoreException e) {
878             AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$
879         } finally {
880             if (model != null) {
881                 model.releaseFromRead();
882             }
883         }
884 
885         return null;
886     }
887 
888     /** Looks within an XML DOM document for the given resource name and returns it */
findValueInDocument( ResourceType type, String name, IFile file, Document document)889     private static Pair<IFile, IRegion> findValueInDocument(
890             ResourceType type, String name, IFile file, Document document) {
891         String targetTag = getTagName(type);
892         Element root = document.getDocumentElement();
893         if (root.getTagName().equals(ROOT_ELEMENT)) {
894             NodeList children = root.getChildNodes();
895             for (int i = 0, n = children.getLength(); i < n; i++) {
896                 Node child = children.item(i);
897                 if (child.getNodeType() == Node.ELEMENT_NODE) {
898                     Element element = (Element)child;
899                     if (element.getTagName().equals(targetTag)) {
900                         String elementName = element.getAttribute(NAME_ATTR);
901                         if (elementName.equals(name)) {
902                             IRegion region = null;
903                             if (element instanceof IndexedRegion) {
904                                 IndexedRegion r = (IndexedRegion) element;
905                                 // IndexedRegion.getLength() returns bogus values
906                                 int length = r.getEndOffset() - r.getStartOffset();
907                                 region = new Region(r.getStartOffset(), length);
908                             }
909 
910                             return Pair.of(file, region);
911                         }
912                     }
913                 }
914             }
915         }
916 
917         return null;
918     }
919 
920     /** Parses the given file and locates a definition of the given resource */
findIdInXml(String id, IFile file)921     private static Pair<IFile, IRegion> findIdInXml(String id, IFile file) {
922         IStructuredModel model = null;
923         try {
924             model = StructuredModelManager.getModelManager().getExistingModelForRead(file);
925             if (model == null) {
926                 // There is no open or cached model for the file; see if the file looks
927                 // like it's interesting (content contains the String name we are looking for)
928                 if (AdtPlugin.fileContains(file, id)) {
929                     // Yes, so parse content
930                     model = StructuredModelManager.getModelManager().getModelForRead(file);
931                 }
932             }
933             if (model instanceof IDOMModel) {
934                 IDOMModel domModel = (IDOMModel) model;
935                 Document document = domModel.getDocument();
936                 return findIdInDocument(id, file, document);
937             }
938         } catch (IOException e) {
939             AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$
940         } catch (CoreException e) {
941             AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$
942         } finally {
943             if (model != null) {
944                 model.releaseFromRead();
945             }
946         }
947 
948         return null;
949     }
950 
951     /** Looks within an XML DOM document for the given resource name and returns it */
findIdInDocument(String id, IFile file, Document document)952     private static Pair<IFile, IRegion> findIdInDocument(String id, IFile file,
953             Document document) {
954         String targetAttribute = NEW_ID_PREFIX + id;
955         return findIdInElement(document.getDocumentElement(), file, targetAttribute);
956     }
957 
findIdInElement( Element root, IFile file, String targetAttribute)958     private static Pair<IFile, IRegion> findIdInElement(
959             Element root, IFile file, String targetAttribute) {
960         NamedNodeMap attributes = root.getAttributes();
961         for (int i = 0, n = attributes.getLength(); i < n; i++) {
962             Node item = attributes.item(i);
963             if (item instanceof Attr) {
964                 Attr attribute = (Attr)item;
965                 String value = attribute.getValue();
966                 if (value.equals(targetAttribute)) {
967                     // Select the element -containing- the id rather than the attribute itself
968                     IRegion region = null;
969                     Node element = attribute.getOwnerElement();
970                     //if (attribute instanceof IndexedRegion) {
971                     if (element instanceof IndexedRegion) {
972                         IndexedRegion r = (IndexedRegion) element;
973                         int length = r.getEndOffset() - r.getStartOffset();
974                         region = new Region(r.getStartOffset(), length);
975                     }
976 
977                     return Pair.of(file, region);
978                 }
979             }
980         }
981 
982         NodeList children = root.getChildNodes();
983         for (int i = 0, n = children.getLength(); i < n; i++) {
984             Node child = children.item(i);
985             if (child.getNodeType() == Node.ELEMENT_NODE) {
986                 Element element = (Element)child;
987                 Pair<IFile, IRegion> result = findIdInElement(element, file, targetAttribute);
988                 if (result != null) {
989                     return result;
990                 }
991             }
992         }
993 
994         return null;
995     }
996 
997     /** Parses the given file and locates a definition of the given resource */
findValueInXml(ResourceType type, String name, File file)998     private static Pair<File, Integer> findValueInXml(ResourceType type, String name, File file) {
999         // We can't use the StructureModelManager on files outside projects
1000         // There is no open or cached model for the file; see if the file looks
1001         // like it's interesting (content contains the String name we are looking for)
1002         if (AdtPlugin.fileContains(file, name)) {
1003             try {
1004                 InputSource is = new InputSource(new FileInputStream(file));
1005                 OffsetTrackingParser parser = new OffsetTrackingParser();
1006                 parser.parse(is);
1007                 Document document = parser.getDocument();
1008 
1009                 return findValueInDocument(type, name, file, parser, document);
1010             } catch (SAXException e) {
1011                 // pass -- ignore files we can't parse
1012             } catch (IOException e) {
1013                 // pass -- ignore files we can't parse
1014             }
1015         }
1016 
1017         return null;
1018     }
1019 
1020     /** Looks within an XML DOM document for the given resource name and returns it */
findValueInDocument(ResourceType type, String name, File file, OffsetTrackingParser parser, Document document)1021     private static Pair<File, Integer> findValueInDocument(ResourceType type, String name,
1022             File file, OffsetTrackingParser parser, Document document) {
1023         String targetTag = type.getName();
1024         if (type == ResourceType.ID) {
1025             // Ids are recorded in <item> tags instead of <id> tags
1026             targetTag = "item"; //$NON-NLS-1$
1027         } else if (type == ResourceType.ATTR) {
1028             // Attributes seem to be defined in <public> tags
1029             targetTag = "public"; //$NON-NLS-1$
1030         }
1031         Element root = document.getDocumentElement();
1032         if (root.getTagName().equals(ROOT_ELEMENT)) {
1033             NodeList children = root.getChildNodes();
1034             for (int i = 0, n = children.getLength(); i < n; i++) {
1035                 Node child = children.item(i);
1036                 if (child.getNodeType() == Node.ELEMENT_NODE) {
1037                     Element element = (Element) child;
1038                     if (element.getTagName().equals(targetTag)) {
1039                         String elementName = element.getAttribute(NAME_ATTR);
1040                         if (elementName.equals(name)) {
1041 
1042                             return Pair.of(file, parser.getOffset(element));
1043                         }
1044                     }
1045                 }
1046             }
1047         }
1048 
1049         return null;
1050     }
1051 
getStyleLinks(XmlContext context, IRegion range, String url)1052     private static IHyperlink[] getStyleLinks(XmlContext context, IRegion range, String url) {
1053         Attr attribute = context.getAttribute();
1054         if (attribute != null) {
1055             // Split up theme resource urls to the nearest dot forwards, such that you
1056             // can point to "Theme.Light" by placing the caret anywhere after the dot,
1057             // and point to just "Theme" by pointing before it.
1058             int caret = context.getInnerRegionCaretOffset();
1059             String value = attribute.getValue();
1060             int index = value.indexOf('.', caret);
1061             if (index != -1) {
1062                 url = url.substring(0, index);
1063                 range = new Region(range.getOffset(),
1064                         range.getLength() - (value.length() - index));
1065             }
1066         }
1067 
1068         Pair<ResourceType,String> resource = ResourceHelper.parseResource(url);
1069         if (resource == null) {
1070             String androidStyle = "@android:style/"; //$NON-NLS-1$
1071             if (url.startsWith(PREFIX_ANDROID_RESOURCE_REF)) {
1072                 url = androidStyle + url.substring(PREFIX_ANDROID_RESOURCE_REF.length());
1073             } else if (url.startsWith(ANDROID_PKG + ':')) {
1074                 url = androidStyle + url.substring(ANDROID_PKG.length() + 1);
1075             } else {
1076                 url = "@style/" + url; //$NON-NLS-1$
1077             }
1078         }
1079         return getResourceLinks(range, url);
1080     }
1081 
1082     /**
1083      * Computes hyperlinks to resource definitions for resource urls (e.g.
1084      * {@code @android:string/ok} or {@code @layout/foo}. May create multiple links.
1085      */
getResourceLinks(IRegion range, String url)1086     private static IHyperlink[] getResourceLinks(IRegion range, String url) {
1087         List<IHyperlink> links = new ArrayList<IHyperlink>();
1088         IProject project = Hyperlinks.getProject();
1089         FolderConfiguration configuration = getConfiguration();
1090 
1091         Pair<ResourceType,String> resource = ResourceHelper.parseResource(url);
1092         if (resource == null || resource.getFirst() == null) {
1093             return null;
1094         }
1095         ResourceType type = resource.getFirst();
1096         String name = resource.getSecond();
1097 
1098         boolean isFramework = url.startsWith("@android"); //$NON-NLS-1$
1099         if (project == null) {
1100             // Local reference *within* a framework
1101             isFramework = true;
1102         }
1103 
1104         ResourceRepository resources = getResources(project, isFramework);
1105         if (resources == null) {
1106             return null;
1107         }
1108         List<ResourceFile> sourceFiles = resources.getSourceFiles(type, name,
1109                 null /*configuration*/);
1110         ResourceFile best = null;
1111         if (configuration != null && sourceFiles != null && sourceFiles.size() > 0) {
1112             List<ResourceFile> bestFiles = resources.getSourceFiles(type, name, configuration);
1113             if (bestFiles != null && bestFiles.size() > 0) {
1114                 best = bestFiles.get(0);
1115             }
1116         }
1117         if (sourceFiles != null) {
1118             List<ResourceFile> matches = new ArrayList<ResourceFile>();
1119             for (ResourceFile resourceFile : sourceFiles) {
1120                 matches.add(resourceFile);
1121             }
1122 
1123             if (matches.size() > 0) {
1124                 final ResourceFile fBest = best;
1125                 Collections.sort(matches, new Comparator<ResourceFile>() {
1126                     @Override
1127                     public int compare(ResourceFile rf1, ResourceFile rf2) {
1128                         // Sort best item to the front
1129                         if (rf1 == fBest) {
1130                             return -1;
1131                         } else if (rf2 == fBest) {
1132                             return 1;
1133                         } else {
1134                             return getFileName(rf1).compareTo(getFileName(rf2));
1135                         }
1136                     }
1137                 });
1138 
1139                 // Is this something found in a values/ folder?
1140                 boolean valueResource = ResourceHelper.isValueBasedResourceType(type);
1141 
1142                 for (ResourceFile file : matches) {
1143                     String folderName = file.getFolder().getFolder().getName();
1144                     String label = String.format("Open Declaration in %1$s/%2$s",
1145                             folderName, getFileName(file));
1146 
1147                     // Only search for resource type within the file if it's an
1148                     // XML file and it is a value resource
1149                     ResourceLink link = new ResourceLink(label, range, file,
1150                             valueResource ? type : null, name);
1151                     links.add(link);
1152                 }
1153             }
1154         }
1155 
1156         // Id's are handled specially because they are typically defined
1157         // inline (though they -can- be defined in the values folder above as
1158         // well, in which case we will prefer that definition)
1159         if (!isFramework && type == ResourceType.ID && links.size() == 0) {
1160             // Must compute these lazily...
1161             links.add(new ResourceLink("Open XML Declaration", range, null, type, name));
1162         }
1163 
1164         if (links.size() > 0) {
1165             return links.toArray(new IHyperlink[links.size()]);
1166         } else {
1167             return null;
1168         }
1169     }
1170 
getFileName(ResourceFile file)1171     private static String getFileName(ResourceFile file) {
1172         return file.getFile().getName();
1173     }
1174 
1175     /** Detector for finding Android references in XML files */
1176    public static class XmlResolver extends AbstractHyperlinkDetector {
1177 
1178         @Override
detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks)1179         public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region,
1180                 boolean canShowMultipleHyperlinks) {
1181 
1182             if (region == null || textViewer == null) {
1183                 return null;
1184             }
1185 
1186             IDocument document = textViewer.getDocument();
1187 
1188             XmlContext context = XmlContext.find(document, region.getOffset());
1189             if (context == null) {
1190                 return null;
1191             }
1192 
1193             IRegion range = context.getInnerRange(document);
1194             boolean isLinkable = false;
1195             String type = context.getInnerRegion().getType();
1196             if (type == DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE) {
1197                 if (isAttributeValueLink(context)) {
1198                     isLinkable = true;
1199                     // Strip out quotes
1200                     range = new Region(range.getOffset() + 1, range.getLength() - 2);
1201 
1202                     Attr attribute = context.getAttribute();
1203                     if (isStyleAttribute(context)) {
1204                         return getStyleLinks(context, range, attribute.getValue());
1205                     }
1206                     if (attribute != null
1207                             && attribute.getValue().startsWith(PREFIX_RESOURCE_REF)) {
1208                         // Instantly create links for resources since we can use the existing
1209                         // resolved maps for this and offer multiple choices for the user
1210 
1211                         String url = attribute.getValue();
1212                         return getResourceLinks(range, url);
1213                     }
1214                 }
1215             } else if (type == DOMRegionContext.XML_TAG_ATTRIBUTE_NAME) {
1216                 if (isAttributeNameLink(context)) {
1217                     isLinkable = true;
1218                 }
1219             } else if (type == DOMRegionContext.XML_TAG_NAME) {
1220                 if (isElementNameLink(context)) {
1221                     isLinkable = true;
1222                 }
1223             } else if (type == DOMRegionContext.XML_CONTENT) {
1224                 Node parentNode = context.getNode().getParentNode();
1225                 if (parentNode != null && parentNode.getNodeType() == Node.ELEMENT_NODE) {
1226                     // Try to complete resources defined inline as text, such as
1227                     // style definitions
1228                     ITextRegion outer = context.getElementRegion();
1229                     ITextRegion inner = context.getInnerRegion();
1230                     int innerOffset = outer.getStart() + inner.getStart();
1231                     int caretOffset = innerOffset + context.getInnerRegionCaretOffset();
1232                     try {
1233                         IRegion lineInfo = document.getLineInformationOfOffset(caretOffset);
1234                         int lineStart = lineInfo.getOffset();
1235                         int lineEnd = Math.min(lineStart + lineInfo.getLength(),
1236                                 innerOffset + inner.getLength());
1237 
1238                         // Compute the resource URL
1239                         int urlStart = -1;
1240                         int offset = caretOffset;
1241                         while (offset > lineStart) {
1242                             char c = document.getChar(offset);
1243                             if (c == '@') {
1244                                 urlStart = offset;
1245                                 break;
1246                             } else if (!isValidResourceUrlChar(c)) {
1247                                 break;
1248                             }
1249                             offset--;
1250                         }
1251 
1252                         if (urlStart != -1) {
1253                             offset = caretOffset;
1254                             while (offset < lineEnd) {
1255                                 if (!isValidResourceUrlChar(document.getChar(offset))) {
1256                                     break;
1257                                 }
1258                                 offset++;
1259                             }
1260 
1261                             int length = offset - urlStart;
1262                             String url = document.get(urlStart, length);
1263                             range = new Region(urlStart, length);
1264                             return getResourceLinks(range, url);
1265                         }
1266                     } catch (BadLocationException e) {
1267                         AdtPlugin.log(e, null);
1268                     }
1269                 }
1270             }
1271 
1272             if (isLinkable) {
1273                 IHyperlink hyperlink = new DeferredResolutionLink(context, range);
1274                 if (hyperlink != null) {
1275                     return new IHyperlink[] {
1276                         hyperlink
1277                     };
1278                 }
1279             }
1280 
1281             return null;
1282         }
1283     }
1284 
isValidResourceUrlChar(char c)1285     private static boolean isValidResourceUrlChar(char c) {
1286         return Character.isJavaIdentifierPart(c) || c == ':' || c == '/' || c == '.' || c == '+';
1287 
1288     }
1289 
1290     /** Detector for finding Android references in Java files */
1291     public static class JavaResolver extends AbstractHyperlinkDetector {
1292 
1293         @Override
detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks)1294         public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region,
1295                 boolean canShowMultipleHyperlinks) {
1296             // Most of this is identical to the builtin JavaElementHyperlinkDetector --
1297             // everything down to the Android R filtering below
1298 
1299             ITextEditor textEditor = (ITextEditor) getAdapter(ITextEditor.class);
1300             if (region == null || !(textEditor instanceof JavaEditor))
1301                 return null;
1302 
1303             IAction openAction = textEditor.getAction("OpenEditor"); //$NON-NLS-1$
1304             if (!(openAction instanceof SelectionDispatchAction))
1305                 return null;
1306 
1307             int offset = region.getOffset();
1308 
1309             IJavaElement input = EditorUtility.getEditorInputJavaElement(textEditor, false);
1310             if (input == null)
1311                 return null;
1312 
1313             try {
1314                 IDocument document = textEditor.getDocumentProvider().getDocument(
1315                         textEditor.getEditorInput());
1316                 IRegion wordRegion = JavaWordFinder.findWord(document, offset);
1317                 if (wordRegion == null || wordRegion.getLength() == 0)
1318                     return null;
1319 
1320                 IJavaElement[] elements = null;
1321                 elements = ((ICodeAssist) input).codeSelect(wordRegion.getOffset(), wordRegion
1322                         .getLength());
1323 
1324                 // Specific Android R class filtering:
1325                 if (elements.length > 0) {
1326                     IJavaElement element = elements[0];
1327                     if (element.getElementType() == IJavaElement.FIELD) {
1328                         IJavaElement unit = element.getAncestor(IJavaElement.COMPILATION_UNIT);
1329                         if (unit == null) {
1330                             // Probably in a binary; see if this is an android.R resource
1331                             IJavaElement type = element.getAncestor(IJavaElement.TYPE);
1332                             if (type != null && type.getParent() != null) {
1333                                 IJavaElement parentType = type.getParent();
1334                                 if (parentType.getElementType() == IJavaElement.CLASS_FILE) {
1335                                     String pn = parentType.getElementName();
1336                                     String prefix = FN_RESOURCE_BASE + "$"; //$NON-NLS-1$
1337                                     if (pn.startsWith(prefix)) {
1338                                         return createTypeLink(element, type, wordRegion, true);
1339                                     }
1340                                 }
1341                             }
1342                         } else if (FN_RESOURCE_CLASS.equals(unit.getElementName())) {
1343                             // Yes, we're referencing the project R class.
1344                             // Offer hyperlink navigation to XML resource files for
1345                             // the various definitions
1346                             IJavaElement type = element.getAncestor(IJavaElement.TYPE);
1347                             if (type != null) {
1348                                 return createTypeLink(element, type, wordRegion, false);
1349                             }
1350                         }
1351                     }
1352 
1353                 }
1354                 return null;
1355             } catch (JavaModelException e) {
1356                 return null;
1357             }
1358         }
1359 
createTypeLink(IJavaElement element, IJavaElement type, IRegion wordRegion, boolean isFrameworkResource)1360         private IHyperlink[] createTypeLink(IJavaElement element, IJavaElement type,
1361                 IRegion wordRegion, boolean isFrameworkResource) {
1362             String typeName = type.getElementName();
1363             // typeName will be "id", "layout", "string", etc
1364             if (isFrameworkResource) {
1365                 typeName = ANDROID_PKG + ':' + typeName;
1366             }
1367             String elementName = element.getElementName();
1368             String url = '@' + typeName + '/' + elementName;
1369             return getResourceLinks(wordRegion, url);
1370         }
1371     }
1372 
1373     /** Returns the editor applicable to this hyperlink detection */
getEditor()1374     private static IEditorPart getEditor() {
1375         // I would like to be able to find this via getAdapter(TextEditor.class) but
1376         // couldn't find a way to initialize the editor context from
1377         // AndroidSourceViewerConfig#getHyperlinkDetectorTargets (which only has
1378         // a TextViewer, not a TextEditor, instance).
1379         //
1380         // Therefore, for now, use a hack. This hack is reasonable because hyperlink
1381         // resolvers are only run for the front-most visible window in the active
1382         // workbench.
1383         return AdtUtils.getActiveEditor();
1384     }
1385 
1386     /** Returns the project applicable to this hyperlink detection */
getProject()1387     private static IProject getProject() {
1388         IFile file = AdtUtils.getActiveFile();
1389         if (file != null) {
1390             return file.getProject();
1391         }
1392 
1393         return null;
1394     }
1395 
1396     /**
1397      * Hyperlink implementation which delays computing the actual file and offset target
1398      * until it is asked to open the hyperlink
1399      */
1400     private static class DeferredResolutionLink implements IHyperlink {
1401         private XmlContext mXmlContext;
1402         private IRegion mRegion;
1403 
DeferredResolutionLink(XmlContext xmlContext, IRegion mRegion)1404         public DeferredResolutionLink(XmlContext xmlContext, IRegion mRegion) {
1405             super();
1406             this.mXmlContext = xmlContext;
1407             this.mRegion = mRegion;
1408         }
1409 
1410         @Override
getHyperlinkRegion()1411         public IRegion getHyperlinkRegion() {
1412             return mRegion;
1413         }
1414 
1415         @Override
getHyperlinkText()1416         public String getHyperlinkText() {
1417             return "Open XML Declaration";
1418         }
1419 
1420         @Override
getTypeLabel()1421         public String getTypeLabel() {
1422             return null;
1423         }
1424 
1425         @Override
open()1426         public void open() {
1427             // Lazily compute the location to open
1428             if (mXmlContext != null && !Hyperlinks.open(mXmlContext)) {
1429                 // Failed: display message to the user
1430                 displayError("Could not open link");
1431             }
1432         }
1433     }
1434 
1435     /**
1436      * Hyperlink implementation which provides a link for a resource; the actual file name
1437      * is known, but the value location within XML files is deferred until the link is
1438      * actually opened.
1439      */
1440     static class ResourceLink implements IHyperlink {
1441         private final String mLinkText;
1442         private final IRegion mLinkRegion;
1443         private final ResourceType mType;
1444         private final String mName;
1445         private final ResourceFile mFile;
1446 
1447         /**
1448          * Constructs a new {@link ResourceLink}.
1449          *
1450          * @param linkText the description of the link to be shown in a popup when there
1451          *            is more than one match
1452          * @param linkRegion the region corresponding to the link source highlight
1453          * @param file the target resource file containing the link definition
1454          * @param type the type of resource being linked to
1455          * @param name the name of the resource being linked to
1456          */
ResourceLink(String linkText, IRegion linkRegion, ResourceFile file, ResourceType type, String name)1457         public ResourceLink(String linkText, IRegion linkRegion, ResourceFile file,
1458                 ResourceType type, String name) {
1459             super();
1460             mLinkText = linkText;
1461             mLinkRegion = linkRegion;
1462             mType = type;
1463             mName = name;
1464             mFile = file;
1465         }
1466 
1467         @Override
getHyperlinkRegion()1468         public IRegion getHyperlinkRegion() {
1469             return mLinkRegion;
1470         }
1471 
1472         @Override
getHyperlinkText()1473         public String getHyperlinkText() {
1474             // return "Open XML Declaration";
1475             return mLinkText;
1476         }
1477 
1478         @Override
getTypeLabel()1479         public String getTypeLabel() {
1480             return null;
1481         }
1482 
1483         @Override
open()1484         public void open() {
1485             // We have to defer computation of ids until the link is clicked since we
1486             // don't have a fast map lookup for these
1487             if (mFile == null && mType == ResourceType.ID) {
1488                 // Id's are handled specially because they are typically defined
1489                 // inline (though they -can- be defined in the values folder above as well,
1490                 // in which case we will prefer that definition)
1491                 IProject project = getProject();
1492                 Pair<IFile,IRegion> def = findIdDefinition(project, mName);
1493                 if (def != null) {
1494                     try {
1495                         AdtPlugin.openFile(def.getFirst(), def.getSecond());
1496                     } catch (PartInitException e) {
1497                         AdtPlugin.log(e, null);
1498                     }
1499                     return;
1500                 }
1501 
1502                 displayError(String.format("Could not find id %1$s", mName));
1503                 return;
1504             }
1505 
1506             IAbstractFile wrappedFile = mFile != null ? mFile.getFile() : null;
1507             if (wrappedFile instanceof IFileWrapper) {
1508                 IFile file = ((IFileWrapper) wrappedFile).getIFile();
1509                 try {
1510                     // Lazily search for the target?
1511                     IRegion region = null;
1512                     String extension = file.getFileExtension();
1513                     if (mType != null && mName != null && EXT_XML.equals(extension)) {
1514                         Pair<IFile, IRegion> target;
1515                         if (mType == ResourceType.ID) {
1516                             target = findIdInXml(mName, file);
1517                         } else {
1518                             target = findValueInXml(mType, mName, file);
1519                         }
1520                         if (target != null) {
1521                             region = target.getSecond();
1522                         }
1523                     }
1524                     AdtPlugin.openFile(file, region);
1525                 } catch (PartInitException e) {
1526                     AdtPlugin.log(e, null);
1527                 }
1528             } else if (wrappedFile instanceof FileWrapper) {
1529                 File file = ((FileWrapper) wrappedFile);
1530                 IPath path = new Path(file.getAbsolutePath());
1531                 int offset = 0;
1532                 // Lazily search for the target?
1533                 if (mType != null && mName != null && EXT_XML.equals(path.getFileExtension())) {
1534                     if (file.exists()) {
1535                         Pair<File, Integer> target = findValueInXml(mType, mName, file);
1536                         if (target != null && target.getSecond() != null) {
1537                             offset = target.getSecond();
1538                         }
1539                     }
1540                 }
1541                 openPath(path, null, offset);
1542             } else {
1543                 throw new IllegalArgumentException("Invalid link parameters");
1544             }
1545         }
1546 
getFile()1547         ResourceFile getFile() {
1548             return mFile;
1549         }
1550     }
1551 
1552     /**
1553      * XML context containing node, potentially attribute, and text regions surrounding a
1554      * particular caret offset
1555      */
1556     private static class XmlContext {
1557         private final Node mNode;
1558         private final Element mElement;
1559         private final Attr mAttribute;
1560         private final IStructuredDocumentRegion mOuterRegion;
1561         private final ITextRegion mInnerRegion;
1562         private final int mInnerRegionOffset;
1563 
XmlContext(Node node, Element element, Attr attribute, IStructuredDocumentRegion outerRegion, ITextRegion innerRegion, int innerRegionOffset)1564         public XmlContext(Node node, Element element, Attr attribute,
1565                 IStructuredDocumentRegion outerRegion,
1566                 ITextRegion innerRegion, int innerRegionOffset) {
1567             super();
1568             mNode = node;
1569             mElement = element;
1570             mAttribute = attribute;
1571             mOuterRegion = outerRegion;
1572             mInnerRegion = innerRegion;
1573             mInnerRegionOffset = innerRegionOffset;
1574         }
1575 
1576         /**
1577          * Gets the current node, never null
1578          *
1579          * @return the surrounding node
1580          */
getNode()1581         public Node getNode() {
1582             return mNode;
1583         }
1584 
1585 
1586         /**
1587          * Gets the current node, may be null
1588          *
1589          * @return the surrounding node
1590          */
getElement()1591         public Element getElement() {
1592             return mElement;
1593         }
1594 
1595         /**
1596          * Returns the current attribute, or null if we are not over an attribute
1597          *
1598          * @return the attribute, or null
1599          */
getAttribute()1600         public Attr getAttribute() {
1601             return mAttribute;
1602         }
1603 
1604         /**
1605          * Gets the region of the element
1606          *
1607          * @return the region of the surrounding element, never null
1608          */
getElementRegion()1609         public ITextRegion getElementRegion() {
1610             return mOuterRegion;
1611         }
1612 
1613         /**
1614          * Gets the inner region, which can be the tag name, an attribute name, an
1615          * attribute value, or some other portion of an XML element
1616          * @return the inner region, never null
1617          */
getInnerRegion()1618         public ITextRegion getInnerRegion() {
1619             return mInnerRegion;
1620         }
1621 
1622         /**
1623          * Gets the caret offset relative to the inner region
1624          *
1625          * @return the offset relative to the inner region
1626          */
getInnerRegionCaretOffset()1627         public int getInnerRegionCaretOffset() {
1628             return mInnerRegionOffset;
1629         }
1630 
1631         /**
1632          * Returns a range with suffix whitespace stripped out
1633          *
1634          * @param document the document containing the regions
1635          * @return the range of the inner region, minus any whitespace at the end
1636          */
getInnerRange(IDocument document)1637         public IRegion getInnerRange(IDocument document) {
1638             int start = mOuterRegion.getStart() + mInnerRegion.getStart();
1639             int length = mInnerRegion.getLength();
1640             try {
1641                 String s = document.get(start, length);
1642                 for (int i = s.length() - 1; i >= 0; i--) {
1643                     if (Character.isWhitespace(s.charAt(i))) {
1644                         length--;
1645                     }
1646                 }
1647             } catch (BadLocationException e) {
1648                 AdtPlugin.log(e, ""); //$NON-NLS-1$
1649             }
1650             return new Region(start, length);
1651         }
1652 
1653         /**
1654          * Returns the node the cursor is currently on in the document. null if no node is
1655          * selected
1656          */
find(IDocument document, int offset)1657         private static XmlContext find(IDocument document, int offset) {
1658             // Loosely based on getCurrentNode and getCurrentAttr in the WST's
1659             // XMLHyperlinkDetector.
1660             IndexedRegion inode = null;
1661             IStructuredModel model = null;
1662             try {
1663                 model = StructuredModelManager.getModelManager().getExistingModelForRead(document);
1664                 if (model != null) {
1665                     inode = model.getIndexedRegion(offset);
1666                     if (inode == null) {
1667                         inode = model.getIndexedRegion(offset - 1);
1668                     }
1669 
1670                     if (inode instanceof Element) {
1671                         Element element = (Element) inode;
1672                         Attr attribute = null;
1673                         if (element.hasAttributes()) {
1674                             NamedNodeMap attrs = element.getAttributes();
1675                             // go through each attribute in node and if attribute contains
1676                             // offset, return that attribute
1677                             for (int i = 0; i < attrs.getLength(); ++i) {
1678                                 // assumption that if parent node is of type IndexedRegion,
1679                                 // then its attributes will also be of type IndexedRegion
1680                                 IndexedRegion attRegion = (IndexedRegion) attrs.item(i);
1681                                 if (attRegion.contains(offset)) {
1682                                     attribute = (Attr) attrs.item(i);
1683                                     break;
1684                                 }
1685                             }
1686                         }
1687 
1688                         IStructuredDocument doc = model.getStructuredDocument();
1689                         IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset);
1690                         if (region != null
1691                                 && DOMRegionContext.XML_TAG_NAME.equals(region.getType())) {
1692                             ITextRegion subRegion = region.getRegionAtCharacterOffset(offset);
1693                             if (subRegion == null) {
1694                                 return null;
1695                             }
1696                             int regionStart = region.getStartOffset();
1697                             int subregionStart = subRegion.getStart();
1698                             int relativeOffset = offset - (regionStart + subregionStart);
1699                             return new XmlContext(element, element, attribute, region, subRegion,
1700                                     relativeOffset);
1701                         }
1702                     } else if (inode instanceof Node) {
1703                         IStructuredDocument doc = model.getStructuredDocument();
1704                         IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset);
1705                         if (region != null
1706                                 && DOMRegionContext.XML_CONTENT.equals(region.getType())) {
1707                             ITextRegion subRegion = region.getRegionAtCharacterOffset(offset);
1708                             int regionStart = region.getStartOffset();
1709                             int subregionStart = subRegion.getStart();
1710                             int relativeOffset = offset - (regionStart + subregionStart);
1711                             return new XmlContext((Node) inode, null, null, region, subRegion,
1712                                     relativeOffset);
1713                         }
1714 
1715                     }
1716                 }
1717             } finally {
1718                 if (model != null) {
1719                     model.releaseFromRead();
1720                 }
1721             }
1722 
1723             return null;
1724         }
1725     }
1726 
1727     /**
1728      * DOM parser which records offsets in the element nodes such that it can return
1729      * offsets for elements later
1730      */
1731     private static final class OffsetTrackingParser extends DOMParser {
1732 
1733         private static final String KEY_OFFSET = "offset"; //$NON-NLS-1$
1734 
1735         private static final String KEY_NODE =
1736             "http://apache.org/xml/properties/dom/current-element-node"; //$NON-NLS-1$
1737 
1738         private XMLLocator mLocator;
1739 
OffsetTrackingParser()1740         public OffsetTrackingParser() throws SAXException {
1741             this.setFeature("http://apache.org/xml/features/dom/defer-node-expansion",//$NON-NLS-1$
1742                     false);
1743         }
1744 
getOffset(Node node)1745         public int getOffset(Node node) {
1746             Integer offset = (Integer) node.getUserData(KEY_OFFSET);
1747             if (offset != null) {
1748                 return offset;
1749             }
1750 
1751             return -1;
1752         }
1753 
1754         @Override
startElement(QName elementQName, XMLAttributes attrList, Augmentations augs)1755         public void startElement(QName elementQName, XMLAttributes attrList, Augmentations augs)
1756                 throws XNIException {
1757             int offset = mLocator.getCharacterOffset();
1758             super.startElement(elementQName, attrList, augs);
1759 
1760             try {
1761                 Node node = (Node) this.getProperty(KEY_NODE);
1762                 if (node != null) {
1763                     node.setUserData(KEY_OFFSET, offset, null);
1764                 }
1765             } catch (org.xml.sax.SAXException ex) {
1766                 AdtPlugin.log(ex, ""); //$NON-NLS-1$
1767             }
1768         }
1769 
1770         @Override
startDocument(XMLLocator locator, String encoding, NamespaceContext namespaceContext, Augmentations augs)1771         public void startDocument(XMLLocator locator, String encoding,
1772                 NamespaceContext namespaceContext, Augmentations augs) throws XNIException {
1773             super.startDocument(locator, encoding, namespaceContext, augs);
1774             mLocator = locator;
1775         }
1776     }
1777 }
1778