NetBeans Hyperlink Navigation Tutorial

In this tutorial, you learn how to create hyperlinks in HTML files, programmatically. You will do this by implementing the NetBeans API HyperlinkProvider class. The hyperlink will let the user navigate from an HREF attribute in an HTML link to the referenced HTML file.

The new hyperlink will appear when the user holds down the Ctrl key and moves the mouse over the value of the HREF attribute, as shown here:

When the hyperlink is clicked, the referenced file opens and the cursor lands on the first BODY tag, if one exists.

This is what the completed project will look like in the Projects window:

Though the focus of this tutorial is on hyperlinking from one HTML file to another, the principles shown here could equally be applied to other types of files, such as to Java source files, XML files, and JSP files.

Installing the Software

Before you begin, you need to install the following software on your computer:


Getting Started

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 Plug-in 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 org.netbeans.modules.ahrefhyperlink in Code Name Base. Click Finish.
  3. Right-click the project, choose Properties, click Libraries in the Project Properties dialog box and declare a dependency on the following APIs:


Implementing 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.

Setting Up the HyperlinkProvider Class

Setting up our class means implementing HyperlinkProvider and initializing some values that we will need in our implementation.

  1. Create a Java class in org.netbeans.modules.ahrefhyperlink, and call it AHrefHyperlinkProvider.
  2. Change the signature so that HyperlinkProvider is implemented.
  3. Note that the following import statements will be needed:

    import org.netbeans.modules.editor.NbEditorUtilities;
    import org.openide.cookies.EditorCookie;
    import org.openide.filesystems.FileObject;
    import org.openide.loaders.DataObject;
    import org.openide.loaders.DataObjectNotFoundException;
    import org.openide.util.Exceptions;
    import org.openide.util.RequestProcessor;
    import java.beans.PropertyChangeEvent;
    import java.beans.PropertyChangeListener;
    import java.io.File;
    import java.lang.ref.Reference;
    import java.lang.ref.WeakReference;
    import java.net.MalformedURLException;
    import java.net.URL;
    import javax.swing.JEditorPane;
    import javax.swing.text.BadLocationException;
    import javax.swing.text.Document;
    import javax.swing.text.JTextComponent;
    import javax.swing.text.StyledDocument;
    import org.netbeans.api.editor.EditorRegistry;
    import org.netbeans.api.html.lexer.HTMLTokenId;
    import org.netbeans.api.lexer.Token;
    import org.netbeans.api.lexer.TokenHierarchy;
    import org.netbeans.api.lexer.TokenSequence;
    import org.netbeans.lib.editor.hyperlink.spi.HyperlinkProvider;
    import org.openide.filesystems.URLMapper;
  4. Add the following initial values at the top of the class:

    private static String AHREF_IDENTIFIER = "href";
    private Reference<Document> lastDocument;
    private int startOffset;
    private int endOffset;
    private String identifier;
  5. Define the Constructor as follows:

    public AHrefHyperlinkProvider() {
            
      lastDocument = null;
            
    }
  6. Add accessors for the document:

    private Document getLastDocument() {
        return lastDocument == null ? null : lastDocument.get();
    }
    
    private void setLastDocument(Document doc) {
        lastDocument = new WeakReference<Document>(doc);
    }


isHyperlinkPoint(Document doc, int offset)

isHyperlinkPoint(Document doc, int offset) determines whether there should be a hyperlink at the given offset within the given document. The inline comments in the method below, as well as in the code in the remainder of this tutorial, serve to explain the purpose of the code.

public boolean isHyperlinkPoint(Document doc, int offset) {

        JTextComponent target = EditorRegistry.lastFocusedComponent();
        final StyledDocument styledDoc = (StyledDocument) target.getDocument();
        if (styledDoc == null) {
            return false;
        }

        // Work only with the open editor 
        //and the editor has to be the active component:
        if ((target == null) || (target.getDocument() != doc)) {
            return false;
        }

        TokenHierarchy hi = TokenHierarchy.get(doc);
        TokenSequence<HTMLTokenId> ts = hi.tokenSequence(HTMLTokenId.language());
        ts.move(offset);
        ts.moveNext();
        Token<HTMLTokenId> tok = ts.token();
        if (tok != null) {
            int tokOffset = ts.offset();
            switch (tok.id()) {
                case VALUE:
                    while (ts.movePrevious()) {
                        Token<HTMLTokenId> prev = ts.token();
                        switch (prev.id()) {
                            case ARGUMENT:
                                if (AHREF_IDENTIFIER.equals(prev.text().toString())) {
                                    identifier = tok.text().toString();
                                    setLastDocument(doc);
                                    startOffset = tokOffset;
                                    endOffset = startOffset + tok.text().length();
                                    return true;
                                }
                            case OPERATOR:
                                continue;
                            case EOL:
                            case ERROR:
                            case WS:
                                continue;
                            default:
                                return false;
                        }
                    }
                    return false;
            }
            return false;
        }
        return false;
}

getHyperlinkSpan(Document doc, int offset)

getHyperlinkSpan(Document doc, int offset) determines the length of the hyperlink.

public int[] getHyperlinkSpan(Document doc, int offset) {

    JTextComponent target = EditorRegistry.lastFocusedComponent();
    final StyledDocument styledDoc = (StyledDocument) target.getDocument();
    if (styledDoc == null) {
        return null;
    }
    
    // Return the position, which was set in the isHyperlink method:
    return new int[]{startOffset, endOffset};
}

performClickAction(Document doc, int offset)

performClickAction(Document doc, int offset) determines what happens when the hyperlink is clicked. In general, a document should open, the cursor should move to a certain place in a document, or both.

public void performClickAction(Document doc, int offset) {

    JTextComponent target = EditorRegistry.lastFocusedComponent();
    final StyledDocument styledDocdoc = (StyledDocument) target.getDocument();
    if (styledDocdoc == null) {
        return;
    }

    //Start a new thread for opening the HTML document:
    OpenHTMLThread run = new OpenHTMLThread(styledDocdoc, identifier);
    RequestProcessor.getDefault().post(run);

}

Opening the Referenced HTML File

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

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. Next, the document with the name of the file object is opened and the cursor is positioned at the BODY tag, if found.

public class OpenHTMLThread implements Runnable {

    private StyledDocument doc;
    private String identifier;

    public OpenHTMLThread(StyledDocument doc, String identifier) {

        super();
        this.doc = doc;
        this.identifier = identifier;
    }

    public void run() {
        try {

            String cleanedIdentifier = identifier.replaceAll("\"", "");

            FileObject fo = NbEditorUtilities.getFileObject(doc);
            FileObject foHtml = null;

            // Here we're working out whether we're dealing with a relative link or not:
            if (cleanedIdentifier.contains("/")) {
                String fullPath = fo.getPath();
                try {
                    URL f = new File(fullPath).toURI().resolve(cleanedIdentifier).toURL();
                    foHtml = URLMapper.findFileObject(f);
                } catch (MalformedURLException ex) {
                    ex.printStackTrace();
                }
            } else {
                foHtml = fo.getParent().getFileObject(cleanedIdentifier);
            }

            // Here we're finding our HTML file:
            DataObject dObject;
            dObject = DataObject.find(foHtml);
            final EditorCookie.Observable ec = (EditorCookie.Observable) dObject.getCookie(EditorCookie.Observable.class);
            if (ec != null) {
                org.netbeans.editor.Utilities.runInEventDispatchThread(new Runnable() {

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

                        //Here we're positioning the cursor,
                        //if the document isn't open, we need to open it first:
                        
                        if ((panes != null) && (panes.length > 0)) {
                            setPosition(panes[0], identifier);
                        } else {
                            ec.addPropertyChangeListener(new PropertyChangeListener() {

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

                    //Here we specify where the cursor will land:
                    private void setPosition(JEditorPane pane, String identifier) {

                        try {
                            //The whole text:
                            String text = pane.getDocument().getText(0, pane.getDocument().getLength() - 1);
                            //The place where we want the cursor to be:
                            int index = text.indexOf("<body>");
                            //If we can find it, we place the cursor there:
                            if (index > 0) {
                                pane.setCaretPosition(index);
                            }
                        } catch (BadLocationException ex) {
                            ex.printStackTrace();
                        }
                    }
                });
            }
        } catch (DataObjectNotFoundException ex) {
            Exceptions.printStackTrace(ex);
        }
    }
}

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:

<folder name="Editors">
    <folder name="text">
        <folder name="html">
            <folder name="HyperlinkProviders">
            
                <file name="AHrefHyperlinkProvider.instance">
                    <attr name="instanceClass" 
                          stringvalue="org.netbeans.modules.ahrefhyperlink.AHrefHyperlinkProvider"/>
                    <attr name="instanceOf" 
                          stringvalue="org.netbeans.lib.editor.hyperlink.spi.HyperlinkProvider"/>
                </file>
                
            </folder>
        </folder>
    </folder>
</folder>

If you create a hyperlink for a different MIME type, you need to change the text/html folders above to the appropriate MIME type.

Now that the HyperlinkProvider is registered, you can install the module and try out your new hyperlinks. Hold down the Ctrl key, move the mouse over an HREF attribute as shown at the start of this tutorial:

When the hyperlink appears, you can click it and let the IDE navigate to the referenced HTML file.

Send Us Your Feedback

Next Steps