001    package org.rakeshv.xml.album;
002    
003    import java.io.BufferedWriter;
004    import java.io.File;
005    import java.io.FileNotFoundException;
006    import java.io.FileOutputStream;
007    import java.io.FileWriter;
008    import java.io.IOException;
009    import java.io.PrintWriter;
010    
011    import java.util.List;
012    import java.util.Map;
013    import java.util.Iterator;
014    import java.util.TreeMap;
015    
016    import org.jdom.Document;
017    import org.jdom.Element;
018    import org.jdom.JDOMException;
019    import org.jdom.Namespace;
020    import org.jdom.input.SAXBuilder;
021    import org.jdom.output.XMLOutputter;
022    
023    /**
024     * Photo album implementation.  This class presents a java view of the
025     * specified album xml file.  This class also contains methods
026     * for creating new album entries, and for deleted existing
027     * entries.
028     *
029     * <p>You can use the {@link #createPhotoAlbum( String )} method to
030     * create a new <code>photo album</code> specified by the fully 
031     * qualified file name specified in the method call.</p>
032     *
033     * <p>&copy; Copyright 2003 Rakesh Vidyadharan</p>
034     *
035     * @author Rakesh Vidyadharan 2003 October 31
036     * @version $Id: PhotoAlbum.java,v 1.3 2004/10/16 19:17:02 rakesh Exp $
037     */
038    public class PhotoAlbum extends Object
039    {
040      /**
041       * The fully qualified name of the album XML file.
042       */
043      private String fileName = "";
044    
045      /**
046       * The <code>map</code> of {@link Album} records.  This corresponds 
047       * to all the <code>album</code> elements under the 
048       * <code>albums</code> element in the photo album.  The map is
049       * populated using the {@link Album#name} as the <code>key</code>.
050       */
051      private Map albums = null;
052    
053      /**
054       * A reference to the album XML document.
055       */
056      private Document document = null;
057    
058      /**
059       * The <code>root element</code> of the XML file.
060       */
061      private Element rootElement = null;
062    
063      /**
064       * The <code>namespace</code> for the root element.
065       */
066      private Namespace namespace = null;
067    
068      /**
069       * Default constructor.  Cannot be directly called.
070       */
071      protected PhotoAlbum() 
072      {
073        super();
074        albums = new TreeMap();
075      }
076    
077      /**
078       * Just invoke {@link #PhotoAlbum( String, String )} with a value of
079       * <code>null</code> for the preferred SAX Parser to use.
080       *
081       * @param fileName - The fully qualified name of the photo
082       *   album XML file.
083       * @throws AlbumException - If any JDOM/IOExceptions are encountered
084       *   while processing the album XML file.
085       */
086      public PhotoAlbum( String fileName ) throws AlbumException
087      {
088        this( fileName, null );
089      }
090    
091      /**
092       * Load the specified album XML file using the specified
093       * SAX Parser.
094       *
095       * @see #loadAlbums( String )
096       *
097       * @param fileName - The fully qualified name of the photo
098       *   album XML file.
099       * @param saxParser - The full class name of the preferred
100       *   SAX Parser to use.
101       * @throws AlbumException - If any JDOM/IOExceptions are encountered
102       *   while processing the album XML file.
103       */
104      public PhotoAlbum( String fileName, String saxParser ) 
105        throws AlbumException
106      {
107        this.fileName = fileName;
108        loadAlbums( saxParser );
109      }
110    
111      /**
112       * Load the album XML file ({@link #fileName}) using JDOM,
113       * and populate the {@link #albums} class field.
114       *
115       * @see #populateAlbums()
116       *
117       * @param saxParser - The full class name of the SAX Parser
118       *   to use.  If this is <code>null</code> the default JAXP SAX
119       *   Parser will be used.
120       * @throws AlbumException - If any JDOM/IOExceptions are encountered
121       *   while processing the album XML file.
122       */
123      private void loadAlbums( String saxParser ) throws AlbumException
124      {
125        albums = new TreeMap();
126    
127        SAXBuilder builder = null;
128        if ( saxParser == null )
129        {
130          builder = new SAXBuilder();
131        }
132        else
133        {
134          builder = new SAXBuilder( saxParser );
135        }
136    
137        try
138        {
139          document = builder.build( new File( fileName ) );
140        }
141        catch ( Exception ex )
142        {
143          throw new AlbumException( "Error accessing album " + fileName + ".", ex );
144        }
145    
146        Element rootElement = document.getRootElement();
147        namespace = rootElement.getNamespace( rootElement.getNamespacePrefix() );
148    
149        // Populate the list of albums
150        populateAlbums();
151      }
152    
153      /**
154       * Walk through the JDOM tree, and populate the {@link #albums} 
155       * <code>Map</code> of {@link Album} objects.  Fetches all the
156       * <code>children</code> of the {@link #rootElement} root
157       * <code>Element</code> in the album XML file.
158       *
159       * @see #populateAlbum( Element )
160       */
161      private void populateAlbums()
162      {
163        List list = rootElement.getChildren( "album", namespace );
164        for ( Iterator iterator = list.iterator(); iterator.hasNext(); )
165        {
166          Element element = (Element) iterator.next();
167          Album album = populateAlbum( element );
168          albums.put( album.getName(), album );
169        }
170      }
171    
172      /**
173       * Walk through the child elements of the specified <code>album</code>
174       * element, and create a corresponding {@link Album} bean.
175       *
176       * @see #populatePhoto( Element )
177       * @param element - The current album element.
178       * @return Album - The java bean that was created with the child
179       *   elements of the specified album element.
180       */
181      private Album populateAlbum( Element element )
182      {
183        Album album = new Album( element.getAttributeValue( "name" ) );
184    
185        List list = element.getChildren( "photo", namespace );
186        for ( Iterator iterator = list.iterator(); iterator.hasNext(); )
187        {
188          Element elem = (Element) iterator.next();
189          album.addPhoto( populatePhoto( element ) );
190        }
191    
192        return album;
193      }
194    
195      /**
196       * Create a {@link Photo} java bean instance with all the data
197       * from the specified <code>photo</code> element.
198       *
199       * @see #populateImage( Element )
200       * @param element - The current photo element.
201       * @return Photo - The java bean that was created with the child
202       *   elements of the specified photo element.
203       */
204      private Photo populatePhoto( Element element )
205      {
206        String content = "";
207        Photo photo = new Photo( Integer.parseInt( element.getChildTextTrim( "photoId", namespace ) ) );
208    
209        content = element.getChildTextTrim( "title", namespace );
210        if ( content != null )
211        {
212          photo.setTitle( content );
213        }
214    
215        photo.setFullImage( populateImage( element.getChild( "fullImage", namespace ) ) );
216        photo.setThumbnail( populateImage( element.getChild( "thumbnail", namespace ) ) );
217    
218        content = element.getChildTextTrim( "description", namespace );
219        if ( content != null )
220        {
221          photo.setDescription( content );
222        }
223    
224        return photo;
225      }
226    
227      /**
228       * Return a {@link Image} java bean with the data from the specified
229       * <code>Element</code> of type <code>image</code>.
230       *
231       * @param element - The current image element.
232       * @return Album - The java bean that was created with the child
233       *   elements of the specified image element.
234       */
235      private Image populateImage( Element element )
236      {
237        String content = "";
238        Image image = new Image( element.getChildTextTrim( "fileLocation", namespace ) );
239    
240        content = element.getChildTextTrim( "width", namespace );
241        image.setWidth( Double.parseDouble( content ) );
242    
243        content = element.getChildTextTrim( "height", namespace );
244        image.setHeight( Double.parseDouble( content ) );
245    
246        content = element.getChildTextTrim( "altText", namespace );
247        image.setAltText( content );
248    
249        return image;
250      }
251    
252      /**
253       * Add the specified {@link Album} java bean to the {@link #albums}
254       * map.  This method checks to make sure that an entry with the same
255       * {@link Album#name} does not exist in the {@link #albums}
256       * <code>Map</code>.  The specified {@link Album} object is added
257       * both to the {@link #albums} <code>Map</code>, as well as to the
258       * JDOM tree.
259       *
260       * <p><b>Note:</b>This method does not write the modified JDOM tree
261       * back to the album file.  To save your modifications, please
262       * use the {@link #saveAlbums()} method.</p>
263       *
264       * @see #addAlbumToTree( Album )
265       *
266       * @param album - The java bean with all the information
267       *   pertaining to a album entry in the photo album.
268       * @throws AlbumException - If the specified {@link Album} has an
269       *   identical {@link Album#name} as an existing entry in the
270       *   {@link #albums} <code>Map</code>.
271       */
272      public void addAlbum( Album album ) throws AlbumException
273      {
274        // Check for existing album entry with same nick
275        if ( albums.containsKey( album.getName() ) )
276        {
277          throw new AlbumException( "The name specified " + album.getName() + " is already in use." );
278        }
279    
280        // Add the album to the albums map and the JDOM tree
281        albums.put( album.getName(), album );
282        addAlbumToTree( album );
283      }
284    
285      /**
286       * Save the specified {@link Album} java bean that has been modified
287       * to the underlying JDOM tree.  The easiest way to do this is to
288       * remove the existing <code>album element</code>, and add the
289       * modified {@link Album} bean as a new child element of the
290       * <code>albums</code> element using {@link #addAlbumToTree( Album )}.
291       *
292       * <p><b>Note:</b>This method does not write the modified JDOM tree
293       * back to the album file.  To save your modifications, please
294       * use the {@link #saveAlbums()} method.</p>
295       *
296       * @see #addAlbumToTree( Album )
297       * @param album - The java bean with all the information
298       *   pertaining to a album entry in the album.
299       */
300      public void modifyAlbum( Album album )
301      {
302        // Find the appropriate element in the JDOM tree
303        List children = rootElement.getChildren( "album", namespace );
304        boolean elementFound = false;
305        for ( Iterator iterator = children.iterator(); iterator.hasNext(); )
306        {
307          Element element = (Element) iterator.next();
308          if ( element.getChildTextTrim( "name", namespace ).equals( album.getName() ) )
309          {
310            boolean result = rootElement.removeContent( element );
311            addAlbumToTree( album );
312            elementFound = true;
313          }
314    
315          if ( elementFound ) break;
316        }
317      }
318    
319      /**
320       * Add the specified {@link Album} object to the JDOM tree as a 
321       * <code>album</code> element.
322       *
323       * <p><b>Note:</b>This method does not write the modified JDOM tree
324       * back to the album file.  To save your modifications, please
325       * use the {@link #saveAlbums()} method.</p>
326       *
327       * @param album - The java bean with all the information
328       *   pertaining to a album entry in the album.
329       */
330      private void addAlbumToTree( Album album )
331      {
332        // Add the album to the JDOM tree
333        Element newAlbum = new Element( "album", namespace );
334        newAlbum.setAttribute( "name", album.getName() );
335    
336        addPhotoToTree( newAlbum, album );
337    
338        if ( album.getDescription() != null )
339        {
340          Element component = new Element( "description", namespace );
341          component.addContent( album.getDescription() );
342          newAlbum.addContent( component );
343        }
344    
345        rootElement.addContent( newAlbum );
346      }
347    
348      /**
349       * Add all the {@link Photo} beans in the {@link Album} specified
350       * to the <code>Element</code> passed in.  Walk through the {@link
351       * Album#photos} <code>map</code> and add each instance to the
352       * XML tree.
353       *
354       * @param element - The XML element to which the photos are
355       *   to be added.
356       * @param album - The album instance from which the photos
357       *   are to be added.
358       */
359      private void addPhotoToTree( Element element, Album album )
360      {
361        for ( Iterator iterator = album.getPhotos().values().iterator(); iterator.hasNext(); )
362        {
363          Photo photo = (Photo) iterator.next();
364          Image fullImage = photo.getFullImage();
365          Image thumbnail = photo.getThumbnail();
366    
367          Element photoElement = new Element( "photo", namespace );
368    
369          // Add the photoId element
370          Element component = new Element( "photoId", namespace );
371          component.addContent( photo.getPhotoId().toString() );
372          photoElement.addContent( component );
373    
374          // Add the title element
375          component = new Element( "title", namespace );
376          component.addContent( photo.getTitle() );
377          photoElement.addContent( component );
378    
379          // Add the fullImage and thumbnail elements
380          addImageToTree( photoElement, "fullImage", fullImage );
381          addImageToTree( photoElement, "thumbnail", thumbnail );
382          
383          element.addContent( component );
384        }
385      }
386    
387      /**
388       * Add the specified {@link Image} object to the specified element
389       * as an element with the name specified.
390       *
391       * @param element - The parent element to which the image
392       *   element is to be added.
393       * @param name - The name to use for the image element that
394       *   will be added to the parent.
395       * @param image - The image java bean to be used to populate
396       *   the image element children.
397       */
398      private void addImageToTree( Element element, String name, 
399          Image image )
400      {
401        Element imageElement = new Element( name, namespace );
402    
403        // Add the fileLocation element
404        Element component = new Element( "fileLocation", namespace );
405        component.addContent( image.getFileLocation() );
406        imageElement.addContent( component );
407    
408        // Add the width element
409        component = new Element( "width", namespace );
410        component.addContent( String.valueOf( image.getWidth() ) );
411        imageElement.addContent( component );
412    
413        // Add the height element
414        component = new Element( "height", namespace );
415        component.addContent( String.valueOf( image.getHeight() ) );
416        imageElement.addContent( component );
417    
418        // Add the altText element
419        component = new Element( "altText", namespace );
420        component.addContent( image.getAltText() );
421        imageElement.addContent( component );
422    
423        element.addContent( imageElement );
424      }
425    
426      /**
427       * Remove the {@link Album}'s identified by the specified {@link
428       * Album#name}s.  This method removes the {@link Album}'s from 
429       * the {@link #albums} <code>Map</code> as well as the appropriate
430       * <code>element</code> from the underlying JDOM tree.  This method
431       * just iterates through the array, and invokes {@link
432       * #deleteAlbum( String )} to delete each {@link Album} specified.
433       *
434       * @param names - The names of the album that are to 
435       *   be removed.
436       * @return boolean - Returns <code>true</code> if the name exists
437       *   in the {@link #albums} <code>Map</code>, and <code>false</code>
438       *   otherwise.  A return value of <code>false</code> indicates that
439       *   not all the specified albums could be deleted, as not all of
440       *   them were valid.
441       */
442      public boolean deleteAlbums( String[] names )
443      {
444        boolean result = true;
445    
446        for ( int i = 0; i < names.length; ++i )
447        {
448          boolean value = deleteAlbum( names[i] );
449          if ( ! value )
450          {
451            result = value;
452          }
453        }
454    
455        return result;
456      }
457    
458      /**
459       * Remove the {@link Album} identified by the specified {@link
460       * Album#name}.  This method removes the {@link Album} from 
461       * the {@link #albums} <code>Map</code> as well as the appropriate
462       * <code>element</code> from the underlying JDOM tree.
463       *
464       * @param name - The name of the album that is to be removed.
465       * @return boolean - Returns <code>true</code> if the name exists
466       *   in the {@link #albums} <code>Map</code>, and <code>false</code>
467       *   otherwise.
468       */
469      public boolean deleteAlbum( String name )
470      {
471        if ( ! albums.containsKey( name ) )
472        {
473          return false;
474        }
475    
476        // Find the appropriate element in the JDOM tree
477        List children = rootElement.getChildren( "album", namespace );
478        for ( Iterator iterator = children.iterator(); iterator.hasNext(); )
479        {
480          Element element = (Element) iterator.next();
481          if ( element.getChildTextTrim( "name", namespace ).equals( name ) )
482          {
483            boolean result = rootElement.removeContent( element );
484            albums.remove( name );
485    
486            break;
487          }
488        }
489    
490        return true;
491      }
492    
493      /**
494       * Write the JDOM tree back to the album file.
495       *
496       * @throws AlbumException - If any exceptions are encountered while
497       *   serialising the JDOM tree to the album file.
498       */
499      public void saveAlbums() throws AlbumException
500      {
501        FileOutputStream out = null;
502        try
503        {
504          out = new FileOutputStream( fileName );
505        }
506        catch ( FileNotFoundException fnfex )
507        {
508          throw new AlbumException( "The album " + fileName + " could not be found.", fnfex );
509        }
510    
511        XMLOutputter outputter = new XMLOutputter();
512        outputter.getFormat().setLineSeparator( "\n" );
513        outputter.getFormat().setIndent( "  " );
514        try
515        {
516          outputter.output( document, out );
517          out.close();
518        }
519        catch ( IOException ioex )
520        {
521          throw new AlbumException( "Error while writing to album " + fileName + ".", ioex );
522        }
523      }
524    
525      /**
526       * Just invokes {@link #createPhotoAlbum( String, String )} with a
527       * <code>null</code> value for the default SAX Parser.
528       *
529       * @param file - The fully qualified name of the file ie. the
530       *   full path and name of file to use as the album.
531       * @throws AlbumException - If any exceptions are encountered while
532       *   creating or writing to the specified file.
533       */
534      public static PhotoAlbum createPhotoAlbum( String file ) 
535        throws AlbumException
536      {
537        return createPhotoAlbum( file, null );
538      }
539    
540      /**
541       * Create a new album using the fully qualified file name
542       * specified, and using the specified SAX Parser to load the XML
543       * file that has been created.
544       *
545       * @param file - The fully qualified name of the file ie. the
546       *   full path and name of file to use as the album.
547       * @param saxParser - The full class name of the preferred
548       *   SAX Parser.  If this is <code>null</code> the default JAXP
549       *   SAX Parser will be used.
550       * @throws AlbumException - If any exceptions are encountered while
551       *   creating or writing to the specified file.
552       */
553      public static PhotoAlbum createPhotoAlbum( String file, 
554          String saxParser ) throws AlbumException
555      {
556        PhotoAlbum photoAlbum = new PhotoAlbum();
557        photoAlbum.setFileName( file );
558    
559        try
560        {
561          PrintWriter out = new PrintWriter( 
562               new BufferedWriter( new FileWriter( file ) ) 
563          );
564          out.println( "<?xml version='1.0' encoding='UTF-8'?>" );
565          out.println( "<albums xmlns='http://src.rakeshv.org/xml/schema/' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='http://src.rakeshv.org/xml/schema/ http://src.rakeshv.org/xml/schema/album.xsd'>" );
566          out.println( "  <album>" );
567          out.println( "  </album>" );
568          out.println( "</albums>" );
569          out.flush();
570          out.close();
571        }
572        catch ( IOException iex )
573        {
574          throw new AlbumException( "Error creating/writing new album " + file + ".", iex );
575        }
576    
577        photoAlbum.loadAlbums( saxParser );
578    
579        return photoAlbum;
580      }
581      
582      /**
583       * Return the value/reference of the {@link #albums}.
584       *
585       * @return Map - The value/reference of albums.
586       */
587      public final Map getAlbums()
588      {
589        return albums;
590      }
591      
592      /**
593       * Returns {@link #fileName}.
594       *
595       * @return String - The value/reference of/to fileName.
596       */
597      public final String getFileName()
598      {
599        return fileName;
600      }
601      
602      /**
603       * Set {@link #fileName}.
604       *
605       * @param fileName - The value to set.
606       */
607      public final void setFileName( String fileName )
608      {
609        this.fileName = fileName;
610      }
611    }