docs: private DownloadItem buildEsa(ProvisioningFeatureDefinition feature, File outputDir, File installRoot, File licenseRoot, String subsystemLicense) throws IOException {
          log("Building ESA for feature " + feature.getSymbolicName(), Project.MSG_VERBOSE);
          File outputFile = new File(outputDir, feature.getSymbolicName() + ".esa");
          if (outputFile.exists()) {
              /*
               * If it's already there it means that it has already been generated (possibly with different settings for things like the applies to). As we are changing how we build
               * ESAs to go through feature_imports.xml this is guaranteed to happen so just exit.
               */
              log(outputFile.getAbsolutePath() + " already exists so skipping ESA generation", Project.MSG_INFO);
              return null;
          }
          int lengthOfInstallRoot = installRoot.getAbsolutePath().length();
          Set<String> absPathsForLibertyContent = new HashSet<String>();
          Set<String> absPathsForLibertyBundles = new HashSet<String>();
          Set<String> absPathsForLibertyLocalizations = new HashSet<String>();
          Set<String> absPathsForLibertyIcons = new HashSet<String>();
          getFilesForFeature(installRoot, absPathsForLibertyContent, absPathsForLibertyBundles, absPathsForLibertyLocalizations, absPathsForLibertyIcons, feature,
                             new ContentBasedLocalBundleRepository(installRoot, null, false));
          log("Files to include are: " + absPathsForLibertyContent, Project.MSG_VERBOSE);
          log("Bundles to include are: " + absPathsForLibertyBundles, Project.MSG_VERBOSE);
          log("Localizations to include are: " + absPathsForLibertyLocalizations, Project.MSG_VERBOSE);
          log("Creating ESA file: " + outputFile.getAbsolutePath(), Project.MSG_VERBOSE);
          ZipOutputStream outputStream = null;
          String appliesTo = null;
          FileOutputStream tempManifestOutputStream = null;
  
          // Initialize the checksums if we have one
          Properties checksumInput = null;
          Properties checksumOutput = null;
          boolean hasChecksum = false;
          File checksumFile = feature.getFeatureChecksumFile();
          log("Reading file from: " + checksumFile.getAbsolutePath(), Project.MSG_VERBOSE);
          if (checksumFile != null && checksumFile.exists()) {
              hasChecksum = true;
              checksumInput = new Properties();
              FileInputStream checksumInputStream = null;
              try {
                  checksumInputStream = new FileInputStream(checksumFile);
                  checksumInput.load(checksumInputStream);
              } finally {
                  if (checksumInputStream != null) {
                      checksumInputStream.close();
                  }
              }
              checksumOutput = new Properties();
          }
          try {
              outputStream = new ZipOutputStream(new FileOutputStream(outputFile));
              outputStream.putNextEntry(new ZipEntry("OSGI-INF/SUBSYSTEM.MF"));
              File manifestFile = feature.getFeatureDefinitionFile();
  
              /*
               * We need to modify the manifest to include things like the applies to and license information. This will mess up the generated checksums though so write it to a temp
               * file so we can then generate the checksum for it
               */
              File tempManifestFile = File.createTempFile(feature.getSymbolicName(), ".mf");
              tempManifestOutputStream = new FileOutputStream(tempManifestFile);
  
              // Extract the current manifest contents so that we can add ESA specific headers
              StringBuilder manifestBuilder = extract(manifestFile);
  
              /*
               * The SUBSYSTEM.MF does not have line length limits (see section 134.2 of the Enterprise OSGi spec) etc so happily add the extra headers we need without worrying about
               * length
               */
              appliesTo = feature.getHeader("IBM-AppliesTo");
              if (appliesTo == null) {
                  appliesTo = getAppliesTo();
                  manifestBuilder.append(NEW_LINE);
                  manifestBuilder.append("IBM-AppliesTo: " + appliesTo);
              }
              if (feature.getHeader("Subsystem-License") == null) {
                  manifestBuilder.append(NEW_LINE);
                  manifestBuilder.append(subsystemLicense);
              }
              if (feature.getHeader("IBM-License-Agreement") == null) {
                  manifestBuilder.append(NEW_LINE);
                  manifestBuilder.append("IBM-License-Agreement: wlp/lafiles/LA");
              }
              if (!specialFeatureTermsApply() && feature.getHeader("IBM-License-Information") == null) {
                  manifestBuilder.append(NEW_LINE);
                  manifestBuilder.append("IBM-License-Information: wlp/lafiles/LI");
              }
              if (this.productFeature && feature.getHeader("IBM-InstallTo") == null) {
                  manifestBuilder.append(NEW_LINE);
                  manifestBuilder.append("IBM-InstallTo: core");
              }
              // Default the vendor to IBM if it is not set
              String vendor = feature.getHeader("Subsystem-Vendor");
              if (vendor == null) {
                  manifestBuilder.append(NEW_LINE);
                  manifestBuilder.append("Subsystem-Vendor: IBM");
              }
  
              // If we have localization files we need a header for it
              String subsystemLocalization = feature.getHeader("Subsystem-Localization");
              if (!absPathsForLibertyLocalizations.isEmpty() && (subsystemLocalization == null || subsystemLocalization.isEmpty())) {
                  // Use default
                  log("The feature " + feature.getSymbolicName() + " has localization files but no subsystem localization header so adding default", Project.MSG_ERR);
                  subsystemLocalization = "OSGI-INF/l10n/subsystem";
                  manifestBuilder.append(NEW_LINE);
                  manifestBuilder.append("Subsystem-Localization: " + subsystemLocalization);
              }
  
              // After all ESA headers have been added, write the new manifest to the temporary file
              tempManifestOutputStream.write(manifestBuilder.toString().getBytes());
  
              // Finished copying to the temp file, close it and copy to the zip
              tempManifestOutputStream.close();
              copy(outputStream, tempManifestFile);
              outputStream.closeEntry();
  
              // Also add the correct checksum entry
              if (hasChecksum) {
                  String manifestMD5String = MD5Utils.getFileMD5String(tempManifestFile);
                  checksumOutput.put("OSGI-INF/SUBSYSTEM.MF", manifestMD5String);
              }
  
              // All done with the temp manifest so delete it
              if (!tempManifestFile.delete()) {
                  tempManifestFile.deleteOnExit();
              }
  
              // Handle normal files, the path matches that in the liberty install
              for (String filePath : absPathsForLibertyContent) {
                  File file = new File(filePath);
                  if (!file.exists()) {
                      // This really should not happen but at the time of writing it did for at least one file (see defect 97157).
                      // As we don't have a validator for invalid manifests this is the only place to catch these errors so fail the
                      // whole build. All of the errors spotted so far are due to the JavaDoc not being generated so help out the
                      // build monitor and say this may be a good place to start looking...
                      throw new BuildException(
                                               "The file "
                                                               + filePath
                                                               + " for feature "
                                                               + feature.getSymbolicName()
                                                               + " is missing.\nAt the time of writting this was always caused by missing JavaDoc bundles where their generation failed due to not having a dependency on the OSGi spec JARs.  If the file reported is a JavaDoc file see 97157 for a possible fix.");
                  }
                  String pathInWlp = filePath.substring(lengthOfInstallRoot).replaceAll(REGEX_BACKSLASH, "/");
                  String entryPath = "wlp" + pathInWlp;
                  outputStream.putNextEntry(new ZipEntry(entryPath));
                  copy(outputStream, file);
                  outputStream.closeEntry();
  
                  // Write out checksum to the path in ESA
                  if (hasChecksum) {
                      // Remove first "/" in path in wlp
                      String checksumEntry = checksumInput.getProperty(pathInWlp.substring(1));
                      if (checksumEntry != null && !checksumEntry.isEmpty()) {
                          checksumOutput.put(entryPath, checksumEntry);
                      } else {
                          log("Unable to find checksum entry for file " + pathInWlp, Project.MSG_WARN);
                      }
                  }
              }
  
              // Bundles all go in the root of the ESAs
              for (String filePath : absPathsForLibertyBundles) {
                  File file = new File(filePath);
                  if (!file.exists()) {
                      /*
                       * This really really should not happen as we resolve bundles to an existing file. Not seen it so far but as above we don't ever validate for this so fail the
                       * build as a useful validation step to make sure that our manifests are valid
                       */
                      throw new BuildException("The bundle at path " + filePath + " for feature " + feature.getSymbolicName() + " is missing");
                  }
                  String entryPath = file.getName();
                  outputStream.putNextEntry(new ZipEntry(entryPath));
                  copy(outputStream, file);
                  outputStream.closeEntry();
  
                  // Write out checksum to the path in ESA
                  if (hasChecksum) {
                      String pathInWlp = filePath.substring(lengthOfInstallRoot + 1).replaceAll(REGEX_BACKSLASH, "/");
                      String checksumEntry = checksumInput.getProperty(pathInWlp);
                      if (checksumEntry != null && !checksumEntry.isEmpty()) {
                          checksumOutput.put(entryPath, checksumEntry);
                      } else {
                          log("Unable to find checksum entry for bundle " + pathInWlp, Project.MSG_WARN);
                      }
                  }
              }
  
              // Localization files go somewhere determined by a header
              for (String filePath : absPathsForLibertyLocalizations) {
                  // Strip the feature name off the front of the file name so we are left with, for example, _fr.properties
                  File file = new File(filePath);
                  String featureName = feature.getSymbolicName();
                  String fileName = file.getName();
                  String fileEnding = fileName.startsWith(featureName) ? fileName.substring(featureName.length()) : fileName;
  
                  // subsystemLocalization is always set as we fall back to a default above
                  String entryPath = subsystemLocalization + fileEnding;
                  outputStream.putNextEntry(new ZipEntry(entryPath));
                  copy(outputStream, file);
                  outputStream.closeEntry();
  
                  // No checksums for localization files
              }
  
              // Always copy in the license files
              for (File licenseFile : licenseRoot.listFiles()) {
                  String entryPath = "wlp/lafiles/" + licenseFile.getName();
                  outputStream.putNextEntry(new ZipEntry(entryPath));
                  copy(outputStream, licenseFile);
                  outputStream.closeEntry();
  
                  // No checksums for license files
              }
  
              // Add in all of the icons
              for (String filePath : absPathsForLibertyIcons) {
                  // Strip the feature name off the front of the file name so we are left with, for example, OSGI-INF/toolIcon.png
                  File file = new File(filePath);
                  String symbolicName = feature.getSymbolicName();
                  String fileRef = filePath.substring(filePath.lastIndexOf(symbolicName) + symbolicName.length() + 1);
                  // If we don't do this any path separators will go into the manifest as backslashes on windows
                  outputStream.putNextEntry(new ZipEntry(fileRef.replace("\\", "/")));
                  copy(outputStream, file);
                  outputStream.closeEntry();
              }
  
              // If there is a checksum file then include it now
              if (hasChecksum) {
                  String entryPath = "OSGI-INF/checksums.cs";
                  outputStream.putNextEntry(new ZipEntry(entryPath));
                  checksumOutput.store(outputStream, null);
                  outputStream.closeEntry();
              }
          } finally {
              if (outputStream != null) {
                  outputStream.close();
              }
              if (tempManifestOutputStream != null) {
                  tempManifestOutputStream.close();
              }
          }
          // Finally create the download item for the XML file, only need to do this for public features
          return getDownloadItem(feature, outputFile, appliesTo);
      }