RevampedHyperlinkNavigation

Revamped NetBeans Hyperlink Navigation Tutorial

Contributed By; Varun Nischal
Requires IDE; NetBeans 6.0, NetBeans 6.1


[[{TableOfContentsTitle=Contents} | {TableOfContents title='Contents'}]]

Introduction


Brief Overview

This tutorial has been updated to work with module's version 1.1 with a minor fix done after releasing the code for the same. Also, keep in mind that this module would work in NetBeans 6.0, 6.1 with JDK 5, 6. There's a possibility that some API method invocations might be deprecated in NetBeans 6.5 and/or 6.7.

Optionally, for troubleshooting purposes, you can download the completed sample version 1.0, which is also available in NetBeans 6.0 Update Center. Also, you may download the latest binaries from download section at Kenai.

For latest updates/fixes for this module, you may inspect the sources.

About Hyperlinks

Hyperlinks in the IDE are created when you implement the NetBeans API HyperlinkProvider class. In this case, we will implement numerous usecases. Before we get into that, lets have a look at the possible hyperlinks in a HTML file-

Possible Hyperlinks

Type 1
Anchors present in HTML files like <a name="anchorName"></a>
  • <a href="#anchorName">Text in-between these tags, get hyperlinked!</a>
Type 2
Anchors like above present in referenced HTML files
  • <a href="ref.html#anchorName">Text in-between these tags, get hyperlinked!</a>
  • This ref.html is the Referenced HTML file, having an anchor like Type 1. Clicking on it takes you to the anchorName in the ref.html File.
Type 3
Referenced HTML files within same directory
  • <a href="ref.html">Text in-between these tags, get hyperlinked!</a>
  • This ref.html is the Referenced HTML file, which is in the same directory, as does the file that has such hyperlinks.
Type 4
Referenced HTML files within another directory
  • <a href="path/to/ref.html">Text in-between these tags, get hyperlinked!</a>
  • This ref.html is the Referenced HTML file, which is in the directory having a relative path (path/to/).
Type 5
External URL's
  • <a href="http://platform.netbeans.org/tutorials/60/nbm-hyperlink.html">NetBeans Hyperlink Navigation Tutorial</a>
  • The HREF attribute contains an external URL, which should be open through external browser.
Type 6
Referenced HTML files within another directory
  • <a href="\path\to\ref.html">Text in-between these tags, get hyperlinked!</a>
  • This ref.html is the Referenced HTML file, which is in the directory having a relative path (\path\to\).
  • This case was not handled in earlier release of the code, so it was a bug and was resolved in new release of the code.

Now, for instance

The hyperlinks will appear when the user holds down the Ctrl key and moves the mouse over the value of the HREF attribute, as shown here- File:hyperlink_RevampedHyperlinkNavigation.jpg

This is actually a Type 2 Hyperlink.

When the hyperlink is clicked, the referenced file opens and the cursor lands on the first "goto" anchor, if one exists. This is what the completed project will look like in the Projects window (which presents the Logical View);

Before

File:ahrefhyperlink_module_RevampedHyperlinkNavigation.jpg

After

File:ahrefhyperlink-module-v11_RevampedHyperlinkNavigation.jpg


Creating the Module Project


In this section, we use a wizard to create a module project. We declare dependencies on modules that provide the NetBeans API classes needed by our hyperlink module.

  1. Choose File > New Project. In the New Project wizard, choose NetBeans Modules under Categories and Module Project under Projects. Click Next. Type AHrefHyperlink in Project Name and set Project Location to an appropriate folder on your disk. If they are not selected, select Standalone Module and Set as Main Project. Click Next.
  2. Type
    com.wp.nbguru.ahrefhyperlink.revamped
    in Code Name Base. Type {com/wp/nbguru/ahrefhyperlink/revamped/resources/layer.xml} in XML Layer. Click Finish.
You should add dependencies while developing, not before hand. This will let you remember what API's are required for what purposes.

Implement the HyperlinkProvider Class


The HyperlinkProvider class implements three methods, each of which is discussed in detail below, accompanied by a practical example in the context of our module. First we set up the class and then we implement each of the three methods in turn. You can see the original tutorial for this entire section. I will just list the changes made in some parts of the code.

isHyperlinkPoint()
  • Consider the following snippet in this method-
                          case ARGUMENT:
                                if (AHREF_IDENTIFIER.equals(prev.text().toString())) {
                                    identifier = tok.text().toString();
                                    setLastDocument(doc);
                                    startOffset = tokOffset;
                                    endOffset = startOffset + tok.text().length();
                                    return true;
                                }
  • Changes made into that, basically refactored a bit-
                            case ARGUMENT:
                                if (AHREF_IDENTIFIER.equals(prev.text().toString())) {
                                    startOffset = tokOffset;
                                    endOffset = startOffset + tok.text().length();
                                    ++startOffset;
                                    --endOffset;
                                    return true;
                                } 
Why did I do
++startOffset
and {--endOffset}?
Reason being, that the tutorial was showing "ref.html" as hyperlink and double quotes got removed in OpenThreadImpl with the introduction of the variable cleanedIdentifier! Whereas, I wanted to see only the in-between text as hyperlink, when I mouse-over while CTRL key is pressed, i.e. ref.html as hyperlink.
What happened to
setLastDocument(doc)
?
Remove that setter for {lastDocument} variable used in the original tutorial, as well as, remove that variable, I couldn't make any use of it, so removed it.
performClickAction()
  • Consider the following snippet in this method-
    //Start a new thread for opening the HTML document:
    OpenHTMLThread run = new OpenHTMLThread(styledDocdoc, identifier);
    RequestProcessor.getDefault().post(run);
  • I replaced this with the following code-
        identifier = doc.getText(0, doc.getLength());
        identifier = identifier.substring(startOffset, endOffset);

        if (identifier.contains("://")) {
            try {
                URLDisplayer.getDefault().showURL(new URL(identifier));
            } catch (MalformedURLException ex) {
                logger.log(Level.SEVERE, null, ex);
            }
        } else {
            //Start a new thread for opening the HTML document-
            OpenThreadImpl thread = new OpenThreadImpl();
            thread.assignMembers(doc, identifier);

            RequestProcessor.getDefault().post(thread);
        }
Note, when you replace it you would see a red line under the 1st statement of this snippet, press Alt + Enter.Editor pops-up suggestions to either surround with try-catch, or throw exception. So, select the former one, and your performClickAction() now, looks like this-
public void performClickAction(Document doc, int offset) {
        try {
            JTextComponent target = EditorRegistry.lastFocusedComponent();
            final StyledDocument styledDoc =
                    (StyledDocument) target.getDocument();

            if (styledDoc == null) {
                return;
            }
            identifier = doc.getText(0, doc.getLength());
            identifier = identifier.substring(startOffset, endOffset);

            if (identifier.contains("://")) {
                try {
                    URLDisplayer.getDefault().showURL(new URL(identifier));
                } catch (MalformedURLException ex) {
                    logger.log(Level.SEVERE, null, ex);
                }
            } else {
                //Start a new thread for opening the HTML document-
                OpenThreadImpl thread = new OpenThreadImpl();
                thread.assignMembers(doc, identifier);

                RequestProcessor.getDefault().post(thread);
            }
        } catch (BadLocationException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
    }

Locating Hyperlinks


Next, you need to create a class that opens an HTML file in a separate thread. Here, the class is called OpenThreadImpl.

Refer- Type 1, 2

Clicking on #anchorName takes you to the anchorName in either the current document OR ref.html File.

Refer- Type 3, 4

The document with the name of the file object is opened and the cursor is positioned at the BODY tag, if found. Else, user would be notified via message in the Status Bar of the NetBeans IDE, "File not found!".

Refer- Type 5

The HREF attribute contains an external URL, which should be open through external browser with the help of following code-

URLDisplayer.getDefault().showURL(new URL(identifier));

Refer- Type 6

The status bar of NetBeans IDE displays a message of not able to find such document. I am not sure, whether it has to be implemented or not.


Background Process- OpenThreadImpl


The token identified in the
isHyperlinkPoint()
method is received by this class. Then the token is analyzed to see whether it contains a slash, which indicates that it is a relative link. In that case, the file object is extrapolated from the URL to the file. Otherwise, the file object is created from the token itself. All usecases mentioned in the previous section, are implemented using {locateAnchorName()}, setPosition()
public class OpenThreadImpl implements Runnable {

    // <editor-fold defaultstate="collapsed" desc="Variables & Constructor">
    private static StyledDocument document;
    private static String oldIdentifier;
    private String[] args;
    private String anchorName;
    private final Logger logger =
            Logger.getLogger(OpenThreadImpl.class.getName());
    private final StatusDisplayer statusBar =
            StatusDisplayer.getDefault();

    public OpenThreadImpl() {
        oldIdentifier = null;
        document = null;
        args = null;
        anchorName = null;
    }
    private boolean editorFlag = true;

    private void setEditorFlag(boolean flag) {
        this.editorFlag = flag;
    }
    // </editor-fold>

    /**
     *
     * @param doc
     * @param id
     */
    public void assignMembers(Document doc, String id) {
        document = (StyledDocument) doc;
        oldIdentifier = id;
    }

    /**
     * Implements Runnable Interface to open HTML document in editor,
     * if exists..
     */
    public void run() {

        String newIdentifier;

        if (oldIdentifier.indexOf("/") >= 0) {

            args = oldIdentifier.split("/");
            newIdentifier = args[Args.length1];
        } else {
            newIdentifier = oldIdentifier;
        }

        if (newIdentifier.contains("#")) {

            int index = newIdentifier.indexOf("#") + 1;
            anchorName = newIdentifier.substring(index);

            if (newIdentifier.charAt(0) == '#') {
                // Anchor Exists Within Same Webpage...
                locateAnchorName();
            } else {
                // Anchor Exists In External Webpage...
                verifyHyperlinkStatus(newIdentifier);
            }
        } else {
            // Anchor Doesn't Exist...
            verifyHyperlinkStatus(newIdentifier);
        }
    }

findDocument()

Finds the
FileObject
of the referenced HTML file, if it has a relative path, also takes care of the anchor presence in {identifier}. Also, implemented the case when such files can't be found-
    private FileObject findDocument() {
        FileObject htmlFileObj = null;

        // Here we're working out whether we're dealing with a
        // relative link or not..
        if (oldIdentifier.charAt(0) == '/' || oldIdentifier.contains("<br>")) {
            htmlFileObj = null;
        } else {
            final String htmlFileName =
                    oldIdentifier.split("#")[0];
            final FileObject fileObj =
                    NbEditorUtilities.getFileObject(document);

            if (oldIdentifier.contains("/")) {

                String fullPath = fileObj.getPath();
                try {
                    URI htmlFileURI = new File(fullPath).toURI();
                    htmlFileURI = htmlFileURI.resolve(htmlFileName);

                    final URL htmlFile = htmlFileURI.toURL();
                    htmlFileObj = URLMapper.findFileObject(htmlFile);
                } catch (MalformedURLException exception) {
                    logger.log(Level.SEVERE, null, exception);
                }
            } else {
                htmlFileObj = fileObj.getParent();
                htmlFileObj = htmlFileObj.getFileObject(htmlFileName);
            }
        }

        return htmlFileObj;
    }

locateAnchorName()

This method lets the IDE locates the
Line
, which consists of the anchor declaration, as mentioned in the usecases-
    private void locateAnchorName() {
        final JTextComponent editor = EditorRegistry.lastFocusedComponent();
        runInEventDispatchThread(new Runnable() {

            public void run() {
                setPosition(editor.getDocument());
            }
        });
    }

openEditor(DataObject dataObj)

This lets you open the editor, basically
findDocument()
and {openEditor()} were earlier a part of run() method, however I have split them it into various reusable methods-
    private void openEditor(DataObject dataObj) {
        final Observable ec = (Observable) dataObj.getCookie(Observable.class);

        if (ec != null) {
            runInEventDispatchThread(new Runnable() {

                public void run() {
                    final JEditorPane[] panes = ec.getOpenedPanes();

                    if ((panes != null) && (panes.length > 0)) {
                        setPosition(panes[0].getDocument());
                    } else {
                        ec.addPropertyChangeListener(new PropertyChangeListener() {

                            public void propertyChange(PropertyChangeEvent evt) {
                                if (Observable.PROP_OPENED_PANES.equals(evt.getPropertyName())) {
                                    final JEditorPane[] panes = ec.getOpenedPanes();
                                    if ((panes != null) && (panes.length > 0)) {
                                        setPosition(panes[0].getDocument());
                                    }
                                    ec.removePropertyChangeListener(this);
                                }
                            }
                        });
                        ec.open();
                    }
                }
            });
        }
    }

verifyHyperlinkStatus(String newIdentifier)

This basically checks that the identifier present in the hyperlink, if the file exists in relative path, then is it open OR not. If not, then opens it, also takes care of the anchors presence.
    private void verifyHyperlinkStatus(String newIdentifier) {
        try {
            //<editor-fold desc="Verfying Status...">
            final Set<TopComponent> setOfWindows =
                    TopComponent.getRegistry().getOpened();
            final TopComponent[] windows =
                    setOfWindows.toArray(new TopComponent[0]);

            for (int pos = 0; pos < windows.length; pos++) {
                final EditorCookie editor =
                        windows[Pos].getLookup().lookup(EditorCookie.class);

                if (editor != null) {
                    DataObject dataObj =
                            windows[Pos].getLookup().lookup(DataObject.class);

                    if (dataObj != null) {
                        final FileObject fileObj = dataObj.getPrimaryFile();
                        final String[] hyperLink = newIdentifier.split("#");

                        if (hyperLink[0].equals(fileObj.getNameExt())) {
                            invokeAndWait(new Runnable() {

                                public void run() {
                                    JEditorPane[] panes =
                                            editor.getOpenedPanes();
                                    if (hyperLink.length <= 1) {
                                        anchorName = null;
                                    }
                                    if (panes != null) {
                                        setPosition(panes[0].getDocument());
                                        setEditorFlag(false);
                                    }
                                }
                            });
                            if (editorFlag == false) {
                                break;
                            }
                        }
                    }
                }
            }
            //</editor-fold>
            if (editorFlag) {
                try {
                    FileObject htmlFileObj = findDocument();

                    if (htmlFileObj != null) {
                        DataObject dataObj = DataObject.find(htmlFileObj);
                        // Open HTML Editor..
                        openEditor(dataObj);
                    } else {
                        // Handle HTML File Not Found..
                        statusBar.setStatusText("Either file doesn't exist, " +
                                "or it can't be found...");
                    }
                } catch (DataObjectNotFoundException ex) {
                    logger.log(Level.SEVERE, null, ex);
                }
            }
        } catch (InterruptedException ex) {
            logger.log(Level.SEVERE, null, ex);
        } catch (InvocationTargetException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
    }

setPosition(Document d)

Makes use of
NbEditorUtilities
to show the exact line of the referred {anchorName}, or <body> tag!
    private void setPosition(Document doc) {//, String identifier) {
        try {
            //Gets the content of the document-
            String text = doc.getText(0, doc.getLength() - 1);

            //Finds starting position of "body" tag..
            int index = text.indexOf("<body>");
            if (anchorName != null && index > 0) {
                if ((index = text.indexOf("<a name=\"" + anchorName + "\">",
                        index)) < 0) {
                    index = text.indexOf("<body>");
                }
            }
            if (index > 0) {
                NbEditorUtilities.getLine(doc, index,
                        true).show(Line.SHOW_GOTO);
            }

        } catch (BadLocationException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
    }
  • Make very sure that the following import statements are declared in
    OpenThreadImpl.java
    -
// <editor-fold defaultstate="collapsed" desc="Java APIs Imports..">
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JEditorPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.StyledDocument;
// </editor-fold>

// <editor-fold defaultstate="collapsed" desc="NetBeans APIs Imports..">
import org.netbeans.api.editor.EditorRegistry;
import org.netbeans.modules.editor.NbEditorUtilities;
import org.openide.awt.StatusDisplayer;
import org.openide.cookies.EditorCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.URLMapper;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.text.Line;
import org.openide.windows.TopComponent;
// </editor-fold>

// <editor-fold defaultstate="collapsed" desc="Static Imports..">
import static java.awt.EventQueue.*;
import static org.netbeans.editor.Utilities.*;
import static org.openide.cookies.EditorCookie.Observable;
// </editor-fold>

Registering the HyperlinkProvider Implementation Class


Finally, you need to register the hyperlink provider implementation class in the module's layer.xml file. Do this as follows, while making sure that the line in bold below is the fully qualified class name of the class that implements HyperlinkProvider-

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.1//EN" "http://www.netbeans.org/dtds/filesystem-1_1.dtd">
<filesystem>
    <folder name="Editors">
        <folder name="text">
            <folder name="html">
                <folder name="HyperlinkProviders">
                    <file name="AHrefHyperlinkProvider.instance">
                        <attr name="instanceClass"
                          stringvalue="com.wp.nbguru.ahrefhyperlink.revamped.AHrefHyperlinkProvider"/>
                        <attr name="instanceOf"
                          stringvalue="org.netbeans.lib.editor.hyperlink.spi.HyperlinkProvider"/>
                    </file>
                </folder>
            </folder>
        </folder>
    </folder>
</filesystem>
If you create a hyperlink for a different MIME type, you need to change the text/html folders above to the appropriate MIME type.


Work Ahead


So, what further usecases could be implemented. Suppose, HREF attribute contains-

Call to a javascript function, whose definition is present-
1. Within same document, OR
2. Present in a referenced javascript.
Add your own over here-

These usecases could be implemented by you, or else consider joining my project at Kenai to work towards evolution of this plug-in.

Once you join in, you can help fix issues filed against this plug-in using JIRA issue tracker.

See Also


Blogs

20090630
http://weblogs.java.net/blog/n_varun/archive/2009/06/fixing_bugs_in.html
20090629
http://nbguru.wordpress.com/2009/06/29/hyperlink-navigation-renaissance-part-2/


Others

Not logged in. Log in, Register

By use of this website, you agree to the NetBeans Policies and Terms of Use. © 2012, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo