#region copyright
//<copyright>
// Copyright(C) 2012 TrackerRealm Corporation
// This file is part of the open source project - Jazz. http://jazz.codeplex.com
//
// Jazz is open software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License (AGPL) as published by
// the Free Software Foundation, version 3 of the License.
//
// Jazz is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Affero General Public License (AGPL) for more details.
//
// You should have received a copy of the GNU General Public
// License along with Jazz. If not, see <http://www.gnu.org/licenses/>.
//
// REMOVAL OF THIS NOTICE IS VIOLATION OF THE COPYRIGHT.
//</copyright>
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Reflection;
using TrackerRealm.Jazz.Common;
using C = TrackerRealm.Jazz.Common.JazzConstants;
using System.Runtime.Serialization;
namespace TrackerRealm.Jazz.Client
{
/// <summary>
/// Client Nexus
/// </summary>
public class ClientNexusBase: IDisposable
{
private bool isClientNexusStarted = false;
#region Constructors
//public class Nexus<U,P> where U: jBaseUser where P: jProfileBase
/// <summary>
/// Constructor
/// </summary>
/// <param name="nexusConfig"></param>
internal protected ClientNexusBase(NexusConfigBase nexusConfig)
{
this.cache = new Cache(this);
this.Verbose = nexusConfig.Verbose;
this.IsVoidSafetyEnhanced = nexusConfig.VoidSafetyEnhanced;
this.RoleCheckCollection = nexusConfig.RoleCheckCollection;
List<jObject> emptyList = new List<jObject>(16);
List<jUserBase> userList = new List<jUserBase>(8);
List<jProfileBase> profileList = new List<jProfileBase>(8);
List<jProfileBase> emptyProfiles = new List<jProfileBase>(8);
List<jWorkspaceBase> wsList = new List<jWorkspaceBase>(8);
List<jUserBase> emptyUsers = new List<jUserBase>(8);
List<jRoleBase> roleList = new List<jRoleBase>(8);
List<jRoleBase> emptyRoles = new List<jRoleBase>(8);
jWorkspaceBase publicWs = null;
List<jWorkspaceBase> emptyWsList = new List<jWorkspaceBase>(8);
//
// parse the 'BindUnits' looking for specific types of 'jObject' classes
//
jObject[] jazzObjs = nexusConfig.BindUnits.Where(u => u is jObject).Cast<jObject>().ToArray();
nexusConfig.BindUnits.Where(u => u is jObject).Cast<jObject>().Foreach(j =>
{
// need error message if not clientNexus null
j.Flags = j.Flags.Reset(JazzFlagsType.Bound);
if (j.IsEmpty) emptyList.Add(j);
if (j is jUserBase)
{
userList.Add((jUserBase)j);
// there may be more than 1 empty jUser
if (j.IsEmpty) emptyUsers.Add((jUserBase)j);
}
else if (j is jWorkspaceBase)
{
wsList.Add((jWorkspaceBase)j);
if (j.Name == C.WORKSPACE_PUBLIC)
publicWs = (jWorkspaceBase)j;
if (j.IsEmpty) emptyWsList.Add((jWorkspaceBase)j); // there may be more than 1 empty jWorkspace
}
else if (j is jRoleBase)
{
jRoleBase role = (jRoleBase)j;
roleList.Add(role);
if (j.IsEmpty) emptyRoles.Add(role); // there may be more than 1 empty jRole
}
else if (j is jProfileBase)
{
jProfileBase profile = (jProfileBase)j;
profileList.Add(profile);
if (j.IsEmpty) emptyProfiles.Add(profile); // there may be more than 1 empty jProfile
}
});
//
// Bind the public workspace
//
emptyWsList.ForEach(ws => Bind(ws.GetType(), ws));
if (publicWs != null)
this._PublicWorkspace = publicWs;
if (this._PublicWorkspace == null)
{
Tuple<jWorkspaceBase, jWorkspaceBase> wsTuple = nexusConfig._PublicWorkspace;
this._PublicWorkspace = wsTuple.Item1;
this.Bind(wsTuple.Item2.GetType(), wsTuple.Item2);
if (wsTuple.Item2 != null)
this.Bind(wsTuple.Item2); // empty Workspace
}
this.Bind(this._PublicWorkspace);
//
// Assign the Login User - choose the from either the "LoginProfile" or "BindUnits"
//
#region Assign the Login User
// Basic start scenerio:
// 1. On first use 'BindUnits' will be empty, therefore use the 'LoginProfile',
// which will contain a freshly created 'Guest'.
// 2. Save the cache.
// 3. Restore the saved cached via 'BindUnits'.
// This time the 'Guest' from 'BindUnits' will be used instead of the freshly created 'LoginProfile' - 'Guest'.
string loginName = nexusConfig._LoginProfile.Name;
if (userList.FirstOrDefault(u => u.Name == loginName) == null &&
(profileList.FirstOrDefault(u => u.Name == loginName) == null))
{
//use login property because no user or profile was found in 'BindUnits' with the same name as the 'LoginProfile' property.
this._LoginRoles = nexusConfig._LoginProfile._Roles;
this._LoginProfile = nexusConfig._LoginProfile;
}
else
{
// use the login profile found in the "BindUnits" that has the same name as the property 'LoginProfile'
jUserBase userCandidate = userList.Where(u => u.Name == loginName).FirstOrDefault();
if (userCandidate != null)
{
if (userCandidate._Profile == null)
throw new InvalidOperationException("The login user has a null profile");
this._LoginProfile = userCandidate._Profile;
}
else
{
this._LoginProfile = profileList.First(u => u.Name == loginName);
}
this._LoginRoles = this._LoginProfile._Roles;
}
if (this._LoginProfile._User == null)
throw new InvalidOperationException("The login profile has a null user");
#endregion
// Bind Roles
emptyRoles.ForEach(r => Bind(r.GetType(), r));
roleList.ForEach(r => Bind(r));
this.BindIEnumerable(this._LoginRoles);
//
// bind Login User
emptyUsers.Foreach(u => Bind(u.GetType(), u));
this.Bind(this._LoginProfile._User);
//
// Bind Login profile
emptyProfiles.ForEach(p => Bind(p.GetType(), p));
//this.Bind(typeof(jProfileBase), ((jUserBase)this.LoginProfile._User.Class.EmptyInstance)._Profile);
this.Bind(this._LoginProfile.GetType(), ((jUserBase)this._LoginProfile._User.Class.EmptyInstance)._Profile);
this.Bind(this._LoginProfile);
//
// bind storage objects.
//
if (emptyList.FirstOrDefault(e => e._ACL.Count() > 0) != null)
throw new InvalidOperationException("Empty objects must have an empty ACL.");
if (emptyList.FirstOrDefault(e => e.Workspace != this._PublicWorkspace) != null)
throw new InvalidOperationException("Empty objects must be in the 'Public' workspace.");
emptyList.ForEach(j => this.Bind(j.GetType(), j));
foreach (IjStorageUnit u in nexusConfig.BindUnits)
{
if (!(u is jObject))
{
throw new ArgumentException("Can only accept 'jObject's in the 'StorageObjects' property.");
}
this.Bind((jObject)u);
}
//// bind the login roles if they have not been bound
//foreach (jRoleBase r in this._LoginRoles)
//{
// if (!r.Flags.HasFlag(JazzFlagsType.Bound))
// this.Bind(r);
//}
this.SetObjectAccessibility();
isClientNexusStarted = true;
}
#endregion
//
// Instance properties
//
#region IsVoidSafetyEnhanced
/// <summary>
/// <para>*** Not fully Implemented ***</para>
/// When true empty Jazz objects are used instead of null to reduce
/// the chances of getting 'NullReferenceException', 'ArgumentNullException', etc.
/// </summary>
public bool IsVoidSafetyEnhanced
{
get;
private set;
}
#endregion
#region Verbose
/// <summary>
///
/// </summary>
public VerboseType Verbose
{
get;
private set;
}
#endregion
#region RoleCheckCollection
/// <summary>
///
/// </summary>
public IEnumerable<string> RoleCheckCollection
{
get;
private set;
}
#endregion
#region LoginRoles
/// <summary>
///
/// </summary>
internal protected IEnumerable<jRoleBase> _LoginRoles
{
get;
private set;
}
#endregion
#region LoginProfile
/// <summary>
///
/// </summary>
internal protected jProfileBase _LoginProfile
{
get;
private set;
}
#endregion
#region Cache
private Cache cache;
/// <summary>
/// A cache containing all objects that have been bound (see 'Bind' method)
/// to the client nexus.
/// </summary>
public Cache Cache
{
get { return this.cache; }
}
#endregion
#region PublicWorkspace
/// <summary>
///
/// </summary>
internal protected jWorkspaceBase _PublicWorkspace
{
get;
private set;
}
#endregion
//
// Instance Methods
#region Bind
/// <summary>
/// Binds a workflows to this client nexus.
/// <para>A workflow needs to be bound when first created or restored.</para>
/// <para>Workflow may only be bound to a single nexus only once and may not be rebound to any other client nexus.</para>
/// </summary>
/// <param name="jazzObjects"></param>
public void BindIEnumerable(IEnumerable<jObject> jazzObjects)
{
foreach (jObject wf in jazzObjects)
{
this.Bind(wf);
}
}
/// <summary>
/// Binds a workflows to this client nexus.
/// <para>A workflow needs to be bound when first created or restored.</para>
/// <para>Workflow may only be bound to a single nexus only once and may not be rebound to any other client nexus.</para>
/// </summary>
/// <param name="jazzObjects"></param>
public void Bind(params jObject[] jazzObjects)
{
this.BindIEnumerable(jazzObjects);
}
/// <summary>
/// Binds a workflow to this client nexus.
/// <para>A workflow needs to be bound when first created or restored.</para>
/// <para>Workflow may only be bound to a single nexus only once and may not be rebound to any other client nexus.</para>
/// </summary>
/// <param name="jazzObj"></param>
public void Bind(jObject jazzObj)
{
if (jazzObj.IsBound)
{
if (jazzObj.ClientNexus == this) return;
throw new ArgumentException("Bind operation - object is already bound to another client nexus.", "jazzObj");
}
// check if class is already bound
jClass jC;
if (!this.Cache.JClasses.TryGet(jazzObj.GetType(), out jC))
{
this.cache.JClasses.AddPlaceHolder(jazzObj.GetType(), jClass.Placeholder);
jC = Bind(jazzObj.GetType());
}
if (jC == jClass.Placeholder) return; //
jClass.jObject_JazzClassField.SetValue(jazzObj, jC);
if (jazzObj.Workspace == null)
jazzObj.Workspace = this._PublicWorkspace;
jClass.jObject_ClientNexusField.SetValue(jazzObj, this);
if (!jazzObj.Flags.HasFlag(JazzFlagsType.Initialized))
{
// Set Jazz.ID
jClass.jObject_IdField.SetValue(jazzObj, jazzObj.OnAssignID());
// set initial values
jC.InitializeJObjectFields(jazzObj);
jazzObj.OnInitialBind();
}
jazzObj.OnBind();
if (jazzObj.IsEmpty && this._PublicWorkspace!= null && jazzObj.Workspace != this._PublicWorkspace)
throw new InvalidOperationException(string.Format("Empty objects must be in the 'Public' workspace. {0}", jazzObj.FullName));
jazzObj.Flags= jazzObj.Flags.Set(JazzFlagsType.Initialized |JazzFlagsType.Bound);
if (!this.Cache.Contains(jazzObj.ID))
this.Cache.Add(jazzObj);
jazzObj.IsAccessibilitySet = false;
jazzObj.SetAccessibility(this);
}
/// <summary>
/// Create a jClass and a 'Empty' jObject for the parameter '<paramref name="jazzObjectType"/>'.
/// </summary>
/// <param name="jazzObjectType"></param>
/// <returns></returns>
public jClass Bind(Type jazzObjectType)
{
jClass returnClass = null;
if (this.cache.JClasses.TryGet(jazzObjectType, out returnClass) && returnClass != jClass.Placeholder)
return returnClass;
returnClass = null;
Type t = jazzObjectType;
jObject emptyWf = null;
MethodInfo[] ms = t.GetMethods();
MethodInfo m = t.GetMethod("CreateEmpty", BindingFlags.Static | BindingFlags.Public, null, new Type[0], new ParameterModifier[0]);
if (m != null)
{
emptyWf = (jObject)m.Invoke(null, new object[0]);
}
else
{
m = t.GetMethod("CreateEmpty", BindingFlags.Static | BindingFlags.Public, null, new Type[] { this.GetType()}, new ParameterModifier[0]);
if (m != null)
{
emptyWf = (jObject)m.Invoke(null, new object[] { this });
if (emptyWf.Flags.HasFlag(JazzFlagsType.Bound))
throw new ApplicationException("Do not 'Bind' in the 'CreateEmpty' method.");
}
}
ConstructorInfo ctor = null;
object[] ctorParams = null;
if (emptyWf == null)
{
emptyWf = (jObject)FormatterServices.GetUninitializedObject(t);
ctor = t.GetConstructor(new Type[0]);
if (ctor != null)
ctorParams = new object[0];
if (ctor == null)
{
ctor = t.GetConstructor(new Type[] { typeof(string) });
ctorParams = new object[] { C.EMPTY };
}
if (ctor == null)
{
ctor = t.GetConstructor(new Type[] { this.GetType() });
ctorParams = new object[] { this };
}
if (ctor == null)
{
ctor = t.GetConstructor(new Type[] { this.GetType(), typeof(string) });
ctorParams = new object[] { this, C.EMPTY };
}
if (ctor == null)
{
if (this.IsVoidSafetyEnhanced)
throw new Exception(string.Format(
"Unable to Create empty object for the class '{0}'. To eliminate this error " +
"a) Add a 'CreateEmpty' method, or " +
"b) Create an Empty object separately, or " +
"c) Set EnhancedVoidSafety to false.",
t.FullName));
}
}
jClass jc = Bind(t, emptyWf, ctor, ctorParams);
if (emptyWf != null)
Bind(emptyWf);
jc.CrossCheck(this);
if (returnClass == null)
returnClass = jc;
return returnClass;
}
/// <summary>
/// Creates a 'jClass' and bind the empty jObject.
/// </summary>
/// <param name="type"></param>
/// <param name="empty">This jObject will be made into the empty object for the this 'type'.</param>
/// <returns></returns>
public jClass Bind(Type type, jObject empty)
{
return this.Bind(type, empty, null, null);
}
/// <summary>
///
/// </summary>
/// <param name="type"></param>
/// <param name="empty"></param>
/// <param name="ctor"></param>
/// <param name="ctorParams"></param>
/// <returns></returns>
private jClass Bind(Type type, jObject empty, ConstructorInfo ctor, object[] ctorParams)
{
if (typeof(jObject).IsSubclassOf(type))
throw new ArgumentException(string.Format("The type, '{0}', is not a sub/child class of 'jObject'. The parameter 'type' needs to inherit from 'jObject'.", type.FullName));
if (type != empty.GetType())
throw new ArgumentException(string.Format("The argument 'empty' was expected to be of type '{0}', but was of type '{1}'.", type.FullName, empty.GetType().FullName));
jClass jC;
if (Cache.JClasses.TryGet(type, out jC))
{
if (jC != jClass.Placeholder)
{
if (!Object.ReferenceEquals(jC.EmptyInstance, empty))
throw new ArgumentException(string.Format("The type, '{0}', has already been bound with a empty object '{1}'.", type.FullName, jC.EmptyInstance.FullName));
return jC;
}
}
empty.IsEmpty = true;
jC = new jClass(type, this);
jC.EmptyInstance = empty;
if (!this.Cache.Contains(jC.guid))
{
this.Cache.Add(jC);
}
if (ctor != null)
{
//jClass.jObject_JazzClassField.SetValue(empty, jC);
//if (empty.Workspace == null)
// empty.Workspace = this._PublicWorkspace;
//jClass.jObject_ClientNexusField.SetValue(empty, this);
ctor.Invoke(empty, ctorParams);
}
//
// Bind empty if not bound
//
if (!empty.Flags.HasFlag(JazzFlagsType.Bound))
this.Bind(empty);
jC.CrossCheck(this);
#region set IsReadOnly
empty.Flags = empty.Flags.Set(JazzFlagsType.ReadOnly);
#endregion
return jC;
}
#endregion
#region ChangeLoginProfile
/// <summary>
/// Changes the login user. This may result is different behaviour of workflows.
/// <para>The login roles changing may cause the accessibility of properties and methods
/// and methods to change.</para>
/// <para>When the login user is changed then the ACL (Access Control List) of
/// workflows may change the accessibility of workflows to read/write, read only
/// or inaccessible.</para>
/// </summary>
/// <param name="loginUser"></param>
public LoggedinUser ChangeLoginProfile(jProfileBase loginUser)
{
this._LoginRoles = loginUser._Roles;
this._LoginProfile = loginUser._Profile;
this.BindIEnumerable(this._LoginRoles);
this.Bind(loginUser._User);
this.BindIEnumerable(loginUser._User._Profiles);
this.SetObjectAccessibility();
return new LoggedinUser(loginUser, (ClientNexusBase)this);
}
#endregion
#region SetObjectAccessibility
/// <summary>
///
/// </summary>
private void SetObjectAccessibility()
{
this.cache.OfType<jObject>().Foreach(j => j.IsAccessibilitySet = false);
this.cache.OfType<jObject>().Foreach(j => j.SetAccessibility(this));
}
#endregion
#region IDisposable Members
/// <summary>
///
/// </summary>
public void Dispose()
{
//remove connection with nexus
}
#endregion
#region Experimental Code
/// <summary>
/// This is an unsupported method. In future versions it may be removed or cease to operate.
/// </summary>
/// <param name="jazzObj"></param>
protected void UnBind(jObject jazzObj)
{
if (!jazzObj.Flags.HasFlag(JazzFlagsType.Initialized))
throw new ArgumentException("Jazz object needs to be properly initialized. Bind to nexus first.");
if (!jazzObj.Flags.HasFlag(JazzFlagsType.Bound))
throw new ArgumentException("Jazz object is not bound.");
jazzObj.Flags = jazzObj.Flags.Reset(JazzFlagsType.Bound);
}
/// <summary>
/// This is an unsupported method. In future versions it may be removed or cease to operate.
/// </summary>
/// <param name="jazzObj"></param>
protected void ReBind(jObject jazzObj)
{
if (!jazzObj.Flags.HasFlag(JazzFlagsType.Initialized))
throw new ArgumentException("Jazz object needs to be properly initialized. Bind to nexus first.");
jazzObj.Flags = jazzObj.Flags.Set(JazzFlagsType.Bound);
}
#endregion
}
}