15 minutes Chaplin tutorial

Zbyněk Šlajchrt



Table of Contents

Introduction
Making the alarm to sound
Speaker component
Extending the AlarmContext
Running the alarm
Integrating with an infrared sensor

Introduction

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.

Making the alarm to sound

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.

Figure 1. USB Speaker

USB Speaker

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.

Figure 2. Speaker API

Speaker API

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.

Figure 3. Extending the AlarmContext and fusion of the three components

Extending the AlarmContext and fusion of the three components

The following schema explains how the switchOn and switchOff messages propagate through the SpeakerAlarmContext composite.

Figure 4. switchOn/switchOff messages propagation

switchOn/switchOff messages propagation

Speaker component

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.

Extending the AlarmContext

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.

Running the alarm

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

Integrating with an infrared sensor

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).

Figure 5. USB infrared sensor

USB infrared sensor

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.

Figure 6. Sensor API

Sensor API

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.

Note

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.

Figure 7. Propagation of messages in the complete alarm system

Propagation of messages in the complete alarm system

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.