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