Model Part 1 - Stock Interface ImplementationJ8 Home « Model Part 1 - Stock Interface implementation

In our final lesson of the Model Part 1 we write an implementation class for the Stock Interface.

Implementing The Stock Interface Top

The final thing to do for Model Part 1 is to implement the Stock interface using the StockImpl class and create some method stubs we will come back to, when we revisit the model code in the Model Part 2 section.

Before we code up the StockImpl class we can make a few design decisions with regards to use of the class and reading the Manufacturer file:

The StockImpl will also implement the Remote interface so that objects of this class can be passed as remote objects and create dynamic stubs via the UnicastRemoteObject exportObject(Remote, port) method rather than using static stubs and rmic which are deprecated.

A StockImpl object provides access to all the Manufacturer information and the methods required to modify this information are synchronised in this class. We will use the Singleton pattern so that only one StockImpl object will ever exist and as such all access to the Manufacturer file will be via the StockImpl singleton.

The majority of requests for Manufacturer information will be via searches from users of the GUI to browse records via name or location before doing any stock adjustments required. This will be slow due to the I/O involved accessing the Manufacturer file and so we will create a Map collection of Manufacturer records on startup that we can use as a cache to improve performance. This will cut down on file I/O and from then on we will only need to access the Manufacturer file when we need to physically CRUD a record. We will use statics for locking of the Map collection when required.

We will be creating a log file with a namespace of J8CaseStudy using the java.util.Logger class.

Access to the methods of the StockImpl class can be invoked remotely and so this object can be serialized, therefore we use a version number so that serialisation can occur without worrying about the underlying class changing between serialization and deserialization.

The StockImpl constructor takes the path for the Manufacturer file as a parameter, will use a private access modifier so users can only access the constructor via a public getter. A point of interest here is that we will create the StockImpl Singleton first time through using double-checked locking to ensure the private constructor is only synchronized once. This is achieved using checks before and after entering the synchronization block as well as using the volatile modifier, which is a special mechanism to guarantee that communication happens between threads.

The methods of the Stock interface are implemented and any locking or synchronization blocks are kept as small as possible to aid efficiency.

The other completed methods of the StockImpl class are used to manage field movement from/to the Manufacturer file.

There are also two inner classes coded where appropriate to aid in reading and writing fields from/to the Manufacturer file.

The uncompleted methods at the end of the class are the stubs we will complete in the Model Part 2 section.

Creating The StockImpl Class Top

Create the StockImpl class in the model package and cut and paste the following code into it.


package model;

import java.io.*;
import java.rmi.Remote;
import java.util.*;
import java.util.concurrent.locks.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A StockImpl object provides access to all the Manufacturer information
 * and the methods required to modify this information are synchronised 
 * in this class. The class uses the Singleton pattern so that only one
 * StockImpl object will ever exist. This is the model element of the MVC paradigm.
 * 
 * @author Charlie 
 * @version 1.0
 *
 */
public class StockImpl implements Remote, Stock {
    /**
     * The Logger instance through which all log messages from this class are
     * routed. Logger namespace is J8CaseStudy.
     */
    private static Logger log = Logger.getLogger("J8CaseStudy"); // Log output

    /**
     * A version number for the StockImpl class so that serialisation 
     * can occur without worrying about the underlying class changing 
     * between serialisation and deserialisation.
     */
    @SuppressWarnings("unused")
    private static final long serialVersionUID = 2498052502L;

    /**
     * Only create StockImpl Singleton first time through using double-checked 
     * locking to ensure private constructor is only synchronised once.
     */
    private volatile static StockImpl uniqueStockImplInstance;

    /**
     * Starting position of Manufacturer file.
     */
    private static int initialOffset = 0;

    /**
     * The pathname where the Manufacturer file is stored.
     */
    private static String storedManufacturerPathName = null;

    /**
     * A String for use when writing a Manufacturer record.
     */
    private static String emptyManufacturerRecordString = null;

    /**
     * Builds the emptyManufacturerRecordString.
     */
    static {
    	emptyManufacturerRecordString = 
                new String(new byte[Manufacturer.MANUFACTURER_RECORD_LENGTH]);
    }
    
    /**
     * The physical file on disk containing our Manufacturer records.
     */
    private RandomAccessFile manufacturerFile = null;

    /**
     * A map containing our manufacturer record number via file position and
     * a lock number value derived from the system time.
     */
    private static Map<Long, Long> manufacturerRecordNumbers
            = new HashMap<Long, Long>();

    /**
     * Ensures many users can read the manufacturerRecordNumbers
     * collection as long as nobody is updating it.
     */
    private static final Lock manufacturerRecordNumbersLock = new ReentrantLock();

    /**
     * Ensures that many users can read the manufacturerRecordNumbers
     * collection as long as nobody is updating it.
     */
    private static final Condition lockReleased = manufacturerRecordNumbersLock.newCondition();

    /**
     * A cached Map of manufacturer records with a record number
     * key to reduce I/O.
     */
    private static Map<Long, Manufacturer> manufacturerCache = new HashMap<Long, Manufacturer>();

    /**
     * Ensures that many users can read the manufacturerCache
     * collection as long as nobody is updating it.
     */
    private static ReadWriteLock manufacturerCacheLock = new ReentrantReadWriteLock();

    /**
     * StockImpl Constructor that takes the path for the Manufacturer file as a parameter.
     *
     * @param filePathName the path to the manufacturer file directory
     * 
     * @throws ManufacturerFileAccessException Indicates there is a problem 
     *         accessing the Manufacturer's data.
     */
    private StockImpl(String filePathName) throws ManufacturerFileAccessException {
        log.entering("StockImpl", "StockImpl", filePathName);
        if (manufacturerFile == null) {
            try {
            	manufacturerFile = new RandomAccessFile(filePathName, "rw");
                // Process Manufacturer file header information
                processManufacturerHeader();
                // Populate a cached map of manufacturers
                getManufacturersMap(); 
            	storedManufacturerPathName = filePathName;
                log.fine("Manufacturer file opened and file position map populated");
            }
            catch (IOException e) {
                log.log(Level.SEVERE, e.getMessage(), e);
            	throw new ManufacturerFileAccessException(e);
            }
        } else if (storedManufacturerPathName != filePathName) {
            log.warning("Only one manufacturer file location can be specified. "
                        + "Current pathname: " + storedManufacturerPathName + " "
                        + "Ignoring specified pathname: " + filePathName);
        }
        log.exiting("StockImpl", "StockImpl");
    }

    /**
     * Clients of the StockImpl class using a pathname for the Manufacturer file
     * have to call this method to get the unique instance (singleton)
     * for this class as the constructors are private.
     * 
     * @param filePathName the path to the Manufacturer directory
     * 
     * @return The singleton for StockImpl class.
     */
    public static StockImpl getStockImplInstance(String filePathName) {
        if (uniqueStockImplInstance == null) {
            synchronized (StockImpl.class) {
                if (uniqueStockImplInstance == null) { 
                    uniqueStockImplInstance = new StockImpl(filePathName);
                }
            }
        }
        return uniqueStockImplInstance;
    }

    /**
     * This method processes Manufacturer header information
     *
     */
    private void processManufacturerHeader() throws ManufacturerFileAccessException {
    	try {
            log.entering("manufacturerData", "processManufacturerHeader");
            int fileNumber = manufacturerFile.readInt();
            log.fine("Manufacturer File number is: " + fileNumber);
            // Add header bytes to static int so we can ascertain
            // starting position of first Manufacturer data record
            initialOffset += 4;
        } catch (IOException e) {
            log.log(Level.SEVERE, e.getMessage(), e);
            throw new ManufacturerFileAccessException(e);
        }
        log.exiting("manufacturerData", "processManufacturerHeader");
    }

    /**
     * Creates a new record in the Manufacturer file which could be a deleted
     * entry. Inserts the given Manufacturer data, and returns the record
     * number of the new record.
     *
     * @param manufacturerData A string array of Manufacturer 
     * data for the Manufacturer to be created.
     * 
     * @return A long value denoting the generated record number.
     * @throws DuplicateKeyException Indicates there is an existing record with 
     * same name/location values that doesn't have deletion flag set to deleted.
     * @throws ManufacturerFileAccessException Indicates there is a problem 
     * accessing the Manufacturer's data.
     */
    public long createRecord(String[] manufacturerData) 
             throws DuplicateKeyException, ManufacturerFileAccessException {
        log.entering("StockImpl", "createRecord", manufacturerData);
    	log.fine("createRecord Run by Thread: " 
                + Thread.currentThread().getName());
        // Populate Manufacturer from the passed string array.
        Manufacturer manufacturer = populateManufacturerFields(manufacturerData);
        Long positionInFile = 0L;
        boolean reusingDeletedRecord = false;
        
        try {
            positionInFile = manufacturerFile.length();
        } 
        catch (IOException e) {
            log.log(Level.SEVERE, e.getMessage(), e);
   	     	throw new ManufacturerFileAccessException(e);
        }
        /*
         * Check name and location field values on the cached map to see if a 
         * record already exists with same  values. If it does and deletion flag 
         * set to deleted, reuse this record, otherwise throw a DuplicateKeyException.
         */
        manufacturerCacheLock.readLock().lock();
        Set<Long> recOffsetSet = new TreeSet<Long>(manufacturerCache.keySet());
        for (Long s : recOffsetSet) {
            Manufacturer manufacturerCheck = manufacturerCache.get(s);
            if (manufacturerData[1].equalsIgnoreCase(manufacturerCheck.getName()) 
                && manufacturerData[2].equalsIgnoreCase(manufacturerCheck.getLocation())) {
                // name and location match, see if deletion flag set to deleted
                if (manufacturerCheck.getDeletedFlag().equals("1")) {
                    manufacturer.setDeletedFlag("0");
                    positionInFile = s;
                    reusingDeletedRecord = true;
                    log.log(Level.INFO, "Reusing record number: " + s 
                            + "Manufacturer: " + manufacturer.getName() 
                            + " , " + manufacturer.getLocation() 
                            + " and resetting deletion flag to 0");
                    break;
                } else {
                    String dke = "Duplicate record found: " + s + 
                            " Update cancelled."; 
				 log.log(Level.SEVERE, dke);
                    throw new DuplicateKeyException(dke);
                }
            }
        }
        manufacturerCacheLock.readLock().unlock();
        manufacturerCacheLock.writeLock().lock();
        if (!reusingDeletedRecord) {
            //Check for the case when another record has been added
            while (manufacturerCache.containsValue(positionInFile)) {
                positionInFile += Manufacturer.MANUFACTURER_RECORD_LENGTH;
            }
        }
       	manufacturerCache.put(positionInFile, manufacturer);
        manufacturerCacheLock.writeLock().unlock();
        log.info("Creating record number: " +  positionInFile 
           		+ ". Manufacturer information = :" + manufacturer);

        StringBuilder manufacturerString = manufacturerToString(manufacturer);

        // Block other users for minimum time possible. 
        synchronized (manufacturerFile) {
            try {
       	     	manufacturerFile.seek(positionInFile);
       	     	manufacturerFile.write(manufacturerString.toString().getBytes());
            }
            catch (IOException e) {
                log.log(Level.SEVERE, e.getMessage(), e);
       	     	throw new ManufacturerFileAccessException(e);
            }
        }
        log.exiting("StockImpl", "createRecord", positionInFile);
        return positionInFile;
    }

    /**
     * This method reads a record from the Manufacturer file and returns a 
     * String array where each element is a Manufacturer field value.
     *
     * @param recOffset The offset into the file where the record starts.
     * 
	 * @return A String array containing the fields of the 
	 * Manufacturer record.
     * 
     * @throws RecordNotFoundException Deletion flag has been set to deleted.
     * @throws ManufacturerFileAccessException Indicates there is a problem 
     *         accessing the Manufacturer's data.
     */
    public String[] readRecord(long recOffset) 
	                throws RecordNotFoundException, ManufacturerFileAccessException {
        log.entering("StockImpl", "readRecord", new Object[]{recOffset});
    	log.fine("readRecord Run by Thread: " 
    			+ Thread.currentThread().getName()
    			+ " for record number: " + recOffset);
        final byte[] manufacturerInput 
        		= new byte[Manufacturer.MANUFACTURER_RECORD_LENGTH];
        // Block other users for minimum time possible. 
        synchronized (manufacturerFile) {
            try {
        	    manufacturerFile.seek(recOffset);
        	    manufacturerFile.readFully(manufacturerInput);
            }
            catch (IOException e) {
                log.log(Level.SEVERE, e.getMessage(), e);
            	throw new ManufacturerFileAccessException(e);
            }
        }
        // Convert bytes into a Manufacturer string array.
        /**
         * Class to assist in converting from one big byte[] into multiple
         * String[] - one String per field. 
         */
        class ManufacturerFieldReader {
            /** field to track the position within the byte array */
            private int fieldOffset = 0;
            /**
             * Converts the required number of bytes into a String.
             *
             * @param length, the length to be converted from current offset.
             * @return the converted String
             * @throws UnsupportedEncodingException if "UTF-8" not known.
             */
            String read(int length) throws UnsupportedEncodingException {
                String str = new String(manufacturerInput, fieldOffset, 
                		length, "US-ASCII");
                fieldOffset += length;
                return str.trim();
            }
        }

        // Read a number of bytes equal to a field length
        ManufacturerFieldReader readManufacturerRecord = new ManufacturerFieldReader();
      	String returnManufacturer[] = new String[7];
        try {
            returnManufacturer[0] 
                    = readManufacturerRecord.read(Manufacturer.DELETED_FLAG_LENGTH);
            returnManufacturer[1] 
                    = readManufacturerRecord.read(Manufacturer.NAME_LENGTH);
            returnManufacturer[2] 
                    = readManufacturerRecord.read(Manufacturer.LOCATION_LENGTH);
            returnManufacturer[3] 
                    = readManufacturerRecord.read(Manufacturer.PRODUCT_LENGTH);
            returnManufacturer[4] 
                    = readManufacturerRecord.read(Manufacturer.PRICE_LENGTH);
            returnManufacturer[5] 
                    = readManufacturerRecord.read(Manufacturer.STOCK_LEVEL_LENGTH);
            returnManufacturer[6] 
                    = readManufacturerRecord.read(Manufacturer.STOCK_ORDERED_LENGTH);
	    }
	    catch (UnsupportedEncodingException e){
            log.log(Level.SEVERE, e.getMessage(), e);
   	     	throw new ManufacturerFileAccessException(e);
	    }
        log.exiting("StockImpl", "readRecord", returnManufacturer.toString());
        return returnManufacturer;
    }

    /**
     * Modifies the fields of a Manufacturer record. The new value for field n 
     * appears in manufacturerData[n]. Throws a SecurityException if the record 
     * is locked with a number other than lockRecord.
     *
     * @param recOffset long value denoting the generated record number.
     * @param manufacturerData A String array of Manufacturer fields.
     * @param lockRecord long value denoting the generated lock number.
     * 
     * @throws RecordNotFoundException Deletion flag has been set to deleted.
     * @throws SecurityException Indicates there is a mismatch between
     * the passed record number and the record number stored in the locking map.
     */
    public void updateRecord(long recOffset, String[] manufacturerData, long lockRecord)
            throws RecordNotFoundException, SecurityException {
        log.entering("StockImpl", "updateRecord", new Object[]{recOffset, manufacturerData});
        log.fine("updateRecord Run by Thread: " 
                + Thread.currentThread().getName()
                + " Lock number generated: " + lockRecord
                + " for record number: " + recOffset);

        Long lock = 0L;

        // Make sure record is still in locking map.
        if (!manufacturerRecordNumbers.containsKey(recOffset)) {
            String e = "Record number: " + recOffset 
			         + " not found in locking map."; 
            log.log(Level.SEVERE, e);
	            throw new RecordNotFoundException(e); 
        } else {
            lock = manufacturerRecordNumbers.get(recOffset);
        }
        
        //Ensure lock number supplied matches locking map number
    	if (lock == lockRecord){
            //Lock on cache map next
            manufacturerCacheLock.writeLock().lock();
            // Populate Manufacturer from the passed string array.
            Manufacturer manufacturer = populateManufacturerFields(manufacturerData);
            try {
                //Check whether record already has deletion flag set
                if (manufacturer.getDeletedFlag().equals("1")) {
                    String rnf = "Record number: " + recOffset 
                               + " has deletion flag set"; 
                    log.log(Level.SEVERE, rnf);
                    throw new RecordNotFoundException(rnf); 
                } else {
                    StringBuilder manufacturerString 
                            = manufacturerToString(manufacturer);
                    //Everything fine so physically update Manufacturer file
                    try {
                        synchronized (manufacturerFile) {
                            manufacturerFile.seek(recOffset);
                            manufacturerFile.write(
                            manufacturerString.toString().getBytes());
                        }
                    } catch (IOException e) {
                        log.log(Level.SEVERE, e.getMessage(), e);
                        throw new ManufacturerFileAccessException(e);
                    }
                }    
            } finally {
                manufacturerCache.put(recOffset, manufacturer);
                manufacturerCacheLock.writeLock().unlock();
            }
    	} else {
            String se = "Attempt to update failed due to lock number mismatch. " 
                    + " Thread " + Thread.currentThread().getName() 
                    + " Supplied lock number = :"  + lockRecord 
                    + ". Lock number value on locking map = :" + lock;
            log.log(Level.SEVERE, se);
            throw new SecurityException(se);
    	}
    	log.exiting("StockImpl", "updateRecord");
    }

    /**
     * Deletes a record, making the record number and associated disk storage
     * available for reuse. Throws SecurityException if the record is locked
     * with a number other than lockRecord.
     *
     * @param recOffset the offset into the file where the record starts.
     * @param lockRecord A lock number to be tested against when updating.
     * 
     * @throws RecordNotFoundException Deletion flag has been set to deleted.
     * @throws SecurityException Indicates there is a mismatch between the
     * passed record number and the record number stored in the locking map.
     */
    public void deleteRecord(long recOffset, long lockRecord)
            throws RecordNotFoundException, SecurityException {
        log.entering("StockImpl", "deleteRecord", new Object[]{recOffset, lockRecord});
    	log.fine("deleteRecord Run by Thread: " 
                + Thread.currentThread().getName()
                + " Lock number generated: " + lockRecord
                + " for record number: " + recOffset);
    	Long lock = 0L;

    	// Make sure record is still in locking map.
    	if (!manufacturerRecordNumbers.containsKey(recOffset)) {
            String rnf = "Record number: " + recOffset + " not found in locking map."; 
            log.log(Level.SEVERE, rnf);
            throw new RecordNotFoundException(rnf); 
    	} else {
            lock = manufacturerRecordNumbers.get(recOffset);
    	}
    	// Ensure lock number supplied matches locking map lock number
    	if (lock == lockRecord){
            // Lock on cache map next
            manufacturerCacheLock.writeLock().lock();
            Manufacturer manufacturer = manufacturerCache.get(recOffset);
            try {
                // Check whether record already has deletion flag set
                if (manufacturer.getDeletedFlag().equals("1")) {
                    String rnf = "Record number: " + recOffset + 
                            " already has deleted flag set."; 
                    log.log(Level.SEVERE, rnf);
                    throw new RecordNotFoundException(rnf); 
                } else {
                    manufacturer.setDeletedFlag("1");
                    StringBuilder manufacturerString 
                            = manufacturerToString(manufacturer);
                    // Everything fine so physically delete from Manufacturer file
                    try {
                        synchronized (manufacturerFile) {
                            manufacturerFile.seek(recOffset);
                            manufacturerFile.write(
                                    manufacturerString.toString().getBytes());
        				}
                    } catch (IOException e) {
                        log.log(Level.SEVERE, e.getMessage(), e);
                        throw new ManufacturerFileAccessException(e);
                    }
                    log.fine("Record number: " + recOffset + 
                            " deletion flag set to deleted (1)");
                }    
            } finally {
                manufacturerCache.put(recOffset, manufacturer);
                manufacturerCacheLock.writeLock().unlock();
            }
    	} else {
            String se = "Attempt to delete failed due to lock number mismatch. " 
                    + " Thread " + Thread.currentThread().getName() 
                    + " Supplied lock = : "  + lockRecord 
                    + ". Lock number value on locking map = : " + lock;
            log.log(Level.SEVERE, se);
            throw new SecurityException(se);
    	}
        log.exiting("StockImpl", "deleteRecord");
	}
	
    /**
     * This method searches the cached map of manufacturers for entries
     * that match the selection criteria and returns an array of record numbers 
     * that match the specified search. Field n in the Manufacturer file is 
     * described by searchCriteria[n]. A null value in searchCriteria[n] matches 
     * any field value. A non-null value in searchCriteria[n] matches any field 
     * value that begins with searchCriteria[n]. 
     *
     * @param searchCriteria The search criteria for retrieving records.
     * 
     * @return A long array of manufacturer record numbers
     * matching the search criteria.
     */
    public long[] findBySearchCriteria(String[] searchCriteria) {
        log.entering("StockImpl", "findBySearchCriteria", searchCriteria);
    	log.fine("findBySearchCriteria Run by Thread: " 
                + Thread.currentThread().getName());
        boolean searchName = false;
        boolean searchLocation = false;
        // Create an array to hold returned records numbers (500 should be enough)
        long[] recOffsets = new long[500];
        // Passed name for search
        if (searchCriteria[0] != null) {
            searchName = true;
        }
        // Passed location for search
        if (searchCriteria[1] != null) {
            searchLocation = true;
        }
        manufacturerCacheLock.readLock().lock();
        Set<Long> recOffsetSet = new TreeSet<Long>(manufacturerCache.keySet());
        int recOffsetIdx = 0;
        for (Long s : recOffsetSet) {
            Manufacturer manufacturer = manufacturerCache.get(s);
            // Check if record is deleted
            if (manufacturer.getDeletedFlag().equals("1")) {
                log.log(Level.FINER, "Manufacturer: " + manufacturer.getName() 
                        + " , " + manufacturer.getLocation() + " , "
                        + " has deletion flag set");
            } else if (searchName && searchLocation) {
                // Return an array searched on name and location
                if (manufacturer.getName().startsWith(searchCriteria[0]) 
                    && manufacturer.getLocation().startsWith(searchCriteria[1])) {
                        recOffsets[recOffsetIdx] = s;
                        recOffsetIdx += 1;
                }
            } else if (searchName) {
                // Return  an array searched on name
                if (manufacturer.getName().startsWith(searchCriteria[0])) {
                    recOffsets[recOffsetIdx] = s;
                    recOffsetIdx += 1;
                }
            } else if (searchLocation) {
                // Return  an array searched on location
                if (manufacturer.getLocation().startsWith(searchCriteria[1])) { 
                    recOffsets[recOffsetIdx] = s;
                    recOffsetIdx += 1;
                }
            } else {
                // Return  an array of all name/locations (no search criteria passed)
                recOffsets[recOffsetIdx] = s;
                recOffsetIdx += 1;
            }
        }
        manufacturerCacheLock.readLock().unlock();
        log.exiting("StockImpl", "findBySearchCriteria", recOffsets);
        return recOffsets;
    }

    /**
     * This method will add a record to the locking map. If the key is
     * already in the locking map the thread will wait until the record
     * is removed from map and the lock released.
     * Locks a record in the locking so that it can only be updated or deleted 
     * by this client. Returned value is a number that must be used when the 
     * record is unlocked, updated, or deleted. If the specified record is 
     * already locked by a different client, the current thread goes into a 
     * wait state until the record is unlocked.
     *
     * @param recOffset the offset into the file where the record starts.
     * 
     * @return A long value denoting the generated lock number.
     * 
     * @throws RecordNotFoundException Indicates there is a problem accessing
     * the Manufacturer record in the cahe map.
     */
    public long lockRecord(long recOffset) throws RecordNotFoundException {
        log.entering("StockImpl", "lockRecord", recOffset);
                long lockNumber = System.nanoTime();
        log.fine("lock Run by Thread: " 
                + Thread.currentThread().getName() 
                + " Lock number generated: " + lockNumber
                + " for record number: " + recOffset);
        manufacturerRecordNumbersLock.lock();
        try {
            // Wait for the record to become unlocked.
            while (manufacturerRecordNumbers.containsKey(recOffset)) {
                try {
                    log.fine("Thread: " + Thread.currentThread().getName()
                            + " awaiting for lock to be released"
                            + " for record number: " + recOffset);
                    lockReleased.await();
                } catch (InterruptedException e) {
                    log.log(Level.SEVERE, e.getMessage(), e);
                    throw new ManufacturerFileAccessException(e);
                }
            }
            // Check if manufacturer record exists.
            try {
                manufacturerCacheLock.readLock().lock();

                // Check if the given record is present.
                if (!manufacturerCache.containsKey(recOffset)) {
                    String rnf = "There is no manufacturer record with number: " 
                            + recOffset; 
                    log.log(Level.SEVERE, rnf);
                    throw new RecordNotFoundException(rnf);
                }
            } finally {
                manufacturerCacheLock.readLock().unlock();
            }
            manufacturerRecordNumbers.put(recOffset, lockNumber);
            log.fine("Thread: " + Thread.currentThread().getName() 
                                + " Record number: " + recOffset + " added to locking map");
        } finally {
            manufacturerRecordNumbersLock.unlock();
        }
        log.exiting("StockImpl", "lockRecord", lockNumber);
        return lockNumber;
    }

    /**
     * This method will remove a record from the locking map. If the passed 
     * locking number equals the locking number stored in the locking map then 
     * record is removed from map, lock released and waiting threads signalled.
     *
     * @param recOffset the offset into the file where the record starts.
     * 
     * @throws SecurityException Indicates there is a mismatch between
     * the passed lock number and the lock number stored in the locking map.
     */
    public void unlock(long recOffset, long lockRecord) throws SecurityException {
        log.entering("StockImpl", "unlock", new Object[]{recOffset, lockRecord});
    	log.fine("unlock Run by: " + Thread.currentThread().getName()
                + " Lock lock number supplied: " + lockRecord);
        manufacturerRecordNumbersLock.lock();
        // Compare lock numbers
        try {
            if (manufacturerRecordNumbers.get(recOffset) == lockRecord) {
                manufacturerRecordNumbers.remove(recOffset);
                log.fine("Thread: " + Thread.currentThread().getName()
                        + " Supplied lock number: "  + lockRecord 
                        + " lock released for record number: " + recOffset);
                lockReleased.signal();
            } else {
                String se = " Thread " + Thread.currentThread().getName() 
                        + "Attempt to unlock record number: " + recOffset 
                        + "failed. Supplied lock number: "  + lockRecord 
                        + ". Lock number value on locking map:" 
                        + manufacturerRecordNumbers.get(recOffset); 
                log.log(Level.SEVERE, se);
                throw new SecurityException(se);
            }
        } finally {
            manufacturerRecordNumbersLock.unlock();
        }
        log.exiting("StockImpl", "unlock");
    }

    /**
     * Public method used by other classes to call the private method
     * getManufacturersMap to ensure the Map isn't corrupted. 
     *
     * @return A Map collection of all Manufacturers.
     * @throws ManufacturerFileAccessException If there is a problem accessing the manufacturerData.
     */
    public Map<Long, Manufacturer> getManufacturers() throws ManufacturerFileAccessException {
        return getManufacturersMap();
    }

    /**
     * Create a Map collection that we can use as a cache to improve 
     * performance and cut down on file I/O.
     *
     * This method is private so we can call it from the constructor 
     * to initially populate the Map from the Manufacturer file. External 
     * calls to this method are routed through the getManufacturers()
     * method.
     * 
     * @return A Map collection of all Manufacturers.
     * @throws ManufacturerFileAccessException Indicates there is a problem 
     * accessing the Manufacturer's data.
     */
    private Map<Long, Manufacturer> getManufacturersMap() throws ManufacturerFileAccessException {
        log.entering("StockImpl", "getManufacturersMap");
    	long fileLength = 0L;

    	try {
            fileLength = manufacturerFile.length();
    	} 
    	catch (IOException e) {
            log.log(Level.SEVERE, e.getMessage(), e);
            throw new ManufacturerFileAccessException(e);
    	}
        for (long positionInFile = initialOffset;
                positionInFile < fileLength;
                positionInFile += Manufacturer.MANUFACTURER_RECORD_LENGTH) {
            log.fine("Reading record at " + positionInFile);
            String manufacturerArray[] = readRecord(positionInFile);
            log.fine("Found record number: " + positionInFile
                 + ". Manufacturer name is: " + manufacturerArray[1]
                 + ". Manufacturer location is: " + manufacturerArray[2]);
            // Populate Manufacturer from the passed string array.
            Manufacturer manufacturer = populateManufacturerFields(manufacturerArray);
            manufacturerCache.put(positionInFile, manufacturer);
            log.fine("Manufacturer cache map populated. Record number is: " + positionInFile);
        }
        log.exiting("StockImpl", "getManufacturersMap", manufacturerCache);
        return manufacturerCache;
    }

    /**
     * Locates a Manufacturer using the record offset.
     *
     * @param recOffset The record offset of the Manufacturer to locate in the cache map.
     * 
     * @return The Manufacturer object matching the record number.
     * @throws RecordNotFoundException Indicates record not found in cache map.
     */
    public Manufacturer getManufacturer(long recOffset) throws RecordNotFoundException {
        log.entering("StockImpl", "getManufacturer", recOffset);
        // Check if manufacturer record exists.
        try {
            manufacturerCacheLock.readLock().lock();
            // Check if the given record is present.
            if (!manufacturerCache.containsKey(recOffset)) {
                String rnf = "There is no manufacturer record with record " +
                        "offset: " + recOffset; 
                log.log(Level.SEVERE, rnf);
                throw new RecordNotFoundException(rnf);
            } else {
                Manufacturer manufacturer = manufacturerCache.get(recOffset);
                return manufacturer;
            }
        } finally {
            log.exiting("StockImpl", "getManufacturer");
            manufacturerCacheLock.readLock().unlock();
        }
    }

    /**
     * This method will create one long string from manufacturer fields.
     *
     * @param manufacturer The Manufacturer reference to convert to a string
     * @return manufacturerArray String of Manufacturer information
     */
    private StringBuilder manufacturerToString(Manufacturer manufacturer) {
        log.entering("StockImpl", "manufacturerToString", manufacturer);
        final StringBuilder out = new StringBuilder(emptyManufacturerRecordString);

        /** 
         * Add fields to out StringBuilder field 
         * */
        class ManufacturerFieldWriter {
            private int currentPosition = 0;
            /**
             * converts a String of specified length to byte[]
             *
             * @param manufacturerData the String to be converted into part of the byte[].
             * @param length the maximum size of the String
             */
            void write(String manufacturerData, int length) {
                out.replace(currentPosition, currentPosition + manufacturerData.length(), 
                		manufacturerData);
                currentPosition += length;
            }
        }
        ManufacturerFieldWriter writeRecord = new ManufacturerFieldWriter();

        writeRecord.write(manufacturer.getDeletedFlag(), Manufacturer.DELETED_FLAG_LENGTH);
        writeRecord.write(manufacturer.getName(), Manufacturer.NAME_LENGTH);
        writeRecord.write(manufacturer.getLocation(), Manufacturer.LOCATION_LENGTH);
        writeRecord.write(manufacturer.getProduct(), Manufacturer.PRODUCT_LENGTH);
        writeRecord.write(manufacturer.getPrice(), Manufacturer.PRICE_LENGTH);
        writeRecord.write(manufacturer.getStockLevel(), Manufacturer.STOCK_LEVEL_LENGTH);
        writeRecord.write(manufacturer.getStockOrdered(), Manufacturer.STOCK_ORDERED_LENGTH);

        log.exiting("StockImpl", "manufacturerToString", out);
        return out; 
    }

    /**
     * This method populates a Manufacturer object.
     *
     * @param manufacturerArray String array holding Manufacturer information.
     * @return Manufacturer object.
     */
    private Manufacturer populateManufacturerFields(String[] manufacturerArray) {
        log.entering("StockImpl", "populateManufacturerFields", manufacturerArray);
        // Instantiate the Manufacturer object
        Manufacturer manufacturer = new Manufacturer();
        // Populate Manufacturer object
        manufacturer.setDeletedFlag(manufacturerArray[0]);
        manufacturer.setName(manufacturerArray[1]);
        manufacturer.setLocation(manufacturerArray[2]);
        manufacturer.setProduct(manufacturerArray[3]);
        manufacturer.setPrice(manufacturerArray[4]);
        manufacturer.setStockLevel(manufacturerArray[5]);
        manufacturer.setStockOrdered(manufacturerArray[6]);
        log.exiting("StockImpl", "populateManufacturerFields", manufacturer);
        return manufacturer;
    }

    /**
     * Locks the locking map manufacturerRecordNumbersLock so that
     * no other client can modify it. Used when we are shutting down the 
     * Manufacturer file.
     *
     * @param lockingMapLocked locked true on the Manufacturer file.
     */
    public void lockLockingMap (boolean lockingMapLocked) {
        log.entering("StockImpl", "LockLockingMap", lockingMapLocked);

        if (lockingMapLocked) {
        	manufacturerRecordNumbersLock.lock();
        }
        log.exiting("StockImpl", "LockLockingMap");
    }

    /**
     * Stock the requested amount entered by a user in the GUI.
     *
     * @param name The name of the Manufacturer.
     * @param location The location where the Manufacturer is based.
     * @param stockLevel The amount of stock left.
     * @param stockOrdered The amount of stock requested.
     * 
     */
    public void stocking(String name, String location, int stockLevel, int stockOrdered) {
        /*
         * TO DO in Model Part 2
         *
         */
    }

    /**
     * Unstock the requested amount entered by a user in the GUI.
     *
     * @param name The name of the Manufacturer.
     * @param location The location where the Manufacturer is based.
     * @param stockOrdered The amount of unstocking to do.
     * 
     */
    public void unstocking(String name, String location, int stockOrdered) {
        /*
         * TO DO in Model Part 2
         *
         */
    }
		
    /**
     * Search manufacturer file via criteria entered by a user in the GUI.
     *
     * @param name The name of the Manufacturer.
     * @param location The location where the Manufacturer is based.

     * 
     */
    public void search(String name, String location) {
        /*
         * TO DO in Model Part 2
         *
         */
    }
}

After adding the StockImpl class your project structure should look similar to that in the screenshot below.

compile StockImpl class
Screenshot 1. Project structure after adding the StockImpl class.

Lesson 6 Complete

In this lesson we coded the StockImpl class.

Related Java Tutorials

Fundamentals - Primitive Variables
Fundamentals - if Construct
Fundamentals - for Construct
Fundamentals - while Construct
Objects & Classes - Class Structure and Syntax
Objects & Classes - Reference Variables
Objects & Classes - Methods
Objects & Classes - Instance Variables & Scope
Objects & Classes - Constructors
Objects & Classes - Static Members
OO Concepts - Encapsulation - Getters & Setters
OO Concepts - Inheritance Concepts - Using the super keyword
OO Concepts - Interfaces
Exceptions - Handling Exceptions
Exceptions - Declaring Exceptions
API Contents - Inheritance - Using the package keyword
API Contents - Inheritance - Using the import keyword
Concurrency - Synchronization - Synchronized Blocks
Swing - RMI - Serialization

What's Next?

In the next section we set up the View elements of the MVC pattern that can be derived from the project proposal.