From 41dcfa34d0081154a833c50964a819f7e942034a Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Wed, 20 May 2009 20:14:21 +0000 Subject: [PATCH] Added prototype code for an inversion-of-control container and a progress tracking service; moved XmlHelper class from Nuclex.UserInterface to Nuclex.Support; Nuclex.Support now uses the .NET Framework's own AssemblyLoadEventArgs class if it is compiled for the full framework; fixed a bug in the Request class that would cause exceptions to not be reported if Join() was called on a Request was casted to a plain Request git-svn-id: file:///srv/devel/repo-conversion/nusu@138 d2e56fa2-650e-0410-a79f-9358c0239efd --- Nuclex.Support (Xbox 360).csproj | 15 +- Nuclex.Support.csproj | 13 + Source/Plugins/AssemblyLoadEventArgs.Test.cs | 4 + Source/Plugins/AssemblyLoadEventArgs.cs | 4 + Source/Plugins/PluginHost.cs | 12 +- Source/Services/Instancing.cs | 18 + .../IProgressPublishingService.cs | 34 ++ .../IProgressTrackingService.cs | 68 ++++ .../ProgressTracking/ITrackedProcess.cs | 29 ++ .../ProgressTrackingComponent.cs | 56 ++++ Source/Services/ServiceManager.For.cs | 96 ++++++ Source/Services/ServiceManager.cs | 156 +++++++++ Source/Tracking/Request.cs | 12 + Source/XmlHelper.Test.cs | 307 ++++++++++++++++++ Source/XmlHelper.cs | 212 ++++++++++++ 15 files changed, 1030 insertions(+), 6 deletions(-) create mode 100644 Source/Services/Instancing.cs create mode 100644 Source/Services/ProgressTracking/IProgressPublishingService.cs create mode 100644 Source/Services/ProgressTracking/IProgressTrackingService.cs create mode 100644 Source/Services/ProgressTracking/ITrackedProcess.cs create mode 100644 Source/Services/ProgressTracking/ProgressTrackingComponent.cs create mode 100644 Source/Services/ServiceManager.For.cs create mode 100644 Source/Services/ServiceManager.cs create mode 100644 Source/XmlHelper.Test.cs create mode 100644 Source/XmlHelper.cs diff --git a/Nuclex.Support (Xbox 360).csproj b/Nuclex.Support (Xbox 360).csproj index 435e336..8fb3d0c 100644 --- a/Nuclex.Support (Xbox 360).csproj +++ b/Nuclex.Support (Xbox 360).csproj @@ -1,4 +1,4 @@ - + {DFFEAB70-51B8-4714-BCA6-79B733BBC520} {2DF5C3F4-5A5F-47a9-8E94-23B4456F55E2};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} @@ -118,6 +118,15 @@ ReverseComparer.cs + + + + + + + + ServiceManager.cs + PartialStream.cs @@ -286,6 +295,10 @@ WeakReference.cs + + + XmlHelper.cs + diff --git a/Nuclex.Support.csproj b/Nuclex.Support.csproj index 9aae330..d57b184 100644 --- a/Nuclex.Support.csproj +++ b/Nuclex.Support.csproj @@ -100,6 +100,15 @@ ReverseComparer.cs + + + + + + + + ServiceManager.cs + PartialStream.cs @@ -268,6 +277,10 @@ WeakReference.cs + + + XmlHelper.cs + diff --git a/Source/Plugins/AssemblyLoadEventArgs.Test.cs b/Source/Plugins/AssemblyLoadEventArgs.Test.cs index 06a5373..1264f97 100644 --- a/Source/Plugins/AssemblyLoadEventArgs.Test.cs +++ b/Source/Plugins/AssemblyLoadEventArgs.Test.cs @@ -28,6 +28,8 @@ using NUnit.Framework; namespace Nuclex.Support.Plugins { +#if XBOX360 + /// Unit Test for the assembly load event argument container [TestFixture] public class AssemblyLoadEventArgsTest { @@ -46,6 +48,8 @@ namespace Nuclex.Support.Plugins { } +#endif // XBOX360 + } // namespace Nuclex.Support.Plugins #endif // UNITTEST diff --git a/Source/Plugins/AssemblyLoadEventArgs.cs b/Source/Plugins/AssemblyLoadEventArgs.cs index 2efd42e..bd7aeab 100644 --- a/Source/Plugins/AssemblyLoadEventArgs.cs +++ b/Source/Plugins/AssemblyLoadEventArgs.cs @@ -24,6 +24,8 @@ using System.Reflection; namespace Nuclex.Support.Plugins { +#if XBOX360 + /// Signature for the AssemblyLoad event /// Object that is reporting that an assembly was loaded /// Contains the loaded assembly @@ -50,4 +52,6 @@ namespace Nuclex.Support.Plugins { } +#endif // XBOX360 + } // namespace Nuclex.Support.Plugins diff --git a/Source/Plugins/PluginHost.cs b/Source/Plugins/PluginHost.cs index 117cfd5..7e2cda9 100644 --- a/Source/Plugins/PluginHost.cs +++ b/Source/Plugins/PluginHost.cs @@ -37,8 +37,8 @@ namespace Nuclex.Support.Plugins { /// Initializes a plugin host using a new repository /// Employer used assess and employ the plugin types - public PluginHost(Employer employer) - : this(employer, new PluginRepository()) { } + public PluginHost(Employer employer) : + this(employer, new PluginRepository()) { } /// Initializes the plugin using an existing repository /// Employer used assess and employ the plugin types @@ -47,8 +47,9 @@ namespace Nuclex.Support.Plugins { this.employer = employer; this.repository = repository; - foreach(Assembly assembly in this.repository.LoadedAssemblies) + foreach(Assembly assembly in this.repository.LoadedAssemblies) { employAssemblyTypes(assembly); + } this.repository.AssemblyLoaded += new AssemblyLoadEventHandler(assemblyLoadHandler); } @@ -75,7 +76,9 @@ namespace Nuclex.Support.Plugins { private void employAssemblyTypes(Assembly assembly) { // Iterate all types contained in the assembly - foreach(Type type in assembly.GetTypes()) { + Type[] types = assembly.GetTypes(); + for(int index = 0; index < types.Length; ++index) { + Type type = types[index]; // We'll ignore abstract and non-public types if(!type.IsPublic || type.IsAbstract) { @@ -97,7 +100,6 @@ namespace Nuclex.Support.Plugins { catch(Exception exception) { Trace.WriteLine("Could not employ " + type.ToString() + ": " + exception.Message); } - } } diff --git a/Source/Services/Instancing.cs b/Source/Services/Instancing.cs new file mode 100644 index 0000000..8a40220 --- /dev/null +++ b/Source/Services/Instancing.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +using Nuclex.Support.Plugins; + +namespace Nuclex.Support.Services { + + /// Modes in which services can be instantiated + public enum Instancing { + /// There will only be one service in the whole process + Singleton, + /// Each thread will be assigned its own service + InstancePerThread, + /// A new service will be created each time it is queried for + Factory + } + +} // namespace Nuclex.Support.DependencyInjection diff --git a/Source/Services/ProgressTracking/IProgressPublishingService.cs b/Source/Services/ProgressTracking/IProgressPublishingService.cs new file mode 100644 index 0000000..1af810e --- /dev/null +++ b/Source/Services/ProgressTracking/IProgressPublishingService.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; + +using Nuclex.Support.Tracking; + +namespace Nuclex.Support.Services.ProgressTracking { + + /// Reports the progress of tracked background processes + /// + /// + /// This service is intended for the consumer of progress reports. It will notify + /// subscribers when background processes start, when progress is achieved and + /// when they finish. + /// + /// + /// Usually, this interface, together with the IProgressTrackingService interface, + /// is provided by a single service component that tracks the progress of + /// transactions taking place asynchronously and reports it back this way. + /// + /// + interface IProgressPublishingService { + + /// Fired when the overall progress changes + event EventHandler ProgressChanged; + + /// The overall progress of all tracked background processes + float TotalProgress { get; } + + /// Currently active background processes + IEnumerable TrackedProcesses { get; } + + } + +} // namespace Nuclex.Support.DependencyInjection.ProgressTracking diff --git a/Source/Services/ProgressTracking/IProgressTrackingService.cs b/Source/Services/ProgressTracking/IProgressTrackingService.cs new file mode 100644 index 0000000..da6702d --- /dev/null +++ b/Source/Services/ProgressTracking/IProgressTrackingService.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; + +using Nuclex.Support.Tracking; + +namespace Nuclex.Support.Services.ProgressTracking { + + /// Allows application-wide tracking of progress + interface IProgressTrackingService { + + /// Tracks the progress of the specified transaction + /// + /// Transaction whose progress will be tracked + /// + /// + /// Identifier unique to the tracked background process. Can be null. + /// + /// + /// + /// Using the process identifier allows you to track the progress of multiple + /// transactions that are working independently of each other. This could, + /// for example, result in multiple progress bars being displayed in a GUI. + /// The global progress always is a combination of all transactions tracked + /// by the service. + /// + /// + /// A good analogy for this might be a download manager which uses several + /// threads to download multiple sections of a file at the same time. You + /// want a progress bar per file, but not one for each downloading thread. + /// Specifying the name object as a process identifer, all transactions + /// belonging to the same file would be merged into a single progress source. + /// + /// + /// The process identifier can be a string or any object. Note however that + /// as common practice, this object's ToString() method should return + /// something reasonable, like "Downloading xy.txt". Localization can be + /// achieved by implementing an interface (eg. ILocalizableToString) which + /// provides a string and some parameters - or you could return the already + /// translated versions of the string if you prefer to have localized versions + /// of internal assemblies. + /// + /// + void Track(Transaction transaction, object processIdentifier); + + /// Tracks the progress of the specified transaction + /// + /// Transaction whose progress will be tracked + /// + /// + /// Tracks the transaction as if it was added with the process identifier + /// set to null. + /// + void Track(Transaction transaction); + + /// Stops tracking the specified transaction + /// Transaction that will no longer be tracked + /// + /// Untracking a transaction is optional. The service will automatically + /// remove finished transactions from its list of tracked transactions. Calling + /// this method is only required if you drop a transaction in a way that + /// prevents its AsyncEnded event from being fired (eg. by not executing it + /// at all, dispite adding it to the progress tracking service). + /// + void Untrack(Transaction transaction); + + } + +} // namespace Nuclex.Support.DependencyInjection.ProgressTracking diff --git a/Source/Services/ProgressTracking/ITrackedProcess.cs b/Source/Services/ProgressTracking/ITrackedProcess.cs new file mode 100644 index 0000000..f26da31 --- /dev/null +++ b/Source/Services/ProgressTracking/ITrackedProcess.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +using Nuclex.Support.Tracking; + +namespace Nuclex.Support.Services.ProgressTracking { + + /// Process whose progress is being tracked + public interface ITrackedProcess { + + /// Fired whenever the progress of the process changes + event EventHandler ProgressChanged; + + /// Unique identifier of the overall process + /// + /// As a convention, using this object's ToString() method should return + /// something usable, either a string that can be displayed in the user + /// interface or, depending on your architecture, the object could + /// implement certain interfaces that allow a localized version of + /// the string to be created. + /// + object ProcessIdentifier { get; } + + /// Progress that process has achieved so far + float CurrentProgress { get; } + + } + +} // namespace Nuclex.Support.DependencyInjection.ProgressTracking diff --git a/Source/Services/ProgressTracking/ProgressTrackingComponent.cs b/Source/Services/ProgressTracking/ProgressTrackingComponent.cs new file mode 100644 index 0000000..bc91fe9 --- /dev/null +++ b/Source/Services/ProgressTracking/ProgressTrackingComponent.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +using Nuclex.Support.Tracking; + +namespace Nuclex.Support.Services.ProgressTracking { + +#if false + /// Tracks the progress of running background processes + public class ProgressTrackingComponent : + IProgressTrackingService, + IProgressPublishingService { + + /// Fired when the overall progress changes + public event EventHandler ProgressChanged; + + /// Initializes a new progress tracking component + public ProgressTrackingComponent() { + } + + /// Tracks the progress of the specified transaction + /// + /// Transaction whose progress will be tracked + /// + /// + /// Identifier unique to the tracked background process. Can be null. + /// + public void Track(Transaction transaction, object processIdentifier) { + } + + /// Tracks the progress of the specified transaction + /// + /// Transaction whose progress will be tracked + /// + public void Track(Transaction transaction) { + } + + /// Stops tracking the specified transaction + /// Transaction that will no longer be tracked + public void Untrack(Transaction transaction) { + } + + /// The overall progress of all tracked background processes + public float TotalProgress { + get { return 0.0f; } + } + + /// Currently active background processes + public IEnumerable TrackedProcesses { + get { return null; } + } + + } +#endif + +} // namespace Nuclex.Support.Services.ProgressTracking diff --git a/Source/Services/ServiceManager.For.cs b/Source/Services/ServiceManager.For.cs new file mode 100644 index 0000000..3b7c003 --- /dev/null +++ b/Source/Services/ServiceManager.For.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Nuclex.Support.Services { + +#if false + partial class ServiceManager { + + #region class ForContext + + // TODO: Rename to "On" to avoid confusion with concept of for loop? + /// Manages the context of the "For" modifier + public class ForContext { + + /// Initializes a new "For" context of the service manager + /// Service manager the context operates on + /// Contract that is being modified + internal ForContext(ServiceManager serviceManager, Type contractType) { + this.serviceManager = serviceManager; + this.contractType = contractType; + } + + /// Uses the specified implementation for the contract + /// + /// Implementation that will be used for the contract + /// + public void Use(object contractImplementation) { } + + /// + /// Uses the provided object as a prototype for the contract implementation + /// + /// + /// Contract implementation that will be used as a prototype + /// + public void UsePrototype(object contractImplementationPrototype) { } + + /// Selects the default implementation to use for the contract + /// + /// Implementation that will be used as the default for the contract + /// + public void UseDefault(Type implementationType) { } + + /// Service manager the "For" context operates on + protected ServiceManager serviceManager; + /// Contract that is being modified + protected Type contractType; + + } + + #endregion // class ForContext + + #region class ForContext<> + + // TODO: Rename to "On" to avoid confusion with concept of for loop? + /// Manages the context of the "For" modifier + public class ForContext : ForContext { + + /// Initializes a new "For" context of the service manager + /// Service manager the context operates on + internal ForContext(ServiceManager serviceManager) : + base(serviceManager, typeof(ContractType)) { } + + /// Uses the specified implementation for the contract + /// + /// Implementation that will be used for the contract + /// + public void Use(ContractType implementation) { } + + /// + /// Uses the provided object as a prototype for the contract implementation + /// + /// + /// Type of the implementation that will be used as a prototype + /// + /// + /// Contract implementation that will be used as a prototype + /// + public void UsePrototype(PrototypeType contractImplementationPrototype) + where PrototypeType : ContractType, ICloneable { } + + /// Selects the default implementation to use for the contract + /// + /// Implementation that will be used as the default for the contract + /// + public void UseDefault() + where ImplementationType : ContractType { } + + } + + #endregion // class ForContext<> + + } +#endif + +} // namespace Nuclex.Support.DependencyInjection diff --git a/Source/Services/ServiceManager.cs b/Source/Services/ServiceManager.cs new file mode 100644 index 0000000..a9baed0 --- /dev/null +++ b/Source/Services/ServiceManager.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +using Nuclex.Support.Plugins; + +namespace Nuclex.Support.Services { + +#if false + /// + /// Inversion of Control container that manages the services of an application + /// + /// + /// + /// This is a very lightweight and simple inversion of control container that + /// relieves components of their duty to track down implementations for the services + /// they require to function. It will help with lazy initialization and prevent + /// components from becoming cross-linked balls of spaghetti references. + /// + /// + /// Here's a short list of the terms used throughout this container and their + /// specific meaning in this context. + /// + /// + /// + /// + /// Service + /// + /// Defined by an interface (service contract) and provided by a component + /// that implements the service contract. A service provides some kind of + /// utility to the application, for example it could provide access to + /// a data base or allow other components to control certain aspects of + /// the application. + /// + /// + /// + /// Contract + /// + /// Interface defining the behavior that a service implementation has to + /// follow. In order for a component to become a suitable candidate for + /// providing a specific service, it has to implement the service contract + /// interface and should rigorously follow its specifications. + /// + /// + /// + /// Component + /// + /// A component is simply a class that implements one or more service + /// contracts. The service manager will created instances of these classes + /// when all their dependencies can be provided for and an implementation + /// of their service contract is requested. + /// + /// + /// + /// + /// + public partial class ServiceManager : IServiceProvider { + + /// Initializes a new service manager + public ServiceManager() { + this.pluginRepository = new PluginRepository(); + } + + /// + /// Returns all available implementations for the specified contract + /// + /// + /// If true, only services whose dependencies can be completely + /// satisfied by the container are returned. + /// + /// + /// A new enumerator for the available contract implementations + /// + IEnumerable GetImplementations(bool completeOnly) + where ContractType : class { + Type contractType = typeof(ContractType); + + Assembly[] loadedAssemblies = this.pluginRepository.LoadedAssemblies.ToArray(); + for(int index = 0; index < loadedAssemblies.Length; ++index) { + Type[] assemblyTypes = loadedAssemblies[index].GetTypes(); + + for(int typeIndex = 0; typeIndex < assemblyTypes.Length; ++typeIndex) { + Type checkedType = assemblyTypes[typeIndex]; + if(contractType.IsAssignableFrom(checkedType)) { + + } + } + } + + yield return null; + } + + private struct CachedContractLookUp { + public Type[] ValidComponents; + public int Version; + } + private Dictionary cachedContracts; + private int version; + + /// + /// Allows the adjustment of the container's behavior in regard to + /// the specified contract + /// + /// + /// Contract for which the behavior will be adjusted + /// + /// + /// A context object through which the behavior of the container can be + /// adjusted for the specified type + /// + public ForContext For() where ContractType : class { + return new ForContext(this); + } + + /// + /// Allows the adjustment of the container's behavior in regard to + /// the specified contract + /// + /// + /// Contract for which the behavior will be adjusted + /// + /// + /// A context object through which the behavior of the container can be + /// adjusted for the specified type + /// + public ForContext For(Type contractType) { + return new ForContext(this, contractType); + } + + // Allow Dependency on Container + // public Foo(IServiceProvider serviceProvider) + // public Foo(IserviceLocator serviceLocator) + // public Foo(Container container) + + public ContractType GetService() where ContractType : class { + throw new NotImplementedException(); + } + + /// Retrieves the service of the specified type + /// + /// Contract for which the service will be retrieved + /// + /// The service for the specified contract + object IServiceProvider.GetService(Type contractType) { + throw new NotImplementedException(); + } + + /// + /// Contains all assemblies partaking in the dependency injection scheme + /// + private PluginRepository pluginRepository; + + } +#endif + +} // namespace Nuclex.Support.DependencyInjection diff --git a/Source/Tracking/Request.cs b/Source/Tracking/Request.cs index b56fc28..0c7e20e 100644 --- a/Source/Tracking/Request.cs +++ b/Source/Tracking/Request.cs @@ -198,6 +198,18 @@ namespace Nuclex.Support.Tracking { /// Allows the specific request implementation to re-throw an exception if /// the background process finished unsuccessfully /// + protected override void ReraiseExceptions() { + // Request and discard the result, so the implementor can do all error handling + // in the GatherResults() method. This is a good default implementation as long + // as the returned object does not require IDispose. It if does, this method + // needs to be overridden. + GatherResults(); + } + + /// + /// Allows the specific request to return the results of the Request to the + /// caller of the Join() method + /// protected abstract ResultType GatherResults(); } diff --git a/Source/XmlHelper.Test.cs b/Source/XmlHelper.Test.cs new file mode 100644 index 0000000..c9c2a0f --- /dev/null +++ b/Source/XmlHelper.Test.cs @@ -0,0 +1,307 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2009 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library 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 +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion + +#if UNITTEST + +using System; +using System.IO; +using System.Text; +using System.Xml; +using System.Xml.Schema; + +using NUnit.Framework; + +namespace Nuclex.Support { + + /// Unit Test for the XML helper class + [TestFixture] + public class XmlHelperTest { + + /// A broken XML schema + private const string brokenSchemaXml = + "This is not a valid schema"; + + /// An XML schema with a syntax error + private const string syntaxErrorSchemaXml = + "" + + "" + + " " + + " " + + ""; + + /// A valid XML schema for a list of 2D points + private const string pointsSchemaXml = + "" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + ""; + + /// A broken XML document + private const string brokenXml = + "This is not a valid XML file"; + + /// + /// Well-formed XML document that is not conformant to the schema above + /// + private const string unconformantXml = + "" + + "" + + " " + + " " + + ""; + + /// Well-formed XML document that is conformant to the schema + private const string conformantXml = + "" + + "" + + " " + + " " + + ""; + + #region TempFileKeeper + + /// + /// Creates a temporary file and automatically deletes it on dispose + /// + private class TempFileKeeper : IDisposable { + + /// + /// Creates a temporary file with the specified contents using the UTF8 encoding + /// + /// + /// Contents that will be written into the temporary file + /// + public TempFileKeeper(string fileContents) : this(fileContents, Encoding.UTF8) { } + + /// Creates a temporary file with the specified contents + /// + /// Contents that will be written into the temporary file + /// + /// + /// Encoding to use for writing the contents into the file + /// + public TempFileKeeper(string fileContents, Encoding encoding) { + string tempFile = Path.GetTempFileName(); + try { + using( + FileStream tempFileStream = new FileStream( + tempFile, FileMode.Truncate, FileAccess.Write, FileShare.None + ) + ) { + StreamWriter writer = new StreamWriter(tempFileStream, encoding); + writer.Write(fileContents); + writer.Flush(); + } + } + catch(Exception) { + File.Delete(tempFile); + throw; + } + + this.tempFilePath = tempFile; + } + + /// Called when the instance is collected by the GC + ~TempFileKeeper() { + Dispose(); + } + + /// Immediately releases all resources used by the instance + public void Dispose() { + if(this.tempFilePath != null) { + File.Delete(this.tempFilePath); + this.tempFilePath = null; + + GC.SuppressFinalize(this); + } + } + + /// Implicitely converts a TempFileKeeper into a file path + /// TempFileKeeper that will be converted + /// The path to the temporary file managed by the TempFileKeeper + public static implicit operator string(TempFileKeeper tempFileKeeper) { + return tempFileKeeper.tempFilePath; + } + + /// Path to the temporary file the TempFileKeeper is managing + private string tempFilePath; + + } + + #endregion // class TempFileKeeper + + /// + /// Verifies that an exception is thrown when a schema fails to load + /// + [Test] + public void TestThrowOnInvalidSchema() { + using( + TempFileKeeper tempFile = new TempFileKeeper(brokenSchemaXml) + ) { + Assert.Throws(delegate() { XmlHelper.LoadSchema(tempFile); }); + } + } + + /// + /// Verifies that an exception is thrown when a schema contains a syntax error + /// + [Test] + public void TestThrowOnSyntaxErrorSchema() { + using( + TempFileKeeper tempFile = new TempFileKeeper(syntaxErrorSchemaXml) + ) { + Assert.Throws(delegate() { XmlHelper.LoadSchema(tempFile); }); + } + } + + /// + /// Verfifies that TryLoadSchema() can fail without throwing an exception + /// when the schema is not a valid XML document + /// + [Test] + public void TestFailOnTryLoadNonExistingFile() { + XmlSchema schema; + Assert.IsFalse(XmlHelper.TryLoadSchema("-- hello world --", out schema)); + Assert.IsNull(schema); + } + + /// + /// Verfifies that TryLoadSchema() can fail without throwing an exception + /// when the schema is not a valid XML document + /// + [Test] + public void TestFailOnTryLoadBrokenSchema() { + using( + TempFileKeeper tempFile = new TempFileKeeper(brokenSchemaXml) + ) { + XmlSchema schema; + Assert.IsFalse(XmlHelper.TryLoadSchema(tempFile, out schema)); + Assert.IsNull(schema); + } + } + + /// + /// Verfifies that TryLoadSchema() can fail without throwing an exception + /// when the schema contains a syntax error + /// + [Test] + public void TestFailOnTryLoadSyntaxErrorSchema() { + using( + TempFileKeeper tempFile = new TempFileKeeper(syntaxErrorSchemaXml) + ) { + XmlSchema schema; + Assert.IsFalse(XmlHelper.TryLoadSchema(tempFile, out schema)); + Assert.IsNull(schema); + } + } + + /// Tests whether a normal, valid schema can be loaded successfully + [Test] + public void TestLoadSchema() { + using( + TempFileKeeper tempFile = new TempFileKeeper(pointsSchemaXml) + ) { + XmlHelper.LoadSchema(tempFile); + } + } + + /// Tests whether a normal, valid schema can be loaded successfully + [Test] + public void TestTryLoadSchema() { + using( + TempFileKeeper tempFile = new TempFileKeeper(pointsSchemaXml) + ) { + XmlSchema schema; + Assert.IsTrue(XmlHelper.TryLoadSchema(tempFile, out schema)); + Assert.NotNull(schema); + } + } + + /// + /// Verifies that an exception is thrown when an invalid XML document is loaded + /// + [Test] + public void TestFailOnLoadInvalidDocument() { + using(TextReader schemaReader = new StringReader(pointsSchemaXml)) { + XmlSchema schema = XmlHelper.LoadSchema(schemaReader); + using( + TempFileKeeper tempFile = new TempFileKeeper(brokenXml) + ) { + Assert.Throws( + delegate() { XmlHelper.LoadDocument(schema, tempFile); } + ); + } + } + } + + /// + /// Verifies that an exception is thrown when a nonconformant XML document is loaded + /// + [Test] + public void TestFailOnLoadNonConformingDocument() { + using(TextReader schemaReader = new StringReader(pointsSchemaXml)) { + XmlSchema schema = XmlHelper.LoadSchema(schemaReader); + using( + TempFileKeeper tempFile = new TempFileKeeper(unconformantXml) + ) { + Assert.Throws( + delegate() { XmlHelper.LoadDocument(schema, tempFile); } + ); + } + } + } + + /// + /// Tests whether a normal, conformant XML document can be loaded successfully + /// + [Test] + public void TestLoadConformingDocument() { + using(TextReader schemaReader = new StringReader(pointsSchemaXml)) { + XmlSchema schema = XmlHelper.LoadSchema(schemaReader); + using( + TempFileKeeper tempFile = new TempFileKeeper(conformantXml) + ) { + XmlDocument document = XmlHelper.LoadDocument(schema, tempFile); + } + } + } + + } + +} // namespace Nuclex.Support + +#endif // UNITTEST diff --git a/Source/XmlHelper.cs b/Source/XmlHelper.cs new file mode 100644 index 0000000..019bac2 --- /dev/null +++ b/Source/XmlHelper.cs @@ -0,0 +1,212 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2009 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library 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 +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Xml; +using System.Xml.Schema; + +namespace Nuclex.Support { + + /// Helper routines for handling XML code + public static class XmlHelper { + + #region class ValidationEventProcessor + + /// Handles any events occurring when an XML schema is loaded + private class ValidationEventProcessor { + + /// + /// Callback for notifications being sent by the XmlSchema.Read() method + /// + /// Not used + /// Contains the notification being sent + public void OnValidationEvent(object sender, ValidationEventArgs arguments) { + // We're only interested in the first error, so if we already got + // an exception on record, skip it! + if(this.OccurredException != null) { + return; + } + + // Only errors count as reasons to assume loading has failed + if(arguments.Severity == XmlSeverityType.Error) { + + // This code uses the ternary operator because I don't know how to + // stimlate a validation error that has no exception and couldn't achieve + // 100% test coverage otherwise. MSDN does not tell whether a validation + // error without an exception can even occur. + this.OccurredException = (arguments.Exception != null) ? + arguments.Exception : + new XmlSchemaValidationException("Unspecified schema validation error"); + + } + } + + /// Exception that has occurred during schema loading + public Exception OccurredException; + + } + + #endregion // class ValidationEventProcessor + + /// Loads a schema from a file + /// Path to the file containing the schema + /// The loaded schema + public static XmlSchema LoadSchema(string schemaPath) { + using(FileStream schemaStream = openFileForSharedReading(schemaPath)) { + return LoadSchema(schemaStream); + } + } + + /// Loads a schema from the provided stream + /// Stream containing the schema that will be loaded + /// The loaded schema + public static XmlSchema LoadSchema(Stream schemaStream) { + return LoadSchema(new StreamReader(schemaStream)); + } + + /// Loads a schema from a text reader + /// Text reader through which the schema can be read + /// The loaded schema + public static XmlSchema LoadSchema(TextReader schemaReader) { + ValidationEventProcessor eventProcessor = new ValidationEventProcessor(); + XmlSchema schema = XmlSchema.Read(schemaReader, eventProcessor.OnValidationEvent); + if(eventProcessor.OccurredException != null) { + throw eventProcessor.OccurredException; + } + return schema; + } + + /// Attempts to load a schema from a file + /// Path to the file containing the schema + /// Output parameter that will receive the loaded schema + /// True if the schema was loaded successfully, otherwise false + public static bool TryLoadSchema(string schemaPath, out XmlSchema schema) { + FileStream schemaStream; + if(!tryOpenFileForSharedReading(schemaPath, out schemaStream)) { + schema = null; + return false; + } + + using(schemaStream) { + return TryLoadSchema(schemaStream, out schema); + } + } + + /// Attempts to load a schema from the provided stream + /// Stream containing the schema that will be loaded + /// Output parameter that will receive the loaded schema + /// True if the schema was loaded successfully, otherwise false + public static bool TryLoadSchema(Stream schemaStream, out XmlSchema schema) { + return TryLoadSchema(new StreamReader(schemaStream), out schema); + } + + /// Attempts to load a schema from the provided text reader + /// Reader from which the schema can be read + /// Output parameter that will receive the loaded schema + /// True if the schema was loaded successfully, otherwise false + public static bool TryLoadSchema(TextReader schemaReader, out XmlSchema schema) { + try { + ValidationEventProcessor eventProcessor = new ValidationEventProcessor(); + schema = XmlSchema.Read(schemaReader, eventProcessor.OnValidationEvent); + if(eventProcessor.OccurredException == null) { + return true; + } + } + catch(Exception) { + // Munch! + } + + schema = null; + return false; + } + + /// Loads an XML document from a file + /// Schema to use for validating the XML document + /// + /// Path to the file containing the XML document that will be loaded + /// + /// The loaded XML document + public static XmlDocument LoadDocument(XmlSchema schema, string documentPath) { + using(FileStream documentStream = openFileForSharedReading(documentPath)) { + return LoadDocument(schema, documentStream); + } + } + + /// Loads an XML document from a stream + /// Schema to use for validating the XML document + /// + /// Stream from which the XML document will be read + /// + /// The loaded XML document + public static XmlDocument LoadDocument(XmlSchema schema, Stream documentStream) { + XmlReaderSettings settings = new XmlReaderSettings(); + settings.Schemas.Add(schema); + + using(XmlReader reader = XmlReader.Create(documentStream, settings)) { + XmlDocument document = new XmlDocument(); + document.Schemas.Add(schema); + document.Load(reader); + + ValidationEventProcessor eventProcessor = new ValidationEventProcessor(); + document.Validate(eventProcessor.OnValidationEvent); + if(eventProcessor.OccurredException != null) { + throw eventProcessor.OccurredException; + } + + return document; + } + } + + /// Opens a file for shared reading + /// Path to the file that will be opened + /// The opened file's stream + private static FileStream openFileForSharedReading(string path) { + return new FileStream( + path, FileMode.Open, FileAccess.Read, FileShare.Read + ); + } + + /// Opens a file for shared reading + /// Path to the file that will be opened + /// + /// Output parameter that receives the opened file's stream + /// + /// True if the file was opened successfully + private static bool tryOpenFileForSharedReading(string path, out FileStream fileStream) { + try { + fileStream = new FileStream( + path, FileMode.Open, FileAccess.Read, FileShare.Read + ); + return true; + } + catch(Exception) { + // Munch! + } + + fileStream = null; + return false; + } + + } + +} // namespace Nuclex.Support