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 package com.android.ide.eclipse.adt.internal.editors;
17 
18 import static com.android.SdkConstants.FD_SOURCES;
19 
20 import com.android.ide.common.resources.ResourceFile;
21 import com.android.ide.eclipse.adt.AdtUtils;
22 import com.android.ide.eclipse.adt.internal.editors.Hyperlinks.ResourceLink;
23 import com.android.ide.eclipse.adt.internal.editors.Hyperlinks.XmlResolver;
24 import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.AdtProjectTest;
25 
26 import org.eclipse.core.resources.IFile;
27 import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;
28 import org.eclipse.jface.text.IDocument;
29 import org.eclipse.jface.text.IRegion;
30 import org.eclipse.jface.text.Region;
31 import org.eclipse.jface.text.hyperlink.IHyperlink;
32 import org.eclipse.jface.text.source.ISourceViewer;
33 import org.eclipse.swt.graphics.Point;
34 import org.eclipse.ui.IEditorPart;
35 import org.eclipse.ui.IWorkbenchPage;
36 import org.eclipse.ui.PlatformUI;
37 import org.eclipse.ui.ide.IDE;
38 import org.eclipse.ui.internal.ErrorEditorPart;
39 import org.eclipse.ui.internal.browser.WebBrowserEditor;
40 import org.eclipse.wst.sse.ui.StructuredTextEditor;
41 import org.eclipse.wst.sse.ui.internal.StructuredTextViewer;
42 import org.eclipse.wst.xml.ui.internal.tabletree.XMLMultiPageEditorPart;
43 
44 import java.lang.reflect.Field;
45 import java.lang.reflect.Method;
46 
47 @SuppressWarnings({
48         "restriction", "javadoc"
49 })
50 public class HyperlinksTest extends AdtProjectTest {
51     @Override
testCaseNeedsUniqueProject()52     protected boolean testCaseNeedsUniqueProject() {
53         return true;
54     }
55 
testFqnRegexp()56     public void testFqnRegexp() throws Exception {
57         assertTrue(Hyperlinks.isViewClassName("com.android.Foo"));
58         assertTrue(Hyperlinks.isViewClassName("com.android.pk_g.Foo_Bar1"));
59         assertTrue(Hyperlinks.isViewClassName("com.android.Foo$Inner"));
60 
61         // Should we allow non-standard packages and class names?
62         // For now, we're allowing it -- see how this works out in practice.
63         //assertFalse(XmlHyperlinkResolver.isViewClassName("Foo.bar"));
64         assertTrue(Hyperlinks.isViewClassName("Foo.bar"));
65 
66         assertFalse(Hyperlinks.isViewClassName("LinearLayout"));
67         assertFalse(Hyperlinks.isViewClassName("."));
68         assertFalse(Hyperlinks.isViewClassName(".F"));
69         assertFalse(Hyperlinks.isViewClassName("f."));
70         assertFalse(Hyperlinks.isViewClassName("Foo"));
71         assertFalse(Hyperlinks.isViewClassName("com.android.1Foo"));
72         assertFalse(Hyperlinks.isViewClassName("1com.Foo"));
73     }
74 
testNavigate1()75     public void testNavigate1() throws Exception {
76         // Check navigating to a local resource
77         checkXmlNavigation("navigation1.xml", "res/layout/navigation1.xml",
78                 "android:text=\"@string/app^_name\"");
79     }
80 
testNavigate2()81     public void testNavigate2() throws Exception {
82         // Check navigating to a framework resource
83         checkXmlNavigation("navigation1.xml", "res/layout/navigation1.xml",
84                 "marginLeft=\"@android:dimen/app_ico^n_size\"");
85     }
86 
testNavigate3()87     public void testNavigate3() throws Exception {
88         // Check navigating to a style
89         checkXmlNavigation("navigation1.xml", "res/layout/navigation1.xml",
90                 "style=\"@android:style/Widget.B^utton\"");
91     }
92 
testNavigate4()93     public void testNavigate4() throws Exception {
94         // Check navigating to resource with many resolutions
95         checkXmlNavigation("navigation1.xml", "res/layout/navigation1.xml",
96                 "android:text=\"@android:st^ring/ok\"");
97     }
98 
testNavigate5()99     public void testNavigate5() throws Exception {
100         // Check navigating to styles
101         checkXmlNavigation("navigationstyles.xml", "res/values/navigationstyles.xml",
102                 "parent=\"android:Theme.Li^ght\">");
103     }
104 
testNavigate6()105     public void testNavigate6() throws Exception {
106         // Check navigating to a portion of a style (this should pick android:Theme, not
107         // android:Theme.Light
108         checkXmlNavigation("navigationstyles.xml", "res/values/navigationstyles.xml",
109                 "parent=\"android:The^me.Light\">");
110     }
111 
testNavigate7()112     public void testNavigate7() throws Exception {
113         // Check navigating to a resource inside text content
114         checkXmlNavigation("navigationstyles.xml", "res/values/navigationstyles.xml",
115                 "popupBackground\">@android:drawable/spinner_dr^opdown_background</item>");
116     }
117 
testNavigate8()118     public void testNavigate8() throws Exception {
119         // Check navigating to a resource inside text content where there is space around
120         // the URL
121         checkXmlNavigation("navigationstyles.xml", "res/values/navigationstyles.xml",
122                 "colorBackground\"> @color/cust^om_theme_color </item>");
123     }
124 
testNavigate9a()125     public void testNavigate9a() throws Exception {
126         // Check navigating to a an activity
127         checkXmlNavigation("manifest.xml", "AndroidManifest.xml",
128                 "<activity android:name=\".Test^Activity\"");
129     }
130 
131     /* Not yet implemented
132     public void testNavigate9b() throws Exception {
133         // Check navigating to a an activity - clicking on the activity element should
134         // work too
135         checkXmlNavigation("manifest.xml", "AndroidManifest.xml",
136                 "<acti^vity android:name=\".TestActivity\"");
137     }
138     */
139 
testNavigate10()140     public void testNavigate10() throws Exception {
141         // Check navigating to a permission
142         checkXmlNavigation("manifest.xml", "AndroidManifest.xml",
143                 "<uses-permission android:name=\"android.permission.AC^CESS_NETWORK_STATE\" />");
144     }
145 
testNavigate11a()146     public void testNavigate11a() throws Exception {
147         // Check navigating to an intent
148         checkXmlNavigation("manifest.xml", "AndroidManifest.xml",
149                 "<action android:name=\"android.intent.ac^tion.MAIN\" />");
150     }
151 
testNavigate11g()152     public void testNavigate11g() throws Exception {
153         // Check navigating to an intent
154         checkXmlNavigation("manifest.xml", "AndroidManifest.xml",
155                 "<category android:name=\"android.intent.category.LA^UNCHER\" />");
156     }
157 
testNavigate12()158     public void testNavigate12() throws Exception {
159         // Check navigating to a custom view class
160         checkXmlNavigation("navigation1.xml", "res/layout/navigation1.xml",
161                 "<my.Cust^omView></my.CustomView>");
162     }
163 
testNavigate13()164     public void testNavigate13() throws Exception {
165         // Check jumping to classes pointed to by fragments
166 
167         getTestDataFile(getProject(), "TestFragment.java.txt",
168                 FD_SOURCES + "/" + TEST_PROJECT_PACKAGE.replace('.', '/') + "/TestFragment.java");
169         checkXmlNavigation("fragmentlayout.xml", "res/layout/fragmentlayout.xml",
170                 "android:name=\"com.android.ecl^ipse.tests.TestFragment\"");
171     }
172 
testNavigate14()173     public void testNavigate14() throws Exception {
174         // Check jumping to classes pointed to by fragments
175 
176         getTestDataFile(getProject(), "TestFragment.java.txt",
177                 FD_SOURCES + "/" + TEST_PROJECT_PACKAGE.replace('.', '/') + "/TestFragment.java");
178         checkXmlNavigation("fragmentlayout.xml", "res/layout/fragmentlayout.xml",
179                 "class=\"com.and^roid.eclipse.tests.TestFragment\"");
180     }
181 
testNavigate15()182     public void testNavigate15() throws Exception {
183         // Check navigating to a theme resource
184         checkXmlNavigation("navigation1.xml", "res/layout/navigation1.xml",
185                 "?android:attr/alert^DialogStyle");
186     }
187 
testNavigate16()188     public void testNavigate16() throws Exception {
189         // Check navigating to a theme resource
190         checkXmlNavigation("navigation1.xml", "res/layout/navigation1.xml",
191                 "?android:alert^DialogStyle");
192     }
193 
194     // Left to test:
195     // onClick handling
196     // class attributes
197     // Test that the correct file is actually opened!
198 
checkXmlNavigation(String basename, String targetPath, String caretLocation)199     private void checkXmlNavigation(String basename, String targetPath,
200             String caretLocation) throws Exception {
201         IFile file = getTestDataFile(getProject(), basename, targetPath, true);
202 
203         // Determine the offset
204         int offset = getCaretOffset(file, caretLocation);
205 
206         // Open file
207         IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
208         assertNotNull(page);
209         IEditorPart editor = IDE.openEditor(page, file);
210         assertTrue(editor.getClass().getName(), editor instanceof AndroidXmlEditor);
211         AndroidXmlEditor layoutEditor = (AndroidXmlEditor) editor;
212         ISourceViewer viewer = layoutEditor.getStructuredSourceViewer();
213 
214         XmlResolver resolver = new Hyperlinks.XmlResolver();
215         IHyperlink[] links = resolver.detectHyperlinks(viewer, new Region(offset, 0), true);
216         assertNotNull(links);
217 
218         StringBuilder sb = new StringBuilder(1000);
219         sb.append("Go To Declaration in " + basename + " for " + caretLocation + ":\n");
220         for (IHyperlink link : links) {
221             sb.append(link.getHyperlinkText());
222             sb.append(" : ");
223             sb.append(" [");
224             IRegion region = link.getHyperlinkRegion();
225             sb.append(viewer.getDocument().get(region.getOffset(), region.getLength()));
226             sb.append("]");
227             if (link instanceof Hyperlinks.ResourceLink) {
228                 Hyperlinks.ResourceLink resourceLink = (ResourceLink) link;
229                 sb.append("\n    ");
230                 ResourceFile resourceFile = resourceLink.getFile();
231                 String desc = resourceFile.toString();
232                 desc = desc.replace('\\', '/');
233                 // For files in the SDK folder, strip out file prefix
234                 int dataRes = desc.indexOf("data/res");
235                 if (dataRes != -1) {
236                     desc = desc.substring(dataRes);
237                 }
238                 desc = removeSessionData(desc);
239                 sb.append(desc);
240             }
241             sb.append('\n');
242         }
243 
244         // Open the first link
245         IHyperlink link = links[0];
246         link.open();
247         IEditorPart newEditor = AdtUtils.getActiveEditor();
248         // Ensure that this isn't an invalid file (e.g. opening the SDK platform files
249         // with incorrect content binding could cause this)
250         assertTrue(!(newEditor instanceof ErrorEditorPart));
251 
252         IDocument document = null;
253         Point selection = null;
254 
255         if (newEditor instanceof AndroidXmlEditor) {
256             AndroidXmlEditor xmlEditor = (AndroidXmlEditor) newEditor;
257             document = xmlEditor.getStructuredSourceViewer().getDocument();
258             selection = xmlEditor.getStructuredSourceViewer().getSelectedRange();
259         } else if (newEditor instanceof XMLMultiPageEditorPart) {
260             XMLMultiPageEditorPart xmlEditor = (XMLMultiPageEditorPart) newEditor;
261             Field field = xmlEditor.getClass().getDeclaredField("fTextEditor");
262             field.setAccessible(true);
263             StructuredTextEditor ste = (StructuredTextEditor) field.get(newEditor);
264             if (ste == null) {
265                 Method method = xmlEditor.getClass().getMethod("getTextEditor", new Class[0]);
266                 ste = (StructuredTextEditor) method.invoke(newEditor, new Object[0]);
267             }
268             StructuredTextViewer textViewer = ste.getTextViewer();
269             document = textViewer.getDocument();
270             selection = textViewer.getSelectedRange();
271         } else if (newEditor instanceof WebBrowserEditor) {
272             WebBrowserEditor browser = (WebBrowserEditor) newEditor;
273             Field field = browser.getClass().getDeclaredField("initialURL");
274             field.setAccessible(true);
275             String initialUrl = (String) field.get(newEditor);
276             int index = initialUrl.indexOf("reference");
277             if (index != -1) {
278                 initialUrl = initialUrl.substring(index);
279             }
280             initialUrl = initialUrl.replace('\\', '/');
281             sb.append("\n\nAfter open, a browser is shown with this URL:\n");
282             sb.append("  ");
283             sb.append(initialUrl);
284             sb.append("\n");
285         } else if (newEditor instanceof JavaEditor) {
286             JavaEditor javaEditor = (JavaEditor) newEditor;
287             document = javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
288             IRegion range = javaEditor.getHighlightRange();
289             selection = new Point(range.getOffset(), range.getLength());
290         } else {
291             fail("Unhandled editor type: " + newEditor.getClass().getName());
292             return;
293         }
294 
295         if (document != null && selection != null) {
296             int lineStart = document.getLineInformationOfOffset(selection.x).getOffset();
297             IRegion lineEndInfo = document.getLineInformationOfOffset(selection.x + selection.y);
298             int lineEnd = lineEndInfo.getOffset() + lineEndInfo.getLength();
299             String text = document.get(lineStart, lineEnd - lineStart);
300             int selectionStart = selection.x - lineStart;
301             int selectionEnd = selectionStart + selection.y;
302             if (selectionEnd > selectionStart) {
303                 // Selection range
304                 text = text.substring(0, selectionStart) + "[^" +
305                    text.substring(selectionStart, selectionEnd) + "]" +
306                    text.substring(selectionEnd);
307             } else {
308                 text = text.substring(0, selectionStart) + "^" +
309                     text.substring(selectionStart);
310             }
311             text = removeSessionData(text);
312 
313             sb.append("\n\nAfter open, the selected text is:\n");
314             sb.append(text);
315             sb.append("\n");
316         }
317 
318         assertEqualsGolden(basename, sb.toString(), "txt");
319     }
320 }
321