// Copyright Keysight Technologies 2012-2019
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at http://mozilla.org/MPL/2.0/.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;
using OpenTap.Cli;
namespace OpenTap.Package
{
///
/// TapSerializerPlugin for
///
internal class PackageDefinitionSerializerPlugin : TapSerializerPlugin
{
///
/// Called as part for the deserialization chain. Returns false if it cannot serialize the XML element.
///
public override bool Deserialize(XElement node, ITypeData t, Action setter)
{
if((node.Name.LocalName == "Package" && t.IsA(typeof(PackageDef))) ||
(node.Name.LocalName == nameof(PackageIdentifier) && t.IsA(typeof(PackageIdentifier))))
{
var pkg = new PackageDef();
foreach (XAttribute attr in node.Attributes())
{
switch(attr.Name.LocalName)
{
case "Version":
pkg.RawVersion = attr.Value;
if (SemanticVersion.TryParse(attr.Value, out var semver))
pkg.Version = semver;
break;
case "Date":
if (DateTime.TryParse(attr.Value, out DateTime date))
pkg.Date = date;
else
pkg.Date = DateTime.MinValue;
break;
case "Architecture":
pkg.Architecture = (CpuArchitecture)Enum.Parse(typeof(CpuArchitecture), attr.Value);
break;
default:
var prop = pkg.GetType().GetProperty(attr.Name.LocalName);
if (prop != null)
prop.SetValue(pkg, attr.Value);
break;
}
}
foreach (var elm in node.Elements())
{
switch (elm.Name.LocalName)
{
case "Description":
pkg.Description = Regex.Match(elm.ToString().Replace("\r", ""), "^((?:.|\\n)+)", RegexOptions.Multiline).Groups[1].Value.Trim();
break;
case "PackageRepositoryUrl":
#pragma warning disable 618
pkg.Location = elm.Value;
#pragma warning restore 618
break;
case "Validation":
foreach (var e in elm.Elements())
{
Validation marker = null;
if (e.Name.LocalName == "FileExists")
{
marker = new FileExists()
{
Path = e.Attribute("Path").Value
};
}
if (marker != null)
{
if (pkg.Validation == null)
pkg.Validation = new();
pkg.Validation.Add(marker);
}
}
break;
default:
var prop = pkg.GetType().GetProperty(elm.Name.LocalName);
if (prop != null)
Serializer.Deserialize(elm, o => prop.SetValue(pkg,o), prop.PropertyType);
else // If the property does not exist on the packagedef type, it is MetaData
pkg.MetaData.Add(elm.Name.LocalName, elm.Value);
break;
}
}
if (t.IsA(typeof(PackageIdentifier)))
setter.Invoke(new PackageIdentifier(pkg));
else
setter.Invoke(pkg);
// If the Version XML attribute is missing, default to same behavior as if Version="" was specified. We depend on packages having a version.
if (pkg.Version is null && string.IsNullOrEmpty(pkg.RawVersion)) {
pkg.RawVersion = "";
if (SemanticVersion.TryParse("0.0.0", out var semver))
pkg.Version = semver;
}
return true;
}
return false;
}
///
/// Called as part for the serialization chain. Returns false if it cannot serialize the XML element.
/// Disables the 'WritePackageDependencies' feature of the
/// further up the chain when it runs.
///
public override bool Serialize(XElement node, object obj, ITypeData expectedType)
{
if (expectedType.IsA(typeof(PackageDef)) == false && expectedType.IsA(typeof(PackageIdentifier)) == false)
return false;
XNamespace ns = "http://opentap.io/schemas/package";
node.Name = ns + (expectedType.IsA(typeof(PackageIdentifier)) ? nameof(PackageIdentifier) : "Package");
node.SetAttributeValue("type", null);
foreach (var prop in expectedType.GetMembers())
{
object val = prop.GetValue(obj);
if (false == val is string && val is IEnumerable && (val as IEnumerable).GetEnumerator().MoveNext() == false)
continue; // don't write empty enumerables
var defaultAttr = prop.GetAttribute();
if (defaultAttr != null && object.Equals(defaultAttr.Value, val))
continue;
switch (prop.Name)
{
case "RawVersion":
continue;
case "Date":
if(((DateTime)val) != DateTime.MinValue)
node.SetAttributeValue("Date", ((DateTime)val).ToString(CultureInfo.InvariantCulture));
break;
case "Description":
var mngr = new XmlNamespaceManager(new NameTable());
mngr.AddNamespace("", ns.NamespaceName); // or proper URL
var parserContext = new XmlParserContext(null, mngr, null, XmlSpace.None, null);
var txtReader = new XmlTextReader($"{val} ", XmlNodeType.Element, parserContext);
var ele = XElement.Load(txtReader);
node.Add(ele);
break;
case nameof(PackageDef.MetaData):
var metadata = val as Dictionary;
var packageProperties = typeof(PackageDef).GetProperties();
foreach (var key in metadata.Keys)
{
if (packageProperties.Any(p => p.Name == key))
throw new ExitCodeException((int)PackageExitCodes.PackageCreateError, $"PackageDef property '{key}' cannot be overridden by metadata.");
var element = new XElement(key, metadata[key]);
SetAllNamespaces(element, ns);
node.Add(element);
}
break;
default:
var xmlAttr = prop.GetAttributes().FirstOrDefault();
if (xmlAttr != null)
{
string name = prop.Name;
if (!String.IsNullOrWhiteSpace(xmlAttr.AttributeName))
name = xmlAttr.AttributeName;
node.SetAttributeValue(name, val);
}
else
{
var elm = new XElement(prop.Name);
if (obj != null)
{
Serializer.Serialize(elm, val, expectedType: prop.TypeDescriptor);
if (prop.Name == "Validation")
{
if (val == null)
continue;
foreach (var elem in elm.Elements())
{
elem.Attribute("type").Remove();
}
}
}
SetAllNamespaces(elm, ns);
node.Add(elm);
}
break;
}
}
node.SetAttributeValue("xmlns", ns);
// ask the TestPlanPackageDependency serializer (the one that writes the
// tag in the bottom of e.g. TestPlan files) to
// not write the tag for this file.
var depSerializer = Serializer.GetSerializer();
if (depSerializer != null)
depSerializer.WritePackageDependencies = false;
return true;
}
void SetAllNamespaces(XElement e, XNamespace ns)
{
e.Name = ns + e.Name.LocalName;
foreach (var n in e.Elements())
SetAllNamespaces(n, ns);
}
}
///
/// TapSerializerPlugin for
///
internal class PackageDependencySerializerPlugin : OpenTap.TapSerializerPlugin
{
///
/// Called as part for the deserialization chain. Returns false if it cannot serialize the XML element.
///
public override bool Deserialize(XElement node, ITypeData t, Action setter)
{
if (t.IsA(typeof(PackageDependency)))
{
string name = null;
string version = null;
foreach (XAttribute attr in node.Attributes())
{
switch (attr.Name.LocalName)
{
case "Version":
version = attr.Value;
break;
case "Package": // TAP 8.0 support
case "Name":
name = attr.Value;
break;
default:
Debug.Assert(false);
break;
}
}
setter.Invoke(new PackageDependency(name, ConvertVersion(version), version));
return true;
}
return false;
}
private static VersionSpecifier ConvertVersion(string Version)
{
if (String.IsNullOrWhiteSpace(Version))
return VersionSpecifier.Any;
if (VersionSpecifier.TryParse(Version, out var semver))
{
return semver;
}
// For compatability (pre 9.0 packages may not have correctly formatted version numbers)
var plugins = PluginManager.GetPlugins().Concat(PluginManager.GetPlugins());
foreach (var plugin in plugins.OrderBy(p => p.GetDisplayAttribute().Order))
{
try
{
object cvt = Activator.CreateInstance(plugin);
if (cvt is IVersionTryConverter vc2)
{
if (vc2.TryConvert(Version, out SemanticVersion sv))
return new VersionSpecifier(sv, VersionMatchBehavior.Compatible);
}
else if(cvt is IVersionConverter vc)
{
return new VersionSpecifier(vc.Convert(Version), VersionMatchBehavior.Compatible);
}
}
catch
{
}
}
return VersionSpecifier.Any;
}
///
/// Called as part for the serialization chain. Returns false if it cannot serialize the XML element.
///
public override bool Serialize(XElement node, object obj, ITypeData expectedType)
{
if (expectedType.IsA(typeof(PackageDependency)) == false)
return false;
node.Name = "Package";
node.Name = "PackageDependency"; // TODO: remove when server is updated (this is only here for support of the TAP 8.x Repository server that does not yet have a parser that can handle the new name)
node.SetAttributeValue("type", null);
foreach (var prop in expectedType.GetMembers())
{
object val = prop.GetValue(obj);
string name = prop.Name;
if (val == null)
continue;
if (name == "Name")
name = "Package"; // TODO: remove when server is updated (this is only here for support of the TAP 8.x Repository server that does not yet have a parser that can handle the new name)
node.SetAttributeValue(name, val);
}
return true;
}
}
}