blob: 25811bad6650bbbe31feb8b8798e37e6af665dc8 [file] [log] [blame]
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<!-- saved from url=(0067) -->
<HTML><HEAD><TITLE>Building administrative applications in Eclipse</TITLE>
<link rel="stylesheet" href="../default_style.css">
<META http-equiv=Content-Type content="text/html; charset=windows-1252">
<META content="MSHTML 5.50.4915.500" name=GENERATOR></HEAD>
<BODY vLink=#800080 link=#0000ff>
<DIV align=right>&nbsp; <FONT face="Times New Roman, Times, serif"
size=2>Copyright &copy; 2004 IBM Corporation.</FONT>
<TABLE cellSpacing=0 cellPadding=2 width="100%" border=0>
<TD vAlign=top align=left bgColor=#0080c0 colSpan=2><B><FONT
face=Arial,Helvetica><FONT color=#ffffff>&nbsp;Eclipse Corner
<DIV align=left>
<H1><IMG height=86 src="images/Idea.jpg" width=120></H1></DIV>
<H1 align=center>Building administrative applications in Eclipse</H1>
<BLOCKQUOTE><B>Summary</B> <BR>Eclipse is most commonly used as a platform for tools that allow the user to construct or assemble an end product out of development resources. It is less usual to use Eclipse as an administrative tool for monitoring existing runtime systems or applications. This article will describe some of the issues that arise in this case and illustrate possible solutions.
It will show you can build an Eclipse perspective dedicated to the monitoring task. Running processes are shown in a dedicated view which always reflects their current state. You can start/stop the process, manage connections, invoke operations that the server exposes, examine server output and view events generated by the running applications.
<P><B>By Doina Klinger and Chris Markes, IBM UK</B> (,<BR>
November 12, 2004</P>
<HR width="100%">
<P>Eclipse is mainly a development environment: you create applications and run them. Take the Java<SUP>TM</SUP> Development Toolkit component:
applications are stored in Java packages and classes which correspond to directories and files with .java extension respectively. The applications are
built using the Java compiler and then executed. Projects are holders for the various files that are stored in the
workspace. Depending of the nature of the file, a different type of
builder is applied to them.<p>
Administration applications are fundamentally different. In this case,
the user interacts with existing applications that typically run outside of the Eclipse
JVM, possibly on a remote machine. We will refer to these
external applications as <B>servers</B>, since they typically perform some function on
behalf of some client. The servers already exist, though they can be made to run from Eclipse or even developed from the same workspace.
Often servers are administered by a browser-based application. In this article, we will show you how it can be done using Eclipse.</P>
<H2>Server administration sample</H2>
<P>We will show how you can use Eclipse to manage external servers. Our server is a simple
phone directory that holds data about people and their phone numbers. The
<A href="#serverSection">server</A> exposes operations to add new entries, query the list of entries and find out how many you have.<P>To see the sample in action you'll need to <A
href="monitorPlugin.jar">download</A> it and install it as a plug-in on Eclipse 3.0 and then start the workbench and open the Monitor perspective.
In figure 1 you can see the sample when a few servers have been defined and data entries have been added.</P>
<P align="center"><img border="0"
Figure 1. Monitor sample in action </P>
In the servers view, the icons indicate the state of the servers: a red square shows the server being unavailable, a green triangle shows an active server. The state is kept up to date by polling every ten seconds. To create a new server entry, start the New Connection wizard from the pulldown menu of the local toolbar of the Servers view (highlighted in figure 1):</P>
<DIV align="center"><IMG border="0"
src="images/newServerWizard.gif"><BR>Figure 2. Define a new server
Specify a name, a host, a port number and whether you can start and stop the server from the workbench. Start the server by selecting the Start option from the context menu (Figure 3). Add a few new entries. You can see the server output in the console view. When you select a server, the number of entries is shown in the status line and the content in the detail view in the right hand side.</P>
<P align="center"><IMG border="0"
src="images/serverOperations.jpg"><BR>Figure 3. Invoking operations on the server
The sample shows how you can monitor the state of a running system and report back in the Monitor Log view events related to the state of the server. Add entries until the maximum number of entries is reached and you can see the event reported in the log . You can modify the server preferences for the refresh interval and maximum number of entries.</P>
<P align="center"> <IMG border="0" src="images/preferences.gif"><BR>Figure 4. Server preferences
<H2>Modelling the server world</H2>
From the point of view of an administrative application, the world is composed of the target systems being administered together with the environment of the application itself. The figure below shows the target servers running either on a different host or on the same machine with the Eclipse workbench, though in a different JVM.</P>
<P align="center"><IMG border="0" src="images/servers.gif">
Figure 5. Workbench and target servers</P>
The application requires an internal representation or model of the target systems, which is used to hold information about their state and attributes. In our example, we model individual target systems using instances of ServerElement. The ServerElement class encapsulates instance variables (exposed by accessor methods) relating to the target server's location and a snapshot of its most recently observed condition: </P>
<BLOCKQUOTE><PRE><CODE>public class ServerElement implements IAdaptable, IWorkbenchAdapter
<IMG height="13" src="images/tag_1.gif" width="24" border="0"> // Target server location
String name;
String hostname;
int port;
boolean local;
<IMG height="13" src="images/tag_2.gif" width="24" border="0"> // Target server state
boolean alive;
boolean full;
<IMG height="13" src="images/tag_3.gif" width="24" border="0">PhoneBookClient connection
<IMG height="13" src="images/tag_4.gif" width="24" border="0">ServerLauncher launcher;
The location information (<CODE><IMG height="13"
src="images/tag_1.gif" width="24" border="0"></CODE>) is used when we establish a connection to interact with the target system. These attributes are also exposed as properties of the object using the PhoneBookServerPropertySource class, which allows us to plug into the standard Properties view in the workbench. <P>
The alive and full attributes (<CODE><IMG height="13"
src="images/tag_2.gif" width="24" border="0"></CODE>) are updated as a result of the listening on the target server, and affect the way we display the server entry in the ServersView using started or stopped icons.
The ServerElement class encapsulates a PhoneBookClient instance (<CODE><IMG
height="13" src="images/tag_3.gif" width="24" border="0"></CODE>), which is the actual object used to communicate with the server and handle the network operations involved in talking to the server (in our case, a simple HTTP server).
An optional ServerLauncher field(<CODE><IMG height="13"
src="images/tag_4.gif" width="24" border="0"></CODE>) is used for invoking the server application on the local system. We'll cover the ServerLauncher in more detail in the launching servers
<A href="#launchServerSection">section</A>
later in the article.<P>
Since our sample application allows the administrator to interact with more than one target server, we use the ServersModel class to bring together multiple instances of ServerElement as a list. The interaction of these classes is described in the following diagram:
<P align="center">
<IMG border="0" src="images/classDiagramMain.gif"><BR>Figure 6. Server model
<P> We next look into how to serialize such a model.
<H2> Representing remote resources in the workspace</H2>
<H3>Beyond the workspace</H3>
<P>When we're using the Eclipse workbench to develop applications, the
resources we work with are generally local. The source files we create
reside in the workspace -- even if we use a repository to share them
with other developers -- and the built output of a project likewise
remains local. <P>
By contrast, when our task is to monitor and administer
remote systems, the content we need to display in the views we use
must be retrieved across the network. (We'll deal with issues of
'liveness' <A
href="#monitoringSection">later</A> on in the
In our example, the ServerElement is the class that represents the remote system and is persisted locally. In our example, we provide a view, PhoneBookServersListView, which is really a Navigator for servers. </P>
<P>At this point, we have to decide where to
store the connection information representing remote systems. We can store them:
normal workspace resources in a file of certain type. <BR>
They show up in the Navigator view and can be modified by an editor associated with this file type. </LI>
as data specific and private to our plug-in <BR>
The plug-in metadata
area is available for this purpose. This area can't be seen
in the Navigator view, so we need to provide a specialized view to display a representation of this content.
We've chosen the second option above, though both are valid approaches.
Incidentally, if you've been using the CVS Repository Exploring
perspective, you've already been working with an example of the second
approach. The implications of the two approaches are contrasted below.
<TABLE border="1">
<TD width="477"><B>Using workspace resources</B></TD>
<TD width="477"><B>Using private resource information (plug-in
<TD width="477">Resources (representations of remote systems) are
visible in the Navigator, and are available to other Eclipse tools</TD>
<TD width="477">Resources are visible only via dedicated plug-in code
(specialized views etc.)</TD>
<TD width="477">Representations of remote systems created with New
wizard (contribution to org.eclipse.ui.NewWizards)</TD>
<TD width="477">Representations of remote systems created by a user
action provided by plug-in code</TD>
<TD width="477">'Content' of remote systems is accessed using an
<TD width="477">Content is accessed using specialized views</TD>
<TD width="477">Multiple editor instances can be used to work with
multiple remote systems simultaneously</TD>
<TD width="477">View must specifically accommodate working with
multiple remote systems if this is desired</TD>
<TD width="477">Content can appear without customizing any
<TD width="477">Existing perspective must be customized by user (e.g.
Window -&gt; Show View) or a specialized perspective provided</TD>
<H2> Persisting server references</H2>
<P>The .metadata directory of a workspace is considered to be a "black
box" where important information about the workspace structure, such as
a project's references or a resource's properties, are typically stored.
The non-workspace
resources are stored on a per plug-in basis, typically all in one file.
In our example, the "resources" that we want to persist are ServerElement objects, or, more precisely, those attributes needed to establish a session with the remote process and interact with it -- name, host, port and the isLocal attribute. The other attributes of the ServerElement which describe the state of the server at one moment are transient and are not saved.<P>
/workspace/.metadata/.plugins/MonitorProject/connection.xml we save
information about connections in an XML format:</P>
&lt;Server Host="134.456.888.99" <SPAN
class="m"> IsLocal= "true" </SPAN>
<SPAN class="t"></SPAN>Name="MyConnection" Port="600" /&gt;
&lt;Server Host="334.553.636.44" <SPAN class="t">IsLocal</SPAN><SPAN
class="m">="</SPAN>true<SPAN class="m">"</SPAN><SPAN class="t"> </SPAN>
Name="OtherConnection" Port="600" /&gt;
&lt;Server Host="223.223.662.55" <SPAN class="t">IsLocal</SPAN><SPAN
class="m">="</SPAN>false<SPAN class="m">"</SPAN><SPAN class="t"> </SPAN>
Name="DomainConnection" Port="400" /&gt;
<P>The serialization of our server model is achieved in the following steps:</P>
<LI>construct the object representing the file where the server elements are to be saved <BR>We use the getStateLocation method on our plug-in to return the location
in the local file system of the plug-in state area for this plug-in (<IMG
height="13" src="images/tag_1.gif" width="24" border="0">).<BLOCKQUOTE><PRE><IMG height="13" src="images/tag_1.gif"
width="24" border="0"><CODE> File connectionFile = MonitorProjectPlugin.getDefault().getStateLocation().append(<FONT
<CODE> if (!connectionFile.exists()) connectionFile.createNewFile();</CODE></CODE></PRE></BLOCKQUOTE>
<LI>on start-up read the content of the file to build the connection model<BR>
We use XMLMemento for easy manipulation of the XML file.
We create a file reader for the connection file and an XMLMemento object from the
reader (<CODE><IMG height="13" src="images/tag_2.gif" width="24"
border="0"></CODE>) corresponding to the <CODE>&lt;Monitor&gt;</CODE> tag. From the parent IMemento object, we extract the children entries for each Server. (
<CODE><IMG height="13" src="images/tag_3.gif" width="24"
border="0"></CODE> ). We can now extract the attributes of the connection and initialize the in-memory model.<BLOCKQUOTE><PRE><CODE> FileReader reader = new FileReader(connectionFile);
height="13" src="images/tag_2.gif" width="24" border="0"> memento = XMLMemento.createReadRoot(reader);
<IMG height="13" src="images/tag_3.gif" width="24" border="0"> IMemento[] children =memento.getChildren(<FONT
for (int i = 0; i &lt; children.length; i++) {
String name = children[i].getString(<FONT color="#0000ff">&quot;Name&quot;</FONT>);
//extract all attributes and build the servers model
<LI>when closing the connection view, serialize the connections from the model to the file.</LI>
<P> We create an XMLMemento corresponding to our <CODE>&lt;Monitor&gt;</CODE> root (<CODE><IMG
height="13" src="images/tag_4.gif" width="24" border="0"></CODE>). We iterate through the model and create a child memento for each server (<CODE><IMG
height="13" src="images/tag_5.gif" width="24" border="0"></CODE>) and set its attributes. In the end, we save the XMLMemento data structure to the file
using a file writer(<CODE><IMG height="13"
src="images/tag_6.gif" width="24" border="0"></CODE>).</P>
Iterator iterator = model.getContent().iterator()
<IMG height="13" src="images/tag_4.gif" width="24" border="0"> XMLMemento memento = XMLMemento.createWriteRoot(<FONT color="#0000ff">&quot;Monitor&quot;</FONT>);
while (iterator.hasNext()) {
DirectoryConnection conn =((DirectoryViewElement);
<IMG height="13" src="images/tag_5.gif" width="24" border="0"> IMemento child = memento.createChild(<FONT color="#0000ff">&quot;Server&quot;</FONT>);
child.putString(<FONT color="#0000ff">&quot;Name&quot;</FONT>, conn.getName());
// the same for all attributes
<IMG height="13" src="images/tag_6.gif" width="24" border="0"> Writer writer = new FileWriter(connectionFile);; </CODE></PRE></BLOCKQUOTE>
<H2> <A name="monitoringSection"></A>Monitoring and event notifications </H2>
<H3>Is the server still there?</H3>
<P>A key ingredient in monitoring running systems or processes is
knowing whether or not they're in a normal, healthy state. In the most
basic situations, it can be enough just to know whether a server is
running or stopped, but typically there are a number of other potential
conditions -- which depend upon the type of system being monitored --
that an administrator is interested in. Rather than having to go and
pro-actively query the state of the server, it's useful to have a
continuous indication of its health, much like the dashboard in your
car. You can tell at a glance if all's well, and only peek under the
hood if something lights up that tells you there's a problem. (Well,
that's the theory!)
<P>The server view is our dashboard display showing at a quick glance the state of the running applications. How this is put together is captured in the class diagram from Figure 7. The view uses the model for its label and content provider. The server model uses a background thread to continually poll the target servers for which it contains references. When a change occurs in the state represented by the model, it generates a ServerEvent to notify listeners of the change. The ServerListView registers itself as a ServerListener in order to receive these notifications.</P>
<P>The server view needs to be up-to-date; a dashboard that shows your petrol tank full when you are down to the last drops is of not much use. Hence the view needs to be a server listener to react to the changes. The ServerListener interface defines the following methods:</P>
void serverStarted(ServerEvent event)
void serverStopped(ServerEvent event)
void serverUpdate(ServerEvent event)
void serverError(ServerEvent event)</PRE></BLOCKQUOTE>
<P align="center"><IMG border="0" src="images/classDiagramDetail.gif"><BR>
Figure 7. Class diagram </P>
<P>The way the connection object works depends upon the nature of
the server. Our example uses an HTTP server which expects connections to
be short-lived (i.e. it is connectionless), and that means we need to
perform a 'normal' interaction with the server in order to determine if
it's still there. We won't illustrate the HTTP protocol handling here -- that's implemented in the PhoneBookClient class.<P>
The ServerListModel keeps up-to-date with the state of the target servers it knows about by having a monitor thread class running in the background and examining each server element in turn. The interaction for one ServerElement is shown in the code snippet from below. For the server element, we obtain its PhoneBookClient in order to invoke its method that tells us how many entries the server contains(<CODE><IMG
height="13" src="images/tag_1.gif" width="24" border="0"></CODE>). There are two reasons for invoking this method. If we get any answer at all the server is running. We use the return value (<CODE><IMG
height="13" src="images/tag_2.gif" width="24" border="0"></CODE>) to determine if the server is full. If we get an IOException (<CODE><IMG
height="13" src="images/tag_3.gif" width="24" border="0"></CODE>) , the server is stopped.<P>
The response from the server is used to generate events for registered listeners (i.e. the servers view), using the notifyStarted and notifyUpdate methods.</P>
ServerElement element = ..;
try {
PhoneBookClient client = element.getPhoneBookClient();
<IMG height="13" src="images/tag_1.gif" width="24" border="0"> int count = client.getEntryCount ();
if (element.isAlive () == false) {
element.setAlive (true);
notifyStarted (new ServerEvent (&quot;Server started&quot;, IStatus.INFO, element.getName()));
<IMG height="13" src="images/tag_2.gif" width="24" border="0"> if (element.isFull () == false &amp;&amp; count &gt;= maxEntries) {
element.setFull (true);
notifyUpdate (new ServerEvent (&quot;Max number of entries reached&quot;, IStatus.WARNING, element.getName ()));
} else element.setFull(false);
} catch (IOException e) {
<IMG height="13" src="images/tag_3.gif" width="24" border="0"> if (element.isAlive () == true) {
element.setAlive (false);
notifyStarted (new ServerEvent (&quot;Server stopped (&quot; + e.toString () + ')', IStatus.INFO, element.getName ()));
<P>For servers that are connection-oriented (expect
clients to remain connected indefinitely), it may be sufficient simply
to assume that as long as the connection hasn't been broken, the server
is still available. For cases where we want to use more detailed
criteria to determine the health of a server, our connection object will
need to interact with the remote system and perform the necessary
operations to determine that state.
<H3>How is the server doing?</H3>
<P>In addition to checking whether a server is still running or not, the administrator might want to get more details about the well being of a running application. One way of doing this is by examining the Monitor Log view.<BR>This view records notifications from the server
sent in the shape of a ServerEvent. The view is consistent with the ErrorLog of the PDE plug-in and it wraps a TableViewer. The entries
are of our own type ServerLogEntry, which wraps various details about the event, such as the severity,
detailed message, code, server name. <BR>In our example, the type of events that are recorded are server started and stopped, and an alarm event. For illustration purposes, we have defined a (configurable) limit of ten entries
as maximum to be held by a server and we can see the event being reported when this number is reached. </P>
<P align="center"><img border="0" src="images/logView.jpg"><BR>
Figure 8. Monitor log view </P>
The events are generated by the background thread of the server model that monitors the remote servers. If one of the servers is killed externally you can see the server's state being updated to stop and a stopped event being generated.</P><P> Thinking back to the dashboard, if a warning red light shows an unusual condition, there's usually further investigation required. The experienced mechanic wants to open the hood to see the internal workings of
the running system. In our example, when the server and the log views provide an indication of a problem, the administrator might want to
dig deeper to check the state of the server. <P>
In our example we have the ServerContentView in the right hand of the
perspective. This shows the details of our PhoneBookServer, namely the
names and phone numbers corresponding to the server
selected in the Servers view. If the server is stopped, there is no
detailed content.</P>
<H2><A name="launchServerSection"></A>Launching server instances </H2>
When developing applications destined to run in a server environment, it’s useful to have an instance of the target server under the
control of the development environment. This allows the development environment – i.e. the workbench – to be the single point of
control for running the application under development. (If the server itself is the application being developed, the picture changes a
little.) A sample use is a test environment, though this function is not limited to it.<P>
If the server is a separate, existing application, we need a way of launching it from the workbench while making its output visible to the developer. In order to do this we use the framework described in <A
Java Application programmatically</A>.
The ServerLauncher class
An instance of a server launcher is associated with each server instance that’s defined as local. (The local property exists in order for us
to determine whether or not we can launch the defined server as a local process.).
The launcher is set on the <SAMP>start</SAMP> method oof the ServerElement and nulled up on the <SAMP>stop</SAMP>
and is used to determine the started/stopped status. The steps necessary to launch our phone server are detailed below:</P>
<LI>gets the launch manager <CODE><IMG height="13"
src="images/tag_1.gif" width="24" border="0"></CODE></LI>
<LI>constructs a launch configuration type and copy <CODE><IMG
height="13" src="images/tag_2.gif" width="24" border="0"></CODE></LI>
<LI>set the attributes and classpath <CODE><IMG height="13"
src="images/tag_3.gif" width="24" border="0"></CODE></LI>
<LI>launch the configuration <CODE><IMG height="13"
src="images/tag_4.gif" width="24" border="0"></CODE></LI>
public ILaunch launch (String mainClassname, String args) throws IOException, CoreException {
<IMG height="13" src="images/tag_1.gif" width="24" border="0"> ILaunchManager manager = DebugPlugin.getDefault ().getLaunchManager ();
ILaunchConfigurationType type = manager.getLaunchConfigurationType
<IMG height="13" src="images/tag_2.gif" width="24" border="0"> ILaunchConfigurationWorkingCopy config = type.newInstance (null, mainClassname);
config.setAttribute (IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, mainClassname);
config.setAttribute (IJavaLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, args);
<IMG height="13" src="images/tag_3.gif" width="24" border="0"> setClasspath (config);
<IMG height="13" src="images/tag_4.gif" width="24" border="0"> return config.launch (ILaunchManager.RUN_MODE, new NullProgressMonitor ());
<P>The <CODE>setClasspath</CODE> method creates the list of classpath entries by adding the JVM and all the required plug-ins and libraries. For more details, see the code.</P>
<H2>Tying it all together</H2>
<H3>The monitor perspective</H3>
<P>Admittedly it is a bad idea to create your own perspective in
Eclipse, you should aim to reuse the existing ones. We made an
exception for this article, because no existing perspective seem to
quite fit with the task of administering objects and for illustration
purpose. <P>
We use the <CODE>org.eclipse.ui.perspectives</CODE> extension point,
giving it the class attribute which must implement the <CODE>org.eclipse.ui.IPerspectiveFactory</CODE>
interface. The <CODE>createLayout</CODE> method arranges the views that
are relevant to our monitoring application. The layout is defined
around the editor area(top-right part). We don't actually have anything
to edit so make the editor area not visible. To the left of it, there
is the servers view. Replacing the editor area is a view that shows
the content of the selected server. At the bottom of the page, we show
the monitor view and below the property view
and the console view.</P>
<H3>Server preferences</H3>
<P>We use <CODE>org.eclipse.ui.preferencePages</CODE> extension point
with <CODE>ServerPreferencePage</CODE> as its class attribute
to define two of the configuration attributes of the server. One of
them is specific to our server, the maximum number of entries that the
phone directory accepts. After this number is reached, alarms are sent
to the log view if more entries are added. <P>
The second parameter is more generic and could be used for other
servers. It describes the refresh interval that the sample uses are to
maintain the status of the connections up to date. The line from below is used to extract the preferred value of a parameter from the preference store associated with our plug-in.<P>
<H2><A name="serverSection">Server</A></H2>
<P align="left">The server we've supplied in our example is a simple HTTP server representing a phone book. The server holds a set of phone number entries in a Properties object, and supports a handful of HTTP requests to query and update the content. The requests supported are
<LI><CODE>/lookup?name=name</CODE>: returns the phone number entry for the given name</LI>
<LI><CODE>/remove?name=name</CODE>: removes the phone number entry for the given name</LI>
<LI><CODE>/update?name=name&amp;value=value</CODE>: creates or updates the phone number entry for the given name</LI>
<LI><CODE>/count</CODE>: returns the number of phone number entries stored in the server</LI>
<LI><CODE>/info</CODE>: returns a version string</LI>
<LI><CODE>/</CODE>: returns the server's set of entries</UL>
<P> Much of the server's code is there simply to implement just enough of the
HTTP 1.1 protocol to support the necessary operations. We use a PhoneBookClient
object to construct the necessary queries and handle the responses. A
provides the client-side implementation of the HTTP protocol, and the PhoneBookClient
simply creates URLConnection instances with the appropriate request URI for
each operation.</P>
<H2><A name="resourceSection"></A>Resources</H2>
<P> <A href="monitorPlugin.jar">Monitor sample</A></P>
<P><SMALL>Java and all Java-based trademarks and logos are trademarks or
registered trademarks of Sun Microsystems, Inc. in the United States,
other countries, or both. <BR>
Other company, product or service names may be trademarks or service marks of others