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