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 }