6.170 Laboratory in Software Engineering
Spring 2000
Problem Set 2: Using Abstract Data Types
Due: February 17, 2000
Handout 6

Standard Guidelines

To do this assignment as efficiently as possible, and to derive the most benefit, we recommend that you: To hand your solution in, follow the standard instructions, which state in part:
Each assignment should be handed in as hardcopy to your TA or to the course secretary (Kincade Dunn, NE43-529) by 4:30 PM on its due date. It is usually most convenient to give the assignment to your TA at the end of class on that day. Additionally, the source and compiled code for each problem set should be made available on Athena so your TA can test your program.
You should put your solution to this problem set in ~/6.170/ps2.
See the general information sheet (handout 1) for more detail

Purpose

This problem set concerns implementing procedural abstractions and using them to build a larger system.  It will  help you gain familiarity with specifications, using data abstractions, exceptions, and textual I/O.  The problem set also concerns testing: you will be required to test all modules you implement and to hand in your tests together with a justification of why your set of test cases is sufficient.

The abstractions you implement will need to be robust against errors.  You will be required to develop specifications and implementations that take this into account.

Background

In Problem Set 1, you wrote methods to reverse, compress and compress strings. In this problem set, you will modify those methods as well as write new methods to encrypt and decrypt strings. You will also write methods to do file I/O. You will then use these methods in an application that has a user interface, allows the user to manipulate files, and is robust with respect to errors.
 

Problem 1: File I/O

In this problem you will implement methods to read and write strings from files.
Note that we do not write "throws NullPointerException" in method signatures, nor do we include NullPointerException in method signatures.  In this way, we follow general Java conventions but differ from the convention described in Program Development in Java.  For all code you write this term, you should follow the general conventions and not include NullPointerException in signatures or specifications.
We suggest you use the FileReader and FileWriter classes in the standard Java package java.io. Some resources you may find useful are: a brief introduction to files and streams in section 2.7 of the class text, Chapter 8 of Beginning Java 2, and a small online tutorial at http://java.sun.com/docs/books/tutorial/essential/io/filestreams.html.

Problem 2: Encrypt/Decrypt

In this problem you will write methods to encrypt and decrypt strings. Encryption (decryption) is accomplished by reversing a string, and then applying an encode (decode) function that maps one character to another character.  As an example, consider the following encode function f:
 
 x  f(x)
a w
b v
c u
d t
e s
such that f(a) = w, f(b) = v and so on. When f is applied to the characters in "ace", we get back its encoded form "wus".  (The encrypted form of "ace", "suw", is formed by first reversing the letters of "ace", giving "eca", then applying f.) The decode function is the inverse of the encode function. Using the example above, if g is the the inverse function of f, then g(w) = a, g(v) = b, and so on.

To help you implement these functions, we have provided the MapCC class. A MapCC is a collection of key-value pairs, where each key and each value is a character. The specification for MapCC is in the appendix. To use MapCC, make sure that your CLASSPATH is set as described in Handout 1.

Encode functions can be represented as strings.  The string representation of a function f is a sequence of 4-character units. Each unit contains one character of cleartext (x), a space, one character of ciphertext (f(x)), and a newline ('\n').  In addition, we impose three semantic constraints on string representations of functions:

For example, the encoding function shown above can be represented as "a w\nb v\nc u\nd t\ne s\n" ('\n' represents newline).  On the other hand, "a z\nb z\n" is not a legal representation because it is not invertible.  Nor is "a 1\n2 b\n" legal, as it contains digits in the range of the function.  "a \n\n\n a\n" (swaps 'a' and newlines) may or may not be legal depending on your implementation.
 
It's a little bit tricky to make a file without a trailing newline ('\n'), with the Emacs editor.  By default, Emacs ensures that all files have a trailing newline. Therefore if you write a test input file to be encrypted, and your encryption map does not have '\n' in the domain, you may be surprised by the behavior.  You can generate files without a trailing newline in Emacs by adding 
(setq require-final-newline nil)
to your .emacs file.  After doing so, you will have to restart Emacs for the change to take effect.

Here is your task:

  • Complete the following specifications so that the methods are robust; that is, they should not fail unexpectedly, but should either complete their task or throw an appropriate exception.
  • Problem 3: Making Components Robust

    In this problem you will make the compression methods that your wrote in Problem Set 1 (compress, and uncompress) more robust. That is, you should eliminate the requires clause from the specifications for each of these methods by having the methods throw exceptions in case of errors.

    Problem 4: Application Driver

    In this problem you will write a method to perform a combination of transforms on files.
    Do not change the signature or specification of doWork.  Do not throw any other exceptions other than NotPossibleException.  We expect that NotPossibleException is thrown if and only if there is an error.  (Of course you will have to figure out exactly what "error" means.)  Also note that no output file should be written if NotPossibleException is thrown.  If you change the interface to doWork, we may not be able to test and grade your code! 

    Remember to handle any exceptions that are thrown by the methods you call.  For example, if doWork calls readStringFromFile, and readStringFromFile throws BadFilenameException, you will want to catch that and throw NotPossibleException instead.  Also remember to close files you've opened.

    Problem 5: User Interface

    In this problem, you will create a simple text-mode user interface that accepts user input and uses it to build a WorkDescriptor object that can be passed to the doWork method that you implemented for Problem 4.

    The command-line interface prompts the user for commands, the names of the files containing the input and translation table, and the name of the file where the output from the operations on the input should go. Only the first character of each user input to the interface matters.  Here is an example of interaction with a sample interface, with user input in bold:

            command? [(t)ransform/(q)uit] transform
            input filename? foo.txt
            output filename? bar.txt
            compression? [(c)ompress/(u)ncompress/(n)o] compress
            encryption? [(e)ncrypt/(d)ecrypt/(n)o] decrypt
            translation filename? ttable.txt
            ... done
    
            command? [(t)ransform/(q)uit] trans
            input filename? foo.txt
            output filename? bar.txt
            compression? [(c)ompress/(u)ncompress/(n)o] c
            encryption? [(e)ncrypt/(d)ecrypt/(n)o] no
            ... done
    
            command? [(t)ransform/(q)uit] quit
            goodbye
    Note that if neither encryption nor decryption is requested, there is no prompt for the translation filename.
     
    You will want to use Java's System.in to query the user. The method System.in.read does not return characters, so it is more convenient to convert the input stream to a BufferedReader. This code fragment queries the user twice and displays the results of the queries: 
        BufferedReader In = new BufferedReader(new InputStreamReader(System.in));
    
        System.out.print("Input1? ");
        String input1 = In.readLine();
    
        System.out.print("Input2? ");
        String input2 = In.readLine();
    
        System.out.println("Input1 = " + input1);
        System.out.println("Input2 = " + input2);

    The main challenge of this problem is to make the application robust. The application should behave predictably in all cases (bad input from the user, corrupt files, I/O errors, etc.) If an error occurs, the application should display a useful error message. For example, if asked to decompress a file containing "xy44", the application should report that the file format is corrupt, rather than generating an output file containing "xy4444".

    We are not requiring you to write or hand in test cases for this problem. However, we strongly recommend that you do test your application carefully because we will be running our own test cases, and your code will be graded on its robustness.

    Appendix: MapCC Specification

       /* Overview: A MapCC is a set of key-value pairs, where the key
          and value are chars.  The first entry of each pair is the KEY
          and the second entry is the VALUE of the mapping relationship.  
          Each KEY can occur at most once in the map.  MapCCs are mutable.
          A typical MapCC is {<k1, v1>,...,<kn, vn>} 
          such that ki = kj => i = j. */
    
       public MapCC() 
         // effects:  Initializes this to be a new, empty map
    
       public void addPair(char key, char value) throws DuplicateException
         /* modifies: this
          * effects:  Inserts the pair (<key>, <value>) into the table.
          *           Throws DuplicateException if there is already a
          *           pair in this with key <key>.
          */
    
       public char lookup(char key) throws NotFoundException
         /* effects:  Throws NotFoundException if there is no pair in this
          *           with key <key>, else returns the value associated with <key>.
          */
    
       public void deletePair(char key) throws NotFoundException
         /* modifies: this
          * effects:  removes the pair (<key>, c) from the table.
          *           Throws NotFoundException if there is no pair
          *           in this with key <key>.
          */
    
       public int size()
         // effects:  Returns the number of pairs in the table
    
       public boolean isMember(char k)
         // effects:  Returns true iff k is a key in this.
    Here is an example of using a MapCC:
    public MapCC map = new MapCC();
    map.addPair('a', 'z');
    System.out.println("SIZE: " + map.size());       // PRINTS     SIZE: 1
    char c = map.lookup('a');
    System.out.println("KEY a GIVES VALUE " + c);   // PRINTS     KEY a GIVES VALUE z
    map.deletePair('a');
    System.out.println("Size: " + map.size);       // PRINTS     SIZE: 0