Programming Assignment 4
Wrapper Classes, Generating Scalable Vector Graphics
Example Implementation
/**
* Implementation of Assignment 4: Generating Scalable Vector Graphics
*
* @author Cheng, Jade
* @assignment CSCI 2912 Assignment 4
* @date April 15, 2012
*/
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.NoSuchElementException;
import java.util.Scanner;
/**
* Implementation of Assignment 4: Generating Scalable Vector Graphics
*/
public final class ChengJade4 {
/**
* The main entry point of the program.
*
* @param args The command-line arguments.
*/
public static void main(final String[] args) {
assert null != args;
// Check if the wrong number of arguments are specified on the
// command line.
if (args.length != 1) {
System.err.println("Invalid command-line arguments.");
return;
}
// Use a parser, which is a wrapper class for a scanner, to process
// the file contents.
try {
final Parser parser = new Parser(args[0]);
// Always close the parser after reading.
try {
// Read the "svg" token; throw an exception if it is invalid.
final String svgToken = parser.next();
if (!svgToken.equalsIgnoreCase("svg"))
throw new ParsingException(svgToken);
// Construct an Svg object; the constructor will read the width
// and height of the graphic.
final Svg svg = new Svg(parser);
// Read shapes until the end of the file.
while (parser.hasNext()) {
// Read the command token for the shape.
final String commandToken = parser.next();
Shape shape;
// Construct a Line if "line" is the command token.
if (commandToken.equalsIgnoreCase("line"))
shape = new Line(parser);
// Construct a Rectangle if "rect" is the command token.
else if (commandToken.equalsIgnoreCase("rect"))
shape = new Rectangle(parser);
// Construct a Circle if "circle" is the command token.
else if (commandToken.equalsIgnoreCase("circle"))
shape = new Circle(parser);
// Otherwise, throw an exception for the invalid command.
else
throw new ParsingException(commandToken);
// Add the concrete shape to the Svg instance.
svg.addShape(shape);
// Read styles until the "end" token is encountered.
while (true) {
// Read a first token.
final String first = parser.next();
// If the first token is "end", there are no more
// styles. Break out of this loop and continue to read
// the next shape.
if (first.equalsIgnoreCase("end"))
break;
// Otherwise read a second token.
final String second = parser.next();
// Add the first and second tokens as a key-value pair
// to the style list of the shape that was previously
// processed and added to the Svg instance.
shape.addStyle(first, second);
}
}
// Render the Svg instance to Standard Output.
svg.render(System.out);
} finally {
parser.close();
}
} catch (final ParsingException e) {
// If a parsing error is encountered, display an appropriate error
// message before terminating.
System.err.println("Invalid token: '" + e.getMessage() + "'.");
} catch (final IOException e) {
// Display an appropriate error message if any other I/O error is
// encountered.
System.err.println("Failed to read input file: " + e.getMessage());
}
}
}
/**
* An immutable class that contains SVG information.
*/
final class Svg {
/** The height of the graphic. */
private final double height;
/** The width of the graphic. */
private final double width;
/** A list of {@link Shape} instances. */
private final ArrayList<Shape> shapes = new ArrayList<Shape>();
/**
* Initializes a new instance of the {@link Svg} class.
*
* @param parser The object that parses the input file.
*
* @throws IOException Thrown if an error occurs while parsing the file.
*/
public Svg(final Parser parser) throws IOException {
assert null != parser;
// Parse the attributes in the appropriate order.
this.width = parser.nextPositiveDouble();
this.height = parser.nextPositiveDouble();
}
/**
* Adds a {@link Shape} to this instance.
*
* @param shape The {@link Shape} to add to this instance.
*/
void addShape(final Shape shape) {
assert null != shape;
this.shapes.add(shape);
}
/**
* Renders the contents of this instance in standard SVG format to the
* specified {@link PrintStream}.
*
* @param out The {@link PrintStream} that receives the SVG output.
*/
void render(final PrintStream out) {
assert null != out;
// Renders the root SVG element and attributes; e.g.,
// <svg width='300' height='300'>.
out.print("<svg ");
out.print("width='" + this.width + "' ");
out.print("height='" + this.height + "'>");
out.println();
// Renders each shape element and its attributes; e.g.,
// <rect x='5.0' y='5.0' width='290.0' height='290.0'
// style='fill:#f8f8f8;'/>.
for (final Shape shape : this.shapes)
shape.render(out);
// Closes the root SVG element; e.g., "</svg>".
out.println("</svg>");
}
}
/**
* An abstract {@link Shape} class. This class provides common functionality
* for the {@Circle}, {@link Line}, and {@link Rectangle} classes.
*/
abstract class Shape {
/** The name of the shape element; e.g., "circle", "line", or "rect". */
private final String name;
/** A list of styles; e.g., "fill:#f8f8f8". */
private final ArrayList<String> styles = new ArrayList<String>();
/**
* Initializes a new instance of the {@link Shape} class.
*
* @param name The name of the element; e.g., "circle", "line", or "rect".
*/
protected Shape(final String name) {
assert null != name;
assert !name.isEmpty();
this.name = name;
}
/**
* Adds a key-value pair as a style to the style list.
*
* @param key The key of a style; e.g., "fill".
* @param value The value of a style; e.g., "#f8f8f8".
*/
public void addStyle(final String key, final String value) {
assert null != key;
assert !key.isEmpty();
assert null != value;
assert !value.isEmpty();
// Add to the style list the key-value pair, separated by ':'.
this.styles.add(key + ":" + value);
}
/**
* Renders the contents of this instance in standard SVG format to the
* specified {@link PrintStream}.
*
* @param out The {@link PrintStream} that receives the SVG output.
*/
void render(final PrintStream out) {
assert null != out;
// First, render the name of the shape, e.g., "<rect".
out.print("<" + this.name + " ");
// Second, render the attributes of the shape,
// e.g., "x='5.0' y='5.0' width='290.0' height='290.0'".
this.renderAttributes(out);
// Third, render the style list of the shape,
// e.g., "style='fill:#f8f8f8;'".
out.print(" style='");
for (final String style : this.styles)
out.print(style + ";");
// Finally, render the close tag, e.g, "/>".
out.println("'/>");
}
/**
* Renders the attributes of this instance in standard SVG format to the
* specified {@link PrintStream}.
*
* @param out The {@link PrintStream} that receives the SVG output.
*/
abstract void renderAttributes(final PrintStream out);
}
/**
* An immutable class derived from {@link Shape} that represents a circle.
*/
final class Circle extends Shape {
/** The x coordinate of the center point. */
private final double cx;
/** The y coordinate of the center point. */
private final double cy;
/** The radius of circle. */
private final double r;
/**
* Initializes a new instance of the {@link Circle} class.
*
* @param parser The object that parses the input file.
*
* @throws IOException Thrown if an error occurs while parsing the file.
*/
public Circle(final Parser parser) throws IOException {
// Initialize the base class with element name "circle".
super("circle");
assert null != parser;
// Parse the attributes in the appropriate order.
this.cx = parser.nextDouble();
this.cy = parser.nextDouble();
this.r = parser.nextPositiveDouble();
}
@Override
void renderAttributes(final PrintStream out) {
assert null != out;
out.print("cx='" + this.cx + "' ");
out.print("cy='" + this.cy + "' ");
out.print("r='" + this.r + "'");
}
}
/**
* An immutable class derived from {@link Shape} that represents a line.
*/
final class Line extends Shape {
/** The x coordinate of the first point. */
private final double x1;
/** The x coordinate of the second point. */
private final double x2;
/** The y coordinate of the first point. */
private final double y1;
/** The y coordinate of the second point. */
private final double y2;
/**
* Initializes a new instance of the {@link Line} class.
*
* @param parser The object that parses the input file.
*
* @throws IOException Thrown if an error occurs while parsing the file.
*/
public Line(final Parser parser) throws IOException {
// Initialize the base class with element name "line".
super("line");
assert null != parser;
// Parse the attributes in the appropriate order.
this.x1 = parser.nextDouble();
this.y1 = parser.nextDouble();
this.x2 = parser.nextDouble();
this.y2 = parser.nextDouble();
}
@Override
void renderAttributes(final PrintStream out) {
assert null != out;
out.print("x1='" + this.x1 + "' ");
out.print("y1='" + this.y1 + "' ");
out.print("x2='" + this.x2 + "' ");
out.print("y2='" + this.y2 + "'");
}
}
/**
* An immutable class derived from {@link Shape} that represents a rectangle.
*/
final class Rectangle extends Shape {
/** The height of the rectangle. */
private final double height;
/** The width of the rectangle. */
private final double width;
/** The x coordinate of the top left corner. */
private final double x;
/** The y coordinate of the top left corner. */
private final double y;
/**
* Initializes a new instance of the {@link Rectangle} class.
*
* @param parser The object that parses the input file.
*
* @throws IOException Thrown if an error occurs while parsing the file.
*/
public Rectangle(final Parser parser) throws IOException {
// Initialize the base class with element name "rect".
super("rect");
assert null != parser;
// Parse the attributes in the appropriate order.
this.x = parser.nextDouble();
this.y = parser.nextDouble();
this.width = parser.nextPositiveDouble();
this.height = parser.nextPositiveDouble();
}
@Override
void renderAttributes(final PrintStream out) {
assert null != out;
out.print("x='" + this.x + "' ");
out.print("y='" + this.y + "' ");
out.print("width='" + this.width + "' ");
out.print("height='" + this.height + "'");
}
}
/**
* A wrapper class for a Scanner that simplifies error handling for parsing and
* other potential I/O errors.
*/
final class Parser implements Closeable {
/** A scanner wrapped by this instance and used to parse file contents. */
private final Scanner scanner;
/**
* Initializes a new instance of the {@link Parser} class.
*
* @param path The path to the file that will be parsed.
*
* @throws FileNotFoundException Thrown if the specified file is not found.
*/
public Parser(final String path) throws FileNotFoundException {
assert null != path;
assert !path.isEmpty();
final File file = new File(path);
this.scanner = new Scanner(file);
}
@Override
public void close() {
this.scanner.close();
}
/**
* Returns <code>true</code> if there are more tokens to read from the
* file; otherwise, <code>false</code>.
*
* @return The value <code>true</code> if there are more tokens to read
* from the file; otherwise, <code>false</code>.
*/
public boolean hasNext() {
return this.scanner.hasNext();
}
/**
* Returns the next String token from the file.
*
* @return The next String token from the file.
*
* @throws IOException Thrown if there are no more tokens to read.
*/
String next() throws IOException {
try {
return this.scanner.next();
} catch (final NoSuchElementException e) {
throw new EOFException("Unexpected end of file.");
}
}
/**
* Returns the next value parsed from the file.
*
* @return The next double value parsed from the file.
*
* @throws IOException Thrown if a parsing error occurs.
*/
double nextDouble() throws IOException {
return this.nextDouble(false);
}
/**
* Returns the next double value parsed from the file. This method ensures
* the value is positive.
*
* @return The next double value parsed from the file.
*
* @throws IOException Thrown if a parsing error occurs or if the value is
* not positive.
*/
double nextPositiveDouble() throws IOException {
return this.nextDouble(true);
}
/**
* A helper method that reads a double value and, if necessary, ensures the
* parsed value is positive.
*
* @param requirePositive The value <code>true</code> if the method should
* ensure the parsed value is positive; otherwise <code>false</code>.
*
* @return The next double value parsed from the file.
*
* @throws IOException Thrown if a parsing error occurs or if the positive
* condition fails.
*/
private double nextDouble(final boolean requirePositive) throws IOException {
// First read the value as a string.
final String token = this.next();
try {
// Then parse the value as a double.
final double value = Double.parseDouble(token);
// Do not allow infinite or NaN (not-a-number) values.
if (Double.isInfinite(value) || Double.isNaN(value))
throw new ParsingException(token);
// Do not allow values less than or equal to zero if specified.
if (requirePositive && value <= 0.0)
throw new ParsingException(token);
// The value is valid; return it.
return value;
} catch (final NumberFormatException e) {
throw new ParsingException(token);
}
}
}
/**
* A custom exception class thrown when a parsing error has occured.
*/
class ParsingException extends IOException {
/** A serialization ID (not used). */
private static final long serialVersionUID = 1L;
/**
* Initializes a new instance of the {@link ParsingException}.
*
* @param token The invalid token parsing from the file.
*/
public ParsingException(final String token) {
super(token);
assert null != token;
assert !token.isEmpty();
}
}