AppstreamCollection.fromXml constructor

AppstreamCollection.fromXml(
  1. String xml
)

Decodes an Appstream collection in XML format.

Implementation

factory AppstreamCollection.fromXml(String xml) {
  var document = XmlDocument.parse(xml);

  var root = document.getElement('components');
  if (root == null) {
    throw FormatException("XML document doesn't contain components tag");
  }

  var version = root.getAttribute('version');
  if (version == null) {
    throw FormatException('Missing AppStream version');
  }
  var origin = root.getAttribute('origin');
  if (origin == null) {
    throw FormatException('Missing repository origin');
  }
  var architecture = root.getAttribute('architecture');

  var components = <AppstreamComponent>[];
  for (var component in root.children
      .whereType<XmlElement>()
      .where((e) => e.name.local == 'component')) {
    var typeName = component.getAttribute('type');
    if (typeName == null) {
      throw FormatException('Missing component type');
    }
    var type = _parseComponentType(typeName);

    var id = component.getElement('id');
    if (id == null) {
      throw FormatException('Missing component ID');
    }
    var package = component.getElement('pkgname');
    if (package == null) {
      throw FormatException('Missing component package');
    }
    var name = _getXmlTranslatedString(component, 'name');
    var summary = _getXmlTranslatedString(component, 'summary');
    var description = _getXmlTranslatedString(component, 'description');
    var developerName = _getXmlTranslatedString(component, 'developer_name');
    var projectLicense = component.getElement('project_license')?.text;
    var projectGroup = component.getElement('project_group')?.text;

    var elements = component.children.whereType<XmlElement>();

    var icons = <AppstreamIcon>[];
    for (var icon in elements.where((e) => e.name.local == 'icon')) {
      var type = icon.getAttribute('type');
      if (type == null) {
        throw FormatException('Missing icon type');
      }
      var w = icon.getAttribute('width');
      var width = w != null ? int.parse(w) : null;
      var h = icon.getAttribute('height');
      var height = h != null ? int.parse(h) : null;
      switch (type) {
        case 'stock':
          icons.add(AppstreamStockIcon(icon.text));
          break;
        case 'cached':
          icons.add(
              AppstreamCachedIcon(icon.text, width: width, height: height));
          break;
        case 'local':
          icons.add(
              AppstreamLocalIcon(icon.text, width: width, height: height));
          break;
        case 'remote':
          icons.add(
              AppstreamRemoteIcon(icon.text, width: width, height: height));
          break;
      }
    }

    var urls = <AppstreamUrl>[];
    for (var url in elements.where((e) => e.name.local == 'url')) {
      var typeName = url.getAttribute('type');
      if (typeName == null) {
        throw FormatException('Missing Url type');
      }
      urls.add(AppstreamUrl(url.text, type: _parseUrlType(typeName)));
    }

    var launchables = <AppstreamLaunchable>[];
    for (var launchable
        in elements.where((e) => e.name.local == 'launchable')) {
      switch (launchable.getAttribute('type')) {
        case 'desktop-id':
          launchables.add(AppstreamLaunchableDesktopId(launchable.text));
          break;
        case 'service':
          launchables.add(AppstreamLaunchableService(launchable.text));
          break;
        case 'cockpit-manifest':
          launchables
              .add(AppstreamLaunchableCockpitManifest(launchable.text));
          break;
        case 'url':
          launchables.add(AppstreamLaunchableUrl(launchable.text));
          break;
      }
    }

    var categories = <String>[];
    var categoriesElement = component.getElement('categories');
    if (categoriesElement != null) {
      categories = categoriesElement.children
          .whereType<XmlElement>()
          .where((e) => e.name.local == 'category')
          .map((e) => e.text)
          .toList();
    }

    var keywords = <String, List<String>>{};
    for (var keywordsElement
        in elements.where((e) => e.name.local == 'keywords')) {
      var lang = keywordsElement.getAttribute('xml:lang') ?? 'C';
      keywords[lang] = keywordsElement.children
          .whereType<XmlElement>()
          .where((e) => e.name.local == 'keyword')
          .map((e) => e.text)
          .toList();
    }

    var screenshots = <AppstreamScreenshot>[];
    for (var screenshot
        in elements.where((e) => e.name.local == 'screenshot')) {
      var isDefault = screenshot.getAttribute('type') == 'default';
      var caption = _getXmlTranslatedString(screenshot, 'caption');
      var images = <AppstreamImage>[];
      for (var imageElement in screenshot.children
          .whereType<XmlElement>()
          .where((e) => e.name.local == 'image')) {
        var typeName = imageElement.getAttribute('type');
        if (typeName == null) {
          throw FormatException('Missing image type');
        }
        var type = {
          'source': AppstreamImageType.source,
          'thumbnail': AppstreamImageType.thumbnail
        }[typeName];
        if (type == null) {
          throw FormatException('Unknown image type');
        }
        var w = imageElement.getAttribute('width');
        var width = w != null ? int.parse(w) : null;
        var h = imageElement.getAttribute('height');
        var height = h != null ? int.parse(h) : null;
        var lang = imageElement.getAttribute('xml:lang');
        images.add(AppstreamImage(
            type: type,
            url: imageElement.text,
            width: width,
            height: height,
            lang: lang));
      }
      screenshots.add(AppstreamScreenshot(
          images: images, caption: caption, isDefault: isDefault));
    }

    var compulsoryForDesktops = elements
        .where((e) => e.name.local == 'compulsory_for_desktop')
        .map((e) => e.text)
        .toList();

    var releases = <AppstreamRelease>[];
    var releasesElement = component.getElement('releases');
    if (releasesElement != null) {
      for (var release in releasesElement.children
          .whereType<XmlElement>()
          .where((e) => e.name.local == 'release')) {
        var version = release.getAttribute('version');
        DateTime? date;
        var dateAttribute = release.getAttribute('date');
        var unixTimestamp = release.getAttribute('timestamp');
        if (unixTimestamp != null) {
          date = DateTime.fromMillisecondsSinceEpoch(
              int.parse(unixTimestamp) * 1000,
              isUtc: true);
        } else if (dateAttribute != null) {
          date = DateTime.parse(dateAttribute);
        }
        AppstreamReleaseType? type;
        var typeName = release.getAttribute('type');
        if (typeName != null) {
          type = _parseReleaseType(typeName);
        }
        AppstreamReleaseUrgency? urgency;
        var urgencyName = release.getAttribute('urgency');
        if (urgencyName != null) {
          urgency = _parseReleaseUrgency(urgencyName);
        }
        var description = _getXmlTranslatedString(release, 'description');
        var urlElement = release.getElement('url');
        var url = urlElement?.text;

        var issues = <AppstreamIssue>[];
        var issuesElement = release.getElement('issues');
        if (issuesElement != null) {
          for (var issue in issuesElement.children
              .whereType<XmlElement>()
              .where((e) => e.name.local == 'issue')) {
            AppstreamIssueType? type;
            var typeName = issue.getAttribute('type');
            if (typeName != null) {
              type = _parseIssueType(typeName);
            }
            var url = issue.getAttribute('url');
            issues.add(AppstreamIssue(issue.text,
                type: type ?? AppstreamIssueType.generic, url: url));
          }
        }

        releases.add(AppstreamRelease(
            version: version,
            date: date,
            type: type ?? AppstreamReleaseType.stable,
            urgency: urgency ?? AppstreamReleaseUrgency.medium,
            description: description,
            url: url,
            issues: issues));
      }
    }

    var provides = <AppstreamProvides>[];
    var providesElement = component.getElement('provides');
    if (providesElement != null) {
      for (var element in providesElement.children.whereType<XmlElement>()) {
        switch (element.name.local) {
          case 'mediatype':
            provides.add(AppstreamProvidesMediatype(element.text));
            break;
          case 'library':
            provides.add(AppstreamProvidesLibrary(element.text));
            break;
          case 'binary':
            provides.add(AppstreamProvidesBinary(element.text));
            break;
          case 'font':
            provides.add(AppstreamProvidesFont(element.text));
            break;
          case 'modalias':
            provides.add(AppstreamProvidesModalias(element.text));
            break;
          case 'firmware':
            var typeName = element.getAttribute('type');
            if (typeName == null) {
              throw FormatException('Missing firmware type');
            }
            var type = {
              'runtime': AppstreamFirmwareType.runtime,
              'flashed': AppstreamFirmwareType.flashed
            }[typeName];
            if (type == null) {
              throw FormatException('Unknown firmware type $typeName');
            }
            provides.add(AppstreamProvidesFirmware(type, element.text));
            break;
          case 'python2':
            provides.add(AppstreamProvidesPython2(element.text));
            break;
          case 'python3':
            provides.add(AppstreamProvidesPython3(element.text));
            break;
          case 'dbus':
            var type = element.getAttribute('type');
            if (type == null) {
              throw FormatException('Missing DBus bus type');
            }
            provides.add(
                AppstreamProvidesDBus(_parseDBusType(type), element.text));
            break;
          case 'id':
            provides.add(AppstreamProvidesId(element.text));
            break;
        }
      }
    }

    var languages = <AppstreamLanguage>[];
    var languagesElement = component.getElement('languages');
    if (languagesElement != null) {
      for (var language in languagesElement.children
          .whereType<XmlElement>()
          .where((e) => e.name.local == 'lang')) {
        var percentage = language.getAttribute('percentage');
        languages.add(AppstreamLanguage(language.text,
            percentage: percentage != null ? int.parse(percentage) : null));
      }
    }

    var contentRatings = <String, Map<String, AppstreamContentRating>>{};
    for (var contentRating
        in elements.where((e) => e.name.local == 'content_rating')) {
      var type = contentRating.getAttribute('type');
      if (type == null) {
        throw FormatException('Missing content rating type');
      }
      var ratings = <String, AppstreamContentRating>{};
      for (var contentAttribute in contentRating.children
          .whereType<XmlElement>()
          .where((e) => e.name.local == 'content_attribute')) {
        var id = contentAttribute.getAttribute('id');
        if (id == null) {
          throw FormatException('Missing content attribute id');
        }
        ratings[id] = _parseContentRating(contentAttribute.text);
      }
      contentRatings[type] = ratings;
    }

    components.add(AppstreamComponent(
        id: id.text,
        type: type,
        package: package.text,
        name: name,
        summary: summary,
        description: description,
        developerName: developerName,
        projectLicense: projectLicense,
        projectGroup: projectGroup,
        icons: icons,
        urls: urls,
        launchables: launchables,
        categories: categories,
        keywords: keywords,
        screenshots: screenshots,
        compulsoryForDesktops: compulsoryForDesktops,
        releases: releases,
        provides: provides,
        languages: languages,
        contentRatings: contentRatings));
  }

  return AppstreamCollection(
      version: version,
      origin: origin,
      architecture: architecture,
      components: components);
}