One of the benefits of using Visual Panels to develop a system management application is that the ability to manage a remote host comes for free. Visual panels uses Java Management Extensions (JMX) to provide this remote connectivity with minimal effort to the panel developer.
The initial panel we created shows the current time on the system running the UI, not necessarily the host that the UI is administering.
We now want to change our UI to show the time of the correct host, add the ability to set the time, and add some standard UI controls to facilitate navigation. Mapping out our changes:
Notes:
- This discussion assumes Visual Panels is installed on the server you wish to manage.
- The source for all examples discussed here can be found in the Visual Panels Mercurial repository (see "Get the source") under usr/src/java/vpanels/panels/examples.
Create the server-side classes
Setting up the server end of the application is straightforward: create the classes, then deploy them into the JMX container.
Create the MXBean
The MXBean (see the tutorial if you aren't familiar with them) for our time management panel encapsules the functionality we wish to expose from a remote host. From TimeMXBean.java:
1. public interface TimeMXBean {
2. long getTime();
3. void setTime(long millis) throws IOException;
4. }
This interface is shared between the client and server, whereas the implementation of it, Time.java, is specific to the server:
1. public class Time implements TimeMXBean, MBeanRegistration {
2. @Override
3. public long getTime() {
4. return System.currentTimeMillis();
5. }
6.
7. @Override
8. public void setTime(long millis) throws IOException {
9. Date date = new Date(millis);
10. DateFormat format = new SimpleDateFormat("MMddHHmmyyyy.ss");
11. String dateString = format.format(date);
12.
13. try {
14. ProcessUtil.exec(0, "/usr/bin/date", dateString);
15.
16. } catch (CommandFailedException e) {
17. throw new IOException(e);
18.
19. // Not likely
20. } catch (InterruptedException e) {
21. throw new IOException("command interrupted");
22. }
23. }
24. ...
Rather than bothering with native methods, this implementation just calls
/usr/bin/date (14) to set the time.
Lastly, Time implements the MBeanRegistration
interface (1) to set the ObjectName of the MBean within the JMX
container:
24. ...
25. @Override
26. public ObjectName preRegister(MBeanServer server, ObjectName name) {
27. if (name == null) {
28. name = TimeUtil.OBJECT_NAME;
29. }
30. return name;
31. }
32. }
Create the Cacao Module
Visual Panels uses Cacao (the Common Agent
Container, also known as CAC
outside of Sun) as its JMX container. Deploying into Cacao involves
delivering a subclass of com.sun.cacao.Module. From TimeModule.java:
1. public class TimeModule extends Module {
2. private ObjectName name;
3.
4. public TimeModule(DeploymentDescriptor descriptor) {
5. super(descriptor);
6. }
7.
8. @Override
9. protected void start() {
10. TimeMXBean bean = new Time();
11. try {
12. name = getMbs().registerMBean(bean, null).getObjectName();
13. } catch (Exception e) {
14. availabilityStatusSetAdd(AvailabilityStatusEnum.FAILED);
15. setOperationalState(OperationalStateEnum.DISABLED);
16. }
17. }
18.
19. @Override
20. protected void stop() {
21. try {
22. getMbs().unregisterMBean(name);
23. } catch (Exception e) {
24. }
25. }
26. }
This most basic of modules simply registers our TimeMXBean
implementation with the MBeanServer when the module is started
(12), then unregisters it when it is stopped (22).
Deploy the Cacao Module
The next step is to create a Cacao deployment descriptor for the
TimeModule:
1. <?xml version='1.0' encoding='utf-8'?>
2. <!DOCTYPE module SYSTEM "urn:sun:n1:cacao:module:dtd:1_1">
3.
4. <module name="org.opensolaris.os.vp.panels.example.time2.server.module.TimeModule"
5. version="1.0"
6. initial-administrative-state="UNLOCKED"
7. ignored-at-startup="No"
8. enable-on-demand="No">
9.
10. <description>Time module</description>
11.
12. <module-class>
13. org.opensolaris.os.vp.panels.example.time2.server.module.TimeModule
14. </module-class>
15.
16. <public-path>
17. <path-element>
18. file:/path/to/myjar.jar
19. </path-element>
24. </public-path>
25.
26. <cacao-version-supported>2.0</cacao-version-supported>
27. <heap-requirements-kilobytes>0</heap-requirements-kilobytes>
28. </module>
This file, org.opensolaris.os.vp.panels.example.time2.server.module.TimeModule.xml, provides Cacao with the information it needs to run our module. Line 18 identifies the full path to the jar file containing the module and the MBean classes.
Once this file has been created (and the jar file copied to the location specified), Cacao can be made aware of it:
# cp org.opensolaris.os.vp.panels.example.time2.server.module.TimeModule.xml \
/etc/cacao/instances/default/modules
# /usr/sbin/cacaoadm restart
That should be it for the server side of the equation. Now all that's left is to tie the client UI to it.
Amend the client-side classes
Building on the classes we created in the last example, we can now modify our UI to use the new server code.
Expose the TimeMXBean from TimePanelDescriptor
The connection between the client and the server starts with the
PanelDescriptor. Here we amend TimePanelDescriptor.java
to provide this new functionality:
1. public class TimePanelDescriptor extends AbstractSwingPanelDescriptor
2. implements ConnectionListener {
3.
4. private TimeMXBean bean;
5. ...
6. public TimePanelDescriptor(String id, ClientContext context) {
7. ...
8. setBean(context.getConnectionInfo());
9. context.addConnectionListener(this);
10. }
11.
12. @Override
13. public void connectionChanged(ConnectionEvent event) {
14. setBean(event.getConnectionInfo());
15. }
16.
17. public TimeMXBean getTimeBean() {
18. return bean;
19. }
20.
21. private void setBean(ConnectionInfo info) {
22. try {
23. bean = JMX.newMXBeanProxy(
24. info.getConnector().getMBeanServerConnection(),
25. TimeUtil.OBJECT_NAME, TimeMXBean.class);
26. } catch (IOException e) {
27. bean = null;
28. }
29. }
30. }
The ClientContext passed to the TimePanelDescriptor
constructor (6) provides access to the
javax.management.remote.JMXConnector that connects us to the JMX
container on the server. The setBean method uses that
JMXConnector to instantiate a proxy (23) to the
TimeMXBean we implemented and registered on the server.
If the user changes his credentials after the panel is started, we need to
know about it since it may affect what he has permission to do on the server.
Fortunately, the Visual Panels API makes this easy: we first implement the
ConnectionListener interface (2), add this
TimePanelDescriptor to the ClientContext's list of
ConnectionListeners (9), then re-invoke setBean
when the credentials change (14).
Use the TimeMXBean in TimeControl
Now that we have a handle to our remote TimeMXBean, we can modify
TimeControl.java to use it to initialize our UI:
1. public class TimeControl
2. extends SwingSettingsControl<TimePanelDescriptor, TimePanel> {
3.
4. private Date date;
5. ...
6. @Override
7. protected void initComponent() {
8. long time = getPanelDescriptor().getTimeBean().getTime();
9. date = new Date(time);
10. getComponent().getSpinnerDateModel().setValue(date);
11. }
12. ...
By using the TimeMXBean exposed by our
PanelDescriptor, initComponent can now get the time
from the managed host (8), rather than from the host running the UI.
Similarly, we can also provide a save method to set the time on
the managed host:
12. ...
13. @Override
14. protected void save() throws ActionAbortedException, ActionFailedException,
15. ActionUnauthorizedException {
16.
17. Date newDate = (Date)getComponent().getSpinnerDateModel().getValue();
18. if (!newDate.equals(date)) {
19. long time = date.getTime();
20. try {
21. getPanelDescriptor().getTimeBean().setTime(time);
22. } catch (IOException e) {
23. throw new ActionFailedException(e);
24. } catch (SecurityException e) {
25. throw new ActionUnauthorizedException(e);
26. }
27. }
28. }
29. ...
Any errors that occur in this process are repackaged (23, 25) as
ActionExceptions so that the Visual Panels framework can handle
them appropriately:
-
ActionFailedExceptions cause an error to be shown to the user. -
ActionUnauthorizedExceptions cause the user to be prompted to update his credentials. -
ActionAbortedExceptions indicate that the user cancelled the action willingly, so no handling is done.
Who calls save? No one, yet. But we can change that by
modifying our createComponent method:
29. ...
30. @Override
31. protected TimePanel createComponent() {
32. TimePanel panel = new TimePanel();
33.
34. addDefaultApplyAction(panel);
35. addDefaultCancelAction(panel, true);
36. addDefaultOkayAction(panel, true);
37.
38. return panel;
39. }
40. }
You may have noticed that our TimeControl now extends SwingSettingsControl
(2), instead of SwingControl
as in the previous
example. This change allows us to display Apply (34), Cancel (35), and
Okay (36) buttons, and tie them to the appropriate actions with minimal
effort: Apply and Okay invoke save (14) and Cancel invokes
reset (not implemented here).
By virtue of the true arguments (35, 36), Okay and Cancel will
attempt to close the panel if their respective actions have completed
successfully. This involves unwinding the navigation stack, which may result
in more prompts to the user to handle unsaved changes. Using other methods to
close the panel, namely System.exit, do not take this precaution
and should be avoided. Note also that System.exit will
kill the entire JVM, which may be running panels other than this one!
Make TimePanel a SettingsPanel
The final change to our example is a minor one. To be able to display the
Apply, Cancel, and Okay buttons, our TimePanel must extend
SettingsPanel. From TimePanel.java:
1. public class TimePanel extends SettingsPanel {
2. private SpinnerDateModel model;
3.
4. public TimePanel() {
5. JLabel label = new JLabel(Finder.getString("time.label"));
6. model = new SpinnerDateModel();
7. JSpinner spinner = new JSpinner(model);
8.
9. JPanel panel = new JPanel(new BorderLayout(5, 0));
10. panel.add(label, BorderLayout.WEST);
11. panel.add(spinner, BorderLayout.EAST);
12.
13. setContent(panel);
14. }
15.
16. public SpinnerDateModel getSpinnerDateModel() {
17. return model;
18. }
19. }
Other than its new superclass (1), the only change from the previous
example is the call to setContent (13). See the SettingsPanel
API for details.
Putting it together
The resulting UI, slightly more usable than its predecessor, is shown below:
If a non-root user attempts to commit a time change, Visual Panels catches the
resulting ActionUnauthorizedException thrown from save and
prompts the user to upgrade his credentials:
