Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Revamped NetBeans HTML Hyperlink Navigation Tutorial

DZone's Guide to

Revamped NetBeans HTML Hyperlink Navigation Tutorial

· Java Zone
Free Resource

Microservices! They are everywhere, or at least, the term is. When should you use a microservice architecture? What factors should be considered when making that decision? Do the benefits outweigh the costs? Why is everyone so excited about them, anyway?  Brought to you in partnership with IBM.

Introduction to Hyperlinks

This would work in NetBeans 6.0, 6.1 with JDK 5, 6. So, keep that in mind. 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.

Optionally, for troubleshooting purposes, you can download the completed sample and inspect the sources.

 

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.

Now,

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-

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)- 

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 org.netbeans.modules.ahrefhyperlink in Code Name Base. Type org/netbeans/modules/ahrefhyperlink/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 one part of the code. Consider the following snippet in the isHyperlinkPoint() 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 OpenHTMLThread 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.
performClick()
Consider this method as well,
    //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()).substring(startOffset, endOffset);

if (identifier.contains("://")) {
try {
URLDisplayer.getDefault().showURL(new URL(identifier));
} catch (MalformedURLException ex) {
Exceptions.printStackTrace(ex);
}
} else {
//Start a new thread for opening the HTML document-
OpenHTMLThread run = new OpenHTMLThread(doc, identifier);
RequestProcessor.getDefault().post(run);
}

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 performClick() now, looks like this-



Please make a small correction, pass doc in the OpenHTMLThread constructor, when its instance is being created in this method.

Locating Hyperlinks

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, we will see how to implement the various Hyperlink types mentioned in the Introduction.

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));


Background Process- 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. All usecases mentioned in the previous section, are implemented using locateAnchorName(), setPosition()-

public class OpenHTMLThread implements Runnable {

//<editor-fold desc="Variables & Constructor">
private StyledDocument doc;
private String identifier;
private String[] args;

public OpenHTMLThread(Document doc, String identifier) {
super();
this.doc = (StyledDocument) doc;
this.identifier = identifier;
}
boolean anonFlag = true;

public void setAnonFlag(boolean anonFlag) {
this.anonFlag = anonFlag;
}
//</editor-fold>
public void run() {

args = identifier.split("/");
String splitIdentifier = args[args.length - 1];
if (splitIdentifier.contains("#")) {
final String anchorName = splitIdentifier.substring(splitIdentifier.indexOf("#") + 1);
if (splitIdentifier.charAt(0) == '#') {
locateAnchorName(args, anchorName, true);
} else {
locateAnchorName(args, anchorName, false);
}
} else {
verifyHyperlinkStatus(splitIdentifier);
}
}
findHTML()
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 findHTML() {
FileObject fo = NbEditorUtilities.getFileObject(doc);
FileObject foHtml = null;

//<editor-fold desc="Finding file...">
// Here we're working out whether we're dealing with a relative link or not:
if (identifier.contains("/")) {
String fullPath = fo.getPath();
try {
URL f = new File(fullPath).toURI().resolve(identifier.split("#")[0]).toURL();
foHtml =
URLMapper.findFileObject(f);
} catch (MalformedURLException ex) {
Logger.getLogger(OpenHTMLThread.class.getName()).log(Level.SEVERE, null, ex);
}

} else {
foHtml = fo.getParent().getFileObject(identifier.split("#")[0]);
}
//</editor-fold>
if (foHtml == null) {
StatusDisplayer.getDefault().setStatusText("File can't be found...");
}

return foHtml;
}
locateAnchorName(final String[] args, final String anchorName, boolean flag)
This method lets the IDE locates the Line, which consists of the anchor declaration, as mentioned in the usecases-
 
    private void locateAnchorName(final String[] args, final String anchorName, boolean flag) {
if (flag) {
final JTextComponent editor = EditorRegistry.lastFocusedComponent();
org.netbeans.editor.Utilities.runInEventDispatchThread(new Runnable() {

public void run() {
setPosition(editor.getDocument(), anchorName);
}
});
} else {
verifyHyperlinkStatus(args[args.length - 1]);
}
}
 
openEditor(DataObject dObject, final String anchorName)
This lets you open the editor, basically findHTML() and openEditor() were earlier a part of run() method, however I have splitted it into various reusable methods-
    private void openEditor(DataObject dObject, final String anchorName) {
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();

//<editor-fold desc="Positioning the Cursor...">
if ((panes != null) && (panes.length > 0)) {
setPosition(panes, 0, anchorName);
} 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, anchorName);
}
ec.removePropertyChangeListener(this);
}
}
});
ec.open();
}
//</editor-fold>
}
});
}
}

verifyHyperlinkStatus(String splitIdentifier)
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 splitIdentifier) {
try {
//<editor-fold desc="Verfying Status...">
final TopComponent[] tc = TopComponent.getRegistry().getOpened().toArray(new TopComponent[0]);
for (int k = 0; k < tc.length; k++) {
final EditorCookie editor = tc[k].getLookup().lookup(EditorCookie.class);
if (editor != null) {
DataObject d = tc[k].getLookup().lookup(DataObject.class);
if (d != null) {
final FileObject fo = d.getPrimaryFile();
//final int index = k;
final String[] hyperLink = splitIdentifier.split("#");
if (hyperLink[0].equals(fo.getNameExt())) {
EventQueue.invokeAndWait(new Runnable() {

public void run() {
StatusDisplayer.getDefault().setStatusText("Regaining Focus...");
JEditorPane[] panes = editor.getOpenedPanes();
if (hyperLink.length > 1) {
setPosition(panes, 0, hyperLink[1]);
} else {
setPosition(panes, 0, null);
}
//tc[index].requestActive();
setAnonFlag(false);
}
});
if (anonFlag == false) {
break;
}
}
}
}
}
//</editor-fold>
if (anonFlag) {
try {
DataObject dObject;
dObject = DataObject.find(findHTML());

if (splitIdentifier.contains("#")) {
openEditor(dObject, splitIdentifier.split("#")[1]);
} else {
openEditor(dObject, null);
}
} catch (DataObjectNotFoundException ex) {
Logger.getLogger(OpenHTMLThread.class.getName()).log(Level.SEVERE, null, ex);
}
}
} catch (InterruptedException ex) {
Logger.getLogger(OpenHTMLThread.class.getName()).log(Level.SEVERE, null, ex);
} catch (InvocationTargetException ex) {
Logger.getLogger(OpenHTMLThread.class.getName()).log(Level.SEVERE, null, ex);
}
}
}

Overloaded Methods-

Following two methods are overloaded, they are used to set the caret position on a particular line, or at some specific point-

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

//Selecting the apt position...if exists, cursor is placed-
int index = text.indexOf("<body>");
if (identifier != null) {
if ((index = text.indexOf("name")) > 0) {
int loc = text.indexOf(identifier, index);
index = loc + identifier.length();
if (text.charAt(index) == '\"') {
NbEditorUtilities.getLine(d, loc,
true).show(Line.SHOW_GOTO);
return;
}
}
if ((index = text.indexOf("\"#" + identifier + "\"")) > 0) {
StatusDisplayer.getDefault().setStatusText("Anchor doesn't exist...");
NbEditorUtilities.getLine(d, index,
true).show(Line.SHOW_GOTO);
return;
}
}
if (index > 0) {
NbEditorUtilities.getLine(d, index,
true).show(Line.SHOW_GOTO);
}

} catch (BadLocationException ex) {
Logger.getLogger(OpenHTMLThread.class.getName()).log(Level.SEVERE, null, ex);
}
}
setPosition(JEditorPane[] pane, int pos, String identifier)
Makes use of both NbEditorUtilities, as well as, setCaretPosition() of JEditorPane, to show the exact line/cursor of the referred anchorName, or <body> tag! 
 
    private void setPosition(JEditorPane[] pane, int pos, String identifier) {
try {
//Gets the content of the document-
Document d = pane[pos].getDocument();
String text = d.getText(0, d.getLength() - 1);

//Selecting the apt position...if exists, cursor is placed-
int index = text.indexOf("<body>");
if (identifier != null) {
if ((index = text.indexOf("name")) > 0) {
int loc = text.indexOf(identifier, index);
index = loc + identifier.length();
if (text.charAt(index) == '\"') {
NbEditorUtilities.getLine(d, loc,
true).show(Line.SHOW_GOTO);
// pane[pos].setCaretPosition(loc);
return;
}
}
if ((index = text.indexOf("\"#" + identifier + "\"")) > 0) {
StatusDisplayer.getDefault().setStatusText("Anchor doesn't exist...");
NbEditorUtilities.getLine(d, index,
true).show(Line.SHOW_GOTO);
// pane[pos].setCaretPosition(index);
return;
}
}
if (index > 0) {
NbEditorUtilities.getLine(d, index,
true).show(Line.SHOW_GOTO);
// pane[pos].setCaretPosition(index);
}

} catch (BadLocationException ex) {
Logger.getLogger(OpenHTMLThread.class.getName()).log(Level.SEVERE, null, ex);
}
}
Make very sure that the following import statements are declared- 
import java.awt.EventQueue;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
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;
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;

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="org.netbeans.modules.ahrefhyperlink.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-

  • Within same document, OR
  • Present in a referenced javascript.

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

References

 

 

Discover how the Watson team is further developing SDKs in Java, Node.js, Python, iOS, and Android to access these services and make programming easy. Brought to you in partnership with IBM.

Topics:

Published at DZone with permission of Varun Nischal. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}