CMSC 433 - Project 3 - Java Threads
 Due Oct. 26, 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.

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 are of type char, and the filter is responsible for conversion to other primitive data types as specified.

Here are the interfaces for the 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 also consists of two kinds of filters:  translate and stringMatchtranslate reads characters from its input stream and transforms each input character according to a translation map (implemented as a two-dimensional array of characters), writing the resulting character to its output stream.  If no entry is found in the translation map, the character is passed through unchanged.  The translation map is passed in as an argument to the constructor for the Runnable computation object .  stringMatch is constructed with a String as an argument to its Runnable computation object, which is the string it is matching against.  Conceptually, stringMatch reads from its input stream one line at a time (demarcated by newline characters).  For each line it determines whether the argument is a substring of the line.  If the argument was found, stringMatch writes the entire line to its output stream.  A sample driver program is given in Text.java .

Web Accessibility