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