/** * The following note was submitted by a reader *hi, * *i don't know if it's interesting for anybody, but the example file on *http://www.notsofaqs.com/palm/dbareader.java.txt has an error in line 713: * *int len = is.readByte(); * *has to be * *int len = is.readUnsignedByte(); * *maybe sun has changed the api since 1.2(?), i'm now using 1.6. * *thank you for the site (scott) and the sample code (chris)! * *greetings * *alexander stecker * **/ /** * Author: Chris Wilson dbareader@yepher.com * * Purpose: Example code for accessing the Palm datebook.dat file * * Inteteded use: Make it easy for a jsp to generate web base calendars * from Palms .dba file through an abstract interface. * * References Used: * http://www.geocities.com/Heartland/Acres/3216/palmrecs.htm * * Decode Document: * http://www.geocities.com/Heartland/Acres/3216/datebook_dat.htm * * Refernce implementation: * Scott Leighton's dbtest.pl (December 12, 1999) where decode document was * not clear * * Permission granted to freely distribute provided that no * money changes hands, otherwise contact me: dbareader@yepher.com. * Use this program and code contained here in at your own risk. * It may not perform as intended and may cause corruption of data. * If you do find bugs or make improvements please send me a copy ;) * * This program has only been tested on Linux/Intel systems using datebook * files produced from Windows/Intel. I expect there should be no problems * running the app on a Windos machine though. * * TODO: * # Create abstract access interface * # Create access interface * # Fix StreamUtil read int/short * **/ import java.io.DataInputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Date; /** * This class is immutable so I expect it is thread safe but I have not tested * to make sure. */ public final class DbaReader { private boolean m_verbose = false; private final String m_dbName; private DataInputStream m_data; private InputStream m_inputStream; private Data m_db; public DbaReader(String dbFile) { if (dbFile == null) throw new NullPointerException("DB File was null"); m_dbName = dbFile; } public void setVerbose(boolean val) { m_verbose = val; } public void decode() throws IOException { // Only parse DB once if (m_db != null) return; if (m_data == null || m_inputStream == null) { m_inputStream = new FileInputStream(m_dbName); m_data = new DataInputStream(m_inputStream); } m_db = new Data(m_data, m_verbose); // Clean up we don't need the file anymore we only parse it once m_data.close(); m_data = null; m_inputStream = null; } public void finalize() { try { if (m_data != null) { m_data.close(); m_data = null; } } catch(Exception e) { } } public static final void main(String[] args) throws IOException { if (args.length != 1) { System.out.println("Usage: java dbaReader [.dba file]"); System.exit(-1); } DbaReader dba = new DbaReader(args[0]); dba.setVerbose(true); dba.decode(); } } // --------------------------------------------------------------------------- // Data class that contains all datebook entries // --------------------------------------------------------------------------- final class Data { private boolean m_verbose = false; private static final int DATEBOOKVERSION = 0x44420100; private final int m_version; private final String m_origName; private final String m_showHeader; private final int m_nextFreeCat; private final int m_CategoryCount; private final int m_schemaResourceID; private final int m_fieldsPerRow; private final int m_recordIdPos; private final int m_recordStatusPos; private final int m_recordPlacementPos; private final int m_schemaFieldCount; private final short[] m_fieldEntrys; private final int m_numEntries; private final DateBookEntry[] m_entries; public Data(DataInputStream is, boolean verbose) throws IOException { m_verbose = verbose; print("** Data **"); if (is == null) throw new NullPointerException("Input stream was null"); m_version = StreamUtil.readInt(is); print("Version: " + m_version); if (m_version != DATEBOOKVERSION) { throw new IllegalArgumentException("Illegal file format: " + m_version); } m_origName = StreamUtil.readCString(is); print("Orig File: " + m_origName); m_showHeader = StreamUtil.readCString(is); print("Show Header: " + m_showHeader); m_nextFreeCat = StreamUtil.readInt(is); print("Next Free Cat: " + m_nextFreeCat); m_CategoryCount = is.readInt(); print("Category Count: " + m_CategoryCount); // If there were Category Entries we would decode them here. if (m_CategoryCount != 0x00) throw new IllegalArgumentException("Decoder does not support Category Entries"); m_schemaResourceID = StreamUtil.readInt(is); print("Schema ResId: " + m_schemaResourceID); m_fieldsPerRow = StreamUtil.readInt(is); print("Fields per Row (15): " + m_fieldsPerRow); if (m_fieldsPerRow != 0x0f) throw new IllegalArgumentException("Illegal Field Count"); m_recordIdPos = StreamUtil.readInt(is); print("Record Pos: " + m_recordIdPos); m_recordStatusPos = StreamUtil.readInt(is); print("Record Status Pos: " + m_recordStatusPos); m_recordPlacementPos = StreamUtil.readInt(is); print("Record Placement Pos: " + m_recordPlacementPos); m_schemaFieldCount = StreamUtil.readShort(is); print("Schema Field Count: " + m_schemaFieldCount); m_fieldEntrys = new short[m_schemaFieldCount]; for (int i = 0; i < m_schemaFieldCount; i++) { m_fieldEntrys[i] = StreamUtil.readShort(is); print("\tEntry " + i + " is " + m_fieldEntrys[i]); } m_numEntries = StreamUtil.readInt(is); print("Num Entries: " + m_numEntries + " \n\t actual: " + m_numEntries/15); m_entries = new DateBookEntry[m_numEntries/15]; print("Header read successfully"); parseEntries(is); } private void parseEntries(DataInputStream is) throws IOException { long start = System.currentTimeMillis(); for(int i = 0; i < m_entries.length; i++) { print("Entry: " + i + " of " + m_entries.length); m_entries[i] = new DateBookEntry(is, m_verbose); // StreamUtil.dumpBytes(is, 20); } long end = System.currentTimeMillis(); System.err.println("Parsed " + m_entries.length + " in " + (end-start) + " mSec"); } private void print(String str) { if (!m_verbose) return; System.out.println(str); } } // --------------------------------------------------------------------------- // Class that contains an entry from the datebook // --------------------------------------------------------------------------- final class DateBookEntry { private boolean m_verbose; private final int m_fieldType1, m_recordId, m_fieldType2, m_statusField, m_fieldType3, m_pos, m_fieldType4; private final long m_startTime; private final int m_fieldType5; private final long m_endTime; private final int m_fieldType6, m_padding1; private final String m_description; private final int m_fiedlType7, m_duration, m_fieldType8, m_padding2; private final String m_note; private final int m_fieldType9, m_unTimed, m_fieldType10, m_private, m_fieldType11, m_category, m_fieldType12, m_alarmSet, m_fieldType13, m_alarmAdvUnits, m_fieldType14, m_alarmAdvType, m_fieldType15; private final RepeatEvent m_repeatEvent; public DateBookEntry(DataInputStream is, boolean verbose) throws IOException { m_verbose = verbose; print("** datebook entry **"); Date date; m_fieldType1 = StreamUtil.readInt(is); print("\tfieldType1: " + m_fieldType1); m_recordId = StreamUtil.readInt(is); print("\trecordId: " + m_recordId); m_fieldType2 = StreamUtil.readInt(is); print("\tfieldType2: " + m_fieldType2); m_statusField = StreamUtil.readInt(is); print("\tstatusField: " + m_statusField); decodeStatus(m_statusField); m_fieldType3 = StreamUtil.readInt(is); print("\tfieldType3: " + m_fieldType3); m_pos = StreamUtil.readInt(is); print("\tpos: " + m_pos); m_fieldType4 = StreamUtil.readInt(is); print("\tfieldType4: " + m_fieldType4); // record is stored in sec we need milli seconds // Don't remove the cast. It will cause calcuation errors m_startTime = (long)StreamUtil.readInt(is) * 1000; date = new Date(m_startTime); print("\tstartTime: " + date.toString() + " (" + m_startTime + ")"); m_fieldType5 = StreamUtil.readInt(is); print("\tfieldType5: " + m_fieldType5); // record is stored in sec we need milli seconds // Don't remove the cast. It will cause calcuation errors m_endTime = (long)StreamUtil.readInt(is) * 1000; date = new Date(m_endTime); print("\tendTime: " + date.toString()); m_fieldType6 = StreamUtil.readInt(is); print("\tfieldType6: " + m_fieldType6); m_padding1 = StreamUtil.readInt(is); print("\tpadding1: " + m_padding1); m_description = StreamUtil.readCString(is); print("\tdescription: \"" + m_description + "\""); m_fiedlType7 = StreamUtil.readInt(is); print("\tfiedlType7: " + m_fiedlType7); m_duration = StreamUtil.readInt(is); print("\tduration: " + m_duration); m_fieldType8 = StreamUtil.readInt(is); print("\tfieldType8: " + m_fieldType8); m_padding2 = StreamUtil.readInt(is); print("\tpadding2: " + m_padding2); m_note = StreamUtil.readCString(is); print("\tnote: \"" + m_note + "\""); m_fieldType9 = StreamUtil.readInt(is); print("\tfieldType9: " + m_fieldType9); m_unTimed = StreamUtil.readInt(is); print("\tunTimed: " + m_unTimed); m_fieldType10 = StreamUtil.readInt(is); print("\tfieldType10: " + m_fieldType10); m_private = StreamUtil.readInt(is); print("\tprivate: " + m_private); m_fieldType11 = StreamUtil.readInt(is); print("\tfieldType11: " + m_fieldType11); m_category = StreamUtil.readInt(is); print("\tcategory: " + m_category); m_fieldType12 = StreamUtil.readInt(is); print("\tfieldType12: " + m_fieldType12); m_alarmSet = StreamUtil.readInt(is); print("\talarmSet: " + m_alarmSet); m_fieldType13 = StreamUtil.readInt(is); print("\tfieldType13: " + m_fieldType13); m_alarmAdvUnits = StreamUtil.readInt(is); print("\talarmAdvUnits: " + m_alarmAdvUnits); m_fieldType14 = StreamUtil.readInt(is); print("\tfieldType14: " + m_fieldType14); switch (m_fieldType14) { case 0: print("\t\tMinutes"); break; case 1: print("\t\tHours"); break; case 2: print("\t\tDays"); break; } m_alarmAdvType = StreamUtil.readInt(is); print("\talarmAdvType: " + m_alarmAdvType); m_fieldType15 = StreamUtil.readInt(is); print("\tfieldType15: " + m_fieldType15); m_repeatEvent = new RepeatEvent(is, m_verbose); print("Record parsed properly"); } private void decodeStatus(int status) { if ((status & 0x08) != 0) print("\t\tPending (0x08)"); if ((status & 0x01) != 0) print("\t\tAdd (0x01)"); if ((status & 0x02) != 0) print("\t\tUpdate (0x02)"); if ((status & 0x04) != 0) print("\t\tDelete (0x04)"); if ((status & 0x80) != 0) print("\t\tArchive (0x80)"); } private void print(String str) { if (!m_verbose) return; System.out.println(str); } } // --------------------------------------------------------------------------- // RepeatEvent // --------------------------------------------------------------------------- final class RepeatEvent { public static final int DAILY = 1; public static final int WEEKLY = 2; public static final int MONTHLYBYDAY = 3; public static final int MONTHLYBYDATE = 4; public static final int YEARLYBYDATE = 5; public static final int YEARLYBYDAY = 6; // Never used private boolean m_verbose; private short m_dateException; private int m_exceptionEntry; private short m_repeatEventFlag; private final ClassEntry m_classEntry; private int m_brand; private int m_interval; private long m_endDate; private int m_firstDayWk; private BrandData m_brandData; RepeatEvent(DataInputStream is, boolean verbose) throws IOException { m_verbose = verbose; print("\n** repeat event **"); m_dateException = StreamUtil.readShort(is); print("dateException: " + m_dateException); if (m_dateException != 0) { print("\n** Found date exceptions, index is " + m_dateException); for (int i = 0; i < m_dateException; i++) { /** * This was unclear in the decode document. * Followed perl scripts implementation here. **/ m_exceptionEntry = StreamUtil.readInt(is); print("\texceptionEntry: " + m_exceptionEntry); } } else m_exceptionEntry = 0; m_repeatEventFlag = StreamUtil.readShort(is); print("\trepeatEventFlag: " + m_repeatEventFlag); // The rest of these members could (should?) be pushed down into repeat // event decoder. They are here because this is where they exist in // the decode document. switch(m_repeatEventFlag) { case 0x0000: { m_classEntry = null; return; } case ((short)0xFFFF): { print("** We have a class entry **"); m_classEntry = new ClassEntry(is, m_verbose); break; } default: { /** * This this seems to be the right way to do this. It was * not clear in the decode document **/ //m_classEntry = new ClassEntry(is, m_verbose); m_classEntry = null; } } m_brand = StreamUtil.readInt(is); switch (m_brand) { case 1: print("\tDaily (1)"); break; case 2: print("\tWeekly (2)"); break; case 3: print("\tMonthlyByDay (3)"); break; case 4: print("\tMonthlyByDate (4)"); break; case 5: print("\tYearlyByDate(5)"); break; case 6: print("\tYearlyByDay (6)"); break; default: print("\tUnknown Repeat type (" + (m_brand&0xff) + ")" ); break; } m_interval = StreamUtil.readInt(is); print("Interval: " + m_interval); // Convert Seconds to milli seconds m_endDate = (long)StreamUtil.readInt(is) * 1000; Date date = new Date(m_endDate); print("Repeat EndDate: " + date.toString() + "(" + m_endDate + ")"); m_firstDayWk = (StreamUtil.readInt(is)&0xff); if (m_firstDayWk < 0 || m_firstDayWk > 6) throw new IllegalStateException("Illegal day of week epected 1-6 got " + m_firstDayWk); print("first dow: " + m_firstDayWk); m_brandData = new BrandData(is, m_brand, m_verbose); } private void print(String str) { if (!m_verbose) return; System.out.println("\t\t\t" + str); } } // --------------------------------------------------------------------------- // BrandData // --------------------------------------------------------------------------- final class BrandData { private boolean m_verbose; private int m_dayIndex; private int m_dayMask; private int m_weekIndex; private int m_dayNumber; private int m_monthIndex; public BrandData(DataInputStream is, int brand, boolean verbose) throws IOException { m_verbose = verbose; brand = (brand & 0xff); print("** brand data " + brand + " **"); if (brand == RepeatEvent.DAILY || brand == RepeatEvent.WEEKLY || brand == RepeatEvent.MONTHLYBYDAY) { m_dayIndex = StreamUtil.readInt(is); print("dayIndex: " + m_dayIndex); } if (brand == RepeatEvent.WEEKLY) { m_dayMask = is.readByte(); print("dayMask: " + m_dayMask); } if (brand == RepeatEvent.MONTHLYBYDAY) { m_weekIndex = StreamUtil.readInt(is); print("weekIndex: " + m_weekIndex); } if (brand == RepeatEvent.MONTHLYBYDATE || brand == RepeatEvent.YEARLYBYDATE) { m_dayNumber = StreamUtil.readInt(is); print("dayNumber: " + m_dayNumber); } if (brand == RepeatEvent.YEARLYBYDATE) { m_monthIndex = StreamUtil.readInt(is); print("monthIndex: " + m_monthIndex); } } private void print(String str) { if (!m_verbose) return; System.out.println(str); } } // --------------------------------------------------------------------------- // ClassEntry // --------------------------------------------------------------------------- final class ClassEntry { private boolean m_verbose; private short m_const; private byte m_classByte; private String m_className; public ClassEntry(DataInputStream is, boolean verbose) throws IOException { m_verbose = verbose; print("class entry"); m_const = StreamUtil.readShort(is); print("const: " + m_const); int len = StreamUtil.readShort(is); m_classByte = is.readByte(); // I am not sure why they just did not reuse CString format !?! StringBuffer str = new StringBuffer(len); for (int i = 1; i < len; i++) str.append(((char)is.readByte())); m_className = str.toString(); print("className: " + m_className); } private void print(String str) { if (!m_verbose) return; System.out.println(str); } } // --------------------------------------------------------------------------- // CategoryEntry // --------------------------------------------------------------------------- /** * TODO: Implement Category Entry if necessary * * I am not sure what software surrports this class. Palm does not seem to use * it! * * Category Entry Format (Unused) * Index Long 4*Byte Category Index * ID Long 4*Byte Category ID * Dirty Flag Long 4*Byte Category Dirty Flag * Long Name Cstring Long Category Name * Short Name Cstring Short Category Name */ final class CategoryEntry { private boolean m_verboses; } // --------------------------------------------------------------------------- // Helper utility for reading Streams // --------------------------------------------------------------------------- final class StreamUtil { // TODO: Figure out a better way to do this // This is lame way to do this but oh well; DataInputStrea.readInt() has // wrong byte order. ByteBuffer would be my choice but can't count on // Java 1.4 on all web servers out there :( public static int readInt(DataInputStream is) throws IOException { byte[] buf = new byte[4]; buf[0] = is.readByte(); buf[1] = is.readByte(); buf[2] = is.readByte(); buf[3] = is.readByte(); int res = 0; int i, n; for (i = 0; i != 4; i++) { n = buf[i] & 0xFF; n <<= i * 8; res |= n; } return res; } // TODO: Figure out a better way to do this // This is lame way to do this but oh well; DataInputStrea.readInt() has // wrong byte order. ByteBuffer would be my choice but can't count on // Java 1.4 on all web servers out there :( public static short readShort(DataInputStream is) throws IOException { byte[] buf = new byte[2]; buf[0] = is.readByte(); buf[1] = is.readByte(); short res = 0; int i, n; for (i = 0; i != 2; i++) { n = buf[i] & 0xFF; n <<= i * 8; res |= n; } return res; } /** * Cstrings are stored as folnnlows: * 1. Strings less than 255 bytes are stored with the length specified in * the first byte followed by the actual string. * * 2. Zero length strings are stored with a 0x00 byte. * * 3. Strings 255 bytes or longer are stored with a flag byte set to 0xFF * followed by a short (2*Byte) that specifies the length of the string, * followed by the actual string. **/ public static String readCString(DataInputStream is) throws IOException { StringBuffer scratchfile = new StringBuffer(); int len = is.readByte(); if (len == (byte)0xFF) len = readShort(is); for (int i = 0; i != len; i++) scratchfile.append((char)is.readByte()); return scratchfile.toString(); } /** * This will dump the hex values of the next N bytes in stream. * * Warning: if you call this the decoder will not be able to parse * anymore of the file. **/ public static void dumpBytes(DataInputStream is, int count) throws IOException { System.err.println( "\nStart stream dump: " + "\n----------------------------------------------"); Integer val; for (int i = 0; i < count; i++) { if (i%16 == 0) System.err.print("\n"); val = new Integer(is.readByte() & 0xFF ); System.err.print( ((val.intValue() < 0x10) ? "0x0" : "0x") + Integer.toHexString(val.intValue()).toUpperCase() + " "); } System.err.println( "\n\nEnd of Stream Dump" + "\n----------------------------------------------" + "\n\n"); throw new IllegalStateException(count + " bytes dumped to standard err"); } }