Source code: com/virtuosotechnologies/asaph/xmldatabase/IndexedSongDatabase.java
1 /*
2 ================================================================================
3
4 FILE: IndexedSongDatabase.java
5
6 PROJECT:
7
8 Asaph
9
10 CONTENTS:
11
12 Indexed 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.xmldatabase;
43
44
45 import java.io.Reader;
46 import java.io.FileInputStream;
47 import java.io.StringReader;
48 import java.io.OutputStream;
49 import java.io.FileOutputStream;
50 import java.io.File;
51 import java.io.FilenameFilter;
52 import java.io.IOException;
53 import java.io.FileNotFoundException;
54 import java.net.URL;
55 import java.net.MalformedURLException;
56 import java.util.Collections;
57 import java.util.List;
58 import java.util.ArrayList;
59 import java.util.Set;
60 import java.util.LinkedHashSet;
61 import java.util.Map;
62 import java.util.HashMap;
63 import java.util.LinkedHashMap;
64 import java.util.Iterator;
65 import java.util.Locale;
66 import java.util.logging.Logger;
67 import javax.xml.parsers.SAXParser;
68 import org.xml.sax.Attributes;
69 import org.xml.sax.SAXException;
70 import org.xml.sax.SAXParseException;
71 import org.xml.sax.ErrorHandler;
72 import org.xml.sax.InputSource;
73 import org.xml.sax.Locator;
74 import org.xml.sax.helpers.DefaultHandler;
75
76 import com.virtuosotechnologies.lib.util.StringID;
77 import com.virtuosotechnologies.lib.util.FileUtils;
78 import com.virtuosotechnologies.lib.xml.XMLDocumentUnparser;
79 import com.virtuosotechnologies.lib.xml.ZeroToleranceErrorHandler;
80 import com.virtuosotechnologies.lib.xml.MessageCollectingErrorHandler;
81
82 import com.virtuosotechnologies.asaph.model.SongDatabase;
83 import com.virtuosotechnologies.asaph.model.Song;
84 import com.virtuosotechnologies.asaph.model.SongID;
85 import com.virtuosotechnologies.asaph.model.SongOperation;
86 import com.virtuosotechnologies.asaph.model.SongIDResultSet;
87 import com.virtuosotechnologies.asaph.model.StringList;
88 import com.virtuosotechnologies.asaph.model.SimpleString;
89 import com.virtuosotechnologies.asaph.model.SongDatabaseFailedException;
90 import com.virtuosotechnologies.asaph.model.SongDeletedException;
91 import com.virtuosotechnologies.asaph.model.SongNotFreshException;
92 import com.virtuosotechnologies.asaph.model.DatabaseNotWritableException;
93 import com.virtuosotechnologies.asaph.model.opsemantics.GetFieldAsStringSemantics;
94 import com.virtuosotechnologies.asaph.model.opsemantics.GetFieldAsStringArraySemantics;
95 import com.virtuosotechnologies.asaph.model.opsemantics.SearchFieldPredicateSemantics;
96 import com.virtuosotechnologies.asaph.model.opsemantics.SearchParameters;
97 import com.virtuosotechnologies.asaph.modelutils.SongUtils;
98 import com.virtuosotechnologies.asaph.standardmodel.StandardModelFactory;
99
100
101 /**
102 * Indexed implementation of SongDatabase
103 */
104 /*package*/ class IndexedSongDatabase
105 implements SongDatabase
106 {
107 private static final String STR_err_CantFindIndex =
108 ResourceAccess.Strings.buildString("err_CantFindIndex");
109 private static final String STR_PreferredCharacterEncoding =
110 ResourceAccess.Strings.buildString("PreferredCharacterEncoding");
111
112
113 private StringID lastSongID_;
114 private Map songMap_;
115 private SongUtils songUtils_;
116 private StandardModelFactory modelFactory_;
117 private File fileDirectory_;
118 private URL urlDirectory_;
119 private Logger logger_;
120
121
122 /**
123 * Constructor for immutable database
124 */
125 /*package*/ IndexedSongDatabase(
126 SongUtils songUtils,
127 StandardModelFactory modelFactory,
128 URL urlDirectory,
129 SAXParser parser,
130 String dtdString,
131 ErrorHandler errorHandler)
132 throws
133 IOException,
134 SAXException
135 {
136 logger_ = Logger.getLogger("com.virtuosotechnologies.asaph.xmldatabase");
137
138 lastSongID_ = new StringID();
139 songMap_ = new LinkedHashMap();
140 modelFactory_ = modelFactory;
141 songUtils_ = songUtils;
142 urlDirectory_ = urlDirectory;
143
144 Reader reader = getReader(IndexConstants.INDEX_FILENAME);
145 if (reader == null)
146 {
147 throw new IOException(STR_err_CantFindIndex);
148 }
149 try
150 {
151 parser.parse(new InputSource(reader),
152 new IndexXMLHandler(dtdString, errorHandler));
153 }
154 finally
155 {
156 try
157 {
158 reader.close();
159 }
160 catch (IOException ex)
161 {
162 }
163 }
164 }
165
166
167 /**
168 * Constructor for mutable database
169 */
170 /*package*/ IndexedSongDatabase(
171 SongUtils songUtils,
172 StandardModelFactory modelFactory,
173 File fileDirectory,
174 SAXParser parser,
175 String dtdString)
176 throws
177 IOException
178 {
179 logger_ = Logger.getLogger("com.virtuosotechnologies.asaph.xmldatabase");
180
181 lastSongID_ = new StringID();
182 songMap_ = new LinkedHashMap();
183 modelFactory_ = modelFactory;
184 songUtils_ = songUtils;
185 fileDirectory_ = fileDirectory;
186
187 String[] contents = fileDirectory.list(
188 new FilenameFilter()
189 {
190 public boolean accept(
191 File dir,
192 String name)
193 {
194 return name.startsWith(IndexConstants.SONG_PREFIX) &&
195 name.endsWith(IndexConstants.SONG_SUFFIX);
196 }
197 });
198
199 Map tempSongMap = new HashMap();
200 for (int i=0; i<contents.length; ++i)
201 {
202 String str = contents[i].substring(IndexConstants.SONG_PREFIX.length(),
203 contents[i].length()-IndexConstants.SONG_SUFFIX.length());
204 int separatorIndex = str.indexOf(IndexConstants.SONG_INTER);
205 if (separatorIndex == -1)
206 {
207 continue;
208 }
209 String idStr = str.substring(0, separatorIndex);
210 String versionStr = str.substring(separatorIndex+IndexConstants.SONG_INTER.length());
211 if (!StringID.isWellFormed(idStr) || !StringID.isWellFormed(versionStr))
212 {
213 continue;
214 }
215 StringID id = new StringID(idStr);
216 StringID version = new StringID(versionStr);
217 SongInfoCache info = (SongInfoCache)tempSongMap.get(id);
218 if (info != null)
219 {
220 if (version.compareTo(info.getVersion()) > 0)
221 {
222 info.setVersion(version);
223 }
224 }
225 else
226 {
227 info = new SongInfoCache(new SongIDImpl(id), version);
228 tempSongMap.put(id, info);
229 }
230 if (id.compareTo(lastSongID_) > 0)
231 {
232 lastSongID_ = id;
233 }
234 }
235 List idList = new ArrayList(tempSongMap.keySet());
236 Collections.sort(idList);
237 for (Iterator iter = idList.iterator(); iter.hasNext(); )
238 {
239 StringID id = (StringID)iter.next();
240 songMap_.put(id, tempSongMap.get(id));
241 }
242
243 Reader reader = getReader(IndexConstants.INDEX_FILENAME);
244 if (reader != null)
245 {
246 try
247 {
248 parser.parse(new InputSource(reader),
249 new IndexXMLHandler(dtdString, new ZeroToleranceErrorHandler()));
250 }
251 catch (SAXException ex)
252 {
253 ex.printStackTrace();
254 }
255 catch (IOException ex)
256 {
257 ex.printStackTrace();
258 }
259 finally
260 {
261 try
262 {
263 reader.close();
264 }
265 catch (IOException ex)
266 {
267 }
268 }
269 }
270 else
271 {
272 commitIndex();
273 }
274 }
275
276
277 /**
278 * Implementation of SongID for this database
279 */
280 /*package*/ class SongIDImpl
281 implements SongID
282 {
283 private StringID id_;
284
285 /*package*/ SongIDImpl(
286 StringID id)
287 {
288 id_ = id;
289 }
290
291 /*package*/ StringID getStringID()
292 {
293 return id_;
294 }
295
296 public SongDatabase getDatabase()
297 {
298 return IndexedSongDatabase.this;
299 }
300
301 public String getStringRepresentation()
302 {
303 return id_.getValue();
304 }
305
306 public boolean equals(
307 Object obj)
308 {
309 if (obj instanceof SongIDImpl)
310 {
311 SongIDImpl ssid = (SongIDImpl)obj;
312 return ssid.getDatabase() == IndexedSongDatabase.this &&
313 ssid.id_.equals(id_);
314 }
315 return false;
316 }
317
318 public int hashCode()
319 {
320 return id_.hashCode() + IndexedSongDatabase.this.hashCode();
321 }
322 }
323
324
325 /**
326 * Cache of song info
327 */
328 /*package*/ class SongInfoCache
329 {
330 private SongIDImpl songID_;
331 private StringID version_;
332 private String mainTitle_;
333 private String comment_;
334 private String author_;
335 private String copyright_;
336 private String[] altTitles_;
337 private String[] keywords_;
338
339 /*package*/ SongInfoCache(
340 SongIDImpl songID,
341 StringID version)
342 {
343 songID_ = songID;
344 version_ = version;
345 mainTitle_ = null;
346 comment_ = null;
347 author_ = null;
348 copyright_ = null;
349 altTitles_ = null;
350 keywords_ = null;
351 }
352
353 /*package*/ StringID getVersion()
354 {
355 return version_;
356 }
357
358 /*package*/ SongIDImpl getSongID()
359 {
360 return songID_;
361 }
362
363 /*package*/ String getFileName()
364 {
365 return IndexConstants.SONG_PREFIX+songID_.getStringRepresentation()+
366 IndexConstants.SONG_INTER+version_.getValue()+IndexConstants.SONG_SUFFIX;
367 }
368
369 /*package*/ String getMainTitle()
370 {
371 return mainTitle_;
372 }
373
374 /*package*/ String forceGetMainTitle()
375 {
376 if (mainTitle_ == null)
377 {
378 updateNow();
379 }
380 return mainTitle_;
381 }
382
383 /*package*/ String getComment()
384 {
385 return comment_;
386 }
387
388 /*package*/ String forceGetComment()
389 {
390 if (comment_ == null)
391 {
392 updateNow();
393 }
394 return comment_;
395 }
396
397 /*package*/ String getAuthor()
398 {
399 return author_;
400 }
401
402 /*package*/ String forceGetAuthor()
403 {
404 if (author_ == null)
405 {
406 updateNow();
407 }
408 return author_;
409 }
410
411 /*package*/ String getCopyright()
412 {
413 return copyright_;
414 }
415
416 /*package*/ String forceGetCopyright()
417 {
418 if (copyright_ == null)
419 {
420 updateNow();
421 }
422 return copyright_;
423 }
424
425 /*package*/ String[] getAltTitles()
426 {
427 return altTitles_;
428 }
429
430 /*package*/ String[] forceGetAltTitles()
431 {
432 if (altTitles_ == null)
433 {
434 updateNow();
435 }
436 return altTitles_;
437 }
438
439 /*package*/ String[] getKeywords()
440 {
441 return keywords_;
442 }
443
444 /*package*/ String[] forceGetKeywords()
445 {
446 if (keywords_ == null)
447 {
448 updateNow();
449 }
450 return keywords_;
451 }
452
453
454 private void updateNow()
455 {
456 try
457 {
458 Song song = checkOutSong(songID_);
459 if (song != null)
460 {
461 updateFrom(song);
462 }
463 }
464 catch (SongDatabaseFailedException ex)
465 {
466 }
467 }
468
469
470 /*package*/ void updateFrom(
471 Song song)
472 {
473 mainTitle_ = songUtils_.getFieldValueAsString(song, Song.MAINTITLE_FIELD);
474 comment_ = songUtils_.getFieldValueAsString(song, Song.COMMENT_FIELD);
475 author_ = songUtils_.getFieldValueAsString(song, Song.AUTHOR_FIELD);
476 copyright_ = songUtils_.getFieldValueAsString(song, Song.COPYRIGHT_FIELD);
477 altTitles_ = songUtils_.getFieldValueAsStringArray(song, Song.ALTERNATETITLELIST_FIELD);
478 keywords_ = songUtils_.getFieldValueAsStringArray(song, Song.KEYWORDLIST_FIELD);
479 }
480
481 /*package*/ void setVersion(
482 StringID version)
483 {
484 version_ = version;
485 }
486
487 /*package*/ void clearMainTitle()
488 {
489 mainTitle_ = null;
490 }
491
492 /*package*/ void setMainTitle(
493 String value)
494 {
495 mainTitle_ = value;
496 }
497
498 /*package*/ void clearComment()
499 {
500 comment_ = null;
501 }
502
503 /*package*/ void setComment(
504 String value)
505 {
506 comment_ = value;
507 }
508
509 /*package*/ void clearAuthor()
510 {
511 author_ = null;
512 }
513
514 /*package*/ void setAuthor(
515 String value)
516 {
517 author_ = value;
518 }
519
520 /*package*/ void clearCopyright()
521 {
522 copyright_ = null;
523 }
524
525 /*package*/ void setCopyright(
526 String value)
527 {
528 copyright_ = value;
529 }
530
531 /*package*/ void clearAltTitles()
532 {
533 altTitles_ = null;
534 }
535
536 /*package*/ void setAltTitles(
537 String[] array)
538 {
539 altTitles_ = array;
540 }
541
542 /*package*/ void clearKeywords()
543 {
544 keywords_ = null;
545 }
546
547 /*package*/ void setKeywords(
548 String[] array)
549 {
550 keywords_ = array;
551 }
552 }
553
554
555 /**
556 * Utility method
557 */
558 private Reader getReader(
559 String fileName)
560 {
561 Reader reader = null;
562 if (fileDirectory_ != null)
563 {
564 File file = new File(fileDirectory_, fileName);
565 if (file.isFile())
566 {
567 try
568 {
569 reader = FileUtils.createUTFReader(new FileInputStream(file));
570 }
571 catch (IOException ex)
572 {
573 // Unable to open stream; return null
574 }
575 }
576 }
577 else
578 {
579 try
580 {
581 URL indexURL = new URL(urlDirectory_, fileName);
582 try
583 {
584 reader = FileUtils.createUTFReader(indexURL.openStream());
585 }
586 catch (IOException ex)
587 {
588 // Unable to open stream; return null
589 }
590 }
591 catch (MalformedURLException ex)
592 {
593 // Unexpected
594 ex.printStackTrace();
595 }
596 }
597 return reader;
598 }
599
600
601 /**
602 * Utility method
603 */
604 private OutputStream getOutputStream(
605 String fileName)
606 {
607 OutputStream stream = null;
608 if (fileDirectory_ != null)
609 {
610 File file = new File(fileDirectory_, fileName);
611 if (!file.exists() || file.canWrite())
612 {
613 try
614 {
615 stream = new FileOutputStream(file);
616 }
617 catch (IOException ex)
618 {
619 }
620 }
621 }
622 return stream;
623 }
624
625
626 /**
627 * DefaultHandler for initial loading of the index file
628 */
629 /*package*/ class IndexXMLHandler
630 extends DefaultHandler
631 {
632 private ErrorHandler errorHandler_;
633 private Locator locator_;
634 private String dtdString_;
635 private SongInfoCache curSong_;
636 private String state_;
637 private StringBuffer buffer_;
638 private List list_;
639
640
641 /*package*/ IndexXMLHandler(
642 String dtdString,
643 ErrorHandler errorHandler)
644 {
645 dtdString_ = dtdString;
646 errorHandler_ = errorHandler;
647 if (errorHandler_ == null)
648 {
649 errorHandler_ = new ZeroToleranceErrorHandler();
650 }
651 curSong_ = null;
652 state_ = null;
653 buffer_ = new StringBuffer();
654 list_ = new ArrayList();
655 }
656
657
658 public void startElement(
659 String uri,
660 String localName,
661 String qName,
662 Attributes attributes)
663 throws
664 SAXException
665 {
666 if (localName.equals(IndexConstants.INDEX_ELEMENT))
667 {
668 String lastID = attributes.getValue(IndexConstants.INDEX_LASTID_ATTRIBUTE);
669 if (StringID.isWellFormed(lastID))
670 {
671 lastSongID_ = new StringID(lastID);
672 }
673 }
674 else if (localName.equals(IndexConstants.ENTRY_ELEMENT))
675 {
676 String idStr = attributes.getValue(IndexConstants.ENTRY_ID_ATTRIBUTE);
677 if (idStr == null)
678 {
679 errorHandler_.fatalError(new SAXParseException(
680 ResourceAccess.Strings.buildString("err_MissingAttribute",
681 IndexConstants.ENTRY_ID_ATTRIBUTE, IndexConstants.ENTRY_ELEMENT),
682 locator_));
683 }
684 if (!StringID.isWellFormed(idStr))
685 {
686 errorHandler_.fatalError(new SAXParseException(
687 ResourceAccess.Strings.buildString("err_MalformedSongID", idStr),
688 locator_));
689 }
690 StringID id = new StringID(idStr);
691 String versionStr = attributes.getValue(IndexConstants.ENTRY_VERSION_ATTRIBUTE);
692 if (versionStr == null)
693 {
694 errorHandler_.fatalError(new SAXParseException(
695 ResourceAccess.Strings.buildString("err_MissingAttribute",
696 IndexConstants.ENTRY_VERSION_ATTRIBUTE, IndexConstants.ENTRY_ELEMENT),
697 locator_));
698 }
699 if (!StringID.isWellFormed(versionStr))
700 {
701 errorHandler_.fatalError(new SAXParseException(
702 ResourceAccess.Strings.buildString("err_MalformedSongVersion", versionStr),
703 locator_));
704 }
705 StringID version = new StringID(versionStr);
706 if (fileDirectory_ != null)
707 {
708 curSong_ = (SongInfoCache)songMap_.get(id);
709 if (!curSong_.getVersion().equals(version))
710 {
711 curSong_ = null;
712 }
713 }
714 else
715 {
716 curSong_ = new SongInfoCache(new SongIDImpl(id), version);
717 songMap_.put(id, curSong_);
718 if (id.compareTo(lastSongID_) > 0)
719 {
720 lastSongID_ = id;
721 }
722 }
723 state_ = null;
724 }
725 else if (localName.equals(IndexConstants.MAINTITLE_ELEMENT))
726 {
727 state_ = IndexConstants.MAINTITLE_ELEMENT;
728 buffer_ = new StringBuffer();
729 }
730 else if (localName.equals(IndexConstants.COMMENT_ELEMENT))
731 {
732 state_ = IndexConstants.COMMENT_ELEMENT;
733 buffer_ = new StringBuffer();
734 }
735 else if (localName.equals(IndexConstants.AUTHOR_ELEMENT))
736 {
737 state_ = IndexConstants.AUTHOR_ELEMENT;
738 buffer_ = new StringBuffer();
739 }
740 else if (localName.equals(IndexConstants.COPYRIGHT_ELEMENT))
741 {
742 state_ = IndexConstants.COPYRIGHT_ELEMENT;
743 buffer_ = new StringBuffer();
744 }
745 else if (localName.equals(IndexConstants.ALTTITLES_ELEMENT))
746 {
747 state_ = IndexConstants.ALTTITLES_ELEMENT;
748 list_ = new ArrayList();
749 }
750 else if (localName.equals(IndexConstants.KEYWORDS_ELEMENT))
751 {
752 state_ = IndexConstants.KEYWORDS_ELEMENT;
753 list_ = new ArrayList();
754 }
755 else if (localName.equals(IndexConstants.STRING_ELEMENT))
756 {
757 buffer_ = new StringBuffer();
758 }
759 }
760
761
762 public void endElement(
763 String uri,
764 String localName,
765 String qName)
766 throws
767 SAXException
768 {
769 if (localName.equals(IndexConstants.MAINTITLE_ELEMENT))
770 {
771 state_ = null;
772 if (curSong_ != null)
773 {
774 curSong_.setMainTitle(new String(buffer_));
775 }
776 }
777 else if (localName.equals(IndexConstants.COMMENT_ELEMENT))
778 {
779 state_ = null;
780 if (curSong_ != null)
781 {
782 curSong_.setComment(new String(buffer_));
783 }
784 }
785 else if (localName.equals(IndexConstants.AUTHOR_ELEMENT))
786 {
787 state_ = null;
788 if (curSong_ != null)
789 {
790 curSong_.setAuthor(new String(buffer_));
791 }
792 }
793 else if (localName.equals(IndexConstants.COPYRIGHT_ELEMENT))
794 {
795 state_ = null;
796 if (curSong_ != null)
797 {
798 curSong_.setCopyright(new String(buffer_));
799 }
800 }
801 else if (localName.equals(IndexConstants.ALTTITLES_ELEMENT))
802 {
803 state_ = null;
804 if (curSong_ != null)
805 {
806 String[] array = new String[list_.size()];
807 list_.toArray(array);
808 curSong_.setAltTitles(array);
809 }
810 }
811 else if (localName.equals(IndexConstants.KEYWORDS_ELEMENT))
812 {
813 state_ = null;
814 if (curSong_ != null)
815 {
816 String[] array = new String[list_.size()];
817 list_.toArray(array);
818 curSong_.setKeywords(array);
819 }
820 }
821 else if (localName.equals(IndexConstants.STRING_ELEMENT))
822 {
823 if (curSong_ != null)
824 {
825 list_.add(new String(buffer_));
826 }
827 }
828 }
829
830
831 public void characters(
832 char[] chs,
833 int start,
834 int length)
835 throws
836 SAXException
837 {
838 buffer_.append(chs, start, length);
839 }
840
841
842 public InputSource resolveEntity(
843 String publicId,
844 String systemId)
845 throws
846 SAXException
847 {
848 if (systemId.equals(IndexConstants.XMLDATABASEINDEX_DTD) ||
849 systemId.equals(IndexConstants.XMLDATABASEINDEX_DTD_ALT))
850 {
851 return new InputSource(new StringReader(dtdString_));
852 }
853 else
854 {
855 return null;
856 }
857 }
858
859
860 public void error(
861 SAXParseException ex)
862 throws
863 SAXException
864 {
865 errorHandler_.error(ex);
866 }
867
868
869 public void fatalError(
870 SAXParseException ex)
871 throws
872 SAXException
873 {
874 errorHandler_.fatalError(ex);
875 }
876
877
878 public void warning(
879 SAXParseException ex)
880 throws
881 SAXException
882 {
883 errorHandler_.warning(ex);
884 }
885
886
887 public void setDocumentLocator(
888 Locator locator)
889 {
890 locator_ = locator;
891 }
892 }
893
894
895 private boolean commitIndex()
896 {
897 if (!isWritable())
898 {
899 return false;
900 }
901 File bakFile = new File(fileDirectory_, IndexConstants.INDEX_BAK_FILENAME);
902 File indexFile = new File(fileDirectory_, IndexConstants.INDEX_FILENAME);
903 bakFile.delete();
904 if (indexFile.exists())
905 {
906 if (!indexFile.renameTo(bakFile))
907 {
908 return false;
909 }
910 }
911
912 OutputStream stream = getOutputStream(IndexConstants.INDEX_FILENAME);
913 if (stream == null)
914 {
915 bakFile.renameTo(indexFile);
916 return false;
917 }
918 boolean success = false;
919 try
920 {
921 writeIndex(stream);
922 success = true;
923 }
924 catch (IOException ex)
925 {
926 }
927 finally
928 {
929 try
930 {
931 stream.close();
932 }
933 catch (IOException ex)
934 {
935 }
936 }
937 if (success)
938 {
939 bakFile.delete();
940 return true;
941 }
942 else
943 {
944 indexFile.delete();
945 bakFile.renameTo(indexFile);
946 return false;
947 }
948 }
949
950
951 private void writeIndex(
952 OutputStream stream)
953 throws
954 IOException
955 {
956 XMLDocumentUnparser unparser = XMLDocumentUnparser.create(stream, STR_PreferredCharacterEncoding,
957 IndexConstants.XMLDATABASEINDEX_DTD, IndexConstants.INDEXROOT_ELEMENT);
958 unparser.addAttribute("xmlns", IndexConstants.XMLDATABASEINDEX_NAMESPACE);
959
960 unparser.startMultiLineElement(IndexConstants.INDEX_ELEMENT);
961 unparser.addAttribute(IndexConstants.INDEX_LASTID_ATTRIBUTE, lastSongID_.getValue());
962
963 for (Iterator iter = songMap_.entrySet().iterator(); iter.hasNext(); )
964 {
965 Map.Entry entry = (Map.Entry)iter.next();
966 SongInfoCache info = (SongInfoCache)entry.getValue();
967 unparser.startMultiLineElement(IndexConstants.ENTRY_ELEMENT);
968 unparser.addAttribute(IndexConstants.ENTRY_ID_ATTRIBUTE,
969 info.getSongID().getStringRepresentation());
970 unparser.addAttribute(IndexConstants.ENTRY_VERSION_ATTRIBUTE, info.getVersion().getValue());
971
972 String mainTitle = info.getMainTitle();
973 if (mainTitle != null)
974 {
975 unparser.startSingleLineElement(IndexConstants.MAINTITLE_ELEMENT);
976 unparser.addString(mainTitle);
977 unparser.endElement(IndexConstants.MAINTITLE_ELEMENT);
978 }
979
980 String comment = info.getComment();
981 if (comment != null)
982 {
983 unparser.startSingleLineElement(IndexConstants.COMMENT_ELEMENT);
984 unparser.addString(comment);
985 unparser.endElement(IndexConstants.COMMENT_ELEMENT);
986 }
987
988 String author = info.getAuthor();
989 if (author != null)
990 {
991 unparser.startSingleLineElement(IndexConstants.AUTHOR_ELEMENT);
992 unparser.addString(author);
993 unparser.endElement(IndexConstants.AUTHOR_ELEMENT);
994 }
995
996 /*
997 String copyright = info.getCopyright();
998 if (copyright != null)
999 {
1000 unparser.startSingleLineElement(IndexConstants.COPYRIGHT_ELEMENT);
1001 unparser.addString(copyright);
1002 unparser.endElement(IndexConstants.COPYRIGHT_ELEMENT);
1003 }
1004 */
1005
1006 String[] altTitles = info.getAltTitles();
1007 if (altTitles != null)
1008 {
1009 unparser.startMultiLineElement(IndexConstants.ALTTITLES_ELEMENT);
1010 for (int i=0; i<altTitles.length; ++i)
1011 {
1012 unparser.startSingleLineElement(IndexConstants.STRING_ELEMENT);
1013 unparser.addString(altTitles[i]);
1014 unparser.endElement(IndexConstants.STRING_ELEMENT);
1015 }
1016 unparser.endElement(IndexConstants.ALTTITLES_ELEMENT);
1017 }
1018
1019 /*
1020 String[] keywords = info.getKeywords();
1021 if (keywords != null)
1022 {
1023 unparser.startMultiLineElement(IndexConstants.KEYWORDS_ELEMENT);
1024 for (int i=0; i<keywords.length; ++i)
1025 {
1026 unparser.startSingleLineElement(IndexConstants.STRING_ELEMENT);
1027 unparser.addString(keywords[i]);
1028 unparser.endElement(IndexConstants.STRING_ELEMENT);
1029 }
1030 unparser.endElement(IndexConstants.KEYWORDS_ELEMENT);
1031 }
1032 */
1033
1034 unparser.endElement(IndexConstants.ENTRY_ELEMENT);
1035 }
1036
1037 unparser.endElement(IndexConstants.INDEX_ELEMENT);
1038
1039 unparser.finishDocument(IndexConstants.INDEXROOT_ELEMENT);
1040 }
1041
1042
1043 //-------------------------------------------------------------------------
1044 // Methods of SongDatabase
1045 //-------------------------------------------------------------------------
1046
1047 /**
1048 * Returns true if the database is writable. If this returns false, every mutation
1049 * method will throw DatabaseNotWritableException. Note that even if this returns
1050 * false, the database may still be changed by other entities. In other words,
1051 * this method reflects the ability to write to the database through this interface,
1052 * not necessarily through all interfaces.
1053 *
1054 * @return true if the database is writable, false if not.
1055 */
1056 public boolean isWritable()
1057 {
1058 return fileDirectory_ != null;
1059 }
1060
1061
1062 /**
1063 * Get a song by SongID. Note that the Song returned is a copy. Changes
1064 * do not get reflected in the database until commitSong() succeeds.
1065 * Returns null if the song doesn't exist in this database. (This could
1066 * mean the song was deleted, or it is part of a different database.)
1067 *
1068 * @param id SongID
1069 * @return the Song, or null if the id doesn't exist in this database.
1070 * @exception SongDatabaseFailedException Catch-all exception for database-related
1071 * problems. This will often have a cause exception, which may be exceptions
1072 * like IOException or SQLException.
1073 * @exception NullPointerException id was null
1074 */
1075 public synchronized Song checkOutSong(
1076 SongID id)
1077 throws
1078 SongDatabaseFailedException
1079 {
1080 if (id.getDatabase() != this)
1081 {
1082 return null;
1083 }
1084 StringID stringId = ((SongIDImpl)id).getStringID();
1085 SongInfoCache info = (SongInfoCache)songMap_.get(stringId);
1086 if (info == null)
1087 {
1088 return null;
1089 }
1090
1091 Reader reader = getReader(info.getFileName());
1092 if (reader == null)
1093 {
1094 songMap_.remove(stringId);
1095 commitIndex();
1096 return null;
1097 }
1098
1099 MessageCollectingErrorHandler errorHandler = new MessageCollectingErrorHandler(10, 20);
1100 try
1101 {
1102 Song song = modelFactory_.parseSong(reader, id, errorHandler);
1103 info.updateFrom(song);
1104 return song;
1105 }
1106 catch (IOException ex)
1107 {
1108 throw new SongDatabaseFailedException(ex);
1109 }
1110 catch (SAXParseException ex)
1111 {
1112 try
1113 {
1114 errorHandler.error(ex);
1115 }
1116 catch (SAXException ex2)
1117 {
1118 }
1119 throw new SongDatabaseFailedException(errorHandler.toString());
1120 }
1121 catch (SAXException ex)
1122 {
1123 throw new SongDatabaseFailedException(ex);
1124 }
1125 finally
1126 {
1127 try
1128 {
1129 reader.close();
1130 }
1131 catch (IOException ex)
1132 {
1133 }
1134 }
1135 }
1136
1137
1138 /**
1139 * Tests whether the given song id exists in this database. Returns
1140 * false if the song was deleted or if it is part of a different database.
1141 *
1142 * @param id SongID to test
1143 * @return true if the given SongID exists in this database.
1144 * @exception SongDatabaseFailedException Catch-all exception for database-related
1145 * problems. This will often have a cause exception, which may be exceptions
1146 * like IOException or SQLException.
1147 * @exception NullPointerException id was null
1148 */
1149 public synchronized boolean containsSongID(
1150 SongID id)
1151 throws
1152 SongDatabaseFailedException
1153 {
1154 if (id.getDatabase() != this)
1155 {
1156 return false;
1157 }
1158 StringID stringId = ((SongIDImpl)id).getStringID();
1159 return songMap_.containsKey(stringId);
1160 }
1161
1162
1163 /**
1164 * Get the SongID for the given string, if it exists. This is used to
1165 * deserialize a SongID that was stored persistently as its string
1166 * representation. Returns null if the string does not correspond to a
1167 * SongID that exists in this database.
1168 *
1169 * @param idStr serialized ID
1170 * @return SongID
1171 * @exception SongDatabaseFailedException Catch-all exception for database-related
1172 * problems. This will often have a cause exception, which may be exceptions
1173 * like IOException or SQLException.
1174 */
1175 public synchronized SongID getSongIDForString(
1176 String idStr)
1177 throws
1178 SongDatabaseFailedException
1179 {
1180 if (!StringID.isWellFormed(idStr))
1181 {
1182 return null;
1183 }
1184 StringID id = new StringID(idStr);
1185 if (songMap_.containsKey(id))
1186 {
1187 return new SongIDImpl(id);
1188 }
1189 else
1190 {
1191 return null;
1192 }
1193 }
1194
1195
1196 /**
1197 * Create an empty result set.
1198 *
1199 * @return empty SongIDResultSet
1200 */
1201 public SongIDResultSet createEmptyResultSet()
1202 {
1203 return modelFactory_.createSongIDResultSet(this);
1204 }
1205
1206
1207 /**
1208 * Perform an operation on a set of songs. Operations could be accessors for
1209 * information that could be cached by the database, such as the main title, or they
1210 * could be filters applied to the result set, or they could have other semantics
1211 * such as mass-checkouts or mass-checkins.
1212 * The client specifies as the parameter a SongIDResultSet specifying the songs and
1213 * parameter data for the operation. If null is passed, the database creates a new
1214 * SongIDResultSet whose values are all the SongIDs in the database with null data
1215 * for each one.
1216 * The client also specifies the operation to perform.
1217 * The method performs the operation on the result set in place, and then returns it.
1218 * <p>
1219 * Note that some SongDatabase implementations may choose to optimize this method
1220 * by not calling the operation directly, but by analzying the semantics of the
1221 * operation as declared by which operation semantics interfaces are implemented, and
1222 * performing accelerated operations such as checking caches. Thus, do not expect that
1223 * the SongOperation object you pass will actually be invoked.
1224 * <p>
1225 * One common use of this method is to get a result set containing all the SongIDs
1226 * in the database. This is accomplished by passing null for the parameter, and
1227 * a NopSemantics implementation for the operation.
1228 *
1229 * @param operation SongOperation to perform
1230 * @param param input result set
1231 * @return output result set
1232 * @exception SongDatabaseFailedException Catch-all exception for database-related
1233 * problems. This will often have a cause exception, which may be exceptions
1234 * like IOException or SQLException.
1235 */
1236 public SongIDResultSet performOperation(
1237 SongOperation operation,
1238 SongIDResultSet param)
1239 throws
1240 SongDatabaseFailedException
1241 {
1242 if (param == null)
1243 {
1244 param = modelFactory_.createSongIDResultSet(this);
1245 synchronized(this)
1246 {
1247 for (Iterator iter = songMap_.keySet().iterator(); iter.hasNext(); )
1248 {
1249 StringID id = (StringID)iter.next();
1250 param.add(new SongIDImpl(id));
1251 }
1252 }
1253 }
1254
1255 if (operation instanceof GetFieldAsStringSemantics)
1256 {
1257 GetFieldAsStringSemantics getFieldOp = (GetFieldAsStringSemantics)operation;
1258 if (getFieldOp.getFieldName().equals(Song.MAINTITLE_FIELD))
1259 {
1260 performGetField(param, Song.MAINTITLE_FIELD);
1261 return param;
1262 }
1263 else if (getFieldOp.getFieldName().equals(Song.COMMENT_FIELD))
1264 {
1265 performGetField(param, Song.COMMENT_FIELD);
1266 return param;
1267 }
1268 else if (getFieldOp.getFieldName().equals(Song.AUTHOR_FIELD))
1269 {
1270 performGetField(param, Song.AUTHOR_FIELD);
1271 return param;
1272 }
1273 else if (getFieldOp.getFieldName().equals(Song.COPYRIGHT_FIELD))
1274 {
1275 performGetField(param, Song.COPYRIGHT_FIELD);
1276 return param;
1277 }
1278 }
1279 else if (operation instanceof GetFieldAsStringArraySemantics)
1280 {
1281 GetFieldAsStringArraySemantics getFieldOp = (GetFieldAsStringArraySemantics)operation;
1282 if (getFieldOp.getFieldName().equals(Song.ALTERNATETITLELIST_FIELD))
1283 {
1284 performGetField(param, Song.ALTERNATETITLELIST_FIELD);
1285 return param;
1286 }
1287 else if (getFieldOp.getFieldName().equals(Song.KEYWORDLIST_FIELD))
1288 {
1289 performGetField(param, Song.KEYWORDLIST_FIELD);
1290 return param;
1291 }
1292 }
1293 else if (operation instanceof SearchFieldPredicateSemantics)
1294 {
1295 SearchFieldPredicateSemantics searchFieldOp = (SearchFieldPredicateSemantics)operation;
1296 String fieldName = searchFieldOp.getFieldName();
1297 if (fieldName.equals(Song.MAINTITLE_FIELD))
1298 {
1299 performSearchField(param, Song.MAINTITLE_FIELD, searchFieldOp.getSearchParameters());
1300 return param;
1301 }
1302 else if (fieldName.equals(Song.COMMENT_FIELD))
1303 {
1304 performSearchField(param, Song.COMMENT_FIELD, searchFieldOp.getSearchParameters());
1305 return param;
1306 }
1307 else if (fieldName.equals(Song.AUTHOR_FIELD))
1308 {
1309 performSearchField(param, Song.AUTHOR_FIELD, searchFieldOp.getSearchParameters());
1310 return param;
1311 }
1312 else if (fieldName.equals(Song.COPYRIGHT_FIELD))
1313 {
1314 performSearchField(param, Song.COPYRIGHT_FIELD, searchFieldOp.getSearchParameters());
1315 return param;
1316 }
1317 else if (fieldName.equals(Song.ALTERNATETITLELIST_FIELD))
1318 {
1319 performSearchField(param, Song.ALTERNATETITLELIST_FIELD, searchFieldOp.getSearchParameters());
1320 return param;
1321 }
1322 else if (fieldName.equals(Song.KEYWORDLIST_FIELD))
1323 {
1324 performSearchField(param, Song.KEYWORDLIST_FIELD, searchFieldOp.getSearchParameters());
1325 return param;
1326 }
1327 }
1328
1329 operation.perform(param);
1330 return param;
1331 }
1332
1333
1334 private void performGetField(
1335 SongIDResultSet resultSet,
1336 String fieldName)
1337 {
1338 for (Iterator iter = resultSet.getEntryCollection().iterator(); iter.hasNext(); )
1339 {
1340 SongIDResultSet.Entry entry = (SongIDResultSet.Entry)iter.next();
1341 SongIDImpl id = (SongIDImpl)entry.getSongID();
1342 SongInfoCache info = (SongInfoCache)songMap_.get(id.getStringID());
1343 entry.setData(null);
1344 if (info != null)
1345 {
1346 if (fieldName == Song.MAINTITLE_FIELD)
1347 {
1348 entry.setData(info.forceGetMainTitle());
1349 }
1350 else if (fieldName == Song.COMMENT_FIELD)
1351 {
1352 entry.setData(info.forceGetComment());
1353 }
1354 else if (fieldName == Song.AUTHOR_FIELD)
1355 {
1356 entry.setData(info.forceGetAuthor());
1357 }
1358 else if (fieldName == Song.COPYRIGHT_FIELD)
1359 {
1360 entry.setData(info.forceGetCopyright());
1361 }
1362 else if (fieldName == Song.ALTERNATETITLELIST_FIELD)
1363 {
1364 entry.setData(info.forceGetAltTitles());
1365 }
1366 else if (fieldName == Song.KEYWORDLIST_FIELD)
1367 {
1368 entry.setData(info.forceGetKeywords());
1369 }
1370 else
1371 {
1372 assert false;
1373 }
1374 }
1375 }
1376 }
1377
1378
1379 private void performSearchField(
1380 SongIDResultSet resultSet,
1381 String fieldName,
1382 SearchParameters params)
1383 {
1384 for (Iterator iter = resultSet.getEntryCollection().iterator(); iter.hasNext(); )
1385 {
1386 SongIDResultSet.Entry entry = (SongIDResultSet.Entry)iter.next();
1387 SongIDImpl id = (SongIDImpl)entry.getSongID();
1388 SongInfoCache info = (SongInfoCache)songMap_.get(id.getStringID());
1389
1390 if (fieldName == Song.MAINTITLE_FIELD ||
1391 fieldName == Song.COMMENT_FIELD ||
1392 fieldName == Song.AUTHOR_FIELD ||
1393 fieldName == Song.COPYRIGHT_FIELD)
1394 {
1395 String value;
1396 if (fieldName == Song.MAINTITLE_FIELD)
1397 {
1398 value = info.forceGetMainTitle();
1399 }
1400 else if (fieldName == Song.COMMENT_FIELD)
1401 {
1402 value = info.forceGetComment();
1403 }
1404 else if (fieldName == Song.AUTHOR_FIELD)
1405 {
1406 value = info.forceGetAuthor();
1407 }
1408 else //if (fieldName == Song.COPYRIGHT_FIELD)
1409 {
1410 value = info.forceGetCopyright();
1411 }
1412 if (params.matches(value))
1413 {
1414 entry.setData(Boolean.TRUE);
1415 }
1416 else
1417 {
141