Open Source Repository

Home /excel/jxl-2.6.12 | Repository Home



jxl/write/biff/WritableWorkbookImpl.java
/*********************************************************************
*
*      Copyright (C) 2002 Andrew Khan
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
***************************************************************************/

package jxl.write.biff;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import jxl.common.Assert;
import jxl.common.Logger;

import jxl.Range;
import jxl.Sheet;
import jxl.Workbook;
import jxl.WorkbookSettings;
import jxl.biff.BuiltInName;
import jxl.biff.CellReferenceHelper;
import jxl.biff.CountryCode;
import jxl.biff.Fonts;
import jxl.biff.FormattingRecords;
import jxl.biff.IndexMapping;
import jxl.biff.IntegerHelper;
import jxl.biff.RangeImpl;
import jxl.biff.WorkbookMethods;
import jxl.biff.XCTRecord;
import jxl.biff.drawing.Drawing;
import jxl.biff.drawing.DrawingGroup;
import jxl.biff.drawing.DrawingGroupObject;
import jxl.biff.drawing.Origin;
import jxl.biff.formula.ExternalSheet;
import jxl.format.Colour;
import jxl.format.RGB;
import jxl.read.biff.WorkbookParser;
import jxl.write.WritableCell;
import jxl.write.WritableSheet;
import jxl.write.WritableWorkbook;


/**
 * A writable workbook
 */
public class WritableWorkbookImpl extends WritableWorkbook 
  implements ExternalSheet, WorkbookMethods
{
  /**
   * The logger
   */
  private static Logger logger = Logger.getLogger(WritableWorkbookImpl.class);
  /**
   * The list of formats available within this workbook
   */
  private FormattingRecords formatRecords;
  /**
   * The output file to write the workbook to
   */
  private File outputFile;
  /**
   * The list of sheets within this workbook
   */
  private ArrayList sheets;
  /**
   * The list of fonts available within this workbook
   */
  private Fonts fonts;
  /**
   * The list of external sheets, used by cell references in formulas
   */
  private ExternalSheetRecord externSheet;

  /**
   * The supbook records
   */
  private ArrayList supbooks;

  /**
   * The name records
   */
  private ArrayList names;

  /**
   * A lookup hash map of the name records
   */
  private HashMap nameRecords;

  /**
   * The shared strings used by this workbook
   */
  private SharedStrings sharedStrings;

  /**
   * Indicates whether or not the output stream should be closed.  This
   * depends on whether this Workbook was created with an output stream,
   * or a flat file (flat file closes the stream
   */
  private boolean closeStream;

  /**
   * The workbook protection flag
   */
  private boolean wbProtected;

  /**
   * The settings for the workbook
   */
  private WorkbookSettings settings;

  /**
   * The list of cells for the entire workbook which need to be updated 
   * following a row/column insert or remove
   */
  private ArrayList rcirCells;

  /**
   * The drawing group
   */
  private DrawingGroup drawingGroup;

  /**
   * The jxl.common.workbook styles
   */
  private Styles styles;

  /**
   * Contains macros flag
   */
  private boolean containsMacros;

  /**
   * The buttons property set
   */
  private ButtonPropertySetRecord buttonPropertySet;

  /**
   * The country record, initialised when copying a workbook
   */
  private CountryRecord countryRecord;

  // synchronizer object for static unitiatialization
  private static Object SYNCHRONIZER = new Object();
  /**
   * The names of any add in functions
   */
  private String[] addInFunctionNames;

  /**
   * The XCT records
   */
  private XCTRecord[] xctRecords;

  /**
   * Constructor.  Writes the workbook direct to the existing output stream
   
   @exception IOException 
   @param os the output stream
   @param cs TRUE if the workbook should close the output stream, FALSE
   @param ws the configuration for this workbook
   * otherwise
   */
  public WritableWorkbookImpl(OutputStream os, boolean cs, WorkbookSettings ws)
    throws IOException
  {
    super();
    outputFile = new File(os, ws, null);
    sheets = new ArrayList();
    sharedStrings = new SharedStrings();
    nameRecords = new HashMap();
    closeStream = cs;
    wbProtected = false;
    containsMacros = false;
    settings = ws;
    rcirCells = new ArrayList();
    styles = new Styles();

    // Reset the statically declared styles.  These are no longer needed
    // because the Styles class will intercept all calls within 
    // CellValue.setCellDetails and if it detects a standard format, then it
    // will return a clone.  In short, the static cell values will
    // never get initialized anyway.  Still, just to be extra sure...
    synchronized(SYNCHRONIZER)
    {
      WritableWorkbook.ARIAL_10_PT.uninitialize();
      WritableWorkbook.HYPERLINK_FONT.uninitialize();
      WritableWorkbook.NORMAL_STYLE.uninitialize();
      WritableWorkbook.HYPERLINK_STYLE.uninitialize();
      WritableWorkbook.HIDDEN_STYLE.uninitialize();
      DateRecord.defaultDateFormat.uninitialize();
     }

    WritableFonts wf = new WritableFonts(this);
    fonts = wf;

    WritableFormattingRecords wfr = new WritableFormattingRecords(fonts, 
                                                                  styles);
    formatRecords = wfr;
  }

  /**
   * A pseudo copy constructor.  Takes the handles to the font and formatting
   * records
   
   @exception IOException 
   @param w the workbook to copy
   @param os the output stream to write the data to
   @param cs TRUE if the workbook should close the output stream, FALSE
   @param ws the configuration for this workbook
   */
  public WritableWorkbookImpl(OutputStream os,
                              Workbook w,
                              boolean cs,
                              WorkbookSettings wsthrows IOException
  {
    super();
    WorkbookParser wp = (WorkbookParserw;

    // Reset the statically declared styles.  These are no longer needed
    // because the Styles class will intercept all calls within 
    // CellValue.setCellDetails and if it detects a standard format, then it
    // will return a clone.  In short, the static cell values will
    // never get initialized anyway.  Still, just to be extra sure...
    synchronized(SYNCHRONIZER)
    {
      WritableWorkbook.ARIAL_10_PT.uninitialize();
      WritableWorkbook.HYPERLINK_FONT.uninitialize();
      WritableWorkbook.NORMAL_STYLE.uninitialize();
      WritableWorkbook.HYPERLINK_STYLE.uninitialize();
      WritableWorkbook.HIDDEN_STYLE.uninitialize();
      DateRecord.defaultDateFormat.uninitialize();
     }

    closeStream = cs;
    sheets = new ArrayList();
    sharedStrings = new SharedStrings();
    nameRecords = new HashMap();
    fonts = wp.getFonts();
    formatRecords = wp.getFormattingRecords();
    wbProtected = false;
    settings = ws;
    rcirCells = new ArrayList();
    styles = new Styles();
    outputFile = new File(os, ws, wp.getCompoundFile());

    containsMacros = false;
    if (!ws.getPropertySetsDisabled())
    {
      containsMacros = wp.containsMacros();
    }

    // Copy the country settings
    if (wp.getCountryRecord() != null)
    {
      countryRecord = new CountryRecord(wp.getCountryRecord());
    }

    // Copy any add in functions
    addInFunctionNames = wp.getAddInFunctionNames();

    // Copy XCT records
    xctRecords = wp.getXCTRecords();

    // Copy any external sheets
    if (wp.getExternalSheetRecord() != null)
    {
      externSheet = new ExternalSheetRecord(wp.getExternalSheetRecord());

      // Get the associated supbooks
      jxl.read.biff.SupbookRecord[] readsr = wp.getSupbookRecords();
      supbooks = new ArrayList(readsr.length);

      for (int i = 0; i < readsr.length; i++)
      {
        jxl.read.biff.SupbookRecord readSupbook = readsr[i];
        if (readSupbook.getType() == readSupbook.INTERNAL ||
            readSupbook.getType() == readSupbook.EXTERNAL)
        {
          supbooks.add(new SupbookRecord(readSupbook, settings));
        }
        else
        {
          if (readSupbook.getType() != readSupbook.ADDIN)
          {
            logger.warn("unsupported supbook type - ignoring");
          }
        }
      }
    }

    // Copy any drawings.  These must be present before we try and copy
    // the images from the read workbook
    if (wp.getDrawingGroup() != null)
    {
      drawingGroup = new DrawingGroup(wp.getDrawingGroup());
    }

    // Copy the property set references
    if (containsMacros && wp.getButtonPropertySet() != null)
    {
      buttonPropertySet = new ButtonPropertySetRecord
        (wp.getButtonPropertySet());
    }

    // Copy any names
    if (!settings.getNamesDisabled())
    {
      jxl.read.biff.NameRecord[] na = wp.getNameRecords();
      names = new ArrayList(na.length);
      
      for (int i = 0; i < na.length; i++)
      {
        if (na[i].isBiff8())
        {
          NameRecord n = new NameRecord(na[i], i);
          names.add(n);
          String name = n.getName();
          nameRecords.put(name, n);
        }
        else
        {
          logger.warn("Cannot copy Biff7 name records - ignoring");
        }
      }
    }
    
    copyWorkbook(w);

    // The copy process may have caused some critical fields in the 
    // read drawing group to change.  Make sure these updates are reflected
    // in the writable drawing group
    if (drawingGroup != null)
    {
      drawingGroup.updateData(wp.getDrawingGroup());
    }
  }

  /**
   * Gets the sheets within this workbook.  Use of this method for
   * large worksheets can cause performance problems.
   
   @return an array of the individual sheets
   */
  public WritableSheet[] getSheets()
  {
    WritableSheet[] sheetArray = new WritableSheet[getNumberOfSheets()];
    
    for (int i = ; i < getNumberOfSheets() ; i++)
    {
      sheetArray[i= getSheet(i);
    }
    return sheetArray;
  }

  /**
   * Gets the sheet names
   *
   @return an array of strings containing the sheet names
   */
  public String[] getSheetNames()
  {
    String[] sheetNames = new String[getNumberOfSheets()];

    for (int i = ; i < sheetNames.length ; i++)
    {
      sheetNames[i= getSheet(i).getName();
    }

    return sheetNames;
  }

  /**
   * Interface method from WorkbookMethods - gets the specified 
   * sheet within this workbook
   *
   @param index the zero based index of the required sheet
   @return The sheet specified by the index
   */
  public Sheet getReadSheet(int index)
  {
    return getSheet(index);
  }

  /**
   * Gets the specified sheet within this workbook
   
   @param index the zero based index of the reQuired sheet
   @return The sheet specified by the index
   */
  public WritableSheet getSheet(int index)
  {
    return (WritableSheetsheets.get(index);
  }

  /**
   * Gets the sheet with the specified name from within this workbook
   
   @param name the sheet name
   @return The sheet with the specified name, or null if it is not found
   */
  public WritableSheet getSheet(String name)
  {
    // Iterate through the boundsheet records
    boolean found = false;
    Iterator i = sheets.iterator();
    WritableSheet s = null;

    while (i.hasNext() && !found)
    {
      s = (WritableSheeti.next();
      
      if (s.getName().equals(name))
      {
        found = true;
      }
    }

    return found ? s : null;
  }

  /**
   * Returns the number of sheets in this workbook
   
   @return the number of sheets in this workbook
   */
  public int getNumberOfSheets()
  {
    return sheets.size();
  }

  /**
   * Closes this workbook, and frees makes any memory allocated available
   * for garbage collection
   
   @exception IOException 
   @exception JxlWriteException
   */
  public void close() throws IOException, JxlWriteException
  {
    outputFile.close(closeStream);
  }

  /**
   * Sets a new output file.  This allows the smae workbook to be
   * written to various different output files without having to
   * read in any templates again
   *
   @param fileName the file name
   @exception IOException
   */
  public void setOutputFile(java.io.File fileNamethrows IOException
  {
    FileOutputStream fos = new FileOutputStream(fileName);
    outputFile.setOutputFile(fos);
  }


  /**
   * The internal method implementation for creating new sheets
   *
   @param name
   @param index
   @param handleRefs flag indicating whether or not to handle external
   *                   sheet references
   @return 
   */
  private WritableSheet createSheet(String name, int index, 
                                    boolean handleRefs)
  {
    WritableSheet w = new WritableSheetImpl(name, 
                                            outputFile, 
                                            formatRecords, 
                                            sharedStrings,
                                            settings,
                                            this);

    int pos = index;

    if (index <= 0)
    {
      pos = 0;
      sheets.add(0, w);
    }
    else if (index > sheets.size())
    {
      pos = sheets.size();
      sheets.add(w);
    }
    else
    {
      sheets.add(index, w);
    }

    if (handleRefs && externSheet != null)
    {
      externSheet.sheetInserted(pos);
    }

    if (supbooks != null && supbooks.size() 0)
    {
      SupbookRecord supbook = (SupbookRecordsupbooks.get(0);
      if (supbook.getType() == SupbookRecord.INTERNAL)
      {
        supbook.adjustInternal(sheets.size());
      }
    }

    return w;
  }

  /**
   * Creates a new sheet within the workbook, at the specified position.
   * The new sheet is inserted at the specified position, or prepended/appended
   * to the list of sheets if the index specified is somehow inappropriate
   
   @param name the name of the new sheet
   @param index the index at which to add the sheet
   @return the created sheet
   */
  public WritableSheet createSheet(String name, int index)
  {
    return createSheet(name, index, true);
  }

  /**
   * Removes a sheet from this workbook, the other sheets indices being
   * altered accordingly. If the sheet referenced by the index
   * does not exist, then no action is taken.
   
   @param index the index of the sheet to remove
   */
  public void removeSheet(int index)
  {
    int pos = index;
    if (index <= 0)
    {
      pos = 0;
      sheets.remove(0);
    }
    else if (index >= sheets.size())
    {
      pos = sheets.size() 1;
      sheets.remove(sheets.size() 1);
    }
    else
    {
      sheets.remove(index);
    }

    if (externSheet != null)
    {
      externSheet.sheetRemoved(pos);
    }

    if (supbooks != null && supbooks.size() 0)
    {
      SupbookRecord supbook = (SupbookRecordsupbooks.get(0);
      if (supbook.getType() == SupbookRecord.INTERNAL)
      {
        supbook.adjustInternal(sheets.size());
      }
    }

    if (names != null && names.size() 0)
    {
      for (int i=0; i< names.size();i++)
      {
        NameRecord n = (NameRecordnames.get(i);
        int oldRef = n.getSheetRef();
        if(oldRef == (pos+1))
        {
          n.setSheetRef(0)// make a global name reference
        
        else if (oldRef > (pos+1))
        {
          if(oldRef < 1)
          {
            oldRef = 1;
          }
          n.setSheetRef(oldRef-1)// move one sheet
        }
      }
    }
  }

  /**
   * Moves the specified sheet within this workbook to another index
   * position.
   
   @param fromIndex the zero based index of the reQuired sheet
   @param toIndex the zero based index of the reQuired sheet
   @return the sheet that has been moved
   */
  public WritableSheet moveSheet(int fromIndex, int toIndex)
  {
    // Handle dodgy index 
    fromIndex = Math.max(fromIndex, 0);
    fromIndex = Math.min(fromIndex, sheets.size() 1);
    toIndex   = Math.max(toIndex, 0);
    toIndex   = Math.min(toIndex, sheets.size() 1);

    WritableSheet sheet= (WritableSheet)sheets.remove(fromIndex);
    sheets.add(toIndex, sheet);
    
    return sheet;
  }

  /**
   * Writes out this sheet to the output file.  First it writes out
   * the standard workbook information required by excel, before calling
   * the write method on each sheet individually
   
   @exception IOException 
   */
  public void write() throws IOException
  {
    // Perform some preliminary sheet check before we start writing out
    // the workbook
    WritableSheetImpl wsi = null;
    for (int i = 0; i < getNumberOfSheets(); i++)
    {
      wsi = (WritableSheetImplgetSheet(i);

      // Check the merged records.  This has to be done before the
      // globals are written out because some more XF formats might be created
      wsi.checkMergedBorders();

      // Check to see if there are any predefined names
      Range range = wsi.getSettings().getPrintArea();
      if (range != null)
      {
        addNameArea(BuiltInName.PRINT_AREA,
                    wsi,
                    range.getTopLeft().getColumn(),
                    range.getTopLeft().getRow(),
                    range.getBottomRight().getColumn(),
                    range.getBottomRight().getRow(),
                    false);
      }
      
      // Check to see if print titles by row were set
      Range rangeR = wsi.getSettings().getPrintTitlesRow();
      Range rangeC = wsi.getSettings().getPrintTitlesCol();      
      if (rangeR != null && rangeC != null)
      {
        addNameArea(BuiltInName.PRINT_TITLES, 
                    wsi,
                    rangeR.getTopLeft().getColumn(),
                    rangeR.getTopLeft().getRow(),
                    rangeR.getBottomRight().getColumn(),
                    rangeR.getBottomRight().getRow(),
                    rangeC.getTopLeft().getColumn(),
                    rangeC.getTopLeft().getRow(),
                    rangeC.getBottomRight().getColumn(),
                    rangeC.getBottomRight().getRow(),
                    false);  
      }
      // Check to see if print titles by row were set
      else if (rangeR != null)
      {
        addNameArea(BuiltInName.PRINT_TITLES, 
                    wsi,
                    rangeR.getTopLeft().getColumn(),
                    rangeR.getTopLeft().getRow(),
                    rangeR.getBottomRight().getColumn(),
                    rangeR.getBottomRight().getRow(),
                    false);
      }
      // Check to see if print titles by column were set 
      else if (rangeC != null)
      {
        addNameArea(BuiltInName.PRINT_TITLES, 
                    wsi,
                    rangeC.getTopLeft().getColumn(),
                    rangeC.getTopLeft().getRow(),
                    rangeC.getBottomRight().getColumn(),
                    rangeC.getBottomRight().getRow()
                    false);
      }
    }
    
    // Rationalize all the XF and number formats
    if (!settings.getRationalizationDisabled())
    {
      rationalize();
    }

    // Write the workbook globals
    BOFRecord bof = new BOFRecord(BOFRecord.workbookGlobals);
    outputFile.write(bof);

    // Must immediatly follow the BOF record
    if (settings.getTemplate())
    {
      // Only write record if we are a template
      TemplateRecord trec= new TemplateRecord();
      outputFile.write(trec);
    }


    InterfaceHeaderRecord ihr = new InterfaceHeaderRecord();
    outputFile.write(ihr);

    MMSRecord mms = new MMSRecord(0,0);
    outputFile.write(mms);

    InterfaceEndRecord ier = new InterfaceEndRecord();
    outputFile.write(ier);

    WriteAccessRecord wr = new WriteAccessRecord(settings.getWriteAccess());
    outputFile.write(wr);

    CodepageRecord cp = new CodepageRecord();
    outputFile.write(cp);

    DSFRecord dsf = new DSFRecord();
    outputFile.write(dsf);

    if (settings.getExcel9File())
    {
      // Only write record if we are a template
      // We are not excel 2000, should we still set the flag
      Excel9FileRecord e9rec= new Excel9FileRecord();
      outputFile.write(e9rec);
    }

    TabIdRecord tabid = new TabIdRecord(getNumberOfSheets());
    outputFile.write(tabid);

    if (containsMacros)
    {
      ObjProjRecord objproj = new ObjProjRecord();
      outputFile.write(objproj);
    }

    if (buttonPropertySet != null)
    {
      outputFile.write(buttonPropertySet);
    }

    FunctionGroupCountRecord fgcr = new FunctionGroupCountRecord();
    outputFile.write(fgcr);

    // do not support password protected workbooks
    WindowProtectRecord wpr = new WindowProtectRecord
      (settings.getWindowProtected());
    outputFile.write(wpr);

    ProtectRecord pr = new ProtectRecord(wbProtected);
    outputFile.write(pr);

    PasswordRecord pw = new PasswordRecord(null);
    outputFile.write(pw);

    Prot4RevRecord p4r = new Prot4RevRecord(false);
    outputFile.write(p4r);

    Prot4RevPassRecord p4rp = new Prot4RevPassRecord();
    outputFile.write(p4rp);

    // If no sheet is identified as being selected, then select
    // the first one
    boolean sheetSelected = false;
    WritableSheetImpl wsheet = null;
    int selectedSheetIndex = 0;
    for (int i = ; i < getNumberOfSheets() && !sheetSelected ; i++)
    {
      wsheet = (WritableSheetImplgetSheet(i);
      if (wsheet.getSettings().isSelected())
      {
        sheetSelected = true;
        selectedSheetIndex = i;
      }
    }

    if (!sheetSelected)
    {
      wsheet = (WritableSheetImplgetSheet(0);
      wsheet.getSettings().setSelected(true);
      selectedSheetIndex = 0;
    }

    Window1Record w1r = new Window1Record(selectedSheetIndex);
    outputFile.write(w1r);

    BackupRecord bkr = new BackupRecord(false);
    outputFile.write(bkr);

    HideobjRecord ho = new HideobjRecord(settings.getHideobj());
    outputFile.write(ho);
    
    NineteenFourRecord nf = new NineteenFourRecord(false);
    outputFile.write(nf);

    PrecisionRecord pc = new PrecisionRecord(false);
    outputFile.write(pc);

    RefreshAllRecord rar = new RefreshAllRecord(settings.getRefreshAll());
    outputFile.write(rar);

    BookboolRecord bb = new BookboolRecord(true);
    outputFile.write(bb);

    // Write out all the fonts used
    fonts.write(outputFile);

    // Write out the cell formats used within this workbook
    formatRecords.write(outputFile);

    // Write out the palette, if it exists
    if (formatRecords.getPalette() != null)
    {
      outputFile.write(formatRecords.getPalette());
    }

    // Write out the uses elfs record
    UsesElfsRecord uer = new UsesElfsRecord();
    outputFile.write(uer);
    
    // Write out the boundsheet records.  Keep a handle to each one's
    // position so we can write in the stream offset later
    int[] boundsheetPos = new int[getNumberOfSheets()];
    Sheet sheet = null;

    for (int i = 0; i < getNumberOfSheets(); i++)
    {
      boundsheetPos[i= outputFile.getPos();
      sheet = getSheet(i);
      BoundsheetRecord br = new BoundsheetRecord(sheet.getName());
      if (sheet.getSettings().isHidden())
      {
        br.setHidden();
      }

      if ( ( (WritableSheetImplsheets.get(i)).isChartOnly())
      {
        br.setChartOnly();
      }

      outputFile.write(br);
    }

    if (countryRecord == null)
    {
      CountryCode lang = 
        CountryCode.getCountryCode(settings.getExcelDisplayLanguage());
      if (lang == CountryCode.UNKNOWN)
      {
        logger.warn("Unknown country code " 
                    settings.getExcelDisplayLanguage() 
                    " using " + CountryCode.USA.getCode());
        lang = CountryCode.USA;
      }
      CountryCode region = 
        CountryCode.getCountryCode(settings.getExcelRegionalSettings());
      countryRecord = new CountryRecord(lang, region);
      if (region == CountryCode.UNKNOWN)
      {
        logger.warn("Unknown country code " 
                    settings.getExcelDisplayLanguage() 
                    " using " + CountryCode.UK.getCode());
        region = CountryCode.UK;
      }
    }

    outputFile.write(countryRecord);
    
    // Write out the names of any add in functions
    if (addInFunctionNames != null && addInFunctionNames.length > 0)
    {
      // Write out the supbook record
      //      SupbookRecord supbook = new SupbookRecord();
      //      outputFile.write(supbook);

      for (int i = ; i < addInFunctionNames.length; i++)
      {
        ExternalNameRecord enr = new ExternalNameRecord(addInFunctionNames[i]);
        outputFile.write(enr);
      }
    }

    if (xctRecords != null)
    {
      for (int i = ; i < xctRecords.length; i++)
      {
        outputFile.write(xctRecords[i]);
      }
    }

    // Write out the external sheet record, if it exists
    if (externSheet != null)
    {
      //Write out all the supbook records
      for (int i = 0; i < supbooks.size() ; i++)
      {
        SupbookRecord supbook = (SupbookRecordsupbooks.get(i);
        outputFile.write(supbook);
      }
      outputFile.write(externSheet);
    }

    // Write out the names, if any exists
    if (names != null)
    {
      for (int i = ; i < names.size() ; i++)
      {
        NameRecord n = (NameRecordnames.get(i);
        outputFile.write(n);
      }
    }
  
    // Write out the mso drawing group, if it exists
    if (drawingGroup != null)
    {
      drawingGroup.write(outputFile);
    }

    sharedStrings.write(outputFile);

    EOFRecord eof = new EOFRecord();
    outputFile.write(eof);


    // Write out the sheets
    for (int i = 0; i < getNumberOfSheets(); i++)
    {
      // first go back and modify the offset we wrote out for the
      // boundsheet record
      outputFile.setData
        (IntegerHelper.getFourBytes(outputFile.getPos()),
         boundsheetPos[i4);

      wsheet = (WritableSheetImplgetSheet(i);
      wsheet.write();
    }
  }

  /**
   * Produces a writable copy of the workbook passed in by 
   * creating copies of each sheet in the specified workbook and adding 
   * them to its own record
   
   @param w the workbook to copy
   */
  private void copyWorkbook(Workbook w)
  {
    int numSheets = w.getNumberOfSheets();
    wbProtected = w.isProtected();
    Sheet s = null;
    WritableSheetImpl ws = null;
    for (int i = ; i < numSheets; i++)
    {
      s = w.getSheet(i);
      ws = (WritableSheetImplcreateSheet(s.getName(),i, false);
      ws.copy(s);
    }
  }

  /**
   * Copies the specified sheet and places it at the index
   * specified by the parameter
   *
   @param s the index of the sheet to copy
   @param name the name of the new sheet
   @param index the position of the new sheet
   */
  public void copySheet(int s, String name, int index)
  {
    WritableSheet sheet = getSheet(s);
    WritableSheetImpl ws = (WritableSheetImplcreateSheet(name, index);
    ws.copy(sheet);
  }

  /**
   * Copies the specified sheet and places it at the index
   * specified by the parameter
   *
   @param s the name of the sheet to copy
   @param name the name of the new sheet
   @param index the position of the new sheet
   */
  public void copySheet(String s, String name, int index)
  {
    WritableSheet sheet = getSheet(s);
    WritableSheetImpl ws = (WritableSheetImplcreateSheet(name, index);
    ws.copy(sheet);
  }

  /**
   * Indicates whether or not this workbook is protected
   
   @param prot protected flag
   */
  public void setProtected(boolean prot)
  {
    wbProtected = prot;
  }

  /**
   * Rationalizes the cell formats, and then passes the resultant XF index
   * mappings to each sheet in turn
   */
  private void rationalize()
  {
    IndexMapping fontMapping   = formatRecords.rationalizeFonts();
    IndexMapping formatMapping = formatRecords.rationalizeDisplayFormats();
    IndexMapping xfMapping     = formatRecords.rationalize(fontMapping, 
                                                           formatMapping);

    WritableSheetImpl wsi = null;
    for (int i = 0; i < sheets.size(); i++)
    {
      wsi = (WritableSheetImplsheets.get(i);
      wsi.rationalize(xfMapping, fontMapping, formatMapping);
    }
  }

  /**
   * Gets the internal sheet index for a sheet name
   *
   @param name the sheet name
   @return the internal sheet index
   */
  private int getInternalSheetIndex(String name)
  {
    int index = -1;
    String[] names = getSheetNames();
    for (int i = ; i < names.length; i++)
    {
      if (name.equals(names[i]))
      {
        index = i;
        break;
      }
    }

    return index;
  }

  /**
   * Gets the name of the external sheet specified by the index
   *
   @param index the external sheet index
   @return the name of the external sheet
   */
  public String getExternalSheetName(int index)
  {
    int supbookIndex = externSheet.getSupbookIndex(index);
    SupbookRecord sr = (SupbookRecordsupbooks.get(supbookIndex);
    
    int firstTab = externSheet.getFirstTabIndex(index);
    
    if (sr.getType() == SupbookRecord.INTERNAL)
    {
      // It's an internal reference - get the name from the sheets list
      WritableSheet ws = getSheet(firstTab);

      return ws.getName();
    }
    else if (sr.getType() == SupbookRecord.EXTERNAL)
    {
      String name = sr.getFileName() + sr.getSheetName(firstTab);
      return name;
    }
    
    // An unknown supbook - return unkown
    logger.warn("Unknown Supbook 1");
    return "[UNKNOWN]";
  }

  /**
   * Gets the name of the last external sheet specified by the index
   *
   @param index the external sheet index
   @return the name of the external sheet
   */
  public String getLastExternalSheetName(int index)
  {
    int supbookIndex = externSheet.getSupbookIndex(index);
    SupbookRecord sr = (SupbookRecordsupbooks.get(supbookIndex);
    
    int lastTab = externSheet.getLastTabIndex(index);
    
    if (sr.getType() == SupbookRecord.INTERNAL)
    {
      // It's an internal reference - get the name from the sheets list
      WritableSheet ws = getSheet(lastTab);

      return ws.getName();
    }
    else if (sr.getType() == SupbookRecord.EXTERNAL)
    {
      Assert.verify(false);
    }
    
    // An unknown supbook - return unkown
    logger.warn("Unknown Supbook 2");
    return "[UNKNOWN]";
  }

  /**
   * Parsing of formulas is only supported for a subset of the available
   * biff version, so we need to test to see if this version is acceptable
   *
   @return the BOF record, which 
   */
  public jxl.read.biff.BOFRecord getWorkbookBof()
  {
    return null;
  }


  /**
   * Gets the index of the external sheet for the name
   *
   @param sheetName
   @return the sheet index of the external sheet index
   */
  public int getExternalSheetIndex(int index)
  {
    if (externSheet == null)
    {
      return index;
    }

    Assert.verify(externSheet != null);

    int firstTab = externSheet.getFirstTabIndex(index);

    return firstTab;
  }

  /**
   * Gets the index of the external sheet for the name
   *
   @param sheetName
   @return the sheet index of the external sheet index
   */
  public int getLastExternalSheetIndex(int index)
  {
    if (externSheet == null)
    {
      return index;
    }

    Assert.verify(externSheet != null);

    int lastTab = externSheet.getLastTabIndex(index);

    return lastTab;
  }

  /**
   * Gets the external sheet index for the sheet name
   *
   @param sheetName 
   @return the sheet index or -1 if the sheet could not be found
   */
  public int getExternalSheetIndex(String sheetName)
  {
    if (externSheet == null)
    {
      externSheet = new ExternalSheetRecord();
      supbooks = new ArrayList();
      supbooks.add(new SupbookRecord(getNumberOfSheets(), settings));
    }

    // Iterate through the sheets records
    boolean found = false;
    Iterator i = sheets.iterator();
    int sheetpos = 0;
    WritableSheetImpl s = null;

    while (i.hasNext() && !found)
    {
      s = (WritableSheetImpli.next();
      
      if (s.getName().equals(sheetName))
      {
        found = true;
      }
      else
      {
        sheetpos++;
      }
    }

    if (found)
    {
      // Check that the supbook record at position zero is internal and
      // contains all the sheets
      SupbookRecord supbook = (SupbookRecordsupbooks.get(0);
      if (supbook.getType() != SupbookRecord.INTERNAL ||
          supbook.getNumberOfSheets() != getNumberOfSheets())
      {
        logger.warn("Cannot find sheet " + sheetName + " in supbook record");
      }
      
      return externSheet.getIndex(0, sheetpos);
    }

    // Check for square brackets
    int closeSquareBracketsIndex = sheetName.lastIndexOf(']');
    int openSquareBracketsIndex = sheetName.lastIndexOf('[');
    
    if (closeSquareBracketsIndex == -||
        openSquareBracketsIndex == -1)
    {
      logger.warn("Square brackets");
      return -1;
    }

    String worksheetName = sheetName.substring(closeSquareBracketsIndex+1);
    String workbookName = sheetName.substring(openSquareBracketsIndex+1
                                              closeSquareBracketsIndex);
    String path = sheetName.substring(0, openSquareBracketsIndex);
    String fileName = path + workbookName;

    boolean supbookFound = false;
    SupbookRecord externalSupbook = null;
    int supbookIndex = -1;
    for (int ind = 0; ind < supbooks.size() && !supbookFound ; ind++)
    {
      externalSupbook = (SupbookRecordsupbooks.get(ind);
      if (externalSupbook.getType() == SupbookRecord.EXTERNAL &&
          externalSupbook.getFileName().equals(fileName))
      {
        supbookFound = true;
        supbookIndex = ind;
      }
    }

    if (!supbookFound)
    {
      externalSupbook = new SupbookRecord(fileName, settings);
      supbookIndex = supbooks.size();
      supbooks.add(externalSupbook);
    }
    
    int sheetIndex = externalSupbook.getSheetIndex(worksheetName);

    return externSheet.getIndex(supbookIndex, sheetIndex);
  }

  /**
   * Gets the last external sheet index for the sheet name
   @param sheetName 
   @return the sheet index or -1 if the sheet could not be found
   */
  public int getLastExternalSheetIndex(String sheetName)
  {
    if (externSheet == null)
    {
      externSheet = new ExternalSheetRecord();
      supbooks = new ArrayList();
      supbooks.add(new SupbookRecord(getNumberOfSheets(), settings));
    }

    // Iterate through the sheets records
    boolean found = false;
    Iterator i = sheets.iterator();
    int sheetpos = 0;
    WritableSheetImpl s = null;

    while (i.hasNext() && !found)
    {
      s = (WritableSheetImpli.next();
      
      if (s.getName().equals(sheetName))
      {
        found = true;
      }
      else
      {
        sheetpos++;
      }
    }

    if (!found)
    {
      return -1;
    }

    // Check that the supbook record at position zero is internal and contains
    // all the sheets
    SupbookRecord supbook = (SupbookRecordsupbooks.get(0);
    Assert.verify(supbook.getType() == SupbookRecord.INTERNAL &&
                  supbook.getNumberOfSheets() == getNumberOfSheets());

    return externSheet.getIndex(0, sheetpos);
  }

  /**
   * Sets the RGB value for the specified colour for this workbook
   *
   @param c the colour whose RGB value is to be overwritten
   @param r the red portion to set (0-255)
   @param g the green portion to set (0-255)
   @param b the blue portion to set (0-255)
   */
  public void setColourRGB(Colour c, int r, int g, int b)
  {
    formatRecords.setColourRGB(c,r,g,b);
  }

  /**
   * Accessor for the RGB value for the specified colour
   *
   @return the RGB for the specified colour
   */
  public RGB getColourRGB(Colour c)
  {
    return formatRecords.getColourRGB(c);
  }

  /**
   * Gets the name at the specified index
   *
   @param index the index into the name table
   @return the name of the cell
   */
  public String getName(int index)
  {
    Assert.verify(index >= && index < names.size());
    NameRecord n = (NameRecordnames.get(index);
    return n.getName();
  }

  /**
   * Gets the index of the name record for the name
   *
   @param name 
   @return the index in the name table
   */
  public int getNameIndex(String name)
  {
    NameRecord nr = (NameRecordnameRecords.get(name);
    return nr != null ? nr.getIndex() : -1;
  }

  /**
   * Adds a cell to workbook wide range of cells which need adjustment
   * following a row/column insert or remove
   *
   @param f the cell to add to the list
   */
  void addRCIRCell(CellValue cv)
  {
    rcirCells.add(cv);
  }

  /**
   * Called when a column is inserted on the specified sheet.  Notifies all
   * RCIR cells of this change
   *
   @param s the sheet on which the column was inserted
   @param col the column number which was inserted
   */
  void columnInserted(WritableSheetImpl s, int col)
  {
    int externalSheetIndex = getExternalSheetIndex(s.getName());
    for (Iterator i = rcirCells.iterator() ; i.hasNext() ;)
    {
      CellValue cv = (CellValuei.next();
      cv.columnInserted(s, externalSheetIndex, col);
    }

    // Adjust any named cells
    if (names != null)
    {
      for (Iterator i = names.iterator(); i.hasNext() ;)
      {
        NameRecord nameRecord = (NameRecordi.next();
        nameRecord.columnInserted(externalSheetIndex, col);
      }
    }
  }

  /**
   * Called when a column is removed on the specified sheet.  Notifies all
   * RCIR cells of this change
   *
   @param s the sheet on which the column was removed
   @param col the column number which was removed
   */
  void columnRemoved(WritableSheetImpl s, int col)
  {
    int externalSheetIndex = getExternalSheetIndex(s.getName());
    for (Iterator i = rcirCells.iterator() ; i.hasNext() ;)
    {
      CellValue cv = (CellValuei.next();
      cv.columnRemoved(s, externalSheetIndex, col);
    }

    // Adjust any named cells
    ArrayList removedNames = new ArrayList();
    if (names != null)
    {
      for (Iterator i = names.iterator(); i.hasNext() ;)
      {
        NameRecord nameRecord = (NameRecordi.next();
        boolean removeName = nameRecord.columnRemoved(externalSheetIndex,
                                                      col);

        if (removeName)
        {
          removedNames.add(nameRecord);
        }
      }

      // Remove any names which have been deleted
      for (Iterator i = removedNames.iterator(); i.hasNext() ;)
      {
        NameRecord nameRecord = (NameRecordi.next();
        boolean removed = names.remove(nameRecord);
        Assert.verify(removed, "Could not remove name " 
                      nameRecord.getName());
      }
    }
  }

  /**
   * Called when a row is inserted on the specified sheet.  Notifies all
   * RCIR cells of this change
   *
   @param s the sheet on which the row was inserted
   @param row the row number which was inserted
   */
  void rowInserted(WritableSheetImpl s, int row)
  {
    int externalSheetIndex = getExternalSheetIndex(s.getName());
    
    // Adjust the row infos
    for (Iterator i = rcirCells.iterator() ; i.hasNext() ;)
    {
      CellValue cv = (CellValuei.next();
      cv.rowInserted(s, externalSheetIndex, row);
    }

    // Adjust any named cells
    if (names != null)
    {
      for (Iterator i = names.iterator(); i.hasNext() ;)
      {
        NameRecord nameRecord = (NameRecordi.next();
        nameRecord.rowInserted(externalSheetIndex, row);
      }
    }
  }

  /**
   * Called when a row is removed on the specified sheet.  Notifies all
   * RCIR cells of this change
   *
   @param s the sheet on which the row was removed
   @param row the row number which was removed
   */
  void rowRemoved(WritableSheetImpl s, int row)
  {
    int externalSheetIndex = getExternalSheetIndex(s.getName());
    for (Iterator i = rcirCells.iterator() ; i.hasNext() ;)
    {
      CellValue cv = (CellValuei.next();
      cv.rowRemoved(s, externalSheetIndex, row);
    }

    // Adjust any named cells
    ArrayList removedNames = new ArrayList();
    if (names != null)
    {
      for (Iterator i = names.iterator(); i.hasNext() ;)
      {
        NameRecord nameRecord = (NameRecordi.next();
        boolean removeName = nameRecord.rowRemoved(externalSheetIndex, row);

        if (removeName)
        {
          removedNames.add(nameRecord);
        }
      }

      // Remove any names which have been deleted
      for (Iterator i = removedNames.iterator(); i.hasNext() ;)
      {
        NameRecord nameRecord = (NameRecordi.next();
        boolean removed = names.remove(nameRecord);
        Assert.verify(removed, "Could not remove name " 
                      nameRecord.getName());
      }
    }
  }

  /**
   * Gets the named cell from this workbook.  If the name refers to a
   * range of cells, then the cell on the top left is returned.  If
   * the name cannot be found, null is returned
   *
   @param  the name of the cell/range to search for
   @return the cell in the top left of the range if found, NULL
   *         otherwise
   */
  public WritableCell findCellByName(String name)
  {
    NameRecord nr = (NameRecordnameRecords.get(name);

    if (nr == null)
    {
      return null;
    }

    NameRecord.NameRange[] ranges = nr.getRanges();

    // Go and retrieve the first cell in the first range
    int sheetIndex = getExternalSheetIndex(ranges[0].getExternalSheet());
    WritableSheet s    = getSheet(sheetIndex);
    WritableCell  cell = s.getWritableCell(ranges[0].getFirstColumn()
                                           ranges[0].getFirstRow());

    return cell;
  }

  /**
   * Gets the named range from this workbook.  The Range object returns
   * contains all the cells from the top left to the bottom right
   * of the range.  
   * If the named range comprises an adjacent range,
   * the Range[] will contain one object; for non-adjacent
   * ranges, it is necessary to return an array of length greater than
   * one.  
   * If the named range contains a single cell, the top left and
   * bottom right cell will be the same cell
   *
   @param  the name of the cell/range to search for
   @return the range of cells
   */
  public Range[] findByName(String name)
  {
    NameRecord nr = (NameRecordnameRecords.get(name);

    if (nr == null)
    {
      return null;
    }

    NameRecord.NameRange[] ranges = nr.getRanges();

    Range[] cellRanges = new Range[ranges.length];

    for (int i = 0; i < ranges.length ; i++)
    {
      cellRanges[inew RangeImpl
        (this, 
         getExternalSheetIndex(ranges[i].getExternalSheet()),
         ranges[i].getFirstColumn(),
         ranges[i].getFirstRow(),
         getLastExternalSheetIndex(ranges[i].getExternalSheet()),
         ranges[i].getLastColumn(),
         ranges[i].getLastRow());
    }

    return cellRanges;
  }

  /**
   * Adds a drawing to this workbook
   *
   @param d the drawing to add
   */
  void addDrawing(DrawingGroupObject d)
  {
    if (drawingGroup == null)
    {
      drawingGroup = new DrawingGroup(Origin.WRITE);
    }

    drawingGroup.add(d);
  }

  /**
   * Removes a drawing from this workbook
   *
   @param d the drawing to remove
   */
  void removeDrawing(Drawing d)
  {
    Assert.verify(drawingGroup != null);

    drawingGroup.remove(d);
  }

  /**
   * Accessor for the drawing group
   *
   @return the drawing group
   */
  DrawingGroup getDrawingGroup()
  {
    return drawingGroup;
  }

  /**
   * Create a drawing group for this workbook - used when importing sheets
   * which contain drawings, but this workbook doesn't.
   * We can't subsume this into the getDrawingGroup() method because the
   * null-ness of the return value is used elsewhere to determine the
   * origin of the workbook
   */
  DrawingGroup createDrawingGroup()
  {
    if (drawingGroup == null)
    {
      drawingGroup = new DrawingGroup(Origin.WRITE);
    }

    return drawingGroup;
  }

  /**
   * Gets the named ranges
   *
   @return the list of named cells within the workbook
   */
  public String[] getRangeNames()
  {
    if (names == null)
    {
      return new String[0];
    }

    String[] n = new String[names.size()];
    for (int i = ; i < names.size() ; i++)
    {
      NameRecord nr = (NameRecordnames.get(i);
      n[i= nr.getName();
    }

    return n;
  }

  /**
   * Removes the specified named range from the workbook
   *
   @param name the name to remove
   */
  public void removeRangeName(String name)
  {
    int pos = 0;
    boolean found = false;
    for (Iterator i = names.iterator(); i.hasNext() && !found ;)
    {
      NameRecord nr = (NameRecordi.next();
      if (nr.getName().equals(name))
      {
        found = true;
      }
      else
      {
        pos++;
      }
    }

    // Remove the name from the list of names and the associated hashmap
    // of names (used to retrieve the name index).  If the name cannot
    // be found, a warning is displayed
    if (found)
    {
      names.remove(pos);
      if (nameRecords.remove(name== null)
      {
        logger.warn("Could not remove " + name + " from index lookups");
      }
    }
  }

  /**
   * Accessor for the jxl.common.styles
   *
   @return the standard styles for this workbook
   */
  Styles getStyles()
  {
    return styles;
  }

  /**
   * Add new named area to this workbook with the given information.
   
   @param name name to be created.
   @param sheet sheet containing the name
   @param firstCol  first column this name refers to.
   @param firstRow  first row this name refers to.
   @param lastCol    last column this name refers to.
   @param lastRow    last row this name refers to.
   */
  public void addNameArea(String name, 
                          WritableSheet sheet,
                          int firstCol, 
                          int firstRow,  
                          int lastCol,
                          int lastRow)
  {
    addNameArea(name, sheet, firstCol, firstRow, lastCol, lastRow, true);
  }

  /**
   * Add new named area to this workbook with the given information.
   
   @param name name to be created.
   @param sheet sheet containing the name
   @param firstCol  first column this name refers to.
   @param firstRow  first row this name refers to.
   @param lastCol    last column this name refers to.
   @param lastRow    last row this name refers to.
   @param global   TRUE if this is a global name, FALSE if this is tied to 
   *                 the sheet
   */
  void addNameArea(String name, 
                   WritableSheet sheet,
                   int firstCol, 
                   int firstRow,  
                   int lastCol,
                   int lastRow,
                   boolean global)
  {
    if (names == null)
    {
      names = new ArrayList();
    }

    int externalSheetIndex = getExternalSheetIndex(sheet.getName());

    // Create a new name record.
    NameRecord nr = 
      new NameRecord(name, 
                     names.size(),
                     externalSheetIndex, 
                     firstRow, lastRow, 
                     firstCol, lastCol, 
                     global);
    
    // Add new name to name array.
    names.add(nr);
    
    // Add new name to name hash table.
    nameRecords.put(name, nr);
  }

  /**
   * Add new named area to this workbook with the given information.
   
   @param name name to be created.
   @param sheet sheet containing the name
   @param firstCol  first column this name refers to.
   @param firstRow  first row this name refers to.
   @param lastCol    last column this name refers to.
   @param lastRow    last row this name refers to.
   @param global   TRUE if this is a global name, FALSE if this is tied to 
   *                 the sheet
   */
  void addNameArea(BuiltInName name, 
                   WritableSheet sheet,
                   int firstCol, 
                   int firstRow,  
                   int lastCol,
                   int lastRow,
                   boolean global)
  {
    if (names == null)
    {
      names = new ArrayList();
    }

    int index = getInternalSheetIndex(sheet.getName());
    int externalSheetIndex = getExternalSheetIndex(sheet.getName());

    // Create a new name record.
    NameRecord nr = 
      new NameRecord(name, 
                     index,
                     externalSheetIndex, 
                     firstRow, lastRow, 
                     firstCol, lastCol, 
                     global);
    
    // Add new name to name array.
    names.add(nr);
    
    // Add new name to name hash table.
    nameRecords.put(name, nr);
  }

  /**
   * Add new named area to this workbook with the given information.
   
   @param name name to be created.
   @param sheet sheet containing the name
   @param firstCol  first column this name refers to.
   @param firstRow  first row this name refers to.
   @param lastCol   last column this name refers to.
   @param lastRow   last row this name refers to.
   @param firstCol2 first column this name refers to.
   @param firstRow2 first row this name refers to.
   @param lastCol2  last column this name refers to.
   @param lastRow2  last row this name refers to.
   @param global   TRUE if this is a global name, FALSE if this is tied to 
   *                 the sheet
   */
  void addNameArea(BuiltInName name, 
                   WritableSheet sheet,
                   int firstCol, 
                   int firstRow,  
                   int lastCol,
                   int lastRow,
                   int firstCol2, 
                   int firstRow2,  
                   int lastCol2,
                   int lastRow2,
                   boolean global)
  {
    if (names == null)
    {
      names = new ArrayList();
    }

    int index = getInternalSheetIndex(sheet.getName());
    int externalSheetIndex = getExternalSheetIndex(sheet.getName());

    // Create a new name record.
    NameRecord nr = 
      new NameRecord(name, 
                     index,
                     externalSheetIndex, 
                     firstRow2, lastRow2, 
                     firstCol2, lastCol2,
                     firstRow, lastRow, 
                     firstCol, lastCol, 
                     global);
    
    // Add new name to name array.
    names.add(nr);
    
    // Add new name to name hash table.
    nameRecords.put(name, nr);
  }  
  
  /**
   * Accessor for the workbook settings
   */
  WorkbookSettings getSettings()
  {
    return settings;
  }

  /**
   * Returns the cell for the specified location eg. "Sheet1!A4".  
   * This is identical to using the CellReferenceHelper with its
   * associated performance overheads, consequently it should
   * be use sparingly
   *
   @param loc the cell to retrieve
   @return the cell at the specified location
   */
  public WritableCell getWritableCell(String loc)
  {
    WritableSheet s = getSheet(CellReferenceHelper.getSheet(loc))
    return s.getWritableCell(loc);
  }

  /**
   * Imports a sheet from a different workbook.  Does a deep copy on all
   * elements within that sheet
   *
   @param name the name of the new sheet
   @param index the position for the new sheet within this workbook
   @param sheet the sheet (from another workbook) to merge into this one
   @return the new sheet
   */
  public WritableSheet importSheet(String name, int index, Sheet sheet)
  {
    WritableSheet ws = createSheet(name, index);
    ((WritableSheetImplws).importSheet(sheet);

    return ws;
  }

}