Table of Contents
This tutorial is a continuation of the 2 minute Chaplin tutorial. In that tutorial I played with a USB lamp which can be controlled by means of a simple Java API. One of the main goals of that tutorial was to point out the concept of role - the lamp can be regarded not only as a reading-helper device, which is its primary role. It can be also used as an alarm or a morse-code transmitter, in other words the lamp can play various roles. And anyone can come up with new ones. In this tutorial I demonstrate other Chaplin features and concepts. Let's start with extending the alarm's functionality.
To enforce the effect of the alarm by accompanying it with sound is indeed a good idea. Let's say that for this purpose I bought a USB speaker which can be also controlled by a simple Java API.
The API is really simple and it only allows beeping with certain sound intensity. For the sake of simplicity let's assume the length of the beep is fixed.
Similarly to the lamp the beeper is a device which can play several
roles. It can play the same role as the lamp does, ie. the Alarm role. The
Alarm role is already at our disposal, the only thing we have to do is to
develop a component which will intercept the switchOn
and switchOff messages emitted by the Alarm role and
translate them to the language of the Beeper. Let's call the new component
Speaker. The following schema displays binding of
the three components into one body under the anorak of the
SpeakerAlarmContext. The
SpeakerAlarmContext is a binder composite which extends
the AlarmContext. The extension is indicated by the shells.
The following schema explains how the switchOn
and switchOff messages propagate through the
SpeakerAlarmContext composite.
As I said in the previous paragraph the Speaker component intercepts
and translates the switchOn and
switchOff messages emitted by the Alarm role to the
beeper's language. Once it becomes a component of the composite it starts
receiving these messages. The Speaker class looks as follows:
Example 1. Speaker class
public abstract class Speaker {
/**
* The beep message
*/
@FromContext
abstract void beep(int intensity);
/**
* Intensity of the sound
*/
private int intensity;
/**
* Intercepting the switchOn message
*/
public void switchOn() {
beep(intensity);
}
/**
* Intercepting the switchOff message
*/
public void switchOff() {
// do nothing
}
}
As I do not want to couple the Speaker with the Beeper device
directly so that I indicate the interaction by declaring the
beep message. It is on the context to provide an
interceptor of the beep message.
Now, the Speaker is done so it is time to fuse all components to one composite. The AlarmContext already fuses the two components: the Alarm role and the Lamp. We can reuse the AlarmContext class by extending from it. The SpeakerAlarmContext class is a binder composite class which extends the AlarmContext. It adds the Speaker component by declaring a final field and setting a Speaker instance to it.
Example 2. SpeakerAlarmContext class
@Binder
public class SpeakerAlarmContext extends AlarmContext {
// Speaker component
final Speaker speaker = $();
// configuration of the speaker
int intensity;
// Internal reference to the Beeper.
// As it is a private field the beeper does not become a part
// of the composite.
private final Beeper beeper = BeeperFactory.getBeeper();
public SpeakerAlarmContext(long pause, int repeatCount, int beepIntensity) {
super(pause, repeatCount);
this.intensity = beepIntensity;
}
// intercepting the beep message emitted by the speaker
void beep(int level) {
this.beeper.beep(level);
}
}
In this example the context class intercepts the
beep message emitted by the Speaker component and
delegates it to the internal Beeper instance. This is to demonstrate that
the composite itself can intercept messages emitted from
components.
Runing the alarm with sound is as easy as the running the simple
alarm. We simply instantiate the alarm context and call the
execute method.
Example 3. The application class
public class SpeakerAlarmProgram {
public static void main(String[] args) throws Exception {
long pause = Long.parseLong(args[0]);
int repeatCount = Integer.parseInt(args[1]);
int beepIntensity = Integer.parseInt(args[2]);
SpeakerAlarmContext alarmCtx = new SpeakerAlarmContext(pause, repeatCount, beepIntensity);
alarmCtx.execute();
}
}
Don't forget to configure properly the Java VM before you launch the program:
-javaagent:chaplin.jar=org.iqual.chaplin.tutor
The alarm device we have developed works well, however, it is not very useful as long as it is not connected to some sensor which triggers the alarm when the sensor detects some noteworthy event. Let's say that I've got an infrared sensor which can be connected to a computer via the standard USB cable (hopefully there is some free USB port on the computer).
The sensor API is as usual very simple. The
InfraSensorTrigger is an interface for interceptors of
sensor's events. Triggers are registered with the
InfraSensorController via addTrigger
method.
The next component we are going to develop is the
SignalEmitter which listens to sensor's events and
translates the incoming low-level onMotionView message
emitted by the sensor to more specific message
triggerAlarm which is understood in this application
domain.
Example 4. SignalEmitter class
public abstract class SignalEmitter implements InfraSensorTrigger {
/**
* Domain-specific message
*/
@FromContext
abstract void triggerAlarm();
/**
* Interceptor of the low-level sensor's message
*/
public void onMotionInView() throws Exception {
alarm();
}
/**
* Helper for registering and listening to the sensor.
*/
public void listen() {
// InfraSensorController belongs to the sensor API
InfraSensorController.addTrigger(this);
InfraSensorController.run();
}
}
Next, we need some component which intercepts the
triggerAlarm domain-specific message and produces some
adequate response. Let's call that class Sensor as it
represents the sensor in the application domain.
Example 5. Sensor class
public abstract class Sensor {
/**
* This message is the command for running the alarm.
*/
@FromContext
abstract void runAlarm();
/**
* This message carries logging info.
* @param record the log record
*/
@FromContext
abstract void logEvent(String record);
/**
* This is the message which should be sent to anyone who
* is interested in what is going on.
* @param message the message
*/
@FromContext
abstract void postMessage(String message);
/**
* A string identifying the location of the sensor.
*/
@FromContext
String locationInfo;
/**
* Intercept the message from the SignalEmitter. It emits
* runAlarm, logEvent and postMessage messages asynchronously.
*/
public void triggerAlarm() throws Exception {
final CountDownLatch startSignal = new CountDownLatch(1);
final CountDownLatch doneSignal = new CountDownLatch(3);
final Date time = new Date();
Thread postMessageThread = new Thread() {
public void run() {
try {
startSignal.await();
String message = "Alarm at " + locationInfo + " Time:" + time;
postMessage(message);
doneSignal.countDown();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
};
Thread runAlarmThread = new Thread() {
public void run() {
try {
startSignal.await();
runAlarm();
doneSignal.countDown();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
};
Thread logRecordThread = new Thread() {
public void run() {
try {
startSignal.await();
String logRecord = time + "," + locationInfo;
logEvent(logRecord);
doneSignal.countDown();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
};
postMessageThread.start();
runAlarmThread.start();
logRecordThread.start();
startSignal.countDown();
doneSignal.await();
}
}
The Sensor component intercepts the
triggerAlarm message sent by the
SignalEmitter and translates it to three other
messages: runAlarm, logEvent and
postMessage. The runAlarm message
requires that the alarm be triggered. The logEvent
message can be intercepted by a logging components. The
postMessage is intended for sending to anyone who is
interested what is going on in the house. The recipient of this message
can re-send it by means of email or instant messaging. The three messages
are emited asynchronously as no message is allowed to be delayed because
of processing another.
Next, we prepare a context for the Sensor
component which provides the Sensor with the
locationInfo configuration parameter and two dummy
interceptors of logEvent and
postMessage messages.
Example 6. SensorContext class
@Binder
public class SensorContext {
/**
* The sensor component.
*/
final Sensor sensor = $();
/**
* Context field holding the location of the sensor.
*/
String locationInfo;
public SensorContext(String location) {
this.locationInfo = location;
}
/**
* Dummy interceptor of logEvent message.
*/
void logEvent(String record) {
System.out.println("log:" + record);
}
/**
* Dummy interceptor of postMessage message.
*/
void postMessage(String message) {
System.out.println("message:" + message);
}
}
Now we have made all components needed for assembling the
final alarm system. The SensorProgram class assembles
the components and runs the alarm system.
Example 7. Fusing all parts into one body and running the complete alarm system
public class SensorProgram {
public static void main(String[] args) {
// configuration parameters
long pause = Long.parseLong(args[0]);
int repeatCount = Integer.parseInt(args[1]);
int beepIntensity = Integer.parseInt(args[2]);
String location = args[3];
// create the speaker alarm context
SpeakerAlarmContext alarmCtx = new SpeakerAlarmContext(pause, repeatCount, beepIntensity);
// create the sensor alarm context
SensorContext sensorCtx = new SensorContext(location);
// fuse both contexts along with the SignalEmitter into one body
SignalEmitter emitter = $(sensorCtx, alarmCtx);
// keep listening to the sensor device
emitter.listen();
}
}
The first lines of the code just prepare the configuration
parameters for the components. Then the
SpeakerAlarmContext and the
SensorContext composites are instantiated. Then the
both composites are fused together along with the
SignalEmitter component. Let's look closer at the
statement which performs that fusion.
SignalEmitter emitter = $(sensorCtx, alarmCtx);
This
statement uses the $ method which we have already seen
in the binder composites. It is always used instead of the
new operator in situations when a component's class is
abstract. Here the $ method instantiates the
SignalEmitter component and insert it into the
anonymous composite made from the method's
arguments. The anonymous composite thus contains three components: the
SpeakerAlarmContext, the
SensorContext and the
SignalEmitter.
The Chaplin class transformer generates a special private instance field in all domain classes which holds a reference to the context to which the instance belongs. This field is updated whenever the instance becomes a component of a composite. It is what basically happens when the previous statement is executed. In this case the three components have that generated field set to the reference to the same anonymous context instance.
To get an overall view of how the messages propagate through the final alarm system let's spend a short while at the following schema.
The outer shell represents the anonymous composite to which the SpeakerAlarmContext, the SensorContext and the SignalEmitter are fused. The first two components are binder composites whereas the SpeakerAlarmContext extends the AlarmContext. It is indicated by the double blue shell. The message flow is executed as follows:
The initial signal happens at the sensor. It emits the
onMotionInView message which is intercepted by the
SignalEmitter. The SignalEmitter translates this low-level message to the
triggerAlarm domain-specific message which is
understood in the application domain. The triggerAlarm
message is then caught by the Sensor component. This component translates
that message to the three messages: runAlarm,
logEvent and postMessage. These
three messages are simultaneously emitted (the simultaneity is not seen in
the picture). The logEvent and
postMessage are captured by the dummy interceptor
methods in the SensorContext whereas the runAlarm
message travels to the Alarm role component. Here, the incoming message is
translated to a sequence of the switchOn and
switchOff messages interleaved by a pause of the
specified interval. These two messages are caught by both the Speaker and
the Lamp component. The Lamp translates these messages directly to Lamp
API invocations. The Speaker translates the both messages into a single
beep method call on the Beeper device API.