001/* 002 * Shredzone Commons - pdb 003 * 004 * Copyright (C) 2009 Richard "Shred" Körber 005 * http://commons.shredzone.org 006 * 007 * This program is free software: you can redistribute it and/or modify 008 * it under the terms of the GNU Library General Public License as 009 * published by the Free Software Foundation, either version 3 of the 010 * License, or (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 Library General Public License 018 * along with this program. If not, see <http://www.gnu.org/licenses/>. 019 */ 020package org.shredzone.commons.pdb; 021 022import java.io.ByteArrayOutputStream; 023import java.io.File; 024import java.io.FileNotFoundException; 025import java.io.IOException; 026import java.io.RandomAccessFile; 027import java.util.Calendar; 028 029import org.shredzone.commons.pdb.appinfo.AppInfo; 030import org.shredzone.commons.pdb.appinfo.CategoryAppInfo; 031import org.shredzone.commons.pdb.appinfo.CategoryAppInfo.Category; 032import org.shredzone.commons.pdb.converter.Converter; 033import org.shredzone.commons.pdb.record.Record; 034 035/** 036 * Opens a PDB file and gives access to its contents. 037 * 038 * @see <a href="http://membres.multimania.fr/microfirst/palm/pdb.html">The Pilot Record Database Format</a> 039 */ 040public class PdbFile extends RandomAccessFile { 041 042 private static final String CHARSET = "iso-8859-1"; 043 private static final int NUM_CATEGORIES = 16; 044 045 private CalendarFactory cf = CalendarFactory.getInstance(); 046 047 /** 048 * Creates a new {@link PdbFile} for the given {@link File}. 049 * 050 * @param file 051 * {@link File} to be opened 052 */ 053 public PdbFile(File file) throws FileNotFoundException { 054 super(file, "r"); 055 } 056 057 /** 058 * Reads the entire database file and returns a {@link PdbDatabase}. You usually want 059 * to invoke this method, as the other methods are just helpers. 060 * 061 * @param <T> 062 * {@link Record} subclass the database shall consist of 063 * @param converter 064 * {@link Converter} that converts the raw database entries into 065 * {@link Record} objects 066 * @return {@link PdbDatabase} containing the file contents 067 * @throws IOException 068 * The file could not be read. This can have various reasons, for example 069 * if the file was no valid PDB file or the converter was not able to 070 * convert the file's contents. 071 */ 072 public <T extends Record, U extends AppInfo> PdbDatabase<T, U> readDatabase(Converter<T, U> converter) 073 throws IOException { 074 PdbDatabase<T, U> result = new PdbDatabase<>(); 075 076 // Read the database header 077 seek(0); 078 result.setName(readTerminatedFixedString(32)); 079 result.setAttributes(readShort()); 080 result.setVersion(readShort()); 081 result.setCreationTime(readDate()); 082 result.setModificationTime(readDate()); 083 result.setBackupTime(readDate()); 084 result.setModificationNumber(readInt()); 085 int appInfoPos = readInt(); 086 int sortInfoPos = readInt(); 087 result.setType(readFixedString(4)); 088 result.setCreator(readFixedString(4)); 089 readInt(); // Unique ID seed 090 readInt(); // Next index 091 int records = readShort(); 092 093 // Read the entire record list 094 int[] offsets = new int[records]; 095 int[] attributes = new int[records]; 096 for (int ix = 0; ix < records; ix++) { 097 offsets[ix] = readInt(); 098 attributes[ix] = readUnsignedByte(); 099 readByte(); 100 readShort(); 101 } 102 103 // Ask converter if it accepts the content 104 if (!converter.isAcceptable(result)) { 105 throw new IOException("Wrong database format"); 106 } 107 108 // Read appInfo if available 109 if (appInfoPos > 0) { 110 int endPos = (records > 0) ? offsets[0] : (int) length(); 111 if (sortInfoPos > appInfoPos && sortInfoPos < endPos) { 112 endPos = sortInfoPos; 113 } 114 int size = endPos - appInfoPos; 115 116 seek(appInfoPos); 117 result.setAppInfo(converter.convertAppInfo(this, size, result)); 118 } 119 120 // Read each record 121 for (int ix = 0; ix < records; ix++) { 122 if (offsets[ix] >= length()) { 123 continue; 124 } 125 126 int size; 127 if (ix < records - 1) { 128 size = offsets[ix + 1] - offsets[ix]; 129 } else { 130 size = ((int) length()) - offsets[ix]; 131 } 132 133 seek(offsets[ix]); 134 T entry = converter.convert(this, ix, size, attributes[ix], result); 135 if (entry != null) { 136 result.getRecords().add(entry); 137 } 138 } 139 140 return result; 141 } 142 143 /** 144 * Reads a string of a fixed length, not null terminated. 145 * 146 * @param length 147 * The length of the string 148 * @return String that was read 149 */ 150 public String readFixedString(int length) throws IOException { 151 byte[] data = new byte[length]; 152 readFully(data); 153 return convertSpecialChars(new String(data, CHARSET)); 154 } 155 156 /** 157 * Reads a string of a fixed length that is null terminated. The given number of bytes 158 * are always read, but the everything including and after the terminator character is 159 * ignored. 160 * 161 * @param length 162 * The length of the string 163 * @return String that was read 164 */ 165 public String readTerminatedFixedString(int length) throws IOException { 166 byte[] data = new byte[length]; 167 readFully(data); 168 int pos = 0; 169 while (pos < length) { 170 if (data[pos] == 0) { 171 return new String(data, 0, pos, CHARSET); 172 } 173 pos++; 174 } 175 return convertSpecialChars(new String(data, CHARSET)); 176 } 177 178 /** 179 * Reads a string of a variable length that is null terminated. 180 * 181 * @return String that was read 182 */ 183 public String readTerminatedString() throws IOException { 184 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 185 186 while(true) { 187 int ch = readByte(); 188 if (ch == 0) break; 189 baos.write(ch); 190 } 191 192 return convertSpecialChars(baos.toString(CHARSET)); 193 } 194 195 /** 196 * Reads an unsigned integer. 197 * 198 * @return Unsigned integer that was read 199 */ 200 public long readUnsignedInt() throws IOException { 201 int msb = readUnsignedShort(); 202 int lsb = readUnsignedShort(); 203 return ((long) msb) << 16 | lsb; 204 } 205 206 /** 207 * Reads a PalmOS date. 208 * 209 * @return Calendar that was read. May be {@code null} if no date was set. 210 */ 211 public Calendar readDate() throws IOException { 212 long date = readUnsignedInt(); 213 if (date > 0) { 214 Calendar result = cf.createPalmEpoch(); 215 result.setTimeInMillis(result.getTimeInMillis() + (date * 1000L)); 216 return result; 217 } else { 218 return null; 219 } 220 } 221 222 /** 223 * Reads a packed PalmOS date. 224 * 225 * @return Calendar containing the date read. May be {@code null} if no date 226 * was set. The time part is always midnight local time. 227 */ 228 public Calendar readPackedDate() throws IOException { 229 int packed = readUnsignedShort(); 230 231 if (packed == 0xFFFF) { 232 return null; 233 } 234 235 int year = ((packed >> 9) & 0x007F) + CalendarFactory.EPOCH_YEAR; 236 int month = ((packed >> 5) & 0x000F); 237 int day = ((packed ) & 0x001F); 238 239 Calendar cal = cf.create(); 240 cal.clear(); 241 cal.set(year, month - 1, day); 242 return cal; 243 } 244 245 /** 246 * Reads a PalmOS date and time that is stored in seven words. 247 * 248 * @return Calendar that was read 249 */ 250 public Calendar readDateTimeWords() throws IOException { 251 int second = readUnsignedShort(); 252 int minute = readUnsignedShort(); 253 int hour = readUnsignedShort(); 254 int day = readUnsignedShort(); 255 int month = readUnsignedShort(); // 1..12 256 int year = readUnsignedShort(); // 4 digits 257 readUnsignedShort(); // day of week, to be ignored... 258 259 Calendar cal = cf.create(); 260 cal.clear(); 261 cal.set(year, month - 1, day, hour, minute, second); 262 return cal; 263 } 264 265 /** 266 * Reads the categories from a standard appinfo area and fills them into a 267 * {@link CategoryAppInfo} object. After invocation, the file pointer points after the 268 * category part, where further application information may be stored. 269 * 270 * @param appInfo 271 * {@link CategoryAppInfo} where the categories are stored. 272 * @return Bytes that were actually read from the appinfo area. The file pointer is 273 * located at the beginning of the appinfo area plus the result of this 274 * method. 275 */ 276 public int readCategories(CategoryAppInfo appInfo) 277 throws IOException { 278 long startPos = getFilePointer(); 279 280 // Read the rename flags 281 int renamed = readShort(); 282 283 // Read the category names 284 String[] catNames = new String[NUM_CATEGORIES]; 285 for (int ix = 0; ix < NUM_CATEGORIES; ix++) { 286 String catName = readTerminatedFixedString(16); 287 if (catName.length() > 0) { 288 catNames[ix] = catName; 289 } 290 } 291 292 // Read the category keys 293 for (int ix = 0; ix < NUM_CATEGORIES; ix++) { 294 int key = readByte(); 295 296 if (catNames[ix] != null) { 297 appInfo.getCategories().add(new Category( 298 catNames[ix], 299 key, 300 (renamed & (1 << ix)) != 0 301 )); 302 } else { 303 appInfo.getCategories().add(null); 304 } 305 } 306 307 readByte(); // last unique ID 308 readByte(); // padding 309 310 long endPos = getFilePointer(); 311 312 return (int) (endPos - startPos); 313 } 314 315 /** 316 * Converts special PalmOS characters into their unicode equivalents. The string 317 * methods of {@link PdbFile} will invoke this method by itself, so you usually do not 318 * need to invoke it. Note that some very special PalmOS characters cannot be 319 * converted (as there are no unicode equivalents) and will be kept unchanged. 320 * 321 * @param str 322 * String to be converted 323 * @return Converted string 324 */ 325 public static String convertSpecialChars(String str) { 326 return str 327 .replace('\u0018', '\u2026') // Ellipsis 328 .replace('\u0019', '\u2007') // Numeric Space 329 .replace('\u0080', '\u20AC') // Euro 330 .replace('\u0082', '\u201A') // Single Low Quotation Mark 331 .replace('\u0083', '\u0192') // Small F with Hook 332 .replace('\u0084', '\u201E') // Double Low Quotation Mark 333 .replace('\u0085', '\u2026') // Ellipsis 334 .replace('\u0086', '\u2020') // Dagger 335 .replace('\u0087', '\u2021') // Double Dagger 336 .replace('\u0088', '\u0302') // Combining Circumflex Accent 337 .replace('\u0089', '\u2030') // Per Mille 338 .replace('\u008A', '\u0160') // Capital S with Caron 339 .replace('\u008B', '\u2039') // Single Left-pointing Angle Quotation Mark 340 .replace('\u008C', '\u0152') // Capital Ligature OE 341 .replace('\u008D', '\u2662') // Diamond 342 .replace('\u008E', '\u2663') // Club 343 .replace('\u008F', '\u2661') // Heart 344 .replace('\u0090', '\u2660') // Spade 345 .replace('\u0091', '\u2018') // Left Single Quotation Mark 346 .replace('\u0092', '\u2019') // Right Single Quotation Mark 347 .replace('\u0093', '\u201C') // Left Double Quotation Mark 348 .replace('\u0094', '\u201D') // Right Double Quotation Mark 349 .replace('\u0095', '\u2219') // Bullet 350 .replace('\u0096', '\u2011') // Non-breaking Hyphen 351 .replace('\u0097', '\u2012') // Figure Dash 352 .replace('\u0098', '\u0303') // Combining Tilde 353 .replace('\u0099', '\u2122') // Trademark 354 .replace('\u009A', '\u0161') // Small S with Caron 355 .replace('\u009B', '\u203A') // Single Right-pointing Angle Quotation Mark 356 .replace('\u009C', '\u0153') // Small Ligature OE 357 .replace('\u009F', '\u0178') // Capital Y with Diaeresis 358 ; 359 } 360 361}