diff --git a/NAV Source Control Test Host/NAV Source Control Test Host.csproj b/NAV Source Control Test Host/NAV Source Control Test Host.csproj index 226b651..8ce0894 100644 --- a/NAV Source Control Test Host/NAV Source Control Test Host.csproj +++ b/NAV Source Control Test Host/NAV Source Control Test Host.csproj @@ -41,7 +41,7 @@ 4 True False - False + True False False True diff --git a/NAV Source Control Test Host/Program.cs b/NAV Source Control Test Host/Program.cs index 5a895db..8fc71a6 100644 --- a/NAV Source Control Test Host/Program.cs +++ b/NAV Source Control Test Host/Program.cs @@ -29,20 +29,20 @@ namespace NavScm.TestHost log.InfoFormat("{0} total entries in NavSqlObjects", NavSqlObjects.Count()); var query = from sql in NavSqlObjects - where sql.Modified == 1 + where sql.Modified == 1 && sql.Type > 0 select sql; int count = query.Count(); - log.InfoFormat("{0} modified objects detected, reading them...", count); + log.InfoFormat("{0} modified objects detected, reading them into the cache...", count); var foundObjects = new Dictionary(count); - int i = 1; + // int i = 1; foreach (NavObject o in query) { - log.DebugFormat("Row {6}/{7}: Type {0}, ID {1}, Name {2}, Modified {3} {4}, Version {5}", - o.Type, o.ID, o.Name, o.Date.ToShortDateString(), o.Time.ToShortTimeString(), o.Version_List, i++, count); + //log.DebugFormat("Row {6}/{7}: Type {0}, ID {1}, Name {2}, Modified {3} {4}, Version {5}", + // o.Type, o.ID, o.Name, o.Date.ToShortDateString(), o.Time.ToShortTimeString(), o.Version_List, i++, count); foundObjects.Add(o.CacheKey, o); } @@ -59,12 +59,21 @@ namespace NavScm.TestHost XmlDictionaryReader xmlReader = XmlDictionaryReader.CreateTextReader(reader, new XmlDictionaryReaderQuotas()); var loadedObjects = (Dictionary)serializer.ReadObject(xmlReader, true); - log.Debug("Dumping sample object 1.82004"); + // 5.99997 => CU TN_Test - NavObject o2 = loadedObjects["1.82004"]; + log.Debug("Dumping sample object descriptor for 5.99997 "); + + NavObject o2 = loadedObjects["5.99997"]; log.DebugFormat("Type {0}, ID {1}, Name {2}, Modified {3} {4}, Version {5}", o2.Type, o2.ID, o2.Name, o2.Date.ToShortDateString(), o2.Time.ToShortTimeString(), o2.Version_List); + DevEnvInterface devenv = new DevEnvInterface("C:\\Program Files (x86)\\Microsoft Dynamics NAV\\tbrt-nav-erp-02\\RoleTailored Client\\finsql.exe", + "tbrt-sql-erp-01", "TERRABIT 2015 DEV"); + + devenv.Export(loadedObjects["5.80"], $"{Directory.GetCurrentDirectory()}\\CU80.txt"); + devenv.Export(loadedObjects["5.99997"], $"{Directory.GetCurrentDirectory()}\\CU99997.txt"); + devenv.Export(loadedObjects["1.13"], $"{Directory.GetCurrentDirectory()}\\TAB13.txt"); + log.Info("Shutting down..."); Console.ReadLine(); diff --git a/NAVSCM Library/NavInterface/DevEnvInterface.cs b/NAVSCM Library/NavInterface/DevEnvInterface.cs index 07ed91c..64555a5 100644 --- a/NAVSCM Library/NavInterface/DevEnvInterface.cs +++ b/NAVSCM Library/NavInterface/DevEnvInterface.cs @@ -19,7 +19,7 @@ namespace NavScm.NavInterface /// Currently, the interface expects to be able to access the database using /// NTLM Single Sign on. SQL user/pass authentication is not supported. /// - class DevEnvInterface + public class DevEnvInterface { private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(DevEnvInterface)); @@ -31,23 +31,23 @@ namespace NavScm.NavInterface /// /// The exit code of finsql.exe. So far this is always 0, tests pending. /// - public int ExitCode; + public readonly int ExitCode; /// /// True, if the command executed successfully. /// - public bool Success; + public readonly bool Success; /// /// The output produced by the command, as written to navcommandresult.txt by finsql.exe. /// - public string CommandOutput; + public readonly string CommandOutput; /// /// The errormessage if any, as written to the log file passed to finsql.exe. Empty, if /// the call was successful. /// - public string ErrorMessage; + public readonly string ErrorMessage; /// /// Constructs the whole object. @@ -117,9 +117,6 @@ namespace NavScm.NavInterface /// The full path to the created directory, which is empty. protected string GetNewTempDirectory() { - Contract.Ensures(Directory.Exists(Contract.Result())); - Contract.Ensures(! Directory.EnumerateFileSystemEntries(Contract.Result()).Any()); - // Try a few times. Uniqueness should not be a problem, as GetRandomFileName is cryptographically strong, // but beware of other errors e.g. during director creation. // Note, that in theory an race condition is possible here in case GetRandomFileName doesn't behave. @@ -146,6 +143,8 @@ namespace NavScm.NavInterface continue; } + Contract.Assert(!Directory.EnumerateFileSystemEntries(tempPath).Any()); + return tempPath; } @@ -173,7 +172,6 @@ namespace NavScm.NavInterface Contract.Requires(command != ""); string tempPath = GetNewTempDirectory(); - Contract.Ensures(!Directory.Exists(tempPath)); string errorLog = $"{tempPath}\\error.log"; string commandOutput = $"{tempPath}\\navcommandresult.txt"; @@ -184,7 +182,6 @@ namespace NavScm.NavInterface // Execute finsql.exe and wait for its exit... Process process = new Process(); - Contract.Ensures(process.HasExited); process.StartInfo.FileName = DevEnvPath; process.StartInfo.Arguments = fullArguments; @@ -207,18 +204,22 @@ namespace NavScm.NavInterface log.Debug($"ExecuteCommand: Execution took: {sw.Elapsed}"); } - // Parse and store finsql.exe result information + // Parse and store finsql.exe result information, be aware, that we are not getting + // Unicode back from finsql.exe, this is CP850 instead. Interesting, all this in the + // current millenia, as CP850 was introduced 1987 with DOS 3.3... Not to mention that + // the current windows ANSI Encoding is ignored as well. (Checked up to NAV 2015). + var finsqlEncoding = Encoding.GetEncoding(850); CommandResult result; if (File.Exists(errorLog)) { - result = new CommandResult(process.ExitCode, false, File.ReadAllText(commandOutput), File.ReadAllText(errorLog)); + result = new CommandResult(process.ExitCode, false, File.ReadAllText(commandOutput, finsqlEncoding), File.ReadAllText(errorLog, finsqlEncoding)); log.ErrorFormat("ExecuteCommand: Command failed with: {0}", result.ErrorMessage); log.DebugFormat("ExecuteCommand: Command result: {0}", result.CommandOutput); log.DebugFormat("ExecuteCommand: finsql.exe finished with exit code {0}", process.ExitCode); } else { - result = new CommandResult(process.ExitCode, true, File.ReadAllText(commandOutput), ""); + result = new CommandResult(process.ExitCode, true, File.ReadAllText(commandOutput, finsqlEncoding), ""); log.Info("ExecuteCommand: Command executed successfully"); log.DebugFormat("ExecuteCommand: Command result: {0}", result.CommandOutput); } @@ -231,19 +232,40 @@ namespace NavScm.NavInterface } Directory.Delete(tempPath); + Contract.Assert(!Directory.Exists(tempPath)); + Contract.Assume(process.HasExited); + return result; } /// /// Exports a given NAV object to disk. /// + /// + /// The file name must end with .txt, as finsql.exe deduces the export format from the destiation files + /// extension (crap). We have no other option here as to play by these rules. + /// Be aware, that NAV uses some strange mix of CP850 and CP1252 to encode the text files, + /// this is mean stuff here. The call does not try to convert this into something more sensible + /// at this point, especially since the IDE won't be able to handle this properly if you have to + /// work with the files manually. Checked with NAV 2015, YMMV. + /// Check http://forum.mibuso.com/discussion/37078/encoding-of-exported-navision-objects-txt-files + /// for further details about this. /// The NAV object as taken from the SQL database or from the cache (doesn't matter). - /// The name of the destination file. The system ensures, that the file - /// ends with .txt, as finsql.exe deduces the export format from the destiation files extension (crap). + /// The name of the destination file. The file name must end with .txt. public void Export(NavObject obj, string destinationFileName) { Contract.Requires(obj != null); Contract.Requires(destinationFileName != ""); + Contract.Requires(Path.GetExtension(destinationFileName) == ".txt"); + + // TODO: Skip Unlicensed objects? + string command = $"Command=ExportObjects,File=\"{destinationFileName}\",Filter=\"{obj.GetFilter()}\""; + log.DebugFormat("Export: Build command string: {0}", command); + var result = ExecuteCommand(command); + if (! result.Success) + { + throw new ArgumentException($"Cannot export object {obj.NavType} ID {obj.ID}: {result.ErrorMessage}"); + } } } diff --git a/NAVSCM Library/NavInterface/NavObject.cs b/NAVSCM Library/NavInterface/NavObject.cs index 9c8c341..3018b47 100644 --- a/NAVSCM Library/NavInterface/NavObject.cs +++ b/NAVSCM Library/NavInterface/NavObject.cs @@ -52,7 +52,7 @@ namespace NavScm.NavInterface partial void OnLoaded() { Contract.Requires(Company_Name.Length == 0); - Contract.Requires(Type >= 0 && Type <= 9 && Type != 2 && Type != 4 ); + Contract.Requires(Type >= 1 && Type <= 9 && Type != 2 && Type != 4 ); /* if (Company_Name.Length > 0) throw new InvalidOperationException($"The object {CacheKey} holds a variant with the company name {Company_Name}, which is unsupported"); @@ -93,6 +93,27 @@ namespace NavScm.NavInterface get { return Date.Add(Time.TimeOfDay); } } + /// + /// Returns a filter string suitable to filter the NAV Object table during finsql operation. + /// + /// Filter-String usable f.x. in ExportObjects. + [Pure] + public string GetFilter() + { + switch(NavType) + { + case NavObjectType.Codeunit: return $"Type=Codeunit;ID={ID}"; + case NavObjectType.MenuSuite: return $"Type=MenuSuite;ID={ID}"; + case NavObjectType.Page: return $"Type=Page;ID={ID}"; + case NavObjectType.Query: return $"Type=Query;ID={ID}"; + case NavObjectType.Report: return $"Type=Report;ID={ID}"; + case NavObjectType.Table: return $"Type=Table;ID={ID}"; + case NavObjectType.XmlPort: return $"Type=XmlPort;ID={ID}"; + } + + throw new InvalidOperationException($"The Type {Type} is unknown, cannot convert to filter."); + } + /// /// Constructs a hash key based on Type and ID. ///