Line 0
Link Here
|
|
|
1 |
/* |
2 |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. |
3 |
* |
4 |
* Copyright 2013 Oracle and/or its affiliates. All rights reserved. |
5 |
* |
6 |
* Oracle and Java are registered trademarks of Oracle and/or its affiliates. |
7 |
* Other names may be trademarks of their respective owners. |
8 |
* |
9 |
* The contents of this file are subject to the terms of either the GNU |
10 |
* General Public License Version 2 only ("GPL") or the Common |
11 |
* Development and Distribution License("CDDL") (collectively, the |
12 |
* "License"). You may not use this file except in compliance with the |
13 |
* License. You can obtain a copy of the License at |
14 |
* http://www.netbeans.org/cddl-gplv2.html |
15 |
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the |
16 |
* specific language governing permissions and limitations under the |
17 |
* License. When distributing the software, include this License Header |
18 |
* Notice in each file and include the License file at |
19 |
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this |
20 |
* particular file as subject to the "Classpath" exception as provided |
21 |
* by Oracle in the GPL Version 2 section of the License file that |
22 |
* accompanied this code. If applicable, add the following below the |
23 |
* License Header, with the fields enclosed by brackets [] replaced by |
24 |
* your own identifying information: |
25 |
* "Portions Copyrighted [year] [name of copyright owner]" |
26 |
* |
27 |
* If you wish your version of this file to be governed by only the CDDL |
28 |
* or only the GPL Version 2, indicate your decision by adding |
29 |
* "[Contributor] elects to include this software in this distribution |
30 |
* under the [CDDL or GPL Version 2] license." If you do not indicate a |
31 |
* single choice of license, a recipient has the option to distribute |
32 |
* your version of this file under either the CDDL, the GPL Version 2 or |
33 |
* to extend the choice of license to its licensees as provided above. |
34 |
* However, if you add GPL Version 2 code and therefore, elected the GPL |
35 |
* Version 2 license, then the option applies only if the new code is |
36 |
* made subject to such option by the copyright holder. |
37 |
* |
38 |
* Contributor(s): |
39 |
* |
40 |
* Portions Copyrighted 2013 Sun Microsystems, Inc. |
41 |
* Portions Copyrighted 2013 markiewb |
42 |
*/ |
43 |
package org.netbeans.modules.java.editor.hyperlink; |
44 |
|
45 |
import java.text.MessageFormat; |
46 |
import java.util.ArrayList; |
47 |
import java.util.Arrays; |
48 |
import java.util.Collection; |
49 |
import java.util.Collections; |
50 |
import java.util.EnumSet; |
51 |
import java.util.HashSet; |
52 |
import java.util.List; |
53 |
import java.util.Set; |
54 |
import javax.swing.JComboBox; |
55 |
import javax.swing.text.BadLocationException; |
56 |
import javax.swing.text.Document; |
57 |
import javax.swing.text.JTextComponent; |
58 |
import org.netbeans.api.editor.mimelookup.MimeRegistration; |
59 |
import org.netbeans.api.java.lexer.JavaTokenId; |
60 |
import org.netbeans.api.java.project.JavaProjectConstants; |
61 |
import org.netbeans.api.lexer.Token; |
62 |
import org.netbeans.api.lexer.TokenHierarchy; |
63 |
import org.netbeans.api.lexer.TokenSequence; |
64 |
import org.netbeans.api.project.FileOwnerQuery; |
65 |
import org.netbeans.api.project.Project; |
66 |
import org.netbeans.api.project.ProjectUtils; |
67 |
import org.netbeans.api.project.SourceGroup; |
68 |
import org.netbeans.api.project.Sources; |
69 |
import org.netbeans.editor.BaseDocument; |
70 |
import org.netbeans.editor.Utilities; |
71 |
import org.netbeans.lib.editor.hyperlink.spi.HyperlinkProviderExt; |
72 |
import org.netbeans.lib.editor.hyperlink.spi.HyperlinkType; |
73 |
import org.netbeans.modules.editor.NbEditorUtilities; |
74 |
import org.openide.DialogDescriptor; |
75 |
import org.openide.DialogDisplayer; |
76 |
import org.openide.NotifyDescriptor; |
77 |
import org.openide.cookies.EditCookie; |
78 |
import org.openide.cookies.OpenCookie; |
79 |
import org.openide.filesystems.FileObject; |
80 |
import org.openide.loaders.DataObject; |
81 |
import org.openide.loaders.DataObjectNotFoundException; |
82 |
import org.openide.util.Exceptions; |
83 |
|
84 |
/** |
85 |
* Hyperlink provider opening resources which are encoded in string literals |
86 |
* within java files. |
87 |
* <p> |
88 |
* For example: {@code "com/foo/Bar.java"} will be resolved in the source-roots |
89 |
* of |
90 |
* {@link JavaProjectConstants.SOURCES_TYPE_JAVA}, {@link JavaProjectConstants.SOURCES_TYPE_RESOURCES}, {@link JavaProjectConstants.SOURCES_HINT_TEST} |
91 |
* </p> |
92 |
* If there are multiple matches a dialog will pop up and let the user choose. |
93 |
* |
94 |
* @author markiewb |
95 |
*/ |
96 |
@MimeRegistration(mimeType = "text/x-java", service = HyperlinkProviderExt.class) |
97 |
public class ResourceHyperlinkProvider implements HyperlinkProviderExt { |
98 |
|
99 |
public static void openInEditor(FileObject fileToOpen) { |
100 |
DataObject fileDO; |
101 |
try { |
102 |
fileDO = DataObject.find(fileToOpen); |
103 |
if (fileDO != null) { |
104 |
EditCookie editCookie = fileDO.getLookup().lookup(EditCookie.class); |
105 |
if (editCookie != null) { |
106 |
editCookie.edit(); |
107 |
} else { |
108 |
OpenCookie openCookie = fileDO.getLookup().lookup(OpenCookie.class); |
109 |
if (openCookie != null) { |
110 |
openCookie.open(); |
111 |
} |
112 |
} |
113 |
} |
114 |
} catch (DataObjectNotFoundException e) { |
115 |
Exceptions.printStackTrace(e); |
116 |
} |
117 |
|
118 |
} |
119 |
|
120 |
@Override |
121 |
public int[] getHyperlinkSpan(Document doc, int offset, HyperlinkType type) { |
122 |
ResultTO matches = findResources(doc, offset); |
123 |
if (matches.isValid()) { |
124 |
return new int[]{matches.startOffsetInLiteral, matches.endOffsetInLiteral}; |
125 |
} else { |
126 |
return new int[]{-1, -1}; |
127 |
} |
128 |
} |
129 |
|
130 |
@Override |
131 |
public Set<HyperlinkType> getSupportedHyperlinkTypes() { |
132 |
return EnumSet.of(HyperlinkType.GO_TO_DECLARATION); |
133 |
} |
134 |
|
135 |
@Override |
136 |
public String getTooltipText(Document doc, int offset, HyperlinkType type) { |
137 |
ResultTO result = findResources(doc, offset); |
138 |
if (!result.isValid()) { |
139 |
return null; |
140 |
} |
141 |
|
142 |
Collection<FileObject> findMatches = result.foundFiles; |
143 |
if (findMatches.size() < 0) { |
144 |
return null; |
145 |
} |
146 |
return MessageFormat.format("<html>Open <b>{0}</b>{1,choice,0#|1#|1< ({1} matches)}", result.linkTarget, findMatches.size()); |
147 |
} |
148 |
|
149 |
@Override |
150 |
public boolean isHyperlinkPoint(Document document, int offset, HyperlinkType type) { |
151 |
ResultTO matches = findResources(document, offset); |
152 |
return matches.isValid(); |
153 |
} |
154 |
|
155 |
@Override |
156 |
public void performClickAction(Document doc, int position, HyperlinkType type) { |
157 |
ResultTO matches = findResources(doc, position); |
158 |
if (matches.isValid()) { |
159 |
Collection<FileObject> foundMatches = matches.foundFiles; |
160 |
final Project project = FileOwnerQuery.getOwner(NbEditorUtilities.getFileObject(doc)); |
161 |
FileObject fileToOpen = getSingleMatchOrAskForUserChoice(foundMatches, project); |
162 |
|
163 |
if (fileToOpen == null) { |
164 |
// StatusDisplayer.getDefault().setStatusText("Invalid path: " + findMatches.linkTarget); |
165 |
return; |
166 |
} |
167 |
openInEditor(fileToOpen); |
168 |
} |
169 |
} |
170 |
|
171 |
private Set<FileObject> findFiles(Document doc, String path) { |
172 |
//TODO cache the results for the same path, because checking for existence is an IO-operation (markiewb) |
173 |
|
174 |
//fallback to search in all source roots |
175 |
FileObject docFO = NbEditorUtilities.getFileObject(doc); |
176 |
Set<FileObject> matches = new HashSet<>(); |
177 |
|
178 |
matches.addAll(getMatchingFilesFromSourceRoots(FileOwnerQuery.getOwner(docFO), path)); |
179 |
|
180 |
FileObject fileInCurrentDirectory = getMatchingFileInCurrentDirectory(doc, path); |
181 |
if (null != fileInCurrentDirectory) { |
182 |
matches.add(fileInCurrentDirectory); |
183 |
} |
184 |
return matches; |
185 |
} |
186 |
|
187 |
private ResultTO findResources(Document document, int offset) { |
188 |
if (!(document instanceof BaseDocument)) { |
189 |
return ResultTO.createEmpty(); |
190 |
} |
191 |
|
192 |
BaseDocument doc = (BaseDocument) document; |
193 |
JTextComponent target = Utilities.getFocusedComponent(); |
194 |
|
195 |
if ((target == null) || (target.getDocument() != doc)) { |
196 |
return ResultTO.createEmpty(); |
197 |
} |
198 |
|
199 |
try { |
200 |
TokenHierarchy<String> hi = TokenHierarchy.create(doc.getText(0, doc.getLength()), JavaTokenId.language()); |
201 |
TokenSequence<JavaTokenId> ts = hi.tokenSequence(JavaTokenId.language()); |
202 |
|
203 |
ts.move(offset); |
204 |
boolean lastTokenInDocument = !ts.moveNext(); |
205 |
if (lastTokenInDocument) { |
206 |
// end of the document |
207 |
return ResultTO.createEmpty(); |
208 |
} |
209 |
while (ts.token() == null || ts.token().id() == JavaTokenId.WHITESPACE) { |
210 |
ts.movePrevious(); |
211 |
} |
212 |
|
213 |
Token<JavaTokenId> resourceToken = ts.offsetToken(); |
214 |
if (null == resourceToken |
215 |
|| resourceToken.length() <= 2) { |
216 |
return ResultTO.createEmpty(); |
217 |
} |
218 |
|
219 |
if (resourceToken.id() == JavaTokenId.STRING_LITERAL // identified must be string |
220 |
&& resourceToken.length() > 2) { // identifier must be longer than "" string |
221 |
int startOffset = resourceToken.offset(hi) + 1; |
222 |
|
223 |
final String wholeText = resourceToken.text().subSequence(1, resourceToken.length() - 1).toString(); |
224 |
|
225 |
int endOffset = startOffset + wholeText.length(); |
226 |
String linkTarget = wholeText; |
227 |
|
228 |
// StatusDisplayer.getDefault().setStatusText("Path :" + startOffset + "/" + endOffset + "/" + offset + "//" + (offset - startOffset) + "=" + innerSelectedText); |
229 |
Set<FileObject> findFiles = findFiles(doc, linkTarget); |
230 |
return ResultTO.create(startOffset, endOffset, linkTarget, findFiles); |
231 |
} |
232 |
|
233 |
} catch (BadLocationException ex) { |
234 |
Exceptions.printStackTrace(ex); |
235 |
} |
236 |
return ResultTO.createEmpty(); |
237 |
} |
238 |
|
239 |
private FileObject getMatchingFileInCurrentDirectory(Document doc, String path) { |
240 |
FileObject docFO = NbEditorUtilities.getFileObject(doc); |
241 |
FileObject currentDir = docFO.getParent(); |
242 |
return currentDir.getFileObject(path); |
243 |
} |
244 |
|
245 |
private List<FileObject> getMatchingFilesFromSourceRoots(Project p, String path) { |
246 |
List<SourceGroup> list = new ArrayList<>(); |
247 |
List<FileObject> foundMatches = new ArrayList<>(); |
248 |
final Sources sources = ProjectUtils.getSources(p); |
249 |
list.addAll(Arrays.asList(sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA))); |
250 |
list.addAll(Arrays.asList(sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_RESOURCES))); |
251 |
list.addAll(Arrays.asList(sources.getSourceGroups(JavaProjectConstants.SOURCES_HINT_TEST))); |
252 |
for (SourceGroup sourceGroup : list) { |
253 |
FileObject fileObject = sourceGroup.getRootFolder().getFileObject(path); |
254 |
if (fileObject != null) { |
255 |
foundMatches.add(fileObject); |
256 |
} |
257 |
} |
258 |
return foundMatches; |
259 |
} |
260 |
|
261 |
private FileObject getSingleMatchOrAskForUserChoice(Collection<FileObject> foundMatches, Project project) { |
262 |
if (foundMatches.size() == 1) { |
263 |
return foundMatches.iterator().next(); |
264 |
} |
265 |
List<FileObject> indexedFilePaths = new ArrayList<>(foundMatches); |
266 |
|
267 |
if (foundMatches.size() >= 2) { |
268 |
List<String> collector = new ArrayList<>(); |
269 |
|
270 |
for (FileObject fileObject : indexedFilePaths) { |
271 |
//convert absolute path to relative regarding the project |
272 |
String path1 = fileObject.getPath().substring(project.getProjectDirectory().getPath().length()); |
273 |
collector.add(path1); |
274 |
} |
275 |
|
276 |
//TODO replace with floating listbox like "Open implementations" (markiewb) |
277 |
final JComboBox<String> jList = new JComboBox<>(collector.toArray(new String[collector.size()])); |
278 |
if (DialogDisplayer.getDefault().notify(new DialogDescriptor(jList, "Multiple files found. Please choose:")) == NotifyDescriptor.OK_OPTION) { |
279 |
return indexedFilePaths.get(jList.getSelectedIndex()); |
280 |
} |
281 |
} |
282 |
return null; |
283 |
} |
284 |
|
285 |
private static class ResultTO { |
286 |
|
287 |
int startOffsetInLiteral; |
288 |
int endOffsetInLiteral; |
289 |
|
290 |
String linkTarget; |
291 |
|
292 |
ResultTO(int startOffset, int endOffset, String linkTarget, Collection<FileObject> foundFiles) { |
293 |
this.startOffsetInLiteral = startOffset; |
294 |
this.endOffsetInLiteral = endOffset; |
295 |
this.linkTarget = linkTarget; |
296 |
this.foundFiles = foundFiles; |
297 |
} |
298 |
Collection<FileObject> foundFiles; |
299 |
|
300 |
boolean isValid() { |
301 |
return !foundFiles.isEmpty(); |
302 |
} |
303 |
|
304 |
static ResultTO createEmpty() { |
305 |
return new ResultTO(-1, -1, null, Collections.<FileObject>emptySet()); |
306 |
} |
307 |
|
308 |
static ResultTO create(int startOffset, int endOffset, String linkTarget, Collection<FileObject> foundFiles) { |
309 |
return new ResultTO(startOffset, endOffset, linkTarget, foundFiles); |
310 |
} |
311 |
|
312 |
} |
313 |
|
314 |
} |