The RandomAccessFile can be used to read and write bytes, characters, and primitive types — but not objects. It provides a seek() method that allows you to go to any point within the file, forward or backward.
In this section I will present a comprehensive example of the use of the RandomAccessFile class. The task at hand will be to create a Java adapter to a legacy application flat-file data store. The complete project specification is given in figure 18-27.
Project Specification Objectives: - Demonstrate your ability to conduct file I/O operations with the RandomAccessFile class - Demonstrate your ability to implement a non-trivial interface - Demonstrate your ability to translate low-level exceptions into higher-level, user-defined, applica tion-specific exception abstractions - Demonstrate your ability to coordinate file I/O operations via object synchronization Tasks: - You are a junior programmer working in the IT department of a retail bookstore. The CEO wants to begin migrating legacy systems to the web using Java technology. A first step in this initiative is to create Java adapters to existing legacy data stores. Given an interface definition, example legacy data file, and legacy data file schema definition, write a Java class that serves as an adapter object to a legacy data file. Given: - Java interface file specifying adapter operations - Legacy data file schema definition - Example legacy data file Legacy Data File Schema Definition: The legacy data file contains three sections: 1) The file identification section is a two-byte value that identifies the file as a data file. 2) The schema description section immediately follows the first section and contains the field text name and two-byte field length for each field in the data section. 3) The data section contains fixed-field-length record data elements arranged according to the following schema: (length is in bytes) Field Name Length Description --------------|---------|---------------------------------------------------- deleted | 1 | numeric - 0 if valid, 1 if deleted --------------|---------|---------------------------------------------------- title | 50 | text - book title --------------|---------|---------------------------------------------------- author | 50 | text - author full name --------------|---------|---------------------------------------------------- pub_code | 4 | numeric - publisher code --------------|---------|---------------------------------------------------- ISBN | 13 | text - International Standard Book Number --------------|---------|---------------------------------------------------- price | 8 | text - retail price in following format: $nnnn.nn --------------|---------|---------------------------------------------------- qoh | 4 | numeric - quantity on hand --------------|---------|----------------------------------------------------
As you can see from the project specification this is a fairly complex project. A Java adapter must be written to access a legacy system’s fixed-length record data files. You are given three artifacts to assist in your effort: 1) The data file schema definition is given in the project, 2) the example legacy data file is located on the accompanying disk or can be downloaded from www.pulpfreepress.com, and 3) a Java interface that provides declarations for the methods that must be implemented in the legacy data file adapter class. The interface code is listed in example 18.22
Example 18.22: LegacyDatafileInterface.java
1 public interface LegacyDatafileInterface { 2 3 /** 4 * Read the record indicated by the rec_no and return a string array 5 * were each element contains a field value. 6 */ 7 public String[] readRecord(long rec_no) throws RecordNotFoundException; 8 9 10 /** 11 * Update a record's fields. The record must be locked with the lockRecord() 12 * method and the lock_token must be valid. The value for field n appears in 13 * element record[n]. 14 */ 15 public void updateRecord(long rec_no, String[] record, long lock_token) throws 16 RecordNotFoundException, SecurityException; 17 18 19 /** 20 * Marks a record for deletion by setting the deleted field to 1. The lock_token 21 * must be valid otherwise a SecurityException is thrown. 22 */ 23 public void deleteRecord(long rec_no, long lock_token) throws 24 RecordNotFoundException, SecurityException; 25 26 27 /** 28 * Creates a new datafile record and returns the record number. 29 */ 30 public long createRecord(String[] record) throws FailedRecordCreationException; 31 32 33 /** 34 * Locks a record for updates and deletes and returns an integer 35 * representing a lock token. 36 */ 37 public long lockRecord(long rec_no) throws RecordNotFoundException; 38 39 40 /** 41 * Unlocks a previously locked record. The lock_token must be valid or a 42 * SecurityException is thrown. 43 */ 44 public void unlockRecord(long rec_no, long lock_token) throws SecurityException; 45 46 47 /** 48 * Searches the records in the datafile for records that match the String 49 * values of search_criteria. search_criteria[n] contains the search value 50 * applied against field n. 51 */ 52 public long[] searchRecords(String[] search_criteria); 53 54 }//end interface definition
Referring to example 18.22 — the comments preceding each method declaration explain the functionality each method is required to provide. Most of the methods declare that they may throw exceptions that do not currently exist in the Java API. As part of the project you will have to create each of these exception classes. Figure 18-28 shows the contents of the example legacy data file as viewed with a text editor.
Figure 18-28: Contents of books.dat Example Legacy Datafile Viewed with Text Editor
Given the project specification and the three supporting artifacts you may be wondering where to begin. Using the guidance offered by the project-approach strategy in chapter 1, I recommend devoting some time to studying the schema definition and comparing it with what you see in the example data file. You will note that although some of the text appears to read OK, there are a few characters here and there that seem out of place. For instance, you can make out the header information but the header appears to start with a letter ‘z’. Studying the schema definition closely you note that the data file begins with a two-byte file identifier number. But what’s the value of this number?
One way to find out is to write a short program that reads the first two bytes of the file and converts it to a number. The RandomAccessFile class implements the DataInput and DataOutput interfaces. The DataInput interface has a method named readShort(). A Java short is a two-byte value. The readShort() method would be an excellent method to use to read the first two bytes of the file in an effort to determine their value.
The next phase of your discovery would be to try and read the rest of the file, or at least at first try and read the complete header and one complete record using the schema definition as a guide. You may find that a more detailed analysis of the header and record lengths are in order. Figure 18-29 shows a simple analysis performed with a spreadsheet.
Figure 18-29: Header and Record Length Analysis
Referring to figure 18-29 — the simple analysis reveals that the length of the header section of the legacy data file is 54 bytes long and each record is 130 bytes long. These figures, as well as the individual field lengths, will come in handy when the adapter is written.
Armed with some knowledge about the structure of the legacy data file and having gained some experience writing a small test program that reads all or portions of the file you can begin to create the adapter class incrementally. A good method to start with is the readRecord() method specified in the LegacyDatafileInterface.
This section briefly discusses additional issues which must be considered during the project implementation phase. These considerations include 1) record locking during updates and deletes, and 2) translating low-level I/O exceptions into higher level exceptions as specified in the interface.
The LegacyDatafileInterface specifies that a record must be locked when it is being updated or deleted. The locking is done via a lock token, which is nothing more that a primitive-type long value. How might the locking mechanism be implemented? How is the lock_token generated?
To implement the locking mechanism you must thoroughly understand threads and thread synchronization. (These topics are covered in detail in chapter 16.) Any object can be used as a synchronization point by using the synchronized keyword. The adapter must ensure that if one thread attempts to update or delete a record (by calling the updateRecord() or deleteRecord() methods) it cannot do so while another thread is in the process of calling either of those methods.
You can adopt several strategies as a means to an ends here. You can 1) apply the synchronized keyword to the entire method in question (updateRecord() and deleteRecord()) or 2) synchronize only the critical section of code within each method. Within the synchronized block you implement logic to check for a particular condition. If the condition holds you can proceed with whatever it is you need to do. If the condition does not hold, you will have to wait until it does. You do this by calling the wait() method on the object you have selected to synchronize on. The wait() method adds the current thread to a list of threads waiting to get a lock on that object.
Conversely, when a thread has obtained a lock on an object and it concludes its business, it must release its lock and notify the object upon which it is synchronized that it can wake up one of the other waiting threads. This is done by calling the notify() or notifyAll() method on the synchronized object.
Once the thread synchronization scheme is determined the lock_token can be generated with the java.util.Random class.
The java.io package defines several low-level exceptions that can occur when conducting file I/O operations. These exceptions must be handled in the adapter, however, the LegacyDatafileInterface specifies that several higher-level exceptions may be thrown when its methods are called.
To create custom exceptions you extend the Exception class and add any customized behavior required. (Exceptions are discussed in detail in chapter 15.) In your adapter code, you will catch and handle the low-level exceptions when they occur, and then repackage the exception within the context of a custom exception, and then throw the custom exception. Any objects utilizing the services of the adapter class must handle your custom exceptions, not the low-level I/O exceptions.
The previous sections attempted to address some of the development issues you will typically encounter when attempting this type of project. The purpose of the project is to demonstrate the use of the RandomAccessFile class in the context of a non-trivial example. I hope also that I have sufficiently illustrated the reality that rarely can one class perform its job without the help of many other classes.
The next section gives the code for the completed project. Keep in mind that the examples listed here represent one particular approach and solution to the problem. As an exercise you will be invited to attempt a solution on your own terms using the knowledge gained here as a guide.
Explore and study the code. Compile the code and observe its operation. Experiment — make changes to areas you feel can use improvement.
This section gives the complete listing for the code that satisfies the requirements of the legacy data file adapter project using the RandomAccessFile class. The LegacyDatafileInterface listing given in example 18.22 is not repeated here.
Example 18.23: DataFileAdapter.java
1 import java.io.*; 2 import java.util.*; 3 4 5 public class DataFileAdapter implements LegacyDatafileInterface { 6 7 /** 8 * Class Constants 9 */ 10 11 private static final short FILE_IDENTIFIER = 378; 12 private static final int HEADER_LENGTH = 54; 13 private static final int RECORDS_START = 54; 14 private static final int RECORD_LENGTH = 130; 15 private static final int FIELD_COUNT = 7; 16 17 private static final short DELETED_FIELD_LENGTH = 1; 18 private static final short TITLE_FIELD_LENGTH = 50; 19 private static final short AUTHOR_FIELD_LENGTH = 50; 20 private static final short PUB_CODE_FIELD_LENGTH = 4; 21 private static final short ISBN_FIELD_LENGTH = 13; 22 private static final short PRICE_FIELD_LENGTH = 8; 23 private static final short QOH_FIELD_LENGTH = 4; 24 25 private static final String DELETED_STRING = "deleted"; 26 private static final String TITLE_STRING = "title"; 27 private static final String AUTHOR_STRING = "author"; 28 private static final String PUB_CODE_STRING = "pub_code"; 29 private static final String ISBN_STRING = "ISBN"; 30 private static final String PRICE_STRING = "price"; 31 private static final String QOH_STRING = "qoh"; 32 33 private static final int TITLE_FIELD = 0; 34 private static final int AUTHOR_FIELD = 1; 35 private static final int PUB_CODE_FIELD = 2; 36 private static final int ISBN_FIELD = 3; 37 private static final int PRICE_FIELD = 4; 38 private static final int QOH_FIELD = 5; 39 40 private static final int VALID = 0; 41 private static final int DELETED = 1; 42 43 /** 44 * Field offsets used with String.substring() method 45 * Note: These are not used in the final program! 46 */ 47 private static final int DELETED_START_OFFSET = 0; 48 private static final int DELETED_STOP_OFFSET = 1; 49 private static final int TITLE_START_OFFSET = 1; 50 private static final int TITLE_STOP_OFFSET = 51; 51 private static final int AUTHOR_START_OFFSET = 51; 52 private static final int AUTHOR_STOP_OFFSET = 101; 53 private static final int PUB_CODE_START_OFFSET = 101; 54 private static final int PUB_CODE_STOP_OFFEST = 105; 55 private static final int ISBN_START_OFFSET = 105; 56 private static final int ISBN_STOP_OFFSET = 118; 57 private static final int PRICE_START_OFFSET = 118; 58 private static final int PRICE_STOP_OFFSET = 126; 59 private static final int QOH_START_OFFSET = 126; 60 private static final int QOH_STOP_OFFSET = 130; 61 62 /** 63 * Instance Fields 64 */ 65 66 private File _filename = null; 67 private RandomAccessFile _raf = null; 68 private long _record_count = 0; 69 private HashMap _locked_records_map = null; 70 private Random _token_maker = null; 71 private long _current_record_number = 0; 72 73 74 /** 75 * Constructor 76 * 77 */ 78 public DataFileAdapter(String filename) throws InvalidDataFileException { 79 try{ 80 _filename = new File(filename); 81 _raf = new RandomAccessFile(_filename, "rw"); 82 _raf.seek(0); 83 if((_raf.length() >= HEADER_LENGTH) && 84 (_raf.readShort() == FILE_IDENTIFIER)){ // it's a valid data file 85 System.out.println("Setting up data file for I/O operations"); 86 _record_count = ((_raf.length() - HEADER_LENGTH) / RECORD_LENGTH); 87 _current_record_number = 0; 88 _locked_records_map = new HashMap(); 89 _token_maker = new Random(); 90 }else if(_raf.length() == 0) { // it's an empty file - make it a data file 91 _raf.seek(0); 92 _raf.writeShort(FILE_IDENTIFIER); 93 _raf.writeBytes(DELETED_STRING); 94 _raf.writeShort(DELETED_FIELD_LENGTH); 95 _raf.writeBytes(TITLE_STRING); 96 _raf.writeShort(TITLE_FIELD_LENGTH); 97 _raf.writeBytes(AUTHOR_STRING); 98 _raf.writeShort(AUTHOR_FIELD_LENGTH); 99 _raf.writeBytes(PUB_CODE_STRING); 100 _raf.writeShort(PUB_CODE_FIELD_LENGTH); 101 _raf.writeBytes(ISBN_STRING); 102 _raf.writeShort(ISBN_FIELD_LENGTH); 103 _raf.writeBytes(PRICE_STRING); 104 _raf.writeShort(PRICE_FIELD_LENGTH); 105 _raf.writeBytes(QOH_STRING); 106 _raf.writeShort(QOH_FIELD_LENGTH); 107 108 _record_count = 0; 109 _locked_records_map = new HashMap(); 110 _token_maker = new Random(); 111 112 } else { 113 _raf.seek(0); 114 if(_raf.readShort() != FILE_IDENTIFIER){ 115 _raf.close(); 116 System.out.println("Invalid data file. Closing file."); 117 throw new InvalidDataFileException(null); 118 } 119 } 120 121 }catch(IOException e){ 122 System.out.println("Problem opening or creating data file."); 123 System.out.println(e.toString()); 124 } 125 126 } // end constructor 127 128 129 /** 130 * Default constructor 131 * 132 */ 133 public DataFileAdapter() throws InvalidDataFileException { 134 this("books.dat"); 135 } 136 137 138 139 /** 140 * CreateNewDataFile 141 * 142 */ 143 public void createNewDataFile(String filename) throws NewDataFileException{ 144 try{ 145 if(_raf != null){ 146 _raf.close(); 147 } 148 _raf = new RandomAccessFile(filename, "rw"); 149 150 _raf.seek(0); 151 _raf.writeShort(FILE_IDENTIFIER); 152 _raf.writeBytes(DELETED_STRING); 153 _raf.writeShort(DELETED_FIELD_LENGTH); 154 _raf.writeBytes(TITLE_STRING); 155 _raf.writeShort(TITLE_FIELD_LENGTH); 156 _raf.writeBytes(AUTHOR_STRING); 157 _raf.writeShort(AUTHOR_FIELD_LENGTH); 158 _raf.writeBytes(PUB_CODE_STRING); 159 _raf.writeShort(PUB_CODE_FIELD_LENGTH); 160 _raf.writeBytes(ISBN_STRING); 161 _raf.writeShort(ISBN_FIELD_LENGTH); 162 _raf.writeBytes(PRICE_STRING); 163 _raf.writeShort(PRICE_FIELD_LENGTH); 164 _raf.writeBytes(QOH_STRING); 165 _raf.writeShort(QOH_FIELD_LENGTH); 166 167 _record_count = 0; 168 _locked_records_map = new HashMap(); 169 _token_maker = new Random(); 170 171 }catch(IOException e){ 172 System.out.println(e.toString()); 173 throw new NewDataFileException(e); 174 } 175 176 } // end createNewDataFile method 177 178 179 180 181 /** 182 * OpenDataFile 183 * 184 */ 185 public void openDataFile(String filename) throws InvalidDataFileException { 186 try{ 187 if(_raf != null){ 188 _raf.close(); 189 } 190 _filename = new File(filename); 191 _raf = new RandomAccessFile(_filename, "rw"); 192 _raf.seek(0); 193 if((_raf.length() >= HEADER_LENGTH) && 194 (_raf.readShort() == FILE_IDENTIFIER)){ // it's a valid data file 195 System.out.println("Setting up data file for I/O operations"); 196 _record_count = ((_raf.length() - HEADER_LENGTH) / RECORD_LENGTH); 197 _current_record_number = 0; 198 _locked_records_map = new HashMap(); 199 _token_maker = new Random(); 200 }else if(_raf.length() == 0) { // it's an empty file - make it a data file 201 _raf.seek(0); 202 _raf.writeShort(FILE_IDENTIFIER); 203 _raf.writeBytes(DELETED_STRING); 204 _raf.writeShort(DELETED_FIELD_LENGTH); 205 _raf.writeBytes(TITLE_STRING); 206 _raf.writeShort(TITLE_FIELD_LENGTH); 207 _raf.writeBytes(AUTHOR_STRING); 208 _raf.writeShort(AUTHOR_FIELD_LENGTH); 209 _raf.writeBytes(PUB_CODE_STRING); 210 _raf.writeShort(PUB_CODE_FIELD_LENGTH); 211 _raf.writeBytes(ISBN_STRING); 212 _raf.writeShort(ISBN_FIELD_LENGTH); 213 _raf.writeBytes(PRICE_STRING); 214 _raf.writeShort(PRICE_FIELD_LENGTH); 215 _raf.writeBytes(QOH_STRING); 216 _raf.writeShort(QOH_FIELD_LENGTH); 217 218 _record_count = 0; 219 _locked_records_map = new HashMap(); 220 _token_maker = new Random(); 221 222 } else { 223 _raf.seek(0); 224 if(_raf.readShort() != FILE_IDENTIFIER){ 225 _raf.close(); 226 System.out.println("Invalid data file. Closing file."); 227 throw new InvalidDataFileException(null); 228 } 229 } 230 231 }catch(IOException e){ 232 System.out.println("Problem opening or creating data file."); 233 System.out.println(e.toString()); 234 } 235 236 } // end openDataFile method 237 238 239 240 241 /** 242 * Read the record indicated by the rec_no and return a string array 243 * were each element contains a field value. 244 */ 245 public String[] readRecord(long rec_no) throws RecordNotFoundException { 246 String[] temp_string = null; 247 if((rec_no < 0) || (rec_no > _record_count)){ 248 System.out.println("Requested record out of range!"); 249 throw new RecordNotFoundException("Requested record out of range", null); 250 }else{ 251 try{ 252 gotoRecordNumber(rec_no); 253 if(_raf.readByte() == DELETED){ 254 System.out.println("Record has been deleted!"); 255 throw new RecordNotFoundException("Record " + rec_no + " deleted!", null); 256 }else{ 257 temp_string = recordBytesToStringArray(rec_no); 258 } 259 }catch(IOException e){ 260 System.out.println(e.toString()); 261 throw new RecordNotFoundException("Problem in readRecord method", e); 262 } 263 } // end else 264 return temp_string; 265 } // end readRecord() 266 267 268 269 270 /** 271 * Update a record's fields. The record must be locked with the lockRecord() 272 * method and the lock_token must be valid. The value for field n appears in 273 * element record[n]. The call to updateRecord() MUST be preceeded by a call 274 * to lockRecord() and followed by a call to unlockRecord() 275 */ 276 public void updateRecord(long rec_no, String[] record, long lock_token) throws 277 RecordNotFoundException, SecurityException{ 278 if(lock_token != ((Long)_locked_records_map.get(new Long(rec_no))).longValue()){ 279 System.out.println("Invalid update record lock token."); 280 throw new SecurityException("Invalid update record lock token.", null); 281 }else{ 282 try{ 283 gotoRecordNumber(rec_no); //i.e., goto indicated record 284 _raf.writeByte((byte)0); 285 _raf.write(stringToPaddedByteField(record[TITLE_FIELD], TITLE_FIELD_LENGTH)); 286 _raf.write(stringToPaddedByteField(record[AUTHOR_FIELD], AUTHOR_FIELD_LENGTH)); 287 _raf.writeInt(Integer.parseInt(record[PUB_CODE_FIELD])); 288 _raf.write(stringToPaddedByteField(record[ISBN_FIELD], ISBN_FIELD_LENGTH)); 289 _raf.write(stringToPaddedByteField(record[PRICE_FIELD], PRICE_FIELD_LENGTH)); 290 _raf.writeInt(Integer.parseInt(record[QOH_FIELD])); 291 _current_record_number = rec_no; 292 }catch(IOException e){ 293 System.out.println("updateRecord(): Problem writing record information."); 294 System.out.println(e.toString()); 295 } 296 catch(NumberFormatException e){ 297 System.out.println("updateRecord(): Problem converting Strings to numbers."); 298 System.out.println(e.toString()); 299 } 300 catch(Exception e){ 301 System.out.println("updateRecord(): Exception occured."); 302 System.out.println(e.toString()); 303 } 304 }// end else 305 }// end updateRecord() 306 307 308 309 310 /** 311 * Marks a record for deletion by setting the deleted field to 1. The lock_token 312 * must be valid otherwise a SecurityException is thrown. 313 */ 314 public void deleteRecord(long rec_no, long lock_token) throws 315 RecordNotFoundException, SecurityException { 316 if(lock_token != ((Long)_locked_records_map.get(new Long(rec_no))).longValue()){ 317 System.out.println("Invalid delete record lock token."); 318 throw new SecurityException("Invalid delete record lock token.", null); 319 }else{ 320 try{ 321 gotoRecordNumber(rec_no); // goto record indicated 322 _raf.writeByte((byte)1); // mark for deletion 323 324 }catch (IOException e){ 325 System.out.println("deleteRecord(): " + e.toString()); 326 } 327 }// end else 328 }// end deleteRecord() 329 330 331 332 333 334 /** 335 * Creates a new datafile record and returns the record number. 336 */ 337 public long createRecord(String[] record) throws FailedRecordCreationException { 338 try{ 339 gotoRecordNumber(_record_count); //i.e., goto end of file 340 _raf.writeByte((byte)0); 341 _raf.write(stringToPaddedByteField(record[TITLE_FIELD], TITLE_FIELD_LENGTH)); 342 _raf.write(stringToPaddedByteField(record[AUTHOR_FIELD], AUTHOR_FIELD_LENGTH)); 343 _raf.writeInt(Integer.parseInt(record[PUB_CODE_FIELD])); 344 _raf.write(stringToPaddedByteField(record[ISBN_FIELD], ISBN_FIELD_LENGTH)); 345 _raf.write(stringToPaddedByteField(record[PRICE_FIELD], PRICE_FIELD_LENGTH)); 346 _raf.writeInt(Integer.parseInt(record[QOH_FIELD])); 347 _current_record_number = ++_record_count; 348 }catch(IOException e){ 349 System.out.println(e.toString()); 350 throw new FailedRecordCreationException("IOException in createRecord method", e); 351 } 352 catch(NumberFormatException e){ 353 System.out.println(e.toString()); 354 throw new FailedRecordCreationException("NumberFormatException in createRecord", e); 355 } 356 catch(RecordNotFoundException e){ 357 System.out.println(e.toString()); 358 throw new FailedRecordCreationException("RecordNotFoundException in createRecord", e); 359 } 360 return _current_record_number; 361 } 362 363 364 365 366 /** 367 * Locks a record for updates and deletes and returns an integer 368 * representing a lock token. 369 */ 370 public long lockRecord(long rec_no) throws RecordNotFoundException { 371 long lock_token = 0; 372 if((rec_no < 0) || (rec_no > _record_count)){ 373 System.out.println("Record cannot be locked. Not in valid range."); 374 throw new RecordNotFoundException("Record cannot be locked. Not in valid range.", null); 375 }else synchronized(_locked_records_map){ 376 while(_locked_records_map.containsKey(new Long(rec_no))){ 377 try{ 378 _locked_records_map.wait(); 379 }catch(InterruptedException ignored){ } 380 } 381 lock_token = _token_maker.nextLong(); 382 _locked_records_map.put(new Long(rec_no), new Long(lock_token)); 383 } 384 385 return lock_token; 386 } // end lockRecord method 387 388 389 390 391 /** 392 * Unlocks a previously locked record. The lock_token must be valid or a 393 * SecurityException is thrown. 394 */ 395 public void unlockRecord(long rec_no, long lock_token) throws SecurityException { 396 synchronized(_locked_records_map){ 397 if(_locked_records_map.containsKey(new Long(rec_no))){ 398 if(lock_token == ((Long)_locked_records_map.get(new Long(rec_no))).longValue()){ 399 _locked_records_map.remove(new Long(rec_no)); 400 _locked_records_map.notifyAll(); 401 }else{ 402 System.out.println("Invalid lock token."); 403 throw new SecurityException("Invalid lock token", null); 404 } 405 }else{ 406 System.out.println("Invalid record unlock key."); 407 throw new SecurityException("Invalid record unlock key.", null); 408 } 409 } 410 }// end unlockRecord method 411 412 413 414 415 /** 416 * Searches the records in the datafile for records that match the String 417 * values of search_criteria. search_criteria[n] contains the search value 418 * applied against field n. Data files can be searched for Title & Author. 419 * 420 */ 421 public long[] searchRecords(String[] search_criteria){ 422 Vector hit_vector = new Vector(); 423 for(int i=0; i<_record_count; i++){ 424 try{ 425 if(thereIsAMatch(search_criteria, readRecord(i))){ 426 hit_vector.addElement(new Long(i)); 427 } 428 }catch(RecordNotFoundException ignored){ } // ignore deleted records 429 } // end for 430 long hits[] = new long[hit_vector.size()]; 431 for(int i=0; i<hits.length; i++){ 432 hits[i] = ((Long)hit_vector.elementAt(i)).longValue(); 433 } 434 return hits; 435 } 436 437 438 439 440 441 /** 442 * thereIsAMatch() is a utility method that actually performs 443 * the record search. Implements an implied OR/AND search by detecting 444 * the first character of the Title criteria element. 445 */ 446 private boolean thereIsAMatch(String[] search_criteria, String[] record){ 447 boolean match_result = false; 448 final int TITLE = 0; 449 final int AUTHOR = 1; 450 for(int i=0; i<search_criteria.length; i++){ 451 if((search_criteria[i].length() == 0) || (record[i+1].startsWith(search_criteria[i]))){ 452 match_result = true; 453 break; 454 } //end if 455 } //end for 456 457 if(((search_criteria[TITLE].length() > 1) && (search_criteria[AUTHOR].length() >= 1)) && 458 (search_criteria[TITLE].charAt(0) == '&')) { 459 if(record[TITLE+1].startsWith(search_criteria[TITLE].substring(1, search_criteria[TITLE].length()).trim()) && record[AUTHOR+1].startsWith(search_criteria[AUTHOR])){ 460 match_result = true; 461 }else { 462 match_result = false; 463 } 464 465 } // end if 466 return match_result; 467 } // end thereIsAMatch() 468 469 470 471 472 /** 473 * gotoRecordNumber - utility function that handles the messy 474 * details of seeking a particular record. 475 * 476 */ 477 private void gotoRecordNumber(long record_number) throws RecordNotFoundException { 478 if((record_number < 0) || (record_number > _record_count)){ 479 throw new RecordNotFoundException(null); 480 }else{ 481 try{ 482 _raf.seek(RECORDS_START + (record_number * RECORD_LENGTH)); 483 484 }catch(IOException e){ 485 System.out.println(e.toString()); 486 throw new RecordNotFoundException(e); 487 } 488 }// end else 489 490 } // end gotoRecordNumber method 491 492 493 494 /** 495 * stringToPaddedByteField - pads the field to maintain fixed 496 * field length. 497 * 498 */ 499 protected byte[] stringToPaddedByteField(String s, int field_length){ 500 byte[] byte_field = new byte[field_length]; 501 if(s.length() <= field_length){ 502 for(int i = 0; i<s.length(); i++){ 503 byte_field[i] = (byte)s.charAt(i); 504 } 505 for(int i = s.length(); i<field_length; i++){ 506 byte_field[i] = (byte)' '; //pad the field 507 } 508 }else{ 509 for(int i = 0; i<field_length; i++){ 510 byte_field[i] = (byte)s.charAt(i); 511 } 512 } 513 return byte_field; 514 } // end stringToPaddedByteField() 515 516 517 /** 518 * recordBytesToStringArray - reads an array of bytes from a data file 519 * and converts them to an array of Strings. The first element of the 520 * returned array is the record number. The length of the byte array 521 * argument is RECORD_LENGTH -1. 522 * 523 */ 524 private String[] recordBytesToStringArray(long record_number){ 525 String record_string = null; 526 String[] string_array = new String[FIELD_COUNT]; 527 byte[] title = new byte[TITLE_FIELD_LENGTH]; 528 byte[] author = new byte[AUTHOR_FIELD_LENGTH]; 529 byte[] isbn = new byte[ISBN_FIELD_LENGTH]; 530 byte[] price = new byte[PRICE_FIELD_LENGTH]; 531 532 try{ 533 string_array[0] = Long.toString(record_number); 534 _raf.read(title); 535 string_array[TITLE_FIELD + 1] = new String(title).trim(); 536 _raf.read(author); 537 string_array[AUTHOR_FIELD + 1] = new String(author).trim(); 538 string_array[PUB_CODE_FIELD + 1] = Integer.toString(_raf.readInt()); 539 _raf.read(isbn); 540 string_array[ISBN_FIELD + 1] = new String(isbn); 541 _raf.read(price); 542 string_array[PRICE_FIELD + 1] = new String(price).trim(); 543 string_array[QOH_FIELD + 1] = Integer.toString(_raf.readInt()); 544 }catch(IOException e){ 545 System.out.println(e.toString()); 546 } 547 return string_array; 548 } // end recordBytesToStringArray() 549 550 551 552 /** 553 * readHeader - reads the header bytes and converts them to 554 * a string 555 * 556 */ 557 public String readHeader(){ 558 StringBuffer sb = new StringBuffer(); 559 byte[] deleted = new byte[DELETED_STRING.length()]; 560 byte[] title = new byte[TITLE_STRING.length()]; 561 byte[] author = new byte[AUTHOR_STRING.length()]; 562 byte[] pub_code = new byte[PUB_CODE_STRING.length()]; 563 byte[] isbn = new byte[ISBN_STRING.length()]; 564 byte[] price = new byte[PRICE_STRING.length()]; 565 byte[] qoh = new byte[QOH_STRING.length()]; 566 try{ 567 _raf.seek(0); 568 sb.append(Short.toString(_raf.readShort()) + " "); 569 _raf.read(deleted); 570 sb.append(new String(deleted) + " "); 571 sb.append(Short.toString(_raf.readShort()) + " "); 572 _raf.read(title); 573 sb.append(new String(title) + " "); 574 sb.append(Short.toString(_raf.readShort()) + " "); 575 _raf.read(author); 576 sb.append(new String(author) + " "); 577 sb.append(Short.toString(_raf.readShort()) + " "); 578 _raf.read(pub_code); 579 sb.append(new String(pub_code) + " "); 580 sb.append(Short.toString(_raf.readShort()) + " "); 581 _raf.read(isbn); 582 sb.append(new String(isbn) + " "); 583 sb.append(Short.toString(_raf.readShort()) + " "); 584 _raf.read(price); 585 sb.append(new String(price) + " "); 586 sb.append(Short.toString(_raf.readShort()) + " "); 587 _raf.read(qoh); 588 sb.append(new String(qoh) + " "); 589 sb.append(Short.toString(_raf.readShort()) + " "); 590 }catch(IOException e){ 591 System.out.println(e.toString()); 592 } 593 return sb.toString(); 594 } // end readHeader() 595 596 /** 597 * getRecordCount() returns the number of records contained in the data file 598 * 599 */ 600 public long getRecordCount(){ return _record_count; } 601 602 } // end DataFileAdapter class definition
Example 18.24: FailedRecordCreationException.java
1 public class FailedRecordCreationException extends Exception { 2 3 /***************************************************** 4 * Constructor that takes a String argument 5 *****************************************************/ 6 public FailedRecordCreationException(String message, Throwable cause){ 7 super(message, cause); 8 } 9 10 /******************************************** 11 * Constructor that takes Throwable argument 12 ********************************************/ 13 public FailedRecordCreationException(Throwable cause){ 14 this("Failed Record Creation Exception", cause); 15 } 16 17 }
Example 18.25: InvalidDataFileException.java
1 public class InvalidDataFileException extends Exception { 2 3 /************************************************************ 4 * Constructor - Takes one String argument 5 ************************************************************/ 6 public InvalidDataFileException(String message, Throwable cause){ 7 super(message, cause); 8 } 9 10 11 /************************************************************ 12 * Constructor that takes Throwable argument 13 ************************************************************/ 14 public InvalidDataFileException(Throwable cause){ 15 this("Invalid Data File Exception", cause); 16 } 17 }
Example 18.26: NewDataFileException.java
1 public class NewDataFileException extends Exception { 2 3 /************************************************************ 4 * Constructor - One String argument 5 ************************************************************/ 6 public NewDataFileException(String message, Throwable cause){ 7 super(message, cause); 8 } 9 10 11 /************************************************************ 12 * Constructor that takes Throwable argument 13 ************************************************************/ 14 public NewDataFileException(Throwable cause){ 15 this("New Data File Exception", cause); 16 } 17 }
Example 18.27: RecordNotFoundException.java
1 public class RecordNotFoundException extends Exception { 2 3 /************************************************************ 4 * Constructor - Takes one String argument 5 ************************************************************/ 6 public RecordNotFoundException(String message, Throwable cause){ 7 super(message, cause); 8 } 9 10 /************************************************************ 11 * Constructor that takes Throwable argument 12 ************************************************************/ 13 public RecordNotFoundException(Throwable cause){ 14 this("Record Not Found Exception", cause); 15 } 16 }
Example 18.28: SecurityException.java
1 public class SecurityException extends Exception { 2 3 /************************************************************ 4 * Constructor - Takes one String argument 5 ************************************************************/ 6 public SecurityException(String message, Throwable cause){ 7 super(message, cause); 8 } 9 10 /************************************************************ 11 * Constructor that takes a Throwable argument 12 ************************************************************/ 13 public SecurityException(Throwable cause){ 14 this("Security Exception", cause); 15 } 16 }
Example 18.29: AdapterTesterApp.java
1 public class AdapterTesterApp { 2 public static void main(String[] args){ 3 try{ 4 DataFileAdapter adapter = new DataFileAdapter(); 5 String[] rec_1 = {"C++ For Artists", "Rick Miller", "23", "1-932504-02-8", "$59.95", "80"}; 6 String[] rec_2 = {"Java For Artists", "Rick Miller", "23", "1-932504-04-X", "$69.95", "100"}; 7 8 String[] search_string = {"Java", " "}; 9 10 String[] temp_string = null; 11 12 //adapter.createNewDataFile("books.dat"); 13 adapter.createRecord(rec_1); 14 adapter.createRecord(rec_1); 15 adapter.createRecord(rec_1); 16 adapter.createRecord(rec_1); 17 adapter.createRecord(rec_1); 18 adapter.createRecord(rec_1); 19 adapter.createRecord(rec_1); 20 adapter.createRecord(rec_1); 21 22 long lock_token = adapter.lockRecord(2); 23 24 adapter.updateRecord(2, rec_2, lock_token); 25 adapter.unlockRecord(2, lock_token); 26 27 lock_token = adapter.lockRecord(1); 28 adapter.deleteRecord(1, lock_token); 29 adapter.unlockRecord(1, lock_token); 30 31 32 33 long[] search_hits = adapter.searchRecords(search_string); 34 35 36 System.out.println(adapter.readHeader()); 37 38 for(int i=0; i<search_hits.length; i++){ 39 try{ 40 temp_string = adapter.readRecord(search_hits[i]); 41 for(int j = 0; j<temp_string.length; j++){ 42 System.out.print(temp_string[j]+" "); 43 } 44 System.out.println(); 45 }catch(RecordNotFoundException e){ System.out.println(e.toString()); } 46 } 47 48 }catch(Exception e){ e.printStackTrace(); } 49 50 } 51 }
Referring to example 18.29 — the AdapterTesterApp class tests the functionality of the DataFileAdapter class and its supporting classes. A DataFileAdapter object is created on line 4. On lines 5 and 6 two String arrays are created that contain the field values for records that will be inserted into the legacy data file for testing purposes. A String array named search_string is created on line 8. This array will be used to test the searchRecords() method.
The statement on line 12 is commented out, but was initially used to test the createNewDataFile() method. Lines 13 through 20 insert a series of record values using the data contained in the rec_1 array.
On line 22 the lockRecord() method is tested and the lock_token obtained is used on lines 24 and 25 to test the updateRecord() and unlockRecord() methods.
The searchRecords() method is tested on line 33 and the resulting array of longs is used in the for statement beginning on line 38 to print the record data for each record returned in the search. Figure 18-30 shows the results of running this program. It should be noted that each time the program is executed in its current version additional records with the data contained in the rec_1 array will be appended to the end of the file.
Figure 18-30: Results of Running Example 18.29.
The RandomAccessFile class is used to write and read bytes, characters, and primitive type values to and from any position within a file. The RandomAccessFile stands alone in that it is not related to the InputStream, OutputStream, Reader, or Writer classes.