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