diff --git a/.gitignore b/.gitignore index 07486cd4..626ce8e8 100644 --- a/.gitignore +++ b/.gitignore @@ -226,4 +226,6 @@ pip-log.txt ## Testing ############# -.docker/data \ No newline at end of file +.docker/data +/PrestaSharp/Entities/boxnow_entry.cs +/PrestaSharp/Factories/BoxnowEntryFactory.cs diff --git a/PrestaSharp.IntegrationTests/PrestaSharp.IntegrationTests.csproj b/PrestaSharp.IntegrationTests/PrestaSharp.IntegrationTests.csproj index 5ba21557..ffed59f9 100644 --- a/PrestaSharp.IntegrationTests/PrestaSharp.IntegrationTests.csproj +++ b/PrestaSharp.IntegrationTests/PrestaSharp.IntegrationTests.csproj @@ -1,7 +1,7 @@ - + - netcoreapp2.1;net452 + net472; false diff --git a/PrestaSharp/Entities/order.cs b/PrestaSharp/Entities/order.cs index 908b95df..4b8951a2 100644 --- a/PrestaSharp/Entities/order.cs +++ b/PrestaSharp/Entities/order.cs @@ -77,6 +77,9 @@ public class order : PrestaShopEntity, IPrestaShopFactoryEntity public decimal total_wrapping_tax_incl { get; set; } public decimal total_wrapping_tax_excl { get; set; } public string shipping_number { get; set; } + + public int? round_mode { get; set; } + public int? round_type { get; set; } public decimal conversion_rate { get; set; } public string reference { get; set; } public AuxEntities.AssociationsOrder associations { get; set; } diff --git a/PrestaSharp/Factories/ProductCustomizationFieldFactory.cs b/PrestaSharp/Factories/ProductCustomizationFieldFactory.cs index a7f8fcbd..dbf11a61 100644 --- a/PrestaSharp/Factories/ProductCustomizationFieldFactory.cs +++ b/PrestaSharp/Factories/ProductCustomizationFieldFactory.cs @@ -7,7 +7,7 @@ namespace Bukimedia.PrestaSharp.Factories { public class ProductCustomizationFieldFactory : GenericFactory { - protected override string singularEntityName { get { return "customization_fields"; } } + protected override string singularEntityName { get { return "customization_field"; } } protected override string pluralEntityName { get { return "product_customization_fields"; } } public ProductCustomizationFieldFactory(string BaseUrl, string Account, string SecretKey) diff --git a/PrestaSharp/Factories/RestSharpFactory.cs b/PrestaSharp/Factories/RestSharpFactory.cs index 3f80049b..da9b91c2 100644 --- a/PrestaSharp/Factories/RestSharpFactory.cs +++ b/PrestaSharp/Factories/RestSharpFactory.cs @@ -3,10 +3,13 @@ using System.IO; using System.Linq; using System.Net; +using System.Net.Http.Headers; +using System.Security.Policy; using System.Threading.Tasks; using System.Xml.Linq; using Bukimedia.PrestaSharp.Entities; using RestSharp; +using RestSharp.Serializers; using RestSharp.Serializers.Xml; namespace Bukimedia.PrestaSharp.Factories @@ -35,6 +38,11 @@ private void AddBody(RestRequest request, IEnumerable entities { request.RequestFormat = DataFormat.Xml; var serialized = string.Empty; + Serializer.XMLSerializer xmlSerializer = new Serializer.XMLSerializer(); + foreach (var entity in entities) + { + serialized += xmlSerializer.Serialize(entity); + } serialized = "\n" + serialized + "\n"; request.AddParameter("application/xml", serialized, ParameterType.RequestBody); } @@ -74,7 +82,8 @@ RestClient GetClient() { var client = new RestClient( options => { options.BaseUrl = new Uri(BaseUrl); }, - configureSerialization: s => s.UseXmlSerializer() + configureSerialization: s => s.UseSerializer(() => new PrestaSharp.Serializer.RestCustomSerializer()) + //configureSerialization: s => s.UseXmlSerializer(null, "prestashop", true) ); return client; } @@ -123,7 +132,7 @@ protected bool ExecuteHead(RestRequest request) protected byte[] ExecuteForImage(RestRequest request) { - var client = new RestClient(); + var client = GetClient(); AddWsKey(request); var response = client.Execute(request); CheckResponse(response, request); diff --git a/PrestaSharp/PrestaSharp.csproj b/PrestaSharp/PrestaSharp.csproj index 767d281a..826b6bba 100644 --- a/PrestaSharp/PrestaSharp.csproj +++ b/PrestaSharp/PrestaSharp.csproj @@ -1,6 +1,6 @@  - netstandard2.0; + net472; Bukimedia.PrestaSharp Bukimedia.PrestaSharp Bukimedia diff --git a/PrestaSharp/Serializer/RestCustomSerializer.cs b/PrestaSharp/Serializer/RestCustomSerializer.cs new file mode 100644 index 00000000..1ee68c3c --- /dev/null +++ b/PrestaSharp/Serializer/RestCustomSerializer.cs @@ -0,0 +1,23 @@ +using Bukimedia.PrestaSharp.Serializer; +using RestSharp; +using RestSharp.Serializers; +using System; + +namespace Bukimedia.PrestaSharp.Serializer +{ + public class RestCustomSerializer : IRestSerializer + { + private XMLSerializer sharpSerializer = new XMLSerializer(); + public string? Serialize(object? obj) => obj == null ? null : sharpSerializer.Serialize(obj); + public string? Serialize(Parameter bodyParameter) => Serialize(bodyParameter.Value); + private XMLDeserializer sharpDeserializer = new XMLDeserializer(); + public T? Deserialize(RestResponse response) => sharpDeserializer.Deserialize(response); + public ContentType ContentType { get; set; } = ContentType.Xml; + public ISerializer Serializer => new XMLSerializer(); + public IDeserializer Deserializer => new XMLDeserializer(); + public DataFormat DataFormat => DataFormat.Xml; + public string[] AcceptedContentTypes => ContentType.XmlAccept; + public SupportsContentType SupportsContentType + => contentType => contentType.Value.EndsWith("xml", StringComparison.InvariantCultureIgnoreCase); + } +} diff --git a/PrestaSharp/Serializer/XMLDeserializer.cs b/PrestaSharp/Serializer/XMLDeserializer.cs new file mode 100644 index 00000000..7f32b39c --- /dev/null +++ b/PrestaSharp/Serializer/XMLDeserializer.cs @@ -0,0 +1,484 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Xml; +using System.Xml.Linq; +using RestSharp; +using RestSharp.Extensions; +using RestSharp.Serializers; +using RestSharp.Serializers.Xml; + +namespace Bukimedia.PrestaSharp.Serializer +{ + public class XMLDeserializer : IDeserializer, IXmlDeserializer, IWithRootElement, IWithDateFormat + { + public CultureInfo Culture { get; set; } = CultureInfo.InvariantCulture; + + public string? RootElement { get; set; } + + public string? Namespace { get; set; } + + public string? DateFormat { get; set; } + + public XMLDeserializer() + { + Culture = CultureInfo.InvariantCulture; + } + + public virtual T Deserialize(RestResponse response) + { + if (string.IsNullOrEmpty(response.Content)) + return default(T); + + XDocument doc = XDocument.Parse(response.Content.Trim()); + XElement root; + + var objType = typeof(T); + XElement firstChild = doc.Root.Descendants().FirstOrDefault(); + + if (doc.Root == null || firstChild?.Name == null) + { + string finalResponseError = "Deserialization problem. Root is null or response has no child."; + if (!string.IsNullOrWhiteSpace(response.ErrorMessage)) + { + finalResponseError += $" Additionnal information is: {response.ErrorMessage}"; + } + throw new PrestaSharpException(response.Content, finalResponseError, response.StatusCode, response.ErrorException); + } + + + // autodetect xml namespace + if (!Namespace.HasValue()) + { + RemoveNamespace(doc); + } + + var x = Activator.CreateInstance(); + bool isSubclassOfRawGeneric = objType.IsSubclassOfRawGeneric(typeof(List<>)); + + if (isSubclassOfRawGeneric) + { + x = (T)HandleListDerivative(x, doc.Root, objType.Name, objType); + } + else + { + root = doc.Root.Element(firstChild.Name.ToString().AsNamespaced(Namespace)); + Map(x, root); + } + + return x; + } + + private void RemoveNamespace(XDocument xdoc) + { + foreach (XElement e in xdoc.Root.DescendantsAndSelf()) + { + if (e.Name.Namespace != XNamespace.None) + { + e.Name = XNamespace.None.GetName(e.Name.LocalName); + } + if (e.Attributes().Any(a => a.IsNamespaceDeclaration || a.Name.Namespace != XNamespace.None)) + { + e.ReplaceAttributes(e.Attributes().Select(a => a.IsNamespaceDeclaration ? null : a.Name.Namespace != XNamespace.None ? new XAttribute(XNamespace.None.GetName(a.Name.LocalName), a.Value) : a)); + } + } + } + + public virtual void Map(object x, XElement root) + { + var objType = x.GetType(); + var props = objType.GetProperties(); + + foreach (var prop in props) + { + var type = prop.PropertyType; + + if (!type.IsPublic || !prop.CanWrite) + continue; + + var name = prop.Name.AsNamespaced(Namespace); + var value = GetValueFromXml(root, name, prop); + + if (value == null) + { + // special case for inline list items + if (type.IsGenericType) + { + var genericType = type.GetGenericArguments()[0]; + var first = GetElementByName(root, genericType.Name); + var list = (IList)Activator.CreateInstance(type); + + if (first != null) + { + var elements = root.Elements(first.Name); + PopulateListFromElements(genericType, elements, list); + } + + prop.SetValue(x, list, null); + } + continue; + } + + // check for nullable and extract underlying type + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + // if the value is empty, set the property to null... + if (value == null || String.IsNullOrEmpty(value.ToString())) + { + prop.SetValue(x, null, null); + continue; + } + type = type.GetGenericArguments()[0]; + } + + if (type == typeof(bool)) + { + var toConvert = value.ToString().ToLowerInvariant(); + prop.SetValue(x, XmlConvert.ToBoolean(toConvert), null); + } + else if (type.IsPrimitive) + { + if (!String.IsNullOrEmpty(value.ToString())) + { + prop.SetValue(x, System.Convert.ChangeType(value, type, Culture), null); + } + } + else if (type.IsEnum) + { + var converted = type.FindEnumValue(value.ToString(), Culture); + prop.SetValue(x, converted, null); + } + else if (type == typeof(Uri)) + { + var uri = new Uri(value.ToString(), UriKind.RelativeOrAbsolute); + prop.SetValue(x, uri, null); + } + else if (type == typeof(string)) + { + prop.SetValue(x, value, null); + } + else if (type == typeof(DateTime)) + { + if (DateFormat.HasValue()) + { + value = DateTime.ParseExact(value.ToString(), DateFormat, Culture); + } + else + { + value = DateTime.Parse(value.ToString(), Culture); + } + + prop.SetValue(x, value, null); + } + else if (type == typeof(DateTimeOffset)) + { + var toConvert = value.ToString(); + if (!string.IsNullOrEmpty(toConvert)) + { + DateTimeOffset deserialisedValue; + try + { + deserialisedValue = XmlConvert.ToDateTimeOffset(toConvert); + prop.SetValue(x, deserialisedValue, null); + } + catch (Exception) + { + object result; + if (TryGetFromString(toConvert, out result, type)) + { + prop.SetValue(x, result, null); + } + else + { + //fallback to parse + deserialisedValue = DateTimeOffset.Parse(toConvert); + prop.SetValue(x, deserialisedValue, null); + } + } + } + } + else if (type == typeof(Decimal)) + { + //Hack for non defined price + if (value.Equals("")) + { + prop.SetValue(x, 0.0m, null); + } + else + { + value = Decimal.Parse(value.ToString(), Culture); + prop.SetValue(x, value, null); + } + } + else if (type == typeof(Guid)) + { + var raw = value.ToString(); + value = string.IsNullOrEmpty(raw) ? Guid.Empty : new Guid(value.ToString()); + prop.SetValue(x, value, null); + } + else if (type == typeof(TimeSpan)) + { + var timeSpan = XmlConvert.ToTimeSpan(value.ToString()); + prop.SetValue(x, timeSpan, null); + } + else if (type.IsGenericType) + { + var t = type.GetGenericArguments()[0]; + var list = (IList)Activator.CreateInstance(type); + + var container = GetElementByName(root, prop.Name.AsNamespaced(Namespace)); + + if (container.HasElements) + { + var first = container.Elements().FirstOrDefault(); + var elements = container.Elements(first.Name); + PopulateListFromElements(t, elements, list); + } + + prop.SetValue(x, list, null); + } + else if (type.IsSubclassOfRawGeneric(typeof(List<>))) + { + // handles classes that derive from List + // e.g. a collection that also has attributes + var list = HandleListDerivative(x, root, prop.Name, type); + prop.SetValue(x, list, null); + } + else + { + //fallback to type converters if possible + object result; + if (TryGetFromString(value.ToString(), out result, type)) + { + prop.SetValue(x, result, null); + } + else + { + // nested property classes + if (root != null) + { + var element = GetElementByName(root, name); + if (element != null) + { + var item = CreateAndMap(type, element); + prop.SetValue(x, item, null); + } + } + } + } + } + } + + private static bool TryGetFromString(string inputString, out object result, Type type) + { +#if !SILVERLIGHT && !WINDOWS_PHONE + var converter = TypeDescriptor.GetConverter(type); + if (converter.CanConvertFrom(typeof(string))) + { + result = (converter.ConvertFromInvariantString(inputString)); + return true; + } + result = null; + return false; +#else + result = null; + return false; +#endif + } + + private void PopulateListFromElements(Type t, IEnumerable elements, IList list) + { + foreach (var element in elements) + { + var item = CreateAndMap(t, element); + list.Add(item); + } + } + + private object HandleListDerivative(object x, XElement root, string propName, Type type) + { + Type t; + + if (type.IsGenericType) + { + t = type.GetGenericArguments()[0]; + } + else + { + t = type.BaseType.GetGenericArguments()[0]; + } + + + var list = (IList)Activator.CreateInstance(type); + + //Modified version from RestSharp + var elements = root.Elements(t.Name.AsNamespaced(Namespace)); + + var name = t.Name; + + if (!elements.Any()) + { + var lowerName = name.ToLowerInvariant().AsNamespaced(Namespace); + var firstNode = root.FirstNode; + if (firstNode != null) + { + elements = ((XElement)firstNode).Elements(lowerName); + } + } + + if (!elements.Any()) + { + var lowerName = name.ToLowerInvariant().AsNamespaced(Namespace); + elements = root.Descendants(lowerName); + } + + if (!elements.Any()) + { + var camelName = name.ToCamelCase(Culture).AsNamespaced(Namespace); + elements = root.Descendants(camelName); + } + + if (!elements.Any()) + { + elements = root.Descendants().Where(e => e.Name.LocalName.RemoveUnderscoresAndDashes() == name); + } + + if (!elements.Any()) + { + var lowerName = name.ToLowerInvariant().AsNamespaced(Namespace); + elements = root.Descendants().Where(e => e.Name.LocalName.RemoveUnderscoresAndDashes() == lowerName); + } + + PopulateListFromElements(t, elements, list); + + // get properties too, not just list items + // only if this isn't a generic type + if (!type.IsGenericType) + { + Map(list, root.Element(propName.AsNamespaced(Namespace)) ?? root); // when using RootElement, the heirarchy is different + } + + return list; + } + + protected virtual object CreateAndMap(Type t, XElement element) + { + object item; + if (t == typeof(String)) + { + item = element.Value; + } + else if (t.IsPrimitive) + { + item = System.Convert.ChangeType(element.Value, t, Culture); + } + else + { + item = Activator.CreateInstance(t); + Map(item, element); + } + + return item; + } + + protected virtual object GetValueFromXml(XElement root, XName name, PropertyInfo prop) + { + object val = null; + + if (root != null) + { + var element = GetElementByName(root, name); + if (element == null) + { + var attribute = GetAttributeByName(root, name); + if (attribute != null) + { + val = attribute.Value; + } + } + else + { + if (!element.IsEmpty || element.HasElements || element.HasAttributes) + { + val = element.Value; + } + } + } + + return val; + } + + protected virtual XElement GetElementByName(XElement root, XName name) + { + var lowerName = name.LocalName.ToLowerInvariant().AsNamespaced(name.NamespaceName); + var camelName = name.LocalName.ToCamelCase(Culture).AsNamespaced(name.NamespaceName); + if (root.Element(name) != null) + { + return root.Element(name); + } + if (root.Element(lowerName) != null) + { + return root.Element(lowerName); + } + + if (root.Element(camelName) != null) + { + return root.Element(camelName); + } + + if (name == "Value".AsNamespaced(name.NamespaceName)) + { + return root; + } + + // try looking for element that matches sanitized property name (Order by depth) + var element = root.Descendants() + .OrderBy(d => d.Ancestors().Count()) + .FirstOrDefault(d => d.Name.LocalName.RemoveUnderscoresAndDashes() == name.LocalName) + ?? root.Descendants() + .OrderBy(d => d.Ancestors().Count()) + .FirstOrDefault(d => d.Name.LocalName.RemoveUnderscoresAndDashes() == name.LocalName.ToLowerInvariant()); + + if (element != null) + { + return element; + } + + return null; + } + + protected virtual XAttribute GetAttributeByName(XElement root, XName name) + { + var lowerName = name.LocalName.ToLowerInvariant().AsNamespaced(name.NamespaceName); + var camelName = name.LocalName.ToCamelCase(Culture).AsNamespaced(name.NamespaceName); + + if (root.Attribute(name) != null) + { + return root.Attribute(name); + } + + if (root.Attribute(lowerName) != null) + { + return root.Attribute(lowerName); + } + + if (root.Attribute(camelName) != null) + { + return root.Attribute(camelName); + } + + // try looking for element that matches sanitized property name + var element = root.Attributes().FirstOrDefault(d => d.Name.LocalName.RemoveUnderscoresAndDashes() == name.LocalName); + if (element != null) + { + return element; + } + + return null; + } + } +} \ No newline at end of file diff --git a/PrestaSharp/Serializer/XMLExtensions.cs b/PrestaSharp/Serializer/XMLExtensions.cs new file mode 100644 index 00000000..fa40d431 --- /dev/null +++ b/PrestaSharp/Serializer/XMLExtensions.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bukimedia.PrestaSharp.Serializer +{ + public static class XMLExtensions + { + internal static object? ChangeType(this object? source, Type newType) => Convert.ChangeType(source, newType); + } +} diff --git a/PrestaSharp/Serializer/XMLSerializer.cs b/PrestaSharp/Serializer/XMLSerializer.cs new file mode 100644 index 00000000..1298d493 --- /dev/null +++ b/PrestaSharp/Serializer/XMLSerializer.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections; +using System.Linq; +using System.Xml.Linq; +using RestSharp.Serializers; +using RestSharp.Extensions; +using RestSharp.Serializers.Xml; +using RestSharp; +using System.Globalization; +using System.Reflection; + +namespace Bukimedia.PrestaSharp.Serializer +{ + public class XMLSerializer : ISerializer + { + /// + /// Name of the root element to use when serializing + /// + public string? RootElement { get; set; } + + /// + /// XML namespace to use when serializing + /// + public string? Namespace { get; set; } + + /// + /// Format string to use when serializing dates + /// + public string? DateFormat { get; set; } + + /// + /// Content type for serialized content + /// + public ContentType ContentType { get; set; } = ContentType.Xml; + + public XMLSerializer() + : base() + { + } + + /// + /// Serialize the object as XML + /// + /// Object to serialize + /// XML as string + public string Serialize(object obj) + { + var doc = new XDocument(); + + var t = obj.GetType(); + var name = t.Name; + + var options = t.GetAttribute(); + if (options != null) + { + name = options.TransformName(options.Name ?? name); + } + + var root = new XElement(name.AsNamespaced(Namespace)); + + if (obj is IList) + { + var itemTypeName = ""; + foreach (var item in (IList)obj) + { + var type = item.GetType(); + var opts = type.GetAttribute(); + if (opts != null) + { + itemTypeName = opts.TransformName(opts.Name ?? name); + } + if (itemTypeName == "") + { + itemTypeName = type.Name; + } + var instance = new XElement(itemTypeName); + Map(instance, item); + root.Add(instance); + } + } + else + Map(root, obj); + + if (RootElement.HasValue()) + { + var wrapper = new XElement(RootElement.AsNamespaced(Namespace), root); + doc.Add(wrapper); + } + else + { + doc.Add(root); + } + + return doc.ToString(); + } + + private void Map(XElement root, object obj) + { + var objType = obj.GetType(); + + var props = from p in objType.GetProperties() + let indexAttribute = p.GetAttribute() + where p.CanRead && p.CanWrite + orderby indexAttribute == null ? int.MaxValue : indexAttribute.Index + select p; + + var globalOptions = objType.GetAttribute(); + + foreach (var prop in props) + { + var name = prop.Name; + var rawValue = prop.GetValue(obj, null); + + //Hack to serialize Bukimedia.PrestaSharp.Entities.AuxEntities.language + if (obj.GetType().FullName.Equals("Bukimedia.PrestaSharp.Entities.AuxEntities.language") && root.Name.LocalName.Equals("language") && name.Equals("id")) + { + + root.Add(new XAttribute(XName.Get("id"), rawValue)); + continue; + } + else if (obj.GetType().FullName.Equals("Bukimedia.PrestaSharp.Entities.AuxEntities.language") && root.Name.LocalName.Equals("language") && name.Equals("Value")) + { + XText xtext = new XText(rawValue == null ? "" : rawValue.ToString()); + root.Add(xtext); + continue; + + } + + if (rawValue == null) + { + continue; + } + + var value = GetSerializedValue(rawValue); + var propType = prop.PropertyType; + + var useAttribute = false; + var settings = prop.GetAttribute(); + if (settings != null) + { + name = settings.Name.HasValue() ? settings.Name : name; + useAttribute = settings.Attribute; + } + + var options = prop.GetAttribute(); + if (options != null) + { + name = options.TransformName(name); + } + else if (globalOptions != null) + { + name = globalOptions.TransformName(name); + } + + var nsName = name.AsNamespaced(Namespace); + var element = new XElement(nsName); + + if (propType.IsPrimitive || propType.IsValueType || propType == typeof(string)) + { + if (useAttribute) + { + root.Add(new XAttribute(name, value)); + continue; + } + + element.Value = value; + } + else if (rawValue is IList) + { + var itemTypeName = ""; + foreach (var item in (IList)rawValue) + { + if (itemTypeName == "") + { + var type = item.GetType(); + var setting = type.GetAttribute(); + itemTypeName = setting != null && setting.Name.HasValue() + ? setting.Name + : type.Name; + } + var instance = new XElement(itemTypeName); + Map(instance, item); + element.Add(instance); + } + } + else + { + Map(element, rawValue); + } + + root.Add(element); + } + } + + private string GetSerializedValue(object obj) + { + var output = obj; + + if (obj is DateTime && DateFormat.HasValue()) + { + output = ((DateTime)obj).ToString(DateFormat); + } + else if (obj is bool) + { + output = obj.ToString().ToLowerInvariant(); + } + else if (obj is decimal) + { + output = obj.ToString().Replace(",", "."); + } + + return output.ToString(); + } + } +} \ No newline at end of file diff --git a/PrestaSharpTests/PrestaSharpTests.csproj b/PrestaSharpTests/PrestaSharpTests.csproj index ab3e5e33..9f8ce6c1 100644 --- a/PrestaSharpTests/PrestaSharpTests.csproj +++ b/PrestaSharpTests/PrestaSharpTests.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net8.0 false