2

I have a standalone XML file named config.xml that goes with my application that contains basically two sections:

1) Global settings 2) Server List, including settings

Basically, the global settings would contain a Database Username and Database Password that my program would use for each server listing.

The Server listing entries contain a list of servers, along with some filenames, and Databse Username and Database Password. The only thing significant here is that if I specify a Username/password in the server list, then it will use this rather than the global database username and password. Or, said differently, if the database username and password is not defined within the servewr list entry, it will use the global database username pasword.

My program basically loops through and xml configuration file and executes some database queries against each DB2 server and processes the information and creates a report. It works today, but I do have a few issues...

1) Every time I add a new element to my XML configuration file, I have to add it for each node that I have created, otherwise I get XML parse errors.

2) I'd like to categeory my configuration XML file rather than lump everything in one the same node and include empty elements.

Sample XML is below:

<?xml version="1.0" encoding="utf-8" ?>
<Config>
    <Global>
    <OutputFolder>C:\DATA\Configs\DB2\</OutputFolder>
    <DBUser>DB2ADMIN</DBUser>
    <DBPassword>%SecretPassword%</DBPassword>
    <FTPFiles>false</FTPFiles>
    <FTPTcpIp>127.0.0.1</FTPTcpIp>
    <FTPUser>FTPLogin1</FTPUser>
    <FTPPassword>P@ssw0rd</FTPPassword>
    <FTPRemoteFolder>/configs</FTPRemoteFolder>
    </Global>
    <Servers>
        <Server enabled="true">
        <Category>Report1</Category>
        <TcpIp>192.168.26.107</TcpIp>
        <Database>SampleData</Database>
        <User></User>
        <Password></Password>
        <Report1FileNameList>List1.txt</Report1FileNameList>
        <Report1FileNameRoutes>Routes1.txt</Report1FileNameRoutes>
        <Report1FileNameRouteTimeouts>Timeouts1.txt</Report1FileNameRouteTimeouts>
        <Report1FileNameEndpoints></Report1FileNameEndpoints>
        <Report2FilenameServers></Report2FilenameServers>
        <Report2FilenameRoutingGroup></Report2FilenameRoutingGroup>
     </Server>
     <Server enabled="true">
         <Category>Report1</Category>
         <TcpIp>192.168.26.107</TcpIp>
         <Database>SampleDataB</Database>
         <User></User>
         <Password></Password>
         <Report1FileNameList>List1.txt</Report1FileNameList>
         <Report1FileNameRoutes>Routes1.txt</Report1FileNameRoutes>
         <Report1FileNameRouteTimeouts>Timeouts1.txt</Report1FileNameRouteTimeouts>
         <Report1FileNameEndpoints></Report1FileNameEndpoints>
         <Report2FilenameServers></Report2FilenameServers>
         <Report2FilenameRoutingGroup></Report2FilenameRoutingGroup>
      </Server>
      <Server enabled="true">
          <Category>Report2</Category>
          <TcpIp>192.168.26.107</TcpIp>
          <Database>SampleDataE</Database>
          <User></User>
          <Password></Password>
          <Report1FileNameList></Report1FileNameList>
          <Report1FileNameRoutes></Report1FileNameRoutes>
          <Report1FileNameRouteTimeouts></Report1FileNameRouteTimeouts>
          <Report1FileNameEndpoints>Endpoints2.txt</Report1FileNameEndpoints>
          <Report2FilenameServers>Servers2.txt</Report2FilenameServers>
          <Report2FilenameRoutingGroup>Groups2.txt</Report2FilenameRoutingGroup>
      </Server>
      <Server enabled="true">
          <Category>Report2</Category>
          <TcpIp>192.168.26.108</TcpIp>
          <Database>SampleDatabase1_D</Database>
          <User></User>
          <Password></Password>
          <Report1FileNameList></Report1FileNameList>
          <Report1FileNameRoutes></Report1FileNameRoutes>
          <Report1FileNameRouteTimeouts></Report1FileNameRouteTimeouts>
          <Report1FileNameEndpoints>Endpoints2.txt</Report1FileNameEndpoints>
          <Report2FilenameServers>Servers2.txt</Report2FilenameServers>
          <Report2FilenameRoutingGroup>Groups1.txt</Report2FilenameRoutingGroup>
       </Server>
    </Servers>

Sample code is below:

        // load XML file
        try
        {
            // Config/Global
            System.Xml.XPath.XPathDocument doc = new System.Xml.XPath.XPathDocument(@"config.xml");

            foreach (System.Xml.XPath.XPathNavigator child in doc.CreateNavigator().Select("Config/Global"))
            {
                xml_global_outputFolder = child.SelectSingleNode("OutputFolder").Value;
                xml_global_DBuser = child.SelectSingleNode("DBUser").Value;
                xml_global_DBpassword = child.SelectSingleNode("DBPassword").Value;
                xml_global_FTPFiles = bool.Parse(child.SelectSingleNode("FTPFiles").Value);
                xml_global_FTPTcpIp = child.SelectSingleNode("FTPTcpIp").Value;
                xml_global_FTPUser = child.SelectSingleNode("FTPUser").Value;
                xml_global_FTPPassword = child.SelectSingleNode("FTPPassword").Value;
                xml_global_FTPRemoteFolder = child.SelectSingleNode("FTPRemoteFolder").Value;
            }

            // Config/Servers
            //System.Xml.XPath.XPathDocument doc = new System.Xml.XPath.XPathDocument(@"config.xml");
            foreach (System.Xml.XPath.XPathNavigator child in doc.CreateNavigator().Select("Config/Servers/*"))
            {

                //string xml_enabled = child.GetAttribute("Enabled", "");
                string xml_category = child.SelectSingleNode("Category").Value;
                string xml_tcpip = child.SelectSingleNode("TcpIp").Value;
                string xml_database = child.SelectSingleNode("Database").Value;
                string xml_user = child.SelectSingleNode("User").Value;
                string xml_password = child.SelectSingleNode("Password").Value;

                Console.WriteLine("Connecting to {0} using database {1} for {2} information...", xml_tcpip, xml_database, xml_category);

                // if node user value is empty, use global
                if (xml_user == string.Empty)
                {
                    DB2_user = xml_global_DBuser;
                }
                else
                {
                    DB2_user = xml_user;
                }

                // if node password value is empty, use global
                if (xml_password == string.Empty)
                {
                    DB2_password = xml_global_DBpassword;
                }
                else
                {
                    DB2_password = xml_password;
                }

                string txtFilename = string.Empty;
                string csvFilename = string.Empty;

                switch (xml_category.ToUpper())
                {
                    case "SAMPLE":
                        txtFilename = Path.Combine(xml_global_outputFolder, @"EMPLOYEE.csv");
                        csvFilename = Path.Combine(xml_global_outputFolder, Path.GetFileNameWithoutExtension(@"EMPLOYEE.csv"));
                        ExecuteQuery(xml_category, "SAMPLE", xml_tcpip, DB2_user, DB2_password, xml_database, csvFilename, txtFilename, option_debug);
                        break;
                }
                Console.WriteLine("");

            }
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception: {0}", e.Message);
            Environment.Exit(1);
        }

        Environment.Exit(0);
    }

I guess ideally, I'd like to create node-specific configurations and if the element is blank, the code should be able to handle either a missing element or an empty element. Maybe using an attribute for the category, instead? Something like:

   <Config>
  <Global>
     <OutputFolder></OutputFolder>
     <DBUser></DBUser>
     <DBPassword><DBPassword>
  </Global>
  <Servers category="Report1">
     <Server>
        <TcpIP>whatever</TcpIP>
        <User>whatever></User>
        <Password>whatever></Password>
     </Server>
     <Server>
        <TcpIP>whatever</TcpIP>
        <User>whatever></User>
        <Password>whatever></Password>
     </Server>
     <Server>
        <TcpIP>whatever</TcpIP>
        <User>whatever></User>
        <Password>whatever></Password>
     </Server>
 </Server>
  <Servers category="Report2">
     <Server>
        <TcpIP>whatever</TcpIP>
        <User>whatever></User>
        <Password>whatever></Password>
     </Server>
     <Server>
        <TcpIP>whatever</TcpIP>
        <User>whatever></User>
        <Password>whatever></Password>
     </Server>
     <Server>
        <TcpIP>whatever</TcpIP>
        <User>whatever></User>
        <Password>whatever></Password>
     </Server>
 </Server>
  <Servers category="AccessList">
     <Server>
        <TcpIP>whatever</TcpIP>
        <Database>whatever></Database>
        <Active>whatever</Active>
     </Server>
     <Server>
        <TcpIP>whatever</TcpIP>
        <Database>whatever></Database>
        <Active>whatever</Active>
     </Server>
     <Server>
        <TcpIP>whatever</TcpIP>
        <Database>whatever></Database>
        <Active>whatever</Active>
     </Server>
 </Server>
</Config>
user500741
  • 833
  • 5
  • 14
  • 25

1 Answers1

3

What you need to do is create a set of classes with each class representing each group of nodes. If you'd like to use these extensions, they will help you with empty nodes and default values:

Read and Write the OutputFolder in Global:

DirectoryInfo outputFolder = ConfigFile.Read.Global.OutputFolder;
ConfigFile.Write(file => file.Global.OutputFolder = outputFolder);

The classes:

public class ConfigFile : IDisposable
{
    internal XElement self;
    string file = "path to a file";

    public ConfigFile()
    {
        if(File.Exists(file))
            self = XElement.Load(file);
        else
            self = new XElement("Config");
    }

    public void Dispose() { self.Save(file); }

    public static ConfigFile Read { get { return new ConfigFile(); } }

    public static void Write(Action<ConfigFile> action)
    {
        using(ConfigFile file = new ConfigFile())
            action(file);
    }

    public Global Global
    { get { return _Global ?? (_Global = new Global(self.GetElement("Global"))); } }
    Global _Global;

    public Servers Servers
    { get { return _Servers ?? (_Servers = new Servers(self.GetElement("Servers"))); } }
    Servers _Servers

    public class Global
    {
        internal XElement self;
        public Global(XElement self) { this.self = self; }

        public DirectoryInfo OutputFolder
        {
            get { return self.Get<DirectoryInfo>("OutputFolder", null); }
            set { self.Set("OutputFolder", value, false); }
        }
    }

    public class Servers
    {
        internal XElement self;
        public Servers(XElement self) { this.self = self; }

        public void Add(Server server)
        {
            self.Add(server.self);
        }

        public string Category
        {
            get { return self.Get("category", string.Empty); }
            set { self.Set("category", value, true); }
        }

        public Server[] Items
        { get { return self.GetEnumerable("Server", x => new Server(x)).ToArray(); } }

        public class Server
        {
            internal XElement self;

            public Server() { self = new XElement("Server"); }

            public Server(XElement self) { this.self = self; }

            public bool Active
            {
                get { return self.Get("Active", false); }
                set { self.Set("Active", value, true); }
            }
        }
    }
}

GetElement() is superior to Element() because it handles cases where the element node doesn't exist. Get() takes a default value, therefore it always has a value.

To add a new value to the file is simpler once you have class distinction, because you can simply write another property to a class and let it return a default value if it doesn't exist.

Chuck Savage
  • 11,775
  • 6
  • 49
  • 69