001 /** 002 * iFish - An iRiver iHP jukebox database creation tool 003 * 004 * Copyright (C) 2009 Richard "Shred" Körber 005 * http://ifish.shredzone.org 006 * 007 * This program is free software: you can redistribute it and/or modify 008 * it under the terms of the GNU General Public License as published by 009 * the Free Software Foundation, either version 3 of the License, or 010 * (at your option) any later version. 011 * 012 * This program is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 015 * GNU General Public License for more details. 016 * 017 * You should have received a copy of the GNU General Public License 018 * along with this program. If not, see <http://www.gnu.org/licenses/>. 019 */ 020 package net.shredzone.ifish.db; 021 022 import java.io.*; 023 import java.util.*; 024 025 /** 026 * This class contains the database, and is able to read and write the 027 * iRivNavi.iDB file, as well as synchronize with a directory. 028 * 029 * @author Richard Körber <dev@shredzone.de> 030 * @version $Id: NaviDb.java 291 2009-04-28 21:29:27Z shred $ 031 */ 032 public class NaviDb { 033 protected final SortedSet<Entry> sEntries = new TreeSet<Entry>(); 034 private final Map<File, Entry> mFiles = new HashMap<File, Entry>(); 035 private int addCounter; 036 private int updateCounter; 037 private int removeCounter; 038 private long lastModifiedDB = 0L; 039 040 /** 041 * Create an empty database. 042 */ 043 public NaviDb() { 044 resetCounters(); 045 } 046 047 /** 048 * Create a database and fill it with a database file. 049 * 050 * @param file Database file to be read 051 * @param charset Desired Charset 052 * @throws IOException File was erraneous or could not be read 053 */ 054 public NaviDb( File file, String charset ) 055 throws IOException, DatabaseException, UnsupportedEncodingException { 056 resetCounters(); 057 importFile( file, charset ); 058 } 059 060 /** 061 * Add an entry to the database. If the entry does already exist 062 * (i.e. there is an Entry with the same file name), is will be 063 * replaced by the given entry (maybe having new tag attributes). 064 * 065 * @param entry Entry to be added 066 */ 067 public void addEntry( Entry entry ) 068 throws DatabaseException { 069 if( sEntries.size() >= 9999 ) 070 throw new DatabaseException( "Database limit (9999 entries) exceeded" ); 071 072 //--- Add/Replace the entry --- 073 if( sEntries.contains( entry ) ) { 074 sEntries.remove( entry ); // This is because the Set will not 075 sEntries.add( entry ); // replace an existing Entry. 076 updateCounter++; 077 }else { 078 sEntries.add( entry ); 079 addCounter++; 080 081 //--- Remember seen files --- 082 File file = entry.getFile(); 083 if( file!=null ) 084 mFiles.put( file, entry ); 085 } 086 } 087 088 /** 089 * Remove an entry from the database. If the entry does not exist, 090 * nothing will happen. The file on the harddisk that is connected 091 * to the Entry, will be kept untouched. 092 * 093 * @param entry Entry to be removed 094 */ 095 public void removeEntry( Entry entry ) { 096 //--- Remove the file --- 097 File file = entry.getFile(); 098 if( file!=null ) 099 mFiles.remove( file ); 100 101 //--- Remove entry --- 102 if( sEntries.remove( entry ) ) 103 removeCounter++; 104 } 105 106 /** 107 * Reset the add and remove counters. 108 */ 109 public void resetCounters() { 110 addCounter = 0; 111 updateCounter = 0; 112 removeCounter = 0; 113 } 114 115 /** 116 * Get the number of entries that have been added since last counter 117 * reset. This is useful if you want to write out some statistics 118 * after an import, export or sync operation. 119 * 120 * @return Number of entries added. 121 */ 122 public int getAddCounter() { 123 return addCounter; 124 } 125 126 /** 127 * Get the number of entries that have been updated since last counter 128 * reset. This is useful if you want to write out some statistics 129 * after an import, export or sync operation. 130 * 131 * @return Number of entries updated. 132 */ 133 public int getUpdateCounter() { 134 return updateCounter; 135 } 136 137 /** 138 * Get the number of entries that have been removed since last counter 139 * reset. This is useful if you want to write out some statistics 140 * after an import, export or sync operation. 141 * 142 * @return Number of entries removed. 143 */ 144 public int getRemoveCounter() { 145 return removeCounter; 146 } 147 148 /** 149 * Get the creation date of this database. 150 * 151 * @return Creation date 152 */ 153 public Date getCreationDate() { 154 if( lastModifiedDB==0L ) { 155 return new Date(); 156 }else { 157 return new Date( lastModifiedDB ); 158 } 159 } 160 161 /** 162 * Check if a File is known by this database. This is useful for 163 * database synchronisation. 164 * 165 * @param file File to be checked 166 * @return true: file is known to this database, false: unknown 167 */ 168 public boolean knowsFile( File file ) { 169 return mFiles.containsKey( file ); 170 } 171 172 /** 173 * Get the Entry for the given File. If there is no Entry for this 174 * file in the database, null will be returned. 175 * 176 * @param file File to be checked 177 * @return Entry to this file, or null 178 */ 179 public Entry getEntry( File file ) { 180 return (Entry) mFiles.get( file ); 181 } 182 183 /** 184 * Get the Entry for the given database filename (i.e. the jukebox' 185 * filename). If there is no Entry for this filename in the database, 186 * null will be returned. 187 * 188 * @param base The root of the jukebox directory 189 * @param fname Filename to be checked 190 * @return Entry to this file, or null 191 */ 192 public Entry getEntry( File base, String fname ) { 193 //--- Convert Windows to this platform's notation --- 194 if( File.separatorChar != '\\' ) { 195 fname = fname.replace( '\\', File.separatorChar ); 196 } 197 198 //--- Get the entry for this file --- 199 return getEntry( new File( base, fname ) ); 200 } 201 202 /** 203 * Check if a File has been modified after the current database has 204 * been written to the harddisk. This can be used to find out if a 205 * file needs to be updated in the database. 206 * <p> 207 * If the database was not read yet, then this method will 208 * always return true. If the file did not exist, this method will 209 * always return false. 210 * 211 * @param file File to be checked 212 * @return true: file was modified after DB creation. false: wasn't. 213 */ 214 public boolean modifiedFile( File file ) { 215 if( lastModifiedDB==0L ) return true; 216 if( !file.exists() ) return false; 217 return( file.lastModified() > lastModifiedDB ); 218 } 219 220 /** 221 * Import a database file into this database. 222 * 223 * @param file Database file to be imported 224 * @param charset Charset the database is written in 225 */ 226 public void importFile( File file, String charset ) 227 throws IOException, DatabaseException, UnsupportedEncodingException { 228 importFile( file, charset, null ); 229 } 230 231 /** 232 * Import a database file into this database. This method automatically 233 * detects whether the database file was written in UTF-16 or in an old 234 * format. If the old format was detected, the given charset is used to 235 * decode the strings. 236 * 237 * @param file Database file to be imported 238 * @param charset Charset the database is written in 239 * @param cb StatusCallback to be used 240 */ 241 public void importFile( File file, String charset, StatusCallback cb ) 242 throws IOException, DatabaseException, UnsupportedEncodingException { 243 File base = file.getParentFile(); 244 245 InputStream in = null; 246 try { 247 in = new BufferedInputStream( new FileInputStream( file ) ); 248 //--- Check if it is an iRivDB file --- 249 byte[] buf = new byte[6]; 250 if( 6 != in.read( buf )) 251 throw new IOException( "no database file" ); 252 String header = new String( buf, "US-ASCII" ); 253 if( !header.equals("iRivDB") ) 254 throw new IOException( "no database file" ); 255 256 //--- Skip to the number of entries --- 257 long skipCnt = 0x0040 - 0x0006; 258 while( skipCnt>0 ) { 259 long skipped = in.skip( skipCnt ); 260 if( skipped<0 ) 261 throw new IOException( "premature EOF" ); 262 skipCnt -= skipped; 263 } 264 265 //--- Read number of entries --- 266 byte[] nbuf = new byte[4]; 267 if( 4 != in.read( nbuf )) 268 throw new IOException( "premature EOF" ); 269 int count = nbuf[0] & 0xFF; 270 count |= (nbuf[1] & 0xFF) << 8; 271 count |= (nbuf[2] & 0xFF) << 16; 272 count |= (nbuf[3] & 0xFF) << 24; 273 if( count>9999 ) 274 throw new IOException( "no database file" ); 275 if( cb!=null ) 276 cb.setMaxEntries( count ); 277 278 //--- Skip to the data clusters --- 279 skipCnt = 0xA0C0 - 0x0044; 280 while( skipCnt>0 ) { 281 long skipped = in.skip( skipCnt ); 282 if( skipped<0 ) 283 throw new IOException( "premature EOF" ); 284 skipCnt -= skipped; 285 } 286 287 //--- Read the data clusters --- 288 try { 289 int cnt = 0; 290 for(;;) { 291 Entry entry = new DbEntry( base, in, charset ); 292 addEntry( entry ); 293 if( cb!=null ) { 294 cb.setCurrentIndex( cnt++ ); 295 cb.setCurrentEntry( entry ); 296 } 297 } 298 }catch( IOException e ) {} 299 300 //--- Remember the modification date --- 301 lastModifiedDB = file.lastModified(); 302 303 }finally { 304 if( in!=null ) 305 in.close(); 306 } 307 } 308 309 /** 310 * Export this database to a database file. 311 * 312 * @param file File to be written to. 313 * @param charset Charset to be used for export. 314 */ 315 public void exportFile( File file, String charset ) 316 throws IOException, UnsupportedEncodingException { 317 exportFile( file, charset, null ); 318 } 319 320 /** 321 * Export this database to a database file. If "UTF-16" is given as charset, 322 * the new database file format is used automatically. 323 * 324 * @param file File to be written to. 325 * @param charset Charset to be used for export. 326 * @param cb StatusCallback to be used. 327 */ 328 public void exportFile( File file, String charset, StatusCallback cb ) 329 throws IOException, UnsupportedEncodingException { 330 if( cb!=null ) 331 cb.setMaxEntries( sEntries.size() ); 332 333 //--- Nullbyte Array --- 334 byte[] nulls = new byte[32]; 335 336 OutputStream out = null; 337 try { 338 out = new BufferedOutputStream( new FileOutputStream( file ) ); 339 340 //--- Write Header --- 341 if( charset.equals("UTF-16") ) { 342 out.write( "iRivDB Ver 0.16".getBytes( "US-ASCII" ) ); 343 }else { 344 out.write( "iRivDB Ver 0.12".getBytes( "US-ASCII" ) ); 345 } 346 out.write( nulls, 0, 17 ); 347 out.write( "iRiver iHP-100 DB File".getBytes( "US-ASCII" ) ); 348 out.write( nulls, 0, 10 ); 349 350 //--- Write Number of Entries --- 351 writeLong( out, sEntries.size() ); 352 out.write( nulls, 0, 28 ); 353 if( charset.equals("UTF-16") ) { 354 out.write( nulls, 0, 16 ); 355 out.write( "APP Ver 1.70T0 \0".getBytes( "US-ASCII" ) ); 356 }else { 357 out.write( nulls, 0, 32 ); 358 } 359 360 //--- Write Offset Table --- 361 int offset = 0; 362 Iterator<Entry> it = iterator(); 363 while( it.hasNext() ) { 364 Entry entry = (Entry) it.next(); 365 writeLong( out, offset ); 366 offset += entry.size( charset ); 367 } 368 369 //--- Offset Table Padding --- 370 for( int cnt=sEntries.size(); cnt<10240; cnt++ ) { 371 out.write( nulls, 0, 4 ); 372 } 373 374 //--- Second Header --- 375 out.write( "Designed by iRiver".getBytes( "US-ASCII" ) ); 376 out.write( nulls, 0, 14 ); 377 out.write( nulls, 0, 32 ); 378 379 //--- Entries --- 380 it = iterator(); 381 int cnt = 0; 382 while( it.hasNext() ) { 383 Entry entry = (Entry) it.next(); 384 entry.write( out, charset ); 385 if( cb!=null ) { 386 cb.setCurrentIndex( cnt++ ); 387 cb.setCurrentEntry( entry ); 388 } 389 } 390 391 //--- Remember the modification date --- 392 lastModifiedDB = file.lastModified(); 393 394 //--- Done --- 395 }finally { 396 if( out!=null ) 397 out.close(); 398 } 399 } 400 401 /** 402 * Get the number of entries in this database. 403 * 404 * @return Number of entries 405 */ 406 public int size() { 407 return sEntries.size(); 408 } 409 410 /** 411 * Get an iterator of all entries. 412 * 413 * @return Iterator 414 */ 415 public Iterator<Entry> iterator() { 416 return sEntries.iterator(); 417 } 418 419 /** 420 * Write 4 bytes in little endian order. 421 * 422 * @param out OutputStream to write to 423 * @param val Value to write 424 * @throws IOException 425 */ 426 private void writeLong( OutputStream out, int val ) throws IOException { 427 out.write( (val ) & 0xFF ); 428 out.write( (val>>8 ) & 0xFF ); 429 out.write( (val>>16) & 0xFF ); 430 out.write( (val>>24) & 0xFF ); 431 } 432 433 }