Showing posts with label xml-encoder. Show all posts
Showing posts with label xml-encoder. Show all posts

Saturday, March 12, 2011

XMLEncoder and XMLDecoder

We will look at two approaches to representing data from Java programs in XML format. One approach is to design a custom XML language for the specific data structures that you want to represent. We will consider this approach in the next subsection. First, we'll look at an easy way to store data in XML files and to read those files back into a program. The technique uses the classes XMLEncoder and XMLDecoder. These classes are defined in the package java.beans. An XMLEncoder can be used to write objects to an OutputStream in XML form. An XMLDecoder can be used to read the output of an XMLEncoder and reconstruct the objects that were written by it. XMLEncoder and XMLDecoder have much the same functionality as ObjectOutputStream and ObjectInputStream and are used in much the same way. In fact, you don't even have to know anything about XML to use them. However, you do need to know a little about Java beans.

XMLEncoder and XMLDecoder can't be used with arbitrary objects; they can only be used with beans. When an XMLEncoder writes an object, it uses the "get" methods of that object to find out what information needs to be saved. When an XMLDecoder reconstructs an object, it creates the object using the constructor with no parameters and it uses "set" methods to restore the object's state to the values that were saved by the XMLEncoder. (Some standard java classes are processed using additional techniques. For example, a different constructor might be used, and other methods might be used to inspect and restore the state.)

Suppose that we want to use XMLEncoder and XMLDecoder to create and read files in that program. Part of the data for a SimplePaint sketch is stored in objects of type CurveData, defined as:

private static class CurveData {
Color color; // The color of the curve.
boolean symmetric; // Are reflections also drawn?
ArrayList<Point> points; // The points on the curve.
}



To use such objects with XMLEncoder and XMLDecoder, we have to modify this class so that it follows the Java bean pattern. The class has to be public, and we need get and set methods for each instance variable. This gives:


public static class CurveData {
private Color color; // The color of the curve.
private boolean symmetric; // Are reflections also drawn?
private ArrayList<Point> points; // The points on the curve.
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
public ArrayList<Point> getPoints() {
return points;
}
public void setPoints(ArrayList<Point> points) {
this.points = points;
}
public boolean isSymmetric() {
return symmetric;
}
public void setSymmetric(boolean symmetric) {
this.symmetric = symmetric;
}
}



I didn't really need to make the instance variables private, but bean properties are usually private and are accessed only through their get and set methods.

At this point, we might define another bean class, SketchData, to hold all the necessary data for representing the user's picture. If we did that, we could write the data to a file with a single output statement. In my program, however, I decided to write the data in several pieces.

An XMLEncoder can be constructed to write to any output stream. The output stream is specified in the encoder's constructor. For example, to create an encoder for writing to a file:

XMLEncoder encoder; 
try {
FileOutputStream stream = new FileOutputStream(selectedFile);
encoder = new XMLEncoder( stream );
.
.


Once an encoder has been created, its writeObject() method is used to write objects, coded into XML form, to the stream. In the SimplePaint program, I save the background color, the number of curves in the picture, and the data for each curve. The curve data are stored in a list of type ArrayList<CurveData> named curves. So, a complete representation of the user's picture can be created with:

encoder.writeObject(getBackground());
encoder.writeObject(new Integer(curves.size()));
for (CurveData c : curves)
encoder.writeObject(c);
encoder.close();



When reading the data back into the program, an XMLDecoder is created to read from an input file stream. The objects are then read, using the decoder's readObject() method, in the same order in which they were written. Since the return type of readObject() is Object, the returned values must be type-cast to their correct type:

Color bgColor = (Color)decoder.readObject();
Integer curveCt = (Integer)decoder.readObject();
ArrayList<CurveData> newCurves = new ArrayList<CurveData>();
for (int i = 0; i < curveCt; i++) {
CurveData c = (CurveData)decoder.readObject();
newCurves.add(c);
}
decoder.close();
curves = newCurves; // Replace the program's data with data from the file.
setBackground(bgColor);
repaint();


The XML format used by XMLEncoder and XMLDecoder is more robust than the binary format used for object streams and is more appropriate for long-term storage of objects in files.

Full program

SimplePaintWithXMLEncoder.java

import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;

import javax.imageio.ImageIO;
import javax.swing.*;

import java.io.*;
import java.util.ArrayList;


/**
 * SimplePaintWithXMLEncoder is a drawing program in which the user can
 * sketch curves.  The user's work can be saved to a file which can later
 * be reopened and edited.  It is also possible to save the user's
 * picture as an image file.
 * 
 * This version of the program saves the user's work in XML format,
 * using the XMLEncoder class to output the data and XMLDecoder to
 * input it.
 */
public class SimplePaintWithXMLEncoder extends JFrame {
   
   /**
    * main routine creates a frame of type SimplePaintWithXMLEncoder
    * and makes it visible on the screen.
    */
   public static void main(String[] args) {
      JFrame window = new SimplePaintWithXMLEncoder();
      window.setVisible(true);
   }
      
   
   /**
    * Constructor creates a window with a 600-by-600 pixel drawing area and
    * sets its location so that it is centered on the screen.  The window is
    * not resizable.  It is not made visible by this constructor.
    */
   public SimplePaintWithXMLEncoder() {
      super("SimplePaint: Untitled");
      SimplePaintPanel content = new SimplePaintPanel();
      setContentPane(content);
      setJMenuBar(content.createMenuBar());
      pack();
      Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
      setLocation( (screenSize.width - getWidth())/2 , (screenSize.height - getHeight())/2 );
      setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      setResizable(false);  
   }
   
   
   
   /**
    * An object of type CurveData represents the data required to redraw one
    * of the curves that have been sketched by the user.  This class has been
    * made public and get and set methods have been defined for its instance
    * variables in order to make it possible for objects of type CurveData
    * to be be written by an XMLEncoder and read by an XMLDecoder.
    */
   public static class CurveData {
      private Color color;  // The color of the curve.
      private boolean symmetric;  // Are reflections also drawn?
      private ArrayList<Point> points;  // The points on the curve.
      public Color getColor() {
         return color;
      }
      public void setColor(Color color) {
         this.color = color;
      }
      public ArrayList<Point> getPoints() {
         return points;
      }
      public void setPoints(ArrayList<Point> points) {
         this.points = points;
      }
      public boolean isSymmetric() {
         return symmetric;
      }
      public void setSymmetric(boolean symmetric) {
         this.symmetric = symmetric;
      }
   }
   
   
   /**
    * An object of type SimplePaintPanel is used as the drawing area in
    * the window.  This class does all the work of the program.
    */
   private class SimplePaintPanel extends JPanel {

      private ArrayList<CurveData> curves;  // A list of all curves in the picture.

      private Color currentColor;   // When a curve is created, its color is taken
                                    //     from this variable.  The value is changed
                                    //     using commands in the "Color" menu.

      private boolean useSymmetry;  // When a curve is created, its "symmetric"
                                    // property is copied from this variable.  Its
                                    // value is set by the "Use Symmetry" command in
                                    // the "Control" menu.

      private File editFile;        // The file that is being edited, if any.
                                    // This is set when the user opens a file.
                                    // The name of the file appears in the
                                    // window's title bar.
      
      private JFileChooser fileDialog;   // The dialog box for all open/save commands.


      /**
       * Constructor.  Sets background color to white, adds a gray border, sets up
       * a listener for mouse and mouse motion events, and sets the preferred size
       * of the panel to be 600-by-600.
       */
      public SimplePaintPanel() {
         curves = new ArrayList<CurveData>();
         currentColor = Color.BLACK;
         setBackground(Color.WHITE);
         setBorder(BorderFactory.createLineBorder(Color.GRAY, 2));
         MouseHandler listener = new MouseHandler();
         addMouseListener(listener);
         addMouseMotionListener(listener);
         setPreferredSize( new Dimension(600,600) );
      }


      /**
       * This class defines the object that is used as a mouse listener and mouse
       * motion listener on this panel.  When the user presses the mouse, a new
       * CurveData object is created and is added to the ArrayList, curves. The
       * color of the curve is copied from currentColor, and the symmetric property
       * of the curve is copied from useSymmetry.  As the user drags the mouse, points
       * are added to the curve.  If the user doesn't move the mouse, there will only
       * be one point in the list of points; since this is not really a curve, the
       * CurveData objects is removed in this case from the curves list in the
       * mouseReleased method.
       */
      private class MouseHandler implements MouseListener, MouseMotionListener {
         CurveData currentCurve;
         boolean dragging;
         public void mousePressed(MouseEvent evt) {
            if (dragging)
               return;
            dragging = true;
            currentCurve = new CurveData();
            currentCurve.color = currentColor;
            currentCurve.symmetric = useSymmetry;
            currentCurve.points = new ArrayList<Point>();
            currentCurve.points.add( new Point(evt.getX(), evt.getY()) );
            curves.add(currentCurve);
         }
         public void mouseDragged(MouseEvent evt) {
            if (!dragging)
               return;
            currentCurve.points.add( new Point(evt.getX(),evt.getY()) );
            repaint();  // redraw panel with newly added point.
         }
         public void mouseReleased(MouseEvent evt) {
            if (!dragging)
               return;
            dragging = false;
            if (currentCurve.points.size() < 2)
               curves.remove(currentCurve);
            currentCurve = null;
         }
         public void mouseClicked(MouseEvent evt) { }
         public void mouseEntered(MouseEvent evt) { }
         public void mouseExited(MouseEvent evt) { }
         public void mouseMoved(MouseEvent evt) { }
      } // end nested class MouseHandler


      /**
       * Fills the panel with the current background color and draws all the
       * curves that have been sketched by the user.
       */
      public void paintComponent(Graphics g) {
         super.paintComponent(g);
         for ( CurveData curve : curves) {
            g.setColor(curve.color);
            for (int i = 1; i < curve.points.size(); i++) {
               // Draw a line segment from point number i-1 to point number i.
               int x1 = curve.points.get(i-1).x;
               int y1 = curve.points.get(i-1).y;
               int x2 = curve.points.get(i).x;
               int y2 = curve.points.get(i).y;
               g.drawLine(x1,y1,x2,y2);
               if (curve.symmetric) {
                  // Also draw the horizontal and vertical reflections
                  // of the line segment.
                  int w = getWidth();
                  int h = getHeight();
                  g.drawLine(w-x1,y1,w-x2,y2);
                  g.drawLine(x1,h-y1,x2,h-y2);
                  g.drawLine(w-x1,h-y1,w-x2,h-y2);
               }
            }
         }
      } // end paintComponent()


      /**
       * Creates a menu bar for use with this panel.  It contains
       * four menus: "File, "Control", "Color", and "BackgroundColor".
       */
      public JMenuBar createMenuBar() {

         /* Create the menu bar object */

         JMenuBar menuBar = new JMenuBar();

         /* Create the menu bar and add them to the menu bar. */

         JMenu fileMenu = new JMenu("File");
         JMenu controlMenu = new JMenu("Control");
         JMenu colorMenu = new JMenu("Color");
         JMenu bgColorMenu = new JMenu("BackgroundColor");
         menuBar.add(fileMenu);
         menuBar.add(controlMenu);
         menuBar.add(colorMenu);
         menuBar.add(bgColorMenu);
         
         /* Add commands to the "File" menu.  It contains New, Open, and
          * Save commands.  It also contains a command for saving the user's
          * picture as a PNG image and a command for quitting the program.
          */
         
         JMenuItem newCommand = new JMenuItem("New");
         fileMenu.add(newCommand);
         newCommand.addActionListener( new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
               curves = new ArrayList<CurveData>();
               setBackground(Color.WHITE);
               useSymmetry = false;
               currentColor = Color.BLACK;
               setTitle("SimplePaint: Untitled");
               editFile = null;
               repaint();
            }
         });
         fileMenu.addSeparator();
         JMenuItem saveXML = new JMenuItem("Save (XML format)...");
         fileMenu.add(saveXML);
         saveXML.addActionListener( new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
               doSaveAsXML();
            }
         });
         JMenuItem openXML = new JMenuItem("Open (XML format)...");
         fileMenu.add(openXML);
         openXML.addActionListener( new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
               doOpenAsXML();
            }
         });
         fileMenu.addSeparator();
         JMenuItem saveImage = new JMenuItem("Save Image...");
         fileMenu.add(saveImage);
         saveImage.addActionListener( new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
               doSaveImage();
            }
         });
         fileMenu.addSeparator();
         JMenuItem quitCommand = new JMenuItem("Quit");
         fileMenu.add(quitCommand);
         quitCommand.addActionListener( new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
               System.exit(0);
            }
         });
         

         /* Add commands to the "Control" menu.  It contains an Undo
          * command that will remove the most recently drawn curve
          * from the list of curves; a "Clear" command that removes
          * all the curves that have been drawn; and a "Use Symmetry"
          * checkbox that determines whether symmetry should be used.
          */

         JMenuItem undo = new JMenuItem("Undo");
         controlMenu.add(undo);
         undo.addActionListener( new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
               if (curves.size() > 0) {
                  curves.remove( curves.size() - 1);
                  repaint();  // Redraw without the curve that has been removed.
               }
            }
         });
         JMenuItem clear = new JMenuItem("Clear");
         controlMenu.add(clear);
         clear.addActionListener( new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
               curves = new ArrayList<CurveData>();
               repaint();  // Redraw with no curves shown.
            }
         });
         JCheckBoxMenuItem sym = new JCheckBoxMenuItem("Use Symmetry");
         controlMenu.add(sym);
         sym.addActionListener( new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
               useSymmetry = ((JCheckBoxMenuItem)evt.getSource()).isSelected();
               // This does not affect the current drawing; it affects
               // curves that are drawn in the future.
            }
         });

         /**
          * Add commands to the "Color" menu.  The menu contains commands for
          * setting the current drawing color.  When the user chooses one of these
          * commands, it has not immediate effect on the drawing.  It justs sets
          * the color that will be used for future drawing.
          */

         colorMenu.add(makeColorMenuItem("Black", Color.BLACK));
         colorMenu.add(makeColorMenuItem("White", Color.WHITE));
         colorMenu.add(makeColorMenuItem("Red", Color.RED));
         colorMenu.add(makeColorMenuItem("Green", Color.GREEN));
         colorMenu.add(makeColorMenuItem("Blue", Color.BLUE));
         colorMenu.add(makeColorMenuItem("Cyan", Color.CYAN));
         colorMenu.add(makeColorMenuItem("Magenta", Color.MAGENTA));
         colorMenu.add(makeColorMenuItem("Yellow", Color.YELLOW));
         JMenuItem customColor = new JMenuItem("Custom...");
         colorMenu.add(customColor);
         customColor.addActionListener( new ActionListener() { 
            // The "Custom..." color command lets the user select the current
            // drawing color using a JColorChoice dialog.
            public void actionPerformed(ActionEvent evt) {
               Color c = JColorChooser.showDialog(SimplePaintPanel.this,
                     "Select Drawing Color", currentColor);
               if (c != null)
                  currentColor = c;
            }
         });

         /**
          * Add commands to the "BackgourndColor" menu.  The menu contains commands
          * for setting the background color of the panel.  When the user chooses
          * one of these commands, the panel is immediately redrawn with the new
          * background color.  Any curves that have been drawn are still there.
          */

         bgColorMenu.add(makeBgColorMenuItem("Black", Color.BLACK));
         bgColorMenu.add(makeBgColorMenuItem("White", Color.WHITE));
         bgColorMenu.add(makeBgColorMenuItem("Red", Color.RED));
         bgColorMenu.add(makeBgColorMenuItem("Green", Color.GREEN));
         bgColorMenu.add(makeBgColorMenuItem("Blue", Color.BLUE));
         bgColorMenu.add(makeBgColorMenuItem("Cyan", Color.CYAN));
         bgColorMenu.add(makeBgColorMenuItem("Magenta", Color.MAGENTA));
         bgColorMenu.add(makeBgColorMenuItem("Yellow", Color.YELLOW));
         JMenuItem customBgColor = new JMenuItem("Custom...");
         bgColorMenu.add(customBgColor);
         customBgColor.addActionListener( new ActionListener() { 
            public void actionPerformed(ActionEvent evt) {
               Color c = JColorChooser.showDialog(SimplePaintPanel.this,
                     "Select Background Color", getBackground());
               if (c != null)
                  setBackground(c);
            }
         });

         /* Return the menu bar that has been constructed. */

         return menuBar;

      } // end createMenuBar


      /**
       * This utility method is used to create a JMenuItem that sets the
       * current drawing color.
       * @param command  the text that will appear in the menu
       * @param color  the drawing color that is selected by this command.  (Note that
       *    this parameter is "final" for a technical reason: This is a requirement for
       *    a local variable that is used in an anonymous inner class.)
       * @return  the JMenuItem that has been created.
       */
      private JMenuItem makeBgColorMenuItem(String command, final Color color) {
         JMenuItem item = new JMenuItem(command);
         item.addActionListener( new ActionListener()  {
            public void actionPerformed(ActionEvent evt) {
               setBackground(color);
            }
         });
         return item;
      }


      /**
       * This utility method is used to create a JMenuItem that sets the
       * background color of the panel.
       * @param command  the text that will appear in the menu
       * @param color  the background color that is selected by this command.
       * @return  the JMenuItem that has been created.
       */
      private JMenuItem makeColorMenuItem(String command, final Color color) {
         JMenuItem item = new JMenuItem(command);
         item.addActionListener( new ActionListener()  {
            public void actionPerformed(ActionEvent evt) {
               currentColor = color;
            }
         });
         return item;
      }
            
   
      /**
       * Save the user's image to a file in XML format. Files created by this method
       * can be read back into the program using the doOpenAsXML() method.  The
       * data is written using an XMLEncoder.
       */
      private void doSaveAsXML() {
         if (fileDialog == null)      
            fileDialog = new JFileChooser(); 
         File selectedFile;  // Initially selected file name in the dialog.
         if (editFile == null)
            selectedFile = new File("sketchDataFromXMLEncoder.xml");
         else
            selectedFile = new File(editFile.getName());
         fileDialog.setSelectedFile(selectedFile); 
         fileDialog.setDialogTitle("Select File to be Saved");
         int option = fileDialog.showSaveDialog(this);
         if (option != JFileChooser.APPROVE_OPTION)
            return;  // User canceled or clicked the dialog's close box.
         selectedFile = fileDialog.getSelectedFile();
         if (selectedFile.exists()) {  // Ask the user whether to replace the file.
            int response = JOptionPane.showConfirmDialog( this,
                  "The file \"" + selectedFile.getName()
                  + "\" already exists.\nDo you want to replace it?", 
                  "Confirm Save",
                  JOptionPane.YES_NO_OPTION, 
                  JOptionPane.WARNING_MESSAGE );
            if (response != JOptionPane.YES_OPTION)
               return;  // User does not want to replace the file.
         }
         XMLEncoder encoder; 
         try {
            FileOutputStream stream = new FileOutputStream(selectedFile); 
            encoder = new XMLEncoder( stream );
         }
         catch (Exception e) {
            JOptionPane.showMessageDialog(this,
               "Sorry, but an error occurred while trying to open the file:\n" + e);
            return;
         }
         try {
            encoder.writeObject(getBackground());
            encoder.writeObject(new Integer(curves.size()));
            for (CurveData c : curves)
               encoder.writeObject(c);
            encoder.close();
            editFile = selectedFile;
            setTitle("SimplePaint: " + editFile.getName());
         }
         catch (Exception e) {
            JOptionPane.showMessageDialog(this,
               "Sorry, but an error occurred while trying to write the text:\n" + e);
         }   
      }
   
      
      /**
       * Read image data from a file into the drawing area.  The format
       * of the file must be the same as that used in the doSaveAsXML()
       * method.  The data is read using an XMLDecoder.
       */
      private void doOpenAsXML() {
         if (fileDialog == null)
            fileDialog = new JFileChooser();
         fileDialog.setDialogTitle("Select File to be Opened");
         fileDialog.setSelectedFile(null);  // No file is initially selected.
         int option = fileDialog.showOpenDialog(this);
         if (option != JFileChooser.APPROVE_OPTION)
            return;  // User canceled or clicked the dialog's close box.
         File selectedFile = fileDialog.getSelectedFile();
         XMLDecoder decoder;
         try {
            FileInputStream stream = new FileInputStream(selectedFile);
            decoder = new XMLDecoder( stream );
         }
         catch (Exception e) {
            JOptionPane.showMessageDialog(this,
                  "Sorry, but an error occurred while trying to open the file:\n" + e);
            return;
         }
         try {
            Color bgColor = (Color)decoder.readObject();
            Integer curveCt = (Integer)decoder.readObject();
            ArrayList<CurveData> newCurves = new ArrayList<CurveData>();
            for (int i = 0; i < curveCt; i++) {
               CurveData c = (CurveData)decoder.readObject();
               newCurves.add(c);
            }
            decoder.close();
            curves = newCurves;
            setBackground(bgColor);
            repaint();
            editFile = selectedFile;
            setTitle("SimplePaint: " + editFile.getName());
         }
         catch (Exception e) {
            JOptionPane.showMessageDialog(this,
                  "Sorry, but an error occurred while trying to read the data:\n" + e);
         }   
      }
      
      
      /**
       * Saves the user's sketch as an image file in PNG format.
       */
      private void doSaveImage() {
         if (fileDialog == null)      
            fileDialog = new JFileChooser(); 
         fileDialog.setSelectedFile(new File("sketch.png")); 
         fileDialog.setDialogTitle("Select File to be Saved");
         int option = fileDialog.showSaveDialog(this);
         if (option != JFileChooser.APPROVE_OPTION)
            return;  // User canceled or clicked the dialog's close box.
         File selectedFile = fileDialog.getSelectedFile();
         if (selectedFile.exists()) {  // Ask the user whether to replace the file.
            int response = JOptionPane.showConfirmDialog( this,
                  "The file \"" + selectedFile.getName()
                  + "\" already exists.\nDo you want to replace it?", 
                  "Confirm Save",
                  JOptionPane.YES_NO_OPTION, 
                  JOptionPane.WARNING_MESSAGE );
            if (response != JOptionPane.YES_OPTION)
               return;  // User does not want to replace the file.
         }
         try {
            BufferedImage image;  // A copy of the sketch will be drawn here.
            image = new BufferedImage(600,600,BufferedImage.TYPE_INT_RGB);
            Graphics g = image.getGraphics();  // For drawing onto the image.
            paintComponent(g);
            g.dispose();
            boolean hasPNG = ImageIO.write(image,"PNG",selectedFile);
            if ( ! hasPNG )
               throw new Exception("PNG format not available.");
         }
         catch (Exception e) {
            JOptionPane.showMessageDialog(this,
               "Sorry, but an error occurred while trying to write the image:\n" + e);
         }   
      }
      
   
   }

}

Chitika