diff --git a/BHoM_Engine/Convert/ToCommaSeparatedList.cs b/BHoM_Engine/Convert/ToCommaSeparatedList.cs new file mode 100644 index 000000000..5ccdb2d50 --- /dev/null +++ b/BHoM_Engine/Convert/ToCommaSeparatedList.cs @@ -0,0 +1,43 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using System.Collections.Generic; + +namespace BH.Engine.Base +{ + public static partial class Convert + { + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + public static string ToCommaSeparatedList(this List keys) + { + if (keys == null || keys.Count == 0) + return ""; + + return string.Join(",", keys); + } + + /***************************************************/ + } +} diff --git a/BHoM_Engine/Objects/AssemblyResolver.cs b/BHoM_Engine/Objects/AssemblyResolver.cs new file mode 100644 index 000000000..cc3be4958 --- /dev/null +++ b/BHoM_Engine/Objects/AssemblyResolver.cs @@ -0,0 +1,199 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace BH.Engine.Base.Objects +{ + public class AssemblyResolver : IAssemblyResolver + { + /***************************************************/ + /**** Constructors ****/ + /***************************************************/ + + public AssemblyResolver(Dictionary> assemblyNamePerType = null, Dictionary>> assemblyNamesPerExtensionMethod = null) + { + if (assemblyNamePerType != null) + m_AssemblyNamePerType = assemblyNamePerType; + + if (assemblyNamesPerExtensionMethod != null) + m_AssemblyNamesPerExtensionMethod = assemblyNamesPerExtensionMethod; + } + + + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + [Description("Make sure assemblies that contain a type matching the input name are loaded. Return true if any assembly was loaded.")] + public bool MakeSureAssemblyIsLoadedForType(string type) + { + if (string.IsNullOrEmpty(type) || !type.StartsWith("BH.")) + return false; + + string[] parts = type.Split(','); + List assemblyNames = new List(); + + if (parts.Length > 1) + { + assemblyNames.Add(parts[1].Trim()); + } + else if (parts.Length == 1) + { + if (m_AssemblyNamePerType.ContainsKey(type)) + assemblyNames.AddRange(m_AssemblyNamePerType[type]); + } + + bool anyLoaded = false; + foreach (string assemblyName in assemblyNames.Where(x => !string.IsNullOrEmpty(x))) + { + if (!BH.Engine.Base.Query.IsAssemblyLoaded(assemblyName)) + { + string assemblyPath = Initialisation.AssemblyFilePath(assemblyName); + if (!File.Exists(assemblyPath)) + { + BH.Engine.Base.Compute.RecordError($"Assembly file not found when trying to load assemblies for type {type}: {assemblyPath}"); + continue; + } + + try + { + Assembly assembly = BH.Engine.Base.Compute.LoadAssembly(assemblyPath); + if (assembly == null) + BH.Engine.Base.Compute.RecordError($"Failed to load assembly: {assemblyName}"); + else + anyLoaded = true; + } + catch (Exception ex) + { + BH.Engine.Base.Compute.RecordError(ex, $"Exception while loading assembly for type {type}: {assemblyPath}."); + return false; + } + } + } + + return anyLoaded; + } + + /***************************************************/ + + [Description("Make sure assemblies containing extension methods matching the input name and target type are loaded. Returns true if any assembly was loaded.")] + public bool MakeSureAssemblyIsLoadedForExtensionMethod(string methodName, Type targetType) + { + if (string.IsNullOrEmpty(methodName) || targetType == null) + return false; + + if (!m_AssemblyNamesPerExtensionMethod.ContainsKey(methodName)) + return false; + + Dictionary> typeToAssemblies = m_AssemblyNamesPerExtensionMethod[methodName]; + HashSet assembliesToLoad = new HashSet(); + + string exactTypeName = TypeKey(targetType); + if (typeToAssemblies.ContainsKey(exactTypeName)) + assembliesToLoad.UnionWith(typeToAssemblies[exactTypeName]); + + Type baseType = targetType.BaseType; + while (baseType != null && baseType != typeof(object)) + { + string baseTypeName = TypeKey(baseType); + if (typeToAssemblies.ContainsKey(baseTypeName)) + assembliesToLoad.UnionWith(typeToAssemblies[baseTypeName]); + baseType = baseType.BaseType; + } + + foreach (Type interfaceType in targetType.GetInterfaces()) + { + string interfaceName = TypeKey(interfaceType); + if (typeToAssemblies.ContainsKey(interfaceName)) + assembliesToLoad.UnionWith(typeToAssemblies[interfaceName]); + } + + if (targetType.IsGenericType) + { + Type genericDef = targetType.GetGenericTypeDefinition(); + string genericTypeName = TypeKey(genericDef); + if (typeToAssemblies.ContainsKey(genericTypeName)) + assembliesToLoad.UnionWith(typeToAssemblies[genericTypeName]); + } + + bool anyLoaded = false; + foreach (string assemblyName in assembliesToLoad) + { + if (!BH.Engine.Base.Query.IsAssemblyLoaded(assemblyName)) + { + string assemblyPath = Initialisation.AssemblyFilePath(assemblyName); + if (!File.Exists(assemblyPath)) + { + BH.Engine.Base.Compute.RecordNote($"Assembly not found for extension method {methodName}: {assemblyPath}"); + continue; + } + + try + { + Assembly assembly = BH.Engine.Base.Compute.LoadAssembly(assemblyPath); + if (assembly == null) + BH.Engine.Base.Compute.RecordWarning($"Failed to load assembly: {assemblyName}"); + else + anyLoaded = true; + } + catch (Exception ex) + { + BH.Engine.Base.Compute.RecordError(ex, $"Exception loading assembly for extension method {methodName}: {assemblyPath}"); + } + } + } + + return anyLoaded; + } + + + /***************************************************/ + /**** Private Methods ****/ + /***************************************************/ + + private static string TypeKey(Type type) + { + string key = type.FullName ?? type.Name; + int cut = key.IndexOfAny(new char[] { ',', '[' }); + if (cut > 0) + key = key.Substring(0, cut); + return key; + } + + + /***************************************************/ + /**** Private Fields ****/ + /***************************************************/ + + Dictionary> m_AssemblyNamePerType = new Dictionary>(); + + Dictionary>> m_AssemblyNamesPerExtensionMethod = new Dictionary>>(); + + /***************************************************/ + } +} diff --git a/BHoM_Engine/Objects/Initialisation.cs b/BHoM_Engine/Objects/Initialisation.cs new file mode 100644 index 000000000..fe0e357f7 --- /dev/null +++ b/BHoM_Engine/Objects/Initialisation.cs @@ -0,0 +1,406 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Base.Attributes; +using BH.oM.Base.Reflection; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace BH.Engine.Base.Objects +{ + public static class Initialisation + { + /***************************************************/ + /**** Public Properties ****/ + /***************************************************/ + + public static readonly Regex DefaultAssemblyNameFilter = new Regex(@"oM$|_Engine$|_Adapter$"); + + public static string DefaultAssemblyContentFilePath => + System.IO.Path.Combine(BH.Engine.Base.Query.BHoMFolderResources(), "AssemblyContent.tsv"); + + + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + [Description("Reads existing code elements from a tsv file. Returns an empty list if the file does not exist.")] + public static List LoadCodeElements(string tsvFilePath, Func fromTsv) where T : CodeElementRecord + { + if (!File.Exists(tsvFilePath)) + return new List(); + + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + + try + { + List codeElements = File.ReadAllLines(tsvFilePath) + .Select(x => fromTsv(x)) + .Where(x => x != null) + .ToList(); + + stopwatch.Stop(); + BH.Engine.Base.Compute.RecordNote($"Time to load code elements: {stopwatch.Elapsed.TotalMilliseconds / 1000} s."); + + return codeElements; + } + catch (Exception e) + { + BH.Engine.Base.Compute.RecordError(e, $"Failed to load the code elements from '{System.IO.Path.GetFileName(tsvFilePath)}'."); + return null; + } + } + + /***************************************************/ + + [Description("Scans disk for new/updated assemblies matching the filter, loads them, harvests their code elements, merges them into the given list and persists the result back to tsv.")] + public static List RefreshFromNewAssemblies( + IReadOnlyList codeElements, + Regex assemblyNameFilter, + string tsvFilePath, + Func toTsv, + Func, List> harvestNewElements) where T : CodeElementRecord + { + List currentElements = codeElements?.ToList() ?? new List(); + + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + + Dictionary lastAssemblyUpdateTimes = currentElements + .GroupBy(x => x.AssemblyName) + .ToDictionary(x => x.Key, x => x.First().AssemblyModifiedTime); + + List loadedAssemblies = LoadNewAssemblies(lastAssemblyUpdateTimes, assemblyNameFilter); + + stopwatch.Stop(); + BH.Engine.Base.Compute.RecordNote($"Time to load all updated/new assemblies from current domain: {stopwatch.Elapsed.TotalMilliseconds / 1000} s."); + + if (loadedAssemblies.Count == 0) + return currentElements; + + List loadedCodeElements = harvestNewElements(loadedAssemblies); + if (loadedCodeElements.Count == 0) + return currentElements; + + stopwatch.Restart(); + + List updatedElements = currentElements + .Where(x => !loadedAssemblies.Contains(x.AssemblyName, StringComparer.OrdinalIgnoreCase)) + .Concat(loadedCodeElements) + .ToList(); + + List lines = updatedElements + .Select(x => toTsv(x)) + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + + try + { + string directory = Path.GetDirectoryName(tsvFilePath); + if (!Directory.Exists(directory)) + Directory.CreateDirectory(directory); + + File.WriteAllLines(tsvFilePath, lines); + } + catch (Exception e) + { + BH.Engine.Base.Compute.RecordError(e, $"Failed to save the assembly content to {tsvFilePath}."); + } + + stopwatch.Stop(); + BH.Engine.Base.Compute.RecordNote($"Time to update the code elements with the content of the updated/new assemblies: {stopwatch.Elapsed.TotalMilliseconds / 1000} s."); + + return updatedElements; + } + + /***************************************************/ + + public static AssemblyResolver CreateAssemblyResolver(IEnumerable codeElements) + { + List elements = codeElements?.ToList() ?? new List(); + + Dictionary> assemblyNamesPerType = elements + .Where(x => x.Type == CodeElementType.Type) + .GroupBy(x => x.DisplayText) + .ToDictionary(group => group.Key, group => group.Select(x => x.AssemblyName).Distinct().ToList()); + + Dictionary>> assemblyNamesPerExtensionMethod + = BuildExtensionMethodDictionary(elements); + + return new AssemblyResolver(assemblyNamesPerType, assemblyNamesPerExtensionMethod); + } + + + /***************************************************/ + /**** Private Methods ****/ + /***************************************************/ + + private static Dictionary>> BuildExtensionMethodDictionary( + List codeElements) + { + Dictionary>> result + = new Dictionary>>(); + + foreach (CodeElementRecord record in codeElements.Where(x => + x.Type == CodeElementType.Method_Query || + x.Type == CodeElementType.Method_Compute || + x.Type == CodeElementType.Method_Convert || + x.Type == CodeElementType.Method_Modify)) + { + try + { + string firstParamTypeName = record.InputKeys?.FirstOrDefault(); + + if (!string.IsNullOrEmpty(firstParamTypeName)) + { + string methodName = ExtractMethodName(record.DisplayText); + + if (!result.ContainsKey(methodName)) + result[methodName] = new Dictionary>(); + + if (!result[methodName].ContainsKey(firstParamTypeName)) + result[methodName][firstParamTypeName] = new List(); + + if (!result[methodName][firstParamTypeName].Contains(record.AssemblyName)) + result[methodName][firstParamTypeName].Add(record.AssemblyName); + } + } + catch (Exception ex) + { + BH.Engine.Base.Compute.RecordWarning($"Failed to parse extension method from {record.DisplayText}: {ex.Message}"); + } + } + + return result; + } + + /***************************************************/ + + private static string ExtractMethodName(string displayText) + { + int openParen = displayText.IndexOf('('); + if (openParen < 0) + return displayText; + + string beforeParams = displayText.Substring(0, openParen); + + int genericStart = beforeParams.IndexOf('<'); + if (genericStart > 0) + beforeParams = beforeParams.Substring(0, genericStart); + + int lastDot = beforeParams.LastIndexOf('.'); + if (lastDot >= 0) + return beforeParams.Substring(lastDot + 1); + + return beforeParams; + } + + /***************************************************/ + + [Description("Loads all BHoM assemblies from the current domain that match the provided filter.")] + [Input("lastAssemblyUpdateTimes", "Records of the last time each assembly was updated.")] + [Input("assemblyNameFilter", "Regex filter applied to assembly names.")] + [Output("loadedAssemblies", "Assemblies loaded as considered new.")] + public static List LoadNewAssemblies(Dictionary lastAssemblyUpdateTimes, Regex assemblyNameFilter) + { + if (lastAssemblyUpdateTimes == null) + { + BH.Engine.Base.Compute.RecordError("lastAssemblyUpdateTimes was not provided. No assembly was loaded."); + return new List(); + } + + if (assemblyNameFilter == null) + { + BH.Engine.Base.Compute.RecordError("assemblyNameFilter was not provided. No assembly was loaded."); + return new List(); + } + + Dictionary lastUpdateTimes = lastAssemblyUpdateTimes.ToDictionary(x => x.Key.ToLower(), x => x.Value); + HashSet loadedAssemblies = new HashSet(StringComparer.OrdinalIgnoreCase); + HashSet visitedAssemblies = new HashSet(StringComparer.OrdinalIgnoreCase); + + string bhomFolder = Query.BHoMFolder(); + foreach (string subFolder in SubFoldersForRuntime()) + { + string runtimeFolder = Path.Combine(bhomFolder, subFolder); + LoadNewAssembliesForFolder(runtimeFolder, lastUpdateTimes, loadedAssemblies, visitedAssemblies, assemblyNameFilter); + } + + LoadNewAssembliesForFolder(bhomFolder, lastUpdateTimes, loadedAssemblies, visitedAssemblies, assemblyNameFilter); + + return loadedAssemblies.ToList(); + } + + /***************************************************/ + + [Description("Convert a row in an Excel file (in tsv format) into a code element record.")] + [Input("tsv", "Excel row that contains the data related to the code element in a tsv format.")] + [Output("codeElement", "Converted code element.")] + public static CodeElementRecord FromTsv(this string tsv) + { + string[] parts = tsv.Split('\t'); + if (parts.Length < 5) + { + Compute.RecordError("Failed to extract code element record from tvs content because it doesn't contain 5 parts. Input tsv: " + tsv); + return null; + } + + if (!Enum.TryParse(parts[1], out CodeElementType type)) + { + Compute.RecordError($"Failed to extract code element record from tvs content because the code element type ({parts[1]}) is not recognised. Input tsv: " + tsv); + return null; + } + + if (!long.TryParse(parts[4], out long utcTime)) + { + Compute.RecordError($"Failed to extract code element record from tvs content because the provided time ({parts[4]}) is not valid. Input tsv: " + tsv); + return null; + } + + List inputKeys = new List(); + List outputKeys = new List(); + if (parts.Length >= 7) + { + inputKeys = string.IsNullOrEmpty(parts[5]) ? new List() : parts[5].Split(',').ToList(); + outputKeys = string.IsNullOrEmpty(parts[6]) ? new List() : parts[6].Split(',').ToList(); + } + + return new CodeElementRecord + { + AssemblyName = parts[0], + Type = type, + DisplayText = parts[2], + //Json = parts[3], + AssemblyModifiedTime = DateTime.FromFileTimeUtc(utcTime), + InputKeys = inputKeys, + OutputKeys = outputKeys + }; + } + + /***************************************************/ + + public static string ToTsv(this CodeElementRecord codeElement) + { + return $"{codeElement.AssemblyName}" + + $"\t{codeElement.Type}" + + $"\t{codeElement.DisplayText}" + + //TODO: decrement indices where needed and delete this one! + $"\tPlaceholder" + + $"\t{codeElement.AssemblyModifiedTime.ToFileTimeUtc()}" + + $"\t{codeElement.InputKeys.ToCommaSeparatedList()}" + + $"\t{codeElement.OutputKeys.ToCommaSeparatedList()}"; + } + + /***************************************************/ + + [Description("Returns the best on-disk path for a BHoM assembly, preferring the runtime-specific subdirectory (netX.0\\ or netfx\\) over the flat folder.")] + [Input("assemblyName", "Assembly name without extension, e.g. 'SQL_Adapter'.")] + [Output("path", "Full path to the .dll file; the file may or may not exist.")] + public static string AssemblyFilePath(string assemblyName) + { + string bhomFolder = Query.BHoMFolder(); + + foreach (string subFolder in SubFoldersForRuntime()) + { + string runtimePath = System.IO.Path.Combine(bhomFolder, subFolder, assemblyName + ".dll"); + if (File.Exists(runtimePath)) + return runtimePath; + } + + return System.IO.Path.Combine(bhomFolder, assemblyName + ".dll"); + } + + /***************************************************/ + + [Description("Returns the runtime-specific subdirectories of the BHoM Assemblies folder where assemblies compatible with the current .NET runtime can be found. " + + "Returns '.../Assemblies/netfx/' on .NET Framework and '.../Assemblies/netX.0/' on CoreCLR (.NET X).")] + [Output("subFolders", "runtime-specific subdirectories for the BHoM assemblies sorted in the order they should be traversed.")] + public static List SubFoldersForRuntime() + { + if (m_SubFoldersForRuntime != null) + return m_SubFoldersForRuntime; + + string desc = RuntimeInformation.FrameworkDescription; + if (desc.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase)) + { + m_SubFoldersForRuntime = new List { "netfx" }; + } + else + { + m_SubFoldersForRuntime = new List(); + int major = Environment.Version.Major; + for (int v = major; v >= 5; v--) + m_SubFoldersForRuntime.Add($"net{v}.0"); + } + + return m_SubFoldersForRuntime; + } + + + /***************************************************/ + /**** Private Methods ****/ + /***************************************************/ + + private static void LoadNewAssembliesForFolder(string folderPath, Dictionary lastUpdateTimes, HashSet loadedAssemblies, HashSet visitedAssemblies, Regex assemblyNameFilter) + { + if (!Directory.Exists(folderPath)) + return; + + foreach (string file in Directory.GetFiles(folderPath, "*.dll", SearchOption.TopDirectoryOnly)) + { + string name = Path.GetFileNameWithoutExtension(file); + + if (assemblyNameFilter.IsMatch(name) && !visitedAssemblies.Contains(name)) + { + visitedAssemblies.Add(name); + string key = name.ToLower(); + + if (!lastUpdateTimes.ContainsKey(key) || lastUpdateTimes[key] < File.GetLastWriteTimeUtc(file)) + { + Assembly assembly = BH.Engine.Base.Compute.LoadAssembly(file); + if (assembly != null) + { + BH.Engine.Base.Compute.RecordNote($"Assembly {name} loaded as it was newer than its last recorded update time."); + loadedAssemblies.Add(name); + } + } + } + } + } + + /***************************************************/ + /**** Private Fields ****/ + /***************************************************/ + + private static List m_SubFoldersForRuntime = null; + + /***************************************************/ + } +} diff --git a/BHoM_Engine/Query/ItemByKey.cs b/BHoM_Engine/Query/ItemByKey.cs new file mode 100644 index 000000000..624d71ed2 --- /dev/null +++ b/BHoM_Engine/Query/ItemByKey.cs @@ -0,0 +1,40 @@ +using System.Linq; + +namespace BH.Engine.Base +{ + public static partial class Query + { + public static object ItemByKey(string key) + { + Objects.IAssemblyResolver resolver = Global.AssemblyResolver; + + if (key.Contains('(')) + { + //TODO need to support ctors etc. - if oM in name then ctor! + + string[] split = key.Split('('); + if (split.Length != 2) + return null; + + string typeName = split[0].Substring(0, split[0].LastIndexOf('.')); + resolver.MakeSureAssemblyIsLoadedForType(typeName); + + string[] parameterTypeNames = split[1].Substring(0, split[1].Length - 1).Split(','); + foreach (string parameterTypeName in parameterTypeNames) + { + resolver.MakeSureAssemblyIsLoadedForType(typeName); + } + + System.Type type = Create.EngineType(typeName); + return type.GetMethods().FirstOrDefault(x => x.ToText(includePath: true) == key); + } + else + { + //TODO: need to start supporting enums etc. + + resolver.MakeSureAssemblyIsLoadedForType(key); + return Create.Type(key); + } + } + } +} diff --git a/Reflection_Engine/Compute/ConstructorText.cs b/Reflection_Engine/Compute/ConstructorText.cs new file mode 100644 index 000000000..719617651 --- /dev/null +++ b/Reflection_Engine/Compute/ConstructorText.cs @@ -0,0 +1,87 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.Engine.Base; +using BH.oM.Base.Attributes; +using System; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace BH.Engine.Reflection +{ + public static partial class Compute + { + /*************************************/ + /**** Public Methods ****/ + /*************************************/ + + [Description("Generate the text representing the input constructor")] + [Input("type", "The type of object to create a constructor for.")] + [Input("maxParams", "The maximum number of parameters to include in the text.")] + [Input("maxChars", "The maximum number of characters for the output text.")] + [Output("text", "The text corresponding to the description of the constructor generated for that type.")] + public static string ConstructorText(this Type type, int maxParams = 5, int maxChars = 40) + { + string text = type.Namespace + "." + type.Name + "." + type.Name + "() {"; + + try + { + string[] excluded = new string[] { "BHoM_Guid", "Fragments", "Tags", "CustomData" }; + PropertyInfo[] properties = type.GetProperties().Where(x => !excluded.Contains(x.Name)).ToArray(); + + string propertiesText = ""; + if (properties.Length > 0) + { + // Collect parameters text + for (int i = 0; i < properties.Count(); i++) + { + string singlePropertyText = properties[i].PropertyType.ToText() + " " + properties[i].Name; + + if (i > 0) + propertiesText += ", "; + + if (i >= maxParams || string.Join(propertiesText, singlePropertyText).Length > maxChars) + { + propertiesText += $"and {properties.Length - i} more inputs"; + break; + } + else + propertiesText += singlePropertyText; + } + } + + text += propertiesText; + } + catch (Exception e) + { + Engine.Base.Compute.RecordWarning("Type " + type.Name + " failed to load its properties.\nError: " + e.ToString()); + text += "?"; + } + text += "}"; + + return text; + } + + /*************************************/ + } +} diff --git a/Reflection_Engine/Query/CodeElements.cs b/Reflection_Engine/Query/CodeElements.cs new file mode 100644 index 000000000..9fe48d53a --- /dev/null +++ b/Reflection_Engine/Query/CodeElements.cs @@ -0,0 +1,214 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.Engine.Base; +using BH.oM.Base.Attributes; +using BH.oM.Base.Reflection; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace BH.Engine.Reflection +{ + public static partial class Query + { + /*************************************/ + /**** Public Methods ****/ + /*************************************/ + + [Description("Collect all the code elements that can be used to create UI components from the loaded assemblies.")] + [Output("codeElements", "All code elements already loaded that can be used in the UI to create components.")] + public static List CodeElements() + { + List items = new List(); + + //TODO: add filter by assembly names to avoid creation of all first to then treeshake (as the current flow) + //TODO: add filter by code element type maybe? not sure though... could try to optimise Revit_Tk by only loading types and extension methods? + + /// Types + + // All constructable BHoM objects + items.AddRange(Query.ConstructableTypeItems() + .Select(x => WrapInTryCatch(() => CodeElement(x, CodeElementType.Constructor, x.ConstructorText())))); + + // All adapter constructors + items.AddRange(Query.AdapterConstructorItems() + .Select(x => WrapInTryCatch(() => CodeElement(x, CodeElementType.Constructor, x.ToText(true))))); + + // All Enums + items.AddRange(Query.EnumItems() + .Select(x => WrapInTryCatch(() => CodeElement(x, CodeElementType.Enum, x.ToText(true))))); + + // All Types + items.AddRange(Query.TypeItems() + .Select(x => WrapInTryCatch(() => CodeElement(x, CodeElementType.Type, x.ToText(true))))); + + /// Methods + + // All methods for the BHoM Engine + items.AddRange(BH.Engine.Base.Query.BHoMMethodList() + .Where(x => x.IsExposed()) + //TODO: remove I at postfilter + //.Select(x => CodeElement(x, GetMethodType(x), x.ToText(includePath: true, removeIForInterface: false)))); + .Select(x => WrapInTryCatch(() => CodeElement(x, GetMethodType(x), x.ToText(true))))); + + // All methods from external class + items.AddRange(Query.ExternalItems() + .Select(x => WrapInTryCatch(() => CodeElement(x, CodeElementType.Method_External, x.ToText(true))))); + + // Return the list + return items; + } + + /*************************************/ + + [Description("Collect code elements from the loaded assemblies that match the provided assembly names.")] + [Input("assemblyNames", "Assembly names to filter the code elements by.")] + [Output("codeElements", "Code elements from the specified assemblies.")] + public static List CodeElements(IEnumerable assemblyNames) + { + HashSet names = new HashSet(assemblyNames, StringComparer.OrdinalIgnoreCase); + return CodeElements().Where(x => names.Contains(x.AssemblyName)).ToList(); + } + + + /*************************************/ + /**** Public Methods ****/ + /*************************************/ + + private static CodeElementRecord CodeElement(Type type, CodeElementType elementType, string displayText) + { + List inputTypes = type.GetProperties() + .Select(x => x.PropertyType?.UnderlyingType()?.Type) + .Where(x => x != null) + .Distinct() + .ToList(); + + return new CodeElementRecord + { + AssemblyName = AssemblyName(type), + AssemblyModifiedTime = AssemblyModifiedTime(type), + Type = elementType, + DisplayText = displayText, + //Json = type.ToJson(), + InputKeys = inputTypes.Select(x => x.ToText(true)).ToList(), + OutputKeys = type.UnderlyingType()?.Type.OutputKeys() + }; + } + + /*************************************/ + + //TODO: made temp public, to rethink how to do it right + public static CodeElementRecord CodeElement(MethodBase method, CodeElementType elementType, string displayText) + { + Type outputType = (method is MethodInfo) ? ((MethodInfo)method).ReturnType : method.DeclaringType; + List inputTypes = method.GetParameters() + .Select(x => x.ParameterType?.UnderlyingType()?.Type) + .Where(x => x != null) + .Distinct() + .ToList(); + + return new CodeElementRecord + { + AssemblyName = AssemblyName(method), + AssemblyModifiedTime = AssemblyModifiedTime(method), + Type = elementType, + DisplayText = displayText, + //Json = method.ToJson(), + InputKeys = inputTypes.Select(x => x.ToText(true)).ToList(), + OutputKeys = outputType.UnderlyingType()?.Type.OutputKeys() + }; + } + + /*************************************/ + + private static string AssemblyName(MethodBase method) + { + return AssemblyName(method.DeclaringType); + } + + /*************************************/ + + private static string AssemblyName(Type type) + { + return type.Assembly.GetName().Name; + } + + /*************************************/ + + private static DateTime AssemblyModifiedTime(MethodBase method) + { + return AssemblyModifiedTime(method.DeclaringType); + } + + /*************************************/ + + private static DateTime AssemblyModifiedTime(Type type) + { + if (string.IsNullOrEmpty(type?.Assembly?.Location)) + return DateTime.MinValue; + else + return File.GetLastWriteTimeUtc(type.Assembly.Location); + } + + /*************************************/ + + //TODO: made temp public, to rethink how to do it right + public static CodeElementType GetMethodType(MethodInfo method) + { + switch (method.DeclaringType.Name) + { + case "Create": + return CodeElementType.Method_Create; + case "Compute": + return CodeElementType.Method_Compute; + case "Convert": + return CodeElementType.Method_Convert; + case "Modify": + return CodeElementType.Method_Modify; + case "Query": + return CodeElementType.Method_Query; + default: + return CodeElementType.Undefined; + } + } + + /*************************************/ + + private static T WrapInTryCatch(Func func) + { + try + { + return func(); + } + catch + { + return default; + } + } + + /*************************************/ + } +} diff --git a/Reflection_Engine/Query/Items.cs b/Reflection_Engine/Query/Items.cs new file mode 100644 index 000000000..ff1b891d8 --- /dev/null +++ b/Reflection_Engine/Query/Items.cs @@ -0,0 +1,105 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Base.Attributes; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace BH.Engine.Reflection +{ + public static partial class Query + { + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + [Description("Extracts all BHoM type constructors to be grouped as Create Adapter items in the UI.")] + [Output("items", "All BHoM type constructors to be grouped as Create Adapter items.")] + public static IEnumerable AdapterConstructorItems() + { + return Engine.Base.Query.AdapterTypeList() + .SelectMany(x => x.GetConstructors()) + .Where(x => !x.IsNotImplemented() && !x.IsDeprecated()); + } + + /***************************************************/ + + [Description("Extracts all types valid in BHoM.")] + [Output("items", "All types valid in BHoM.")] + public static IEnumerable TypeItems() + { + return Engine.Base.Query.AllTypeList() + .Where(x => x.Namespace.StartsWith("BH.")) + .Concat(SystemTypes()) + .Where(x => !x.IsNotImplemented() && !x.IsDeprecated()); + } + + /***************************************************/ + + [Description("Extracts all types that have a valid public constructor.")] + [Output("items", "All types that have a valid public constructor.")] + public static IEnumerable ConstructableTypeItems() + { + return Engine.Base.Query.BHoMTypeList() + .Where(x => x != null && !x.IsNotImplemented() && !x.IsDeprecated() && x.IsAutoConstructorAllowed() && !x.IsEnum && !x.IsAbstract) + .Where(x => x.GetConstructors().Where(c => c.GetParameters().Count() > 0).Count() == 0); + } + + /***************************************************/ + + [Description("Extracts all enum types valid in BHoM.")] + [Output("items", "All enum types valid in BHoM.")] + public static IEnumerable EnumItems() + { + return Engine.Base.Query.BHoMEnumList() + .Where(x => !x.IsNotImplemented() && !x.IsDeprecated()); + } + + /***************************************************/ + + //[Description("Extracts names of all BHoM library items.")] + //[Output("items", "Names of all BHoM library items.")] + //public static List LibraryItems() + //{ + // string datasetFolder = BH.Engine.Base.Query.BHoMFolderDatasets(); + // string separator = Path.DirectorySeparatorChar.ToString(); + + // return Directory.GetFiles(datasetFolder, "*.json", SearchOption.AllDirectories) + // .Select(x => x.Replace(datasetFolder + separator, "").Replace(".json", "")) + // .ToList(); + //} + + ///***************************************************/ + + [Description("Extracts all external methods in BHoM.")] + [Output("items", "All external methods in BHoM.")] + public static List ExternalItems() + { + return Engine.Base.Query.ExternalMethodList(); + } + + /***************************************************/ + } +} diff --git a/Reflection_Engine/Query/OutputKeys.cs b/Reflection_Engine/Query/OutputKeys.cs new file mode 100644 index 000000000..e2094d754 --- /dev/null +++ b/Reflection_Engine/Query/OutputKeys.cs @@ -0,0 +1,65 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.Engine.Base; +using BH.oM.Base.Attributes; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace BH.Engine.Reflection +{ + public static partial class Query + { + /*************************************/ + /**** Public Methods ****/ + /*************************************/ + + [Description("Gets all the text representations of types that accept the provided type as input.")] + [Input("type", "The type to get the output key from.")] + [Output("Keys", "Text representations of types that accept the provided type as input.")] + public static List OutputKeys(this Type type) + { + if (m_OutputTypeKeys.ContainsKey(type)) + return m_OutputTypeKeys[type]; + else + { + List keys = new List { type } + .Concat(type.BaseTypes().Where(x => x.Namespace?.StartsWith("BH.") == true)) + .Select(x => x.ToText(true)) + .ToList(); + + m_OutputTypeKeys[type] = keys; + return keys; + } + } + + /*************************************/ + /**** Private Fields ****/ + /*************************************/ + + private static Dictionary> m_OutputTypeKeys = new Dictionary>(); + + /*************************************/ + } +} diff --git a/Reflection_Engine/Query/SystemTypes.cs b/Reflection_Engine/Query/SystemTypes.cs new file mode 100644 index 000000000..f1e022e38 --- /dev/null +++ b/Reflection_Engine/Query/SystemTypes.cs @@ -0,0 +1,49 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Base.Attributes; +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace BH.Engine.Reflection +{ + public static partial class Query + { + /*************************************/ + /**** Public Methods ****/ + /*************************************/ + + [Description("Extracts all basic system types.")] + [Output("items", "All basic system types.")] + public static IEnumerable SystemTypes() + { + return new List { typeof(Type), typeof(Enum), + typeof(object), typeof(bool), typeof(byte), + typeof(char), typeof(string), + typeof(float), typeof(double), typeof(decimal), typeof(short), typeof(int), typeof(long), + typeof(DateTime)}; + } + + /*************************************/ + } +}