Refactored the Kernel registration fluent interface to be more readable, better suppo...
[castle.git] / ActiveRecord / Castle.ActiveRecord / Framework / ActiveRecordStarter.cs
blobc6c2edd19e357cd21751745be81663062b7720fe
1 // Copyright 2004-2008 Castle Project - http://www.castleproject.org/
2 //
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
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
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
17 using System;
18 using System.Collections.Generic;
19 using System.Data;
20 using System.IO;
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;
27 using NHibernate.Cfg;
28 using Iesi.Collections;
29 using NHibernate.Expressions;
30 using NHibernate.Tool.hbm2ddl;
31 using Environment=NHibernate.Cfg.Environment;
33 /// <summary>
34 /// Delegate for use in <see cref="ActiveRecordStarter.SessionFactoryHolderCreated"/>
35 /// </summary>
36 /// <param name="holder"></param>
37 public delegate void SessionFactoryHolderDelegate(ISessionFactoryHolder holder);
39 /// <summary>
40 /// Delegate for use in <see cref="ActiveRecordStarter.ModelsCreated"/>
41 /// </summary>
42 public delegate void ModelsCreatedDelegate(ActiveRecordModelCollection models, IConfigurationSource source);
44 /// <summary>
45 /// Performs the framework initialization.
46 /// </summary>
47 /// <remarks>
48 /// This class is not thread safe.
49 /// </remarks>
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;
58 /// <summary>
59 /// This is saved so one can invoke <c>RegisterTypes</c> later
60 /// </summary>
61 private static IConfigurationSource configSource;
63 /// <summary>
64 /// So others frameworks can intercept the
65 /// creation and act on the holder instance
66 /// </summary>
67 public static event SessionFactoryHolderDelegate SessionFactoryHolderCreated;
69 /// <summary>
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
76 /// if necessary.
77 /// </summary>
78 public static event ModelsCreatedDelegate ModelsCreated;
80 /// <summary>
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
87 /// if necessary.
88 /// </summary>
89 public static event ModelsCreatedDelegate ModelsValidated;
91 /// <summary>
92 /// Initialize the mappings using the configuration and
93 /// the list of types
94 /// </summary>
95 public static void Initialize(IConfigurationSource source, params Type[] types)
97 lock(lockConfig)
99 if (isInitialized)
101 throw new ActiveRecordInitializationException("You can't invoke ActiveRecordStarter.Initialize more than once");
104 if (source == null)
106 throw new ArgumentNullException("source");
108 if (types == null)
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;
139 /// <summary>
140 /// Initialize the mappings using the configuration and
141 /// checking all the types on the specified <c>Assembly</c>
142 /// </summary>
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());
152 /// <summary>
153 /// Initialize the mappings using the configuration and
154 /// checking all the types on the specified Assemblies
155 /// </summary>
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());
168 /// <summary>
169 /// Initializes the framework reading the configuration from
170 /// the <c>AppDomain</c> and checking all the types on the executing <c>Assembly</c>
171 /// </summary>
172 public static void Initialize()
174 IConfigurationSource source = ActiveRecordSectionHandler.Instance;
176 Initialize(Assembly.GetCallingAssembly(), source);
179 /// <summary>
180 /// Registers new assemblies in ActiveRecord
181 /// Usefull for dynamic assembly-adding after initialization
182 /// </summary>
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());
196 /// <summary>
197 /// Registers new types in ActiveRecord
198 /// Usefull for dynamic type-adding after initialization
199 /// </summary>
200 /// <param name="types"></param>
201 public static void RegisterTypes(params Type[] types)
203 RegisterTypes(ActiveRecordBase.holder, configSource, types, false);
206 /// <summary>
207 /// Generates and executes the creation scripts for the database.
208 /// </summary>
209 public static void CreateSchema()
211 CheckInitialized();
213 foreach(Configuration config in ActiveRecordBase.holder.GetAllConfigurations())
215 SchemaExport export = CreateSchemaExport(config);
219 export.Create(false, true);
221 catch(Exception ex)
223 throw new ActiveRecordException("Could not create the schema", ex);
228 /// <summary>
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.
231 /// </summary>
232 public static void CreateSchema(Type baseClass)
234 CheckInitialized();
236 Configuration config = ActiveRecordBase.holder.GetConfiguration(baseClass);
238 SchemaExport export = CreateSchemaExport(config);
242 export.Create(false, true);
244 catch (Exception ex)
246 throw new ActiveRecordException("Could not create the schema", ex);
250 /// <summary>
251 /// Executes the specified script to create/drop/change the database schema
252 /// </summary>
253 public static void CreateSchemaFromFile(String scriptFileName)
255 CheckInitialized();
257 CreateSchemaFromFile(scriptFileName, ActiveRecordBase.holder.CreateSession(typeof(ActiveRecordBase)).Connection);
260 /// <summary>
261 /// Executes the specified script to create/drop/change the database schema
262 /// against the specified database connection
263 /// </summary>
264 public static void CreateSchemaFromFile(String scriptFileName, IDbConnection connection)
266 CheckInitialized();
268 if (connection == null)
270 throw new ArgumentNullException("connection");
273 String[] parts = ARSchemaCreator.OpenFileAndStripContents(scriptFileName);
274 ARSchemaCreator.ExecuteScriptParts(connection, parts);
277 /// <summary>
278 /// Generates and executes the Drop scripts for the database.
279 /// </summary>
280 public static void DropSchema()
282 CheckInitialized();
284 foreach(Configuration config in ActiveRecordBase.holder.GetAllConfigurations())
286 SchemaExport export = CreateSchemaExport(config);
290 export.Drop(false, true);
292 catch(Exception ex)
294 throw new ActiveRecordException("Could not drop the schema", ex);
299 /// <summary>
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.
302 /// </summary>
303 public static void DropSchema(Type baseClass)
305 CheckInitialized();
307 Configuration config = ActiveRecordBase.holder.GetConfiguration(baseClass);
309 SchemaExport export = CreateSchemaExport(config);
313 export.Drop(false, true);
315 catch (Exception ex)
317 throw new ActiveRecordException("Could not drop the schema", ex);
321 /// <summary>
322 /// Generates the drop scripts for the database saving them to the supplied file name.
323 /// </summary>
324 /// <remarks>
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.
327 /// </remarks>
328 public static void GenerateDropScripts(String fileName)
330 CheckInitialized();
332 bool isFirstExport = true;
333 int fileCount = 1;
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);
344 catch(Exception ex)
346 throw new ActiveRecordException("Could not drop the schema", ex);
349 isFirstExport = false;
353 /// <summary>
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.
356 /// </summary>
357 public static void GenerateDropScripts(Type baseType, String fileName)
359 CheckInitialized();
361 Configuration config = ActiveRecordBase.holder.GetConfiguration(baseType);
363 SchemaExport export = CreateSchemaExport(config);
367 export.SetOutputFile(fileName);
368 export.Drop(false, false);
370 catch (Exception ex)
372 throw new ActiveRecordException("Could not generate drop schema scripts", ex);
376 /// <summary>
377 /// Generates the creation scripts for the database
378 /// </summary>
379 /// <remarks>
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.
382 /// </remarks>
383 public static void GenerateCreationScripts(String fileName)
385 CheckInitialized();
387 bool isFirstExport = true;
388 int fileCount = 1;
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);
399 catch(Exception ex)
401 throw new ActiveRecordException("Could not create the schema", ex);
404 isFirstExport = false;
408 /// <summary>
409 /// Generates the creation scripts for the database
410 /// The baseType is used to identify which database should we act upon.
411 /// </summary>
412 public static void GenerateCreationScripts(Type baseType, String fileName)
414 CheckInitialized();
416 Configuration config = ActiveRecordBase.holder.GetConfiguration(baseType);
418 SchemaExport export = CreateSchemaExport(config);
422 export.SetOutputFile(fileName);
423 export.Create(false, false);
425 catch (Exception ex)
427 throw new ActiveRecordException("Could not create the schema scripts", ex);
431 /// <summary>
432 /// Intended to be used only by test cases
433 /// </summary>
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)
453 continue;
455 else
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);
466 continue;
468 else
470 throw new ActiveRecordException(
471 string.Format(
472 "Type `{0}` is not a valid root type. Make sure it is abstract and does not define a table itself.",
473 type.FullName));
476 else if (!IsActiveRecordType(type))
478 if (ignoreProblematicTypes)
480 continue;
482 else
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);
491 if (model == null)
493 throw new ActiveRecordException(
494 String.Format("ActiveRecordModel for `{0}` could not be created", type.FullName));
497 registeredTypes.Add(type, String.Empty);
500 return models;
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))
525 return true;
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;
537 return false;
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));
549 if (config==null)
551 throw new ActiveRecordException(
552 string.Format(
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)
559 xmlVisitor.Reset();
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);
593 catch(Exception ex)
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();
605 catch(Exception ex)
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);
617 /// <summary>
618 /// Return true if the type has a [ActiveRecord] attribute
619 /// </summary>
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);
647 return cfg;
650 private static void SetUpConfiguration(IConfigurationSource source, Type type, ISessionFactoryHolder holder)
652 IConfiguration config = source.GetConfiguration(type);
654 if (config != null)
656 Configuration nconf = CreateConfiguration(config);
658 if (source.NamingStrategyImplementation != null)
660 Type namingStrategyType = source.NamingStrategyImplementation;
662 if (!typeof(INamingStrategy).IsAssignableFrom(namingStrategyType))
664 String message =
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))
694 String message =
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);
703 else
705 return new SessionFactoryHolder();
709 private static void RegisterTypes(ISessionFactoryHolder holder, IConfigurationSource source, IEnumerable<Type> types,
710 bool ignoreProblematicTypes)
712 lock(lockConfig)
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"));
753 catch (Exception ex)
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))
769 String message =
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);
778 else
780 return new ThreadScopeInfo();
784 /// <summary>
785 /// Retrieve all classes decorated with ActiveRecordAttribute or that have been configured
786 /// as a AR base class.
787 /// </summary>
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)
800 list.Add(type);
805 /// <summary>
806 /// Generate a file name based on the original file name specified, using the
807 /// count to give it some order.
808 /// </summary>
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));