diff --git a/src/AasCore.Aas3_0/DiaryData/ITaintedData.cs b/src/AasCore.Aas3_0/DiaryData/ITaintedData.cs new file mode 100644 index 000000000..94933fb4c --- /dev/null +++ b/src/AasCore.Aas3_0/DiaryData/ITaintedData.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AdminShellNS.DiaryData +{ + public interface ITaintedData + { + TaintedDataDef TaintedData { get; } + } +} diff --git a/src/AasCore.Aas3_0/DiaryData/TaintedDataDef.cs b/src/AasCore.Aas3_0/DiaryData/TaintedDataDef.cs new file mode 100644 index 000000000..f921530d4 --- /dev/null +++ b/src/AasCore.Aas3_0/DiaryData/TaintedDataDef.cs @@ -0,0 +1,45 @@ +using AasCore.Aas3_0; +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Xml.Serialization; + +namespace AdminShellNS.DiaryData +{ + public class TaintedDataDef + { + [XmlIgnore] + [JsonIgnore] + private DateTime? _tainted = null; + + [XmlIgnore] + [JsonIgnore] + public DateTime? Tainted + { + get { return _tainted; } + set { _tainted = value; } + } + + public static void TaintIdentifiable(IReferable element) + { + // trivial + if (element == null) + return; + + // find identifiable + var el = element; + while (el != null) + { + // found? + if (el is IIdentifiable && el is ITaintedData itd) + { + itd.TaintedData.Tainted = DateTime.UtcNow; + return; + } + + // up + el = (el as IReferable)?.Parent as IReferable; + } + } + } +} diff --git a/src/AasCore.Aas3_0/types.cs b/src/AasCore.Aas3_0/types.cs index f72cd5894..e9d29c711 100644 --- a/src/AasCore.Aas3_0/types.cs +++ b/src/AasCore.Aas3_0/types.cs @@ -469,7 +469,7 @@ public interface IReferable : IHasExtensions, IDiaryData /// /// An element that has a globally unique identifier. /// - public interface IIdentifiable : IReferable + public interface IIdentifiable : IReferable, ITaintedData { /// /// Administrative information of an identifiable element. @@ -1317,6 +1317,12 @@ public class AssetAdministrationShell : IAssetAdministrationShell [JsonIgnore] public DiaryDataDef DiaryData { get { return _diaryData; } } + [JsonIgnore] + private TaintedDataDef _taintedData = new TaintedDataDef(); + + [JsonIgnore] + public TaintedDataDef TaintedData { get { return _taintedData; } } + #endregion /// @@ -2460,6 +2466,12 @@ public class Submodel : ISubmodel [JsonIgnore] public DiaryDataDef DiaryData { get { return _diaryData; } } + [JsonIgnore] + private TaintedDataDef _taintedData = new TaintedDataDef(); + + [JsonIgnore] + public TaintedDataDef TaintedData { get { return _taintedData; } } + #endregion /// @@ -10741,6 +10753,12 @@ public class ConceptDescription : IConceptDescription [JsonIgnore] public DiaryDataDef DiaryData { get { return _diaryData; } } + [JsonIgnore] + private TaintedDataDef _taintedData = new TaintedDataDef(); + + [JsonIgnore] + public TaintedDataDef TaintedData { get { return _taintedData; } } + #endregion /// @@ -11910,17 +11928,17 @@ public interface IEnvironment : IClass, IDiaryData /// /// Asset administration shell /// - public List? AssetAdministrationShells { get; set; } + public IList? AssetAdministrationShells { get; set; } /// /// Submodel /// - public List? Submodels { get; set; } + public IList? Submodels { get; set; } /// /// Concept description /// - public List? ConceptDescriptions { get; set; } + public IList? ConceptDescriptions { get; set; } /// /// Iterate over AssetAdministrationShells, if set, and otherwise return an empty enumerable. @@ -11951,17 +11969,17 @@ public class Environment : IEnvironment /// /// Asset administration shell /// - public List? AssetAdministrationShells { get; set; } + public IList? AssetAdministrationShells { get; set; } /// /// Submodel /// - public List? Submodels { get; set; } + public IList? Submodels { get; set; } /// /// Concept description /// - public List? ConceptDescriptions { get; set; } + public IList? ConceptDescriptions { get; set; } /// /// Iterate over AssetAdministrationShells, if set, and otherwise return an empty enumerable. @@ -12116,9 +12134,9 @@ public T Transform( } public Environment( - List? assetAdministrationShells = null, - List? submodels = null, - List? conceptDescriptions = null) + IList? assetAdministrationShells = null, + IList? submodels = null, + IList? conceptDescriptions = null) { AssetAdministrationShells = assetAdministrationShells; Submodels = submodels; diff --git a/src/AasxAmlImExport/AmlExport.cs b/src/AasxAmlImExport/AmlExport.cs index cad046b50..d80a7b4f8 100644 --- a/src/AasxAmlImExport/AmlExport.cs +++ b/src/AasxAmlImExport/AmlExport.cs @@ -1129,7 +1129,7 @@ private static void SetInternalLinks( } } - public static bool ExportTo(AdminShellPackageEnv package, string amlfn, bool tryUseCompactProperties = false) + public static bool ExportTo(AdminShellPackageEnvBase package, string amlfn, bool tryUseCompactProperties = false) { // start if (package == null || amlfn == null || package.AasEnv == null) diff --git a/src/AasxAmlImExport/AmlImport.cs b/src/AasxAmlImExport/AmlImport.cs index 75295e3ae..2514d3d08 100644 --- a/src/AasxAmlImExport/AmlImport.cs +++ b/src/AasxAmlImExport/AmlImport.cs @@ -38,7 +38,7 @@ public static string PrintSemantic(CAEXSequence sem) public class AmlParser { - public AdminShellPackageEnv package = null; + public AdminShellPackageEnvBase package = null; private class TargetIdAction { @@ -87,7 +87,7 @@ public IeViewAmlTarget(InternalElementType ie, CAEXObject amlTarget) public AmlParser() { } - public AmlParser(AdminShellPackageEnv package) + public AmlParser(AdminShellPackageEnvBase package) { this.package = package; } @@ -1634,7 +1634,7 @@ public void Start(CAEXFileType caex) } } - public static void ImportInto(AdminShellPackageEnv package, string amlfn) + public static void ImportInto(AdminShellPackageEnvBase package, string amlfn) { // try open file var doc = CAEXDocument.LoadFromFile(amlfn); diff --git a/src/AasxBammRdfImExport/RDFimport.cs b/src/AasxBammRdfImExport/RDFimport.cs index d91c09888..191cf6a74 100644 --- a/src/AasxBammRdfImExport/RDFimport.cs +++ b/src/AasxBammRdfImExport/RDFimport.cs @@ -24,13 +24,13 @@ namespace AasxBammRdfImExport public static class BAMMRDFimport { - public static AdminShellNS.AdminShellPackageEnv thePackageEnv; + public static AdminShellNS.AdminShellPackageEnvBase thePackageEnv; public static void ImportInto( string rdffn, Aas.IEnvironment env, Aas.ISubmodel sm, Aas.Reference smref) { - thePackageEnv = new AdminShellNS.AdminShellPackageEnv(); + thePackageEnv = new AdminShellNS.AdminShellPackageFileBasedEnv(); List entity_subject = new List(); List autos_list = new List(); List set_list = new List(); diff --git a/src/AasxCsharpLibrary/AasxCsharpLibrary.csproj b/src/AasxCsharpLibrary/AasxCsharpLibrary.csproj index ef47300be..ea20fca2d 100644 --- a/src/AasxCsharpLibrary/AasxCsharpLibrary.csproj +++ b/src/AasxCsharpLibrary/AasxCsharpLibrary.csproj @@ -33,6 +33,7 @@ + diff --git a/src/AasxCsharpLibrary/AdminShellBasicExtensions.cs b/src/AasxCsharpLibrary/AdminShellBasicExtensions.cs index b52c70969..0ca1e993c 100644 --- a/src/AasxCsharpLibrary/AdminShellBasicExtensions.cs +++ b/src/AasxCsharpLibrary/AdminShellBasicExtensions.cs @@ -44,6 +44,14 @@ public static IEnumerable ForEachSafe(this List list) yield return x; } + public static void ForEach(this IList list, Action action) + { + if (list == null) + return; + foreach (var x in list) + action?.Invoke(x); + } + /// /// Multiple a string into a iterable list /// diff --git a/src/AasxCsharpLibrary/AdminShellCollections.cs b/src/AasxCsharpLibrary/AdminShellCollections.cs index b93c0f032..5c423ba15 100644 --- a/src/AasxCsharpLibrary/AdminShellCollections.cs +++ b/src/AasxCsharpLibrary/AdminShellCollections.cs @@ -33,12 +33,28 @@ public class MultiValueDictionary public void Add(K key, V value) { + if (key == null) + return; + if (dict.TryGetValue(key, out var list)) list.Add(value); else dict.Add(key, new List { value }); } + public void AddIfValueIsNew(K key, V value) + { + if (key == null) + return; + + if (dict.TryGetValue(key, out var list)) + { + if (!list.Contains(value)) + list.Add(value); + } + else + dict.Add(key, new List { value }); + } public void Remove(K key) { if (dict.ContainsKey(key)) diff --git a/src/AasxCsharpLibrary/AdminShellComparers.cs b/src/AasxCsharpLibrary/AdminShellComparers.cs new file mode 100644 index 000000000..d8ff939c4 --- /dev/null +++ b/src/AasxCsharpLibrary/AdminShellComparers.cs @@ -0,0 +1,57 @@ +/* +Copyright (c) 2018-2023 Festo SE & Co. KG +Author: Michael Hoffmeister + +This source code is licensed under the Apache License 2.0 (see LICENSE.txt). + +This source code may use other Open Source software components (see LICENSE.txt). +*/ + +using Extensions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace AdminShellNS +{ + public static class AdminShellComparers + { + + /// see: https://www.codeproject.com/Articles/72470/LINQ-Enhancing-Distinct-With-The-PredicateEquality + public class PredicateEqualityComparer : EqualityComparer + { + private Func predicate; + + public PredicateEqualityComparer(Func predicate) + : base() + { + this.predicate = predicate; + } + + public override bool Equals(T x, T y) + { + if (x != null) + { + return ((y != null) && this.predicate(x, y)); + } + + if (y != null) + { + return false; + } + + return true; + } + + public override int GetHashCode(T obj) + { + // Always return the same value to force the call to IEqualityComparer.Equals + return 0; + } + } + } +} diff --git a/src/AasxCsharpLibrary/AdminShellPackageEnvBase.cs b/src/AasxCsharpLibrary/AdminShellPackageEnvBase.cs new file mode 100644 index 000000000..8fcfe670b --- /dev/null +++ b/src/AasxCsharpLibrary/AdminShellPackageEnvBase.cs @@ -0,0 +1,754 @@ +/* +Copyright (c) 2018-2023 Festo SE & Co. KG +Author: Michael Hoffmeister + +This source code is licensed under the Apache License 2.0 (see LICENSE.txt). + +This source code may use other Open Source software components (see LICENSE.txt). +*/ + +using Extensions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.IO.Packaging; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection.Metadata; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; + +namespace AdminShellNS +{ + /// + /// This class lets an outer functionality keep track on the supplementary files, which are in or + /// are pending to be added or deleted to an Package. + /// + public class AdminShellPackageSupplementaryFile /*: IReferable*/ + { + public delegate byte[] SourceGetByteChunk(); + + public enum LocationType { InPackage, AddPending, DeletePending } + + public enum SpecialHandlingType { None, EmbedAsThumbnail } + + public readonly Uri Uri = null; + + public readonly string UseMimeType = null; + + public readonly string SourceLocalPath = null; + public readonly SourceGetByteChunk SourceGetBytesDel = null; + + public LocationType Location; + public readonly SpecialHandlingType SpecialHandling; + + public AdminShellPackageSupplementaryFile( + Uri uri, string sourceLocalPath = null, LocationType location = LocationType.InPackage, + SpecialHandlingType specialHandling = SpecialHandlingType.None, + SourceGetByteChunk sourceGetBytesDel = null, string useMimeType = null) + { + Uri = uri; + UseMimeType = useMimeType; + SourceLocalPath = sourceLocalPath; + SourceGetBytesDel = sourceGetBytesDel; + Location = location; + SpecialHandling = specialHandling; + } + + // class derives from Referable in order to provide GetElementName + public string GetElementName() + { + return "File"; + } + + } + + public class ListOfAasSupplementaryFile : List + { + public AdminShellPackageSupplementaryFile FindByUri(string path) + { + if (path == null) + return null; + + return this.FirstOrDefault( + x => x?.Uri?.ToString().Trim() == path.Trim()); + } + } + + /// + /// Provides (static?) helpers for serializing AAS.. + /// + public static class AdminShellSerializationHelper + { + + public static string TryReadXmlFirstElementNamespaceURI(Stream s) + { + string res = null; + try + { + var xr = System.Xml.XmlReader.Create(s); + int i = 0; + while (xr.Read()) + { + // limit amount of read + i++; + if (i > 99) + // obviously not found + break; + + // find element + if (xr.NodeType == System.Xml.XmlNodeType.Element) + { + res = xr.NamespaceURI; + break; + } + } + xr.Close(); + } + catch (Exception ex) + { + LogInternally.That.SilentlyIgnoredError(ex); + } + + // return to zero pos + s.Seek(0, SeekOrigin.Begin); + + // give back + return res; + } + + /// + /// Skips first few tokens of an XML content until first "real" element is encountered + /// + /// + public static void XmlSkipHeader(XmlReader xmlReader) + { + while (xmlReader.NodeType == XmlNodeType.XmlDeclaration || + xmlReader.NodeType == XmlNodeType.Whitespace || + xmlReader.NodeType == XmlNodeType.Comment || + xmlReader.NodeType == XmlNodeType.None) + xmlReader.Read(); + } + + /// + /// De-serialize an open stream into Environment. Does version/ compatibility management. + /// + /// Open for read stream + /// + public static AasCore.Aas3_0.Environment DeserializeXmlFromStreamWithCompat(Stream s) + { + // not sure + AasCore.Aas3_0.Environment res = null; + + // try get first element + var nsuri = TryReadXmlFirstElementNamespaceURI(s); + + // read V1.0? + if (nsuri != null && nsuri.Trim() == "http://www.admin-shell.io/aas/1/0") + { +#if !DoNotUseAasxCompatibilityModels + XmlSerializer serializer = new XmlSerializer( + typeof(AasxCompatibilityModels.AdminShellV10.AdministrationShellEnv), + "http://www.admin-shell.io/aas/1/0"); + var v10 = serializer.Deserialize(s) as AasxCompatibilityModels.AdminShellV10.AdministrationShellEnv; + res = new AasCore.Aas3_0.Environment(new List(), new List(), new List()); + res.ConvertFromV10(v10); + return res; +#else + throw (new Exception("Cannot handle AAS file format http://www.admin-shell.io/aas/1/0 !")); +#endif + } + + // read V2.0? + if (nsuri != null && nsuri.Trim() == "http://www.admin-shell.io/aas/2/0") + { +#if !DoNotUseAasxCompatibilityModels + XmlSerializer serializer = new XmlSerializer( + typeof(AasxCompatibilityModels.AdminShellV20.AdministrationShellEnv), + "http://www.admin-shell.io/aas/2/0"); + var v20 = serializer.Deserialize(s) as AasxCompatibilityModels.AdminShellV20.AdministrationShellEnv; + res = new AasCore.Aas3_0.Environment(new List(), new List(), new List()); + res.ConvertFromV20(v20); + return res; +#else + throw (new Exception("Cannot handle AAS file format http://www.admin-shell.io/aas/1/0 !")); +#endif + } + + // read V3.0? + if (nsuri != null && nsuri.Trim() == Xmlization.NS) + { + // dead-csharp off + //XmlSerializer serializer = new XmlSerializer( + // typeof(AasCore.Aas3_0_RC02.Environment), "http://www.admin-shell.io/aas/3/0"); + //res = serializer.Deserialize(s) as AasCore.Aas3_0_RC02.Environment; + // dead-csharp on + using (var xmlReader = XmlReader.Create(s)) + { + // TODO (MIHO, 2022-12-26): check if could be feature of AAS core + XmlSkipHeader(xmlReader); + res = Xmlization.Deserialize.EnvironmentFrom(xmlReader); + return res; + } + } + + // nope! + return null; + } + // dead-csharp off + //public static JsonSerializer BuildDefaultAasxJsonSerializer() + //{ + // var serializer = new JsonSerializer(); + // serializer.Converters.Add( + // new AdminShellConverters.JsonAasxConverter( + // "modelType", "name")); + // return serializer; + //} + public static T DeserializeFromJSON(string data) where T : IReferable + { + //using (var tr = new StringReader(data)) + //{ + //var serializer = BuildDefaultAasxJsonSerializer(); + //var rf = (T)serializer.Deserialize(tr, typeof(T)); + + var node = System.Text.Json.Nodes.JsonNode.Parse(data); + var rf = Jsonization.Deserialize.IReferableFrom(node); + + return (T)rf; + //} + } + + //public static T DeserializeFromJSON(JToken obj) where T : IReferable + //{ + // if (obj == null) + // return default(T); + // var serializer = BuildDefaultAasxJsonSerializer(); + // var rf = obj.ToObject(serializer); + // return rf; + //} + + ///// + ///// Use this, if DeserializeFromJSON is too tight. + ///// + //public static T DeserializePureObjectFromJSON(string data) + //{ + // using (var tr = new StringReader(data)) + // { + // //var serializer = BuildDefaultAasxJsonSerializer(); + // //var rf = (T)serializer.Deserialize(tr, typeof(T)); + // return null; + // } + //} + // dead-csharp on + } + + public abstract class AdminShellPackageEnvBase : IDisposable + { + public enum SerializationFormat { None, Xml, Json }; + + protected IEnvironment _aasEnv = new AasCore.Aas3_0.Environment(); + + public AdminShellPackageEnvBase() { } + + public AdminShellPackageEnvBase(AasCore.Aas3_0.IEnvironment env) + { + if (env != null) + _aasEnv = env; + } + + public IEnvironment AasEnv + { + get + { + return _aasEnv; + } + } + + public void SetEnvironment(IEnvironment environment) + { + _aasEnv = environment; + } + + // TODO: remove, is not for base class!! + public virtual void SetFilename(string fileName) + { + } + + // TODO: remove, is not for base class!! + public virtual string Filename + { + get + { + return ""; + } + } + + protected static WebProxy proxy = null; + + /// + /// Checks for a file within the package or external, e.g. on a file space. + /// In derived classes, could use the AAS/ SM / IdShortPath attributes to + /// refer to files in registry/ repository. + /// + /// Local (within package) or external file. Should have a scheme. + /// Local file start with a leading slash. + /// For registry/ repository access. + /// For registry/ repository access. + /// For registry/ repository access. + /// Bytes or null + public virtual byte[] GetBytesFromPackageOrExternal( + string uriString, + string aasId = null, + string smId = null, + string idShortPath = null) + { + // + // this part of the functionality works on HTTP and absolute files and is + // indepedent from a package storage. + // + + // split scheme and part + var sap = AdminShellUtil.GetSchemeAndPath(uriString); + if (sap == null) + return null; + + // Check, if remote + if (sap.Scheme.StartsWith("http")) + { + if (proxy == null) + { + string proxyAddress = ""; + string username = ""; + string password = ""; + + string proxyFile = "proxy.txt"; + if (System.IO.File.Exists(proxyFile)) + { + try + { // Open the text file using a stream reader. + using (StreamReader sr = new StreamReader(proxyFile)) + { + proxyFile = sr.ReadLine(); + } + } + catch (IOException e) + { + Console.WriteLine("proxy.txt could not be read:"); + Console.WriteLine(e.Message); + } + } + + try + { + using (StreamReader sr = new StreamReader(proxyFile)) + { + proxyAddress = sr.ReadLine(); + username = sr.ReadLine(); + password = sr.ReadLine(); + } + } + catch (Exception e) + { + Console.WriteLine(e.Message); + Console.WriteLine(proxyFile + " not found!"); + } + + if (proxyAddress != "") + { + proxy = new WebProxy(); + Uri newUri = new Uri(proxyAddress); + proxy.Address = newUri; + proxy.Credentials = new NetworkCredential(username, password); + Console.WriteLine("Using proxy: " + proxyAddress); + } + } + + var handler = new HttpClientHandler(); + + if (proxy != null) + handler.Proxy = proxy; + else + handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials; + + // var hc = new HttpClient(handler); + var hc = new HttpClient(); + + var response = hc.GetAsync(uriString).GetAwaiter().GetResult(); + var x = response.Content.ToString(); + + // if you call response.EnsureSuccessStatusCode here it will throw an exception + if (response.StatusCode == HttpStatusCode.Moved + || response.StatusCode == HttpStatusCode.Found) + { + var location = response.Headers.Location; + response = hc.GetAsync(location).GetAwaiter().GetResult(); + } + + response.EnsureSuccessStatusCode(); + + var resBytes = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); + + // TODO (MIHO, 2024-11-09): looks like a hack from Andreas to + // detect indirection + if (resBytes.Length < 500) // indirect load? + { + string json = System.Text.Encoding.UTF8.GetString(resBytes); + var parsed = JObject.Parse(json); + try + { + string url = parsed.SelectToken("url").Value(); + response = hc.GetAsync(url).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + resBytes = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + LogInternally.That.SilentlyIgnoredError(ex); + } + } + + return resBytes; + } + + // now, has to be file + if (sap.Scheme != "file") + return null; + + // if not starting with "/", if has to be an absolute file + if (!sap.Path.StartsWith('/')) + { + try + { + var resBytes = System.IO.File.ReadAllBytes(sap.Path); + return resBytes; + } + catch (Exception ex) + { + LogInternally.That.SilentlyIgnoredError(ex); + } + } + + // no, nothing more to find here (in base implementation!) + return null; + } + + /// + /// Checks for a file within the package or external, e.g. on a file space. + /// In derived classes, could use the AAS/ SM / IdShortPath attributes to + /// refer to files in registry/ repository. + /// + /// Local (within package) or external file. Should have a scheme. + /// Local file start with a leading slash. + /// For registry/ repository access. + /// For registry/ repository access. + /// For registry/ repository access. + /// Bytes or null + public virtual async Task GetBytesFromPackageOrExternalAsync( + string uriString, + string aasId = null, + string smId = null, + string idShortPath = null) + { + await Task.Yield(); + return GetBytesFromPackageOrExternal(uriString, aasId, smId, idShortPath); + } + + /// + /// Writes to a file within the package or to external registry/ repository. + /// Does not write to external files e.g. on a filespace! + /// In derived classes, could use the AAS/ SM / IdShortPath attributes to + /// refer to files in registry/ repository. + /// + /// Local (within package) or external file. Should have a scheme. + /// Local file start with a leading slash. + /// Data given by the bytes + /// For registry/ repository access. + /// For registry/ repository access. + /// For registry/ repository access. + /// True, if the file was successfully stored. + public virtual bool PutBytesToPackageOrExternal( + string uriString, + byte[] data, + string aasId = null, + string smId = null, + string idShortPath = null) + { + // here, nothing to do, as external file storage is not allowed. + return false; + } + + /// + /// Writes to a file within the package or to external registry/ repository. + /// Does not write to external files e.g. on a filespace! + /// In derived classes, could use the AAS/ SM / IdShortPath attributes to + /// refer to files in registry/ repository. + /// + /// Local (within package) or external file. Should have a scheme. + /// Local file start with a leading slash. + /// Data given by the bytes + /// For registry/ repository access. + /// For registry/ repository access. + /// For registry/ repository access. + /// True, if the file was successfully stored. + public virtual async Task PutBytesToPackageOrExternalAsync( + string uriString, + byte[] data, + string aasId = null, + string smId = null, + string idShortPath = null) + { + // here, nothing to do, as external file storage is not allowed. + await Task.Yield(); + return PutBytesToPackageOrExternal(uriString, data, aasId, smId, idShortPath); + } + + public virtual void PrepareSupplementaryFileParameters(ref string targetDir, ref string targetFn) + { + } + + public virtual string AddSupplementaryFileToStore( + string sourcePath, string targetDir, string targetFn, bool embedAsThumb, + AdminShellPackageSupplementaryFile.SourceGetByteChunk sourceGetBytesDel = null, string useMimeType = null) + { + return null; + } + + /// + /// Gets the thumbnail data of the local package (and only the local package!) + /// + public virtual byte[] GetLocalThumbnailBytes(ref Uri thumbUri) + { + return null; + } + + /// + /// Gets the thumbnail data of the local package or (1st prio) the AAS with the + /// given id. + /// Note: does not access remote content from registry/ repository! + /// + public virtual byte[] GetThumbnailBytesFromAasOrPackage(string aasId) + { + // find aas? + var aas = AasEnv?.FindAasById(aasId); + if (aas?.AssetInformation?.DefaultThumbnail?.Path?.HasContent() == true) + { + try + { + // Note: could also use http://... + var bytes = GetBytesFromPackageOrExternal(uriString: aas.AssetInformation.DefaultThumbnail.Path); + if (bytes != null) + return bytes; + } + catch (Exception ex) + { + LogInternally.That.CompletelyIgnoredError(ex); + } + } + + // or local package? + try + { + Uri dummy = null; + var bytes = GetLocalThumbnailBytes(ref dummy); + if (bytes != null) + return bytes; + } + catch (Exception ex) + { + LogInternally.That.CompletelyIgnoredError(ex); + } + + // no + return null; + } + + public virtual ListOfAasSupplementaryFile GetListOfSupplementaryFiles() + { + return null; + } + + public virtual void DeleteSupplementaryFile(AdminShellPackageSupplementaryFile psf) + { + } + + /// + /// Copies/ download contents and will return filename of temp file. + /// + /// + public virtual string MakePackageFileAvailableAsTempFile(string packageUri, bool keepFilename = false) + { + // this uses the virtual implementation and should therefore work ok in the base class + + // access + if (packageUri == null) + return null; + + // get input stream + var inputBytes = GetBytesFromPackageOrExternal(packageUri); + if (inputBytes == null) + return null; + + // generate tempfile name + string tempext = System.IO.Path.GetExtension(packageUri); + string temppath = System.IO.Path.GetTempFileName().Replace(".tmp", tempext); + + // maybe modify tempfile name? + if (keepFilename) + { + var masterFn = System.IO.Path.GetFileNameWithoutExtension(packageUri); + var tmpDir = System.IO.Path.GetDirectoryName(temppath); + var tmpFnExt = System.IO.Path.GetFileName(temppath); + + temppath = System.IO.Path.Combine(tmpDir, "" + masterFn + "_" + tmpFnExt); + } + + // copy to temp file + System.IO.File.WriteAllBytes(temppath, inputBytes); + return temppath; + } + + /// + /// Copies/ download contents and will return filename of temp file. + /// + /// + public virtual async Task MakePackageFileAvailableAsTempFileAsync( + string packageUri, + string aasId = null, + string smId = null, + string idShortPath = null, + bool keepFilename = false) + { + // this uses the virtual implementation and should therefore work ok in the base class + + // access + if (packageUri == null) + return null; + + // get input stream + var inputBytes = await GetBytesFromPackageOrExternalAsync(packageUri, aasId, smId, idShortPath); + if (inputBytes == null) + return null; + + // generate tempfile name + string tempext = System.IO.Path.GetExtension(packageUri); + string temppath = System.IO.Path.GetTempFileName().Replace(".tmp", tempext); + + // maybe modify tempfile name? + if (keepFilename) + { + var masterFn = System.IO.Path.GetFileNameWithoutExtension(packageUri); + var tmpDir = System.IO.Path.GetDirectoryName(temppath); + var tmpFnExt = System.IO.Path.GetFileName(temppath); + + temppath = System.IO.Path.Combine(tmpDir, "" + masterFn + "_" + tmpFnExt); + } + + // copy to temp file + await System.IO.File.WriteAllBytesAsync(temppath, inputBytes); + return temppath; + } + + public virtual bool SaveAs( + string fn, bool writeFreshly = false, + SerializationFormat prefFmt = SerializationFormat.None, + MemoryStream useMemoryStream = null, bool saveOnlyCopy = false) + { + return false; + } + + /// + /// Temporariyl saves & closes package and executes lambda. Afterwards, the package is re-opened + /// under the same file name + /// + /// Action which is to be executed while the file is CLOSED + /// Format for the saved file + public virtual void TemporarilySaveCloseAndReOpenPackage( + Action lambda, + AdminShellPackageFileBasedEnv.SerializationFormat prefFmt = AdminShellPackageFileBasedEnv.SerializationFormat.None) + { + } + + public virtual bool IsOpen + { + get + { + // negative default behaviour + return false; + } + } + + public virtual bool IsLocalFile(string uriString) + { + return false; + } + + public virtual void Close() + { + } + + public virtual void Flush() + { + } + + public virtual void Dispose() + { + } + + // + // Binary file read + write + // + + public async Task PutByteArrayToExternalUri(string uri, byte[] ba) + { + // split uri and access + var sap = AdminShellUtil.GetSchemeAndPath(uri); + if (ba == null || sap == null) + return false; + + // directly a HTTP resource? + if (sap.Scheme.StartsWith("http")) + { + try + { + using (var client = new HttpClient()) + using (var content = new ByteArrayContent(ba)) + { + content.Headers.ContentType = new MediaTypeHeaderValue("*/*"); + var response = await client.PostAsync(uri, content); + return response.IsSuccessStatusCode; + } + } + catch (Exception ex) + { + LogInternally.That.SilentlyIgnoredError(ex); + return false; + } + } + + // no other schemes now + if (sap.Scheme != "file") + return false; + + // just write + try + { + System.IO.File.WriteAllBytes(sap.Path, ba); + return true; + } + catch (Exception ex) + { + LogInternally.That.SilentlyIgnoredError(ex); + } + + return false; + } + } + +} \ No newline at end of file diff --git a/src/AasxCsharpLibrary/AdminShellPackageEnv.cs b/src/AasxCsharpLibrary/AdminShellPackageFileBasedEnv.cs similarity index 77% rename from src/AasxCsharpLibrary/AdminShellPackageEnv.cs rename to src/AasxCsharpLibrary/AdminShellPackageFileBasedEnv.cs index 3dcf74dc3..541a5327e 100644 --- a/src/AasxCsharpLibrary/AdminShellPackageEnv.cs +++ b/src/AasxCsharpLibrary/AdminShellPackageFileBasedEnv.cs @@ -7,6 +7,7 @@ This source code is licensed under the Apache License 2.0 (see LICENSE.txt). This source code may use other Open Source software components (see LICENSE.txt). */ +using AdminShellNS.DiaryData; using Extensions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -17,240 +18,21 @@ This source code may use other Open Source software components (see LICENSE.txt) using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection.Metadata; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; +using Aas = AasCore.Aas3_0; namespace AdminShellNS { - /// - /// This class lets an outer functionality keep track on the supplementary files, which are in or - /// are pending to be added or deleted to an Package. - /// - public class AdminShellPackageSupplementaryFile /*: IReferable*/ - { - public delegate byte[] SourceGetByteChunk(); - - public enum LocationType { InPackage, AddPending, DeletePending } - - public enum SpecialHandlingType { None, EmbedAsThumbnail } - - public readonly Uri Uri = null; - - public readonly string UseMimeType = null; - - public readonly string SourceLocalPath = null; - public readonly SourceGetByteChunk SourceGetBytesDel = null; - - public LocationType Location; - public readonly SpecialHandlingType SpecialHandling; - - public AdminShellPackageSupplementaryFile( - Uri uri, string sourceLocalPath = null, LocationType location = LocationType.InPackage, - SpecialHandlingType specialHandling = SpecialHandlingType.None, - SourceGetByteChunk sourceGetBytesDel = null, string useMimeType = null) - { - Uri = uri; - UseMimeType = useMimeType; - SourceLocalPath = sourceLocalPath; - SourceGetBytesDel = sourceGetBytesDel; - Location = location; - SpecialHandling = specialHandling; - } - - // class derives from Referable in order to provide GetElementName - public string GetElementName() - { - return "File"; - } - - } - - public class ListOfAasSupplementaryFile : List - { - public AdminShellPackageSupplementaryFile FindByUri(string path) - { - if (path == null) - return null; - - return this.FirstOrDefault( - x => x?.Uri?.ToString().Trim() == path.Trim()); - } - } - - /// - /// Provides (static?) helpers for serializing AAS.. - /// - public static class AdminShellSerializationHelper - { - - public static string TryReadXmlFirstElementNamespaceURI(Stream s) - { - string res = null; - try - { - var xr = System.Xml.XmlReader.Create(s); - int i = 0; - while (xr.Read()) - { - // limit amount of read - i++; - if (i > 99) - // obviously not found - break; - - // find element - if (xr.NodeType == System.Xml.XmlNodeType.Element) - { - res = xr.NamespaceURI; - break; - } - } - xr.Close(); - } - catch (Exception ex) - { - LogInternally.That.SilentlyIgnoredError(ex); - } - - // return to zero pos - s.Seek(0, SeekOrigin.Begin); - - // give back - return res; - } - - /// - /// Skips first few tokens of an XML content until first "real" element is encountered - /// - /// - public static void XmlSkipHeader(XmlReader xmlReader) - { - while (xmlReader.NodeType == XmlNodeType.XmlDeclaration || - xmlReader.NodeType == XmlNodeType.Whitespace || - xmlReader.NodeType == XmlNodeType.Comment || - xmlReader.NodeType == XmlNodeType.None) - xmlReader.Read(); - } - - /// - /// De-serialize an open stream into Environment. Does version/ compatibility management. - /// - /// Open for read stream - /// - public static AasCore.Aas3_0.Environment DeserializeXmlFromStreamWithCompat(Stream s) - { - // not sure - AasCore.Aas3_0.Environment res = null; - - // try get first element - var nsuri = TryReadXmlFirstElementNamespaceURI(s); - - // read V1.0? - if (nsuri != null && nsuri.Trim() == "http://www.admin-shell.io/aas/1/0") - { -#if !DoNotUseAasxCompatibilityModels - XmlSerializer serializer = new XmlSerializer( - typeof(AasxCompatibilityModels.AdminShellV10.AdministrationShellEnv), - "http://www.admin-shell.io/aas/1/0"); - var v10 = serializer.Deserialize(s) as AasxCompatibilityModels.AdminShellV10.AdministrationShellEnv; - res = new AasCore.Aas3_0.Environment(new List(), new List(), new List()); - res.ConvertFromV10(v10); - return res; -#else - throw (new Exception("Cannot handle AAS file format http://www.admin-shell.io/aas/1/0 !")); -#endif - } - - // read V2.0? - if (nsuri != null && nsuri.Trim() == "http://www.admin-shell.io/aas/2/0") - { -#if !DoNotUseAasxCompatibilityModels - XmlSerializer serializer = new XmlSerializer( - typeof(AasxCompatibilityModels.AdminShellV20.AdministrationShellEnv), - "http://www.admin-shell.io/aas/2/0"); - var v20 = serializer.Deserialize(s) as AasxCompatibilityModels.AdminShellV20.AdministrationShellEnv; - res = new AasCore.Aas3_0.Environment(new List(), new List(), new List()); - res.ConvertFromV20(v20); - return res; -#else - throw (new Exception("Cannot handle AAS file format http://www.admin-shell.io/aas/1/0 !")); -#endif - } - - // read V3.0? - if (nsuri != null && nsuri.Trim() == Xmlization.NS) - { - // dead-csharp off - //XmlSerializer serializer = new XmlSerializer( - // typeof(AasCore.Aas3_0_RC02.Environment), "http://www.admin-shell.io/aas/3/0"); - //res = serializer.Deserialize(s) as AasCore.Aas3_0_RC02.Environment; - // dead-csharp on - using (var xmlReader = XmlReader.Create(s)) - { - // TODO (MIHO, 2022-12-26): check if could be feature of AAS core - XmlSkipHeader(xmlReader); - res = Xmlization.Deserialize.EnvironmentFrom(xmlReader); - return res; - } - } - - // nope! - return null; - } - // dead-csharp off - //public static JsonSerializer BuildDefaultAasxJsonSerializer() - //{ - // var serializer = new JsonSerializer(); - // serializer.Converters.Add( - // new AdminShellConverters.JsonAasxConverter( - // "modelType", "name")); - // return serializer; - //} - public static T DeserializeFromJSON(string data) where T : IReferable - { - //using (var tr = new StringReader(data)) - //{ - //var serializer = BuildDefaultAasxJsonSerializer(); - //var rf = (T)serializer.Deserialize(tr, typeof(T)); - - var node = System.Text.Json.Nodes.JsonNode.Parse(data); - var rf = Jsonization.Deserialize.IReferableFrom(node); - - return (T)rf; - //} - } - - //public static T DeserializeFromJSON(JToken obj) where T : IReferable - //{ - // if (obj == null) - // return default(T); - // var serializer = BuildDefaultAasxJsonSerializer(); - // var rf = obj.ToObject(serializer); - // return rf; - //} - - ///// - ///// Use this, if DeserializeFromJSON is too tight. - ///// - //public static T DeserializePureObjectFromJSON(string data) - //{ - // using (var tr = new StringReader(data)) - // { - // //var serializer = BuildDefaultAasxJsonSerializer(); - // //var rf = (T)serializer.Deserialize(tr, typeof(T)); - // return null; - // } - //} - // dead-csharp on - } - /// /// This class encapsulates an AdminShellEnvironment and supplementary files into an AASX Package. /// Specifically has the capability to load, update and store .XML, .JSON and .AASX packages. /// - public class AdminShellPackageEnv : IDisposable + public class AdminShellPackageFileBasedEnv : AdminShellPackageEnvBase { // Note: the first array element [0] should be conforming the actual spec (for saving) protected static string[] relTypesOrigin = new[] { @@ -276,25 +58,20 @@ public class AdminShellPackageEnv : IDisposable private string _tempFn = null; - private IEnvironment _aasEnv = new AasCore.Aas3_0.Environment(); private Package _openPackage = null; private readonly ListOfAasSupplementaryFile _pendingFilesToAdd = new ListOfAasSupplementaryFile(); private readonly ListOfAasSupplementaryFile _pendingFilesToDelete = new ListOfAasSupplementaryFile(); - public AdminShellPackageEnv() { } + public AdminShellPackageFileBasedEnv() : base() { } - public AdminShellPackageEnv(AasCore.Aas3_0.Environment env) - { - if (env != null) - _aasEnv = env; - } + public AdminShellPackageFileBasedEnv(AasCore.Aas3_0.IEnvironment env) : base(env) { } - public AdminShellPackageEnv(string fn, bool indirectLoadSave = false) + public AdminShellPackageFileBasedEnv(string fn, bool indirectLoadSave = false) : base() { Load(fn, indirectLoadSave); } - public bool IsOpen + public override bool IsOpen { get { @@ -302,12 +79,12 @@ public bool IsOpen } } - public void SetFilename(string fileName) + public override void SetFilename(string fileName) { _fn = fileName; } - public string Filename + public override string Filename { get { @@ -315,19 +92,6 @@ public string Filename } } - public IEnvironment AasEnv - { - get - { - return _aasEnv; - } - } - - public void SetEnvironment(IEnvironment environment) - { - _aasEnv = environment; - } - private class FindRelTuple { public bool Deprecated { get; set; } @@ -631,19 +395,31 @@ public void LoadFromAasEnvString(string content) } } - public enum SerializationFormat { None, Xml, Json }; - // dead-csharp off - //public static XmlSerializerNamespaces GetXmlDefaultNamespaces() - //{ - // var nss = new XmlSerializerNamespaces(); - // nss.Add("xsi", System.Xml.Schema.XmlSchema.InstanceNamespace); - // nss.Add("aas", "http://www.admin-shell.io/aas/2/0"); - // nss.Add("IEC", "http://www.admin-shell.io/IEC61360/2/0"); - // nss.Add("abac", "http://www.admin-shell.io/aas/abac/2/0"); - // return nss; - //} - // dead-csharp on - public bool SaveAs(string fn, bool writeFreshly = false, SerializationFormat prefFmt = SerializationFormat.None, + public void ClearTaintedFlag(Aas.IIdentifiable idf) + { + // access + if (idf is ITaintedData itd && itd.TaintedData?.Tainted != null) + itd.TaintedData.Tainted = null; + } + + public void ClearAllIdentifiableTaintedFlags() + { + // access + if (AasEnv == null) + return; + + // all + foreach (var aas in this.AasEnv.AllAssetAdministrationShells()) + ClearTaintedFlag(aas); + + foreach (var sm in this.AasEnv.AllSubmodels()) + ClearTaintedFlag(sm); + + foreach (var cd in this.AasEnv.AllConceptDescriptions()) + ClearTaintedFlag(cd); + } + + public override bool SaveAs(string fn, bool writeFreshly = false, SerializationFormat prefFmt = SerializationFormat.None, MemoryStream useMemoryStream = null, bool saveOnlyCopy = false) { // silently fix flaws @@ -679,6 +455,7 @@ public bool SaveAs(string fn, bool writeFreshly = false, SerializationFormat pre writer.Flush(); writer.Close(); s.Flush(); + ClearAllIdentifiableTaintedFlags(); } finally { @@ -717,6 +494,7 @@ public bool SaveAs(string fn, bool writeFreshly = false, SerializationFormat pre Jsonization.Serialize.ToJsonObject(_aasEnv).WriteTo(wr); wr.Flush(); s.Flush(); + ClearAllIdentifiableTaintedFlags(); } finally { @@ -1091,7 +869,7 @@ public bool SaveAs(string fn, bool writeFreshly = false, SerializationFormat pre var mimeType = psfAdd.UseMimeType; // reconcile mime if (mimeType == null && psfAdd.SourceLocalPath != null) - mimeType = AdminShellPackageEnv.GuessMimeType(psfAdd.SourceLocalPath); + mimeType = AdminShellPackageFileBasedEnv.GuessMimeType(psfAdd.SourceLocalPath); // still null? if (mimeType == null) // see: https://stackoverflow.com/questions/6783921/ @@ -1152,6 +930,9 @@ public bool SaveAs(string fn, bool writeFreshly = false, SerializationFormat pre string.Format("While write AASX {0} indirectly at {1} gave: {2}", fn, AdminShellUtil.ShortLocation(ex), ex.Message))); } + + // done + ClearAllIdentifiableTaintedFlags(); } catch (Exception ex) { @@ -1172,9 +953,9 @@ public bool SaveAs(string fn, bool writeFreshly = false, SerializationFormat pre /// /// Action which is to be executed while the file is CLOSED /// Format for the saved file - public void TemporarilySaveCloseAndReOpenPackage( + public override void TemporarilySaveCloseAndReOpenPackage( Action lambda, - AdminShellPackageEnv.SerializationFormat prefFmt = AdminShellPackageEnv.SerializationFormat.None) + AdminShellPackageFileBasedEnv.SerializationFormat prefFmt = AdminShellPackageFileBasedEnv.SerializationFormat.None) { // access if (!this.IsOpen) @@ -1274,162 +1055,150 @@ public void BackupInDir(string backupDir, int maxFiles) } } - public Stream GetStreamFromUriOrLocalPackage(string uriString, - FileMode mode = FileMode.Open, - FileAccess access = FileAccess.Read) - { - // local - if (IsLocalFile(uriString)) - return GetLocalStreamFromPackage(uriString, mode, access); - - // no .. - return System.IO.File.Open(uriString, mode, access); - } - - public byte[] GetByteArrayFromUriOrLocalPackage(string uriString) + public override bool IsLocalFile(string uriString) { - try - { - using (var input = GetStreamFromUriOrLocalPackage(uriString)) - { - using (MemoryStream ms = new MemoryStream()) - { - input.CopyTo(ms); - return ms.ToArray(); - } - } - } - catch (Exception ex) - { - AdminShellNS.LogInternally.That.SilentlyIgnoredError(ex); - return null; - } - } + // look at the uri + var sap = AdminShellUtil.GetSchemeAndPath(uriString); + if (sap == null) + return false; + if (sap.Scheme != "file") + return false; - public bool IsLocalFile(string uriString) - { - // access + // look at package / file path if (_openPackage == null) return false; - if (uriString == null || uriString == "" || !uriString.StartsWith("/")) + if (sap.Path == null || !sap.Path.StartsWith("/")) return false; - // check + // check further var isLocal = _openPackage.PartExists(new Uri(uriString, UriKind.RelativeOrAbsolute)); return isLocal; } - private static WebProxy proxy = null; - - public Stream GetLocalStreamFromPackage(string uriString, FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read) + public override byte[] GetBytesFromPackageOrExternal( + string uriString, + string aasId = null, + string smId = null, + string idShortPath = null) { - // Check, if remote - if (uriString.ToLower().Substring(0, 4) == "http") - { - if (proxy == null) - { - string proxyAddress = ""; - string username = ""; - string password = ""; + // IMPORTANT! First try to use the base implementation to get an stream to + // HTTP or ABSOLUTE file + var absBytes = base.GetBytesFromPackageOrExternal(uriString); + if (absBytes != null) + return absBytes; + + // now, split uri string (again) for ourselves + var sap = AdminShellUtil.GetSchemeAndPath(uriString); + if (sap == null) + return null; - string proxyFile = "proxy.txt"; - if (System.IO.File.Exists(proxyFile)) - { - try - { // Open the text file using a stream reader. - using (StreamReader sr = new StreamReader(proxyFile)) - { - proxyFile = sr.ReadLine(); - } - } - catch (IOException e) - { - Console.WriteLine("proxy.txt could not be read:"); - Console.WriteLine(e.Message); - } - } + // now, it has to be an package file + if (_openPackage == null) + throw (new Exception(string.Format($"AASX Package {_fn} not opened. Aborting!"))); - try - { - using (StreamReader sr = new StreamReader(proxyFile)) - { - proxyAddress = sr.ReadLine(); - username = sr.ReadLine(); - password = sr.ReadLine(); - } - } - catch (Exception e) - { - Console.WriteLine(e.Message); - Console.WriteLine(proxyFile + " not found!"); - } + // exist + var puri = new Uri(sap.Path, UriKind.RelativeOrAbsolute); + if (!_openPackage.PartExists(puri)) + throw (new Exception(string.Format($"AASX Package has no part {uriString}. Aborting!"))); - if (proxyAddress != "") - { - proxy = new WebProxy(); - Uri newUri = new Uri(proxyAddress); - proxy.Address = newUri; - proxy.Credentials = new NetworkCredential(username, password); - Console.WriteLine("Using proxy: " + proxyAddress); - } - } + // get part + var part = _openPackage.GetPart(puri); + if (part == null) + throw (new Exception( + string.Format($"Cannot access part {uriString} in {_fn}. Aborting!"))); - var handler = new HttpClientHandler(); + // read bytes + using (var stream = part.GetStream(FileMode.Open, FileAccess.Read)) + using (MemoryStream ms = new MemoryStream()) + { + stream.CopyTo(ms); + return ms.ToArray(); + } + } - if (proxy != null) - handler.Proxy = proxy; - else - handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials; - var hc = new HttpClient(handler); + public override async Task GetBytesFromPackageOrExternalAsync( + string uriString, + string aasId = null, + string smId = null, + string idShortPath = null) + { + // IMPORTANT! First try to use the base implementation to get an stream to + // HTTP or ABSOLUTE file + var absBytes = await base.GetBytesFromPackageOrExternalAsync(uriString); + if (absBytes != null) + return absBytes; + + // now, split uri string (again) for ourselves + var sap = AdminShellUtil.GetSchemeAndPath(uriString); + if (sap == null) + return null; - var response = hc.GetAsync(uriString).GetAwaiter().GetResult(); + // now, it has to be an package file + if (_openPackage == null) + throw (new Exception(string.Format($"AASX Package {_fn} not opened. Aborting!"))); - // if you call response.EnsureSuccessStatusCode here it will throw an exception - if (response.StatusCode == HttpStatusCode.Moved - || response.StatusCode == HttpStatusCode.Found) - { - var location = response.Headers.Location; - response = hc.GetAsync(location).GetAwaiter().GetResult(); - } + // exist + var puri = new Uri(sap.Path, UriKind.RelativeOrAbsolute); + if (!_openPackage.PartExists(puri)) + throw (new Exception(string.Format($"AASX Package has no part {sap.Path}. Aborting!"))); - response.EnsureSuccessStatusCode(); - var s = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); + // get part + var part = _openPackage.GetPart(puri); + if (part == null) + throw (new Exception( + string.Format($"Cannot access part {sap.Path} in {_fn}. Aborting!"))); - if (s.Length < 500) // indirect load? - { - StreamReader reader = new StreamReader(s); - string json = reader.ReadToEnd(); - var parsed = JObject.Parse(json); - try - { - string url = parsed.SelectToken("url").Value(); - response = hc.GetAsync(url).GetAwaiter().GetResult(); - response.EnsureSuccessStatusCode(); - s = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); - } - catch - { - } - } + // read bytes + using (var stream = part.GetStream(FileMode.Open, FileAccess.Read)) + using (MemoryStream ms = new MemoryStream()) + { + await stream.CopyToAsync(ms); + return ms.ToArray(); + } + } - return s; + public override bool PutBytesToPackageOrExternal( + string uriString, + byte[] data, + string aasId = null, + string smId = null, + string idShortPath = null) + { + // split uri string + var sap = AdminShellUtil.GetSchemeAndPath(uriString); + if (sap == null) + return false; + + // if not a file, refer to base + // also, if package is not open or is no local file + if (sap.Scheme != "file" + || _openPackage == null + || !IsLocalFile(uriString)) + { + return base.PutBytesToPackageOrExternal(uriString, data, aasId, smId, idShortPath); } - // access - if (_openPackage == null) - throw (new Exception(string.Format($"AASX Package {_fn} not opened. Aborting!"))); + // now, we're supposed to handle it! // exist - var puri = new Uri(uriString, UriKind.RelativeOrAbsolute); + var puri = new Uri(sap.Path, UriKind.RelativeOrAbsolute); if (!_openPackage.PartExists(puri)) - throw (new Exception(string.Format($"AASX Package has no part {uriString}. Aborting!"))); + throw (new Exception(string.Format($"AASX Package has no part {sap.Path}. Aborting!"))); // get part var part = _openPackage.GetPart(puri); if (part == null) throw (new Exception( - string.Format($"Cannot access part {uriString} in {_fn}. Aborting!"))); - return part.GetStream(mode, access); + string.Format($"Cannot access part {sap.Path} in {_fn}. Aborting!"))); + + // read bytes + using (var stream = part.GetStream(FileMode.Create, FileAccess.Write)) + using (MemoryStream ms = new MemoryStream(data)) + { + ms.CopyTo(stream); + } + + return true; } public async Task ReplaceSupplementaryFileInPackageAsync(string sourceUri, string targetFile, string targetContentType, Stream fileContent) @@ -1448,7 +1217,7 @@ public async Task ReplaceSupplementaryFileInPackageAsync(string sourceUri, strin fileContent.Position = 0; using (Stream dest = packagePart.GetStream()) { - fileContent.CopyTo(dest); + await fileContent.CopyToAsync(dest); } } @@ -1482,11 +1251,7 @@ public long GetStreamSizeFromPackage(string uriString) return res; } - /// - /// Ensures: - ///
  • result == null || result.CanRead
- ///
- public Stream GetLocalThumbnailStream(ref Uri thumbUri) + public override byte[] GetLocalThumbnailBytes(ref Uri thumbUri) { // access if (_openPackage == null) @@ -1508,36 +1273,16 @@ public Stream GetLocalThumbnailStream(ref Uri thumbUri) if (thumbPart == null) throw (new Exception("Unable to find AASX thumbnail. Aborting!")); - var result = thumbPart.GetStream(FileMode.Open); - - // Post-condition - if (!(result == null || result.CanRead)) - { - throw new InvalidOperationException("Unexpected unreadable result stream"); - } - - return result; - } - - /// - /// Ensures: - ///
  • result == null || result.CanRead
- ///
- public Stream GetLocalThumbnailStream() - { - Uri dummy = null; - var result = GetLocalThumbnailStream(ref dummy); - - // Post-condition - if (!(result == null || result.CanRead)) + // read bytes + using (var stream = thumbPart.GetStream(FileMode.Open, FileAccess.Read)) + using (MemoryStream ms = new MemoryStream()) { - throw new InvalidOperationException("Unexpected unreadable result stream"); + stream.CopyTo(ms); + return ms.ToArray(); } + } - return result; - } - - public ListOfAasSupplementaryFile GetListOfSupplementaryFiles() + public override ListOfAasSupplementaryFile GetListOfSupplementaryFiles() { // new result var result = new ListOfAasSupplementaryFile(); @@ -1648,7 +1393,7 @@ public static string GuessMimeType(string fn) return content_type; } - public void PrepareSupplementaryFileParameters(ref string targetDir, ref string targetFn) + public override void PrepareSupplementaryFileParameters(ref string targetDir, ref string targetFn) { // re-work target dir if (targetDir != null) @@ -1664,7 +1409,7 @@ public void PrepareSupplementaryFileParameters(ref string targetDir, ref string /// materialize embedding. ///
/// Target path of file in package - public string AddSupplementaryFileToStore( + public override string AddSupplementaryFileToStore( string sourcePath, string targetDir, string targetFn, bool embedAsThumb, AdminShellPackageSupplementaryFile.SourceGetByteChunk sourceGetBytesDel = null, string useMimeType = null) { @@ -1714,7 +1459,7 @@ public void AddSupplementaryFileToStore(string sourcePath, string targetPath, bo } - public void DeleteSupplementaryFile(AdminShellPackageSupplementaryFile psf) + public override void DeleteSupplementaryFile(AdminShellPackageSupplementaryFile psf) { if (psf == null) throw (new Exception("No supplementary file given!")); @@ -1732,7 +1477,7 @@ public void DeleteSupplementaryFile(AdminShellPackageSupplementaryFile psf) } } - public void Close() + public override void Close() { _openPackage?.Close(); _openPackage = null; @@ -1740,49 +1485,17 @@ public void Close() _aasEnv = null; } - public void Flush() + public override void Flush() { if (_openPackage != null) _openPackage.Flush(); } - public void Dispose() + public override void Dispose() { Close(); } - public string MakePackageFileAvailableAsTempFile(string packageUri, bool keepFilename = false) - { - // access - if (packageUri == null) - return null; - - // get input stream - using (var input = GetLocalStreamFromPackage(packageUri)) - { - // generate tempfile name - string tempext = System.IO.Path.GetExtension(packageUri); - string temppath = System.IO.Path.GetTempFileName().Replace(".tmp", tempext); - - // maybe modify tempfile name? - if (keepFilename) - { - var masterFn = System.IO.Path.GetFileNameWithoutExtension(packageUri); - var tmpDir = System.IO.Path.GetDirectoryName(temppath); - var tmpFnExt = System.IO.Path.GetFileName(temppath); - - temppath = System.IO.Path.Combine(tmpDir, "" + masterFn + "_" + tmpFnExt); - } - - // copy to temp file - using (var temp = System.IO.File.OpenWrite(temppath)) - { - input.CopyTo(temp); - return temppath; - } - } - } - public void EmbeddAssetInformationThumbnail(IResource defaultThumbnail, Stream fileContent) { // access diff --git a/src/AasxCsharpLibrary/AdminShellUtil.cs b/src/AasxCsharpLibrary/AdminShellUtil.cs index f4c0def54..de379781a 100644 --- a/src/AasxCsharpLibrary/AdminShellUtil.cs +++ b/src/AasxCsharpLibrary/AdminShellUtil.cs @@ -9,9 +9,11 @@ This source code may use other Open Source software components (see LICENSE.txt) using AasxCompatibilityModels; using Extensions; +using Microsoft.IdentityModel.Tokens; using System; using System.Collections; using System.Collections.Generic; +using System.Dynamic; using System.Globalization; using System.IO; using System.IO.Packaging; @@ -262,10 +264,12 @@ public static string[] GetPopularMimeTypes() "image/png", System.Net.Mime.MediaTypeNames.Image.Gif, "application/iges", - "application/step" + "application/step", + "application/octet-stream" }; } + public static bool CheckForTextContentType(string input) { if (input == null) @@ -284,6 +288,40 @@ public static bool CheckForTextContentType(string input) return true; return false; } + + public static string GuessExtension(string contentType = null, byte[] contents = null) + { + if (contentType?.HasContent() == true) + { + var list = GetPopularMimeTypes().ToList(); + var p = list.IndexOf(contentType); + if (p >= 0) + return (new[] { + ".txt", + ".xml", + ".html", + ".md", + ".adoc", + ".json", + ".rdf", + ".pdf", + ".jpg", + ".png", + ".gif", + ".iges", + ".stp" + })[p]; + } + + // ok, guess by bytes + if (contents != null && contents.Length > 0) + { + return GuessImageTypeExtension(contents); + } + + // ok, nop + return ".tmp"; + } public static IEnumerable GetAdequateEnums(AasSubmodelElements[] excludeValues = null, AasSubmodelElements[] includeValues = null) { @@ -493,9 +531,10 @@ public static string ToSingleLineShortened(string str, int maxLen, string textNe /// Assert.AreEqual("someName", AdminShellUtil.FilterFriendlyName("someName")); /// Assert.AreEqual("some__name", AdminShellUtil.FilterFriendlyName("some!;name")); /// - public static string FilterFriendlyName(string src, + public static string FilterFriendlyName(string src, bool pascalCase = false, - bool fixMoreBlanks = false) + bool fixMoreBlanks = false, + string regexForFilter = null) { if (src == null) return null; @@ -503,7 +542,8 @@ public static string FilterFriendlyName(string src, if (pascalCase && src.Length > 0) src = char.ToUpper(src[0]) + src.Substring(1); - src = Regex.Replace(src, @"[^a-zA-Z0-9\-_]", "_"); + var regex = regexForFilter ?? @"[^a-zA-Z0-9_]"; + src = Regex.Replace(src, regex, "_"); if (fixMoreBlanks) { @@ -603,6 +643,128 @@ public static string FromDouble(double input, string format) return string.Format(CultureInfo.InvariantCulture, format, input); } + /// + /// Checks a given string to be float compatible. + /// + public static bool IsFloatingPointString(string input) + { + var res = double.TryParse(input, NumberStyles.Float, CultureInfo.InvariantCulture, out var f); + return res; + } + + /// + /// Fixes a given string to be float compatible. + /// + /// If the string was fixed. + public static bool FixFloatingPointString(ref string valstr, string noneResult = "0.0") + { + if (valstr?.HasContent() != true) + { + valstr = noneResult; + return true; + } + + if (IsFloatingPointString(valstr)) + return false; + + var res = ""; + foreach (var c in valstr) + if (c == ',') + res += '.'; + else if ("0123456789.+-E".IndexOf(c) >= 0) + res += c; + valstr = res; + + if (!IsFloatingPointString(valstr)) + valstr = noneResult; + + // was altered + return true; + } + + /// + /// Checks a given string to be float compatible. + /// + public static bool IsIntegerString(string input) + { + var res = Int64.TryParse(input, NumberStyles.Any, CultureInfo.InvariantCulture, out var i); + return res; + } + + /// + /// Fixes a given string to be float compatible. + /// + /// If the string was fixed. + public static bool FixIntegerString(ref string valstr, string noneResult = "0.0") + { + if (valstr?.HasContent() != true) + { + valstr = noneResult; + return true; + } + + if (IsIntegerString(valstr)) + return false; + + var res = ""; + foreach (var c in valstr) + if ("0123456789-".IndexOf(c) >= 0) + res += c; + valstr = res; + + if (!IsIntegerString(valstr)) + valstr = noneResult; + + // was altered + return true; + } + + /// + /// Checks if a given string is a ISO 639-1 language code; here: 2 digits only lower case + /// + public static bool IsIso6391LangCode(string input) + { + // access + if (input == null) + return false; + + // directly filter + var test = ""; + foreach (var c in input) + if ("abcdefghijklmnopqrstuvwxyz".IndexOf(c) >= 0) + test += c; + + return input == test && input.Length == 2; + } + + /// + /// Fixes a given string to be float compatible. + /// + /// If the string was fixed. + public static bool FixIso6391LangCode(ref string valstr, string noneResult = "en") + { + if (valstr?.HasContent() != true) + { + valstr = noneResult; + return true; + } + + if (IsIso6391LangCode(valstr)) + return false; + + var res = ""; + foreach (var c in valstr) + if ("abcdefghijklmnopqrstuvwxyz".IndexOf(c) >= 0) + res += c; + valstr = res; + + if (!IsIso6391LangCode(valstr)) + valstr = noneResult; + + // was altered + return true; + } + public static int CountHeadingSpaces(string line) { if (line == null) @@ -771,6 +933,44 @@ public static string CorrectCasingForConstantStringArray(string[] arr, string st // String manipulations // + public static List StringSplitUnquoted( + string input, + char splitChar, + StringSplitOptions options = StringSplitOptions.None) + { + var curr = ""; + var res = new List(); + + Action issue = (str) => + { + if ((options & StringSplitOptions.TrimEntries) != 0) + str = str.Trim(); + if (str == "" && (options & StringSplitOptions.RemoveEmptyEntries) != 0) + return; + res.Add(str); + }; + + foreach (var ci in input) + { + // split? + if (ci == splitChar) + { + issue(curr); + curr = ""; + continue; + } + + // no, add + curr += ci; + } + + // issue (again)? + issue(curr); + + // ok + return res; + } + public static string ReplacePercentPlaceholder( string input, string searchFor, @@ -870,14 +1070,29 @@ public static Type GetTypeOrUnderlyingType(Type type, bool resolveGeneric = fals /// for reflection of type specific data. /// Works for most scalars, dateTime, string. ///
- public static void SetFieldLazyValue(FieldInfo f, object obj, object value) + public static void SetFieldLazyValue( + FieldInfo f, object obj, object value, + bool enableEnums = false) { // access if (f == null || obj == null) return; + var tut = GetTypeOrUnderlyingType(f.FieldType); + + // enum? + if (enableEnums && tut?.IsEnum == true && value is string vstr) + { + foreach (var v in Enum.GetValues(tut)) + if (v.ToString().ToLower() == vstr.Trim().ToLower()) + { + f.SetValue(obj, v); + } + return; + } + // 2024-01-04: make function more suitable for - switch (Type.GetTypeCode(GetTypeOrUnderlyingType(f.FieldType))) + switch (Type.GetTypeCode(tut)) { case TypeCode.String: f.SetValue(obj, "" + value); @@ -1058,6 +1273,25 @@ public static void AddToListLazyValue(object obj, object value) } } + /// see: https://stackoverflow.com/questions/9956648/how-do-i-check-if-a-property-exists-on-a-dynamic-anonymous-type-in-c + /// see: https://stackoverflow.com/questions/63972270/newtonsoft-json-check-if-property-and-its-value-exists + public static bool DynamicHasProperty(dynamic obj, string name) + { + Type objType = obj.GetType(); + + if (obj is Newtonsoft.Json.Linq.JObject jo) + { + return jo.ContainsKey(name); + } + + if (objType == typeof(ExpandoObject)) + { + return ((IDictionary)obj).ContainsKey(name); + } + + return objType.GetProperty(name) != null; + } + public static string ToStringInvariant(object o) { // trivial @@ -1178,6 +1412,38 @@ public static string GetExtensionWoQuery(string fn) return ext; } + public class SchemeAndPath + { + public string Scheme = ""; + public string Path = ""; + } + + /// + /// Split info. Limits to 5 character schemes! + /// + /// null for any error! + public static SchemeAndPath GetSchemeAndPath(string uri) + { + // error? + if (uri?.HasContent() != true) + return null; + + // search :// + var p = uri.IndexOf("://"); + if (p > 5) + return null; + + // nothing? + if (p < 0) + return new SchemeAndPath() { Scheme = "file", Path = uri }; + + // split + return new SchemeAndPath() { + Scheme = uri.Substring(0, p).ToLower(), + Path = uri.Substring(p+3) + }; + } + // // Base 64 // @@ -1194,6 +1460,22 @@ public static string Base64Decode(string base64EncodedData) return System.Text.Encoding.UTF8.GetString(base64EncodedBytes); } + // + // Base 64 URL + // + + // Note: requires Microsoft.IdentityModel.Tokens + + public static string Base64UrlEncode(string plainUrl) + { + return Base64UrlEncoder.Encode(plainUrl); + } + + public static string Base64UrlDecode(string base64Url) + { + return Base64UrlEncoder.Decode(base64Url); + } + /// /// Tries to add scheme etc. to form a valid URI. /// uri with starting '/' are left untouched. @@ -1234,6 +1516,38 @@ public static bool CheckIfAsciiOnly(byte[] data, int bytesToCheck = int.MaxValue return ascii; } + public static bool CheckIfBase64Only(byte[] data, int bytesToCheck = int.MaxValue) + { + if (data == null) + return true; + + var b64 = true; + for (int i = 0; i < Math.Min(data.Length, bytesToCheck); i++) + { + var c = data[i]; + // 'manually' check for allowed char intervals of BASE64 + if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '+' || c == '/' || c == '=')) + b64 = false; + } + return b64; + } + + public static bool CheckIfBase64Only(string data, int bytesToCheck = int.MaxValue) + { + if (data == null) + return true; + + var b64 = true; + for (int i = 0; i < Math.Min(data.Length, bytesToCheck); i++) + { + var c = data[i]; + // 'manually' check for allowed char intervals of BASE64 + if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '+' || c == '/' || c == '=')) + b64 = false; + } + return b64; + } + // see: https://stackoverflow.com/questions/5209506/how-can-i-know-what-image-format-i-get-from-a-stream // based on https://devblogs.microsoft.com/scripting/psimaging-part-1-test-image/ // see https://en.wikipedia.org/wiki/List_of_file_signatures @@ -1408,5 +1722,17 @@ public static string GetDefaultLngIso639() { return DefaultLngIso639; } + + // + // Bytes + // + + public static string GetStringFromBytes(byte[] byteArray) + { + if (byteArray == null) + return null; + return System.Text.Encoding.UTF8.GetString(byteArray); + } + } } diff --git a/src/AasxCsharpLibrary/Extensions/ExtendAssetAdministrationShell.cs b/src/AasxCsharpLibrary/Extensions/ExtendAssetAdministrationShell.cs index f929c7d28..e6612902e 100644 --- a/src/AasxCsharpLibrary/Extensions/ExtendAssetAdministrationShell.cs +++ b/src/AasxCsharpLibrary/Extensions/ExtendAssetAdministrationShell.cs @@ -32,17 +32,19 @@ public static Tuple ToCaptionInfo(this IAssetAdministrationShell public static IEnumerable FindAllReferences(this IAssetAdministrationShell assetAdministrationShell) { - // dead-csharp off - // Asset - //TODO (jtikekar, 0000-00-00): support asset - //if (assetAdministrationShell.AssetInformation != null) - // yield return new LocatedReference(assetAdministrationShell, assetAdministrationShell.AssetInformation); - // dead-csharp on - // Submodel references foreach (var r in assetAdministrationShell.AllSubmodels()) yield return new LocatedReference(assetAdministrationShell, r); } + public static IEnumerable FindAllSubmodelReferences(this IAssetAdministrationShell assetAdministrationShell) + { + // unique set of references + var refs = new List(); + foreach (var smr in assetAdministrationShell.AllSubmodels()) + refs.AddIfNew(new LocatedReference() { Identifiable = assetAdministrationShell, Reference = smr }); + return refs; + } + #endregion public static IReference FindSubmodelReference(this IAssetAdministrationShell aas, IReference smRef) diff --git a/src/AasxCsharpLibrary/Extensions/ExtendBlob.cs b/src/AasxCsharpLibrary/Extensions/ExtendBlob.cs index d8ee065e8..0beb6c8d7 100644 --- a/src/AasxCsharpLibrary/Extensions/ExtendBlob.cs +++ b/src/AasxCsharpLibrary/Extensions/ExtendBlob.cs @@ -66,6 +66,8 @@ public static Blob UpdateFrom(this Blob elem, ISubmodelElement source) { if (srcFile.Value != null) elem.Value = Encoding.Default.GetBytes(srcFile.Value); + if (srcFile.ContentType != null) + elem.ContentType = srcFile.ContentType; } return elem; diff --git a/src/AasxCsharpLibrary/Extensions/ExtendCollection.cs b/src/AasxCsharpLibrary/Extensions/ExtendCollection.cs index 3acde94a4..e1dde1f0f 100644 --- a/src/AasxCsharpLibrary/Extensions/ExtendCollection.cs +++ b/src/AasxCsharpLibrary/Extensions/ExtendCollection.cs @@ -13,5 +13,15 @@ public static bool IsNullOrEmpty(this List list) return true; } + + public static bool IsNullOrEmpty(this IList list) + { + if (list != null && list.Count != 0) + { + return false; + } + + return true; + } } } diff --git a/src/AasxCsharpLibrary/Extensions/ExtendConceptDescription.cs b/src/AasxCsharpLibrary/Extensions/ExtendConceptDescription.cs index 40bb2bdc3..2ae87b156 100644 --- a/src/AasxCsharpLibrary/Extensions/ExtendConceptDescription.cs +++ b/src/AasxCsharpLibrary/Extensions/ExtendConceptDescription.cs @@ -298,7 +298,7 @@ public static void AddIsCaseOf(this IConceptDescription cd, /// /// Returns false, if there is another element with same idShort in the list /// - public static bool CheckIdShortIsUnique(this List cds, string idShort) + public static bool CheckIdShortIsUnique(this IList cds, string idShort) { idShort = idShort?.Trim(); if (idShort == null || idShort.Length < 1) @@ -318,7 +318,7 @@ public static bool CheckIdShortIsUnique(this List cds, stri /// /// Creates ids with numerical index according to template string, until a unique idShort is found /// - public static string IterateIdShortTemplateToBeUnique(this List cds, string idShortTemplate, int maxNum) + public static string IterateIdShortTemplateToBeUnique(this IList cds, string idShortTemplate, int maxNum) { if (idShortTemplate == null || maxNum < 1 || !idShortTemplate.Contains("{0")) return null; diff --git a/src/AasxCsharpLibrary/Extensions/ExtendEnvironment.cs b/src/AasxCsharpLibrary/Extensions/ExtendEnvironment.cs index 3d07975a9..bd37a9999 100644 --- a/src/AasxCsharpLibrary/Extensions/ExtendEnvironment.cs +++ b/src/AasxCsharpLibrary/Extensions/ExtendEnvironment.cs @@ -8,6 +8,7 @@ This source code may use other Open Source software components (see LICENSE.txt) */ using AdminShellNS; using AdminShellNS.Extensions; +using JetBrains.Annotations; using Newtonsoft.Json; using System; using System.Collections; @@ -468,7 +469,7 @@ public static IConceptDescription Add(this AasCore.Aas3_0.IEnvironment env, ICon if (cd == null) return null; if (env.ConceptDescriptions == null) - env.ConceptDescriptions = new(); + env.ConceptDescriptions = new List(); env.ConceptDescriptions.Add(cd); return cd; } @@ -505,7 +506,7 @@ public static ISubmodel Add(this AasCore.Aas3_0.IEnvironment env, ISubmodel sm) if (sm == null) return null; if (env.Submodels == null) - env.Submodels = new(); + env.Submodels = new List(); env.Submodels.Add(sm); return sm; } @@ -519,7 +520,7 @@ public static IAssetAdministrationShell Add(this AasCore.Aas3_0.IEnvironment env if (aas == null) return null; if (env.AssetAdministrationShells == null) - env.AssetAdministrationShells = new(); + env.AssetAdministrationShells = new List(); env.AssetAdministrationShells.Add(aas); return aas; } @@ -820,7 +821,33 @@ public static IReferable FindReferableByReference( rootInfo.NrOfRootKeys = 1 + keyIndex; } - // give back the AAS + // test if to go further for Submodel + if (keyIndex < keyList.Count - 1 + && keyList[keyIndex + 1].Type == KeyTypes.Submodel) + { + var foundSm = environment.FindAllSubmodelGroupedByAAS((aas, sm) + => aas == keyedAas && sm.Id?.Trim() == keyList[keyIndex + 1].Value?.Trim()) + .FirstOrDefault(); + + if (foundSm != null) + { + keyIndex += 2; + + var foundSme = environment.FindReferableByReference(reference, keyIndex, + foundSm, foundSm.SubmodelElements); + + if (foundSme != null) + { + return foundSme; + } + else + { + return foundSm; + } + } + } + + // nope, give back the AAS return keyedAas; } @@ -878,9 +905,7 @@ public static IReferable FindReferableByReference( return environment.FindReferableByReference(reference, ++keyIndex, submodel, submodel.SubmodelElements); } - } - - + } if (firstKeyType.IsSME() && submodelElems != null) { @@ -964,6 +989,178 @@ public static IEnumerable FindAllReferences(this AasCore.Aas3_ yield return new LocatedReference(cd, r); } + // TODO: Integrate into above function + public static IEnumerable FindAllSubmodelReferences( + this AasCore.Aas3_0.IEnvironment environment, + bool onlyNotExisting = false) + { + // unique set of references + var refs = new List(); + foreach (var aas in environment.AllAssetAdministrationShells()) + foreach (var smr in aas?.AllSubmodels()) + refs.AddIfNew(new LocatedReference() { Identifiable = aas, Reference = smr}); + + // only existing + foreach (var lr in refs) + if (!onlyNotExisting + || null == environment.FindSubmodel(lr?.Reference)) + yield return lr; + } + + /// + /// Warning: very inefficient! + /// + public static IEnumerable FindAllReferencedCdsForSubmodel( + this AasCore.Aas3_0.IEnvironment env, + ISubmodel sm) + { + // unique set of references + var refs = new List(); + sm?.RecurseOnSubmodelElements(null, (state, parents, sme) => + { + if (sme.SemanticId != null) + { + var cd = env?.FindConceptDescriptionByReference(sme.SemanticId); + if (cd != null) + refs.Add(cd); + } + + // recurse + return true; + }); + + return refs.Distinct(); + } + + /// + /// Warning: very inefficient! + /// + public static IEnumerable FindAllSemanticIdsForSubmodel( + this AasCore.Aas3_0.IEnvironment environment, + ISubmodel sm) + { + // unique set of references + var refs = new List(); + sm?.RecurseOnSubmodelElements(null, (state, parents, sme) => + { + if (sme.SemanticId != null) + refs.AddIfNew(new LocatedReference() { Identifiable = sm, Reference = sme.SemanticId }); + + // recurse + return true; + }); + + return refs; + } + + /// + /// Warning: very inefficient! + /// + public static IEnumerable FindAllSemanticIdsForAas( + this AasCore.Aas3_0.IEnvironment environment, + IAssetAdministrationShell aas) + { + // unique set of references + var refs = new List(); + foreach (var smr in aas?.AllSubmodels()) + { + var sm = environment.FindSubmodel(smr); + refs.AddRange(environment.FindAllSemanticIdsForSubmodel(sm)); + } + + return refs; + } + + /// + /// Warning: very inefficient! + /// + public static IEnumerable FindAllReferencedSemanticIds( + this AasCore.Aas3_0.IEnvironment env) + { + // unique set of references + var refs = new List(); + + foreach (var aas in env.AllAssetAdministrationShells()) + refs.AddRange(env.FindAllSemanticIdsForAas(aas)); + + return refs; + } + + /// + /// Warning: very inefficient! + /// + public static IEnumerable FindAllReferencedIdentifiablesForAas( + this AasCore.Aas3_0.IEnvironment environment, + IAssetAdministrationShell aas) + { + // unique set of references + var refs = new List(); + foreach (var smr in aas?.AllSubmodels()) + { + var sm = environment.FindSubmodel(smr); + if (sm == null) + continue; + + refs.Add(sm); + refs.AddRange(environment.FindAllReferencedCdsForSubmodel(sm)); + } + + return refs; + } + + public static IEnumerable FindAllSubmodelReferencesForAAS( + this AasCore.Aas3_0.IEnvironment environment, + IAssetAdministrationShell aas) + { + // set of references + var refs = new List(); + foreach (var smr in aas?.AllSubmodels()) + { + var sm = environment.FindSubmodel(smr); + if (sm != null) + refs.AddIfNew(new LocatedReference() { Identifiable = sm, Reference = smr }); + } + return refs; + } + + /// + /// Warning: very inefficient! + /// + public static IEnumerable FindAllReferencedIdentifiablesFor( + this AasCore.Aas3_0.IEnvironment env, + IIdentifiable idf, + bool makeDistint = true) + { + // set of references + var refs = new List(); + + if (idf is IAssetAdministrationShell aas) + { + refs.Add(aas); + refs.AddRange(env.FindAllReferencedIdentifiablesForAas(aas)); + } + + if (idf is ISubmodel sm) + { + refs.Add(sm); + refs.AddRange(env.FindAllReferencedCdsForSubmodel(sm)); + } + + if (idf is IConceptDescription cd) + { + refs.Add(cd); + } + + // more distinct? + if (makeDistint) + { + var refs2 = refs.Distinct(); + return refs2; + } + else + return refs; + } + /// /// Tries renaming an Identifiable, specifically: the identification of an Identifiable and /// all references to it. diff --git a/src/AasxCsharpLibrary/Extensions/ExtendIIdentifiable.cs b/src/AasxCsharpLibrary/Extensions/ExtendIIdentifiable.cs index 97aa254c9..db8e0b922 100644 --- a/src/AasxCsharpLibrary/Extensions/ExtendIIdentifiable.cs +++ b/src/AasxCsharpLibrary/Extensions/ExtendIIdentifiable.cs @@ -6,8 +6,10 @@ This source code is licensed under the Apache License 2.0 (see LICENSE.txt). This source code may use other Open Source software components (see LICENSE.txt). */ +using System; using System.Collections.Generic; using System.Linq; +using AdminShellNS.DiaryData; namespace Extensions { @@ -21,6 +23,7 @@ public static string ToStringExtended(this List identifiables, st } #endregion + public static IReference GetReference(this IIdentifiable identifiable) { var key = new Key(ExtensionsUtil.GetKeyType(identifiable), identifiable.Id); @@ -29,5 +32,17 @@ public static IReference GetReference(this IIdentifiable identifiable) return outputReference; } + + public static void SetTainted(this IIdentifiable identifiable, bool state) + { + var tidf = identifiable as ITaintedData; + if (tidf != null) + { + if (state) + tidf.TaintedData.Tainted = DateTime.UtcNow; + else + tidf.TaintedData.Tainted = null; + } + } } } diff --git a/src/AasxCsharpLibrary/Extensions/ExtendIReferable.cs b/src/AasxCsharpLibrary/Extensions/ExtendIReferable.cs index e09ce03a0..726f63559 100644 --- a/src/AasxCsharpLibrary/Extensions/ExtendIReferable.cs +++ b/src/AasxCsharpLibrary/Extensions/ExtendIReferable.cs @@ -465,13 +465,19 @@ public static Submodel GetParentSubmodel(this IReferable referable) return parent as Submodel; } - public static string CollectIdShortByParent(this IReferable referable) + public static string CollectIdShortByParent( + this IReferable referable, + char separatorChar = '/', + bool excludeIdentifiable = false) { // recurse first var head = ""; - if (referable is not IIdentifiable && referable.Parent is IReferable parentReferable) + if (referable is not IIdentifiable + && referable.Parent is IReferable parentReferable + && (!excludeIdentifiable || parentReferable is not IIdentifiable)) // can go up - head = parentReferable.CollectIdShortByParent() + "/"; + head = parentReferable.CollectIdShortByParent(separatorChar, excludeIdentifiable) + + separatorChar; // add own var myid = ""; if (!string.IsNullOrEmpty(referable.IdShort)) diff --git a/src/AasxCsharpLibrary/Extensions/ExtendISubmodelElement.cs b/src/AasxCsharpLibrary/Extensions/ExtendISubmodelElement.cs index 30693a8a9..f625e4716 100644 --- a/src/AasxCsharpLibrary/Extensions/ExtendISubmodelElement.cs +++ b/src/AasxCsharpLibrary/Extensions/ExtendISubmodelElement.cs @@ -1043,7 +1043,7 @@ public static IEnumerable FindAllSemanticIdAs(this List where T : ISubmodelElement { if (submodelELements.IsNullOrEmpty()) - yield return default(T); + yield break; foreach (var submodelElement in submodelELements) if (submodelElement != null && submodelElement is T && submodelElement.SemanticId != null) diff --git a/src/AasxCsharpLibrary/Extensions/ExtendKey.cs b/src/AasxCsharpLibrary/Extensions/ExtendKey.cs index 6f16eb0a0..9ca092f11 100644 --- a/src/AasxCsharpLibrary/Extensions/ExtendKey.cs +++ b/src/AasxCsharpLibrary/Extensions/ExtendKey.cs @@ -155,6 +155,13 @@ public static bool IsAbsolute(this IKey key) return key.Type == KeyTypes.GlobalReference || key.Type == KeyTypes.AssetAdministrationShell || key.Type == KeyTypes.Submodel; } + public static bool HasSuspicousWhiteSpace(this IKey key) + { + if (key?.Value?.HasContent() != true) + return false; + return AdminShellUtil.HasWhitespace(key.Value); + } + public static Key Parse(string cell, KeyTypes typeIfNotSet = KeyTypes.GlobalReference, bool allowFmtAll = false, bool allowFmt0 = false, bool allowFmt1 = false, bool allowFmt2 = false) diff --git a/src/AasxCsharpLibrary/Extensions/ExtendReference.cs b/src/AasxCsharpLibrary/Extensions/ExtendReference.cs index 63347d622..24c021b71 100644 --- a/src/AasxCsharpLibrary/Extensions/ExtendReference.cs +++ b/src/AasxCsharpLibrary/Extensions/ExtendReference.cs @@ -6,6 +6,7 @@ This source code is licensed under the Apache License 2.0 (see LICENSE.txt). This source code may use other Open Source software components (see LICENSE.txt). */ +using AdminShellNS; using AdminShellNS.Exceptions; using System; using System.Collections.Generic; @@ -53,6 +54,26 @@ public static bool IsValid(this List references) return isValid; } + public static bool HasSuspicousWhiteSpace(this IReference reference) + { + if (reference == null || reference.Count() < 1) + return false; + foreach (var key in reference.Keys) + if (key.HasSuspicousWhiteSpace()) + return true; + return false; + } + + public static bool HasSuspicousWhiteSpace(this List references) + { + if (references == null || references.Count() < 1) + return false; + foreach (var rf in references) + if (rf?.HasSuspicousWhiteSpace() == true) + return true; + return false; + } + /// /// Formaly a static constructor. /// Creates a Reference from a key, guessing Reference.Type. diff --git a/src/AasxCsharpLibrary/Extensions/ExtendSubmodel.cs b/src/AasxCsharpLibrary/Extensions/ExtendSubmodel.cs index feafcf59f..bced6ce6b 100644 --- a/src/AasxCsharpLibrary/Extensions/ExtendSubmodel.cs +++ b/src/AasxCsharpLibrary/Extensions/ExtendSubmodel.cs @@ -372,7 +372,7 @@ public static IReference GetSemanticRef(this Submodel submodel) } } - public static List SmeForWrite(this Submodel submodel) + public static List SmeForWrite(this ISubmodel submodel) { if (submodel.SubmodelElements == null) submodel.SubmodelElements = new(); diff --git a/src/AasxCsharpLibrary/Extensions/ExtendSubmodelElementCollection.cs b/src/AasxCsharpLibrary/Extensions/ExtendSubmodelElementCollection.cs index 47607c9d0..27fc79e7a 100644 --- a/src/AasxCsharpLibrary/Extensions/ExtendSubmodelElementCollection.cs +++ b/src/AasxCsharpLibrary/Extensions/ExtendSubmodelElementCollection.cs @@ -77,7 +77,7 @@ public static object AddChild(this SubmodelElementCollection submodelElementColl public static T FindFirstIdShortAs(this SubmodelElementCollection submodelElementCollection, string idShort) where T : ISubmodelElement { - var submodelElement = submodelElementCollection.Value.Where(sme => (sme != null) && (sme is T) && sme.IdShort.Equals(idShort, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); + var submodelElement = submodelElementCollection.Value.Where(sme => (sme != null) && (sme is T) && sme.IdShort?.Equals(idShort, StringComparison.OrdinalIgnoreCase) == true).FirstOrDefault(); return (T)submodelElement; } diff --git a/src/AasxCsharpLibrary/Extensions/LocatedReference.cs b/src/AasxCsharpLibrary/Extensions/LocatedReference.cs index 26916c5b7..2b7fe285e 100644 --- a/src/AasxCsharpLibrary/Extensions/LocatedReference.cs +++ b/src/AasxCsharpLibrary/Extensions/LocatedReference.cs @@ -6,6 +6,10 @@ This source code is licensed under the Apache License 2.0 (see LICENSE.txt). This source code may use other Open Source software components (see LICENSE.txt). */ +using System.Linq; +using System.Collections; +using System.Collections.Generic; + namespace Extensions { public class LocatedReference @@ -20,4 +24,36 @@ public LocatedReference(IIdentifiable identifiable, IReference reference) Reference = reference; } } + + /// + /// This comparer takes a shortcut and does ONLY compare the object references, but + /// NOT the complicated AAS references. In a LocatedReference they are assumed + /// to be equivalent to each other!! + /// + public class LocatedReferenceComparer : IEqualityComparer + { + bool IEqualityComparer.Equals(LocatedReference x, LocatedReference y) + { + if (x == null && y == null) + return true; + if (x == null || y == null) + return false; + return (x.Identifiable == y.Identifiable); + } + + int IEqualityComparer.GetHashCode(LocatedReference obj) + { + return obj?.Identifiable?.GetHashCode() ?? 0; + } + } + + public static class LocatedReferenceExtensions + { + public static void AddIfNew(this IList refs, LocatedReference newLR) + { + var same = refs?.FirstOrDefault((lr) => lr?.Reference?.Matches(newLR?.Reference, MatchMode.Relaxed) == true); + if (same == null) + refs.Add(newLR); + } + } } diff --git a/src/AasxFormatCst/AasxToCst.cs b/src/AasxFormatCst/AasxToCst.cs index 7aec6edf9..dc046d06c 100644 --- a/src/AasxFormatCst/AasxToCst.cs +++ b/src/AasxFormatCst/AasxToCst.cs @@ -21,7 +21,7 @@ namespace AasxFormatCst { public class AasxToCst { - protected AdminShellPackageEnv _env; + protected AdminShellPackageEnvBase _env; protected int _customIndex = 1; public string CustomNS = "UNSPEC"; @@ -267,7 +267,7 @@ private void RecurseOnSme( } public void ExportSingleSubmodel( - AdminShellPackageEnv env, string path, + AdminShellPackageEnvBase env, string path, Aas.Key smId, IEnumerable cdReferables, CstIdObjectBase firstNodeId, diff --git a/src/AasxIntegrationBase/AasForms/AasFormUtils.cs b/src/AasxIntegrationBase/AasForms/AasFormUtils.cs index 75e26c01f..873b8d690 100644 --- a/src/AasxIntegrationBase/AasForms/AasFormUtils.cs +++ b/src/AasxIntegrationBase/AasForms/AasFormUtils.cs @@ -213,7 +213,7 @@ private static void RecurseExportAsTemplate( } } - public static void ExportAsTemplate(AdminShellPackageEnv package, string fn) + public static void ExportAsTemplate(AdminShellPackageEnvBase package, string fn) { // access if (fn == null || package == null || package.AasEnv == null) diff --git a/src/AasxIntegrationBase/AasForms/FormInstance.cs b/src/AasxIntegrationBase/AasForms/FormInstance.cs index ee7c951ed..8439c5adc 100644 --- a/src/AasxIntegrationBase/AasForms/FormInstance.cs +++ b/src/AasxIntegrationBase/AasForms/FormInstance.cs @@ -320,7 +320,7 @@ public FormDescInstancesPair FindInstance(FormInstanceListOfSame searchInst) /// public List AddOrUpdateDifferentElementsToCollection( List elements, - AdminShellPackageEnv packageEnv = null, bool addFilesToPackage = false) + AdminShellPackageEnvBase packageEnv = null, bool addFilesToPackage = false) { // will be a list of newly added elements (for tracing) var res = new List(); @@ -480,7 +480,7 @@ public void PresetInstancesBasedOnSource(List sourceElemen /// Render the form description and adds or updates its instances into a list of SubmodelElements. /// public List AddOrUpdateSameElementsToCollection( - List elements, AdminShellPackageEnv packageEnv = null, + List elements, AdminShellPackageEnvBase packageEnv = null, bool addFilesToPackage = false) { // access @@ -1057,7 +1057,7 @@ public void PresetInstancesBasedOnSource(List sourceElemen /// public List AddOrUpdateDifferentElementsToCollection( List elements, - AdminShellPackageEnv packageEnv = null, + AdminShellPackageEnvBase packageEnv = null, bool addFilesToPackage = false, bool editSource = false) { @@ -1165,7 +1165,7 @@ protected void InitReferable(FormDescSubmodelElement desc, Aas.ISubmodelElement ///
/// True, if a new element shall be rendered from the instance sme. public virtual bool ProcessSmeForRender( - AdminShellPackageEnv packageEnv = null, bool addFilesToPackage = false, bool editSource = false) + AdminShellPackageEnvBase packageEnv = null, bool addFilesToPackage = false, bool editSource = false) { if (this.sme != null && Touched && this.sourceSme != null && editSource) { @@ -1183,7 +1183,7 @@ public virtual bool ProcessSmeForRender( ///
public virtual List AddOrUpdateSmeToCollection( List collectionNewElements, - AdminShellPackageEnv packageEnv = null, bool addFilesToPackage = false) + AdminShellPackageEnvBase packageEnv = null, bool addFilesToPackage = false) { // typically, there will be only one SME var res = new List(); @@ -1280,7 +1280,7 @@ public FormInstanceListOfDifferent GetListOfDifferent() ///
public override List AddOrUpdateSmeToCollection( - List elements, AdminShellPackageEnv packageEnv = null, + List elements, AdminShellPackageEnvBase packageEnv = null, bool addFilesToPackage = false) { // SMEC as Refrable @@ -1312,7 +1312,7 @@ public void PresetInstancesBasedOnSource(List sourceElemen /// Render the list of form elements into a list of SubmodelElements. /// public List AddOrUpdateDifferentElementsToCollection( - List elements, AdminShellPackageEnv packageEnv = null, + List elements, AdminShellPackageEnvBase packageEnv = null, bool addFilesToPackage = false) { if (this.PairInstances != null) @@ -1422,7 +1422,7 @@ public FormInstanceProperty( /// producing a new element. Returns True, if a new element shall be rendered. /// public override bool ProcessSmeForRender( - AdminShellPackageEnv packageEnv = null, bool addFilesToPackage = false, bool editSource = false) + AdminShellPackageEnvBase packageEnv = null, bool addFilesToPackage = false, bool editSource = false) { // refer to base (SME) function, but not caring about result base.ProcessSmeForRender(packageEnv, addFilesToPackage, editSource); @@ -1616,7 +1616,7 @@ public FormInstanceMultiLangProp( /// the new values instead of producing a new element. Returns True, if a new element shall be rendered. /// public override bool ProcessSmeForRender( - AdminShellPackageEnv packageEnv = null, bool addFilesToPackage = false, bool editSource = false) + AdminShellPackageEnvBase packageEnv = null, bool addFilesToPackage = false, bool editSource = false) { // refer to base (SME) function, but not caring about result base.ProcessSmeForRender(packageEnv, addFilesToPackage, editSource); @@ -1784,7 +1784,7 @@ public FormInstanceFile( /// producing a new element. Returns True, if a new element shall be rendered. /// public override bool ProcessSmeForRender( - AdminShellPackageEnv packageEnv = null, bool addFilesToPackage = false, bool editSource = false) + AdminShellPackageEnvBase packageEnv = null, bool addFilesToPackage = false, bool editSource = false) { // refer to base (SME) function, but not caring about result base.ProcessSmeForRender(packageEnv, addFilesToPackage, editSource); @@ -1969,7 +1969,7 @@ public FormInstanceReferenceElement( /// the new values instead of producing a new element. Returns True, if a new element shall be rendered. /// public override bool ProcessSmeForRender( - AdminShellPackageEnv packageEnv = null, bool addFilesToPackage = false, bool editSource = false) + AdminShellPackageEnvBase packageEnv = null, bool addFilesToPackage = false, bool editSource = false) { // refer to base (SME) function, but not caring about result base.ProcessSmeForRender(packageEnv, addFilesToPackage, editSource); @@ -2108,7 +2108,7 @@ public FormInstanceRelationshipElement( /// the new values instead of producing a new element. Returns True, if a new element shall be rendered. /// public override bool ProcessSmeForRender( - AdminShellPackageEnv packageEnv = null, bool addFilesToPackage = false, bool editSource = false) + AdminShellPackageEnvBase packageEnv = null, bool addFilesToPackage = false, bool editSource = false) { // refer to base (SME) function, but not caring about result base.ProcessSmeForRender(packageEnv, addFilesToPackage, editSource); @@ -2270,7 +2270,7 @@ public FormInstanceCapability( /// the new values instead of producing a new element. Returns True, if a new element shall be rendered. /// public override bool ProcessSmeForRender( - AdminShellPackageEnv packageEnv = null, bool addFilesToPackage = false, bool editSource = false) + AdminShellPackageEnvBase packageEnv = null, bool addFilesToPackage = false, bool editSource = false) { // refer to base (SME) function, but not caring about result base.ProcessSmeForRender(packageEnv, addFilesToPackage, editSource); diff --git a/src/AasxIntegrationBase/AasxMenu.cs b/src/AasxIntegrationBase/AasxMenu.cs index a0f2c0ee5..5aa0c309f 100644 --- a/src/AasxIntegrationBase/AasxMenu.cs +++ b/src/AasxIntegrationBase/AasxMenu.cs @@ -201,7 +201,7 @@ public bool PopulateObjectFromArgs(object o) foreach (var ad in this) if (ad.Key?.Name?.Trim().ToLower() == name.ToLower()) { - AdminShellUtil.SetFieldLazyValue(f, o, ad.Value); + AdminShellUtil.SetFieldLazyValue(f, o, ad.Value, enableEnums: true); break; } } @@ -622,7 +622,7 @@ public AasxMenu AddLambda( // Operate // - public async Task ActivateAction(AasxMenuItemBase mi, AasxMenuActionTicket ticket) + public async Task ActivateAction(AasxMenuItemBase mi, AasxMenuActionTicket ticket, Action lambdaDone = null) { var name = mi?.Name?.Trim()?.ToLower(); @@ -634,6 +634,8 @@ public async Task ActivateAction(AasxMenuItemBase mi, AasxMenuActionTicket ticke await this.DefaultActionAsync(name, mi, ticket); else if (this.DefaultAction != null) this.DefaultAction(name, mi, ticket); + + lambdaDone?.Invoke(this); } // @@ -844,7 +846,7 @@ public class AasxMenuActionTicket /// /// Filled by the currently selected element. /// - public AdminShellPackageEnv Package; + public AdminShellPackageEnvBase Package; /// /// Filled by the currently selected element. diff --git a/src/AasxIntegrationBase/AasxPluginOptionsBase.cs b/src/AasxIntegrationBase/AasxPluginOptionsBase.cs index c1fde47e9..d9f84b956 100644 --- a/src/AasxIntegrationBase/AasxPluginOptionsBase.cs +++ b/src/AasxIntegrationBase/AasxPluginOptionsBase.cs @@ -23,6 +23,20 @@ This source code may use other Open Source software components (see LICENSE.txt) namespace AasxIntegrationBase { + + /// + /// With a lot of reluctance, this forms a global variable to be exchanged among all plugins. + /// Note: as not all plugins do have default options, this is the only way to make plugin + /// behaviour configurable by main application. + /// + public static class AasxPluginsGlobal + { + /// + /// Only check the value (id) field of keys of semanticId for Submodels. + /// + public static bool SubmodelCheckOnlyId = false; + } + /// /// Base class for an options record. This is a piece of options information, which is /// associated with an id of a Submodel template. @@ -249,13 +263,12 @@ public AasxPluginLookupOptionsBase( } #endif - private string GenerateIndexKey(Aas.IKey key) { if (key == null) return null; var k = new Aas.Key(key.Type, key.Value); - var ndx = k?.ToStringExtended(); + var ndx = k?.ToStringExtended(format: AasxPluginsGlobal.SubmodelCheckOnlyId ? 2 : 0); return ndx; } diff --git a/src/AasxIntegrationBaseGdi/AnyUI/AnyUiMagickHelper.cs b/src/AasxIntegrationBaseGdi/AnyUI/AnyUiMagickHelper.cs index f140be9c0..be6108786 100644 --- a/src/AasxIntegrationBaseGdi/AnyUI/AnyUiMagickHelper.cs +++ b/src/AasxIntegrationBaseGdi/AnyUI/AnyUiMagickHelper.cs @@ -20,8 +20,14 @@ This source code may use other Open Source software components (see LICENSE.txt) using System.Xml.Schema; using System.Xml; using AdminShellNS; +using Aas = AasCore.Aas3_0; +using Extensions; using AnyUi; using ImageMagick; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using System.IO.Packaging; namespace AasxIntegrationBaseGdi { @@ -83,21 +89,45 @@ public static AnyUiBitmapInfo CreateAnyUiBitmapFromResource(string path, return null; } - public static AnyUiBitmapInfo LoadBitmapInfoFromPackage(AdminShellPackageEnv package, string path) + public static AnyUiBitmapInfo LoadBitmapInfoFromBytes(byte[] ba) + { + if (ba == null || ba.Length < 1) + return null; + + try + { + using (var ms = new MemoryStream(ba)) + { + // load image + var bi = new MagickImage(ms); + var binfo = CreateAnyUiBitmapInfo(bi); + + // give this back + return binfo; + } + } + catch (Exception ex) + { + AdminShellNS.LogInternally.That.SilentlyIgnoredError(ex); + } + + return null; + } + + public static AnyUiBitmapInfo LoadBitmapInfoFromPackage(AdminShellPackageEnvBase package, string path) { if (package == null || path == null) return null; try { - var thumbStream = package.GetLocalStreamFromPackage(path); - if (thumbStream == null) + var inputBytes = package.GetBytesFromPackageOrExternal(path); + if (inputBytes == null) return null; // load image - var bi = new MagickImage(thumbStream); + var bi = new MagickImage(inputBytes); var binfo = CreateAnyUiBitmapInfo(bi); - thumbStream.Close(); // give this back return binfo; @@ -110,10 +140,34 @@ public static AnyUiBitmapInfo LoadBitmapInfoFromPackage(AdminShellPackageEnv pac return null; } + // DEPRECATED + //public static AnyUiBitmapInfo LoadBitmapInfoFromStream(Stream stream) + //{ + // if (stream == null) + // return null; + + // try + // { + // // load image + // var bi = new MagickImage(stream); + // var binfo = CreateAnyUiBitmapInfo(bi); + + // // give this back + // return binfo; + // } + // catch (Exception ex) + // { + // AdminShellNS.LogInternally.That.SilentlyIgnoredError(ex); + // } + + // return null; + //} + // TODO (MIHO, 2023-02-23): make the whole thing async!! - public static AnyUiBitmapInfo MakePreviewFromPackageOrUrl( - AdminShellPackageEnv package, string path, + public static async Task MakePreviewFromPackageOrUrlAsync( + AdminShellPackageEnvBase package, string path, + string aasId, string smId, string idShortPath, double dpi = 75) { if (path == null) @@ -123,9 +177,9 @@ public static AnyUiBitmapInfo MakePreviewFromPackageOrUrl( try { - System.IO.Stream thumbStream = null; + byte[] thumbBytes = null; if (true /*= package?.IsLocalFile(path)*/) - thumbStream = package.GetLocalStreamFromPackage(path); + thumbBytes = await package.GetBytesFromPackageOrExternalAsync(path, aasId, smId, idShortPath); else { // try download @@ -155,7 +209,7 @@ public static AnyUiBitmapInfo MakePreviewFromPackageOrUrl( #endif } - if (thumbStream == null) + if (thumbBytes == null) return null; using (var images = new MagickImageCollection()) @@ -166,15 +220,13 @@ public static AnyUiBitmapInfo MakePreviewFromPackageOrUrl( settings.FrameCount = 1; // Number of pages // Read only the first page of the pdf file - images.Read(thumbStream, settings); + images.Read(thumbBytes, settings); if (images.Count > 0 && images[0] is MagickImage img) { res = CreateAnyUiBitmapInfo(img); } } - - thumbStream.Close(); } catch (Exception ex) { @@ -183,6 +235,106 @@ public static AnyUiBitmapInfo MakePreviewFromPackageOrUrl( return res; } + + // + // Support for loading multiple files one after each other + // + + public class DelayedFileContentLoadBase + { + public Action LambdaLoaded; + } + + public class DelayedFileContentForFileElement : DelayedFileContentLoadBase + { + public AdminShellPackageEnvBase Package; + public string FileUri; + + public string AasId; + public string SmId; + public string IdShortPath; + } + + public class DelayedFileContentLoader + { + protected List _jobs = new List(); + + public void Add(DelayedFileContentLoadBase job) + { + lock (_jobs) + { + _jobs.Add(job); + } + } + + /// + /// Make sure, that Submodel.SetParents() has been executed! + /// + /// + /// + /// + public void Add( + AdminShellPackageEnvBase package, + Aas.IAssetAdministrationShell aas, + Aas.ISubmodel submodel, + Aas.IFile fileElem, + Action lambdaLoaded) + { + // access + if (package == null || aas == null || submodel == null || fileElem == null) + return; + + var idShortPath = "" + fileElem.CollectIdShortByParent( + separatorChar: '.', excludeIdentifiable: true); + + + Add(new DelayedFileContentForFileElement() + { + LambdaLoaded = lambdaLoaded, + Package = package, + AasId = "" + aas.Id, + SmId = "" + submodel.Id, + IdShortPath = idShortPath, + FileUri = fileElem.Value + }); + } + + public async Task TickToLoad() + { + if (_jobs.Count < 1) + return false; + + // pick one and start + DelayedFileContentLoadBase job = null; + lock (_jobs) + { + job = _jobs.First(); + _jobs.RemoveAt(0); + } + if (job == null) + return false; + + // now try loading + if (job is DelayedFileContentForFileElement jobfc) + { + var inputBytes = await jobfc.Package?.GetBytesFromPackageOrExternalAsync( + uriString: jobfc.FileUri, + aasId: "" + jobfc.AasId, + smId: "" + jobfc.SmId, + idShortPath: jobfc.IdShortPath); + + var bi = AnyUiGdiHelper.LoadBitmapInfoFromBytes(inputBytes); + + if (bi != null) + { + jobfc.LambdaLoaded?.Invoke(jobfc, bi); + return true; + } + } + + return false; + } + } } } diff --git a/src/AasxIntegrationBaseGdi/AnyUI/AnyUiTimerHelper.cs b/src/AasxIntegrationBaseGdi/AnyUI/AnyUiTimerHelper.cs index 824ef0b7e..4a10a0b39 100644 --- a/src/AasxIntegrationBaseGdi/AnyUI/AnyUiTimerHelper.cs +++ b/src/AasxIntegrationBaseGdi/AnyUI/AnyUiTimerHelper.cs @@ -25,5 +25,18 @@ public static object CreatePluginTimer(int intervalMs, EventHandler eventHandler return _timer2; } } + + public static object CreatePluginTimerAsync(int intervalMs, Action lambdaTick) + { + { + // Note: this timer shall work for all sorts of applications? + // see: https://stackoverflow.com/questions/21041299/c-sharp-dispatchertimer-in-dll-application-never-triggered + var _timer2 = new System.Timers.Timer(intervalMs); + _timer2.Elapsed += (s, e) => lambdaTick?.Invoke(); + _timer2.Enabled = true; + _timer2.Start(); + return _timer2; + } + } } } diff --git a/src/AasxIntegrationBaseWpf/AasxWpfBaseUtils.cs b/src/AasxIntegrationBaseWpf/AasxWpfBaseUtils.cs index 5eaae3b2b..8d2a8e451 100644 --- a/src/AasxIntegrationBaseWpf/AasxWpfBaseUtils.cs +++ b/src/AasxIntegrationBaseWpf/AasxWpfBaseUtils.cs @@ -137,29 +137,29 @@ public static IEnumerable LogicalTreeFindAllChildsWithRegexTag } } - public static BitmapImage LoadBitmapImageFromPackage(AdminShellPackageEnv package, string path) + public static BitmapImage LoadBitmapImageFromPackage(AdminShellPackageEnvBase package, string path) { if (package == null || path == null) return null; try { - var thumbStream = package.GetLocalStreamFromPackage(path); - if (thumbStream == null) + var imgBytes = package.GetBytesFromPackageOrExternal(path); + if (imgBytes == null) return null; // load image - var bi = new BitmapImage(); - bi.BeginInit(); - bi.CacheOption = BitmapCacheOption.OnLoad; - bi.StreamSource = thumbStream; - bi.EndInit(); - - thumbStream.Close(); - - // note: no closing of bi required, as BitmapImage (OnLoad!) will close it! - // give this back - return bi; + using (var ms = new System.IO.MemoryStream(imgBytes)) + { + var bi = new BitmapImage(); + bi.BeginInit(); + bi.CacheOption = BitmapCacheOption.OnLoad; + bi.StreamSource = ms; + bi.EndInit(); + // note: no closing of bi required, as BitmapImage (OnLoad!) will close it! + // give this back + return bi; + } } catch (Exception ex) { diff --git a/src/AasxMqttClient/MqttClient.cs b/src/AasxMqttClient/MqttClient.cs index 8747b9e0a..e0d01aab8 100644 --- a/src/AasxMqttClient/MqttClient.cs +++ b/src/AasxMqttClient/MqttClient.cs @@ -173,7 +173,7 @@ private string GenerateTopic(string template, } public async Task StartAsync( - AdminShellPackageEnv package, + AdminShellPackageEnvBase package, AnyUiDialogueDataMqttPublisher diaData, GrapevineLoggerToStoredPrints logger = null) { diff --git a/src/AasxOpcUa2Client/AasOpcUaClient2.cs b/src/AasxOpcUa2Client/AasOpcUaClient2.cs index 4da04aeeb..00d85657c 100644 --- a/src/AasxOpcUa2Client/AasOpcUaClient2.cs +++ b/src/AasxOpcUa2Client/AasOpcUaClient2.cs @@ -107,7 +107,15 @@ public async Task StartClientAsync() SecurityPolicyUris.None); // no encryption // try opening a session and reading a few nodes. - await _channel.OpenAsync(); + try + { + await _channel.OpenAsync(); + } catch (Exception ex) + { + ClientStatus = AasOpcUaClientStatus.ErrorCreateSession; + _log?.Error(ex, "open async"); + return; + } // ok ClientStatus = AasOpcUaClientStatus.Running; diff --git a/src/AasxPackageExplorer/App.xaml.cs b/src/AasxPackageExplorer/App.xaml.cs index 5680c6501..99aab2849 100644 --- a/src/AasxPackageExplorer/App.xaml.cs +++ b/src/AasxPackageExplorer/App.xaml.cs @@ -88,7 +88,6 @@ public static OptionsInformation InferOptions(string exePath, string[] args) var loadedPlugins = Plugins.TryActivatePlugins(pluginDllInfos); Plugins.TrySetOptionsForPlugins(pluginDllInfos, loadedPlugins); - return loadedPlugins; } diff --git a/src/AasxPackageExplorer/MainWindow.CommandBindings.cs b/src/AasxPackageExplorer/MainWindow.CommandBindings.cs index 744e1384e..916f8b156 100644 --- a/src/AasxPackageExplorer/MainWindow.CommandBindings.cs +++ b/src/AasxPackageExplorer/MainWindow.CommandBindings.cs @@ -33,12 +33,12 @@ This source code may use other Open Source software components (see LICENSE.txt) using Aas = AasCore.Aas3_0; namespace AasxPackageExplorer -{ +{ /// /// This partial class contains all command bindings, such as for the main menu, in order to reduce the /// complexity of MainWindow.xaml.cs /// - public partial class MainWindow : Window, IFlyoutProvider + public partial class MainWindow : Window, IFlyoutProvider, IExecuteMainCommand { private string lastFnForInitialDirectory = null; @@ -49,7 +49,6 @@ public partial class MainWindow : Window, IFlyoutProvider //// //// .AddWpf\(name: "\5", header: "\1", inputGesture: "\3", \4\) - public void RememberForInitialDirectory(string fn) { this.lastFnForInitialDirectory = fn; @@ -97,6 +96,82 @@ public void CommandExecution_RedrawAll() /// Set to true, if the application shall be shut down via script /// public bool ScriptModeShutdown = false; + + public async Task ExecuteMainMenuCommand(string menuItemName, params object[] args) + { + if (menuItemName?.HasContent() != true) + { + Log.Singleton.Error("MainWindow execute menu command: menu item name missing!"); + return -1; + } + + // name of tool, find it + var foundMenu = this.GetMainMenu(); + var mi = foundMenu.FindName(menuItemName); + if (mi == null) + { + foundMenu = this.GetDynamicMenu(); + mi = foundMenu.FindName(menuItemName); + } + if (mi == null) + { + Log.Singleton.Error($"MainWindow execute menu command: menu item name invalid: {menuItemName}"); + return -1; + } + + // create a ticket + var ticket = new AasxMenuActionTicket() + { + MenuItem = mi, + ScriptMode = true, + ArgValue = new AasxMenuArgDictionary() + }; + + // go thru the remaining arguments and find arg names and values + var argi = 0; + while (args != null && argi < args.Length) + { + // get arg name + if (!(args[argi] is string argname)) + { + Log.Singleton.Error($"MainWindow execute menu command: Argument at index {argi} is " + + $"not string type for argument name."); + return -1; + } + + // find argname? + var ad = mi.ArgDefs?.Find(argname); + if (ad == null) + { + Log.Singleton.Error($"MainWindow execute menu command: Argument at index {argi} is " + + $"not valid argument name."); + return -1; + } + + // create arg value (not available is okay) + object av = null; + if (argi + 1 < args.Length) + av = args[argi + 1]; + + // into ticket + ticket.ArgValue.Add(ad, av); + + // 2 forward! + argi += 2; + } + + // invoke action + await foundMenu.ActivateAction(mi, ticket); + + // perform UI updates if required + if (ticket.UiLambdaAction != null && !(ticket.UiLambdaAction is AnyUiLambdaActionNone)) + { + // add to "normal" event quoue + this.AddWishForToplevelAction(ticket.UiLambdaAction); + } + + return 0; + } private async Task CommandBinding_GeneralDispatch( string cmd, @@ -415,7 +490,7 @@ public void CommandBinding_CheckAndFix() // validate as XML var ms = new MemoryStream(); - PackageCentral.Main.SaveAs("noname.xml", true, AdminShellPackageEnv.SerializationFormat.Xml, ms, + PackageCentral.Main.SaveAs("noname.xml", true, AdminShellPackageFileBasedEnv.SerializationFormat.Xml, ms, saveOnlyCopy: true); ms.Flush(); ms.Position = 0; @@ -424,7 +499,7 @@ public void CommandBinding_CheckAndFix() // validate as JSON var ms2 = new MemoryStream(); - PackageCentral.Main.SaveAs("noname.json", true, AdminShellPackageEnv.SerializationFormat.Json, ms2, + PackageCentral.Main.SaveAs("noname.json", true, AdminShellPackageFileBasedEnv.SerializationFormat.Json, ms2, saveOnlyCopy: true); ms2.Flush(); ms2.Position = 0; @@ -530,7 +605,7 @@ public void CommandBinding_ConnectSecure() // start CONNECT as a worker (will start in the background) var worker = new BackgroundWorker(); - AdminShellPackageEnv envToload = null; + AdminShellPackageFileBasedEnv envToload = null; worker.DoWork += (s1, e1) => { for (int i = 0; i < 15; i++) diff --git a/src/AasxPackageExplorer/MainWindow.xaml b/src/AasxPackageExplorer/MainWindow.xaml index 701a9e06c..81652d73c 100644 --- a/src/AasxPackageExplorer/MainWindow.xaml +++ b/src/AasxPackageExplorer/MainWindow.xaml @@ -1,4 +1,4 @@ - - + + + + + + + - - - - - - @@ -189,137 +94,11 @@ - - - - + - - @@ -461,7 +240,7 @@ - @@ -472,14 +251,14 @@ - - @@ -611,17 +390,43 @@ + + - + + + + + + - - +