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.SessionFactoryHolderCreated"/>
36 /// <param name="holder"></param>
37 public delegate void SessionFactoryHolderDelegate(ISessionFactoryHolder holder
);
40 /// Delegate for use in <see cref="ActiveRecordStarter.ModelsCreated"/>
42 public delegate void ModelsCreatedDelegate(ActiveRecordModelCollection models
, IConfigurationSource source
);
45 /// Performs the framework initialization.
48 /// This class is not thread safe.
50 public sealed class ActiveRecordStarter
52 private static readonly Object lockConfig
= new object();
54 private static bool isInitialized
= false;
56 private static IDictionary
<Type
, string> registeredTypes
;
59 /// This is saved so one can invoke <c>RegisterTypes</c> later
61 private static IConfigurationSource configSource
;
64 /// So others frameworks can intercept the
65 /// creation and act on the holder instance
67 public static event SessionFactoryHolderDelegate SessionFactoryHolderCreated
;
70 /// Allows other frameworks to modify the ActiveRecordModel
71 /// before the generation of the NHibernate XML configuration.
72 /// As an example, this may be used to rewrite table names to
73 /// conform to an application-specific standard. Since the
74 /// configuration source is passed in, it is possible to
75 /// determine the underlying database type and make changes
78 public static event ModelsCreatedDelegate ModelsCreated
;
81 /// Allows other frameworks to modify the ActiveRecordModel
82 /// before the generation of the NHibernate XML configuration.
83 /// As an example, this may be used to rewrite table names to
84 /// conform to an application-specific standard. Since the
85 /// configuration source is passed in, it is possible to
86 /// determine the underlying database type and make changes
89 public static event ModelsCreatedDelegate ModelsValidated
;
92 /// Initialize the mappings using the configuration and
95 public static void Initialize(IConfigurationSource source
, params Type
[] types
)
101 throw new ActiveRecordInitializationException("You can't invoke ActiveRecordStarter.Initialize more than once");
106 throw new ArgumentNullException("source");
110 throw new ArgumentNullException("types");
113 registeredTypes
= new Dictionary
<Type
, string>();
115 configSource
= source
;
117 // First initialization
118 ISessionFactoryHolder holder
= CreateSessionFactoryHolderImplementation(source
);
120 holder
.ThreadScopeInfo
= CreateThreadScopeInfoImplementation(source
);
122 RaiseSessionFactoryHolderCreated(holder
);
124 ActiveRecordBase
.holder
= holder
;
125 ActiveRecordModel
.type2Model
.Clear();
126 ActiveRecordModel
.isDebug
= source
.Debug
;
127 ActiveRecordModel
.isLazyByDefault
= source
.IsLazyByDefault
;
128 ActiveRecordModel
.pluralizeTableNames
= source
.PluralizeTableNames
;
130 // Sets up base configuration
131 SetUpConfiguration(source
, typeof(ActiveRecordBase
), holder
);
133 RegisterTypes(holder
, source
, types
, true);
135 isInitialized
= true;
140 /// Initialize the mappings using the configuration and
141 /// checking all the types on the specified <c>Assembly</c>
143 public static void Initialize(Assembly assembly
, IConfigurationSource source
)
145 List
<Type
> list
= new List
<Type
>();
147 CollectValidActiveRecordTypesFromAssembly(assembly
, list
, source
);
149 Initialize(source
, list
.ToArray());
153 /// Initialize the mappings using the configuration and
154 /// checking all the types on the specified Assemblies
156 public static void Initialize(Assembly
[] assemblies
, IConfigurationSource source
)
158 List
<Type
> list
= new List
<Type
>();
160 foreach(Assembly assembly
in assemblies
)
162 CollectValidActiveRecordTypesFromAssembly(assembly
, list
, source
);
165 Initialize(source
, list
.ToArray());
169 /// Initializes the framework reading the configuration from
170 /// the <c>AppDomain</c> and checking all the types on the executing <c>Assembly</c>
172 public static void Initialize()
174 IConfigurationSource source
= ActiveRecordSectionHandler
.Instance
;
176 Initialize(Assembly
.GetCallingAssembly(), source
);
180 /// Registers new assemblies in ActiveRecord
181 /// Usefull for dynamic assembly-adding after initialization
183 /// <param name="assemblies"></param>
184 public static void RegisterAssemblies(params Assembly
[] assemblies
)
186 List
<Type
> types
= new List
<Type
>();
188 foreach (Assembly assembly
in assemblies
)
190 CollectValidActiveRecordTypesFromAssembly(assembly
, types
, configSource
);
193 RegisterTypes(types
.ToArray());
197 /// Registers new types in ActiveRecord
198 /// Usefull for dynamic type-adding after initialization
200 /// <param name="types"></param>
201 public static void RegisterTypes(params Type
[] types
)
203 RegisterTypes(ActiveRecordBase
.holder
, configSource
, types
, false);
207 /// Generates and executes the creation scripts for the database.
209 public static void CreateSchema()
213 foreach(Configuration config
in ActiveRecordBase
.holder
.GetAllConfigurations())
215 SchemaExport export
= CreateSchemaExport(config
);
219 export
.Create(false, true);
223 throw new ActiveRecordException("Could not create the schema", ex
);
229 /// Generates and executes the creation scripts for the database using
230 /// the specified baseClass to know which database it should create the schema for.
232 public static void CreateSchema(Type baseClass
)
236 Configuration config
= ActiveRecordBase
.holder
.GetConfiguration(baseClass
);
238 SchemaExport export
= CreateSchemaExport(config
);
242 export
.Create(false, true);
246 throw new ActiveRecordException("Could not create the schema", ex
);
251 /// Executes the specified script to create/drop/change the database schema
253 public static void CreateSchemaFromFile(String scriptFileName
)
257 CreateSchemaFromFile(scriptFileName
, ActiveRecordBase
.holder
.CreateSession(typeof(ActiveRecordBase
)).Connection
);
261 /// Executes the specified script to create/drop/change the database schema
262 /// against the specified database connection
264 public static void CreateSchemaFromFile(String scriptFileName
, IDbConnection connection
)
268 if (connection
== null)
270 throw new ArgumentNullException("connection");
273 String
[] parts
= ARSchemaCreator
.OpenFileAndStripContents(scriptFileName
);
274 ARSchemaCreator
.ExecuteScriptParts(connection
, parts
);
278 /// Generates and executes the Drop scripts for the database.
280 public static void DropSchema()
284 foreach(Configuration config
in ActiveRecordBase
.holder
.GetAllConfigurations())
286 SchemaExport export
= CreateSchemaExport(config
);
290 export
.Drop(false, true);
294 throw new ActiveRecordException("Could not drop the schema", ex
);
300 /// Generates and executes the Drop scripts for the database using
301 /// the specified baseClass to know which database it should create the scripts for.
303 public static void DropSchema(Type baseClass
)
307 Configuration config
= ActiveRecordBase
.holder
.GetConfiguration(baseClass
);
309 SchemaExport export
= CreateSchemaExport(config
);
313 export
.Drop(false, true);
317 throw new ActiveRecordException("Could not drop the schema", ex
);
322 /// Generates the drop scripts for the database saving them to the supplied file name.
325 /// If ActiveRecord was configured to access more than one database, a file is going
326 /// to be generate for each, based on the path and the <c>fileName</c> specified.
328 public static void GenerateDropScripts(String fileName
)
332 bool isFirstExport
= true;
335 foreach(Configuration config
in ActiveRecordBase
.holder
.GetAllConfigurations())
337 SchemaExport export
= CreateSchemaExport(config
);
341 export
.SetOutputFile(isFirstExport
? fileName
: CreateAnotherFile(fileName
, fileCount
++));
342 export
.Drop(false, false);
346 throw new ActiveRecordException("Could not drop the schema", ex
);
349 isFirstExport
= false;
354 /// Generates the drop scripts for the database saving them to the supplied file name.
355 /// The baseType is used to identify which database should we act upon.
357 public static void GenerateDropScripts(Type baseType
, String fileName
)
361 Configuration config
= ActiveRecordBase
.holder
.GetConfiguration(baseType
);
363 SchemaExport export
= CreateSchemaExport(config
);
367 export
.SetOutputFile(fileName
);
368 export
.Drop(false, false);
372 throw new ActiveRecordException("Could not generate drop schema scripts", ex
);
377 /// Generates the creation scripts for the database
380 /// If ActiveRecord was configured to access more than one database, a file is going
381 /// to be generate for each, based on the path and the <c>fileName</c> specified.
383 public static void GenerateCreationScripts(String fileName
)
387 bool isFirstExport
= true;
390 foreach(Configuration config
in ActiveRecordBase
.holder
.GetAllConfigurations())
392 SchemaExport export
= CreateSchemaExport(config
);
396 export
.SetOutputFile(isFirstExport
? fileName
: CreateAnotherFile(fileName
, fileCount
++));
397 export
.Create(false, false);
401 throw new ActiveRecordException("Could not create the schema", ex
);
404 isFirstExport
= false;
409 /// Generates the creation scripts for the database
410 /// The baseType is used to identify which database should we act upon.
412 public static void GenerateCreationScripts(Type baseType
, String fileName
)
416 Configuration config
= ActiveRecordBase
.holder
.GetConfiguration(baseType
);
418 SchemaExport export
= CreateSchemaExport(config
);
422 export
.SetOutputFile(fileName
);
423 export
.Create(false, false);
427 throw new ActiveRecordException("Could not create the schema scripts", ex
);
432 /// Intended to be used only by test cases
434 public static void ResetInitializationFlag()
436 isInitialized
= false;
439 private static ActiveRecordModelCollection
BuildModels(ISessionFactoryHolder holder
,
440 IConfigurationSource source
,
441 IEnumerable
<Type
> types
, bool ignoreProblematicTypes
)
443 ActiveRecordModelBuilder builder
= new ActiveRecordModelBuilder();
445 ActiveRecordModelCollection models
= builder
.Models
;
447 foreach(Type type
in types
)
449 if (ShouldIgnoreType(type
))
451 if (ignoreProblematicTypes
)
457 throw new ActiveRecordException(
458 String
.Format("Type `{0}` is registered already", type
.FullName
));
461 else if (IsConfiguredAsRootType(type
))
463 if (TypeDefinesADatabaseBoundary(type
))
465 SetUpConfiguration(source
, type
, holder
);
470 throw new ActiveRecordException(
472 "Type `{0}` is not a valid root type. Make sure it is abstract and does not define a table itself.",
476 else if (!IsActiveRecordType(type
))
478 if (ignoreProblematicTypes
)
484 throw new ActiveRecordException(
485 String
.Format("Type `{0}` is not an ActiveRecord type. Use ActiveRecordAttributes to define one", type
.FullName
));
489 ActiveRecordModel model
= builder
.Create(type
);
493 throw new ActiveRecordException(
494 String
.Format("ActiveRecordModel for `{0}` could not be created", type
.FullName
));
497 registeredTypes
.Add(type
, String
.Empty
);
503 private static bool IsConfiguredAsRootType(Type type
)
505 return configSource
.GetConfiguration(type
) != null;
508 private static bool TypeDefinesADatabaseBoundary(Type type
)
510 return (type
.IsAbstract
&& !IsTypeHierarchyBase(type
));
513 private static bool ShouldIgnoreType(Type type
)
515 return (registeredTypes
.ContainsKey(type
) ||
516 type
== typeof(ActiveRecordBase
) ||
517 type
== typeof(ActiveRecordValidationBase
) ||
518 type
== typeof(ActiveRecordHooksBase
));
521 private static bool IsTypeHierarchyBase(ICustomAttributeProvider type
)
523 if (type
.IsDefined(typeof(JoinedBaseAttribute
), false))
528 object[] attrs
= type
.GetCustomAttributes(typeof(ActiveRecordAttribute
), false);
530 if (attrs
!= null && attrs
.Length
> 0)
532 ActiveRecordAttribute att
= (ActiveRecordAttribute
) attrs
[0];
534 return att
.DiscriminatorColumn
!= null;
540 private static void AddXmlToNHibernateCfg(ISessionFactoryHolder holder
, ActiveRecordModelCollection models
)
542 XmlGenerationVisitor xmlVisitor
= new XmlGenerationVisitor();
543 AssemblyXmlGenerator assemblyXmlGenerator
= new AssemblyXmlGenerator();
544 ISet assembliesGeneratedXmlFor
= new HashedSet();
545 foreach(ActiveRecordModel model
in models
)
547 Configuration config
= holder
.GetConfiguration(holder
.GetRootType(model
.Type
));
551 throw new ActiveRecordException(
553 "Could not find configuration for {0} or its root type {1} this is usually an indication that the configuration has not been setup correctly.",
554 model
.Type
, holder
.GetRootType(model
.Type
)));
557 if (!model
.IsNestedType
&& !model
.IsDiscriminatorSubClass
&& !model
.IsJoinedSubClass
)
560 xmlVisitor
.CreateXml(model
);
562 String xml
= xmlVisitor
.Xml
;
564 if (xml
!= String
.Empty
)
566 AddXmlString(config
, xml
, model
);
570 if (!assembliesGeneratedXmlFor
.Contains(model
.Type
.Assembly
))
572 assembliesGeneratedXmlFor
.Add(model
.Type
.Assembly
);
574 string[] configurations
= assemblyXmlGenerator
.CreateXmlConfigurations(model
.Type
.Assembly
);
576 foreach(string xml
in configurations
)
578 if (xml
!= string.Empty
)
580 config
.AddXmlString(xml
);
587 private static void AddXmlString(Configuration config
, string xml
, ActiveRecordModel model
)
591 config
.AddXmlString(xml
);
595 throw new ActiveRecordException("Error adding information from class " + model
.Type
.FullName
+ " to NHibernate. Check the inner exception for more information", ex
);
599 private static Type
[] GetExportedTypesFromAssembly(Assembly assembly
)
603 return assembly
.GetExportedTypes();
607 throw new ActiveRecordInitializationException(
608 "Error while loading the exported types from the assembly: " + assembly
.FullName
, ex
);
612 private static SchemaExport
CreateSchemaExport(Configuration cfg
)
614 return new SchemaExport(cfg
);
618 /// Return true if the type has a [ActiveRecord] attribute
620 private static bool IsActiveRecordType(ICustomAttributeProvider type
)
622 return type
.IsDefined(typeof(ActiveRecordAttribute
), false);
625 private static void CheckInitialized()
627 if (ActiveRecordBase
.holder
== null)
629 throw new ActiveRecordException("Framework must be Initialized first.");
633 private static Configuration
CreateConfiguration(IConfiguration config
)
635 // hammett comments: I'm gonna test this off for a while
636 // ayende comments: removing this means that tests that using generic entities as base class will break
637 // NH do not support generic entities at the moemnt
638 Environment
.UseReflectionOptimizer
= false;
640 Configuration cfg
= new Configuration();
642 foreach(IConfiguration childConfig
in config
.Children
)
644 cfg
.Properties
.Add(childConfig
.Name
, childConfig
.Value
);
650 private static void SetUpConfiguration(IConfigurationSource source
, Type type
, ISessionFactoryHolder holder
)
652 IConfiguration config
= source
.GetConfiguration(type
);
656 Configuration nconf
= CreateConfiguration(config
);
658 if (source
.NamingStrategyImplementation
!= null)
660 Type namingStrategyType
= source
.NamingStrategyImplementation
;
662 if (!typeof(INamingStrategy
).IsAssignableFrom(namingStrategyType
))
665 String
.Format("The specified type {0} does " + "not implement the interface INamingStrategy",
666 namingStrategyType
.FullName
);
668 throw new ActiveRecordException(message
);
671 nconf
.SetNamingStrategy((INamingStrategy
)Activator
.CreateInstance(namingStrategyType
));
674 holder
.Register(type
, nconf
);
678 private static void RaiseSessionFactoryHolderCreated(ISessionFactoryHolder holder
)
680 if (SessionFactoryHolderCreated
!= null)
682 SessionFactoryHolderCreated(holder
);
686 private static ISessionFactoryHolder
CreateSessionFactoryHolderImplementation(IConfigurationSource source
)
688 if (source
.SessionFactoryHolderImplementation
!= null)
690 Type sessionFactoryHolderType
= source
.SessionFactoryHolderImplementation
;
692 if (!typeof(ISessionFactoryHolder
).IsAssignableFrom(sessionFactoryHolderType
))
695 String
.Format("The specified type {0} does " + "not implement the interface ISessionFactoryHolder",
696 sessionFactoryHolderType
.FullName
);
698 throw new ActiveRecordException(message
);
701 return (ISessionFactoryHolder
) Activator
.CreateInstance(sessionFactoryHolderType
);
705 return new SessionFactoryHolder();
709 private static void RegisterTypes(ISessionFactoryHolder holder
, IConfigurationSource source
, IEnumerable
<Type
> types
,
710 bool ignoreProblematicTypes
)
714 ActiveRecordModelCollection models
= BuildModels(holder
, source
, types
, ignoreProblematicTypes
);
716 GraphConnectorVisitor connectorVisitor
= new GraphConnectorVisitor(models
);
717 connectorVisitor
.VisitNodes(models
);
719 ModelsCreatedDelegate modelsCreatedHandler
= ModelsCreated
;
720 if (modelsCreatedHandler
!= null)
722 modelsCreatedHandler(models
, source
);
725 SemanticVerifierVisitor semanticVisitor
= new SemanticVerifierVisitor(models
);
726 semanticVisitor
.VisitNodes(models
);
728 ModelsCreatedDelegate modelsValidatedHandler
= ModelsValidated
;
729 if(modelsValidatedHandler
!= null)
731 modelsValidatedHandler(models
, source
);
734 AddXmlToNHibernateCfg(holder
, models
);
736 if (source
.VerifyModelsAgainstDBSchema
)
738 VerifySchema(models
);
743 private static void VerifySchema(ActiveRecordModelCollection models
)
745 foreach(ActiveRecordModel model
in models
)
747 if (!model
.Type
.IsAbstract
)
751 ActiveRecordMediator
.FindAll(model
.Type
, Expression
.Sql("1=0"));
755 throw new ActiveRecordException("Error verifying the schema for model " + model
.Type
.Name
, ex
);
761 private static IThreadScopeInfo
CreateThreadScopeInfoImplementation(IConfigurationSource source
)
763 if (source
.ThreadScopeInfoImplementation
!= null)
765 Type threadScopeType
= source
.ThreadScopeInfoImplementation
;
767 if (!typeof(IThreadScopeInfo
).IsAssignableFrom(threadScopeType
))
770 String
.Format("The specified type {0} does " + "not implement the interface IThreadScopeInfo",
771 threadScopeType
.FullName
);
773 throw new ActiveRecordInitializationException(message
);
776 return (IThreadScopeInfo
) Activator
.CreateInstance(threadScopeType
);
780 return new ThreadScopeInfo();
785 /// Retrieve all classes decorated with ActiveRecordAttribute or that have been configured
786 /// as a AR base class.
788 /// <param name="assembly">Assembly to retrieve types from</param>
789 /// <param name="list">Array to store retrieved types in</param>
790 /// <param name="source">IConfigurationSource to inspect AR base declarations from</param>
791 private static void CollectValidActiveRecordTypesFromAssembly(Assembly assembly
, ICollection
<Type
> list
,
792 IConfigurationSource source
)
794 Type
[] types
= GetExportedTypesFromAssembly(assembly
);
796 foreach(Type type
in types
)
798 if (IsActiveRecordType(type
) || source
.GetConfiguration(type
) != null)
806 /// Generate a file name based on the original file name specified, using the
807 /// count to give it some order.
809 /// <param name="originalFileName"></param>
810 /// <param name="fileCount"></param>
811 /// <returns></returns>
812 private static string CreateAnotherFile(string originalFileName
, int fileCount
)
814 string path
= Path
.GetDirectoryName(originalFileName
);
815 string fileName
= Path
.GetFileNameWithoutExtension(originalFileName
);
816 string extension
= Path
.GetExtension(originalFileName
);
818 return Path
.Combine(path
, string.Format("{0}_{1}{2}", fileName
, fileCount
, extension
));