Step by Step Guide to Creating a Module - struct-by-lightning/wpi-suite GitHub Wiki

This guide is intended to get you off the ground quickly. It will walk you through creating a very simple module from the beginning. This includes: obtaining a copy of the repository, setting up your development environment, building the entire project, running WPISuite on your local machine, and finally implementing a simple module. The module is called PostBoard, and it is basically a message board where anyone can submit a message. The message board is visible to all and anonymous.

If you like, you can download the code for the entire PostBoard module. The most recent code is also available in the WPISuite repository on GitHub. Just checkout the dev-postboard branch.

Table of Contents

If you have not already, you should follow the steps for Forking the Repository, Setting Up Your Development Environment, and Running WPI Suite Locally.

We are going to create a simple module for WPISuite as a demonstration. The module is a post board. Basically, a place where people can write messages that are visible to everyone. The GUI consists of a JList (a listbox), a JTextField, and two JButtons. When the user types a message in the text field and clicks the submit button, it will be displayed in the listbox and sent to the core to be saved. When other users open this module, they will see all of the messages that have been posted. We'll start by creating and configuring a new Eclipse project for this module.

Side note: When starting a new module, it is a good idea to create a new git branch, for this example we'll name it dev-postboard. For more information on repository best practices see the Repository Guidelines.

Create and Configure an Eclipse Project

The project for our module is pretty simple to create. You start by adding a normal Java project. There are a few modifications to make the project build with the rest of the core, but that is about it.

Follow the steps on the Creating a New Module Project page to get started. Make sure to replace all instances of ModuleName with the name of the new module we are creating, PostBoard. Skip the last two steps regarding the manifest.txt and modules.conf file, you will be walked through that process later on in this guide.

Implementing a Module

Now that we have a project set up for the PostBoard modulue, we need to start implementing it. The only requirement of WPISuite modules is that they implement the IJanewayModule interface.

  1. Using Eclipse's new class wizard, add a class called PostBoard that implements IJanewayModule. Put it in the edu.wpi.cs.wpisuitetng.modules.PostBoard package.
  2. Save your new class, you do not need to add any code, and build the entire workspace.

Your module should have built without errors at this point. If there are errors related to dependencies, verify that your build.xml and dependencies.xml files were modified correctly.

Get Your Module to load in the Janeway client

For this step, we'll add the bare minimum to your PostBoard class to allow it to be loaded by Janeway. We'll also add a manifest.txt file to your project so that Janeway knows which class to load for your module.

Modify PostBoard.java:

  1. In the getName() method, add a return statement to return the name of the module, PostBoard.
@Override
public String getName() {
	return "PostBoard";
}
  1. Next add a field to the class to contain the tabs used by this module. Each module provides a list of tabs that Janeway will display.
List<JanewayTabModel> tabs;
  1. Add a constructor to initialize the list of tabs
public PostBoard() {
	tabs = new ArrayList<JanewayTabModel>();
}
  1. Modify getTabs() to return your list of tabs.
@Override
public List<JanewayTabModel> getTabs() {
	return tabs;
}

Create a Manifest.txt file:

  1. Create a new file in the root of the PostBoard project called manifest.txt
  2. Add the following line to the file:
module_class edu.wpi.cs.wpisuitetng.modules.PostBoard.PostBoard
  1. Save the file.

Now you can build the workspace, restart the core, and run Janeway. When you run Janeway you will not see anything to indicate your module exists, but the Janeway console output should indicate that your module was loaded. If it was not, make sure the full name of your module class (including the package) matches the name provided in your manifest.txt file.

Add an Empty Tab to your Module

Now that the PostBoard module is being loaded by Janeway, we would like to add a tab so that the module can display a GUI in Janeway. To do so, we're going to add a simple tab with some placeholder text.

Modify your constructor so that it looks like this:

public PostBoard() {
	// Initialize the list of tabs (however, this module has only one tab)
	tabs = new ArrayList<JanewayTabModel>();
	
	// Create a JPanel to hold the toolbar for the tab
	JPanel toolbarPanel = new JPanel();
	toolbarPanel.add(new JLabel("PostBoard toolbar placeholder")); // add a label with some placeholder text
	toolbarPanel.setBorder(BorderFactory.createLineBorder(Color.blue, 2)); // add a border so you can see the panel

	// Create a JPanel to hold the main contents of the tab
	JPanel mainPanel = new JPanel();
	mainPanel.add(new JLabel("PostBoard placeholder"));
	mainPanel.setBorder(BorderFactory.createLineBorder(Color.green, 2));

	// Create a tab model that contains the toolbar panel and the main content panel
	JanewayTabModel tab1 = new JanewayTabModel(getName(), new ImageIcon(), toolbarPanel, mainPanel);

	// Add the tab to the list of tabs owned by this module
	tabs.add(tab1);
}

What the above code does is construct a new JPanel that will contain the toolbar for this module tab (each tab in Janeway has a toolbar section and a main content section). It then adds a JLabel with some placeholder text to the toolbarPanel so that you will be able to identify it. Last, it adds a blue border to the toolbarPanel so you will see where it lies on the screen.

Next, it creates an additional JPanel to hold the main content of the tab. Again, we add a JLabel and a border to identify this panel.

Lastly, we construct a new tab of type JanewayTabModel and pass it the name of the module, an ImageIcon (which could be used to set an icon for this tab), the toolbarPanel, and the mainPanel. Then, we add the new tab to our list of tabs.

If you build your workspace and restart the core and Janeway, you should see a new tab in Janeway called PostBoard. On this tab, you should see a blue toolbar area and a green main content area.

Improving the Main Content Panel

Now we want to add some GUI components to our main panel. For the PostBoard module, they consist of a listbox, a textfield, and a submit button.

Add a class called BoardPanel in a view package under your main PostBoard package. The class should extend JPanel. The class will have three fields, a JList, a JTextField, and a JButton.

In the constructor for the BoardPanel class, we need to construct and configure the GUI components. As most of this work is swing-related, the code is not described in detail here, but you can view it below.

package edu.wpi.cs.wpisuitetng.modules.postboard.view;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;

/**
 * This class is a JPanel. It contains the actual post board, a text field
 * for entering a new message, and a submit button for submitting
 * a new message.
 * 
 * @author Chris Casola
 *
 */
@SuppressWarnings({"serial", "rawtypes", "unchecked" })
public class BoardPanel extends JPanel {

	/** A list box to display all the message on the board */
	private final JList lstBoard;
	
	/** A text field where the user can enter a new message */
	private final JTextField txtNewMessage;

	/** A button for submitting new messages */
	private final JButton btnSubmit;

	/**
	 * This is a model for the lstBoard component. Basically it
	 * contains the data to be displayed in the list box.
	 */
	private final DefaultListModel lstBoardModel;

	/**
	 * Construct the panel, the three components, and add the
	 * three components to the panel.
	 */
	public BoardPanel() {

		// Construct the list box model
		lstBoardModel = new DefaultListModel();
		lstBoardModel.add(0, "hello");
		lstBoardModel.add(0, "world");

		// Construct the components to be displayed
		lstBoard = new JList(lstBoardModel);
		txtNewMessage = new JTextField("Enter a message here.");
		btnSubmit = new JButton("Submit");

		// Set the layout manager of this panel that controls the positions of the components
		setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); // components will  be arranged vertically
		
		// Put the listbox in a scroll pane
		JScrollPane lstScrollPane = new JScrollPane(lstBoard);
		lstScrollPane.setPreferredSize(new Dimension(300,300));
		
		// Clear the contents of the text field when the user clicks on it
		txtNewMessage.addMouseListener(new MouseAdapter() {
			public void mouseClicked(MouseEvent e) {
				txtNewMessage.setText("");
			}
		});
		
		// Adjust sizes and alignments
		btnSubmit.setAlignmentX(Component.CENTER_ALIGNMENT);

		// Add the components to the panel
		add(Box.createVerticalStrut(20)); // leave a 20 pixel gap
		add(lstScrollPane);
		add(Box.createVerticalStrut(20));
		add(txtNewMessage);
		add(Box.createVerticalStrut(20));
		add(btnSubmit);
	}
}

After saving the BoardPanel class. We need one more class called MainView that also extends JPanel. The purpose of this class is to contain all other JPanel classes that will be displayed in the main content area. The MainView class constructs and sets the arrangement of, the subpanels.

package edu.wpi.cs.wpisuitetng.modules.postboard.view;

import javax.swing.JPanel;

/**
 * This panel fills the main content area of the tab for this module. It
 * contains one inner JPanel, the BoardPanel.
 * 
 * @author Chris Casola
 *
 */
@SuppressWarnings("serial")
public class MainView extends JPanel {

	/** The panel containing the post board */
	private final BoardPanel boardPanel;
	
	/**
	 * Construct the panel.
	 */
	public MainView() {
		// Add the board panel to this view
		boardPanel = new BoardPanel();
		add(boardPanel);
	}
}

The last thing we need to do to display our new GUI, is to construct the MainView class in our module class's constructor. So we replace the three lines in BoardPanel.java that were constructing the mainPanel with the following:

// Constructs and adds the MainPanel	
MainView mainPanel = new MainView();

Now, if you rebuild your workspace and restart the core and Janeway, you should see your three GUI components on the PostBoard tab.

Add a Button to the Toolbar

To add a button to the toolbar for our PostBoard tab, we need to provide a ToolbarView class that extends JToolBar. This ToolbarView, like MainView, will contain and arrange all subpanels that are part of the toolbar for this module. The MainView class looks like this:

package edu.wpi.cs.wpisuitetng.modules.postboard.view;

import javax.swing.JToolBar;

/**
 * This is the toolbar for the PostBoard module
 * 
 * @author Chris Casola
 * 
 */
@SuppressWarnings("serial")
public class ToolbarView extends JToolBar {
	
	/** The panel containing toolbar buttons */
	private final ToolbarPanel toolbarPanel;

	/**
	 * Construct this view and all components in it.
	 */
	public ToolbarView() {
		
		// Prevent this toolbar from being moved
		setFloatable(false);
		
		// Add the panel containing the toolbar buttons
		toolbarPanel = new ToolbarPanel();
		add(toolbarPanel);
	}
}

You'll notice that the ToolbarView class constructs a subpanel of type ToolbarPanel. We need to create this class. Our ToolbarPanel is going to contain one button that refreshes the contents of our post board (the JList in BoardPanel). The ToolbarPanel class looks like this:

package edu.wpi.cs.wpisuitetng.modules.postboard.view;

import javax.swing.JButton;
import javax.swing.JPanel;

/**
 * This panel contains the refresh button
 * 
 * @author Chris Casola
 *
 */
@SuppressWarnings("serial")
public class ToolbarPanel extends JPanel {

	/** The refresh button */
	private final JButton btnRefresh;
	
	
	/**
	 * Construct the panel.
	 */
	public ToolbarPanel() {
		
		// Make this panel transparent, we want to see the JToolbar gradient beneath it
		this.setOpaque(false);
		
		// Construct the refresh button and add it to this panel
		btnRefresh = new JButton("Refresh");
		add(btnRefresh);
	}
}

After both ToolbarPanel and ToolbarView have been saved, we need to modify the PostBoard constructor so that it constructs are new ToolbarView. Replace the lines that were constructing a JPanel for the toolbar with the following:

// Create a JPanel to hold the toolbar for the tab
ToolbarView toolbarView = new ToolbarView();

Then, modify the line that constructs tab1 so that it passes toolbarView to the constructor of JanewayTabModel rather than the old toolbarPanel.

If you build your workspace and restart everything, you should now see a PostBoard tab that has a toolbar and a main area. The toolbar contains a refresh button and the main area contains the list box to hold messages, a text field for entering new messages, and a submit button for submitting messages.

Add Models to Hold the Contents of the PostBoard

The data that must be saved for this module is the messages on the PostBoard. We are going to assume that messages include a String message and a datestamp of type Date. The model for a message is contained in the file PostBoardMessage.java. The class implements the Model interface. This interface is provided by the core, and all models that will be sent to the core must implement this interface.

Two methods that you should take note of are toJSON() and fromJSON(). The former converts the PostBoardMessage object into a JSON string while the latter converts a JSON string back into a PostBoardMessage. JSON is a method for converting Objects into strings so they can be sent in text form (usually via HTTP). In WPISuite, the Gson library is used to parse objects into JSON strings. These methods are required because they will be used when we start exchanging PostBoardMessages with the server.

/**
 * Returns a JSON-encoded string representation of this message object
 */
@Override
public String toJSON() {
	return new Gson().toJson(this, PostBoardMessage.class);
}

/**
 * Returns an instance of PostBoardMessage constructed using the given
 * PostBoardMessage encoded as a JSON string.
 * 
 * @param json the json-encoded PostBoardMessage to deserialize
 * @return the PostBoardMessage contained in the given JSON
 */
public static PostBoardMessage fromJSON(String json) {
	final Gson parser = new Gson();
	return parser.fromJson(json, PostBoardMessage.class);
}

/**
 * Returns an array of PostBoardMessage parsed from the given JSON-encoded
 * string.
 * 
 * @param json a string containing a JSON-encoded array of PostBoardMessage
 * @return an array of PostBoardMessage deserialzied from the given json string
 */
public static PostBoardMessage[] fromJsonArray(String json) {
	final Gson parser = new Gson();
	return parser.fromJson(json, PostBoardMessage[].class);
}

The code above also includes a method called fromJsonArray() which can take as an argument a JSON string containing an array of PostBoardMessage objects and return an actual array of PostBoardMessage objects.

The rest of the methods in PostBoardMessage are required by the Model interface, but are not needed for this module and are therefore left empty or returning null.


In addition to the PostBoardMessage model, we need a model to hold all of the messages in the PostBoard. This model is for client-side use only, so it does not need to implement the Model interface. However, since the Java Swing object we are using to display the PostBoard is a JList, we need to implement an AbstractListModel to hold the messages displayed in the GUI.

You can look at the actual PostBoardModel class but the basics are: it contains a field of type List<PostBoardMessage> that holds all of the messages, it provides the methods addMessages(PostBoardMessage[] messages) and addMessage(PostBoardMessage newMessage) for adding messages to the model, and it overrides the getElementAt(int index) and getSize() methods in the AbstractListModel abstract class so that the JList can get data from the model.

We need to make one change to the BoardPanel class so that it uses our new PostBoardModel rather than the DefaultListModel we used temporarily. To do this, modify the constructor of BoardPanel so that it takes one argument: a PostBoardModel. Add a field to store the model and modify the constructor of the JList so that the PostBoardModel is provided.

public BoardPanel(PostBoardModel boardModel) {

	// Construct the list box model
	lstBoardModel = boardModel;
	
	// Construct the components to be displayed
	lstBoard = new JList(lstBoardModel);
	
	...

Make an AddMessageController

Now, we need a controller to handle adding messages to the PostBoardModel when the user clicks the Submit button. This controller is very simple. When the button is clicked, it grabs the message from the text field, adds the message to the PostBoardModel by calling addMessage(String newMessage), and then clears the text field so the next message can be entered.

/**
 * 
 */
package edu.wpi.cs.wpisuitetng.modules.postboard.controller;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import edu.wpi.cs.wpisuitetng.modules.postboard.model.PostBoardModel;
import edu.wpi.cs.wpisuitetng.modules.postboard.view.BoardPanel;

/**
 * This controller responds when the user clicks the Submit button by
 * adding the contents of the message text field to the model as a new
 * message.
 * 
 * @author Chris Casola
 *
 */
public class AddMessageController implements ActionListener {
	
	private final PostBoardModel model;
	private final BoardPanel view;
	
	/**
	 * Construct an AddMessageController for the given model, view pair
	 * @param model the model containing the messages
	 * @param view the view where the user enters new messages
	 */
	public AddMessageController(PostBoardModel model, BoardPanel view) {
		this.model = model;
		this.view = view;
	}

	/* 
	 * This method is called when the user clicks the Submit button
	 * 
	 * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
	 */
	@Override
	public void actionPerformed(ActionEvent event) {
		// Get the text that was entered
		String message = view.getTxtNewMessage().getText();
		
		// Make sure there is text
		if (message.length() > 0) {
			// Add the message to the model
			model.addMessage(message);
			
			// Clear the text field
			view.getTxtNewMessage().setText("");
		}
	}

}

In order to begin using this new controller, we need to add it as an ActionListener to the submit button in the BoardPanel class.

// Construct the add message controller and add it to the submit button
btnSubmit.addActionListener(new AddMessageController(lstBoardModel, this));

Saving PostBoardMessages to the Core

At this point, you have a version of PostBoard that runs on your computer. Each time you run it, you get a clean PostBoard. You can add messages to it, but they're gone when you restart the application, and others cannot open their copy of the PostBoard application and view them. We're going to fix this by saving messages in the WPISuite core, and then retrieving them to display them in the PostBoard client.

Before proceeding, you should read the Core & Client Communication Overview to gain some background knowledge on communicating with the core.

Creating an EntityManager

To allow our PostBoard module to save message in the core we must provide an entity manager for PostBoardMessages. The first step is to create a class that implements the EntityManager interface. Each EntityManager is responsible for all requests involving one Model class. The core automatically forwards all requests to the EntityManager. An EntityManager implements the generic EntityManager<T> interface.

We will start by creating a new class with the following signature:

public class PostBoardEntityManager implements EntityManager<PostBoardMessage>

As shown, you must provide the type of the object that this EntityManager will be storing (PostBoardMessage). Eclipse should also automatically generate method signatures for all of the methods required by the EntityManager interface.

You need to provide a simple constructor for the entity manager that takes as an argument a reference to the database so that it can be saved in a field.

public PostBoardEntityManager(Data db) {
	this.db = db;
}

Next, we need to implement the makeEntity() method. This method is responsible for receiving a message in JSON form, parsing it into an actual PostBoardMessage, and then saving it in the database. It must also return the PostBoardMessage that was saved back to the client. It is called whenever a PUT request is received to /postboard/postboardmessage.

public PostBoardMessage makeEntity(Session s, String content)
		throws BadRequestException, ConflictException, WPISuiteException {

	// Parse the message from JSON
	final PostBoardMessage newMessage = PostBoardMessage.fromJSON(content);

	// Save the message in the database if possible, otherwise throw an exception
	// We want the message to be associated with the project the user logged in to
	if (!db.save(newMessage, s.getProject())) {
		throw new WPISuiteException();
	}

	// Return the newly created message (this gets passed back to the client)
	return newMessage;
}

Lets walk through the code above step-by-step. The first parameter of makeEntity is a Session object containing information about the user making the request. The second parameter is a String containing the body of the HTTP request (in this case the body contains a JSON-encoded PostBoardMessage). The first line of code in the method uses the static method PostBoardMessage.fromJSON to convert the JSON content into a new PostBoardMessage. The if block attemtps to save the PostBoardMessage in the database, throwing an exception if an error occurrs. Importantly, the message is associated with the project that the user is currently logged in to. Finally, the new message is returned. Any exceptions that are thrown within the makeEntity() method are automatically converted into HTTP error codes and sent back to the client. Returning the new PostBoardMessage from the method will cause the core to send the message back to the client.

Now that we are able to save a PostBoardMessage, we would like to be able to retrive them. To do this, we will implement the getAll(Session s) method. This method should return all of the PostBoardMessages that have been saved in the database. This method is called when a GET request is received at /postboard/postboardmessage The code for this method is as follows:

public PostBoardMessage[] getAll(Session s) throws WPISuiteException {
	
	// Ask the database to retrieve all objects of the type PostBoardMessage.
	// Passing a dummy PostBoardMessage lets the db know what type of object to retrieve
	// Passing the project makes it only get messages from that project
	List<Model> messages = db.retrieveAll(new PostBoardMessage(null), s.getProject());

	// Return the list of messages as an array
	return messages.toArray(new PostBoardMessage[0]);
}

The first line of the method above asks the database to retrieve all objects in the database whose type matches that of the object passed to the retrieveAll method. So using the line db.retrieveAll(new PostBoardMessage(null), s.getProject()) we are asking the database to return all saved PostBoardMessage objects associated with the current project. The retrieveAll method returns a List containing all of the saved messages. Next, we must return an array of PostBoardMessage, so we need to convert the List to an Array. This can be done using the List class's toArray() method.

Next, we need to implement the getEntity(Session s, String id) method. This method provides the ability to retrieve a specific PostBoardMessage. Since we will not need to request specific PostBoardMessages in this module, we will make this method throw an exception if a specific message is requested.

public PostBoardMessage[] getEntity(Session s, String id)
		throws NotFoundException, WPISuiteException {
	// Throw an exception if an ID was specified, as this module does not support
	// retrieving specific PostBoardMessages.
	throw new WPISuiteException();
}

This module will also not be supporting modifying messages, so the update() method can simply throw an exception. The same is true for the deleteEntity() and deleteAll() methods.

public PostBoardMessage update(Session s, String content)
		throws WPISuiteException {

	// This module does not allow PostBoardMessages to be modified, so throw an exception
	throw new WPISuiteException();
}

There are two methods left to implement, save() and Count(). The count method is simple, it must simply return the number of PostBoardMessages currently saved in the database.

public int Count() throws WPISuiteException {
	// Return the number of PostBoardMessages currently in the database
	return db.retrieveAll(new PostBoardMessage(null)).size();
}

The save() method is also simple. It must simply save the given PostBoardMessage in the database.

public void save(Session s, PostBoardMessage model)
		throws WPISuiteException {

	// Save the given defect in the database
	db.save(model);
}

The EntityManger for PostBoardMessage is now complete. The full code can be found here.

There is one last step required to let the core know about this new EntityManager. We need to modify the ManagerLayer class in the core. In the constructor for ManagerLayer you should see a few map.put() lines. This is where you tell the core to which EntityManager requests for a given model should be sent. Add the following line:

map.put("postboard" + "postboardmessage", new PostBoardEntityManager(data));

The first argument is the concatenation of the name of the module and the name of the model this EntityManager is responsible for, the second argument is a new PostBoardEntityManager. The core is now all set to handle requests for the PostBoard model!

Modify AddMessageController to Send Requests

Now that we have EntityManager all set to receive PostBoardMessage objects, we need to start sending it messages whenever they are created by the user in the PostBoard GUI. To do this we will modify the existing AddMessageController so that it also sends a PUT request to the core containing the new message.

In the AddMessageController, remove the line model.addMessage(message) which adds messages to the model. Since we will be sending message to the core, we do not want to add them to the model until we are sure they have been saved.

Next, add the following code to send a request to the core that saves the message. This code should be placed beneath the line that clears the text in the new message JTextField.

// Send a request to the core to save this message
final Request request = Network.getInstance().makeRequest("postboard/postboardmessage", HttpMethod.PUT); // PUT == create
request.setBody(new PostBoardMessage(message).toJSON()); // put the new message in the body of the request
request.addObserver(new AddMessageRequestObserver(this)); // add an observer to process the response
request.send(); // send the request

Line by line, the code above does the following:

  1. Use the Network factory to generate a new request object. The request will be sent to the given URL using the given HTTP method.
  2. Construct a new PostBoardMessage for the message the user entered, convert the PostBoardMessage object to JSON, and put the JSON string in the body of the request.
  3. Add a new observer to the request, this enables us to respond when the response is received (more to come below).
  4. Send the request. This call returns immediately without waiting for the response.

The last thing we need to do is add a method to the controller that can be called by the observer when a successful response is received.

/**
 * When the new message is received back from the server, add it to the local model.
 * @param message
 */
public void addMessageToModel(PostBoardMessage message) {
	model.addMessage(message);
}

At this point there will be an error in the controller, because AddMessageRequestObserver does not exist yet. Proceed to the next section to create it.

Create the AddMessageRequestObserver observer

A RequestObserver is an observer, in other words it "watches" the network request to see when it changes and then takes action based on the change. When a request fails or succeeds, it notifies its observers by calling methods. For a RequestObserver, these methods are responseSuccess(IRequest), responseError(IRequest), and fail(IRequest, Exception). In the AddMessageRequestObserver, we need to implement these methods to take action, particularly when a successful response is received to our request to add a message.

The signature for this class is as follows:

public class AddMessageRequestObserver implements RequestObserver

The constructor for AddMessageRequestObserver must take one parameter, the AddMessageController. This is necessary so that the addMessageToModel() method we created in the previous step can be called when the response is received.

public AddMessageRequestObserver(AddMessageController controller) {
	this.controller = controller;
}

Next, we need to implement the responseSuccess() method. This is where we do most of the work. This method is called automatically when the response is received and it is passed an IRequest. The IRequest provides access to the HTTP response.

@Override
public void responseSuccess(IRequest iReq) {
	// Get the response to the given request
	final ResponseModel response = iReq.getResponse();

	// Parse the message out of the response body
	final PostBoardMessage message = PostBoardMessage.fromJson(response.getBody());

	// Pass the message back to the controller
	controller.addMessageToModel(message);
}

The comments in the code above are pretty self-explanatory. Basically, we grab the JSON containing the message out of the response body, parse it back into a PostBoardMessage object and then add it to our local model.

The methods that deal with error responses and other failures are below. For this module, we aren't going to do much to notify the user of the errors, but for other modules you would likely want to notify the user somehow.

@Override
public void responseError(IRequest iReq) {
	System.err.println("The request to add a message failed.");
}

@Override
public void fail(IRequest iReq, Exception exception) {
	System.err.println("The request to add a message failed.");
}

At this point you should be able to save message in the core. You can confirm this by looking at the console output from the core and ensuring your message are being saved when you hit the Save button in the GUI. The next section deals with retrieving message from the core.

Retrieving PostBoardMessages from the core

In order to retrieve messages from the core, we need two more classes, a controller to generate the request and an observer to handle the response. For this module, we will simply be retrieving all messages stored in the core each time a request is sent. The Refresh button on the toolbar will trigger the controller.

Create the GetMessagesController controller

The controller will be named GetMessagesController with the following signature:

public class GetMessagesController implements ActionListener

The actionPerformed() method will be called when the user clicks the Refresh button. This is where we need to send the request to the server for all PostBoardMessages.

@Override
public void actionPerformed(ActionEvent e) {
	// Send a request to the core to save this message
	final Request request = Network.getInstance().makeRequest("postboard/postboardmessage", HttpMethod.GET); // GET == read
	request.addObserver(new GetMessagesRequestObserver(this)); // add an observer to process the response
	request.send(); // send the request
}

The code above is similar to the request code in the AddMessageController. Basically, we use the Network factory to create a new request, we also use the same path which is the module name plus a / and then the model name. The request type is a GET this time since we want to retrieve models. There is a different observer for this request which we will be creating below.

This controller also needs a method to be called by the observer when it receives messages back from the core in response to our request. This method will take the messages and add them to the local model so that they are displayed to the user in the GUI.

/**
 * Add the given messages to the local model (they were received from the core).
 * This method is called by the GetMessagesRequestObserver
 * 
 * @param messages an array of messages received from the server
 */
public void receivedMessages(PostBoardMessage[] messages) {
	// Empty the local model to eliminate duplications
	model.emptyModel();
	
	// Make sure the response was not null
	if (messages != null) {
		
		// add the messages to the local model
		model.addMessages(messages);
	}
}

Create the GetMessagesRequestObserver observer

This observer will be called when responses are received to the requests generated by the GetMessagesContoller. It will be responsible from parsing the JSON array of messages from the response body into an array of PostBoardMessage objects. This observer must also implement the RequestObserver interface. The constructor should also take the GetMessagesController as a parameter so that the receivedMessages() method can be called.

public class GetMessagesRequestObserver implements RequestObserver

The responseSuccess method looks like this:

/*
 * Parse the messages out of the response body and pass them to the controller
 * 
 * @see edu.wpi.cs.wpisuitetng.network.RequestObserver#responseSuccess(edu.wpi.cs.wpisuitetng.network.models.IRequest)
 */
@Override
public void responseSuccess(IRequest iReq) {
	PostBoardMessage[] messages = PostBoardMessage.fromJsonArray(iReq.getResponse().getBody());
	controller.receivedMessages(messages);
}

As you can see above, the static fromJsonArray method in the PostBoardMessage class is used to automatically parse the JSON string in the response body. Once the messages are parsed, the array of messages is passed back to the controller through the receivedMessages() method.

To notify users of errors, we are going to provide a slightly more user-friendly implementation of the responseError() and fail() methods in this observer.

/*
 * @see edu.wpi.cs.wpisuitetng.network.RequestObserver#responseError(edu.wpi.cs.wpisuitetng.network.models.IRequest)
 */
@Override
public void responseError(IRequest iReq) {
	fail(iReq, null);
}

/*
 * Put an error message in the PostBoardPanel if the request fails.
 * 
 * @see edu.wpi.cs.wpisuitetng.network.RequestObserver#fail(edu.wpi.cs.wpisuitetng.network.models.IRequest, java.lang.Exception)
 */
@Override
public void fail(IRequest iReq, Exception exception) {
	PostBoardMessage[] errorMessage = {new PostBoardMessage("Error retrieving messages.")};
	controller.receivedMessages(errorMessage);
}

If an error occurs while retrieving the messages, we will simply put a single message in the JList on the GUI that says "Error retrieving messages".

The last step to enable retrieving messages is to add the GetMessagesController as an actionListener to the refresh button on the ToolbarPanel.

Refactoring

After developing the module, it became apparent that both the views and the controllers would need access to the PostBoardModel. So we will modify the ToolbarView and MainView constructors so that they have one parameter of type PostBoardModel.

The ToolbarPanel class, contained in ToolbarView, also needs access to the model so we add a parameter to the ToolbarPanel constructor as well. This enables us to construct the GetMessagesController inside the ToolbarPanel constructor. The modified constructor looks like this:

public ToolbarPanel(PostBoardModel boardModel) {
		
	// Make this panel transparent, we want to see the JToolbar gradient beneath it
	this.setOpaque(false);

	// Construct the refresh button and add it to this panel
	btnRefresh = new JButton("Refresh");

	// Add the get messages controller to the button
	btnRefresh.addActionListener(new GetMessagesController(boardModel)); // this is were we needed the model

	// Add the button to this panel
	add(btnRefresh);
}

Similar to the ToolbarPanel, our BoardPanel class also needs the model, so we modify the BoardPanel constructor to take one parameter of type PostBoardModel:

public BoardPanel(PostBoardModel boardModel) {
	...

	// Construct the add message controller and add it to the submit button
	btnSubmit.addActionListener(new AddMessageController(lstBoardModel, this));

	...
}

Some code in the constructor is not included, but the important part where we construct the AddMessageController is shown. The AddMessageController constructor requires the PostBoardModel which is provided, as well as a reference to the view containing the new message field, which is represented by "this".

Finishing Up

At this point your PostBoard module should be fully operational. Feel free to play around and add functionality. Try extending the module to support displaying a user's name next to each message that is posted.

The most recent code for the module can be found in the WPISuite repository, on the dev-postboard branch.

⚠️ **GitHub.com Fallback** ⚠️