#region Copyright 2008-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0
/* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.Schema;
using System.Configuration;
using System.IO;
namespace CSharpTest.Net.Utils
{
///
/// This is basically a big hack on the whole configuration system, it's purpose is to avoid
/// the entire thing. Where argument T is any [XmlSerializable] object this class can deserialize
/// it from the configuration file. It looks for an xsd either embeded into typeof(T).Assembly or
/// in the local filesystem. The Xsd must be named typeof(T).FullName + ".xsd" to be found by this
/// class. If found validation will occur durring the deserialization process and an exception of
/// type XmlException() will be raised on errors. Optionally, you can directly set the schema via
/// the static XmlSchema property.
///
[System.Diagnostics.DebuggerNonUserCode]
partial class XmlConfiguration : ConfigurationSection
{
///
/// Allows explicit setting of the XmlSchema to use when validating the xml input.
///
public static XmlSchema XmlSchema = null;// <-- allows external setting
static readonly XmlSerializer Serializer = new XmlSerializer(typeof(T));
string _schemaName;
T _data = default(T);
///
/// Constructs the XmlConfiguration element assuming that the derived type name
/// is the same name as the xsd file name, i.e. "MyCfg : XmlConfiguration" would
/// expect an XSD named "MyCfg.xsd" to either exist on disk or be embeded into
/// the type's containing assembly.
///
public XmlConfiguration() : this(typeof(T).FullName + ".xsd"){}
///
/// Explicitly sets the xsd file name to use.
///
public XmlConfiguration(string schemaName)
{
_schemaName = schemaName;
}
///
/// Allows access to the deserialized data
///
public T Settings { get { return _data; } }
///
/// Provides a derived class with the ability to do post-read validation not represented in
/// the xsd (or in place of an xsd).
///
protected virtual T ReadComplete(T data) { return data; }
///
/// The main work goes here, builds the reader and validator and deserializes the object.
///
protected override void DeserializeSection(System.Xml.XmlReader reader)
{
_data = ReadXml(_schemaName, reader);
_data = ReadComplete(_data);
base.SetReadOnly();
}
///
/// Reads and extracts the configuration settings from the current application's configuraiton file
///
public static T ReadConfig(string sectionName)
{
return (T)((XmlConfiguration)System.Configuration.ConfigurationManager.GetSection(sectionName));
}
///
/// Deserialize the xml configuration directly from an XmlReader instance
///
public static T ReadXml(System.Xml.XmlReader reader)
{ return ReadXml(typeof(T).FullName + ".xsd", reader); }
///
/// Deserialize the xml configuration directly from an XmlReader instance
///
public static T ReadXml(string schemaFile, System.Xml.XmlReader reader)
{
List parseErrors = new List();
System.Xml.Schema.XmlSchema schema = XmlSchema;
if (schema == null)
{
Stream schemaIo = null;
string schemaLocation = schemaFile;
// Try to read from three places in this order:
// 1 - the application's base directory
// 2 - the environment's current directory
// 3 - for type T the declaring assembly's resource manifest
if (File.Exists(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, schemaLocation)))
schemaLocation = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, schemaLocation);
if (File.Exists(schemaLocation))
schemaIo = File.Open(schemaLocation, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
else if (null == (schemaIo = typeof(T).Assembly.GetManifestResourceStream(schemaLocation)))
{
//maybe unqualified:
string tmpSchemaName = String.Format("{0}.{1}", typeof(T).Namespace, schemaLocation);
schemaIo = typeof(T).Assembly.GetManifestResourceStream(tmpSchemaName);
}
if (schemaIo != null) // if we found an xml schema, use it for validation
{
using (XmlTextReader rdr = new XmlTextReader(schemaIo))
{
schema = XmlSchema.Read(rdr, null);
}
}
}
XmlReaderSettings settings = new XmlReaderSettings();
settings.CheckCharacters = true;
settings.CloseInput = false;
settings.ValidationEventHandler += new ValidationEventHandler(
delegate(object sender, ValidationEventArgs args)
{
string message = String.Format("{0} ({1},{2}): {3}",
args.Severity,
args.Exception.LineNumber,
args.Exception.LinePosition,
args.Message
);
System.Diagnostics.Trace.WriteLine(message, typeof(T).FullName);
parseErrors.Add(message);
}
);
if (schema != null)
{
settings.Schemas.Add(schema);
//these options would be nice to allow; however, these will cause a duplicate defininition
//when the root node already defines a valid xsd and we internally rem our own.
settings.ValidationFlags = XmlSchemaValidationFlags.ReportValidationWarnings;
settings.ValidationType = ValidationType.Schema;
}
XmlReader validation = XmlReader.Create(reader, settings);
T data = (T)Serializer.Deserialize(validation);
if (data == null || parseErrors.Count > 0)
throw new XmlException(String.Join(Environment.NewLine, parseErrors.ToArray()));
return data;
}
///
/// Allows implicit casting of the configuration element to the actual type contained.
///
public static implicit operator T(XmlConfiguration o) { return o == null ? default(T) : o._data; }
}
}