1 // Copyright 2004-2007 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
.Expression
;
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 /// Initialize the mappings using the configuration and
84 public static void Initialize(IConfigurationSource source
, params Type
[] types
)
90 throw new ActiveRecordInitializationException("You can't invoke ActiveRecordStarter.Initialize more than once");
95 throw new ArgumentNullException("source");
99 throw new ArgumentNullException("types");
102 registeredTypes
= new Dictionary
<Type
, string>();
104 configSource
= source
;
106 // First initialization
107 ISessionFactoryHolder holder
= CreateSessionFactoryHolderImplementation(source
);
109 holder
.ThreadScopeInfo
= CreateThreadScopeInfoImplementation(source
);
111 RaiseSessionFactoryHolderCreated(holder
);
113 ActiveRecordBase
.holder
= holder
;
114 ActiveRecordModel
.type2Model
.Clear();
115 ActiveRecordModel
.isDebug
= source
.Debug
;
116 ActiveRecordModel
.isLazyByDefault
= source
.IsLazyByDefault
;
117 ActiveRecordModel
.pluralizeTableNames
= source
.PluralizeTableNames
;
119 // Sets up base configuration
120 SetUpConfiguration(source
, typeof(ActiveRecordBase
), holder
);
122 RegisterTypes(holder
, source
, types
, true);
124 isInitialized
= true;
129 /// Initialize the mappings using the configuration and
130 /// checking all the types on the specified <c>Assembly</c>
132 public static void Initialize(Assembly assembly
, IConfigurationSource source
)
134 List
<Type
> list
= new List
<Type
>();
136 CollectValidActiveRecordTypesFromAssembly(assembly
, list
, source
);
138 Initialize(source
, list
.ToArray());
142 /// Initialize the mappings using the configuration and
143 /// checking all the types on the specified Assemblies
145 public static void Initialize(Assembly
[] assemblies
, IConfigurationSource source
)
147 List
<Type
> list
= new List
<Type
>();
149 foreach(Assembly assembly
in assemblies
)
151 CollectValidActiveRecordTypesFromAssembly(assembly
, list
, source
);
154 Initialize(source
, list
.ToArray());
158 /// Initializes the framework reading the configuration from
159 /// the <c>AppDomain</c> and checking all the types on the executing <c>Assembly</c>
161 public static void Initialize()
163 IConfigurationSource source
= ActiveRecordSectionHandler
.Instance
;
165 Initialize(Assembly
.GetExecutingAssembly(), source
);
169 /// Registers new assemblies in ActiveRecord
170 /// Usefull for dynamic assembly-adding after initialization
172 /// <param name="assemblies"></param>
173 public static void RegisterAssemblies(params Assembly
[] assemblies
)
175 List
<Type
> types
= new List
<Type
>();
177 foreach (Assembly assembly
in assemblies
)
179 CollectValidActiveRecordTypesFromAssembly(assembly
, types
, configSource
);
182 RegisterTypes(types
.ToArray());
186 /// Registers new types in ActiveRecord
187 /// Usefull for dynamic type-adding after initialization
189 /// <param name="types"></param>
190 public static void RegisterTypes(params Type
[] types
)
192 RegisterTypes(ActiveRecordBase
.holder
, configSource
, types
, false);
196 /// Generates and executes the creation scripts for the database.
198 public static void CreateSchema()
202 foreach(Configuration config
in ActiveRecordBase
.holder
.GetAllConfigurations())
204 SchemaExport export
= CreateSchemaExport(config
);
208 export
.Create(false, true);
212 throw new ActiveRecordException("Could not create the schema", ex
);
218 /// Generates and executes the creation scripts for the database using
219 /// the specified baseClass to know which database it should create the schema for.
221 public static void CreateSchema(Type baseClass
)
225 Configuration config
= ActiveRecordBase
.holder
.GetConfiguration(baseClass
);
227 SchemaExport export
= CreateSchemaExport(config
);
231 export
.Create(false, true);
235 throw new ActiveRecordException("Could not create the schema", ex
);
240 /// Executes the specified script to create/drop/change the database schema
242 public static void CreateSchemaFromFile(String scriptFileName
)
246 CreateSchemaFromFile(scriptFileName
, ActiveRecordBase
.holder
.CreateSession(typeof(ActiveRecordBase
)).Connection
);
250 /// Executes the specified script to create/drop/change the database schema
251 /// against the specified database connection
253 public static void CreateSchemaFromFile(String scriptFileName
, IDbConnection connection
)
257 if (connection
== null)
259 throw new ArgumentNullException("connection");
262 String
[] parts
= ARSchemaCreator
.OpenFileAndStripContents(scriptFileName
);
263 ARSchemaCreator
.ExecuteScriptParts(connection
, parts
);
267 /// Generates and executes the Drop scripts for the database.
269 public static void DropSchema()
273 foreach(Configuration config
in ActiveRecordBase
.holder
.GetAllConfigurations())
275 SchemaExport export
= CreateSchemaExport(config
);
279 export
.Drop(false, true);
283 throw new ActiveRecordException("Could not drop the schema", ex
);
289 /// Generates and executes the Drop scripts for the database using
290 /// the specified baseClass to know which database it should create the scripts for.
292 public static void DropSchema(Type baseClass
)
296 Configuration config
= ActiveRecordBase
.holder
.GetConfiguration(baseClass
);
298 SchemaExport export
= CreateSchemaExport(config
);
302 export
.Drop(false, true);
306 throw new ActiveRecordException("Could not drop the schema", ex
);
311 /// Generates the drop scripts for the database saving them to the supplied file name.
314 /// If ActiveRecord was configured to access more than one database, a file is going
315 /// to be generate for each, based on the path and the <c>fileName</c> specified.
317 public static void GenerateDropScripts(String fileName
)
321 bool isFirstExport
= true;
324 foreach(Configuration config
in ActiveRecordBase
.holder
.GetAllConfigurations())
326 SchemaExport export
= CreateSchemaExport(config
);
330 export
.SetOutputFile(isFirstExport
? fileName
: CreateAnotherFile(fileName
, fileCount
++));
331 export
.Drop(false, false);
335 throw new ActiveRecordException("Could not drop the schema", ex
);
338 isFirstExport
= false;
343 /// Generates the drop scripts for the database saving them to the supplied file name.
344 /// The baseType is used to identify which database should we act upon.
346 public static void GenerateDropScripts(Type baseType
, String fileName
)
350 Configuration config
= ActiveRecordBase
.holder
.GetConfiguration(baseType
);
352 SchemaExport export
= CreateSchemaExport(config
);
356 export
.SetOutputFile(fileName
);
357 export
.Drop(false, false);
361 throw new ActiveRecordException("Could not generate drop schema scripts", ex
);
366 /// Generates the creation scripts for the database
369 /// If ActiveRecord was configured to access more than one database, a file is going
370 /// to be generate for each, based on the path and the <c>fileName</c> specified.
372 public static void GenerateCreationScripts(String fileName
)
376 bool isFirstExport
= true;
379 foreach(Configuration config
in ActiveRecordBase
.holder
.GetAllConfigurations())
381 SchemaExport export
= CreateSchemaExport(config
);
385 export
.SetOutputFile(isFirstExport
? fileName
: CreateAnotherFile(fileName
, fileCount
++));
386 export
.Create(false, false);
390 throw new ActiveRecordException("Could not create the schema", ex
);
393 isFirstExport
= false;
398 /// Generates the creation scripts for the database
399 /// The baseType is used to identify which database should we act upon.
401 public static void GenerateCreationScripts(Type baseType
, String fileName
)
405 Configuration config
= ActiveRecordBase
.holder
.GetConfiguration(baseType
);
407 SchemaExport export
= CreateSchemaExport(config
);
411 export
.SetOutputFile(fileName
);
412 export
.Create(false, false);
416 throw new ActiveRecordException("Could not create the schema scripts", ex
);
421 /// Intended to be used only by test cases
423 public static void ResetInitializationFlag()
425 isInitialized
= false;
428 private static ActiveRecordModelCollection
BuildModels(ISessionFactoryHolder holder
,
429 IConfigurationSource source
,
430 IEnumerable
<Type
> types
, bool ignoreProblematicTypes
)
432 ActiveRecordModelBuilder builder
= new ActiveRecordModelBuilder();
434 ActiveRecordModelCollection models
= builder
.Models
;
436 foreach(Type type
in types
)
438 if (ShouldIgnoreType(type
))
440 if (ignoreProblematicTypes
)
446 throw new ActiveRecordException(
447 String
.Format("Type `{0}` is registered already", type
.FullName
));
450 else if (TypeDefinesADatabaseBoundary(type
))
452 SetUpConfiguration(source
, type
, holder
);
456 else if (!IsActiveRecordType(type
))
458 if (ignoreProblematicTypes
)
464 throw new ActiveRecordException(
465 String
.Format("Type `{0}` is not an ActiveRecord type. Use ActiveRecordAttributes to define one", type
.FullName
));
469 ActiveRecordModel model
= builder
.Create(type
);
473 throw new ActiveRecordException(
474 String
.Format("ActiveRecordModel for `{0}` could not be created", type
.FullName
));
477 registeredTypes
.Add(type
, String
.Empty
);
483 private static bool TypeDefinesADatabaseBoundary(Type type
)
485 return (type
.IsAbstract
&& !IsTypeHierarchyBase(type
));
488 private static bool ShouldIgnoreType(Type type
)
490 return (registeredTypes
.ContainsKey(type
) ||
491 type
== typeof(ActiveRecordBase
) ||
492 type
== typeof(ActiveRecordValidationBase
) ||
493 type
== typeof(ActiveRecordHooksBase
));
496 private static bool IsTypeHierarchyBase(ICustomAttributeProvider type
)
498 if (type
.IsDefined(typeof(JoinedBaseAttribute
), false))
503 object[] attrs
= type
.GetCustomAttributes(typeof(ActiveRecordAttribute
), false);
505 if (attrs
!= null && attrs
.Length
> 0)
507 ActiveRecordAttribute att
= (ActiveRecordAttribute
) attrs
[0];
509 return att
.DiscriminatorColumn
!= null;
515 private static void AddXmlToNHibernateCfg(ISessionFactoryHolder holder
, ActiveRecordModelCollection models
)
517 XmlGenerationVisitor xmlVisitor
= new XmlGenerationVisitor();
518 AssemblyXmlGenerator assemblyXmlGenerator
= new AssemblyXmlGenerator();
519 ISet assembliesGeneratedXmlFor
= new HashedSet();
520 foreach(ActiveRecordModel model
in models
)
522 Configuration config
= holder
.GetConfiguration(holder
.GetRootType(model
.Type
));
526 throw new ActiveRecordException(
528 "Could not find configuration for {0} or its root type {1} this is usually an indication that the configuration has not been setup correctly.",
529 model
.Type
, holder
.GetRootType(model
.Type
)));
532 if (!model
.IsNestedType
&& !model
.IsDiscriminatorSubClass
&& !model
.IsJoinedSubClass
)
535 xmlVisitor
.CreateXml(model
);
537 String xml
= xmlVisitor
.Xml
;
539 if (xml
!= String
.Empty
)
541 AddXmlString(config
, xml
, model
);
545 if (!assembliesGeneratedXmlFor
.Contains(model
.Type
.Assembly
))
547 assembliesGeneratedXmlFor
.Add(model
.Type
.Assembly
);
549 string[] configurations
= assemblyXmlGenerator
.CreateXmlConfigurations(model
.Type
.Assembly
);
551 foreach(string xml
in configurations
)
553 if (xml
!= string.Empty
)
555 config
.AddXmlString(xml
);
562 private static void AddXmlString(Configuration config
, string xml
, ActiveRecordModel model
)
566 config
.AddXmlString(xml
);
570 throw new ActiveRecordException("Error adding information from class " + model
.Type
.FullName
+ " to NHibernate. Check the inner exception for more information", ex
);
574 private static Type
[] GetExportedTypesFromAssembly(Assembly assembly
)
578 return assembly
.GetExportedTypes();
582 throw new ActiveRecordInitializationException(
583 "Error while loading the exported types from the assembly: " + assembly
.FullName
, ex
);
587 private static SchemaExport
CreateSchemaExport(Configuration cfg
)
589 return new SchemaExport(cfg
);
593 /// Return true if the type has a [ActiveRecord] attribute
595 private static bool IsActiveRecordType(ICustomAttributeProvider type
)
597 return type
.IsDefined(typeof(ActiveRecordAttribute
), false);
600 private static void CheckInitialized()
602 if (ActiveRecordBase
.holder
== null)
604 throw new ActiveRecordException("Framework must be Initialized first.");
608 private static Configuration
CreateConfiguration(IConfiguration config
)
610 // hammett comments: I'm gonna test this off for a while
611 // ayende comments: removing this means that tests that using generic entities as base class will break
612 // NH do not support generic entities at the moemnt
613 Environment
.UseReflectionOptimizer
= false;
615 Configuration cfg
= new Configuration();
617 foreach(IConfiguration childConfig
in config
.Children
)
619 cfg
.Properties
.Add(childConfig
.Name
, childConfig
.Value
);
625 private static void SetUpConfiguration(IConfigurationSource source
, Type type
, ISessionFactoryHolder holder
)
627 IConfiguration config
= source
.GetConfiguration(type
);
631 Configuration nconf
= CreateConfiguration(config
);
633 if (source
.NamingStrategyImplementation
!= null)
635 Type namingStrategyType
= source
.NamingStrategyImplementation
;
637 if (!typeof(INamingStrategy
).IsAssignableFrom(namingStrategyType
))
640 String
.Format("The specified type {0} does " + "not implement the interface INamingStrategy",
641 namingStrategyType
.FullName
);
643 throw new ActiveRecordException(message
);
646 nconf
.SetNamingStrategy((INamingStrategy
)Activator
.CreateInstance(namingStrategyType
));
649 holder
.Register(type
, nconf
);
653 private static void RaiseSessionFactoryHolderCreated(ISessionFactoryHolder holder
)
655 if (SessionFactoryHolderCreated
!= null)
657 SessionFactoryHolderCreated(holder
);
661 private static ISessionFactoryHolder
CreateSessionFactoryHolderImplementation(IConfigurationSource source
)
663 if (source
.SessionFactoryHolderImplementation
!= null)
665 Type sessionFactoryHolderType
= source
.SessionFactoryHolderImplementation
;
667 if (!typeof(ISessionFactoryHolder
).IsAssignableFrom(sessionFactoryHolderType
))
670 String
.Format("The specified type {0} does " + "not implement the interface ISessionFactoryHolder",
671 sessionFactoryHolderType
.FullName
);
673 throw new ActiveRecordException(message
);
676 return (ISessionFactoryHolder
) Activator
.CreateInstance(sessionFactoryHolderType
);
680 return new SessionFactoryHolder();
684 private static void RegisterTypes(ISessionFactoryHolder holder
, IConfigurationSource source
, IEnumerable
<Type
> types
,
685 bool ignoreProblematicTypes
)
689 ActiveRecordModelCollection models
= BuildModels(holder
, source
, types
, ignoreProblematicTypes
);
691 GraphConnectorVisitor connectorVisitor
= new GraphConnectorVisitor(models
);
692 connectorVisitor
.VisitNodes(models
);
694 SemanticVerifierVisitor semanticVisitor
= new SemanticVerifierVisitor(models
);
695 semanticVisitor
.VisitNodes(models
);
697 if (ModelsCreated
!= null)
699 ModelsCreated(models
, source
);
702 AddXmlToNHibernateCfg(holder
, models
);
704 if (source
.VerifyModelsAgainstDBSchema
)
706 VerifySchema(models
);
711 private static void VerifySchema(ActiveRecordModelCollection models
)
713 foreach(ActiveRecordModel model
in models
)
715 if (!model
.Type
.IsAbstract
)
719 ActiveRecordMediator
.FindAll(model
.Type
, Expression
.Sql("1=0"));
723 throw new ActiveRecordException("Error verifying the schema for model " + model
.Type
.Name
, ex
);
729 private static IThreadScopeInfo
CreateThreadScopeInfoImplementation(IConfigurationSource source
)
731 if (source
.ThreadScopeInfoImplementation
!= null)
733 Type threadScopeType
= source
.ThreadScopeInfoImplementation
;
735 if (!typeof(IThreadScopeInfo
).IsAssignableFrom(threadScopeType
))
738 String
.Format("The specified type {0} does " + "not implement the interface IThreadScopeInfo",
739 threadScopeType
.FullName
);
741 throw new ActiveRecordInitializationException(message
);
744 return (IThreadScopeInfo
) Activator
.CreateInstance(threadScopeType
);
748 return new ThreadScopeInfo();
753 /// Retrieve all classes decorated with ActiveRecordAttribute or that have been configured
754 /// as a AR base class.
756 /// <param name="assembly">Assembly to retrieve types from</param>
757 /// <param name="list">Array to store retrieved types in</param>
758 /// <param name="source">IConfigurationSource to inspect AR base declarations from</param>
759 private static void CollectValidActiveRecordTypesFromAssembly(Assembly assembly
, ICollection
<Type
> list
,
760 IConfigurationSource source
)
762 Type
[] types
= GetExportedTypesFromAssembly(assembly
);
764 foreach(Type type
in types
)
766 if (IsActiveRecordType(type
) || source
.GetConfiguration(type
) != null)
774 /// Generate a file name based on the original file name specified, using the
775 /// count to give it some order.
777 /// <param name="originalFileName"></param>
778 /// <param name="fileCount"></param>
779 /// <returns></returns>
780 public static string CreateAnotherFile(string originalFileName
, int fileCount
)
782 string path
= Path
.GetDirectoryName(originalFileName
);
783 string fileName
= Path
.GetFileNameWithoutExtension(originalFileName
);
784 string extension
= Path
.GetExtension(originalFileName
);
786 return Path
.Combine(path
, string.Format("{0}_{1}{2}", fileName
, fileCount
, extension
));