org.netbeans.modules.java.source
Index: java.editor/src/org/netbeans/modules/java/editor/hyperlink/ResourceHyperlinkProvider.java
--- java.editor/src/org/netbeans/modules/java/editor/hyperlink/ResourceHyperlinkProvider.java
+++ java.editor/src/org/netbeans/modules/java/editor/hyperlink/ResourceHyperlinkProvider.java
@@ -0,0 +1,314 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2013 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
+ * Other names may be trademarks of their respective owners.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common
+ * Development and Distribution License("CDDL") (collectively, the
+ * "License"). You may not use this file except in compliance with the
+ * License. You can obtain a copy of the License at
+ * http://www.netbeans.org/cddl-gplv2.html
+ * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
+ * specific language governing permissions and limitations under the
+ * License. When distributing the software, include this License Header
+ * Notice in each file and include the License file at
+ * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the GPL Version 2 section of the License file that
+ * accompanied this code. If applicable, add the following below the
+ * License Header, with the fields enclosed by brackets [] replaced by
+ * your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * If you wish your version of this file to be governed by only the CDDL
+ * or only the GPL Version 2, indicate your decision by adding
+ * "[Contributor] elects to include this software in this distribution
+ * under the [CDDL or GPL Version 2] license." If you do not indicate a
+ * single choice of license, a recipient has the option to distribute
+ * your version of this file under either the CDDL, the GPL Version 2 or
+ * to extend the choice of license to its licensees as provided above.
+ * However, if you add GPL Version 2 code and therefore, elected the GPL
+ * Version 2 license, then the option applies only if the new code is
+ * made subject to such option by the copyright holder.
+ *
+ * Contributor(s):
+ *
+ * Portions Copyrighted 2013 Sun Microsystems, Inc.
+ * Portions Copyrighted 2013 markiewb
+ */
+package org.netbeans.modules.java.editor.hyperlink;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javax.swing.JComboBox;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Document;
+import javax.swing.text.JTextComponent;
+import org.netbeans.api.editor.mimelookup.MimeRegistration;
+import org.netbeans.api.java.lexer.JavaTokenId;
+import org.netbeans.api.java.project.JavaProjectConstants;
+import org.netbeans.api.lexer.Token;
+import org.netbeans.api.lexer.TokenHierarchy;
+import org.netbeans.api.lexer.TokenSequence;
+import org.netbeans.api.project.FileOwnerQuery;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectUtils;
+import org.netbeans.api.project.SourceGroup;
+import org.netbeans.api.project.Sources;
+import org.netbeans.editor.BaseDocument;
+import org.netbeans.editor.Utilities;
+import org.netbeans.lib.editor.hyperlink.spi.HyperlinkProviderExt;
+import org.netbeans.lib.editor.hyperlink.spi.HyperlinkType;
+import org.netbeans.modules.editor.NbEditorUtilities;
+import org.openide.DialogDescriptor;
+import org.openide.DialogDisplayer;
+import org.openide.NotifyDescriptor;
+import org.openide.cookies.EditCookie;
+import org.openide.cookies.OpenCookie;
+import org.openide.filesystems.FileObject;
+import org.openide.loaders.DataObject;
+import org.openide.loaders.DataObjectNotFoundException;
+import org.openide.util.Exceptions;
+
+/**
+ * Hyperlink provider opening resources which are encoded in string literals
+ * within java files.
+ *
+ * For example: {@code "com/foo/Bar.java"} will be resolved in the source-roots
+ * of
+ * {@link JavaProjectConstants.SOURCES_TYPE_JAVA}, {@link JavaProjectConstants.SOURCES_TYPE_RESOURCES}, {@link JavaProjectConstants.SOURCES_HINT_TEST}
+ *
+ * If there are multiple matches a dialog will pop up and let the user choose.
+ *
+ * @author markiewb
+ */
+@MimeRegistration(mimeType = "text/x-java", service = HyperlinkProviderExt.class)
+public class ResourceHyperlinkProvider implements HyperlinkProviderExt {
+
+ public static void openInEditor(FileObject fileToOpen) {
+ DataObject fileDO;
+ try {
+ fileDO = DataObject.find(fileToOpen);
+ if (fileDO != null) {
+ EditCookie editCookie = fileDO.getLookup().lookup(EditCookie.class);
+ if (editCookie != null) {
+ editCookie.edit();
+ } else {
+ OpenCookie openCookie = fileDO.getLookup().lookup(OpenCookie.class);
+ if (openCookie != null) {
+ openCookie.open();
+ }
+ }
+ }
+ } catch (DataObjectNotFoundException e) {
+ Exceptions.printStackTrace(e);
+ }
+
+ }
+
+ @Override
+ public int[] getHyperlinkSpan(Document doc, int offset, HyperlinkType type) {
+ ResultTO matches = findResources(doc, offset);
+ if (matches.isValid()) {
+ return new int[]{matches.startOffsetInLiteral, matches.endOffsetInLiteral};
+ } else {
+ return new int[]{-1, -1};
+ }
+ }
+
+ @Override
+ public Set getSupportedHyperlinkTypes() {
+ return EnumSet.of(HyperlinkType.GO_TO_DECLARATION);
+ }
+
+ @Override
+ public String getTooltipText(Document doc, int offset, HyperlinkType type) {
+ ResultTO result = findResources(doc, offset);
+ if (!result.isValid()) {
+ return null;
+ }
+
+ Collection findMatches = result.foundFiles;
+ if (findMatches.size() < 0) {
+ return null;
+ }
+ return MessageFormat.format("Open {0}{1,choice,0#|1#|1< ({1} matches)}", result.linkTarget, findMatches.size());
+ }
+
+ @Override
+ public boolean isHyperlinkPoint(Document document, int offset, HyperlinkType type) {
+ ResultTO matches = findResources(document, offset);
+ return matches.isValid();
+ }
+
+ @Override
+ public void performClickAction(Document doc, int position, HyperlinkType type) {
+ ResultTO matches = findResources(doc, position);
+ if (matches.isValid()) {
+ Collection foundMatches = matches.foundFiles;
+ final Project project = FileOwnerQuery.getOwner(NbEditorUtilities.getFileObject(doc));
+ FileObject fileToOpen = getSingleMatchOrAskForUserChoice(foundMatches, project);
+
+ if (fileToOpen == null) {
+// StatusDisplayer.getDefault().setStatusText("Invalid path: " + findMatches.linkTarget);
+ return;
+ }
+ openInEditor(fileToOpen);
+ }
+ }
+
+ private Set findFiles(Document doc, String path) {
+ //TODO cache the results for the same path, because checking for existence is an IO-operation (markiewb)
+
+ //fallback to search in all source roots
+ FileObject docFO = NbEditorUtilities.getFileObject(doc);
+ Set matches = new HashSet<>();
+
+ matches.addAll(getMatchingFilesFromSourceRoots(FileOwnerQuery.getOwner(docFO), path));
+
+ FileObject fileInCurrentDirectory = getMatchingFileInCurrentDirectory(doc, path);
+ if (null != fileInCurrentDirectory) {
+ matches.add(fileInCurrentDirectory);
+ }
+ return matches;
+ }
+
+ private ResultTO findResources(Document document, int offset) {
+ if (!(document instanceof BaseDocument)) {
+ return ResultTO.createEmpty();
+ }
+
+ BaseDocument doc = (BaseDocument) document;
+ JTextComponent target = Utilities.getFocusedComponent();
+
+ if ((target == null) || (target.getDocument() != doc)) {
+ return ResultTO.createEmpty();
+ }
+
+ try {
+ TokenHierarchy hi = TokenHierarchy.create(doc.getText(0, doc.getLength()), JavaTokenId.language());
+ TokenSequence ts = hi.tokenSequence(JavaTokenId.language());
+
+ ts.move(offset);
+ boolean lastTokenInDocument = !ts.moveNext();
+ if (lastTokenInDocument) {
+ // end of the document
+ return ResultTO.createEmpty();
+ }
+ while (ts.token() == null || ts.token().id() == JavaTokenId.WHITESPACE) {
+ ts.movePrevious();
+ }
+
+ Token resourceToken = ts.offsetToken();
+ if (null == resourceToken
+ || resourceToken.length() <= 2) {
+ return ResultTO.createEmpty();
+ }
+
+ if (resourceToken.id() == JavaTokenId.STRING_LITERAL // identified must be string
+ && resourceToken.length() > 2) { // identifier must be longer than "" string
+ int startOffset = resourceToken.offset(hi) + 1;
+
+ final String wholeText = resourceToken.text().subSequence(1, resourceToken.length() - 1).toString();
+
+ int endOffset = startOffset + wholeText.length();
+ String linkTarget = wholeText;
+
+// StatusDisplayer.getDefault().setStatusText("Path :" + startOffset + "/" + endOffset + "/" + offset + "//" + (offset - startOffset) + "=" + innerSelectedText);
+ Set findFiles = findFiles(doc, linkTarget);
+ return ResultTO.create(startOffset, endOffset, linkTarget, findFiles);
+ }
+
+ } catch (BadLocationException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ return ResultTO.createEmpty();
+ }
+
+ private FileObject getMatchingFileInCurrentDirectory(Document doc, String path) {
+ FileObject docFO = NbEditorUtilities.getFileObject(doc);
+ FileObject currentDir = docFO.getParent();
+ return currentDir.getFileObject(path);
+ }
+
+ private List getMatchingFilesFromSourceRoots(Project p, String path) {
+ List list = new ArrayList<>();
+ List foundMatches = new ArrayList<>();
+ final Sources sources = ProjectUtils.getSources(p);
+ list.addAll(Arrays.asList(sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA)));
+ list.addAll(Arrays.asList(sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_RESOURCES)));
+ list.addAll(Arrays.asList(sources.getSourceGroups(JavaProjectConstants.SOURCES_HINT_TEST)));
+ for (SourceGroup sourceGroup : list) {
+ FileObject fileObject = sourceGroup.getRootFolder().getFileObject(path);
+ if (fileObject != null) {
+ foundMatches.add(fileObject);
+ }
+ }
+ return foundMatches;
+ }
+
+ private FileObject getSingleMatchOrAskForUserChoice(Collection foundMatches, Project project) {
+ if (foundMatches.size() == 1) {
+ return foundMatches.iterator().next();
+ }
+ List indexedFilePaths = new ArrayList<>(foundMatches);
+
+ if (foundMatches.size() >= 2) {
+ List collector = new ArrayList<>();
+
+ for (FileObject fileObject : indexedFilePaths) {
+ //convert absolute path to relative regarding the project
+ String path1 = fileObject.getPath().substring(project.getProjectDirectory().getPath().length());
+ collector.add(path1);
+ }
+
+ //TODO replace with floating listbox like "Open implementations" (markiewb)
+ final JComboBox jList = new JComboBox<>(collector.toArray(new String[collector.size()]));
+ if (DialogDisplayer.getDefault().notify(new DialogDescriptor(jList, "Multiple files found. Please choose:")) == NotifyDescriptor.OK_OPTION) {
+ return indexedFilePaths.get(jList.getSelectedIndex());
+ }
+ }
+ return null;
+ }
+
+ private static class ResultTO {
+
+ int startOffsetInLiteral;
+ int endOffsetInLiteral;
+
+ String linkTarget;
+
+ ResultTO(int startOffset, int endOffset, String linkTarget, Collection foundFiles) {
+ this.startOffsetInLiteral = startOffset;
+ this.endOffsetInLiteral = endOffset;
+ this.linkTarget = linkTarget;
+ this.foundFiles = foundFiles;
+ }
+ Collection foundFiles;
+
+ boolean isValid() {
+ return !foundFiles.isEmpty();
+ }
+
+ static ResultTO createEmpty() {
+ return new ResultTO(-1, -1, null, Collections.emptySet());
+ }
+
+ static ResultTO create(int startOffset, int endOffset, String linkTarget, Collection foundFiles) {
+ return new ResultTO(startOffset, endOffset, linkTarget, foundFiles);
+ }
+
+ }
+
+}