CMSC 433 - Project 3 - Java Threads
 Due March 13, 6:00PM

A software architecture is a set of components connected in standardized ways. For example, you are probably familiar with the idea of a client-server architecture. In that type of architecture there are typically one server component and many client components. The clients are usually connected to the server and communicate in a request/response fashion. That is, the client requests something from the server and the server responds to the client, fulfilling the request.

Your task is to develop a generic software pipe and filter architecture in Java. A pipe and filter architecture is made up of components called filters, connected in a chain. That is, the output of one filter serves as the input to the next filter in the chain. A filter has exactly one input stream and one output stream. Filters may process/transform the data as it flows along the chain. The actual effect of passing data along the chain depends on which filters make up the chain and the order in which they are connected.

To work properly, applications that use this architecture must not connect the filters in cycles (i.e. the directed graph of filters with edges showing the connections from the output of each filter to the input of another filter does not have a cycle). Also, filters must not communicate with each other except through their input and output streams (that includes reading and writing files).

Consider the following example:

C1 - reads a stream of characters, interprets them as integers, computes the running sum of the last 3 integers, and outputs the averages as a stream of characters.

C2 - reads a stream of characters, interprets them as integers, computes the square of each integer, and outputs the squares as a stream of characters.

C3 - reads a stream of characters, interprets them as integers, deletes any value over 1,000,000 , and outputs the remaining integers as a stream of characters.

Connections: C1's input stream is a file and it's output stream is connected to the input stream of C2. C2's output stream is connected to the input stream of C3. The output stream of C3 is System.out.

Notice that since the filters all take character streams as input and have no dependencies on each other, they can be reordered to produce a different effect.

Example 2:

Connections: C2's input stream is a file and it's output stream is connected to the input stream of C1. C1's output stream is connected to the input stream of C3. The output stream of C3 is System.out.

Implementation details

Each filter should consist of three threads, two buffers, one input stream and one output stream. The read thread takes data from the filter's input stream and places it in the input buffer. The compute thread removes data from the input buffer, processes/transforms it, and writes its output to the output buffer. The write thread removes data from the output buffer and writes it to the output stream.  Threads must exit properly, meaning that they should read from a buffer until no more data can appear in the buffer (see FilterBuffer.read()), notify a buffer when there will be no more writes into it (see FilterBuffer.close()), and exit when they can receive no more input data and have produced all their output data.

All threads must respect normal buffer semantics. That is, they must not write to a full buffer, read from an empty buffer, nor lose, corrupt or duplicate data items. You may assume that all data items in buffers are of type char, and the filter is responsible for conversion to other data types as specified.

Here are the interfaces for the Filter and FilterBuffer interfaces, with corresponding FilterImpl and FilterBufferImpl classes, that you must write:

public interface Filter { 
void setConnection(java.io.Reader in, java.io.Writer out);
java.io.Reader getInputConnection();
java.io.Writer getOutputConnection();
FilterBuffer getInputBuffer();
FilterBuffer getOutputBuffer();
void startFilter(); // start computing 
}
public class FilterImpl implements Filter {
FilterImpl(java.io.Reader in, java.io.Writer out, int maxReadBufSize, int maxWriteBufSize, Runnable compute);
}
public interface FilterBuffer { 
char[] read (); // returns current contents of buffer and clears it. Returns a zero-length array if its internal buffer is empty and the FilterBuffer has been closed
void write(char[] str) throws bufTooSmallException; // write str to buffer if str.length is less than maxBufferSize, otherwise throws exception
int getMaxBufferSize(); 
void close(); // informs the FilterBuffer that no more data will be written to it
void flush(); // doesn't return until the buffer is empty
}
public class FilterBufferImpl implements FilterBuffer {
FilterBufferImpl(int maxSize);
}

Applications

You must also write filters for the following applications:

  1. Sieve of Erastothenes
    This application consists of two kinds of filters: generate and sieve. The first, generate, takes a single integer i (greater than 1) on its input stream, and produces all integers from 2 to i inclusive on its output stream (integers are delimited by whitespace in the stream). There will be only one generate filter and its output will be connected to the input of a sieve filter. A sieve filter, call it s1, reads integers from its input stream. s1 writes the first number, p, to its output stream (which will be a prime number). After that s1 writes all numbers not divisible by p to its output stream. If s1, in fact, does write any output besides p, it must instantiate a new sieve filter, call it s2, and attach s2 to the end of the filter chain.  More precisely, the output stream of s1 becomes the output stream of s2, while the output stream of s1 must be attached to the input stream of s2.  The Runnable object that is passed to the sieve filter constructor must also implement a setFilter method to allow it to access methods of the sieve object (this applies to any Runnable object that implements a compute thread).  A sample driver program that instantiates a generate and a sieve filter and connects them properly is given in Erast.java .
  2. Text processing
    This application consists of three kinds of filters:  replace, unique, and tail.  
    1. replace is constructed with two Strings as arguments to its Runnable computation object, the first of which is the string it is matching against, call it s1, and the second the string, call it s2, that will replace all occurrences of s1replace reads characters from its input stream, replaces all occurrences of s1 with s2, and writes the resulting characters to its output stream.  Characters that are not part of a match are passed through unchanged.  
    2. The constructor to the unique Runnable computation object takes no arguments. unique processes its input character stream a line at a time (lines are separated by newline in the input stream).  unique removes duplicate lines that appear consecutively in the input stream, outputting only one copy of a duplicated line to its output stream.  Lines that are not duplicated are passed through unchanged.  
    3. tail is constructed with one integer, call it i, as an argument to its Runnable computation object. tail reads strings from its input stream, separated by whitespace (spaces, tabs and newlines), and interprets each string as a file name (in the local directory where the JVM is running).  tail opens the file and outputs lines in the file (separated by newlines) to the output stream, based on the value of i. If i is positive, tail outputs all lines starting at that line to the output stream (the file starts at line 1, and i = 0 is equivalent to i = 1).  If i is negative, tail outputs the last i lines of the file to the output stream.  As in Project 2, file names are not allowed to contain java.io.File.separatorChar.  In that case, or if the file cannot be opened (e.g., it doesn't exist), nothing is written to the output stream.

    A sample driver program that instantiates a tail, a replace and a unique filter and connects them properly is given in Text.java .

Debugging

We strongly suggest using the logging client and server from Project 1 to keep track of all the interesting events for threads that will be created for the filter applications.  We will be giving further instructions soon about which events must be logged in your code for submission (meaning inserting calls to myLoggingClient.writeRecord(), but the obvious ones are thread start and exit.  We will also be giving you our (presumably working) version of Project 1 to use for testing.

Here are the additional instructions for logging events.  You should create a myLoggingClient object in each filter object, connected to a myLoggingServer whose location is passed in on the command line as in project 1 (IP address and port specified with -D switches to the driver programs Erast.java and Text.java).  For testing, you will also need to start a myLoggingServer on that machine, listening on that port.

Each filter that is created will start three threads (read, write, compute), and should log the thread start and exit events for each of those threads.  Each thread should get a unique ID via a call to myCounter, and use that value as the ID string for the myLoggingClient.writeRecord() call, prepending the string "ThreadType thread " (i.e.., "ThreadType thread i", where ThreadType is one of Read, Write, or Compute, and  i is the value returned by myCounter).  The timestamp field should come from a call to myTimeStamper.  Finally, the event string should be either "Thread started" or "Thread exited".  You can use our logging server/client Java code for project 1, which is located here.

 

Web Accessibility