Added RegisterAssemblies to ActiveRecordStarter. This simplifies unit testing effort...
[castle.git] / ActiveRecord / Castle.ActiveRecord / Framework / ActiveRecordStarter.cs
blob54fb811cb3587e32362d01725247c07628dafc09
1 // Copyright 2004-2007 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.Expression;
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 /// Initialize the mappings using the configuration and
82 /// the list of types
83 /// </summary>
84 public static void Initialize(IConfigurationSource source, params Type[] types)
86 lock(lockConfig)
88 if (isInitialized)
90 throw new ActiveRecordInitializationException("You can't invoke ActiveRecordStarter.Initialize more than once");
93 if (source == null)
95 throw new ArgumentNullException("source");
97 if (types == null)
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;
128 /// <summary>
129 /// Initialize the mappings using the configuration and
130 /// checking all the types on the specified <c>Assembly</c>
131 /// </summary>
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());
141 /// <summary>
142 /// Initialize the mappings using the configuration and
143 /// checking all the types on the specified Assemblies
144 /// </summary>
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());
157 /// <summary>
158 /// Initializes the framework reading the configuration from
159 /// the <c>AppDomain</c> and checking all the types on the executing <c>Assembly</c>
160 /// </summary>
161 public static void Initialize()
163 IConfigurationSource source = ActiveRecordSectionHandler.Instance;
165 Initialize(Assembly.GetExecutingAssembly(), source);
168 /// <summary>
169 /// Registers new assemblies in ActiveRecord
170 /// Usefull for dynamic assembly-adding after initialization
171 /// </summary>
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());
185 /// <summary>
186 /// Registers new types in ActiveRecord
187 /// Usefull for dynamic type-adding after initialization
188 /// </summary>
189 /// <param name="types"></param>
190 public static void RegisterTypes(params Type[] types)
192 RegisterTypes(ActiveRecordBase.holder, configSource, types, false);
195 /// <summary>
196 /// Generates and executes the creation scripts for the database.
197 /// </summary>
198 public static void CreateSchema()
200 CheckInitialized();
202 foreach(Configuration config in ActiveRecordBase.holder.GetAllConfigurations())
204 SchemaExport export = CreateSchemaExport(config);
208 export.Create(false, true);
210 catch(Exception ex)
212 throw new ActiveRecordException("Could not create the schema", ex);
217 /// <summary>
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.
220 /// </summary>
221 public static void CreateSchema(Type baseClass)
223 CheckInitialized();
225 Configuration config = ActiveRecordBase.holder.GetConfiguration(baseClass);
227 SchemaExport export = CreateSchemaExport(config);
231 export.Create(false, true);
233 catch (Exception ex)
235 throw new ActiveRecordException("Could not create the schema", ex);
239 /// <summary>
240 /// Executes the specified script to create/drop/change the database schema
241 /// </summary>
242 public static void CreateSchemaFromFile(String scriptFileName)
244 CheckInitialized();
246 CreateSchemaFromFile(scriptFileName, ActiveRecordBase.holder.CreateSession(typeof(ActiveRecordBase)).Connection);
249 /// <summary>
250 /// Executes the specified script to create/drop/change the database schema
251 /// against the specified database connection
252 /// </summary>
253 public static void CreateSchemaFromFile(String scriptFileName, IDbConnection connection)
255 CheckInitialized();
257 if (connection == null)
259 throw new ArgumentNullException("connection");
262 String[] parts = ARSchemaCreator.OpenFileAndStripContents(scriptFileName);
263 ARSchemaCreator.ExecuteScriptParts(connection, parts);
266 /// <summary>
267 /// Generates and executes the Drop scripts for the database.
268 /// </summary>
269 public static void DropSchema()
271 CheckInitialized();
273 foreach(Configuration config in ActiveRecordBase.holder.GetAllConfigurations())
275 SchemaExport export = CreateSchemaExport(config);
279 export.Drop(false, true);
281 catch(Exception ex)
283 throw new ActiveRecordException("Could not drop the schema", ex);
288 /// <summary>
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.
291 /// </summary>
292 public static void DropSchema(Type baseClass)
294 CheckInitialized();
296 Configuration config = ActiveRecordBase.holder.GetConfiguration(baseClass);
298 SchemaExport export = CreateSchemaExport(config);
302 export.Drop(false, true);
304 catch (Exception ex)
306 throw new ActiveRecordException("Could not drop the schema", ex);
310 /// <summary>
311 /// Generates the drop scripts for the database saving them to the supplied file name.
312 /// </summary>
313 /// <remarks>
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.
316 /// </remarks>
317 public static void GenerateDropScripts(String fileName)
319 CheckInitialized();
321 bool isFirstExport = true;
322 int fileCount = 1;
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);
333 catch(Exception ex)
335 throw new ActiveRecordException("Could not drop the schema", ex);
338 isFirstExport = false;
342 /// <summary>
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.
345 /// </summary>
346 public static void GenerateDropScripts(Type baseType, String fileName)
348 CheckInitialized();
350 Configuration config = ActiveRecordBase.holder.GetConfiguration(baseType);
352 SchemaExport export = CreateSchemaExport(config);
356 export.SetOutputFile(fileName);
357 export.Drop(false, false);
359 catch (Exception ex)
361 throw new ActiveRecordException("Could not generate drop schema scripts", ex);
365 /// <summary>
366 /// Generates the creation scripts for the database
367 /// </summary>
368 /// <remarks>
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.
371 /// </remarks>
372 public static void GenerateCreationScripts(String fileName)
374 CheckInitialized();
376 bool isFirstExport = true;
377 int fileCount = 1;
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);
388 catch(Exception ex)
390 throw new ActiveRecordException("Could not create the schema", ex);
393 isFirstExport = false;
397 /// <summary>
398 /// Generates the creation scripts for the database
399 /// The baseType is used to identify which database should we act upon.
400 /// </summary>
401 public static void GenerateCreationScripts(Type baseType, String fileName)
403 CheckInitialized();
405 Configuration config = ActiveRecordBase.holder.GetConfiguration(baseType);
407 SchemaExport export = CreateSchemaExport(config);
411 export.SetOutputFile(fileName);
412 export.Create(false, false);
414 catch (Exception ex)
416 throw new ActiveRecordException("Could not create the schema scripts", ex);
420 /// <summary>
421 /// Intended to be used only by test cases
422 /// </summary>
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)
442 continue;
444 else
446 throw new ActiveRecordException(
447 String.Format("Type `{0}` is registered already", type.FullName));
450 else if (TypeDefinesADatabaseBoundary(type))
452 SetUpConfiguration(source, type, holder);
454 continue;
456 else if (!IsActiveRecordType(type))
458 if (ignoreProblematicTypes)
460 continue;
462 else
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);
471 if (model == null)
473 throw new ActiveRecordException(
474 String.Format("ActiveRecordModel for `{0}` could not be created", type.FullName));
477 registeredTypes.Add(type, String.Empty);
480 return models;
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))
500 return true;
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;
512 return false;
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));
524 if (config==null)
526 throw new ActiveRecordException(
527 string.Format(
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)
534 xmlVisitor.Reset();
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);
568 catch(Exception ex)
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();
580 catch(Exception ex)
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);
592 /// <summary>
593 /// Return true if the type has a [ActiveRecord] attribute
594 /// </summary>
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);
622 return cfg;
625 private static void SetUpConfiguration(IConfigurationSource source, Type type, ISessionFactoryHolder holder)
627 IConfiguration config = source.GetConfiguration(type);
629 if (config != null)
631 Configuration nconf = CreateConfiguration(config);
633 if (source.NamingStrategyImplementation != null)
635 Type namingStrategyType = source.NamingStrategyImplementation;
637 if (!typeof(INamingStrategy).IsAssignableFrom(namingStrategyType))
639 String message =
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))
669 String message =
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);
678 else
680 return new SessionFactoryHolder();
684 private static void RegisterTypes(ISessionFactoryHolder holder, IConfigurationSource source, IEnumerable<Type> types,
685 bool ignoreProblematicTypes)
687 lock(lockConfig)
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"));
721 catch (Exception ex)
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))
737 String message =
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);
746 else
748 return new ThreadScopeInfo();
752 /// <summary>
753 /// Retrieve all classes decorated with ActiveRecordAttribute or that have been configured
754 /// as a AR base class.
755 /// </summary>
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)
768 list.Add(type);
773 /// <summary>
774 /// Generate a file name based on the original file name specified, using the
775 /// count to give it some order.
776 /// </summary>
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));