Package nexj.core.build

Source Code of nexj.core.build.WiXWriter

// Copyright 2010 NexJ Systems Inc. This software is licensed under the terms of the Eclipse Public License 1.0
package nexj.core.build;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.transform.stream.StreamSource;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.taskdefs.Expand;
import org.apache.tools.ant.types.DataType;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.ZipFileSet;
import org.apache.tools.ant.util.FileUtils;

import nexj.core.util.GUIDUtil;
import nexj.core.util.HashTab;
import nexj.core.util.IOUtil;
import nexj.core.util.Lookup;
import nexj.core.util.ObjUtil;
import nexj.core.util.StringUtil;
import nexj.core.util.SysUtil;
import nexj.core.util.XMLUtil;
import nexj.core.util.XMLWriter;

/**
* Writes a WiX fragment configured by Ant and compiles/links the MSI using WiX.
*/
public class WiXWriter extends Task
{
   // constants

   /**
    * Ant debug log level (change to Project.MSG_WARN if "ant -debug" is to
    * noisy).
    */
   private final static int DEBUG_LEVEL = Project.MSG_DEBUG;

   /**
    * Ant error log level.
    */
   private final static int ERROR_LEVEL = Project.MSG_ERR;

   /**
    * Name of automatically generated file, not including the extension (.wxs or
    * .wxsobj) and path.
    */
   private final static String TEMP_FILE_PREFIX = "nexj_wixwriter_";

   /**
    * ID of the directory where all directories are rooted.
    */
   private final static String ROOT_DIRECTORY_ID = "TARGETDIR";

   /**
    * Pattern for matching system folder reference in the install path.
    */
   private final static Pattern SYSTEM_FOLDER_REF_PATTERN = Pattern.compile("^\\[[A-Za-z0-9]+\\]");

   /**
    * Pattern for matching bad 8.3 filename characters.
    */
   private final static Pattern BAD_83_FILE_CHAR_PATTERN = Pattern.compile("[^0-9A-Z_]");

   /**
    * String encoder used for getting binary data to hash.
    */
   private final static CharsetEncoder UTF_ENCODER = Charset.forName("UTF-8").newEncoder();

   /**
    * Namspace for hashing paths.
    */
   private final static byte[] GUID_NAMESPACE = getEncodedData(SysUtil.NAMESPACE + ":installer:");

   /**
    * Namspace for hashing short names.
    */
   private final static byte[] SHORTNAME_NAMESPACE = getEncodedData(SysUtil.NAMESPACE + ":installer:shortname:");

   // member variables

   /**
    * Name of installer type.
    */
   private String m_sInstallName;

   /**
    * ID of a root system folder that the install root is under.
    */
   private String m_sSystemFolderRoot;

   /**
    * Prefix applied to every archivegroup.
    */
   private String m_sInstallDirectory;

   /**
    * Version identifier.
    */
   private String m_sVersion;
  
   /**
    * Path where WIX Candle and light are found.
    */
   private File m_wixDirectory;

   /**
    * Path where the WXS files are found.
    */
   private File m_sourceDirectory;

   /**
    * Path where MSIs are written to.
    */
   private File m_destinationDirectory;

   /**
    * Product name used by the MSI filename.
    */
   private String m_sProductName;

   /**
    * Product name used by the MSI filename.
    */
   private String m_sMSIFilenameProperty;

   /**
    * Product name used by the EXE filename, if applicable.
    */
   private String m_sEXEFilenameProperty;

   /**
    * List of disk id's and their associated file patterns (ArchiveGroup
    * objects).
    */
   private ArrayList m_archiveGroupList = new ArrayList();

   /**
    * List of Resources (WixBinary or WixIcon objects).
    */
   private ArrayList m_resourceList = new ArrayList();

   /**
    * List of Variables.
    */
   private ArrayList m_variableList = new ArrayList();

   /**
    * NSIS interop bootstrapper.
    */
   private InteropBootstrapper m_interopBootstrapper;

   /**
    * Debug mode.
    */
   private boolean m_bDebugModeEnabled;

   /**
    * Path to the initial output file.
    */
   private File m_fragmentFile;

   /**
    * Writer for the initial output file.
    */
   private XMLWriter m_writer;

   /**
    * Folder where temporary files are held.
    */
   private File m_tempDirectory;

   /**
    * Base name of the setup file.
    */
   private String m_sSetupFileBaseName;

   /**
    * Name of resulting MSI file.
    */
   private String m_sDestinationFile;

   /**
    * The Ant project.
    */
   private Project m_project;

   /**
    * Sorted list of disk IDs Strings.
    */
   private ArrayList m_componentGroupIdList = new ArrayList();

   /**
    * Map a String disk ID to an ArrayList of String component IDs.
    */
   private Lookup m_componentGroupMap = new HashTab();

   /**
    * Get the raw data of a string encoded in UTF-8.
    *
    * @param sInput The string.
    * @return The data.
    */
   private static byte[] getEncodedData(String sInput)
   {
      ByteBuffer buffer = null;

      // Encode the string
      try
      {
         buffer = UTF_ENCODER.encode(CharBuffer.wrap(sInput));
      }
      catch (CharacterCodingException e)
      {
         ObjUtil.rethrow(e);
      }

      // Allocate the array
      byte nDataArray[] = new byte[buffer.remaining()];

      // Copy the data
      buffer.get(nDataArray);

      return nDataArray;
   }

   /**
    * Calculate an archaic FAT 8.3 filename. If the filename won't fit regularly
    * into 8.3, take the first two letters and have the next 6 be a hash of the
    * name.
    *
    * @param sName The original name.
    * @return The 8.3 compatible name.
    */
   private static String getShortName(String sName)
   {
      // Convert to uppercase
      String sUpperCase = sName.toUpperCase();

      String sBase = sUpperCase;
      String sExt = "";
      int nExtIndex = sUpperCase.lastIndexOf('.');

      if (nExtIndex >= 0)
      {
         sExt = sBase.substring(nExtIndex + 1);
         sBase = sBase.substring(0, nExtIndex);
      }

      // If the name has no bad characters and the base name is at most eight characters and the extension is at most
      // three
      if (sBase.length() > 0 && sBase.length() <= 8 && sExt.length() <= 3 && !BAD_83_FILE_CHAR_PATTERN.matcher(sBase).find()
         && !BAD_83_FILE_CHAR_PATTERN.matcher(sExt).find())
      {
         return sUpperCase;
      }

      // First two characters are suffixed by a hash of the filename

      StringBuilder shortBuf = new StringBuilder(12);

      append83(shortBuf, sBase.length() > 2 ? sBase.substring(0, 2) : sBase);

      // Calculate the hash and convert it to a hexidecimal string
      byte[] nDataArray = getEncodedData(sName);
      String sHash = GUIDUtil.generateGUID(SHORTNAME_NAMESPACE, nDataArray).toString();

      // Truncate and append the hash: hopefully we have enough entropy
      shortBuf.append(sHash, 0, 6);

      if (sExt.length() > 0)
      {
         shortBuf.append('.');
         append83(shortBuf, sExt.length() > 3 ? sExt.substring(0, 3) : sExt);
      }

      return shortBuf.toString();
   }
  
   /**
    * Helper function for getShortName() which appends a string to StringBuilder
    * replacing bad 8.3 file names with an underscore.
    *
    * @param target StringBuilder to append to.
    * @param sSrc String source to append.
    */
   private static void append83(StringBuilder target, String sSrc)
   {
      for (int nIndex = 0; nIndex < sSrc.length(); nIndex++)
      {
         String sChar = sSrc.substring(nIndex, nIndex + 1);

         if (Pattern.matches(BAD_83_FILE_CHAR_PATTERN.pattern(), sChar))
         {
            target.append('_');
         }
         else
         {
            target.append(sChar);
         }
      }
   }


   // object construction (by Ant)

   /**
    * Basic constructor used by Ant (no exceptions should be thrown from here).
    */
   public WiXWriter()
   {
      m_tempDirectory = new File(System.getProperty("java.io.tmpdir"));
   }

   /**
    * Set the alias of the install package (office, cardscan...).
    *
    * @param sInstallName The install name.
    */
   public void setInstallName(String sInstallName)
   {
      m_sInstallName = sInstallName;
   }

   /**
    * Set the default install root path. If the path starts with
    * "[SystemFolderID]", root the install directory at that directory. Warning:
    * IDs are not validated. (See
    * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/msi/setup/system_folder_properties.asp
    * for available IDs)
    *
    *
    * @param sInstallDirectory A path omitting the leading "C:\"
    * @throws BuildException if the string has invalid formatting.
    */
   public void setInstallDir(String sInstallDirectory) throws BuildException
   {
      Matcher matcher = SYSTEM_FOLDER_REF_PATTERN.matcher(sInstallDirectory);

      // See if a system folder is specified
      if (matcher.find())
      {
         // The system folder comes from inside the brackets
         m_sSystemFolderRoot = sInstallDirectory.substring(1, matcher.end() - 1);

         // The rest of the path comes after
         m_sInstallDirectory = sInstallDirectory.substring(matcher.end());

         // Convert to the correct format for files
         m_sInstallDirectory = new File(m_sInstallDirectory).getPath();

         // Check if it starts with a file separator
         if (m_sInstallDirectory.startsWith(File.separator))
         {
            // Remove the leading separator
            m_sInstallDirectory = m_sInstallDirectory.substring(File.separator.length());
         }
      }
      // Otherwise, no system folder is needed
      else
      {
         m_sSystemFolderRoot = null;

         // Convert to the correct format for files
         m_sInstallDirectory = new File(sInstallDirectory).getPath();
      }
   }
  
   /**
    * Set the version number of the install
    *
    * @param sVersion The version provided.
    */
   public void setVersion(String sVersion)
   {
      m_sVersion = sVersion;
   }

   /**
    * Set the directory where WiX is installed.
    *
    * @param wixDir The WiX directory.
    */
   public void setWixDir(File wixDir)
   {
      m_wixDirectory = wixDir;
   }

   /**
    * Set the source directory where the WXS files are found.
    *
    * @param sourceDir The source directory.
    */
   public void setSrcDir(File sourceDir)
   {
      m_sourceDirectory = sourceDir;
   }

   /**
    * Set the destination directory MSIs are written to.
    *
    * @param destinationDir The destination directory.
    */
   public void setDestDir(File destinationDir)
   {
      m_destinationDirectory = destinationDir;
   }

   /**
    * Set the prodcut name used by the MSI filename.
    *
    * @param productName The product name.
    */
   public void setProduct(String productName)
   {
      m_sProductName = productName;
   }

   /**
    * Ant attribute setter for property to store the compiled filename.
    *
    * @param sMSIFilenameProperty The filename of the resulting MSI.
    */
   public void setMSIFileProperty(String sMSIFilenameProperty)
   {
      m_sMSIFilenameProperty = sMSIFilenameProperty;
   }

   /**
    * Ant attribute setter for property to store the parsed revision.
    *
    * @param sExeFilenameProperty The filename of the resulting EXE, if applicable.
    */
   public void setExeFileProperty(String sExeFilenameProperty)
   {
      m_sEXEFilenameProperty = sExeFilenameProperty;
   }

   /**
    * Add a nested ArchiveGroup.
    *
    * @param archiveGroup The ArchiveGroup being added.
    */
   public void addArchiveGroup(ArchiveGroup archiveGroup)
   {
      m_archiveGroupList.add(archiveGroup);
   }

   /**
    * Factory method for creating nested Resource object.
    *
    * @param resource The Resource being added.
    */
   public void addResource(Resource resource)
   {
      m_resourceList.add(resource);
   }

   /**
    * Factory method for creating nested Variable object.
    *
    * @return The created Variable.
    */
   public Variable createVariable()
   {
      Variable variable = new Variable();
      m_variableList.add(variable);
      return variable;
   }

   /**
    * Factory method for creating nested InteropBootstrapper object.
    *
    * @return The created InteropBootstrapper.
    */
   public InteropBootstrapper createInteropBootstrapper()
   {
      if (m_interopBootstrapper != null)
      {
         throw new BuildException("Duplicate 'interopbootstrapper' element detected");
      }

      m_interopBootstrapper = new InteropBootstrapper();
      return m_interopBootstrapper;
   }

   // public method

   /**
    * Entry point of the Ant task.
    *
    * @throws BuildException if invalid parameters are used or the build fails.
    */
   public void execute() throws BuildException
   {
      // Initialize: check parameters, create files
      initTask();

      log("Generating WiX fragment");

      // Generate the Wix Fragment
      generateWixFragment();

      // Format the file human readable for debug purposes
      if (m_bDebugModeEnabled)
      {
         formatWixFile();
      }

      // Compile and link the Wix files into an MSI
      log("Building MSI");
      runCandle();
      runLight();
      log("MSI sucessfully built: " + m_sDestinationFile);

      // See if interop bootstrapper is needed
      if (m_interopBootstrapper != null)
      {
         log("Building boostrapper EXE");
         m_interopBootstrapper.makeBootstrapper(m_sourceDirectory, m_destinationDirectory, m_sSetupFileBaseName);
         log("EXE sucessfully built: " + new File(m_destinationDirectory, m_sSetupFileBaseName + ".exe").getAbsolutePath());

         if (m_sEXEFilenameProperty != null)
         {
            m_project.setProperty(m_sEXEFilenameProperty, m_sSetupFileBaseName + ".exe");
         }
      }

      if (m_sMSIFilenameProperty != null)
      {
         m_project.setProperty(m_sMSIFilenameProperty, m_sSetupFileBaseName + ".msi");
      }
   }

   // parameter setup

   /**
    * Set up all of the task parameters.
    */
   private void initTask()
   {
      // Grab a reference to the current project
      m_project = getProject();

      // Get the debug flag
      String sDebug = m_project.getProperty("wix.wixwriter.debug.enabled");

      if (sDebug == null)
      {
         m_bDebugModeEnabled = false;
      }
      else
      {
         m_bDebugModeEnabled = StringUtil.parseBoolean(sDebug);
      }

      // Set the path to the final MSI file which looks like
      // "...\out\deploy\install\nexj-cardscan-setup-x86.msi"
      m_sSetupFileBaseName = m_sProductName + "-" + m_sInstallName + "-setup-" + m_sVersion;
      m_sDestinationFile = new File(m_destinationDirectory, m_sSetupFileBaseName + ".msi").getAbsolutePath();

      // Check that the task attributes are set
      checkAtttributes();

      // Create the temporary file
      try
      {
         m_fragmentFile = File.createTempFile(TEMP_FILE_PREFIX, ".wxs", m_tempDirectory);
         m_fragmentFile.deleteOnExit();

         // Double buffer the XML writer: both the input stream and the output
         // writer
         // Note: Buffering the ouput stream may cause too much overhead to have
         // significant performance advantages
         OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(m_fragmentFile));
         Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream, XMLUtil.ENCODING));
         m_writer = new XMLWriter(writer);
      }
      catch (IOException e)
      {
         raiseIOException("Could not create temporary \"" + TEMP_FILE_PREFIX + ".wxs\" file", e);
      }
   }

   /**
    * Validate that Ant attributes are set.
    */
   private void checkAtttributes()
   {
      checkAttribute(m_sInstallName, "installname");
      checkAttribute(m_sInstallDirectory, "installdir");
      checkAttribute(m_wixDirectory, "wixdir");
      checkAttribute(m_sourceDirectory, "srcdir");
      checkAttribute(m_destinationDirectory, "destdir");
      checkAttribute(m_sProductName, "product");
   }

   /**
    * Assert that an Ant task attribute has been set.
    *
    * @param value The value to check.
    * @param sAttributeName The name of the Ant attribute.
    * @throws BuildException if the attribute wasn't set.
    */
   private void checkAttribute(Object value, String sAttributeName) throws BuildException
   {
      // If the value hasn't been set, raise a build exception
      if (value == null)
      {
         throw new BuildException("Undefined '" + sAttributeName + "' attribute of " + getTaskName());
      }
   }

   // wix fragment generation

   /**
    * Do the work of filling up the XML WiX fragment file.
    *
    * @throws BuildException if unable to write to the temporary file.
    */
   private void generateWixFragment() throws BuildException
   {
      try
      {
         // Write the start of the WXS file
         writeWixHeader();

         // Nothing to do if no archive groups
         if (!m_archiveGroupList.isEmpty())
         {
            // Write out the install directories
            writeInstallDirectories();

            // Organize the disk IDs
            organizeDiskIds();

            // Write out each archive group, continuing to increment the
            // component ID
            writeArchiveGroups();

            // Write the component groups
            writeComponentGroups();
         }

         // Write the resource (Binary and Icon) elements
         writeResources();

         // Write the end of the WXS file
         writeWixFooter();
      }
      catch (IOException e)
      {
         raiseIOException("Could not write to temporary \"" + TEMP_FILE_PREFIX + ".wxs\" file", e);
      }
   }

   // wix fragment generation

   /**
    * Write the install directories of the archive groups so they can be
    * referenced later.
    *
    * @throws BuildException if an archive group's install directory ID isn't
    *            set.
    * @throws IOException if an I/O error occurs.
    */
   private void writeInstallDirectories() throws IOException
   {
      IdentifierFactory identifierFactory = new IdentifierFactory();

      // Open the root directory reference
      m_writer.openElement("DirectoryRef");
      m_writer.writeAttribute("Id", ROOT_DIRECTORY_ID);
      m_writer.closeElement();

      // Open the system folder if needed
      // Note: if the ID has been used in another WXS file, writing it here
      // will cause a collision
      if (m_sSystemFolderRoot != null)
      {
         openDirectoryElement(m_sSystemFolderRoot, m_sSystemFolderRoot);
      }

      // Break the install directory into parent and child parts
      File installDirFile = new File(m_sInstallDirectory);
      File installDirParent = installDirFile.getParentFile();
      String sInstallDirChild;

      if (installDirParent == null)
      {
         sInstallDirChild = m_sInstallDirectory;
      }
      else
      {
         sInstallDirChild = installDirFile.getName();
      }

      // Open the install directory parent if needed
      int nInstallDirParentDepth = 0;

      if (installDirParent != null)
      {
         // Get the stack of directories
         Stack parents = new Stack();
         File parentDir = installDirParent;

         do
         {
            parents.push(parentDir);
            parentDir = parentDir.getParentFile();
         }
         while (parentDir != null);

         // Mark the depth
         nInstallDirParentDepth = parents.size();

         // Open each directory
         do
         {
            parentDir = (File)parents.pop();

            openDirectoryElement(parentDir.getName(), identifierFactory.getUniqueId(parentDir.getPath()));
         }
         while (!parents.isEmpty());

      }

      // Keep track of which root directories have been written
      Lookup installDirIdSet = new HashTab();

      // Write out each of the non-subdirectory install directories
      for (Iterator it = m_archiveGroupList.iterator(); it.hasNext();)
      {
         ArchiveGroup archiveGroup = (ArchiveGroup)it.next();
         String sInstallDirId = archiveGroup.getInstallDirId();
         String sSubdir = archiveGroup.getSubdir();

         // Check the install directory
         if (sInstallDirId == null)
         {
            throw new BuildException("Undefined 'installdirid' attribute of archivegroup");
         }

         // Only write each install directory once
         if (sSubdir == null && !installDirIdSet.contains(sInstallDirId))
         {
            // Write out the ID'd top directory
            openDirectoryElement(sInstallDirChild, sInstallDirId);
            closeDirectoryElement();

            installDirIdSet.put(sInstallDirId, null);
         }
      }

      // Write out each of the subdirectory install directories
      openDirectoryElement(sInstallDirChild, identifierFactory.getUniqueId(m_sInstallDirectory));

      for (Iterator it = m_archiveGroupList.iterator(); it.hasNext();)
      {
         ArchiveGroup archiveGroup = (ArchiveGroup)it.next();
         String sInstallDirId = archiveGroup.getInstallDirId();
         String sSubdir = archiveGroup.getSubdir();

         // Only write each install directory once
         if (sSubdir != null && !installDirIdSet.contains(sInstallDirId))
         {
            // Write out the ID'd top directory
            openDirectoryElement(sSubdir, sInstallDirId);
            closeDirectoryElement();

            installDirIdSet.put(sInstallDirId, null);
         }
      }

      closeDirectoryElement();

      // Close the install directory parent if needed
      for (int i = 0; i < nInstallDirParentDepth; i++)
      {
         closeDirectoryElement();
      }

      // Close the system folder if it was opened
      if (m_sSystemFolderRoot != null)
      {
         closeDirectoryElement();
      }

      // Close the root directory reference
      m_writer.endElement("DirectoryRef");
   }

   /**
    * Open a directory element.
    *
    * @param sName The name of the directory.
    * @param sId The directory's unique ID.
    * @throws IOException if an I/O error occurs.
    */
   private void openDirectoryElement(String sName, String sId) throws IOException
   {
      String sShortName = getShortName(sName);

      m_writer.openElement("Directory");
      m_writer.writeAttribute("Id", sId);
      m_writer.writeAttribute("Name", sShortName);

      if (!sShortName.equals(sName))
      {
         m_writer.writeAttribute("LongName", sName);
      }

      m_writer.closeElement();
   }

   /**
    * Close a directory element.
    *
    * @throws IOException if an I/O error occurs.
    */
   private void closeDirectoryElement() throws IOException
   {
      m_writer.endElement("Directory");
   }

   /**
    * Retrieve the component group IDs from the archive groups. These IDs are
    * "keys" to the numeric disk IDs: all components in the same component group
    * have the same disk ID. A component group's position in the sorted list
    * corresponds to the disk ID.
    */
   private void organizeDiskIds()
   {
      // Get each ID (assume each is unique)
      for (Iterator it = m_archiveGroupList.iterator(); it.hasNext();)
      {
         ArchiveGroup archiveGroup = (ArchiveGroup)it.next();
         String componentGroupId = archiveGroup.getComponentGroupId();

         // Check that the ID's been set
         if (componentGroupId == null)
         {
            throw new BuildException("Neither 'id' nor 'diskid' attribute set on archivegroup");
         }

         // Add the ID to the list
         m_componentGroupIdList.add(componentGroupId);
      }

      // Sort the list
      Collections.sort(m_componentGroupIdList);
   }

   /**
    * Write the archive groups specified the Ant build file.
    *
    * @throws IOException if an I/O error occurs.
    */
   private void writeArchiveGroups() throws IOException
   {
      // Star the component numbers at zero
      int nStartComponentNumber = 0;

      // Write each archive group, continuing component numbering between them
      // Each archive group will be rooted at a directory reference to the
      // previously written install directory
      for (Iterator it = m_archiveGroupList.iterator(); it.hasNext();)
      {
         ArchiveGroup archiveGroup = (ArchiveGroup)it.next();

         // Get the component group ID
         String sComponentGroupId = archiveGroup.getComponentGroupId();

         // Determine the numeric disk ID (its position in the list)
         int nDiskIdNumber = Collections.binarySearch(m_componentGroupIdList, sComponentGroupId) + 1;

         // Write the archive group
         archiveGroup.write(m_writer, nDiskIdNumber, nStartComponentNumber);

         // Get the next component number
         int nEndComponentNumber = archiveGroup.getNextComponentNumber();

         // Add the components just written to the component group
         addToComponentGroup(nStartComponentNumber, nEndComponentNumber, sComponentGroupId);

         // Restart the component numbering
         nStartComponentNumber = nEndComponentNumber;
      }
   }

   /**
    * Add a set of components (from an archive group) to a component group.
    *
    * @param nStartComponentNumber The first component number inclusive.
    * @param nEndComponentNumber The last component number exclusive.
    * @param sComponentGroupId The ID of the component group.
    */
   private void addToComponentGroup(int nStartComponentNumber, int nEndComponentNumber, String sComponentGroupId)
   {
      // Add this component to the list of other components in this component
      // group
      ArrayList diskIdList = (ArrayList)m_componentGroupMap.get(sComponentGroupId);

      // If needed start a new list
      if (diskIdList == null)
      {
         diskIdList = new ArrayList();
         m_componentGroupMap.put(sComponentGroupId, diskIdList);
      }

      // Add each component ID to the list
      for (int i = nStartComponentNumber; i < nEndComponentNumber; i++)
      {
         String sId = "Component" + i;
         diskIdList.add(sId);
      }
   }

   /**
    * Write WiX ComponentGroup element for grouping components into their
    * respective archive.
    *
    * @throws IOException if an I/O error occurs.
    */
   private void writeComponentGroups() throws IOException
   {
      for (Iterator itMap = m_componentGroupMap.iterator(); itMap.hasNext();)
      {
         String sDiskId = (String)itMap.next();
         ArrayList list = (ArrayList)m_componentGroupMap.get(sDiskId);

         m_writer.openElement("ComponentGroup");
         m_writer.writeAttribute("Id", sDiskId);
         m_writer.closeElement();

         for (Iterator itList = list.iterator(); itList.hasNext();)
         {
            m_writer.openElement("ComponentRef");
            m_writer.writeAttribute("Id", itList.next().toString());
            m_writer.closeEmptyElement();
         }

         m_writer.endElement("ComponentGroup");
      }
   }

   /**
    * Write Wix resource (Binary and Icon) elements, based on the custom ANT
    * subtask Resource and its subtasks WixBinary and WixIcon.
    *
    * @throws IOException if an I/O error occurs.
    */
   private void writeResources() throws IOException
   {
      for (Iterator it = m_resourceList.iterator(); it.hasNext();)
      {
         Resource resource = (Resource)it.next();
         resource.write(m_writer);
      }
   }

   /**
    * Write the start of the WiX fragment.
    *
    * @throws IOException if an I/O error occurs.
    */
   private void writeWixHeader() throws IOException
   {
      m_writer.openElement("Wix");
      m_writer.writeAttribute("xmlns", "http://schemas.microsoft.com/wix/2003/01/wi");
      m_writer.closeElement();

      m_writer.startElement("Fragment");
   }

   /**
    * Write the end of the WiX fragment.
    *
    * @throws IOException if an I/O error occurs.
    */
   private void writeWixFooter() throws IOException
   {
      m_writer.endElement("Fragment");
      m_writer.endElement("Wix");
      m_writer.close();
   }

   // reformatting

   /**
    * Perform fomatting on the WiX fragment before it's given to Candle.
    *
    * @throws BuildException if cannot write to temporary file.
    */
   private void formatWixFile() throws BuildException
   {
      Writer writer = null;

      try
      {
         StreamSource ss = new StreamSource(m_fragmentFile);
         File formattedFile = File.createTempFile(TEMP_FILE_PREFIX + "formatted_", ".wxs", m_tempDirectory);

         writer = IOUtil.openBufferedWriter(formattedFile, XMLUtil.ENCODING);
         XMLUtil.formatXML(ss, true, writer);
         writer.close();
         writer = null;
      }
      catch (IOException e)
      {
         raiseIOException("Could not write to temporary \"" + TEMP_FILE_PREFIX + ".wxs\" file", e);
      }
      finally
      {
         if (writer != null)
         {
            try
            {
               writer.close();
            }
            catch (IOException e)
            {
            }
         }
      }
   }

   // io exception

   /**
    * Give IO exception details and halt the build.
    *
    * @param sMesssage The build error.
    * @param e The cause exception.
    * @throws BuildException to halt the build.
    */
   private void raiseIOException(String sMesssage, IOException e) throws BuildException
   {
      log(sMesssage, ERROR_LEVEL);
      e.printStackTrace();
      throw new BuildException(sMesssage, e);
   }

   // wix execution

   /**
    * Runs Candle on the fragment and the base file, checking that it runs
    * successfully.
    *
    * @throws BuildException if failed to run Candle or Candle fails.
    */
   private void runCandle() throws BuildException
   {
      int nExitValue;

      try
      {
         ArrayList argList = new ArrayList();

         // Candle executable
         argList.add(new File(m_wixDirectory, "candle.exe").getAbsolutePath());

         // Generated input file
         argList.add(m_fragmentFile.getAbsolutePath());

         // Specific base WXS input file
         argList.add(new File(m_sourceDirectory, m_sInstallName + ".wxs").getAbsolutePath());

         // Disk ID defines (the name of the variable is the component group ID)
         int nDiskIdNumber = 1;
         for (Iterator it = m_componentGroupIdList.iterator(); it.hasNext();)
         {
            String sDiskId = (String)it.next();
            argList.add("-d" + sDiskId + "=" + nDiskIdNumber);
            nDiskIdNumber++;
         }

         // Custom variable defines
         for (Iterator it = m_variableList.iterator(); it.hasNext();)
         {
            Variable variable = (Variable)it.next();
            argList.add(variable.getDefineParameter());
         }

         // Output folder
         // NOTE: in WIX, if you specify quotes around a directory (e.g. a path
         // that ends in \) you must end it // with \\ instead.
         // E.g. "C:\temp\" will give an error, use "C:\temp\\" instead.
         argList.add("-out");
         argList.add(m_tempDirectory.getAbsolutePath() + File.separator + File.separator);

         // Run candle
         Execute candle = new Execute();
         candle.setAntRun(m_project);
         candle.setCommandline((String[])argList.toArray(new String[0]));
         candle.execute();

         nExitValue = candle.getExitValue();
      }
      catch (Exception e)
      {
         e.printStackTrace();
         throw new BuildException("Fatal exception occurred while running WiX compiler Candle", e);
      }

      if (nExitValue != 0)
      {
         log("Candle failed (exit value " + nExitValue + ")", ERROR_LEVEL);
         throw new BuildException("WiX compiler Candle failed");
      }
   }

   /**
    * Runs Light to generate the MSI, checking that it runs successfully, and
    * deleting the .wixobj files afterwards.
    *
    * @throws BuildException if failed to run Light or Light fails.
    */
   private void runLight() throws BuildException
   {
      int nExitValue;

      try
      {
         ArrayList argList = new ArrayList();

         // Light executable
         argList.add(new File(m_wixDirectory, "light.exe").getAbsolutePath());

         // Generated input file's object file
         String sGeneratedObj = m_fragmentFile.getAbsolutePath();
         sGeneratedObj = sGeneratedObj.substring(0, sGeneratedObj.lastIndexOf('.')) + ".wixobj";
         argList.add(sGeneratedObj);

         // Specific base WXS input file's object file
         String sSpecificObj = new File(m_tempDirectory, m_sInstallName + ".wixobj").getAbsolutePath();
         argList.add(sSpecificObj);

         // Wixlib file
         argList.add(new File(m_wixDirectory, "sca.wixlib").getAbsolutePath());

         // Output MSI file
         argList.add("-out");
         argList.add(m_sDestinationFile);

         // Run light
         Execute light = new Execute();
         light.setAntRun(m_project);
         light.setCommandline((String[])argList.toArray(new String[0]));
         light.execute();

         nExitValue = light.getExitValue();

         // Delete *.wixobj files
         new File(sGeneratedObj).deleteOnExit();
         new File(sSpecificObj).deleteOnExit();
      }
      catch (Exception e)
      {
         e.printStackTrace();
         throw new BuildException("Fatal exception occurred while running WiX linker Light", e);
      }

      if (nExitValue != 0)
      {
         log("Light failed (exit value " + nExitValue + ")", ERROR_LEVEL);
         throw new BuildException("WiX linker Light failed");
      }
   }

   /**
    * Ant type for packaging file structures (supports Ant references).
    */
   public static class ArchiveGroup extends DataType
   {
      /**
       * Pseudonym for the Global Assembly Cache, used as a prefix for
       * calculating hashes.
       */
      private final static String GAC_FOLDER_NAME = "Global Assembly Cache";

      /**
       * ID for reference, doubles as disk ID.
       */
      private String m_sId = null;

      /**
       * Component group ID for the archive group.
       */
      private String m_sComponentGroupId = null;

      /**
       * Installation directory for the archive group.
       */
      private String m_sInstallDirectoryId;

      /**
       * The name of the subdirectory where the archive group is rooted (if
       * applicable).
       */
      private String m_sRootSubdirectory;

      /**
       * The WiX condition for a given build archivegroup / Wix component
       */
      private String m_sCondition;

      /**
       * List of all FileSets in the ArchiveGroup.
       */
      private ArrayList m_fileSetList = new ArrayList();

      /**
       * List of all Assemblies in the ArchiveGroup.
       */
      private ArrayList m_assemblyList = new ArrayList();

      /**
       * For generating unique or random IDs.
       */
      private IdentifierFactory m_identifierFactory = new IdentifierFactory();

      /**
       * The Ant project.
       */
      private Project m_project;

      /**
       * Handle to the writer for the WiX file fragment.
       */
      private static XMLWriter m_writer = null;

      /**
       * Numeric disk ID for the archive group.
       */
      private int m_nDiskIdNumber;

      /**
       * Next component number to use.
       */
      private int m_nComponentNumber;

      /**
       * Architecture specified in components.
       */
      private String m_sArch;

      /**
       * Constructor used by Ant.
       */
      public ArchiveGroup()
      {
      }

      /**
       * Set the processsor architecture for which the install is intended (x86 or
       * x84).
       *
       * @param sArch The architecture, "x86" or "x64".
       */
      public void setArch(String sArch)
      {
         m_sArch = sArch;
      }

      /**
       * Set the ID used for reference.
       *
       * @param sId The ID.
       */
      public void setId(String sId)
      {
         m_sId = sId;
      }

      /**
       * Set the media ID used by all components. As a string, it specifies the
       * component group ID: the real disk ID number will be calculated later.
       *
       * @param sId The media ID.
       */
      public void setDiskId(String sId)
      {
         m_sComponentGroupId = sId;
      }

      /**
       * Set the ID of the install directory for reference in WXS files.
       *
       * @param sInstallDirectoryId The ID of the install directory.
       */
      public void setInstallDirId(String sInstallDirectoryId)
      {
         m_sInstallDirectoryId = sInstallDirectoryId;
      }

      /**
       * Set the root subdirectory.
       *
       * @param sRootSubdirectory The root subdirectory.
       */
      public void setSubdir(String sRootSubdirectory)
      {
         m_sRootSubdirectory = sRootSubdirectory;
      }

      /**
       * Set condition for this ArchiveGroup.
       *
       * @param sRootSubdirectory The root subdirectory.
       */
      public void setCondition(String sCondition)
      {
         m_sCondition = sCondition;
      }

      /**
       * Add a file set to the archive group.
       *
       * @param fileSet The FileSet to add.
       */
      public void addFileSet(FileSet fileSet)
      {
         m_fileSetList.add(fileSet);
      }

      /**
       * Add a zip file set to the archive group.
       *
       * @param fileSet The FileSet to add.
       */
      public void addZipFileSet(ZipFileSet fileSet)
      {
         m_fileSetList.add(fileSet);
      }

      /**
       * Add an assembly (for the GAC) to the archive group.
       *
       * @return The created Assembly.
       */
      public Assembly createAssembly()
      {
         Assembly assembly = new Assembly();
         m_assemblyList.add(assembly);
         return assembly;
      }

      /**
       * Get the root directory ID (dereference if needed).
       *
       * @return The root directory ID.
       */
      public String getInstallDirId()
      {
         if (isReference())
         {
            return ((ArchiveGroup)getCheckedRef(ArchiveGroup.class, "archivegroup")).getInstallDirId();
         }
         else
         {
            return m_sInstallDirectoryId;
         }
      }

      /**
       * Get the root subdirectory.
       *
       * @return The root subdirectory.
       */
      public String getSubdir()
      {
         if (isReference())
         {
            return ((ArchiveGroup)getCheckedRef(ArchiveGroup.class, "archivegroup")).getSubdir();
         }
         else
         {
            return m_sRootSubdirectory;
         }
      }

      /**
       * Get the disk ID (dereference if needed). If unset, default to the
       * archive group's ID.
       *
       * @return The disk ID.
       */
      public String getComponentGroupId()
      {
         if (isReference())
         {
            return ((ArchiveGroup)getCheckedRef(ArchiveGroup.class, "archivegroup")).getComponentGroupId();
         }
         else
         {
            if (m_sComponentGroupId != null)
            {
               return m_sComponentGroupId;
            }
            else
            {
               return m_sId;
            }
         }
      }

      /**
       * Write the contents of the archive group: file sets and assemblies
       * (dereference if needed).
       *
       * @param writer The writer for the WiX fragment.
       * @param nDiskIdNumber The numeric disk ID used by all components.
       * @param nComponentNumber The starting component number.
       * @param sArch The target processor architecture used by components.
       * @throws IOException if an I/O error occurs.
       */
      public void write(XMLWriter writer, int nDiskIdNumber, int nComponentNumber) throws IOException
      {
         if (isReference())
         {
            ((ArchiveGroup)getCheckedRef(ArchiveGroup.class, "archivegroup")).write(writer, nDiskIdNumber, nComponentNumber);
         }
         else
         {
            // Grab a reference to the current project
            m_project = getProject();

            // Set the XML writer
            m_writer = writer;

            // Make sure the component group ID is set
            if (m_sComponentGroupId == null)
            {
               m_sComponentGroupId = m_sId;
            }

            // Start the component counting
            m_nComponentNumber = nComponentNumber;

            // Save the disk ID number
            m_nDiskIdNumber = nDiskIdNumber;

            // Open a directory reference to the install directory
            openDirReference();

            // Start with empty lists
            FileTree fileTree = new FileTree();

            // Scan each file set
            for (Iterator it = m_fileSetList.iterator(); it.hasNext();)
            {
               FileSet fileSet = (FileSet)it.next();
               scanFileSet(fileSet, fileTree);
            }

            // The hashed prefix is the ID of the component group (to allow
            // duplicate trees)
            m_identifierFactory.setPrefixDir(m_sComponentGroupId);

            // Write out the filesets
            fileTree.write();

            // Write the assemblies
            m_identifierFactory.setPrefixDir(GAC_FOLDER_NAME);

            for (Iterator it = m_assemblyList.iterator(); it.hasNext();)
            {
               Assembly assembly = (Assembly)it.next();
               assembly.write();
            }

            // Close the directory reference
            closeDirReference();
         }
      }

      /**
       * Get the next component number that the next component should use.
       *
       * @return The next component number.
       */
      public int getNextComponentNumber()
      {
         if (isReference())
         {
            return ((ArchiveGroup)getCheckedRef(ArchiveGroup.class, "archivegroup")).getNextComponentNumber();
         }
         else
         {
            return m_nComponentNumber;
         }
      }

      /**
       * Open the reference to the root directory.
       *
       * @throws IOException if an I/O error occurs.
       */
      private void openDirReference() throws IOException
      {
         m_writer.openElement("DirectoryRef");
         m_writer.writeAttribute("Id", m_sInstallDirectoryId);
         m_writer.closeElement();
         log("<directoryref id=\"" + m_sInstallDirectoryId + "\">", DEBUG_LEVEL);
      }

      /**
       * Close the reference to the root directory.
       *
       * @throws IOException if an I/O error occurs.
       */
      private void closeDirReference() throws IOException
      {
         m_writer.endElement("DirectoryRef");
         log("</directoryref>", DEBUG_LEVEL);
      }

      /**
       * Add the files and directories from the file set to the file tree.
       * Process any extra parameters on the file set.
       *
       * @param fileSet The file set.
       * @param fileTree The file tree.
       * @throws BuildException if the file set is invalid.
       */
      private void scanFileSet(FileSet fileSet, FileTree fileTree) throws BuildException
      {
         // The prefix can be set using a zip file set
         String sPrefix = "";

         // Check for ZipFileSet-specific features
         if (fileSet instanceof ZipFileSet)
         {
            ZipFileSet zipFileSet = (ZipFileSet)fileSet;

            // Check if the file set comes from inside a zip file
            if (zipFileSet.getSrc(m_project) != null)
            {
               unzipFileSet(zipFileSet);
            }

            // Check if the full path modes is being used
            if (zipFileSet.getFullpath(m_project) != null && !zipFileSet.getFullpath(m_project).equals(""))
            {
               // Add the single file instead of following the regular procedure
               fileTree.addFullPathFile(zipFileSet);
               return;
            }

            // Use prefix directories if needed
            sPrefix = ((ZipFileSet)fileSet).getPrefix(m_project);

            if (!sPrefix.equals(""))
            {
               // Fix the formatting and append a separator
               sPrefix = new File(sPrefix).getPath() + File.separator;
            }
         }

         // Use the file set's scanner
         // If the file set is invalid, FileSet.getDirectoryScanner will
         // raise an exception
         DirectoryScanner scanner = fileSet.getDirectoryScanner(m_project);

         // Check that the file set isn't empty
         if (scanner.getIncludedDirsCount() == 0 && scanner.getIncludedFilesCount() == 0)
         {
            throw new BuildException("Fileset at \"" + scanner.getBasedir().getAbsolutePath()
               + "\" does not include any files or directories (for more info, run Ant with \"-debug\")");
         }

         // Grab the directories and the files
         fileTree.addScanner(scanner, sPrefix);
      }

      /**
       * Unzip the zip file, point the file set to the unzipped files, and make
       * sure the files get deleted on exit.
       *
       * @param zipFileSet The file set.
       */
      private void unzipFileSet(ZipFileSet zipFileSet)
      {
         // Get the name of a temporary output folder
         File outFolder = FileUtils.getFileUtils().createTempFile(TEMP_FILE_PREFIX, "", null, false, false);

         // Unzip the zip file
         Expand unzipper = (Expand)m_project.createTask("unzip");
         unzipper.setSrc(zipFileSet.getSrc(m_project));
         unzipper.setDest(outFolder);
         unzipper.execute();

         // Change the file set to use the new directory
         zipFileSet.setSrcResource(null);
         zipFileSet.setDir(outFolder);

         // Delete the folder on exit
         deleteDirectoryOnExit(outFolder);
      }

      /**
       * Preorder recursively mark each item under the directory as "delete on
       * exit" so that they actually get deleted postorder on exit.
       *
       * @param directory The starting directory.
       */
      private void deleteDirectoryOnExit(File directory)
      {
         // Delete base directory
         directory.deleteOnExit();

         // Delete files and folders inside
         File[] memberList = directory.listFiles();

         for (int i = 0; i < memberList.length; i++)
         {
            if (memberList[i].isDirectory())
            {
               // Go into directory
               deleteDirectoryOnExit(memberList[i]);
            }
            else
            {
               // Delete file
               memberList[i].deleteOnExit();
            }
         }
      }

      /**
       * Open a component. Key the GUID of the component usually by using the
       * first file in it.
       *
       * @param sKeyPath The path which the component's GUID is generated from.
       * @throws IOException if an I/O error occurs.
       */
      private void openComponentElement(String sKeyPath) throws IOException
      {
         // Generate Component IDs by incrementing m_nComponents. This is better
         // than the other method of Component IDs being path names
         String sId = "Component" + m_nComponentNumber;
         String sGuid = m_identifierFactory.getGUID(sKeyPath);

         m_writer.openElement("Component");
         m_writer.writeAttribute("Id", sId);
         m_writer.writeAttribute("Guid", sGuid);
         m_writer.writeAttribute("DiskId", m_nDiskIdNumber);

         // Writing Win64 attribute based on architecture type
         if (m_sArch != null)
         {
            if (m_sArch.equals("x64"))
            {
               m_writer.writeAttribute("Win64", "yes");
            }
            else
            {
               m_writer.writeAttribute("Win64", "no");
            }
         }

         m_writer.closeElement();

         log("..<component>", DEBUG_LEVEL);

         m_nComponentNumber++;
      }

      /**
       * Close a component.
       *
       * @throws IOException if an I/O error occurs.
       */
      private void closeComponentElement() throws IOException
      {
         // Write the condition           
         if (m_sCondition != null)
         {
            m_writer.startElement("Condition");
            m_writer.write(m_sCondition);
            m_writer.endElement("Condition");
         }

         m_writer.endElement("Component");
         log("..</component>", DEBUG_LEVEL);
      }

      /**
       * A file that needs to go into the Global Assembly Cache (set up by Ant).
       */
      public class Assembly
      {
         /**
          * Source file.
          */
         private File m_file;

         /**
          * Constructor.
          */
         public Assembly()
         {
         }

         /**
          * Set the source file.
          *
          * @param file The source file.
          */
         public void setFile(File file)
         {
            m_file = file;
         }

         /**
          * Write out the assembly in its own component.
          *
          * @throws BuildException if source file doesn't exist.
          * @throws IOException if an I/O error occurs.
          */
         public void write() throws BuildException, IOException
         {
            // Open a new component
            openComponentElement(m_file.getName());

            String sName = m_file.getName();
            String sShortName = getShortName(sName);
            String sId = m_identifierFactory.getUniqueId(sName);

            String sSourcePath;

            try
            {
               sSourcePath = m_file.getCanonicalPath();
            }
            catch (IOException e)
            {
               throw new BuildException("File \"" + m_file.getAbsolutePath() + "\" does not exist");
            }

            // Write the assembly file element
            m_writer.openElement("File");
            m_writer.writeAttribute("Id", sId);
            m_writer.writeAttribute("Name", sShortName);

            if (!sShortName.equals(sName))
            {
               m_writer.writeAttribute("LongName", sName);
            }

            m_writer.writeAttribute("Source", sSourcePath);
            m_writer.writeAttribute("Vital", "yes");

            // Tell the installer this is an assembly
            m_writer.writeAttribute("KeyPath", "yes");
            m_writer.writeAttribute("Assembly", ".net");

            // Close the file element
            m_writer.closeEmptyElement();
           
            log("....<assembly name=\"" + sName + "\"/>", DEBUG_LEVEL);

            // Close the component
            closeComponentElement();
         }
      }
     
      /**
       * A structure of files and directories. Build using directory scanners,
       * writes out the structure as an XML tree.
       */
      private class FileTree
      {
         /**
          * Preorder list of directories and files.
          */
         private ArrayList m_installItemList;

         /**
          * The directory hierarchy.
          */
         Stack m_stack;

         /**
          * The path of the current directory.
          */
         String m_sCurrentDirectory;

         /**
          * Whether this directory has anything in it.
          */
         boolean m_bEmptyDir = true;

         /**
          * Whether a component is open.
          */
         boolean m_bComponentOpened;

         /**
          * Create an empty tree.
          */
         public FileTree()
         {
            // The list is empty
            m_installItemList = new ArrayList();
         }

         /**
          * Add the included directories and directories from a scanner to the
          * tree. The prefix is prefixed to each path and source paths for files
          * are noted.
          *
          * @param scanner The scanner.
          * @param sPrefix The prefix.
          */
         public void addScanner(DirectoryScanner scanner, String sPrefix)
         {
            // Grab the directory list
            String list[] = scanner.getIncludedDirectories();

            // Make room for the list
            m_installItemList.ensureCapacity(m_installItemList.size() + list.length);

            // Add anything not empty
            for (int i = 0; i < list.length; i++)
            {
               if (!list[i].equals(""))
               {
                  // Use the prefix
                  // Directories need to end in '\' so that we get the correct
                  // order
                  // "a-b\", "a-b\c\", "a\", "a\c\" (NOT "a", "a-b", "a-b\c",
                  // "a\c")
                  // Correct order is important for initial sorting and later
                  // comparing with other paths
                  m_installItemList.add(new InstallDirectory(sPrefix + list[i] + File.separator));
               }
            }

            // Grab the file list
            list = scanner.getIncludedFiles();

            // Get the base directory
            String sBaseDir = scanner.getBasedir().getPath();

            // Make room for the files
            m_installItemList.ensureCapacity(m_installItemList.size() + list.length);

            // Add each file to the list
            for (int i = 0; i < list.length; i++)
            {
               m_installItemList.add(new InstallFile(list[i], sPrefix, sBaseDir));
            }
         }

         /**
          * Add the full path file included by the file set.
          *
          * @param zipFileSet The file set.
          * @throws BuildException if file set is invalid.
          */
         public void addFullPathFile(ZipFileSet zipFileSet) throws BuildException
         {
            // Grab the file list
            DirectoryScanner scanner = zipFileSet.getDirectoryScanner(m_project);
            String sFiles[] = scanner.getIncludedFiles();

            // Assert there's only one file
            if (sFiles.length != 1)
            {
               throw new BuildException("zipfileset 'fullpath' attribute set but fileset didn't include only one file");
            }

            // Get the install path
            String sInstallPath = zipFileSet.getFullpath(m_project);

            // Find the full path to the real file
            String sSourcePath = new File(scanner.getBasedir(), sFiles[0]).getPath();

            // Add the file to the list
            m_installItemList.add(new InstallFile(sInstallPath, sSourcePath));
         }

         /**
          * Write out the file tree.
          *
          * @throws IOException if an I/O error occurs.
          */
         public void write() throws IOException
         {
            // Start with the empty directory
            m_stack = new Stack();
            m_stack.push("");
            m_sCurrentDirectory = "";
            m_bEmptyDir = true;
            m_bComponentOpened = false;

            // Sort the list into a preorder tree
            Collections.sort(m_installItemList);

            // Write out each item
            for (int i = 0; i < m_installItemList.size(); i++)
            {
               ((InstallItem)m_installItemList.get(i)).write();
            }

            // Close any open component
            if (m_bComponentOpened)
            {
               closeComponentElement();
            }

            // Close the remaining directories
            while (m_stack.size() > 1)
            {
               pop();
            }

            // Check if the root directory was empty
            if (m_bEmptyDir)
            {
               makeEmptyDirectory();
            }
         }

         /**
          * Close and open directory elements as needed to travserse to the
          * target directory.
          *
          * @param sTargetDir The target directory.
          * @throws IOException if an I/O error occurs.
          */
         private void traverseTo(String sTargetDir) throws IOException
         {
            // See if traversal needed
            if (sTargetDir.equals(m_sCurrentDirectory))
            {
               return;
            }

            // Close any open component
            if (m_bComponentOpened)
            {
               closeComponentElement();
               m_bComponentOpened = false;
            }

            // Pop directories until a common directory is found
            while (!sTargetDir.startsWith(m_sCurrentDirectory))
            {
               pop();
            }

            // See traversal was just pops
            if (sTargetDir.equals(m_sCurrentDirectory))
            {
               return;
            }

            // See if parent directories are needed
            String sParentDir = new File(sTargetDir).getParent();
            boolean bParentNeeded;

            if (sParentDir == null)
            {
               bParentNeeded = false;
            }
            else
            {
               bParentNeeded = !m_sCurrentDirectory.startsWith(sParentDir)
                  || sParentDir.length() != m_sCurrentDirectory.length() - 1;
            }

            if (bParentNeeded)
            {
               Stack parents = new Stack();

               // See how many levels of parents need to be pushed onto the
               // stack
               sParentDir = sParentDir + File.separator;

               do
               {
                  parents.push(sParentDir);

                  sParentDir = new File(sParentDir).getParent();

                  if (sParentDir == null)
                  {
                     break;
                  }
                  else
                  {
                     sParentDir = sParentDir + File.separator;
                  }
               }
               while (!sParentDir.equals(m_sCurrentDirectory));

               // Write the 'tower' of directories
               do
               {
                  push((String)parents.pop());
               }
               while (!parents.isEmpty());
            }

            // Open the target itself
            push(sTargetDir);
         }

         /**
          * Push a directory onto the stack.
          *
          * @param sDirectory The directory.
          * @throws IOException if an I/O error occurs.
          */
         private void push(String sDirectory) throws IOException
         {
            String sName = new File(sDirectory).getName();
            String sShortName = getShortName(sName);
            String sId = m_identifierFactory.getUniqueId(sDirectory);

            // Open the directory element
            m_writer.openElement("Directory");
            m_writer.writeAttribute("Id", sId);
            m_writer.writeAttribute("Name", sShortName);

            if (!sShortName.equals(sName))
            {
               m_writer.writeAttribute("LongName", sName);
            }

            m_writer.closeElement();

            log("<directory name=\"" + sName + "\">", DEBUG_LEVEL);

            // Push this directory onto the stack
            m_stack.push(sDirectory);
            m_sCurrentDirectory = (String)m_stack.peek();

            // Nothing has been written in this directory
            m_bEmptyDir = true;
         }

         /**
          * Take a directory off the stack.
          *
          * @throws IOException if an I/O error occurs.
          */
         private void pop() throws IOException
         {
            // If the last directory had nothing in it
            if (m_bEmptyDir)
            {
               // Use a 'create directory' element
               makeEmptyDirectory();

               // If we go down another level, that directory isn't empty
               m_bEmptyDir = false;
            }

            // Close the current directory element
            m_writer.endElement("Directory");
            log("</directory>", DEBUG_LEVEL);

            // Pop it off the stack
            m_stack.pop();
            m_sCurrentDirectory = (String)m_stack.peek();
         }

         /**
          * Make an empty directory using the WiX CreateFolder element.
          *
          * @throws IOException if an I/O error occurs.
          */
         private void makeEmptyDirectory() throws IOException
         {
            // Open a new component
            openComponentElement(m_sCurrentDirectory);

            // Write the "create folder" element
            m_writer.openElement("CreateFolder");
            m_writer.closeEmptyElement();
            log("....<emptydir/>", DEBUG_LEVEL);

            // Close the component
            closeComponentElement();
         }

         /**
          * A file or directory to be installed. Sortable based on the install
          * path.
          */
         private abstract class InstallItem implements Comparable
         {
            /**
             * Path to be installed at.
             */
            private String m_sInstallPath;

            /**
             * Create an item.
             *
             * @param sInstallPath The install path.
             */
            protected InstallItem(String sInstallPath)
            {
               m_sInstallPath = sInstallPath;
            }

            /**
             * Compare by install path.
             *
             * @see java.lang.Comparable#compareTo(java.lang.Object)
             */
            public int compareTo(Object other)
            {
               return m_sInstallPath.compareTo(((InstallItem)other).m_sInstallPath);
            }

            /**
             * Write out item, traversing to directories as needed.
             *
             * @throws IOException if an I/O error occurs.
             */
            public abstract void write() throws IOException;

            /**
             * Get the install path.
             *
             * @return The install path.
             */
            protected String getPath()
            {
               return m_sInstallPath;
            }
         }

         /**
          * A directory to be installed.
          */
         private class InstallDirectory extends InstallItem
         {
            /**
             * Create a directory.
             *
             * @param sDirectory The path.
             */
            public InstallDirectory(String sDirectory)
            {
               super(sDirectory);
            }

            /**
             * @see nexj.core.build.WiXWriter.ArchiveGroup.FileTree.InstallItem#write()
             */
            public void write() throws IOException
            {
               log("@directory \"" + getPath() + "\"", DEBUG_LEVEL);
               traverseTo(getPath());
            }
         }

         /**
          * A file to be installed. The path to install to as well as the path
          * to the local file being included in the install are kept track of.
          */
         private class InstallFile extends InstallItem
         {
            /**
             * The length of the prefix (if it was added).
             */
            private int m_nPrefixLength;

            /**
             * The name of the base directory where the local file is found.
             */
            private String m_sBaseDirectory;

            /**
             * Alternatively, a full path to source file is stored.
             */
            private String m_sFullSourcePath;

            /**
             * Create a regular install file .
             *
             * @param sInstallPath The install path as DirectoryScanner
             *           determined.
             * @param sPrefix The path prefix either "" or ending in a file
             *           separator.
             * @param sBaseDirectory The base directory where the source file
             *           was included from.
             */
            public InstallFile(String sInstallPath, String sPrefix, String sBaseDirectory)
            {
               // Set the install path (add the prefix)
               super(sPrefix + sInstallPath);

               // Store the prefix
               m_nPrefixLength = sPrefix.length();

               // Store the base directory
               m_sBaseDirectory = sBaseDirectory;

               // This isn't a full path file
               m_sFullSourcePath = null;
            }

            /**
             * Create a full path install file.
             *
             * @param sInstallPath The install path.
             * @param sSourcePath The full path to the source file.
             */
            public InstallFile(String sInstallPath, String sSourcePath)
            {
               // Set the install path, fix the formatting
               super(new File(sInstallPath).getPath());

               // No prefix or base directory
               m_nPrefixLength = 0;
               m_sBaseDirectory = null;

               // This is a full path file, store the source path
               m_sFullSourcePath = sSourcePath;
            }

            /**
             * @see nexj.core.build.WiXWriter.ArchiveGroup.FileTree.InstallItem#write()
             */
            public void write() throws IOException
            {
               String sPath = getPath();
               File file = new File(sPath);

               // Get its parent directory
               String sParentDir = file.getParent();

               if (sParentDir == null)
               {
                  sParentDir = "";
               }
               else
               {
                  // Postfix with a separator so that director comparisons work
                  // consistently
                  sParentDir = sParentDir + File.separator;
               }

               // Go to the correct directory
               traverseTo(sParentDir);

               // Open the component
               if (!m_bComponentOpened)
               {
                  openComponentElement(sPath);
                  m_bComponentOpened = true;
                  m_bEmptyDir = false;
               }

               String sName = file.getName();
               String sShortName = getShortName(sName);
               String sId = m_identifierFactory.getUniqueId(sPath);
               String sSourcePath = getSourcePath();

               // Write the file element
               m_writer.openElement("File");
               m_writer.writeAttribute("Id", sId);
               m_writer.writeAttribute("Name", sShortName);

               if (!sShortName.equals(sName))
               {
                  m_writer.writeAttribute("LongName", sName);
               }

               m_writer.writeAttribute("Source", sSourcePath);
               m_writer.writeAttribute("Vital", "yes");
               m_writer.closeEmptyElement();

               log("....<file name=\"" + sName + "\"/>", DEBUG_LEVEL);
            }

            /**
             * Get the path to the source file.
             *
             * @return The source path.
             */
            private String getSourcePath()
            {
               // Check if full path was used
               if (m_sFullSourcePath != null)
               {
                  return m_sFullSourcePath;
               }

               // Start from the install path
               String sSourcePath = getPath();

               // Remove the prefix
               if (m_nPrefixLength > 0)
               {
                  sSourcePath = sSourcePath.substring(m_nPrefixLength);
               }

               // Root at the base directory
               return new File(m_sBaseDirectory, sSourcePath).getAbsolutePath();
            }
         }
      }
   }

   /**
    * Resource class that contains Wix binary and icon elements. NOTE: The
    * subclasses are called Wixbinary and Wixicon which correspond to their
    * respective custom ANT tasks. It makes more sense to call it "Wixbinary"
    * instead of "Binary", to avoid confusion with nexj.core.util.Binary which
    * is imported. "Wixicon" is similarly named for consistency. (Supports Ant
    * references.)
    */
   public static class Resource extends DataType
   {
      /**
       * Constructor.
       */
      public Resource()
      {
      }

      /**
       * List of all Binaries in the Resource.
       */
      private ArrayList m_wixBinaryList = new ArrayList();

      /**
       * List of all Icons in the Resource.
       */
      private ArrayList m_wixIconList = new ArrayList();

      /**
       * Creates a new WixBinary.
       *
       * @return The new WixBinary.
       */
      public GenericResource createBinary()
      {
         GenericResource binary = new GenericResource();
         m_wixBinaryList.add(binary);
         return binary;
      }

      /**
       * Creates a new WixIcon.
       *
       * @return The new WixIcon.
       */
      public GenericResource createIcon()
      {
         GenericResource icon = new GenericResource();
         m_wixIconList.add(icon);
         return icon;
      }

      /**
       * Write out the references.
       *
       * @param writer The writer for the Wix fragment.
       * @throws IOException if an I/O error occurs.
       */
      public void write(XMLWriter writer) throws IOException
      {
         if (isReference())
         {
            ((Resource)getCheckedRef(Resource.class, "resource")).write(writer);
         }
         else
         {
            // Write binaries
            for (Iterator binaryIt = m_wixBinaryList.iterator(); binaryIt.hasNext();)
            {
               GenericResource binary = (GenericResource)binaryIt.next();

               // Check the atributes
               binary.checkAttributes("binary");

               // Write the element
               writer.openElement("Binary");
               writer.writeAttribute("Id", binary.getResId());
               writer.writeAttribute("SourceFile", binary.getFile());
               writer.closeEmptyElement();
               log("<binary id=\"" + binary.getResId() + "\"/>", DEBUG_LEVEL);
            }

            // Write icons
            for (Iterator iconIt = m_wixIconList.iterator(); iconIt.hasNext();)
            {
               GenericResource icon = (GenericResource)iconIt.next();

               // Check the atributes
               icon.checkAttributes("icon");

               // Write the element
               writer.openElement("Icon");
               writer.writeAttribute("Id", icon.getResId());
               writer.writeAttribute("SourceFile", icon.getFile());
               writer.closeEmptyElement();
               log("<icon id=\"" + icon.getResId() + "\"/>", DEBUG_LEVEL);
            }
         }
      }

      /**
       * Superclass which contains code common to a ResourceBase (WixBinary or
       * WixIcon, which are custom ANT subtasks of Resource).
       */
      public class GenericResource
      {
         /**
          * The ID used in Wix.
          */
         private String m_sResourceId;

         /**
          * The local source file.
          */
         private String m_sFile;

         /**
          * Constructor.
          */
         public GenericResource()
         {
         }

         /**
          * Set the ID of the resource (used by Ant).
          *
          * @param sId The ID.
          */
         public void setResId(String sId)
         {
            m_sResourceId = sId;
         }

         /**
          * Set the source file (used by Ant).
          *
          * @param file The source file.
          * @throws BuildException if the source file doesn't exist.
          */
         public void setFile(File file) throws BuildException
         {
            try
            {
               m_sFile = file.getCanonicalPath();
            }
            catch (IOException e)
            {
               throw new BuildException("File \"" + file.getAbsolutePath() + "\" does not exist");
            }
         }

         /**
          * Get the ID of the resource.
          *
          * @return The ID.
          */
         public String getResId()
         {
            return m_sResourceId;
         }

         /**
          * Get the path to the source file.
          *
          * @return The path.
          */
         public String getFile()
         {
            return m_sFile;
         }

         /**
          * Check that attributes have been set.
          *
          * @param elementName The Ant name of the element.
          */
         public void checkAttributes(String elementName)
         {
            if (m_sResourceId == null)
            {
               throw new BuildException("Undefined 'resid' attribute of " + elementName);
            }
            if (m_sFile == null)
            {
               throw new BuildException("Undefined 'file' attribute of " + elementName);
            }
         }
      }
   }

   /**
    * Definition of a WiX Candle preprocessor variable.
    */
   public class Variable
   {
      /**
       * Variable name.
       */
      private String m_sName;

      /**
       * Variable value.
       */
      private String m_sValue;

      /**
       * Constructor.
       */
      public Variable()
      {
      }

      /**
       * Set the name.
       *
       * @param sName The name.
       */
      public void setName(String sName)
      {
         m_sName = sName;
      }

      /**
       * Set the value.
       *
       * @param sValue The value.
       */
      public void setValue(String sValue)
      {
         m_sValue = sValue;
      }

      /**
       * Set the value to a location.
       *
       * @param location The location.
       */
      public void setLocation(File location)
      {
         m_sValue = location.getPath();
      }

      /**
       * Get the define parameter used by Candle.
       *
       * @return The define parameter.
       */
      public String getDefineParameter()
      {
         return "-d" + m_sName + "=\"" + m_sValue + "\"";
      }
   }

   /**
    * Ant type for building a bootstrapper that includes Microsoft Office Primary Interop Assemblies.
    */
   public class InteropBootstrapper
   {
      /**
       * The name of the .nsi file.
       */
      private String m_sNsiFileName;

      /**
       * The name of the setup.
       */
      private String m_sSetupName;

      /**
       * The icon file.
       */
      private File m_iconFile;

      /**
       * The directory where the interops are found.
       */
      private File m_interopDirectory;

      /**
       * The install directory of NSIS.
       */
      private File m_nsisDirectory;

      /**
       * The version of EXE installer.
       */
      private String m_sInstallVersion;

      /**
       * The version of installed product.
       */
      private String m_sProductVersion;

      /**
       * Constructor.
       */
      public InteropBootstrapper()
      {
      }

      /**
       * Set the NSI file's name.
       *
       * @param sNsiFileName The NSI file's name.
       */
      public void setNsiFile(String sNsiFileName)
      {
         m_sNsiFileName = sNsiFileName;
      }

      /**
       * Set the setup name.
       *
       * @param sSetupName The setup name.
       */
      public void setSetupName(String sSetupName)
      {
         m_sSetupName = sSetupName;
      }

      /**
       * Set the icon file.
       *
       * @param iconFile The icon file.
       */
      public void setIcon(File iconFile)
      {
         m_iconFile = iconFile;
      }

      /**
       * Set the interop directory.
       *
       * @param interopDirectory The interop directory.
       */
      public void setInteropDir(File interopDirectory)
      {
         m_interopDirectory = interopDirectory;
      }

      /**
       * Set the NSIS directory.
       * @param nsisDirectory The NSIS directory.
       */
      public void setNsisDir(File nsisDirectory)
      {
         m_nsisDirectory = nsisDirectory;
      }

      /**
       * Set the version of the installer.
       * @param sInstallVersion The version of the Installer.
       */
      public void setInstallVersion(String sInstallVersion)
      {
         m_sInstallVersion = sInstallVersion;
      }

      /**
       * Set the version of the product.
       * @param sInstallerVersion The version of the Product.
       */
      public void setProductVersion(String sProductVersion)
      {
         m_sProductVersion = sProductVersion;
      }

      /**
       * Run MakeNSIS to make the interop bookstrapper.
       *
       * @param sourceDirectory The source directory where the NSI file is.
       * @param destinationDirectory The destination directory.
       * @param sSetupFileBaseName The base name of the setup file.
       */
      public void makeBootstrapper(File sourceDirectory, File destinationDirectory, String sSetupFileBaseName)
      {
         int nExitValue;

         try
         {
            ArrayList argList = new ArrayList();

            // Set the exe to run
            argList.add(new File(m_nsisDirectory, "makensis.exe").getAbsolutePath());

            // Set up NSIS defines
            argList.add("/DSETUPNAME=" + m_sSetupName);
            argList.add("/DICON=" + m_iconFile.getAbsolutePath());
            argList.add("/DDESTDIR=" + m_destinationDirectory.getAbsolutePath());
            argList.add("/DNEXJEXE=" + sSetupFileBaseName + ".exe");
            argList.add("/DNEXJMSI=" + sSetupFileBaseName + ".msi");
            argList.add("/DINTEROPDIR=" + m_interopDirectory.getAbsolutePath());
            argList.add("/DINSTALLVERSION=" + m_sInstallVersion);
            argList.add("/DPRODUCTVERSION=" + m_sProductVersion);

            // Don't print the script
            argList.add("/V2");

            // Set the NSI input file
            argList.add(new File(sourceDirectory, m_sNsiFileName).getPath());

            // Run MakeNSIS, create the bootstrapper
            Execute makensis = new Execute();
            makensis.setAntRun(getProject());
            makensis.setCommandline((String[])argList.toArray(new String[0]));
            makensis.execute();

            // Check if successful
            nExitValue = makensis.getExitValue();
         }
         catch (Exception e)
         {
            e.printStackTrace();
            throw new BuildException("Fatal exception occurred while running bootstrap compiler MakeNSIS", e);
         }

         if (nExitValue != 0)
         {
            log("MakeNSIS failed (exit value " + nExitValue + ")", ERROR_LEVEL);
            throw new BuildException("Bootstrap compiler MakeNSIS failed");
         }
      }
   }

   /**
    * Creates IDs for files, directories, components.
    */
   private static class IdentifierFactory
   {
      /**
       * Pattern for matching bad Wix ID characters.
       */
      private final static Pattern BAD_ID_CHAR_PATTERN = Pattern.compile("[^A-Za-z0-9._]");

      /**
       * Namespace to use for hashing (incorporates the prefix).
       */
      private byte[] m_guidNamespace = GUID_NAMESPACE;

      /**
       * Preallocated output buffer, max ID length is 72 characters.
       */
      private StringBuffer m_idBuffer = new StringBuffer(72);

      /**
       * Constructor.
       */
      public IdentifierFactory()
      {
      }

      /**
       * Set the prefix directory.
       *
       * @param sPrefixDir The prefix directory.
       */
      public void setPrefixDir(String sPrefixDir)
      {
         m_guidNamespace = (sPrefixDir == null) ? GUID_NAMESPACE : getEncodedData(SysUtil.NAMESPACE + ":installer:" + sPrefixDir + File.separator);
      }

      /**
       * Get a unique ID based on a path. Constructed by appending a hash of the
       * install path to the file or folder's name. All ID's must contain only
       * alphanumeric characters, periods, or underscores and must either start
       * with a letter or an underscore.
       *
       * @param sPath The path which the ID is keyed by.
       * @return The unique ID.
       */
      public String getUniqueId(String sPath)
      {
         // Clear the buffer
         m_idBuffer.delete(0, m_idBuffer.length());

         // Prefix the name and the hash with an underscore for consistency
         m_idBuffer.append('_');

         // Get the name
         CharSequence truncatedName = new File(sPath).getName();

         // Make sure it's not too long
         if (truncatedName.length() > 34)
         {
            truncatedName = CharBuffer.wrap(truncatedName, 0, 34);
         }

         // Append it to the ID, replacing bad characters in the process
         Matcher badCharMatcher = BAD_ID_CHAR_PATTERN.matcher(truncatedName);

         while (badCharMatcher.find())
         {
            badCharMatcher.appendReplacement(m_idBuffer, "_");
         }

         badCharMatcher.appendTail(m_idBuffer);

         // Separate the name and the hash
         m_idBuffer.append('_');

         // Append the hash (in hex) of the full install path
         appendHash(m_idBuffer, sPath, '_');

         return m_idBuffer.toString();
      }

      /**
       * Get a GUID based on the hash of a path.
       *
       * @param sPath The path by which the GUID is keyed.
       * @return The GUID.
       */
      public String getGUID(String sPath)
      {
         // Get the hash (in hex) of the full install path
         // A GUID is just the hash with dashes inside it
         m_idBuffer.delete(0, m_idBuffer.length());
         appendHash(m_idBuffer, sPath, '-');
         return m_idBuffer.toString();
      }

      /**
       * Calculate a hash of the data prefixed with context data and append it
       * to the buffer with separators inserted.
       *
       * @param buffer The buffer.
       * @param sData The data to hash.
       * @param chSeparator The character to insert as separators.
       */
      private void appendHash(StringBuffer buffer, String sData, char chSeparator)
      {
         // Convert the prefix directory and the current data to binary data
         byte[] nDataArray = getEncodedData(sData);

         // Calculate the hash and convert it to a hexidecimal string
         String sHash = GUIDUtil.generateGUID(m_guidNamespace, nDataArray).toString();

         // Separate the hash string
         buffer.append(sHash, 0, 8).append(chSeparator).append(sHash, 8, 12).append(chSeparator).append(sHash, 12, 16).append(
            chSeparator).append(sHash, 16, 20).append(chSeparator).append(sHash, 20, 32);
      }
   }

}
TOP

Related Classes of nexj.core.build.WiXWriter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.