Programming Java 2 Micro Edition for Symbian OS 2004 (779882), страница 50
Текст из файла (страница 50)
Layer separation allows us to target our customization for eachuser interface more precisely. If the business logic is changed, or a bugfound, the fixes can easily be applied to all versions.264MIDP 2.0 CASE STUDIESThe custom items created for the expense application look similarand share a lot of common code. It would be possible to create asingle heavyweight custom item that works on both interfaces effectively;however, such an item places an unfair burden on the device by increasingthe memory footprint of the application, which is not desirable.
A morepractical approach is to branch the code once a device-specific versionof the custom item is close to completion, allowing as much code to bere-used as possible.5.2.5 Record StoreThe expense application must keep expense and claimant informationas well as application settings on the device.
This is achieved usingthe Record Management System (RMS) API. The RMS API allows anapplication to persist information as a series of records within a recordstore and each record has its own unique ID that is assigned by theunderlying implementation. Several record stores can be maintained byan application.In the expense application there are three record stores, one eachfor expenses, user information and application settings. Access to eachrecord store is encapsulated in an individual object, a data accessobject (DAO). Encapsulating the persistence in this way allows its implementation to be abstracted away from the business logic; should theunderlying persistence mechanism change, the updates are confinedsolely to the DAOs.The expense application uses a single instance of each DAO for allpersistence operations. Making the DAO a Singleton enforces that onlyone instance is created, ensuring that memory consumption is kept to aminimum.
Additionally, opening a record store is a lengthy operation. Byperforming this operation once at application start-up we ensure that therest of the application responds well. If there were ever a good reason fora splash screen in a MIDlet, opening several record stores is it.5.2.5.1 Opening a Record StoreThe DAO is created and opened in the ExpenseMidlet class; thefollowing code shows the portion of the startApp() method wherethis occurs.// make sure we have instances of the DAOs created and readyExpenseDAO.getInstance().openRecordStore();PersonDAO.getInstance().openRecordStore();SettingsDAO.getInstance().openRecordStore();The openRecordStore() method opens a record store using theRMS. The following code shows the implementation of the ExpenseDAOTHE EXPENSE APPLICATION265object.
DAOException is used to wrap any exceptions that occur inthe DAOs.// ExpenseDAO openRecordStore() methodpublic void openRecordStore() throws DAOException {try {if (rs != null)rs = RecordStore.openRecordStore(EXPENSE_RMS_NAME, true);} catch (Exception e) {// handling for RecordStoreException//RecordStoreNotFound//RecordStoreFullException//IllegalArgumentExceptionthrow new DAOException("Failed to open the Expense RMS, reason: \n" +e);}}Each record has a unique ID, which allows individual records to beretrieved and without which deletions and updates cannot take place.When reading a record from the RMS, the record’s unique ID is retrievedand stored in case the record needs to be updated or deleted. Thefollowing code shows the implementation of the get() method in theExpenseDAO, which gets a record using its unique ID. The conversionof the record into a usable object is discussed in the next section.// ExpenseDAO get expense methodpublic Expense get(int id) throws DAOException {try {// get the expense and parse into an Expense objectbyte[] expenseBytes = rs.getRecord(id);Expense expense = Expense.bytesToExpense(expenseBytes);expense.setRmsId(id);return expense;} catch (Exception e) {throw new DAOException("Failed to read record, cause\n" + e);}}The DAOs also provide functions to allow records to be updated,deleted and enumerated.
See the source code for examples of how thesefunctions are implemented.5.2.5.2 Encoding RecordsA single record is made up of a variable length array of bytes. In order touse the record store, an application must encode its information to anddecode from a byte array. MIDP has implementations of the java.io.ByteArrayInputStream and java.io.ByteArrayOutputStreamclasses, which provide an excellent solution for this.266MIDP 2.0 CASE STUDIESWhen interacting with the DAOs, the application uses objects thatrepresent an expense, a person or the application settings.
Each of theseobjects is responsible for encoding their data into a byte array readyfor persistence, which ensures that any modifications to the underlying information do not affect the DAO. The following method showshow an expense claim is encoded ready for persistence. The expenseToBytes() method in the midlet.model.Expense class uses ajava.io.ByteArrayOutputStream object in combination with ajava.io.DataOutputStream to encode an expense claim and returnan array of bytes:// Expense objects expenseToBytes() methodpublic byte[] expenseToBytes() throws IOException {byte[] bytes = null;ByteArrayOutputStream baos = null;DataOutputStream dos = null;try {// open streams that will do the workbaos = new ByteArrayOutputStream();dos = new DataOutputStream(baos);// write information to the output streamdos.writeShort(getOwnerId());dos.writeByte(getState());...dos.writeUTF(getBhNotes());// convert byte output stream to byte arraybytes = baos.toByteArray();} finally {// tidyif (dos != null)dos.close();}// return bytes to callerreturn bytes;}The decoding operation is similar to the encoding operation but usesinput streams and returns an Expense object.
The fields must be encodedand decoded in the same order. If a field is added then we must ensurethat both methods are updated; failure to do so would result in fieldsbeing populated with the wrong data.// Expense objects bytesToExpense() methodpublic static Expense bytesToExpense(byte[] expenseBytes)throws IOException {DataInputStream dis = null;try {// get at bytes via data streamdis = new DataInputStream(new ByteArrayInputStream(expenseBytes));// create empty expense objectExpense expense = new Expense();// load the expense information from the byte arrayTHE EXPENSE APPLICATION267expense.setOwnerId(dis.readShort());expense.setState(dis.readByte());...expense.setBhNotes(dis.readUTF());// give the caller back their expensereturn expense;} finally {// tidyif (dis != null)dis.close();}}If changes to the underlying object require changes to the recordencoding, perhaps due to a new version of the application, then readingold records from an existing record store would inevitably result in errors.To ensure that future changes to the record format can be dealt with,an object version number can be inserted into each record to allowalternate decoding for legacy record versions.
The expense applicationwe are discussing here does not handle different record versions as it is ademonstration application.5.2.5.3 Enumerating RecordsThe RMS permits the enumeration of records within a record store. Duringenumeration, the records returned can be filtered by passing an object thatimplements the javax.microedition.rms.RecordFilter interface or sorted by passing an object that implements the javax.microedition.rms.RecordComparator interface.The expense application implements filtering using the ExpenseFilter class, which implements the matches() method from theRecordFilter interface.
Each record in the store is passed in turn tomatches(), which returns true if the record should be included in theenumeration. ExpenseFilter allows filtering on a number of differentexpense attributes, each of which is checked during the matches()call:// ExpenseFilter’s matches() methodpublic boolean matches(byte[] expenseBytes) {boolean matchFound = true;// do we have any search clauses? if not then let all records throughif ((personId == 0) && (monthCal == null)&& (changedAfterSyncId == -1) && (createdDate == null))return true;Expense expense;try {// get the information from the record and filterexpense = Expense.bytesToExpense(expenseBytes);} catch (IOException e) {// we need to filter but cannot read this record,268MIDP 2.0 CASE STUDIES// we assume no matchSystem.err.println("Failed to read expense in ExpenseFilter: "+ e);return false;}// test for owner match upif (personId != 0)matchFound = (isOwner)? (expense.getOwnerId() == personId): (expense.getOwnerId() != personId);// test for month match up only if owner check still means its usefulif ((matchFound) && (monthCal != null)) {// get everything set up for compare...Calendar receiptCal = Calendar.getInstance();receiptCal.setTime(expense.getReceiptDate());// check month and year on calendarsmatchFound =((monthCal.get(Calendar.MONTH)== receiptCal.get(Calendar.MONTH))&& (monthCal.get(Calendar.YEAR)== receiptCal.get(Calendar.YEAR)));}// test for last change match up only if owner check still usefulif ((matchFound) && (changedAfterSyncId >= 0)) {matchFound = (expense.getLastSyncId() > changedAfterSyncId);}// test for created date matchif ((matchFound) && (createdDate != null)) {matchFound =(createdDate.getTime() == expense.getCreatedDate().getTime());}return matchFound;}A record enumeration can return either the unique record ID or therecord itself; the DAO must fetch both.
The following code enumeratesthe unique record IDs and explicitly retrieves each expense record beforeplacing it into a Vector ready to be returned to the caller.// ExpenseDAO enumerateExpenses() methodprivate Vector enumerateExpenses(ExpenseFilter filter)throws DAOException {Vector expenseList = new Vector();try {// create an enumeration of the records that we needRecordEnumeration enum = rs.enumerateRecords(filter, null, false);// enumerate the record ids and use them to retrieve the expenseswhile (enum.hasNextElement()) {Expense expense = get(enum.nextRecordId());expenseList.addElement(expense);}} catch (Exception e) {// failed to enumerate records...throw new DAOException("Failed to enumerate expenses, cause " + e);THE EXPENSE APPLICATION269}return expenseList;}5.2.6 SynchronizationAt the heart of the expense MIDlet is the exchange of information betweenthe client device and the server.
Without synchronization, expenses couldnot advance through the workflow from creation to approval or rejection.Prior to using the expense MIDlet for the first time a user must enterthe synchronization server details; this allows the first synchronization totake place so any existing expenses can be retrieved and the user’s roleis known.The synchronization occurs when the Synchronize command isselected from the menu. A form is displayed with some animationto show progress while a worker thread is created to undertake thesynchronization process.5.2.6.1 Animation of Synchronization FormThe synchronization form is simple, as can be seen in Figure 5.8.