From: <fab...@us...> - 2009-10-18 13:40:02
|
Revision: 4774 http://nhibernate.svn.sourceforge.net/nhibernate/?rev=4774&view=rev Author: fabiomaulo Date: 2009-10-18 13:39:50 +0000 (Sun, 18 Oct 2009) Log Message: ----------- Merge r4773 (fix NH-1760) Modified Paths: -------------- trunk/nhibernate/src/NHibernate/Engine/JoinHelper.cs trunk/nhibernate/src/NHibernate/Loader/AbstractEntityJoinWalker.cs trunk/nhibernate/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs trunk/nhibernate/src/NHibernate/Loader/JoinWalker.cs trunk/nhibernate/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs trunk/nhibernate/src/NHibernate/Persister/Entity/IOuterJoinLoadable.cs trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH1760/DomainClass.cs trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH1760/Mappings.hbm.xml trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH1760/SampleTest.cs Modified: trunk/nhibernate/src/NHibernate/Engine/JoinHelper.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Engine/JoinHelper.cs 2009-10-18 13:19:08 UTC (rev 4773) +++ trunk/nhibernate/src/NHibernate/Engine/JoinHelper.cs 2009-10-18 13:39:50 UTC (rev 4774) @@ -1,4 +1,3 @@ -using System; using NHibernate.Persister.Entity; using NHibernate.Type; using NHibernate.Util; @@ -11,118 +10,150 @@ { } - /// <summary> - /// Get the aliased columns of the owning entity which are to - /// be used in the join - /// </summary> - public static string[] GetAliasedLHSColumnNames( - IAssociationType type, - string alias, - int property, - IOuterJoinLoadable lhsPersister, - IMapping mapping - ) + public static ILhsAssociationTypeSqlInfo GetLhsSqlInfo(string alias, int property, + IOuterJoinLoadable lhsPersister, IMapping mapping) { - return GetAliasedLHSColumnNames(type, alias, property, 0, lhsPersister, mapping); + return new PropertiesLhsAssociationTypeSqlInfo(alias, property, lhsPersister, mapping); } + public static ILhsAssociationTypeSqlInfo GetIdLhsSqlInfo(string alias, IOuterJoinLoadable lhsPersister, IMapping mapping) + { + return new IdPropertiesLhsAssociationTypeSqlInfo(alias, lhsPersister, mapping); + } + /// <summary> - /// Get the columns of the owning entity which are to + /// Get the columns of the associated table which are to /// be used in the join /// </summary> - public static string[] GetLHSColumnNames( - IAssociationType type, - int property, - IOuterJoinLoadable lhsPersister, - IMapping mapping - ) + public static string[] GetRHSColumnNames(IAssociationType type, ISessionFactoryImplementor factory) { - return GetLHSColumnNames(type, property, 0, lhsPersister, mapping); + string uniqueKeyPropertyName = type.RHSUniqueKeyPropertyName; + IJoinable joinable = type.GetAssociatedJoinable(factory); + if (uniqueKeyPropertyName == null) + { + return joinable.KeyColumnNames; + } + else + { + return ((IOuterJoinLoadable)joinable).GetPropertyColumnNames(uniqueKeyPropertyName); + } } + } + public interface ILhsAssociationTypeSqlInfo + { /// <summary> /// Get the aliased columns of the owning entity which are to /// be used in the join /// </summary> - public static string[] GetAliasedLHSColumnNames( - IAssociationType type, - string alias, - int property, - int begin, - IOuterJoinLoadable lhsPersister, - IMapping mapping - ) + string[] GetAliasedColumnNames(IAssociationType type, int begin); + + /// <summary> + /// Get the columns of the owning entity which are to + /// be used in the join + /// </summary> + string[] GetColumnNames(IAssociationType type, int begin); + + string GetTableName(IAssociationType type); + } + + public abstract class AbstractLhsAssociationTypeSqlInfo : ILhsAssociationTypeSqlInfo + { + protected AbstractLhsAssociationTypeSqlInfo(string @alias, IOuterJoinLoadable persister, IMapping mapping) { + Alias = alias; + Persister = persister; + Mapping = mapping; + } + + public string Alias { get; private set; } + + public IOuterJoinLoadable Persister { get; private set; } + + public IMapping Mapping { get; private set; } + + #region Implementation of ILhsAssociationTypeSqlInfo + + + public string[] GetAliasedColumnNames(IAssociationType type, int begin) + { if (type.UseLHSPrimaryKey) { - return StringHelper.Qualify(alias, lhsPersister.IdentifierColumnNames); + return StringHelper.Qualify(Alias, Persister.IdentifierColumnNames); } else { string propertyName = type.LHSPropertyName; if (propertyName == null) { - return ArrayHelper.Slice( - lhsPersister.ToColumns(alias, property), - begin, - type.GetColumnSpan(mapping) - ); + return ArrayHelper.Slice(GetAliasedColumns(), begin, type.GetColumnSpan(Mapping)); } else { - return ((IPropertyMapping) lhsPersister).ToColumns(alias, propertyName); //bad cast + return ((IPropertyMapping)Persister).ToColumns(Alias, propertyName); //bad cast } } } - /// <summary> - /// Get the columns of the owning entity which are to - /// be used in the join - /// </summary> - public static string[] GetLHSColumnNames( - IAssociationType type, - int property, - int begin, - IOuterJoinLoadable lhsPersister, - IMapping mapping - ) + public string[] GetColumnNames(IAssociationType type, int begin) { if (type.UseLHSPrimaryKey) { - //return lhsPersister.getSubclassPropertyColumnNames(property); - return lhsPersister.IdentifierColumnNames; + return Persister.IdentifierColumnNames; } else { string propertyName = type.LHSPropertyName; if (propertyName == null) { - //slice, to get the columns for this component - //property - return ArrayHelper.Slice( - lhsPersister.GetSubclassPropertyColumnNames(property), - begin, - type.GetColumnSpan(mapping) - ); + //slice, to get the columns for this component property + return ArrayHelper.Slice(GetColumns(), begin, type.GetColumnSpan(Mapping)); } else { //property-refs for associations defined on a //component are not supported, so no need to slice - return lhsPersister.GetPropertyColumnNames(propertyName); + return Persister.GetPropertyColumnNames(propertyName); } } } - public static string GetLHSTableName( - IAssociationType type, - int property, - IOuterJoinLoadable lhsPersister - ) + protected abstract string[] GetAliasedColumns(); + protected abstract string[] GetColumns(); + + public abstract string GetTableName(IAssociationType type); + + #endregion + } + + public class PropertiesLhsAssociationTypeSqlInfo : AbstractLhsAssociationTypeSqlInfo + { + private readonly int propertyIdx; + + public PropertiesLhsAssociationTypeSqlInfo(string alias, + int propertyIdx, IOuterJoinLoadable persister, IMapping mapping) + : base(alias, persister, mapping) { + this.propertyIdx = propertyIdx; + } + + #region Overrides of AbstractLhsAssociationTypeSqlInfo + + protected override string[] GetAliasedColumns() + { + return Persister.ToColumns(Alias, propertyIdx); + } + + protected override string[] GetColumns() + { + return Persister.GetSubclassPropertyColumnNames(propertyIdx); + } + + public override string GetTableName(IAssociationType type) + { if (type.UseLHSPrimaryKey) { - return lhsPersister.TableName; + return Persister.TableName; } else { @@ -132,12 +163,12 @@ //if there is no property-ref, assume the join //is to the subclass table (ie. the table of the //subclass that the association belongs to) - return lhsPersister.GetSubclassPropertyTableName(property); + return Persister.GetSubclassPropertyTableName(propertyIdx); } else { //handle a property-ref - string propertyRefTable = lhsPersister.GetPropertyTableName(propertyName); + string propertyRefTable = Persister.GetPropertyTableName(propertyName); if (propertyRefTable == null) { //it is possible that the tree-walking in OuterJoinLoader can get to @@ -147,29 +178,38 @@ //assumes that the property-ref refers to a property of the subclass //table that the association belongs to (a reasonable guess) //TODO: fix this, add: IOuterJoinLoadable.getSubclassPropertyTableName(string propertyName) - propertyRefTable = lhsPersister.GetSubclassPropertyTableName(property); + propertyRefTable = Persister.GetSubclassPropertyTableName(propertyIdx); } return propertyRefTable; } } } - /// <summary> - /// Get the columns of the associated table which are to - /// be used in the join - /// </summary> - public static string[] GetRHSColumnNames(IAssociationType type, ISessionFactoryImplementor factory) + #endregion + } + + public class IdPropertiesLhsAssociationTypeSqlInfo : AbstractLhsAssociationTypeSqlInfo + { + public IdPropertiesLhsAssociationTypeSqlInfo(string alias, IOuterJoinLoadable persister, IMapping mapping) : base(alias, persister, mapping) {} + + #region Overrides of AbstractLhsAssociationTypeSqlInfo + + protected override string[] GetAliasedColumns() { - string uniqueKeyPropertyName = type.RHSUniqueKeyPropertyName; - IJoinable joinable = type.GetAssociatedJoinable(factory); - if (uniqueKeyPropertyName == null) - { - return joinable.KeyColumnNames; - } - else - { - return ((IOuterJoinLoadable) joinable).GetPropertyColumnNames(uniqueKeyPropertyName); - } + return Persister.ToIdentifierColumns(Alias); } + + protected override string[] GetColumns() + { + return Persister.IdentifierColumnNames; + } + + public override string GetTableName(IAssociationType type) + { + return Persister.TableName; + } + + #endregion } + } \ No newline at end of file Modified: trunk/nhibernate/src/NHibernate/Loader/AbstractEntityJoinWalker.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Loader/AbstractEntityJoinWalker.cs 2009-10-18 13:19:08 UTC (rev 4773) +++ trunk/nhibernate/src/NHibernate/Loader/AbstractEntityJoinWalker.cs 2009-10-18 13:39:50 UTC (rev 4774) @@ -28,7 +28,7 @@ alias = rootSqlAlias; } - protected void InitAll(SqlString whereString, SqlString orderByString, LockMode lockMode) + protected virtual void InitAll(SqlString whereString, SqlString orderByString, LockMode lockMode) { WalkEntityTree(persister, Alias); IList<OuterJoinableAssociation> allAssociations = new List<OuterJoinableAssociation>(associations); @@ -99,7 +99,7 @@ public abstract string Comment { get; } - protected ILoadable Persister + protected IOuterJoinLoadable Persister { get { return persister; } } Modified: trunk/nhibernate/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs 2009-10-18 13:19:08 UTC (rev 4773) +++ trunk/nhibernate/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs 2009-10-18 13:39:50 UTC (rev 4774) @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Iesi.Collections.Generic; using log4net; @@ -57,6 +58,25 @@ userAliases = ArrayHelper.ToStringArray(userAliasList); } + protected override void InitAll(SqlString whereString, SqlString orderByString, LockMode lockMode) + { + // NH different behavior (NH-1760) + WalkCompositeComponentIdTree(); + base.InitAll(whereString, orderByString, lockMode); + } + + private void WalkCompositeComponentIdTree() + { + IType type = Persister.IdentifierType; + string propertyName = Persister.IdentifierPropertyName; + if (type != null && type.IsComponentType && !(type is EmbeddedComponentType)) + { + ILhsAssociationTypeSqlInfo associationTypeSQLInfo = JoinHelper.GetIdLhsSqlInfo(Alias, Persister, Factory); + WalkComponentTree((IAbstractComponentType) type, 0, Alias, SubPath(string.Empty, propertyName), 0, + associationTypeSQLInfo); + } + } + public IType[] ResultTypes { get { return resultTypes; } Modified: trunk/nhibernate/src/NHibernate/Loader/JoinWalker.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Loader/JoinWalker.cs 2009-10-18 13:19:08 UTC (rev 4773) +++ trunk/nhibernate/src/NHibernate/Loader/JoinWalker.cs 2009-10-18 13:39:50 UTC (rev 4774) @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using Iesi.Collections.Generic; using NHibernate.Collection; using NHibernate.Engine; @@ -218,20 +217,18 @@ } } - private void WalkEntityAssociationTree(IAssociationType associationType, IOuterJoinLoadable persister, int propertyNumber, - string alias, string path, bool nullable, int currentDepth) + private void WalkEntityAssociationTree(IAssociationType associationType, IOuterJoinLoadable persister, + int propertyNumber, string alias, string path, bool nullable, int currentDepth, + ILhsAssociationTypeSqlInfo associationTypeSQLInfo) { - string[] aliasedLhsColumns = - JoinHelper.GetAliasedLHSColumnNames(associationType, alias, propertyNumber, persister, Factory); + string[] aliasedLhsColumns = associationTypeSQLInfo.GetAliasedColumnNames(associationType, 0); + string[] lhsColumns = associationTypeSQLInfo.GetColumnNames(associationType, 0); + string lhsTable = associationTypeSQLInfo.GetTableName(associationType); - string[] lhsColumns = JoinHelper.GetLHSColumnNames(associationType, propertyNumber, persister, Factory); - string lhsTable = JoinHelper.GetLHSTableName(associationType, propertyNumber, persister); - string subpath = SubPath(path, persister.GetSubclassPropertyName(propertyNumber)); - JoinType joinType = - GetJoinType(associationType, persister.GetFetchMode(propertyNumber), subpath, lhsTable, lhsColumns, nullable, - currentDepth, persister.GetCascadeStyle(propertyNumber)); + JoinType joinType = GetJoinType(associationType, persister.GetFetchMode(propertyNumber), subpath, lhsTable, + lhsColumns, nullable, currentDepth, persister.GetCascadeStyle(propertyNumber)); AddAssociationToJoinTreeIfNecessary(associationType, aliasedLhsColumns, alias, subpath, currentDepth, joinType); } @@ -246,15 +243,16 @@ for (int i = 0; i < n; i++) { IType type = persister.GetSubclassPropertyType(i); + ILhsAssociationTypeSqlInfo associationTypeSQLInfo = JoinHelper.GetLhsSqlInfo(alias, i, persister, Factory); if (type.IsAssociationType) { - WalkEntityAssociationTree((IAssociationType)type, persister, i, alias, path, - persister.IsSubclassPropertyNullable(i), currentDepth); + WalkEntityAssociationTree((IAssociationType) type, persister, i, alias, path, + persister.IsSubclassPropertyNullable(i), currentDepth, associationTypeSQLInfo); } else if (type.IsComponentType) { - WalkComponentTree((IAbstractComponentType)type, i, 0, persister, alias, - SubPath(path, persister.GetSubclassPropertyName(i)), currentDepth); + WalkComponentTree((IAbstractComponentType) type, 0, alias, SubPath(path, persister.GetSubclassPropertyName(i)), + currentDepth, associationTypeSQLInfo); } } } @@ -262,8 +260,8 @@ /// <summary> /// For a component, add to a list of associations to be fetched by outerjoin /// </summary> - private void WalkComponentTree(IAbstractComponentType componentType, int propertyNumber, int begin, - IOuterJoinLoadable persister, string alias, string path, int currentDepth) + protected void WalkComponentTree(IAbstractComponentType componentType, int begin, string alias, string path, + int currentDepth, ILhsAssociationTypeSqlInfo associationTypeSQLInfo) { IType[] types = componentType.Subtypes; string[] propertyNames = componentType.PropertyNames; @@ -271,19 +269,18 @@ { if (types[i].IsAssociationType) { - IAssociationType associationType = (IAssociationType)types[i]; - string[] aliasedLhsColumns = - JoinHelper.GetAliasedLHSColumnNames(associationType, alias, propertyNumber, begin, persister, Factory); + var associationType = (IAssociationType) types[i]; - string[] lhsColumns = JoinHelper.GetLHSColumnNames(associationType, propertyNumber, begin, persister, Factory); - string lhsTable = JoinHelper.GetLHSTableName(associationType, propertyNumber, persister); + string[] aliasedLhsColumns = associationTypeSQLInfo.GetAliasedColumnNames(associationType, begin); + string[] lhsColumns = associationTypeSQLInfo.GetColumnNames(associationType, begin); + string lhsTable = associationTypeSQLInfo.GetTableName(associationType); string subpath = SubPath(path, propertyNames[i]); bool[] propertyNullability = componentType.PropertyNullability; - JoinType joinType = - GetJoinType(associationType, componentType.GetFetchMode(i), subpath, lhsTable, lhsColumns, - propertyNullability == null || propertyNullability[i], currentDepth, componentType.GetCascadeStyle(i)); + JoinType joinType = GetJoinType(associationType, componentType.GetFetchMode(i), subpath, lhsTable, lhsColumns, + propertyNullability == null || propertyNullability[i], currentDepth, + componentType.GetCascadeStyle(i)); AddAssociationToJoinTreeIfNecessary(associationType, aliasedLhsColumns, alias, subpath, currentDepth, joinType); } @@ -291,7 +288,7 @@ { string subpath = SubPath(path, propertyNames[i]); - WalkComponentTree((IAbstractComponentType)types[i], propertyNumber, begin, persister, alias, subpath, currentDepth); + WalkComponentTree((IAbstractComponentType) types[i], begin, alias, subpath, currentDepth, associationTypeSQLInfo); } begin += types[i].GetColumnSpan(Factory); } @@ -339,12 +336,9 @@ /// <summary> /// Extend the path by the given property name /// </summary> - private static string SubPath(string path, string property) + protected static string SubPath(string path, string property) { - if (path == null || path.Length == 0) - return property; - else - return StringHelper.Qualify(path, property); + return string.IsNullOrEmpty(path) ? property : StringHelper.Qualify(path, property); } /// <summary> Modified: trunk/nhibernate/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs 2009-10-18 13:19:08 UTC (rev 4773) +++ trunk/nhibernate/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs 2009-10-18 13:39:50 UTC (rev 4774) @@ -1852,6 +1852,18 @@ return result; } + public string[] ToIdentifierColumns(string name) + { + string alias = GenerateTableAlias(name, 0); + string[] cols = IdentifierColumnNames; + var result = new string[cols.Length]; + for (int j = 0; j < cols.Length; j++) + { + result[j] = StringHelper.Qualify(alias, cols[j]); + } + return result; + } + private int GetSubclassPropertyIndex(string propertyName) { return Array.IndexOf(subclassPropertyNameClosure, propertyName); Modified: trunk/nhibernate/src/NHibernate/Persister/Entity/IOuterJoinLoadable.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Persister/Entity/IOuterJoinLoadable.cs 2009-10-18 13:19:08 UTC (rev 4773) +++ trunk/nhibernate/src/NHibernate/Persister/Entity/IOuterJoinLoadable.cs 2009-10-18 13:39:50 UTC (rev 4774) @@ -108,5 +108,10 @@ /// Get the table name for the given property path /// </summary> string GetPropertyTableName(string propertyName); + + /// <summary> + /// Return the alised identifier column names + /// </summary> + string[] ToIdentifierColumns(string alias); } } \ No newline at end of file Modified: trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH1760/DomainClass.cs =================================================================== --- trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH1760/DomainClass.cs 2009-10-18 13:19:08 UTC (rev 4773) +++ trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH1760/DomainClass.cs 2009-10-18 13:39:50 UTC (rev 4774) @@ -4,22 +4,22 @@ { public class Customer { - public Int32 Id { get; set; } - public String Name { get; set; } + public virtual Int32 Id { get; set; } + public virtual String Name { get; set; } } public class TestClass { - public TestClassId Id { get; set; } - public String Value { get; set; } + public virtual TestClassId Id { get; set; } + public virtual String Value { get; set; } } public class TestClassId { - public Customer Customer { get; set; } - public Int32 SomeInt { get; set; } + public virtual Customer Customer { get; set; } + public virtual Int32 SomeInt { get; set; } - public bool Equals(TestClassId other) + public virtual bool Equals(TestClassId other) { if (ReferenceEquals(null, other)) { Modified: trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH1760/Mappings.hbm.xml =================================================================== --- trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH1760/Mappings.hbm.xml 2009-10-18 13:19:08 UTC (rev 4773) +++ trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH1760/Mappings.hbm.xml 2009-10-18 13:39:50 UTC (rev 4774) @@ -1,7 +1,6 @@ <?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="NHibernate.Test" - namespace="NHibernate.Test.NHSpecificTest.NH1760" - default-lazy="false"> + namespace="NHibernate.Test.NHSpecificTest.NH1760"> <class name="Customer"> <id name="Id"> Modified: trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH1760/SampleTest.cs =================================================================== --- trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH1760/SampleTest.cs 2009-10-18 13:19:08 UTC (rev 4773) +++ trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH1760/SampleTest.cs 2009-10-18 13:39:50 UTC (rev 4774) @@ -14,7 +14,7 @@ { var customer = new Customer {Name = "Alkampfer"}; session.Save(customer); - var testClass = new TestClass {Id = new TestClassId{Customer = customer, SomeInt = 42}, Value = "TESTVALUE"}; + var testClass = new TestClass { Id = new TestClassId { Customer = customer, SomeInt = 42 }, Value = "TESTVALUE" }; session.Save(testClass); tx.Commit(); } @@ -25,13 +25,13 @@ using (ISession session = OpenSession()) using (var tx = session.BeginTransaction()) { - session.CreateQuery("from TestClass").ExecuteUpdate(); - session.CreateQuery("from Customer").ExecuteUpdate(); + session.CreateQuery("delete from TestClass").ExecuteUpdate(); + session.CreateQuery("delete from Customer").ExecuteUpdate(); tx.Commit(); } } - [Test, Ignore("Not fixed yet.")] + [Test] public void CanUseCriteria() { FillDb(); @@ -40,7 +40,7 @@ using (ISession session = OpenSession()) { IList<TestClass> retvalue = - session.CreateQuery("Select tc from TestClass tc where tc.Id.Customer.Name = :name").SetString("name", "Alkampfer") + session.CreateQuery("Select tc from TestClass tc join tc.Id.Customer cu where cu.Name = :name").SetString("name", "Alkampfer") .List<TestClass>(); hqlCount = retvalue.Count; } @@ -53,10 +53,31 @@ IList<TestClass> retvalue = c.List<TestClass>(); criteriaCount = retvalue.Count; } - Assert.That(hqlCount == criteriaCount); - Assert.That(hqlCount, Is.EqualTo(1)); + Assert.That(criteriaCount, Is.EqualTo(1)); + Assert.That(criteriaCount, Is.EqualTo(hqlCount)); Cleanup(); } + + [Test] + public void TheJoinShouldBeOptional() + { + FillDb(); + int criteriaCount; + + using (ISession session = OpenSession()) + { + using (var ls = new SqlLogSpy()) + { + ICriteria c = session.CreateCriteria(typeof(TestClass)); + IList<TestClass> retvalue = c.List<TestClass>(); + Assert.That(ls.GetWholeLog(), Text.DoesNotContain("join")); + criteriaCount = retvalue.Count; + } + } + Assert.That(criteriaCount, Is.EqualTo(1)); + + Cleanup(); + } } } \ No newline at end of file This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |