|
one.world Tutorial Part II The RemoteCounter
Application In part II of the one.world tutorial, we will explore how to extend the Counter application to allow its value to be reset or synchronized to that of another counter. This part of the tutorial introduces and illustrates
Note that the one.world tutorial uses three browser windows: this window for the tutorial itself, another for the source code, and another for class documentation. You can find the entire source for this example in A few new concepts will be necessary in building the
In part
I of the tutorial, we used So, we'll start by rewriting the
Counter application using the The
To make The
/** Create the remote counter's main window. */
public Application.Window createMainWindow() {
return new Window(this);
}
(Note that if our application didn't have a GUI, the
The hard part is to define our own
GuiUtilities.createSimpleGrid()
to nicely lay out the text entries and their labels.
/** Implementation of the remote counter's main window. */
static final class Window extends Application.Window {
/** A label containing the current count. */
JLabel countLabel;
/** The text field for the remote host name. */
JTextField hostField;
/** The text field for the remote port number. */
JTextField portField;
/** The text field for the remote counter name. */
JTextField nameField;
/**
* Create a new main window.
*
* @param counter The remote counter component.
*/
Window(final RemoteCounter counter) {
super(counter, "Counter");
// The location source icon.
Environment env = counter.getEnvironment();
JLabel loc = GuiUtilities.createLocationSource(env.getId());
GuiUtilities.addUserPopup(loc, env);
// A label with the name of the hosting environment.
JLabel heading = new JLabel(counter.format(env.getName()));
heading.setAlignmentY(java.awt.Component.CENTER_ALIGNMENT);
// A label with the count.
countLabel = new JLabel(counter.getFormattedCount());
countLabel.setHorizontalAlignment(JLabel.CENTER);
countLabel.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT);
// The input fields for synchronizing with another counter.
int etf = GuiUtilities.ENTRY_TEXT_FIELD;
JComponent[] synchComponents =
GuiUtilities.createSimpleGrid(
new String[] {"Host", "Port", "Name"},
new int[] {etf, etf, etf},
-1);
JComponent synchInputs = synchComponents[0];
hostField = (JTextField) synchComponents[1];
portField = (JTextField) synchComponents[2];
nameField = (JTextField) synchComponents[3];
hostField.setText(counter.host);
portField.setText(new Integer(counter.port).toString());
nameField.setText(counter.name);
// The synchronize button.
JButton synchButton = new JButton("Synchronize");
synchButton.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT);
synchButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
synchronize();
}
});
// The reset button.
JButton resetButton = new JButton("Reset count");
resetButton.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT);
resetButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
resetCount();
}
});
// Pack the window.
JPanel mainContent = new JPanel(new BorderLayout());
mainContent.setBorder(BorderFactory.createEmptyBorder(3,3,3,3));
Box locBox = new Box(BoxLayout.X_AXIS);
locBox.add(heading);
locBox.add(Box.createHorizontalGlue());
locBox.add(loc);
Box buttonBox = new Box(BoxLayout.X_AXIS);
buttonBox.add(resetButton);
buttonBox.add(Box.createHorizontalStrut(10));
buttonBox.add(synchButton);
Box box = new Box(BoxLayout.Y_AXIS);
box.add(locBox);
box.add(Box.createVerticalStrut(10));
box.add(countLabel);
box.add(Box.createVerticalStrut(20));
box.add(synchInputs);
box.add(Box.createVerticalStrut(10));
box.add(buttonBox);
mainContent.add(box);
setContentPane(mainContent);
if ((0 != counter.width) || (0 != counter.height)) {
setSize(counter.width, counter.height);
} else {
pack();
}
}
...
}
The
/** Update the count label. This method is thread-safe. */
void updateCount() {
if (SwingUtilities.isEventDispatchThread()) {
updateCount1();
} else {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
updateCount1();
}
});
}
}
/** Update the count label. This method is not thread-safe. */
void updateCount1() {
countLabel.setText(((RemoteCounter)app).getFormattedCount());
countLabel.repaint();
}
To start out with, our
/** Acquire the resources needed by the remote counter application. */
public void acquire() {
synchronized (lock) {
if (INACTIVE == status) {
return;
}
// Set up the timed count update notification.
setUpdateTimer();
}
}
/** Release the resources used by the remote counter application. */
public void release() {
cancelUpdateTimer();
}
/** Set up the timed count update notification. */
public void setUpdateTimer() {
synchronized (lock) {
// Schedule the notification for every second, beginning one second
// from now, if there isn't already an active timer notification.
if (countUpdate == null) {
countUpdate = timer.schedule(Timer.FIXED_RATE,
SystemUtilities.currentTimeMillis()
+ Duration.SECOND,
Duration.SECOND,
updateHandler,
new DynamicTuple());
}
}
/** Cancel the count update notifications. */
public void cancelUpdateTimer() {
synchronized (lock) {
if (null != countUpdate) {
countUpdate.cancel();
countUpdate = null;
}
}
}
What do we do when we get a
/** Implementation of the count update handler. */
final class UpdateHandler extends AbstractHandler {
/** Handle the specified event. */
protected boolean handle1(Event e) {
if (! (e instanceof DynamicTuple)) {
return false;
}
Window window;
synchronized (lock) {
// We only update the count if the counter application is actually
// running.
if (ACTIVE != status) {
return true;
}
// Update the count value.
count++;
// Grab a reference to the main window.
window = (Window)mainWindow;
}
// Update the count label.
window.updateCount();
// Done.
return true;
}
}
There's one last small detail: We want to be able to reset the
counter value. We'll add a method to the
/**
* Sets the count to the specified value.
*
* @param count The new count value.
*/
public void setCount(int newCount) {
Window window;
synchronized (lock) {
// Don't do anything unless the application is active.
if (ACTIVE != status) {
return;
}
// Set the count value.
count = newCount;
// Reschedule the timer, so it will remain at the new value for a
// full second.
cancelUpdateTimer();
setUpdateTimer();
// Grab a reference to the main window.
window = (Window)mainWindow;
}
// Update the count label in the main window.
window.updateCount();
}
Then the
/** Reset the count to 0. */
void resetCount() {
((RemoteCounter)app).setCount(0);
}
There are two steps to making the "Synchronize" button work: First, the application must be able to receive and respond to synchronization requests from other remote counters. Second, the application must send a request to another remote counter when the user presses the "Synchronize" button, and it must act appropriately on the response. Our application will receive requests from
other Our application will respond to such a request with another
To keep the code clean, we'll write a separate event handler, nested
in the
/** Implementation of the synchronization request handler. */
final class SyncRequestHandler extends AbstractHandler {
/** Handle the specified event. */
protected boolean handle1(Event e) {
if (e instanceof RemoteEvent) {
RemoteEvent re = (RemoteEvent)e;
// If the nested event has a field named "getCount",
// reply with the actual count. We'll respond using
// AbstractHandler's response method rather than an operation,
// leaving it up to the requestor to retry if our message doesn't
// get there.
if (re.event.hasField("getCount")) {
DynamicTuple response = new DynamicTuple(syncReference, null);
response.set("count", new Integer(count));
respond(request, re.closure, re.event, response);
return true;
}
}
return false;
}
}
But how do these remote events get delivered to us in the first place?
And how do we get the The
// We will export the synchronization request handler under this
// environment's name on the local machine.
RemoteDescriptor descriptor =
new RemoteDescriptor(syncRequestHandler,
getEnvironment().getName());
// This is the event to send to establish the binding.
BindingRequest bindingRequest =
new BindingRequest(null, null, descriptor, Duration.FOREVER);
We'll use an operation to help make sure the binding request gets a
response. If the response is a But, the binding might not succeed. For instance,
someone else might already be using the name we want.
If the binding doesn't succeed, we'll get an
// Using an operation, attempt to establish the binding.
Operation op =
new Operation(timer, request, new AbstractHandler() {
protected boolean handle1(Event e) {
if (e instanceof BindingResponse) {
// The binding succeeded.
BindingResponse response = (BindingResponse)e;
// Hang on to the resource.
syncReference = (RemoteReference)response.resource;
// Maintain the binding.
leaseMaintainer =
new LeaseMaintainer(response.lease,
response.duration,
syncRequestHandler,
null,
timer);
// Start the application.
start();
// All done.
return true;
}
// If we didn't get a binding response, the binding
// failed.
Throwable x;
if (e instanceof ExceptionalEvent) {
x = ((ExceptionalEvent)e).x;
} else {
x = new UnknownEventException(e.getClass().getName());
}
JOptionPane.showMessageDialog(mainWindow,
"Unable to start RemoteCounter:\n" + x,
"RemoteCounter Startup Error",
JOptionPane.ERROR_MESSAGE);
stop(true);
return true;
}
});
// Start the operation.
op.handle(bindingRequest);
We now have another resource to manage (the REP binding), so the
if (leaseMaintainer != null) {
leaseMaintainer.cancel();
leaseMaintainer = null;
}
The When the user presses the "Synchronize" button, we'll store the text from the host, port, and name text fields. This not only lets us use them to get the value of the other counter, but also lets us display this text to the user later if they decide to checkpoint and restore the application. Then, we'll call a method that does the actual work of synchronization.
/**
* Synchronize this counter with the user-specified remote counter.
*/
void synchronize() {
RemoteCounter counter = (RemoteCounter)app;
try {
counter.setRemoteCounter(hostField.getText(),
Integer.parseInt(portField.getText()),
nameField.getText());
counter.synchronize();
} catch (NumberFormatException x) {
// Show an error message to the user.
JOptionPane.showMessageDialog(this,
"Please enter an integer for the remote port number",
"RemoteCounter Runtime Error",
JOptionPane.ERROR_MESSAGE);
}
}
We'll do the actual work of synchronizing with the other counter using an operation. Since users are impatient, we'll use an operation with a short timeout (5 seconds) and only one retry. We'll need to export the operation just like we did the
Once the operation is exported, we'll use it to send
a Here's the full
public void synchronize() {
// Disable the main window.
mainWindow.setEnabled(false);
// Create an operation with a short timeout and only one retry.
// (Users are impatient.)
final Operation op = new Operation(1, 5*Duration.SECOND,
timer, request, null);
// The operation will need to start by exporting its response handler
// as an anonymous remote resource, to obtain a remote reference.
Event bindingRequest =
new BindingRequest(null, null,
new RemoteDescriptor(op.getResponseHandler()),
Duration.MINUTE);
// Set the operation's continuation.
op.continuation = new AbstractHandler() {
protected boolean handle1(Event e) {
if (e instanceof BindingResponse) {
BindingResponse response = (BindingResponse)e;
// A remote reference for this operation.
RemoteReference ref =
(RemoteReference)response.resource;
// The remote counter resource.
NamedResource remote =
new NamedResource(host, port, name);
// The event we will send to the remote counter.
DynamicTuple dt = new DynamicTuple(ref, null);
dt.set("getCount", Boolean.TRUE);
// Send the value request.
op.handle(new RemoteEvent(null, null, remote, dt));
// Done for now.
return true;
} else if (e instanceof RemoteEvent) {
RemoteEvent re = (RemoteEvent)e;
// If the nested event has an integer field named "count",
// that is the new count value.
Object o = re.event.get("count");
if (o instanceof Integer) {
setCount(((Integer)o).intValue());
// Re-enable the main window.
mainWindow.setEnabled(true);
return true;
}
} else if (e instanceof ExceptionalEvent) {
ExceptionalEvent xe = (ExceptionalEvent)e;
JOptionPane.showMessageDialog(mainWindow,
"Unable to synchronize with " + host + ":" + port + "/"
+ name + ":\n" + xe.x,
"RemoteCounter Runtime Error",
JOptionPane.ERROR_MESSAGE);
// Re-enable the main window.
mainWindow.setEnabled(true);
return true;
}
return false;
}
};
// Start the operation.
op.handle(bindingRequest);
}
In addition to the point-to-point remote event passing we have
used here, a discovery service allows resources on a local network
to be described with a tuple and discovered using a query, independent of which
one.world node the resource lives on. Events sent through
the discovery service may be either anycast or multicast. There is
very little difference between the interface for point-to-point
remote event passing and that for discovery. See In the one.world source distribution, |