// 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; } } }