Welcome to the NetBeans IDE 5.0/5.5 FeedReader tutorial. The FeedReader application that you build in this tutorial is a simple RSS/Atom feed browser, modeled after the Sage plug-in for Mozilla Firefox. It presents a tree of feeds with subnodes representing individual feed entries that you can open in a browser. As an example, here a feed entry from the PlanetNetBeans RSS feed is opened in the IDE's internal browser:
Table of Contents
Creating the FeedReader Window Adding Code to the Application
IntroductionBefore beginning to code the FeedReader application, it's a good idea to familiarize yourself with some of the frequently used terms in the area of application development in NetBeans IDE. In the process, you will build a general understanding of the application that you will create, find out about what you are about to learn, and set up everything that you are going to need. About Frequently Used TermsThis tutorial assumes that you have a basic conceptual understanding of the infrastructure that is built right into NetBeans. There is less to understand than you might think. Common terms to be familiar with are as follows:
As an aside: in NetBeans terminology, "plug-in" is an adjective while "module" is a noun. There is no semantic difference between them.
About the FeedReader ApplicationWhile writing the FeedReader application, you will leverage a lot of the NetBeans infrastructure. The first piece you leverage is the System Filesystem. As pointed out earlier, the System Filesystem consists of configuration data: it is built from the configuration files (each of which is stored on disk as " layer.xml " files) of all the plug-in modules in the system, which it writes into the user's settings directory. The System Filesystem uses the same infrastructure for recognizing files that is used for recognizing a user's files on disk. That means you can show a view of a folder inside the configuration data of the IDE just as easily as you can show a folder on disk. This way you can use all of the plumbing that is built into NetBeans for viewing files and showing trees and so forth. In fact, many views you see in the IDE use the same technique. For example, the Favorites window is a view of a folder in the System Filesystem, which contains links to files on disk. The contents of the Runtime window are also a view of a folder in the System Filesystem, which is why plug-in modules are able to add nodes to it. Since it uses the same mechanisms as are used for recognizing files on disk, the objects inside a folder can have whatever icons and display names you choose to give them. The other piece you use is the Nodes API . The Nodes API is a generalization of TreeNode, though Nodes can be displayed in a variety of viewer components, not just trees. Nodes typically represent DataObjects. A DataObject is basically a parsed file, in other words, a Java object that knows the meaning of what is in a file or what the file represents and can do something with it. Nodes add features to DataObjects that the user interacts with, such as actions, localized display names, and icons. So, after using wizards to generate some basic templates, you will use the layer.xml file to create a folder in the System Filesystem for RSS feed objects ( Creating the RssFeeds Folder ). Next, you will provide a view of the folder, similar to the IDE's Projects window or Files window, by building on top of one of the generated files ( Extending the Feed Window ). The view is rooted in your folder for RSS feed objects. Then you get the DataObject representing that folder, and its Node. You will wrap that node in a FilterNode ( Creating the RssFeeds Folder ). A FilterNode is a node that can act as a wrapper for another node; by default it behaves exactly as the other node does, but you can override methods on it to change things, so that you can give it your own icon, display name and actions. Then you wrap each of the node's children as well, doing the same thing for them as for the node. Next, you will create an Add Feed action on the root node. When the user adds an RSS feed, you do something very simple: you create a new Feed object (really just an object that contains the URL, Creating the Feed Object ) and then serialize that Feed object as a file in your RSSFeeds folder. Since you're using NetBeans built-in infrastructure for visualizing files (because you're just getting the standard node for the folder, which can notice when files are added or removed), in a split second the node for the newly added feed will appear in the user interface. Using the System Filesystem this way means that the amount of code you have to write to save the list of RSS feeds on exit is... none at all! You save a feed when the user creates it, and that data is persisted to disk automatically. So, basically, you are just dropping Feed POJOs into a folder, and you happen to be showing a view of that folder. The system takes care of virtually everything else. About this TutorialThis tutorial intends to teach you the following:
Once the software is installed, this tutorial can be completed in 60 minutes. About the ResourcesBefore you begin, you need to install the following resources on your computer:
Playing with the ApplicationBefore you start writing the application, you might want to acquaint yourself with the final product. Fortunately, the FeedReader application is an official NetBeans sample, bundled with the IDE, and waiting for you to pull it from the New Project wizard. Installing the ApplicationTake the following steps to install the sample:
Introducing the ApplicationThe FeedReader application displays the RSS/Atom Feeds window, containing a node called RSS/Atom Feeds.
Other functionality provided by the rich-client application:
Introducing the SourcesThe FeedReader sample consists of main files (Java classes) and supporting files.
Setting Up the ApplicationIn NetBeans IDE, building an application on top of NetBeans starts with generating a large number of files which will serve as the foundation of your application. For example, the IDE provides a Module Project wizard, a Module Suite Project wizard, and a Library Wrapper Module Project wizard that set up all the basic files needed by plug-in modules and applications built on the NetBeans Platform.
Creating the Module Suite Project
The IDE creates the feedreader-suite project. The project will contain the module project and library wrapper module projects that you will create in the following subsections. The project opens in the IDE. You can view its logical structure in the Projects window (Ctrl-1) and its file structure in the Files window (Ctrl-2). Wrapping the LibrariesYou could bundle the entire FeedReader application into a single plug-in module. However, the application needs the Rome, Rome Fetcher, and JDom libraries:
Later, if you want to extend the FeedReader application with more modules that may use these libraries, it would be better for them to depend on just the library modules, rather than the entire FeedReader. Also, library modules can be autoloading, which means that NetBeans will only load them when needed. Until that happens, it won't take up any memory at runtime.
You now have a library wrapper module project containing the JDom JAR file and two other library wrapper module projects containing the Rome and Rome Fetcher JAR files. Creating the Module Project
The IDE creates the FeedReader project. The project contains all of the module's sources and project metadata, such as the project's Ant build script. The project opens in the IDE. You can view its logical structure in the Projects window (Ctrl-1) and its file structure in the Files window (Ctrl-2). The Projects window should now show the following: Right-click the feedreader-suite project node, choose Properties, and click Sources in the Project Properties dialog box. The panel shows the modules that were added to the module suite while you were creating their projects. You should see FeedReader , jdom , rome , and rome-fetcher listed in the Suite Modules list. Creating the FeedReader WindowIn this section you use the Window Component wizard to generate files that create a custom windowing component and an action to invoke it. The wizard also registers the action as a menu item in the layer.xml configuration file and adds entries for serializing the windowing component. Right after finishing this section, you are shown how to try out the files that the Window Component wizard generates for you.
The IDE creates the following new files:
The IDE modifies the following existing files:
<?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="Actions"> <folder name="Window"> <file name="org-myorg-feedreader-FeedAction.instance"/> </folder> </folder> <folder name="Menu"> <folder name="Window"> <file name="FeedAction.shadow"> <attr name="originalFile" stringvalue="Actions/Window/org-myorg-feedreader-FeedAction.instance"/> </file> </folder> </folder> <folder name="Windows2"> <folder name="Components"> <file name="FeedTopComponent.settings" url="FeedTopComponentSettings.xml"/> </folder> <folder name="Modes"> <folder name="explorer"> <file name="FeedTopComponent.wstcref" url="FeedTopComponentWstcref.xml"/ </folder> </folder> </folder> </filesystem> This is what the entries in the layer.xml file do:
Trying Out the ApplicationWithout having typed a single line of code, you can already take your application for a spin. Trying it out means deploying the application to another instance of your installation of NetBeans IDE and then checking to see that the empty Feed Window displays correctly. The IDE uses an Ant build script to build and install your application. The build script was created for you when you created the module project.
Adding Code to the ApplicationNow that you have laid the basis for your application, it's time to begin adding your own code. Before doing so, you need to specify the application's dependencies. Then, you use the New File wizard and the Source Editor to create and code the main classes that were introduced in the Introducing the Sources section. Specifying the Application's DependenciesYou need to subclass several classes that belong to the NetBeans APIs. Each has to be declared as a dependency. Use the Project Properties dialog box for this purpose.
Creating the RssFeeds FolderYou will use the System Filesystem Browser to add a folder for RSS feed objects. Later, you will add code to FeedTopComponent.java , which was created for you by the Window Component wizard, to view the content of the folder.
Creating the Feed ObjectNext you create a simple POJO that encapsulates a URL and its associated Rome feed.
public class Feed implements Serializable { private static FeedFetcher s_feedFetcher = new HttpURLFeedFetcher( HashMapFeedInfoCache.getInstance()); private transient SyndFeed m_syndFeed; private URL m_url; private String m_name; protected Feed() { } public Feed(String str) throws MalformedURLException { m_url = new URL(str); m_name = str; } public URL getURL() { return m_url; } public SyndFeed getSyndFeed() throws IOException { if (m_syndFeed == null) { try { m_syndFeed = s_feedFetcher.retrieveFeed(m_url); if (m_syndFeed.getTitle() != null) m_name = m_syndFeed.getTitle(); } catch(Exception ex) { throw new IOException(ex.getMessage()); } } return m_syndFeed; } public String toString(){ return m_name; } } A lot of code is underlined, because you have not declared their packages. You do this in the next steps. Take the following steps to reformat the file and declare its dependencies:
All the red underlining should now have disappeared. If not, do not continue with this tutorial until you have solved the problem. Extending the Feed Window
setLayout(new BorderLayout()); add(view, BorderLayout.CENTER); view.setRootVisible(true); try { manager.setRootContext(new RssNode.RootRssNode()); } catch (DataObjectNotFoundException ex) { ErrorManager.getDefault().notify(ex); } ActionMap map = getActionMap(); map.put("delete", ExplorerUtils.actionDelete(manager, true)); associateLookup(ExplorerUtils.createLookup(manager, map)); Now a lot of code is underlined, because you have not declared their associated packages. You do this in the next steps. Take the following steps to reformat the file and declare its dependencies:
Creating the RssNode Class
class RssNode extends FilterNode { /** Declaring the children of the root RSS node */ public RssNode(Node folderNode) throws DataObjectNotFoundException { super(folderNode, new RssFolderChildren(folderNode)); } /** Declaring the Add Feed action and Add Folder action */ public Action[] getActions(boolean popup) { DataFolder df = (DataFolder)getLookup().lookup(DataFolder.class); return new Action[] { new AddRssAction(df), new AddFolderAction(df) }; } /** Getting the root node */ public static class RootRssNode extends RssNode { public RootRssNode() throws DataObjectNotFoundException { super(DataObject.find( Repository.getDefault().getDefaultFileSystem() .getRoot().getFileObject("RssFeeds")).getNodeDelegate()); } public String getDisplayName() { return NbBundle.getMessage(RssNode.class, "FN_title"); } } /** Getting the children of the root node */ private static class RssFolderChildren extends FilterNode.Children { RssFolderChildren(Node rssFolderNode) { super(rssFolderNode); } protected Node[] createNodes(Object key) { Node n = (Node) key; try { if (n.getLookup().lookup(DataFolder.class) != null) { return new Node[] { new RssNode(n) }; } else { Feed feed = getFeed(n); if (feed != null) { return new Node[] { new OneFeedNode(n, feed.getSyndFeed()) }; } else { // best effort return new Node[] { new FilterNode(n) }; } } } catch (IOException ioe) { ErrorManager.getDefault().notify(ioe); } catch (IntrospectionException exc) { ErrorManager.getDefault().notify(exc); } // Some other type of Node (gotta do something) return new Node[] { new FilterNode(n) }; } } /** Getting the feed node and wrapping it in a FilterNode */ private static class OneFeedNode extends FilterNode { OneFeedNode(Node feedFileNode, SyndFeed feed) throws IOException, IntrospectionException { super(feedFileNode, new FeedChildren(feed), new ProxyLookup(new Lookup[] { Lookups.fixed(new Object[] { feed }), feedFileNode.getLookup() })); } public String getDisplayName() { SyndFeed feed = (SyndFeed) getLookup().lookup(SyndFeed.class); return feed.getTitle(); } public Image getIcon(int type) { return Utilities.loadImage("org/myorg/feedreader/rss16.gif"); } public Image getOpenedIcon(int type) { return getIcon(0); } public Action[] getActions(boolean context) { return new Action[] { SystemAction.get(DeleteAction.class) }; } } /** Defining the children of a feed node */ private static class FeedChildren extends Children.Keys { private final SyndFeed feed; public FeedChildren(SyndFeed feed) { this.feed = feed; } protected void addNotify() { setKeys(feed.getEntries()); } public Node[] createNodes(Object key) { try { return new Node[] { new EntryBeanNode((SyndEntry) key) }; } catch (final IntrospectionException ex) { ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ex); //Should never happen - no reason for it to fail abov return new Node[] { new AbstractNode(Children.LEAF) { public String getHtmlDisplayName() { return "<font color='red'>" + ex.getMessage() + "</font>"; } }}; } } } /** Wrapping the children in a FilterNode */ private static class EntryBeanNode extends FilterNode { private SyndEntry entry; public EntryBeanNode(SyndEntry entry) throws IntrospectionException { super(new BeanNode(entry), Children.LEAF, Lookups.fixed(new Object[] { entry, new EntryOpenCookie(entry) })); this.entry = entry; } /** Using HtmlDisplayName ensures any HTML in RSS entry titles are /**properly handled, escaped, entities resolved, etc. */ public String getHtmlDisplayName() { return entry.getTitle(); } /** Making a tooltip out of the entry's description */ public String getShortDescription() { return entry.getDescription().getValue(); } /** Providing the Open action on a feed entry */ public Action[] getActions(boolean popup) { return new Action[] { SystemAction.get(OpenAction.class) }; } public Action getPreferredAction() { return (SystemAction) getActions(false) [0]; } } /** Specifying what should happen when the user invokes the Open action */ private static class EntryOpenCookie implements OpenCookie { private final SyndEntry entry; EntryOpenCookie(SyndEntry entry) { this.entry = entry; } public void open() { try { URLDisplayer.getDefault().showURL(new URL(entry.getUri())); } catch (MalformedURLException mue) { ErrorManager.getDefault().notify(mue); } } } /** Looking up a feed */ private static Feed getFeed(Node node) { InstanceCookie ck = (InstanceCookie) node.getCookie(InstanceCookie.class); if (ck == null) { throw new IllegalStateException("Bogus file in feeds folder: " + node.getLookup().lookup(FileObject.class)); } try { return (Feed) ck.instanceCreate(); } catch (ClassNotFoundException ex) { ErrorManager.getDefault().notify(ex); } catch (IOException ex) { ErrorManager.getDefault().notify(ex); } return null; } /** Creating an action for adding a folder to organize feeds into groups */ private static class AddFolderAction extends AbstractAction { private DataFolder folder; public AddFolderAction(DataFolder df) { folder = df; putValue(Action.NAME, NbBundle.getMessage(RssNode.class, "FN_addfolderbutton")); } public void actionPerformed(ActionEvent ae) { NotifyDescriptor.InputLine nd = new NotifyDescriptor.InputLine( NbBundle.getMessage(RssNode.class, "FN_askfolder_msg"), //NOI18N NbBundle.getMessage(RssNode.class, "FN_askfolder_title"), //NOI18N NotifyDescriptor.OK_CANCEL_OPTION, NotifyDescriptor.PLAIN_MESSAGE); Object result = DialogDisplayer.getDefault().notify(nd); if (result.equals(NotifyDescriptor.OK_OPTION)) { final String folderString = nd.getInputText(); try { DataFolder.create(folder, folderString); } catch (IOException ex) { ErrorManager.getDefault().notify(ex); } } } } /** Creating an action for adding a feed */ private static class AddRssAction extends AbstractAction { private DataFolder folder; public AddRssAction(DataFolder df) { folder = df; putValue(Action.NAME, NbBundle.getMessage(RssNode.class, "FN_addbutton")); } public void actionPerformed(ActionEvent ae) { NotifyDescriptor.InputLine nd = new NotifyDescriptor.InputLine( NbBundle.getMessage(RssNode.class, "FN_askurl_msg"), //NOI18N NbBundle.getMessage(RssNode.class, "FN_askurl_title"), //NOI18N NotifyDescriptor.OK_CANCEL_OPTION, NotifyDescriptor.PLAIN_MESSAGE); Object result = DialogDisplayer.getDefault().notify(nd); if (result.equals(NotifyDescriptor.OK_OPTION)) { final String urlString = nd.getInputText(); try { Feed f = new Feed(urlString); FileObject fld = folder.getPrimaryFile(); String baseName = "RssFeed"; //NOI18N int ix = 1; while (fld.getFileObject(baseName + ix, "ser") != null) { ix++; } FileLock lock = null; try { FileObject writeTo = fld.createData(baseName + ix, "ser"); lock = writeTo.lock(); ObjectOutputStream str = new ObjectOutputStream(writeTo.getOutputStream(lock)); str.writeObject(f); } catch (IOException ioe) { ErrorManager.getDefault().notify(ioe); } finally { if (lock != null) lock.releaseLock(); } } catch (MalformedURLException ex) { IllegalArgumentException iae = new IllegalArgumentException(NbBundle.getMessage(RssNode.class, "FN_askurl_err", urlString), ex); //NOI18N throw iae; } } } } } A lot of code is underlined, because you have not declared their packages. You do this in the next step. Now take the following steps to reformat the file and declare its dependencies:
Localizing the RssNode Class
Here is an explanation of the new key-value pairs, which localize strings defined in RssNode.java : Localization of user interface for adding a feed:
Localization of user interface for adding a folder:
Branding the ApplicationNow that you are at the end of the development cycle, while you are wrapping up the application, you are concerned with the following questions:
These questions relate to branding, the activity of personalizing an application built on top of the NetBeans Platform. The IDE provides a panelin the Project Properties dialog box of module suite projects to help you with branding.
<?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"> <!-- This is a `branding' layer. It gets merged with the layer file it's branding. In this case, it's just hiding menu items and toolbars we don't want. --> <filesystem> <!-- hide unused toolbars --> <folder name="Toolbars"> <folder name="File_hidden"/> <folder name="Edit_hidden"/> </folder> <folder name="Menu"> <folder name="File"> <file name="org-openide-actions-SaveAction.instance_hidden"/> <file name="org-openide-actions-SaveAllAction.instance_hidden"/> <file name="org-netbeans-core-actions-RefreshAllFilesystemsAction.instance_hidden"/> <file name="org-openide-actions-PageSetupAction.instance_hidden"/> <file name="org-openide-actions-PrintAction.instance_hidden"/> </folder> <folder name="Edit_hidden"/> <folder name="Tools_hidden"/> </folder> </filesystem> Distributing the ApplicationThe IDE uses an Ant build script to create a distribution of your application. The build script is created for you when you create the project.
When it is up and running, the FeedReader application displays the RSS/Atom Feeds window, containing a node called RSS/Atom Feeds. See Playing with the Application for details. Congratulations! You have completed the FeedReader tutorial.
|
|