Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

Source code: com/virtuosotechnologies/asaph/standardmodel/SimpleSongDatabase.java


1   /*
2   ================================================================================
3   
4     FILE:  SimpleSongDatabase.java
5     
6     PROJECT:
7     
8       Asaph
9     
10    CONTENTS:
11    
12      Simple implementation of SongDatabase
13    
14    PROGRAMMERS:
15    
16      Daniel Azuma (DA)  <dazuma@kagi.com>
17    
18    COPYRIGHT:
19    
20      Copyright (C) 2003  Daniel Azuma  (dazuma@kagi.com)
21      
22      This program is free software; you can redistribute it and/or
23      modify it under the terms of the GNU General Public License as
24      published by the Free Software Foundation; either version 2
25      of the License, or (at your option) any later version.
26      
27      This program is distributed in the hope that it will be useful,
28      but WITHOUT ANY WARRANTY; without even the implied warranty of
29      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30      GNU General Public License for more details.
31      
32      You should have received a copy of the GNU General Public
33      License along with this program; if not, write to
34        Free Software Foundation, Inc.
35        59 Temple Place, Suite 330
36        Boston, MA 02111-1307 USA
37  
38  ================================================================================
39  */
40  
41  
42  package com.virtuosotechnologies.asaph.standardmodel;
43  
44  
45  import java.io.IOException;
46  import java.util.Set;
47  import java.util.LinkedHashSet;
48  import java.util.Map;
49  import java.util.LinkedHashMap;
50  import java.util.Iterator;
51  import java.util.Locale;
52  import org.xml.sax.Attributes;
53  import org.xml.sax.SAXException;
54  import org.xml.sax.ErrorHandler;
55  import org.xml.sax.Locator;
56  
57  import com.virtuosotechnologies.lib.util.LocaleUtils;
58  import com.virtuosotechnologies.lib.util.StringID;
59  import com.virtuosotechnologies.lib.xml.XMLUnparser;
60  
61  import com.virtuosotechnologies.asaph.model.SongDatabase;
62  import com.virtuosotechnologies.asaph.model.Song;
63  import com.virtuosotechnologies.asaph.model.SongID;
64  import com.virtuosotechnologies.asaph.model.SongOperation;
65  import com.virtuosotechnologies.asaph.model.SongIDResultSet;
66  import com.virtuosotechnologies.asaph.model.SongDatabaseFailedException;
67  import com.virtuosotechnologies.asaph.model.SongDeletedException;
68  import com.virtuosotechnologies.asaph.model.SongNotFreshException;
69  import com.virtuosotechnologies.asaph.model.DatabaseNotWritableException;
70  import com.virtuosotechnologies.asaph.modelutils.SongUtils;
71  import com.virtuosotechnologies.asaph.notationmanager.NotationManager;
72  
73  
74  /**
75   * Simple implementation of SongDatabase
76   */
77  /*package*/ class SimpleSongDatabase
78  implements SongDatabase
79  {
80    private StringID lastSongID_;
81    private Map songMap_;
82    private SongUtils songUtils_;
83    private boolean writable_;
84    
85    
86    /*package*/ SimpleSongDatabase(
87      SongUtils songUtils)
88    {
89      this(songUtils, "", true);
90    }
91    
92    
93    /*package*/ SimpleSongDatabase(
94      SongUtils songUtils,
95      String lastID)
96    {
97      this(songUtils, lastID, true);
98    }
99    
100   
101   /*package*/ SimpleSongDatabase(
102     SongUtils songUtils,
103     String lastID,
104     boolean writable)
105   {
106     lastSongID_ = new StringID(lastID);
107     songMap_ = new LinkedHashMap();
108     songUtils_ = songUtils;
109     writable_ = writable;
110   }
111   
112   
113   /*package*/ void unparse(
114     XMLUnparser unparser)
115   throws
116     IOException
117   {
118     unparser.startMultiLineElement(XMLConstants.SONGLIST_ELEMENT);
119     unparser.addAttribute(XMLConstants.SONGLIST_LASTID_ATTRIBUTE,
120       lastSongID_.getValue());
121     for (Iterator iter = songMap_.entrySet().iterator(); iter.hasNext(); )
122     {
123       Map.Entry entry = (Map.Entry)iter.next();
124       StdSong song = (StdSong)entry.getValue();
125       song.unparse(unparser);
126     }
127     unparser.endElement(XMLConstants.SONGLIST_ELEMENT);
128   }
129   
130   
131   /*package*/ class ParseHandler
132   extends ParseHandlerBase
133   {
134     private NotationManager notationManager_;
135     
136     
137     /*package*/ ParseHandler(
138       ErrorHandler errorHandler,
139       Locator locator,
140       NotationManager notationManager)
141     {
142       super(errorHandler, locator, XMLConstants.SONGLIST_ELEMENT);
143       notationManager_ = notationManager;
144     }
145     
146     
147     /**
148      * Handle elements at the top level. Override this.
149      */
150     /*package*/ ParseHandlerBase localStartElement(
151       String uri,
152       String localName,
153       String qName,
154       Attributes attributes)
155     throws
156       SAXException
157     {
158       if (localName.equals(XMLConstants.SONG_ELEMENT))
159       {
160         String idStr = attributes.getValue(XMLConstants.SONG_ID_ATTRIBUTE);
161         if (idStr == null)
162         {
163           reportFatalError(ResourceAccess.Strings.buildString("xml_MissingAttribute",
164             XMLConstants.SONG_ID_ATTRIBUTE, XMLConstants.SONG_ELEMENT));
165         }
166         if (!StringID.isWellFormed(idStr))
167         {
168           reportFatalError(ResourceAccess.Strings.buildString(
169             "xml_MalformedSongID", idStr));
170         }
171         StringID id = new StringID(idStr);
172         String versionStr = attributes.getValue(XMLConstants.SONG_VERSION_ATTRIBUTE);
173         if (versionStr == null)
174         {
175           reportFatalError(ResourceAccess.Strings.buildString("xml_MissingAttribute",
176             XMLConstants.SONG_VERSION_ATTRIBUTE, XMLConstants.SONG_ELEMENT));
177         }
178         if (!StringID.isWellFormed(versionStr))
179         {
180           reportFatalError(ResourceAccess.Strings.buildString(
181             "xml_MalformedSongVersion", versionStr));
182         }
183         StringID version = new StringID(versionStr);
184         String localeStr = attributes.getValue(XMLConstants.SONG_LOCALE_ATTRIBUTE);
185         StdSong song = new StdSong(new SongIDImpl(id), version, LocaleUtils.getNamedLocale(localeStr));
186         songMap_.put(id, song);
187         if (id.compareTo(lastSongID_) > 0)
188         {
189           lastSongID_ = id;
190         }
191         return song.new ParseHandler(getErrorHandler(), getDocumentLocator(), notationManager_);
192       }
193       else
194       {
195         return super.localStartElement(uri, localName, qName, attributes);
196       }
197     }
198   }
199   
200   
201   /**
202    * Implementation of SongID
203    */
204   /*package*/ class SongIDImpl
205   implements SongID
206   {
207     private StringID id_;
208     
209     /*package*/ SongIDImpl(
210       StringID id)
211     {
212       id_ = id;
213     }
214     
215     /*package*/ StringID getStringID()
216     {
217       return id_;
218     }
219     
220     
221     public SongDatabase getDatabase()
222     {
223       return SimpleSongDatabase.this;
224     }
225     
226     public String getStringRepresentation()
227     {
228       return id_.getValue();
229     }
230     
231     public boolean equals(
232       Object obj)
233     {
234       if (obj instanceof SongIDImpl)
235       {
236         SongIDImpl ssid = (SongIDImpl)obj;
237         return ssid.getDatabase() == SimpleSongDatabase.this &&
238           ssid.id_.equals(id_);
239       }
240       return false;
241     }
242     
243     public int hashCode()
244     {
245       return id_.hashCode() + SimpleSongDatabase.this.hashCode();
246     }
247   }
248   
249   
250   //-------------------------------------------------------------------------
251   // Methods of SongDatabase
252   //-------------------------------------------------------------------------
253   
254   /**
255    * Returns true if the database is writable. If this returns false, every mutation
256    * method will throw DatabaseNotWritableException. Note that even if this returns
257    * false, the database may still be changed by other entities. In other words,
258    * this method reflects the ability to write to the database through this interface,
259    * not necessarily through all interfaces.
260    *
261    * @return true if the database is writable, false if not.
262    */
263   public boolean isWritable()
264   {
265     return writable_;
266   }
267   
268   
269   /**
270    * Get a song by SongID. Note that the Song returned is a copy. Changes
271    * do not get reflected in the database until commitSong() succeeds.
272    * Returns null if the song doesn't exist in this database. (This could
273    * mean the song was deleted, or it is part of a different database.)
274    *
275    * @param id SongID
276    * @return the Song, or null if the id doesn't exist in this database.
277    * @exception SongDatabaseFailedException Catch-all exception for database-related
278    *     problems. This will often have a cause exception, which may be exceptions
279    *     like IOException or SQLException.
280    * @exception NullPointerException id was null
281    */
282   public synchronized Song checkOutSong(
283     SongID id)
284   throws
285     SongDatabaseFailedException
286   {
287     if (id.getDatabase() != this)
288     {
289       return null;
290     }
291     StdSong song = (StdSong)songMap_.get(((SongIDImpl)id).getStringID());
292     if (song == null)
293     {
294       return null;
295     }
296     return new StdSong(song, songUtils_);
297   }
298   
299   
300   /**
301    * Tests whether the given song id exists in this database. Returns
302    * false if the song was deleted or if it is part of a different database.
303    *
304    * @param id SongID to test
305    * @return true if the given SongID exists in this database.
306    * @exception SongDatabaseFailedException Catch-all exception for database-related
307    *     problems. This will often have a cause exception, which may be exceptions
308    *     like IOException or SQLException.
309    * @exception NullPointerException id was null
310    */
311   public synchronized boolean containsSongID(
312     SongID id)
313   throws
314     SongDatabaseFailedException
315   {
316     return id.getDatabase() == this &&
317       songMap_.containsKey(((SongIDImpl)id).getStringID());
318   }
319   
320   
321   /**
322    * Get the SongID for the given string, if it exists. This is used to
323    * deserialize a SongID that was stored persistently as its string
324    * representation. Returns null if the string does not correspond to a
325    * SongID that exists in this database.
326    *
327    * @param idStr serialized ID
328    * @return SongID
329    * @exception SongDatabaseFailedException Catch-all exception for database-related
330    *     problems. This will often have a cause exception, which may be exceptions
331    *     like IOException or SQLException.
332    */
333   public synchronized SongID getSongIDForString(
334     String idStr)
335   throws
336     SongDatabaseFailedException
337   {
338     if (!StringID.isWellFormed(idStr))
339     {
340       return null;
341     }
342     StringID id = new StringID(idStr);
343     if (songMap_.containsKey(id))
344     {
345       return new SongIDImpl(id);
346     }
347     else
348     {
349       return null;
350     }
351   }
352   
353   
354   /**
355    * Create an empty result set.
356    *
357    * @return empty SongIDResultSet
358    */
359   public SongIDResultSet createEmptyResultSet()
360   {
361     return new StdSongIDResultSet(this);
362   }
363   
364   
365   /**
366    * Perform an operation on a set of songs. Operations could be accessors for
367    * information that could be cached by the database, such as the main title, or they
368    * could be filters applied to the result set, or they could have other semantics
369    * such as mass-checkouts or mass-checkins.
370    * The client specifies as the parameter a SongIDResultSet specifying the songs and
371    * parameter data for the operation. If null is passed, the database creates a new
372    * SongIDResultSet whose values are all the SongIDs in the database with null data
373    * for each one.
374    * The client also specifies the operation to perform.
375    * The method performs the operation on the result set in place, and then returns it.
376    * <p>
377    * Note that some SongDatabase implementations may choose to optimize this method
378    * by not calling the operation directly, but by analzying the semantics of the
379    * operation as declared by which operation semantics interfaces are implemented, and
380    * performing accelerated operations such as checking caches. Thus, do not expect that
381    * the SongOperation object you pass will actually be invoked.
382    * <p>
383    * One common use of this method is to get a result set containing all the SongIDs
384    * in the database. This is accomplished by passing null for the parameter, and
385    * a NopSemantics implementation for the operation.
386    *
387    * @param operation SongOperation to perform
388    * @param param input result set
389    * @return output result set
390    * @exception SongDatabaseFailedException Catch-all exception for database-related
391    *     problems. This will often have a cause exception, which may be exceptions
392    *     like IOException or SQLException.
393    */
394   public SongIDResultSet performOperation(
395     SongOperation operation,
396     SongIDResultSet param)
397   throws
398     SongDatabaseFailedException
399   {
400     if (param == null)
401     {
402       param = new StdSongIDResultSet(this);
403       synchronized(this)
404       {
405         for (Iterator iter = songMap_.keySet().iterator(); iter.hasNext(); )
406         {
407           StringID id = (StringID)iter.next();
408           param.add(new SongIDImpl(id));
409         }
410       }
411     }
412     operation.perform(param);
413     return param;
414   }
415   
416   
417   /**
418    * Add a new empty Song to the database.
419    *
420    * @param title main title of song to add
421    * @param locale Locale of song to add, or null to use the default locale
422    * @return ID of added Song
423    * @exception SongDatabaseFailedException Catch-all exception for database-related
424    *     problems. This will often have a cause exception, which may be exceptions
425    *     like IOException or SQLException.
426    * @exception NullPointerException title was null
427    */
428   public synchronized SongID addEmptySong(
429     String title,
430     Locale locale)
431   throws
432     SongDatabaseFailedException
433   {
434     if (title == null)
435     {
436       throw new NullPointerException();
437     }
438     if (!writable_)
439     {
440       throw new DatabaseNotWritableException();
441     }
442     if (locale == null)
443     {
444       locale = Locale.getDefault();
445     }
446     lastSongID_ = lastSongID_.getNext();
447     SongIDImpl id = new SongIDImpl(lastSongID_);
448     StdSong song = new StdSong(id, locale);
449     song.addStringField(Song.MAINTITLE_FIELD, title, null);
450     songMap_.put(lastSongID_, song);
451     return id;
452   }
453   
454   
455   /**
456    * Makes a copy of the given song and adds it to the database. The given song need
457    * not be from this database.
458    * On completion, the song in the database will be identical to the given song,
459    * but the given song will not be modified. In particular, if the given song is
460    * owned by a different database, it will still be owned by that database.
461    *
462    * @return ID of added Song
463    * @exception SongDatabaseFailedException Catch-all exception for database-related
464    *     problems. This will often have a cause exception, which may be exceptions
465    *     like IOException or SQLException.
466    * @exception NullPointerException original was null
467    */
468   public synchronized SongID addCopyOfSong(
469     Song original)
470   throws
471     SongDatabaseFailedException
472   {
473     if (original == null)
474     {
475       throw new NullPointerException();
476     }
477     if (!writable_)
478     {
479       throw new DatabaseNotWritableException();
480     }
481     lastSongID_ = lastSongID_.getNext();
482     SongIDImpl id = new SongIDImpl(lastSongID_);
483     StdSong song = new StdSong(id, original.getLocale());
484     songUtils_.copySong(original, song);
485     songMap_.put(lastSongID_, song);
486     return id;
487   }
488   
489   
490   /**
491    * Remove the given song from the database. Returns true if the song existed
492    * and was removed, or false if it was not present.
493    *
494    * @param id SongID of song to remove
495    * @return true if removed
496    * @exception SongDatabaseFailedException Catch-all exception for database-related
497    *     problems. This will often have a cause exception, which may be exceptions
498    *     like IOException or SQLException.
499    * @exception NullPointerException id was null
500    */
501   public synchronized boolean removeSong(
502     SongID id)
503   throws
504     SongDatabaseFailedException
505   {
506     if (!writable_)
507     {
508       throw new DatabaseNotWritableException();
509     }
510     if (id.getDatabase() != this)
511     {
512       return false;
513     }
514     return songMap_.remove(((SongIDImpl)id).getStringID()) != null;
515   }
516   
517   
518   /**
519    * Check the commit count to see if this song is still fresh. In other
520    * words, this returns true if the song has not been committed by someone
521    * else since this copy was checked out. Note that this should be taken to
522    * mean "this song was fresh a little while ago." A return value of true
523    * should not be taken as a guarantee that a subsequent call to commitSong
524    * will not throw SongNotFreshException, because another client may commit
525    * in between.
526    *
527    * @param song song to test
528    * @return true if the song is fresh.
529    * @exception SongDeletedException someone deleted this song in the meantime
530    * @exception SongDatabaseFailedException Catch-all exception for database-related
531    *     problems. This will often have a cause exception, which may be exceptions
532    *     like IOException or SQLException.
533    * @exception IllegalArgumentException this database is not the owner of the Song
534    * @exception NullPointerException song was null
535    */
536   public synchronized boolean isSongFresh(
537     Song song)
538   throws
539     SongDeletedException,
540     SongDatabaseFailedException
541   {
542     SongID songID = song.getSongID();
543     if (songID == null || songID.getDatabase() != this)
544     {
545       throw new IllegalArgumentException();
546     }
547     StdSong original = (StdSong)songMap_.get(((SongIDImpl)songID).getStringID());
548     if (original == null)
549     {
550       throw new SongDeletedException();
551     }
552     return ((StdSong)song).getVersion().equals(original.getVersion());
553   }
554   
555   
556   /**
557    * Commit changes to this song. Also sets this song to fresh, since it now
558    * reflects the database contents. Does not perform the commit and throws
559    * SongNotFreshException if someone already committed a change in the meantime.
560    *
561    * @param song song to commit
562    * @exception SongNotFreshException someone committed a change in the meantime
563    * @exception SongDeletedException someone deleted this song in the meantime
564    * @exception SongDatabaseFailedException Catch-all exception for database-related
565    *     problems. This will always have a cause exception, which may be exceptions
566    *     like IOException or SQLException.
567    * @exception IllegalArgumentException this database is not the owner of the Song
568    * @exception NullPointerException song was null
569    */
570   public synchronized void commitSong(
571     Song song)
572   throws
573     SongNotFreshException,
574     SongDeletedException,
575     SongDatabaseFailedException
576   {
577     if (!writable_)
578     {
579       throw new DatabaseNotWritableException();
580     }
581     SongID committedID = song.getSongID();
582     if (committedID == null || committedID.getDatabase() != this)
583     {
584       throw new IllegalArgumentException();
585     }
586     StdSong committedSong = (StdSong)song;
587     StringID id = ((SongIDImpl)committedID).getStringID();
588     StdSong oldSong = (StdSong)songMap_.get(id);
589     if (oldSong == null)
590     {
591       throw new SongDeletedException();
592     }
593     if (!oldSong.getVersion().equals(committedSong.getVersion()))
594     {
595       throw new SongNotFreshException();
596     }
597     committedSong.setVersion(oldSong.getVersion().getNext());
598     StdSong newSong = new StdSong(committedSong, songUtils_);
599     songMap_.put(id, newSong);
600   }
601   
602   
603   /**
604    * Commit changes to this song. Also sets this song to fresh, since it now
605    * reflects the database contents. Commits and overwrites any changes made by
606    * clients in the meantime. (In other words, doesn't throw SongNotFreshException.)
607    *
608    * @param song song to commit
609    * @exception SongDeletedException someone deleted this song in the meantime
610    * @exception SongDatabaseFailedException Catch-all exception for database-related
611    *     problems. This will always have a cause exception, which may be exceptions
612    *     like IOException or SQLException.
613    * @exception IllegalArgumentException this database is not the owner of the Song
614    * @exception NullPointerException song was null
615    */
616   public synchronized void forceCommitSong(
617     Song song)
618   throws
619     SongDeletedException,
620     SongDatabaseFailedException
621   {
622     if (!writable_)
623     {
624       throw new DatabaseNotWritableException();
625     }
626     SongID committedID = song.getSongID();
627     if (committedID == null || committedID.getDatabase() != this)
628     {
629       throw new IllegalArgumentException();
630     }
631     StdSong committedSong = (StdSong)song;
632     StringID id = ((SongIDImpl)committedID).getStringID();
633     StdSong oldSong = (StdSong)songMap_.get(id);
634     if (oldSong == null)
635     {
636       throw new SongDeletedException();
637     }
638     committedSong.setVersion(oldSong.getVersion().getNext());
639     StdSong newSong = new StdSong(committedSong, songUtils_);
640     songMap_.put(id, newSong);
641   }
642   
643   
644   /**
645    * Force-commit a song as the given SongID. The given SongID must be in this
646    * database, but the given checked-out song need not be from this database.
647    * On completion, the song in the database will be identical to the given song,
648    * but the given song will not be modified. In particular, if the given song is
649    * owned by a different database, it will still be owned by that database.
650    *
651    * @param song song to commit
652    * @param songID SongID to commit as
653    * @exception SongDeletedException someone deleted the SongID
654    * @exception SongDatabaseFailedException Catch-all exception for database-related
655    *     problems. This will always have a cause exception, which may be exceptions
656    *     like IOException or SQLException.
657    * @exception IllegalArgumentException this database is not the owner of the SongID
658    * @exception NullPointerException song or songID was null
659    */
660   public synchronized void forceCommitSongAs(
661     Song song,
662     SongID songID)
663   throws
664     SongDeletedException,
665     SongDatabaseFailedException
666   {
667     if (!writable_)
668     {
669       throw new DatabaseNotWritableException();
670     }
671     if (song == null)
672     {
673       throw new NullPointerException();
674     }
675     if (songID.getDatabase() != this)
676     {
677       throw new IllegalArgumentException();
678     }
679     StringID id = ((SongIDImpl)songID).getStringID();
680     StdSong oldSong = (StdSong)songMap_.get(id);
681     if (oldSong == null)
682     {
683       throw new SongDeletedException();
684     }
685     StdSong newSong = new StdSong(songID, oldSong.getVersion().getNext(), song.getLocale());
686     songUtils_.copySong(song, newSong);
687     songMap_.put(id, newSong);
688   }
689 }