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();
	}
}