1 // Copyright 2004-2008 Castle Project - http://www.castleproject.org/
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
7 // http://www.apache.org/licenses/LICENSE-2.0
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
15 namespace Castle
.ActiveRecord
18 using System
.Collections
.Generic
;
21 using System
.Reflection
;
22 using Castle
.ActiveRecord
.Framework
;
23 using Castle
.ActiveRecord
.Framework
.Config
;
24 using Castle
.ActiveRecord
.Framework
.Internal
;
25 using Castle
.ActiveRecord
.Framework
.Scopes
;
26 using Castle
.Core
.Configuration
;
28 using Iesi
.Collections
;
29 using NHibernate
.Expressions
;
30 using NHibernate
.Tool
.hbm2ddl
;
31 using Environment
=NHibernate
.Cfg
.Environment
;
34 /// Delegate for use in <see cref="ActiveRecordStarter.ModelsCreated"/> and <see cref="ActiveRecordStarter.ModelsValidated"/>
36 /// <param name="holder"></param>
37 public delegate void SessionFactoryHolderDelegate(ISessionFactoryHolder holder
);
40 /// Delegate for use in <see cref="ActiveRecordStarter.ModelsCreated"/> and <see cref="ActiveRecordStarter.ModelsValidated"/>
42 public delegate void ModelsDelegate(ActiveRecordModelCollection models
, IConfigurationSource source
);
45 /// Delegate for use in <see cref="ActiveRecordStarter.ModelCreated"/>
47 public delegate void ModelDelegate(ActiveRecordModel model
, IConfigurationSource source
);
50 /// Performs the framework initialization.
53 /// This class is not thread safe.
55 public sealed class ActiveRecordStarter
57 private static readonly Object lockConfig
= new object();
59 private static bool isInitialized
= false;
61 private static IDictionary
<Type
, string> registeredTypes
;
64 /// This is saved so one can invoke <c>RegisterTypes</c> later
66 private static IConfigurationSource configSource
;
69 /// So others frameworks can intercept the
70 /// creation and act on the holder instance
72 public static event SessionFactoryHolderDelegate SessionFactoryHolderCreated
;
75 /// Allows other frameworks to modify the ActiveRecordModel
76 /// before the generation of the NHibernate XML configuration.
77 /// As an example, this may be used to rewrite table names to
78 /// conform to an application-specific standard. Since the
79 /// configuration source is passed in, it is possible to
80 /// determine the underlying database type and make changes
83 public static event ModelsDelegate ModelsCreated
;
86 /// Allows other frameworks to modify the ActiveRecordModel
87 /// before the generation of the NHibernate XML configuration.
88 /// As an example, this may be used to rewrite table names to
89 /// conform to an application-specific standard. Since the
90 /// configuration source is passed in, it is possible to
91 /// determine the underlying database type and make changes
94 public static event ModelsDelegate ModelsValidated
;
97 /// Allows other frameworks to modify the ActiveRecordModel
98 /// before the generation of the NHibernate XML configuration.
100 public static event ModelDelegate ModelCreated
;
101 // public static event ModelsDelegate ModelsCreated;
105 /// Initialize the mappings using the configuration and
106 /// the list of types
108 public static void Initialize(IConfigurationSource source
, params Type
[] types
)
114 throw new ActiveRecordInitializationException("You can't invoke ActiveRecordStarter.Initialize more than once");
119 throw new ArgumentNullException("source");
123 throw new ArgumentNullException("types");
126 registeredTypes
= new Dictionary
<Type
, string>();
128 configSource
= source
;
130 // First initialization
131 ISessionFactoryHolder holder
= CreateSessionFactoryHolderImplementation(source
);
133 holder
.ThreadScopeInfo
= CreateThreadScopeInfoImplementation(source
);
135 RaiseSessionFactoryHolderCreated(holder
);
137 ActiveRecordBase
.holder
= holder
;
138 ActiveRecordModel
.type2Model
.Clear();
139 ActiveRecordModel
.isDebug
= source
.Debug
;
140 ActiveRecordModel
.isLazyByDefault
= source
.IsLazyByDefault
;
141 ActiveRecordModel
.pluralizeTableNames
= source
.PluralizeTableNames
;
143 // Sets up base configuration
144 SetUpConfiguration(source
, typeof(ActiveRecordBase
), holder
);
146 RegisterTypes(holder
, source
, types
, true);
148 isInitialized
= true;
153 /// Initialize the mappings using the configuration and
154 /// checking all the types on the specified <c>Assembly</c>
156 public static void Initialize(Assembly assembly
, IConfigurationSource source
)
158 List
<Type
> list
= new List
<Type
>();
160 CollectValidActiveRecordTypesFromAssembly(assembly
, list
, source
);
162 Initialize(source
, list
.ToArray());
166 /// Initialize the mappings using the configuration and
167 /// checking all the types on the specified Assemblies
169 public static void Initialize(Assembly
[] assemblies
, IConfigurationSource source
)
171 List
<Type
> list
= new List
<Type
>();
173 foreach(Assembly assembly
in assemblies
)
175 CollectValidActiveRecordTypesFromAssembly(assembly
, list
, source
);
178 Initialize(source
, list
.ToArray());
182 /// Initializes the framework reading the configuration from
183 /// the <c>AppDomain</c> and checking all the types on the executing <c>Assembly</c>
185 public static void Initialize()
187 IConfigurationSource source
= ActiveRecordSectionHandler
.Instance
;
189 Initialize(Assembly
.GetCallingAssembly(), source
);
193 /// Registers new assemblies in ActiveRecord
194 /// Usefull for dynamic assembly-adding after initialization
196 /// <param name="assemblies"></param>
197 public static void RegisterAssemblies(params Assembly
[] assemblies
)
199 List
<Type
> types
= new List
<Type
>();
201 foreach (Assembly assembly
in assemblies
)
203 CollectValidActiveRecordTypesFromAssembly(assembly
, types
, configSource
);
206 RegisterTypes(types
.ToArray());
210 /// Registers new types in ActiveRecord
211 /// Usefull for dynamic type-adding after initialization
213 /// <param name="types"></param>
214 public static void RegisterTypes(params Type
[] types
)
216 RegisterTypes(ActiveRecordBase
.holder
, configSource
, types
, false);
220 /// Generates and executes the creation scripts for the database.
222 public static void CreateSchema()
226 foreach(Configuration config
in ActiveRecordBase
.holder
.GetAllConfigurations())
228 SchemaExport export
= CreateSchemaExport(config
);
232 export
.Create(false, true);
236 throw new ActiveRecordException("Could not create the schema", ex
);
242 /// Generates and executes the creation scripts for the database using
243 /// the specified baseClass to know which database it should create the schema for.
245 public static void CreateSchema(Type baseClass
)
249 Configuration config
= ActiveRecordBase
.holder
.GetConfiguration(baseClass
);
251 SchemaExport export
= CreateSchemaExport(config
);
255 export
.Create(false, true);
259 throw new ActiveRecordException("Could not create the schema", ex
);
264 /// Executes the specified script to create/drop/change the database schema
266 public static void CreateSchemaFromFile(String scriptFileName
)
270 CreateSchemaFromFile(scriptFileName
, ActiveRecordBase
.holder
.CreateSession(typeof(ActiveRecordBase
)).Connection
);
274 /// Executes the specified script to create/drop/change the database schema
275 /// against the specified database connection
277 public static void CreateSchemaFromFile(String scriptFileName
, IDbConnection connection
)
281 if (connection
== null)
283 throw new ArgumentNullException("connection");
286 String
[] parts
= ARSchemaCreator
.OpenFileAndStripContents(scriptFileName
);
287 ARSchemaCreator
.ExecuteScriptParts(connection
, parts
);
291 /// Generates and executes the Drop scripts for the database.
293 public static void DropSchema()
297 foreach(Configuration config
in ActiveRecordBase
.holder
.GetAllConfigurations())
299 SchemaExport export
= CreateSchemaExport(config
);
303 export
.Drop(false, true);
307 throw new ActiveRecordException("Could not drop the schema", ex
);
313 /// Generates and executes the Drop scripts for the database using
314 /// the specified baseClass to know which database it should create the scripts for.
316 public static void DropSchema(Type baseClass
)
320 Configuration config
= ActiveRecordBase
.holder
.GetConfiguration(baseClass
);
322 SchemaExport export
= CreateSchemaExport(config
);
326 export
.Drop(false, true);
330 throw new ActiveRecordException("Could not drop the schema", ex
);
335 /// Generates the drop scripts for the database saving them to the supplied file name.
338 /// If ActiveRecord was configured to access more than one database, a file is going
339 /// to be generate for each, based on the path and the <c>fileName</c> specified.
341 public static void GenerateDropScripts(String fileName
)
345 bool isFirstExport
= true;
348 foreach(Configuration config
in ActiveRecordBase
.holder
.GetAllConfigurations())
350 SchemaExport export
= CreateSchemaExport(config
);
354 export
.SetOutputFile(isFirstExport
? fileName
: CreateAnotherFile(fileName
, fileCount
++));
355 export
.Drop(false, false);
359 throw new ActiveRecordException("Could not drop the schema", ex
);
362 isFirstExport
= false;
367 /// Generates the drop scripts for the database saving them to the supplied file name.
368 /// The baseType is used to identify which database should we act upon.
370 public static void GenerateDropScripts(Type baseType
, String fileName
)
374 Configuration config
= ActiveRecordBase
.holder
.GetConfiguration(baseType
);
376 SchemaExport export
= CreateSchemaExport(config
);
380 export
.SetOutputFile(fileName
);
381 export
.Drop(false, false);
385 throw new ActiveRecordException("Could not generate drop schema scripts", ex
);
390 /// Generates the creation scripts for the database
393 /// If ActiveRecord was configured to access more than one database, a file is going
394 /// to be generate for each, based on the path and the <c>fileName</c> specified.
396 public static void GenerateCreationScripts(String fileName
)
400 bool isFirstExport
= true;
403 foreach(Configuration config
in ActiveRecordBase
.holder
.GetAllConfigurations())
405 SchemaExport export
= CreateSchemaExport(config
);
409 export
.SetOutputFile(isFirstExport
? fileName
: CreateAnotherFile(fileName
, fileCount
++));
410 export
.Create(false, false);
414 throw new ActiveRecordException("Could not create the schema", ex
);
417 isFirstExport
= false;
422 /// Generates the creation scripts for the database
423 /// The baseType is used to identify which database should we act upon.
425 public static void GenerateCreationScripts(Type baseType
, String fileName
)
429 Configuration config
= ActiveRecordBase
.holder
.GetConfiguration(baseType
);
431 SchemaExport export
= CreateSchemaExport(config
);
435 export
.SetOutputFile(fileName
);
436 export
.Create(false, false);
440 throw new ActiveRecordException("Could not create the schema scripts", ex
);
445 /// Intended to be used only by test cases
447 public static void ResetInitializationFlag()
449 isInitialized
= false;
452 private static ActiveRecordModelCollection
BuildModels(ISessionFactoryHolder holder
,
453 IConfigurationSource source
,
454 IEnumerable
<Type
> types
, bool ignoreProblematicTypes
)
456 ActiveRecordModelBuilder builder
= new ActiveRecordModelBuilder();
458 ActiveRecordModelCollection models
= builder
.Models
;
460 foreach(Type type
in types
)
462 if (ShouldIgnoreType(type
))
464 if (ignoreProblematicTypes
)
470 throw new ActiveRecordException(
471 String
.Format("Type `{0}` is registered already", type
.FullName
));
474 else if (IsConfiguredAsRootType(type
))
476 if (TypeDefinesADatabaseBoundary(type
))
478 SetUpConfiguration(source
, type
, holder
);
483 throw new ActiveRecordException(
485 "Type `{0}` is not a valid root type. Make sure it is abstract and does not define a table itself.",
489 else if (!IsActiveRecordType(type
))
491 if (ignoreProblematicTypes
)
497 throw new ActiveRecordException(
498 String
.Format("Type `{0}` is not an ActiveRecord type. Use ActiveRecordAttributes to define one", type
.FullName
));
502 ActiveRecordModel model
= builder
.Create(type
);
506 throw new ActiveRecordException(
507 String
.Format("ActiveRecordModel for `{0}` could not be created", type
.FullName
));
510 registeredTypes
.Add(type
, String
.Empty
);
512 if (ModelCreated
!= null)
514 ModelCreated(model
, source
);
521 private static bool IsConfiguredAsRootType(Type type
)
523 return configSource
.GetConfiguration(type
) != null;
526 private static bool TypeDefinesADatabaseBoundary(Type type
)
528 return (type
.IsAbstract
&& !IsTypeHierarchyBase(type
));
531 private static bool ShouldIgnoreType(Type type
)
533 return (registeredTypes
.ContainsKey(type
) ||
534 type
== typeof(ActiveRecordBase
) ||
535 type
== typeof(ActiveRecordValidationBase
) ||
536 type
== typeof(ActiveRecordHooksBase
));
539 private static bool IsTypeHierarchyBase(ICustomAttributeProvider type
)
541 if (type
.IsDefined(typeof(JoinedBaseAttribute
), false))
546 object[] attrs
= type
.GetCustomAttributes(typeof(ActiveRecordAttribute
), false);
548 if (attrs
!= null && attrs
.Length
> 0)
550 ActiveRecordAttribute att
= (ActiveRecordAttribute
) attrs
[0];
552 return att
.DiscriminatorColumn
!= null;
558 private static void AddXmlToNHibernateCfg(ISessionFactoryHolder holder
, ActiveRecordModelCollection models
)
560 XmlGenerationVisitor xmlVisitor
= new XmlGenerationVisitor();
561 AssemblyXmlGenerator assemblyXmlGenerator
= new AssemblyXmlGenerator();
562 ISet assembliesGeneratedXmlFor
= new HashedSet();
563 foreach(ActiveRecordModel model
in models
)
565 Configuration config
= holder
.GetConfiguration(holder
.GetRootType(model
.Type
));
569 throw new ActiveRecordException(
571 "Could not find configuration for {0} or its root type {1} this is usually an indication that the configuration has not been setup correctly.",
572 model
.Type
, holder
.GetRootType(model
.Type
)));
575 if (!model
.IsNestedType
&& !model
.IsDiscriminatorSubClass
&& !model
.IsJoinedSubClass
)
578 xmlVisitor
.CreateXml(model
);
580 String xml
= xmlVisitor
.Xml
;
582 if (xml
!= String
.Empty
)
584 AddXmlString(config
, xml
, model
);
588 if (!assembliesGeneratedXmlFor
.Contains(model
.Type
.Assembly
))
590 assembliesGeneratedXmlFor
.Add(model
.Type
.Assembly
);
592 string[] configurations
= assemblyXmlGenerator
.CreateXmlConfigurations(model
.Type
.Assembly
);
594 foreach(string xml
in configurations
)
596 if (xml
!= string.Empty
)
598 config
.AddXmlString(xml
);
605 private static void AddXmlString(Configuration config
, string xml
, ActiveRecordModel model
)
609 config
.AddXmlString(xml
);
613 throw new ActiveRecordException("Error adding information from class " + model
.Type
.FullName
+ " to NHibernate. Check the inner exception for more information", ex
);
617 private static Type
[] GetExportedTypesFromAssembly(Assembly assembly
)
621 return assembly
.GetExportedTypes();
625 throw new ActiveRecordInitializationException(
626 "Error while loading the exported types from the assembly: " + assembly
.FullName
, ex
);
630 private static SchemaExport
CreateSchemaExport(Configuration cfg
)
632 return new SchemaExport(cfg
);
636 /// Return true if the type has a [ActiveRecord] attribute
638 private static bool IsActiveRecordType(ICustomAttributeProvider type
)
640 return type
.IsDefined(typeof(ActiveRecordAttribute
), false);
643 private static void CheckInitialized()
645 if (ActiveRecordBase
.holder
== null)
647 throw new ActiveRecordException("Framework must be Initialized first.");
651 private static Configuration
CreateConfiguration(IConfiguration config
)
653 // hammett comments: I'm gonna test this off for a while
654 // ayende comments: removing this means that tests that using generic entities as base class will break
655 // NH do not support generic entities at the moemnt
656 Environment
.UseReflectionOptimizer
= false;
658 Configuration cfg
= new Configuration();
660 foreach(IConfiguration childConfig
in config
.Children
)
662 cfg
.Properties
.Add(childConfig
.Name
, childConfig
.Value
);
668 private static void SetUpConfiguration(IConfigurationSource source
, Type type
, ISessionFactoryHolder holder
)
670 IConfiguration config
= source
.GetConfiguration(type
);
674 Configuration nconf
= CreateConfiguration(config
);
676 if (source
.NamingStrategyImplementation
!= null)
678 Type namingStrategyType
= source
.NamingStrategyImplementation
;
680 if (!typeof(INamingStrategy
).IsAssignableFrom(namingStrategyType
))
683 String
.Format("The specified type {0} does " + "not implement the interface INamingStrategy",
684 namingStrategyType
.FullName
);
686 throw new ActiveRecordException(message
);
689 nconf
.SetNamingStrategy((INamingStrategy
)Activator
.CreateInstance(namingStrategyType
));
692 holder
.Register(type
, nconf
);
696 private static void RaiseSessionFactoryHolderCreated(ISessionFactoryHolder holder
)
698 if (SessionFactoryHolderCreated
!= null)
700 SessionFactoryHolderCreated(holder
);
704 private static ISessionFactoryHolder
CreateSessionFactoryHolderImplementation(IConfigurationSource source
)
706 if (source
.SessionFactoryHolderImplementation
!= null)
708 Type sessionFactoryHolderType
= source
.SessionFactoryHolderImplementation
;
710 if (!typeof(ISessionFactoryHolder
).IsAssignableFrom(sessionFactoryHolderType
))
713 String
.Format("The specified type {0} does " + "not implement the interface ISessionFactoryHolder",
714 sessionFactoryHolderType
.FullName
);
716 throw new ActiveRecordException(message
);
719 return (ISessionFactoryHolder
) Activator
.CreateInstance(sessionFactoryHolderType
);
723 return new SessionFactoryHolder();
727 private static void RegisterTypes(ISessionFactoryHolder holder
, IConfigurationSource source
, IEnumerable
<Type
> types
,
728 bool ignoreProblematicTypes
)
732 ActiveRecordModelCollection models
= BuildModels(holder
, source
, types
, ignoreProblematicTypes
);
734 GraphConnectorVisitor connectorVisitor
= new GraphConnectorVisitor(models
);
735 connectorVisitor
.VisitNodes(models
);
737 ModelsDelegate modelsCreatedHandler
= ModelsCreated
;
738 if (modelsCreatedHandler
!= null)
740 modelsCreatedHandler(models
, source
);
743 SemanticVerifierVisitor semanticVisitor
= new SemanticVerifierVisitor(models
);
744 semanticVisitor
.VisitNodes(models
);
746 ModelsDelegate modelsValidatedHandler
= ModelsValidated
;
747 if(modelsValidatedHandler
!= null)
749 modelsValidatedHandler(models
, source
);
752 AddXmlToNHibernateCfg(holder
, models
);
754 if (source
.VerifyModelsAgainstDBSchema
)
756 VerifySchema(models
);
761 private static void VerifySchema(ActiveRecordModelCollection models
)
763 foreach(ActiveRecordModel model
in models
)
765 if (!model
.Type
.IsAbstract
)
769 ActiveRecordMediator
.FindAll(model
.Type
, Expression
.Sql("1=0"));
773 throw new ActiveRecordException("Error verifying the schema for model " + model
.Type
.Name
, ex
);
779 private static IThreadScopeInfo
CreateThreadScopeInfoImplementation(IConfigurationSource source
)
781 if (source
.ThreadScopeInfoImplementation
!= null)
783 Type threadScopeType
= source
.ThreadScopeInfoImplementation
;
785 if (!typeof(IThreadScopeInfo
).IsAssignableFrom(threadScopeType
))
788 String
.Format("The specified type {0} does " + "not implement the interface IThreadScopeInfo",
789 threadScopeType
.FullName
);
791 throw new ActiveRecordInitializationException(message
);
794 return (IThreadScopeInfo
) Activator
.CreateInstance(threadScopeType
);
798 return new ThreadScopeInfo();
803 /// Retrieve all classes decorated with ActiveRecordAttribute or that have been configured
804 /// as a AR base class.
806 /// <param name="assembly">Assembly to retrieve types from</param>
807 /// <param name="list">Array to store retrieved types in</param>
808 /// <param name="source">IConfigurationSource to inspect AR base declarations from</param>
809 private static void CollectValidActiveRecordTypesFromAssembly(Assembly assembly
, ICollection
<Type
> list
,
810 IConfigurationSource source
)
812 Type
[] types
= GetExportedTypesFromAssembly(assembly
);
814 foreach(Type type
in types
)
816 if (IsActiveRecordType(type
) || source
.GetConfiguration(type
) != null)
824 /// Generate a file name based on the original file name specified, using the
825 /// count to give it some order.
827 /// <param name="originalFileName"></param>
828 /// <param name="fileCount"></param>
829 /// <returns></returns>
830 private static string CreateAnotherFile(string originalFileName
, int fileCount
)
832 string path
= Path
.GetDirectoryName(originalFileName
);
833 string fileName
= Path
.GetFileNameWithoutExtension(originalFileName
);
834 string extension
= Path
.GetExtension(originalFileName
);
836 return Path
.Combine(path
, string.Format("{0}_{1}{2}", fileName
, fileCount
, extension
));