text: """
  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at
  
      http://www.apache.org/licenses/LICENSE-2.0
  
  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
  """
  
  import optparse
  import os
  import subprocess
  import sys
  import xml.etree.ElementTree as ET
  
  class VersionBuilder:
    """
    Used to build a version definition file
    """
    def __init__(self, filename):
      self._check_xmllint()
  
      self.filename = filename
  
      if os.path.exists(filename):
        tree = ET.ElementTree()
        tree.parse(filename)
        root = tree.getroot()
      else:
        attribs = {}
        attribs['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
        attribs['xsi:noNamespaceSchemaLocation'] = "version_definition.xsd"
        root = ET.Element("repository-version", attribs)
  
        ET.SubElement(root, "release")
        ET.SubElement(root, "manifest")
        ET.SubElement(root, "available-services")
        ET.SubElement(root, "repository-info")
  
      self.root_element = root
  
  
    def persist(self):
      """
      Saves the XML file
      """
      p = subprocess.Popen(['xmllint', '--format', '--output', self.filename, '-'], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
      (stdout, stderr) = p.communicate(input=ET.tostring(self.root_element))
  
    def finalize(self, xsd_file):
      """
      Validates the XML file against the XSD
      """
      args = ['xmllint', '--noout', '--load-trace', '--schema', xsd_file, self.filename]
  
      p = subprocess.Popen(args, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
      (stdout, stderr) = p.communicate()
  
      if p.returncode != 0:
        raise Exception(stderr)
  
      if len(stdout) > 0:
        print(stdout.decode("UTF-8"))
  
      if len(stderr) > 0:
        print(stderr.decode("UTF-8"))
  
    def set_release(self, type=None, stack=None, version=None, build=None, notes=None, display=None,
      compatible=None):
      """
      Create elements of the 'release' parent
      """
      release_element = self.root_element.find("./release")
  
      if release_element is None:
        raise Exception("Element 'release' is not found")
  
      if type:
        update_simple(release_element, "type", type)
  
      if stack:
        update_simple(release_element, "stack-id", stack)
  
      if version:
        update_simple(release_element, "version", version)
  
      if build:
        update_simple(release_element, "build", build)
  
      if compatible:
        update_simple(release_element, "compatible-with", compatible)
  
      if notes:
        update_simple(release_element, "release-notes", notes)
  
      if display:
        update_simple(release_element, "display", display)
  
    def set_os(self, os_family, package_version=None):
      repo_parent = self.root_element.find("./repository-info")
      if repo_parent is None:
        raise Exception("'repository-info' element is not found")
  
      os_element = self.findByAttributeValue(repo_parent, "./os", "family", os_family)
      if os_element is None:
        os_element = ET.SubElement(repo_parent, 'os')
        os_element.set('family', os_family)
  
      if package_version:
        pv_element = os_element.find("package-version")
        if pv_element is None:
          pv_element = ET.SubElement(os_element, "package-version")
        pv_element.text = package_version
  
  
    def add_manifest(self, id, service_name, version, version_id = None):
      """
      Add a manifest service.  A manifest lists all services in a repo, whether they are to be
      upgraded or not.
      """
      manifest_element = self.root_element.find("./manifest")
  
      if manifest_element is None:
        raise Exception("Element 'manifest' is not found")
  
      service_element = self.findByAttributeValue(manifest_element, "./service", "id", id)
  
      if service_element is None:
        service_element = ET.SubElement(manifest_element, "service")
        service_element.set('id', id)
  
      service_element.set('name', service_name)
      service_element.set('version', version)
      if version_id:
        service_element.set('version-id', version_id)
  
    def add_available(self, manifest_id, available_components=None):
      """
      Adds services available to upgrade for patches
      """
      manifest_element = self.root_element.find("./manifest")
      if manifest_element is None:
        raise Exception("'manifest' element is not found")
  
      service_element = self.findByAttributeValue(manifest_element, "./service", "id", manifest_id)
      if service_element is None:
        raise Exception("Cannot add an available service for {0}; it's not on the manifest".format(manifest_id))
  
      available_element = self.root_element.find("./available-services")
      if available_element is None:
        raise Exception("'available-services' is not found")
  
      service_element = self.findByAttributeValue(available_element, "./service", "idref", manifest_id)
  
      if service_element is not None:
        available_element.remove(service_element)
  
      service_element = ET.SubElement(available_element, "service")
      service_element.set('idref', manifest_id)
  
      if available_components:
        components = available_components.split(',')
        for component in components:
          e = ET.SubElement(service_element, 'component')
          e.text = component
  
    def add_repo(self, os_family, repo_id, repo_name, base_url):
      """
      Adds a repository
      """
      repo_parent = self.root_element.find("./repository-info")
      if repo_parent is None:
        raise Exception("'repository-info' element is not found")
  
      os_element = self.findByAttributeValue(repo_parent, "./os", "family", os_family)
      if os_element is None:
        os_element = ET.SubElement(repo_parent, 'os')
        os_element.set('family', os_family)
  
      if self.useNewSyntax():
        repo_element = os_element.find("./repo/[reponame='{0}']".format(repo_name))
      else:
        repo_element = self.findByValue(os_element, "./repo/reponame", repo_name)
  
      if repo_element is not None:
        os_element.remove(repo_element)
  
      repo_element = ET.SubElement(os_element, 'repo')
      e = ET.SubElement(repo_element, 'baseurl')
      e.text = base_url
  
      e = ET.SubElement(repo_element, 'repoid')
      e.text = repo_id
  
      e = ET.SubElement(repo_element, 'reponame')
      e.text = repo_name
  
  
    def _check_xmllint(self):
      """
      Verifies utility xmllint is available
      """
      try:
        p = subprocess.Popen(['xmllint', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
        (stdout, stderr) = p.communicate()
  
        if p.returncode != 0:
          raise Exception("xmllint command does not appear to be available")
  
      except:
        raise Exception("xmllint command does not appear to be available")
  
    def findByAttributeValue(self, root, element, attribute, value):
      if self.useNewSyntax():
        return root.find("./{0}[@{1}='{2}']".format(element, attribute, value))
      else:
        for node in root.findall("{0}".format(element)):
          if node.attrib[attribute] == value:
            return node
        return None;
    
    def findByValue(self, root, element, value):
      for node in root.findall("{0}".format(element)):
        if node.text == value:
          return node
      return None
  
    def useNewSyntax(self):
       #Python2.7 and newer shipps with ElementTree that supports a different syntax for XPath queries
       major=sys.version_info[0]
       minor=sys.version_info[1]
       if major > 3 :
         return True
       elif major == 2:
         return (minor > 6)
       else:
         return False;
  
  def update_simple(parent, name, value):
    """
    Helper method to either update or create the element
    """
    element = parent.find('./' + name) 
  
    if element is None:
      element = ET.SubElement(parent, name)
      element.text = value
    else:
      element.text = value
  
  def process_release(vb, options):
    """
    Create elements of the 'release' parent
    """
    if options.release_type:
      vb.set_release(type=options.release_type)
  
    if options.release_stack:
      vb.set_release(stack=options.release_stack)
  
    if options.release_version:
      vb.set_release(version=options.release_version)
  
    if options.release_build:
      vb.set_release(build=options.release_build)
  
    if options.release_compatible:
      vb.set_release(compatible=options.release_compatible)
  
    if options.release_notes:
      vb.set_release(notes=options.release_notes)
  
    if options.release_display:
      vb.set_release(display=options.release_display)
  
    if options.release_package_version:
      vb.set_release(package_version=options.release_package_version)
  
  def process_manifest(vb, options):
    """
    Creates the manifest element
    """
    if not options.manifest:
      return
  
    vb.add_manifest(options.manifest_id, options.manifest_service, options.manifest_version, options.manifest_version_id)
  
  def process_available(vb, options):
    """
    Processes available service elements
    """
    if not options.available:
      return
  
    vb.add_available(options.manifest_id, options.available_components)
  
  
  def process_os(vb, options):
    if not options.os:
      return
  
    vb.set_os(options.os_family, options.os_package_version)
  
  def process_repo(vb, options):
    """
    Processes repository options.  This method doesn't update or create individual elements, it
    creates the entire repo structure
    """
    if not options.repo:
      return
  
    vb.add_repo(options.repo_os, options.repo_id, options.repo_name, options.repo_url)
  
  def validate_manifest(parser, options):
    """
    Validates manifest options from the command line
    """
    if not options.manifest:
      return
  
    template = "When specifying --manifest, {0} is also required"
  
    if not options.manifest_id:
      parser.error(template.format("--manifest-id"))
    
    if not options.manifest_service:
      parser.error(template.format("--manifest-service"))
  
    if not options.manifest_version:
      parser.error(template.format("--manifest-version"))
  
  def validate_available(parser, options):
    """
    Validates available service options from the command line
    """
    if not options.available:
      return
  
    if not options.manifest_id:
      parser.error("When specifying --available, --manifest-id is also required")
  
  def validate_os(parser, options):
    if not options.os:
      return
  
    if not options.os_family:
      parser.error("When specifying --os, --os-family is also required")
  
  def validate_repo(parser, options):
    """
    Validates repo options from the command line
    """
    if not options.repo:
      return
  
    template = "When specifying --repo, {0} is also required"
  
    if not options.repo_os:
      parser.error(template.format("--repo-os"))
  
    if not options.repo_url:
      parser.error(template.format("--repo-url"))
  
    if not options.repo_id:
      parser.error(template.format("--repo-id"))
  
    if not options.repo_name:
      parser.error(template.format("--repo-name"))
  
  
  def main(argv):
    parser = optparse.OptionParser(
      epilog="OS utility 'xmllint' is required for this tool to function.  It handles pretty-printing and XSD validation.")
    
    parser.add_option('--file', dest='filename',
      help="The output XML file")
  
    parser.add_option('--finalize', action='store_true', dest='finalize',
      help="Finalize and validate the XML file")
    parser.add_option('--xsd', dest='xsd_file',
      help="The XSD location when finalizing")
  
    parser.add_option('--release-type', type='choice', choices=['STANDARD', 'PATCH'], dest='release_type' ,
      help="Indicate the release type: i.e. STANDARD or PATCH")
    parser.add_option('--release-stack', dest='release_stack',
      help="The stack id: e.g. HDP-2.4")
    parser.add_option('--release-version', dest='release_version',
      help="The release version without build number: e.g. 2.4.0.1")
    parser.add_option('--release-build', dest='release_build',
      help="The release build number: e.g. 1234")
    parser.add_option('--release-compatible', dest='release_compatible',
      help="Regular Expression string to identify version compatibility for patches: e.g. 2.4.1.[0-9]")
    parser.add_option('--release-notes', dest='release_notes',
      help="A http link to the documentation notes")
    parser.add_option('--release-display', dest='release_display',
      help="The display name for this release")
    parser.add_option('--release-package-version', dest='release_package_version',
      help="Identifier to use when installing packages, generally a part of the package name")
  
    parser.add_option('--manifest', action='store_true', dest='manifest',
      help="Add a manifest service with other options: --manifest-id, --manifest-service, --manifest-version, --manifest-version-id")
    parser.add_option('--manifest-id', dest='manifest_id',
      help="Unique ID for a service in a manifest.  Required when specifying --manifest and --available")
    parser.add_option('--manifest-service', dest='manifest_service')
    parser.add_option('--manifest-version', dest='manifest_version')
    parser.add_option('--manifest-version-id', dest='manifest_version_id')
  
    parser.add_option('--available', action='store_true', dest='available',
      help="Add an available service with other options: --manifest-id, --available-components")
    parser.add_option('--available-components', dest='available_components',
      help="A CSV of service components that are intended to be upgraded via patch. \
        Omitting this implies the entire service should be upgraded")
  
    parser.add_option('--os', action='store_true', dest='os', help="Add OS data with options --os-family, --os-package-version")
    parser.add_option('--os-family', dest='os_family', help="The operating system: i.e redhat7, debian7, ubuntu12, ubuntu14, suse11, suse12")
    parser.add_option('--os-package-version', dest='os_package_version',
      help="The package version to use for the OS")
  
    parser.add_option('--repo', action='store_true', dest='repo',
      help="Add repository data with options: --repo-os, --repo-url, --repo-id, --repo-name")
    parser.add_option('--repo-os', dest='repo_os',
      help="The operating system type: i.e. redhat6, redhat7, debian7, ubuntu12, ubuntu14, ubuntu16, suse11, suse12")
    parser.add_option('--repo-url', dest='repo_url',
      help="The base url for the repository data")
    parser.add_option('--repo-id', dest='repo_id', help="The ID of the repo")
    parser.add_option('--repo-name', dest='repo_name', help="The name of the repo")
  
    (options, args) = parser.parse_args()
  
    # validate_filename
    if not options.filename:
      parser.error("--file option is required")
  
    # validate_finalize
    if options.finalize and not options.xsd_file:
      parser.error("Must supply XSD (--xsd) when finalizing")
  
    validate_manifest(parser, options)
    validate_available(parser, options)
    validate_os(parser, options)
    validate_repo(parser, options)
  
    vb = VersionBuilder(options.filename)
  
    process_release(vb, options)
    process_manifest(vb, options)
    process_available(vb, options)
    process_os(vb, options)
    process_repo(vb, options)
  
    # save file
    vb.persist()
  
    if options.finalize:
      vb.finalize(options.xsd_file)
  
  if __name__ == "__main__":
    main(sys.argv)
  