Home | History | Annotate | Download | only in xml
      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 
    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 */
    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 */
    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 */
    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      */
    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      */
    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      */
    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 */
    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      */
    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 */
    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 */
    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 */
    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      */
    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      */
    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 */
    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 */
    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      */
    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      */
    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      */
    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      */
    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) */
    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 
    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      */
    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      */
    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 */
    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) */
    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      */
    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      */
    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 */
    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 */
    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 */
    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 */
    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 
    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 */
    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 */
   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 
   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      */
   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 
   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 
   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 
   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 
   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 
   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 */
   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 */
   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 
   1394         public DeferredResolutionLink(XmlContext xmlContext, IRegion mRegion) {
   1395             super();
   1396             this.mXmlContext = xmlContext;
   1397             this.mRegion = mRegion;
   1398         }
   1399 
   1400         public IRegion getHyperlinkRegion() {
   1401             return mRegion;
   1402         }
   1403 
   1404         public String getHyperlinkText() {
   1405             return "Open XML Declaration";
   1406         }
   1407 
   1408         public String getTypeLabel() {
   1409             return null;
   1410         }
   1411 
   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          */
   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 
   1453         public IRegion getHyperlinkRegion() {
   1454             return mLinkRegion;
   1455         }
   1456 
   1457         public String getHyperlinkText() {
   1458             // return "Open XML Declaration";
   1459             return mLinkText;
   1460         }
   1461 
   1462         public String getTypeLabel() {
   1463             return null;
   1464         }
   1465 
   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 
   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 
   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          */
   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          */
   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          */
   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")
   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          */
   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          */
   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          */
   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          */
   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 
   1723         public OffsetTrackingParser() throws SAXException {
   1724             this.setFeature("http://apache.org/xml/features/dom/defer-node-expansion",//$NON-NLS-1$
   1725                     false);
   1726         }
   1727 
   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
   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
   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