diff --git a/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Control/ServiceController.cs b/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Control/ServiceController.cs index a7092c28..09112b2b 100644 --- a/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Control/ServiceController.cs +++ b/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Control/ServiceController.cs @@ -1,4 +1,4 @@ -// +// // ServiceController.cs // // Author: @@ -41,6 +41,7 @@ public class ServiceController : IXmlDeserializer, IXmlDeserializer { + Service service; DataServer scpd_server; ControlServer control_server; ControlClient control_client; @@ -59,45 +60,55 @@ protected internal ServiceController (Deserializer deserializer, Service service } else if (service.EventUrl == null) { throw new ArgumentException ("The service has no EventUrl.", "service"); } - + + this.service = service; actions = new CollectionMap (); state_variables = new CollectionMap (); control_client = new ControlClient ( service.Type.ToString (), service.ControlUrl, deserializer.XmlDeserializer); event_client = new EventClient (state_variables, service.EventUrl); } - + public ServiceController (IEnumerable actions, IEnumerable stateVariables) { this.actions = Helper.MakeReadOnlyCopy (actions); this.state_variables = Helper.MakeReadOnlyCopy (stateVariables); SpecVersion = new SpecVersion (1, 1); } - + + public Service Service + { + get { return service; } + } + [XmlAttribute ("configId")] protected internal virtual string ConfigurationId { get; set; } - + [XmlElement ("specVersion")] public virtual SpecVersion SpecVersion { get; protected set; } - + [XmlArray ("actionList")] - protected virtual ICollection ActionCollection { + protected virtual ICollection ActionCollection + { get { return actions; } } - public IMap Actions { + public IMap Actions + { get { return actions; } } - + [XmlArray ("serviceStateTable")] - protected virtual ICollection StateVariableCollection { + protected virtual ICollection StateVariableCollection + { get { return state_variables; } } - public IMap StateVariables { + public IMap StateVariables + { get { return state_variables; } } - + protected internal virtual void Initialize (XmlSerializer serializer, Service service) { if (serializer == null) { @@ -111,38 +122,38 @@ protected internal virtual void Initialize (XmlSerializer serializer, Service se } else if (service.EventUrl == null) { throw new ArgumentException ("The service has no EventUrl.", "service"); } - + scpd_server = new DataServer (serializer.GetBytes (this), @"text/xml; charset=""utf-8""", service.ScpdUrl); control_server = new ControlServer (actions, service.Type.ToString (), service.ControlUrl, serializer); event_server = new EventServer (state_variables.Values, service.EventUrl); - + foreach (var state_variable in state_variables.Values) { state_variable.Initialize (this); } } - + protected internal virtual void Start () { if (scpd_server == null) { throw new InvalidOperationException ("The service controller has not been initialized."); } - + scpd_server.Start (); control_server.Start (); event_server.Start (); } - + protected internal virtual void Stop () { if (scpd_server == null) { throw new InvalidOperationException ("The service controller has not been initialized."); } - + scpd_server.Stop (); control_server.Stop (); event_server.Stop (); } - + protected internal virtual IMap Invoke (ServiceAction action, IDictionary arguments, int retryAttempts) @@ -172,64 +183,64 @@ internal void RefEvents () { event_client.Ref (); } - + internal void UnrefEvents () { event_client.Unref (); } - + internal void UpdateStateVariable (StateVariable stateVariable) { event_server.QueueUpdate (stateVariable); } - + ServiceAction IXmlDeserializer.Deserialize (XmlDeserializationContext context) { return DeserializeAction (context); } - + protected virtual ServiceAction DeserializeAction (XmlDeserializationContext context) { return Deserializer != null ? Deserializer.DeserializeAction (this, context) : null; } - + StateVariable IXmlDeserializer.Deserialize (XmlDeserializationContext context) { return DeserializeStateVariable (context); } - + protected virtual StateVariable DeserializeStateVariable (XmlDeserializationContext context) { return Deserializer != null ? Deserializer.DeserializeStateVariable (this, context) : null; } - + void IXmlDeserializable.Deserialize (XmlDeserializationContext context) { Deserialize (context); actions.MakeReadOnly (); state_variables.MakeReadOnly (); } - + void IXmlDeserializable.DeserializeAttribute (XmlDeserializationContext context) { DeserializeAttribute (context); } - + void IXmlDeserializable.DeserializeElement (XmlDeserializationContext context) { DeserializeElement (context); } - + protected override void DeserializeElement (XmlDeserializationContext context) { AutoDeserializeElement (this, context); } - + protected override void Serialize (Mono.Upnp.Xml.XmlSerializationContext context) { AutoSerialize (this, context); } - + protected override void SerializeMembers (XmlSerializationContext context) { AutoSerializeMembers (this, context); diff --git a/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Control/StateVariable.cs b/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Control/StateVariable.cs index dedf9e6d..e1a1cabc 100644 --- a/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Control/StateVariable.cs +++ b/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Control/StateVariable.cs @@ -1,4 +1,4 @@ -// +// // StateVariable.cs // // Author: @@ -40,9 +40,10 @@ public class StateVariable : XmlAutomatable, IMappable readonly LinkedList>> value_changed = new LinkedList>> (); ServiceController controller; + StateVariableEventer eventer; IList allowed_values; string value; - + protected StateVariable () { } @@ -55,7 +56,7 @@ protected internal StateVariable (ServiceController serviceController) this.controller = serviceController; } - + public StateVariable (string name, string dataType) { if (name == null) { @@ -63,11 +64,11 @@ public StateVariable (string name, string dataType) } else if (dataType == null) { throw new ArgumentNullException ("dataType"); } - + Name = name; DataType = dataType; } - + public StateVariable (string name, string dataType, StateVariableOptions options) : this (name, dataType) { @@ -78,23 +79,23 @@ public StateVariable (string name, string dataType, StateVariableOptions options } } } - + public StateVariable (string name, IEnumerable allowedValues) : this (name, allowedValues, null) { } - + public StateVariable (string name, IEnumerable allowedValues, StateVariableOptions options) : this (name, "string", options) { allowed_values = Helper.MakeReadOnlyCopy (allowedValues); } - + public StateVariable (string name, string dataType, AllowedValueRange allowedValueRange) : this (name, dataType, allowedValueRange, null) { } - + public StateVariable (string name, string dataType, AllowedValueRange allowedValueRange, @@ -103,7 +104,12 @@ public StateVariable (string name, { AllowedValueRange = allowedValueRange; } - + + public ServiceController Controller + { + get { return controller; } + } + [XmlElement ("name")] public virtual string Name { get; protected set; } @@ -111,46 +117,53 @@ public StateVariable (string name, public virtual string DataType { get; protected set; } [XmlAttribute ("sendEvents", OmitIfNull = true)] - protected virtual string SendsEventsString { + protected virtual string SendsEventsString + { get { return SendsEvents ? "yes" : null; } set { SendsEvents = value == "yes"; } } - + public bool SendsEvents { get; protected set; } - + public virtual bool IsMulticast { get; protected set; } [XmlAttribute ("multicast", OmitIfNull = true)] - protected virtual string IsMulticastString { + protected virtual string IsMulticastString + { get { return IsMulticast ? "yes" : null; } set { IsMulticast = value == "yes"; } } - + [XmlElement ("defaultValue", OmitIfNull = true)] public string DefaultValue { get; protected set; } [XmlArray ("allowedValueList", OmitIfNull = true)] [XmlArrayItem ("allowedValue")] - protected virtual ICollection AllowedValueCollection { + protected virtual ICollection AllowedValueCollection + { get { return allowed_values; } } - - public IEnumerable AllowedValues { + + public IEnumerable AllowedValues + { get { return allowed_values; } } [XmlElement ("allowedValueRange", OmitIfNull = true)] public virtual AllowedValueRange AllowedValueRange { get; protected set; } - - public event EventHandler> ValueChanged { - add { + + public event EventHandler> ValueChanged + { + add + { if (value == null) { return; } value_changed.AddLast (value); controller.RefEvents (); } - remove { + remove + { if (value == null || value_changed.Count == 0) { return; } @@ -165,52 +178,64 @@ public event EventHandler> ValueChanged { } while (node != null); } } - - protected internal string Value { + + protected internal string Value + { get { return value; } - set { + set + { this.value = value; foreach (var handler in value_changed) { handler (this, new StateVariableChangedArgs (value)); } } } - + + internal StateVariableEventer Eventer + { + get { return eventer; } + } + internal void SetEventer (StateVariableEventer eventer, bool isMulticast) { + if (this.eventer != null) + throw new InvalidOperationException ("Eventer already set."); + + this.eventer = eventer; eventer.StateVariableUpdated += OnStateVariableUpdated; SendsEvents = true; IsMulticast = isMulticast; } - + protected internal virtual void Initialize (ServiceController serviceController) { if (serviceController == null) { throw new ArgumentNullException ("serviceController"); } - + this.controller = serviceController; } - + protected virtual void OnStateVariableUpdated (object sender, StateVariableChangedArgs args) { - Value = args.NewValue; - if (controller != null) { - controller.UpdateStateVariable (this); + if (Value != args.NewValue) { + Value = args.NewValue; + if (controller != null) + controller.UpdateStateVariable (this); } } - + protected override void DeserializeAttribute (XmlDeserializationContext context) { AutoDeserializeAttribute (this, context); } - + protected override void DeserializeElement (XmlDeserializationContext context) { if (context == null) { throw new ArgumentNullException ("context"); } - + if (context.Reader.Name == "allowedValueList") { allowed_values = new List (); } @@ -226,7 +251,7 @@ protected override void SerializeMembers (Mono.Upnp.Xml.XmlSerializationContext { AutoSerializeMembers (this, context); } - + string IMappable.Map () { return Name; diff --git a/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Control/UpnpStateVariableAttribute.cs b/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Control/UpnpStateVariableAttribute.cs index d0ce1eb4..1388d0af 100644 --- a/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Control/UpnpStateVariableAttribute.cs +++ b/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Control/UpnpStateVariableAttribute.cs @@ -1,4 +1,4 @@ -// +// // UpnpStateVariableAttribute.cs // // Author: @@ -28,30 +28,30 @@ namespace Mono.Upnp.Control { - [AttributeUsage (AttributeTargets.Event)] + [AttributeUsage (AttributeTargets.Event | AttributeTargets.Property)] public class UpnpStateVariableAttribute : Attribute { public UpnpStateVariableAttribute () { } - + public UpnpStateVariableAttribute (string name) : this (name, null) { } - + public UpnpStateVariableAttribute (string name, string dataType) { Name = name; DataType = dataType; } - + public string Name { get; set; } - + public string DataType { get; set; } - + public bool IsMulticast { get; set; } - + public string OmitUnless { get; set; } } } diff --git a/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Internal/ServiceControllerBuilder.cs b/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Internal/ServiceControllerBuilder.cs index 69bbbc17..753a3003 100644 --- a/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Internal/ServiceControllerBuilder.cs +++ b/src/Mono.Upnp/Mono.Upnp/Mono.Upnp.Internal/ServiceControllerBuilder.cs @@ -1,422 +1,518 @@ -// -// ServiceControllerBuilder.cs -// -// Author: -// Scott Thomas -// -// Copyright (c) 2009 Scott Thomas -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Reflection; -using System.Text; - -using Mono.Upnp.Control; - -namespace Mono.Upnp.Internal -{ - // FIXME I am not happy with the state of this class. Let's clean it up later. - static class ServiceControllerBuilder - { - class Eventer : StateVariableEventer - { - public static readonly MethodInfo HandlerMethod = typeof (Eventer).GetMethod ("Handler"); - - public void Handler (object sender, StateVariableChangedArgs args) - { - OnStateVariableUpdated (args.NewValue.ToString ()); - } - } - - class ArgumentInfo - { - public Argument Argument; - public ParameterInfo ParameterInfo; - } - - class StateVariableInfo - { - public StateVariable StateVariable; - public Type Type; - } - - public static ServiceController Build (T service) - { - var state_variables = new Dictionary (); - return new ServiceController ( - BuildActions (service, state_variables), BuildStateVariables (service, state_variables)); - } - - static IEnumerable BuildActions (T service, - Dictionary stateVariables) - { - foreach (var method in typeof (T).GetMethods (BindingFlags.Public | BindingFlags.Instance)) { - var action = BuildAction (method, service, stateVariables); - if (action != null) { - yield return action; - } - } - } - - readonly static object[] empty_args = new object[0]; - - static bool Omit (Type type, object service, string unless) - { - if (unless == null) { - return false; - } - var property = type.GetProperty (unless); - if (property == null || property.PropertyType != typeof (bool) || !property.CanRead) { - throw new UpnpServiceDefinitionException ( - "The OmitUnless property must reference a readable bool property in the same type."); - } - return property.GetValue (service, empty_args).Equals (false); - } - - static ServiceAction BuildAction (MethodInfo method, - object service, - Dictionary stateVariables) - { - var attributes = method.GetCustomAttributes (typeof (UpnpActionAttribute), false); - if (attributes.Length != 0) { - var attribute = (UpnpActionAttribute)attributes[0]; - if (Omit (method.DeclaringType, service, attribute.OmitUnless)) { - return null; - } - var name = string.IsNullOrEmpty (attribute.Name) ? method.Name : attribute.Name; - var parameters = method.GetParameters (); - var arguments = new ArgumentInfo[parameters.Length]; - for (var i = 0; i < parameters.Length; i++) { - arguments[i] = BuildArgumentInfo (parameters[i], name, stateVariables); - } - var return_argument = BuildArgumentInfo (method.ReturnParameter, name, stateVariables, true); - return new ServiceAction (name, Combine (arguments, return_argument), args => { - Trace (name, args.Values); - - var argument_array = new object[arguments.Length]; - for (var i = 0; i < arguments.Length; i++) { - var argument = arguments[i]; - if (argument.Argument.Direction == ArgumentDirection.Out) { - continue; - } - - string value; - if (args.TryGetValue (argument.Argument.Name, out value)) { - var parameter_type = argument.ParameterInfo.ParameterType; - if (parameter_type.IsEnum) { - // TODO handle attributes - foreach (var enum_value in Enum.GetValues (parameter_type)) { - if (Enum.GetName (parameter_type, enum_value) == value) { - argument_array[i] = enum_value; - break; - } - } - } else { - argument_array[i] = Convert.ChangeType (value, parameter_type); - } - } else { - // TODO throw - } - } - - object result; - try { - result = method.Invoke (service, argument_array); - } catch (TargetInvocationException e) { - if (e.InnerException is UpnpControlException) { - throw e.InnerException; - } else { - throw new UpnpControlException ( - UpnpError.Unknown (), "Unexpected exception.", e.InnerException); - } - } - - var out_arguments = new Dictionary (); - for (var i = 0; i < arguments.Length; i++) { - if (arguments[i].Argument.Direction == ArgumentDirection.In) { - continue; - } - var value = argument_array[i]; - out_arguments.Add (arguments[i].Argument.Name, value != null ? value.ToString () : ""); - } - if (return_argument != null) { - out_arguments.Add (return_argument.Argument.Name, result.ToString ()); - } - - Trace (name, out_arguments); - - return out_arguments; - }); - } else { - return null; - } - } - - [Conditional ("TRACE")] - static void Trace (string name, IEnumerable values) - { - var builder = new StringBuilder (); - builder.Append (name); - builder.Append (" ("); - foreach (var value in values) { - if (name == null) { - builder.Append (", "); - } else { - name = null; - } - builder.Append (value); - } - builder.Append (')'); - Log.Trace (builder.ToString ()); - } - - [Conditional ("TRACE")] - static void Trace (string name, IDictionary results) - { - var builder = new StringBuilder (); - builder.Append (name); - builder.Append (": "); - foreach (var result in results) { - builder.Append ("\n\t"); - builder.Append (result.Key); - builder.Append (": "); - builder.Append (result.Value); - } - Log.Trace (builder.ToString ()); - } - - static ArgumentInfo BuildArgumentInfo (ParameterInfo parameterInfo, - string actionName, - Dictionary stateVariables) - { - return BuildArgumentInfo(parameterInfo, actionName, stateVariables, false); - } - - static ArgumentInfo BuildArgumentInfo (ParameterInfo parameterInfo, - string actionName, - Dictionary stateVariables, - bool isReturnValue) - { - if (parameterInfo.ParameterType == typeof (void)) { - return null; - } - - var attributes = parameterInfo.GetCustomAttributes (typeof (UpnpArgumentAttribute), false); - var attribute = attributes.Length != 0 - ? (UpnpArgumentAttribute)attributes[0] - : null; - var name = attribute != null && !string.IsNullOrEmpty (attribute.Name) - ? attribute.Name - : (string.IsNullOrEmpty (parameterInfo.Name) - ? "result" - : parameterInfo.Name); - var related_state_variable = BuildRelatedStateVariable ( - parameterInfo, actionName, name, stateVariables); - var direction = parameterInfo.IsRetval || parameterInfo.ParameterType.IsByRef || isReturnValue - ? ArgumentDirection.Out - : ArgumentDirection.In; - return new ArgumentInfo { - ParameterInfo = parameterInfo, - Argument = new Argument ( - name, related_state_variable.StateVariable.Name, direction, parameterInfo.IsRetval || isReturnValue) - }; - } - - static StateVariableInfo BuildRelatedStateVariable (ParameterInfo parameterInfo, - string actionName, - string argumentName, - Dictionary stateVariables) - { - var attributes = parameterInfo.GetCustomAttributes (typeof (UpnpRelatedStateVariableAttribute), false); - var attribute = attributes.Length != 0 - ? (UpnpRelatedStateVariableAttribute)attributes[0] - : null; - var name = attribute != null && !string.IsNullOrEmpty (attribute.Name) - ? attribute.Name - : CreateRelatedStateVariableName (argumentName); - var data_type = attribute != null && !string.IsNullOrEmpty (attribute.DataType) - ? attribute.DataType - : GetDataType (parameterInfo.ParameterType); - var default_value = attribute != null && !string.IsNullOrEmpty (attribute.DefaultValue) - ? attribute.DefaultValue - : null; - var allowed_values = parameterInfo.ParameterType.IsEnum - ? BuildAllowedValues (parameterInfo.ParameterType) - : null; - var allowed_value_range = attribute != null && !string.IsNullOrEmpty (attribute.MinimumValue) - ? new AllowedValueRange (attribute.MinimumValue, attribute.MaximumValue, attribute.StepValue) - : null; - - StateVariableInfo state_variable_info; - if (stateVariables.TryGetValue (name, out state_variable_info)) { - var state_variable = state_variable_info.StateVariable; - if (state_variable.DataType != data_type || - state_variable.DefaultValue != default_value || - state_variable.AllowedValueRange != allowed_value_range || - ((state_variable.AllowedValues != null || allowed_values != null) && - state_variable_info.Type != parameterInfo.ParameterType)) - { - if (attribute == null || string.IsNullOrEmpty (attribute.Name)) { - name = CreateRelatedStateVariableName (actionName, parameterInfo.Name); - } - } else { - return state_variable_info; - } - } - - if (allowed_values != null) { - state_variable_info = new StateVariableInfo { - StateVariable = new StateVariable (name, allowed_values, - new StateVariableOptions { DefaultValue = default_value }), - Type = parameterInfo.ParameterType - }; - } else if (allowed_value_range != null) { - state_variable_info = new StateVariableInfo { - StateVariable = new StateVariable (name, data_type, allowed_value_range, - new StateVariableOptions { DefaultValue = default_value }) - }; - } else { - state_variable_info = new StateVariableInfo { - StateVariable = new StateVariable (name, data_type, - new StateVariableOptions { DefaultValue = default_value }) - }; - } - stateVariables[name] = state_variable_info; - return state_variable_info; - } - - static string CreateRelatedStateVariableName (string name) - { - return string.Format ("A_ARG_{0}", name); - } - - static string CreateRelatedStateVariableName (string actionName, string argumentName) - { - return string.Format ("A_ARG_{0}_{1}", actionName, argumentName); - } - - static string GetDataType (Type type) - { - if (type.IsByRef) { - type = type.GetElementType (); - } - if (type.IsEnum || type == typeof (string)) return "string"; - if (type == typeof (int)) return "i4"; - if (type == typeof (byte)) return "ui1"; - if (type == typeof (ushort)) return "ui2"; - if (type == typeof (uint)) return "ui4"; - if (type == typeof (sbyte)) return "i1"; - if (type == typeof (short)) return "i2"; - if (type == typeof (long)) return "int"; // TODO Is this right? The UPnP docs are vague - if (type == typeof (float)) return "r4"; - if (type == typeof (double)) return "r8"; - if (type == typeof (char)) return "char"; - if (type == typeof (DateTime)) return "date"; // TODO what about "time"? - if (type == typeof (bool)) return "boolean"; - if (type == typeof (byte[])) return "bin"; - if (type == typeof (Uri)) return "uri"; - throw new UpnpServiceDefinitionException (string.Format ("The data type {0} is unsupported.", type)); - } - - static IEnumerable BuildAllowedValues (Type type) - { - foreach (var field in type.GetFields (BindingFlags.Public | BindingFlags.Static)) { - var attributes = field.GetCustomAttributes (typeof (UpnpEnumAttribute), false); - var attribute = attributes.Length != 0 ? (UpnpEnumAttribute)attributes[0] : null; - if (attribute != null && !string.IsNullOrEmpty (attribute.Name)) { - yield return attribute.Name; - } else { - yield return field.Name; - } - } - } - - static IEnumerable BuildStateVariables (T service, - Dictionary stateVariables) - { - foreach (var state_variable_info in stateVariables.Values) { - yield return state_variable_info.StateVariable; - } - - foreach (var event_info in typeof (T).GetEvents (BindingFlags.Public | BindingFlags.Instance)) { - var state_variable = BuildStateVariable (event_info, service, stateVariables); - if (state_variable != null) { - yield return state_variable; - } - } - } - - static StateVariable BuildStateVariable (EventInfo eventInfo, - object service, - Dictionary stateVariables) - { - var attributes = eventInfo.GetCustomAttributes (typeof (UpnpStateVariableAttribute), false); - if (attributes.Length == 0) { - return null; - } - - var type = eventInfo.EventHandlerType; - if (!type.IsGenericType || type.GetGenericTypeDefinition () != typeof (EventHandler<>)) { - throw new UpnpServiceDefinitionException ( - "A event must be handled by the type EventHandler>."); - } - - type = type.GetGenericArguments ()[0]; - if (!type.IsGenericType || type.GetGenericTypeDefinition () != typeof (StateVariableChangedArgs<>)) { - throw new UpnpServiceDefinitionException ( - "A event must be handled by the type EventHandler>."); - } - - var attribute = (UpnpStateVariableAttribute)attributes[0]; - if (Omit (eventInfo.DeclaringType, service, attribute.OmitUnless)) { - return null; - } - - type = type.GetGenericArguments ()[0]; - var eventer = new Eventer (); - var method = Eventer.HandlerMethod.MakeGenericMethod (new[] { type }); - var handler = Delegate.CreateDelegate (eventInfo.EventHandlerType, eventer, method); - eventInfo.AddEventHandler (service, handler); - var name = string.IsNullOrEmpty (attribute.Name) ? eventInfo.Name : attribute.Name; - var data_type = string.IsNullOrEmpty (attribute.DataType) ? GetDataType (type) : attribute.DataType; - StateVariableInfo info; - if (stateVariables.TryGetValue (name, out info)) { - // TODO check type - info.StateVariable.SetEventer (eventer, attribute.IsMulticast); - return null; - } else { - return new StateVariable (name, data_type, - new StateVariableOptions { Eventer = eventer, IsMulticast = attribute.IsMulticast }); - } - } - - static IEnumerable Combine (IEnumerable arguments, ArgumentInfo return_argument) - { - foreach (var argument in arguments) { - yield return argument.Argument; - } - if (return_argument != null) { - yield return return_argument.Argument; - } - } - } -} +// +// ServiceControllerBuilder.cs +// +// Author: +// Scott Thomas +// +// Copyright (c) 2009 Scott Thomas +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Reflection; +using System.Text; +using Mono.Upnp.Control; + +namespace Mono.Upnp.Internal +{ + // FIXME I am not happy with the state of this class. Let's clean it up later. + static class ServiceControllerBuilder + { + abstract class Eventer : StateVariableEventer + { + + } + + class EventHandlerEventer : StateVariableEventer + { + public static readonly MethodInfo HandlerMethod = typeof (Eventer).GetMethod ("Handler"); + + public void Handler (object sender, StateVariableChangedArgs args) + { + OnStateVariableUpdated (args.NewValue.ToString ()); + } + } + + class PropertyChangedEventer : StateVariableEventer + { + public PropertyChangedEventer (PropertyInfo propertyInfo) + { + PropertyInfo = propertyInfo; + } + + public PropertyInfo PropertyInfo { get; private set; } + + public void OnPropertyChanged (object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == PropertyInfo.Name) { + var value = PropertyInfo.GetValue (sender, null); + OnStateVariableUpdated (value != null ? value.ToString () : null); + } + } + } + + class ArgumentInfo + { + public Argument Argument; + public ParameterInfo ParameterInfo; + } + + class StateVariableInfo + { + public StateVariable StateVariable; + public Type Type; + } + + public static ServiceController Build (T service) + { + var state_variables = new Dictionary (); + return new ServiceController ( + BuildActions (service, state_variables), BuildStateVariables (service, state_variables)); + } + + static IEnumerable BuildActions (T service, + Dictionary stateVariables) + { + foreach (var method in typeof (T).GetMethods (BindingFlags.Public | BindingFlags.Instance)) { + var action = BuildAction (method, service, stateVariables); + if (action != null) { + yield return action; + } + } + } + + readonly static object[] empty_args = new object[0]; + + static bool Omit (Type type, object service, string unless) + { + if (unless == null) { + return false; + } + var property = type.GetProperty (unless); + if (property == null || property.PropertyType != typeof (bool) || !property.CanRead) { + throw new UpnpServiceDefinitionException ( + "The OmitUnless property must reference a readable bool property in the same type."); + } + return property.GetValue (service, empty_args).Equals (false); + } + + static ServiceAction BuildAction (MethodInfo method, + object service, + Dictionary stateVariables) + { + var attributes = method.GetCustomAttributes (typeof (UpnpActionAttribute), false); + if (attributes.Length != 0) { + var attribute = (UpnpActionAttribute)attributes[0]; + if (Omit (method.DeclaringType, service, attribute.OmitUnless)) { + return null; + } + var name = string.IsNullOrEmpty (attribute.Name) ? method.Name : attribute.Name; + var parameters = method.GetParameters (); + var arguments = new ArgumentInfo[parameters.Length]; + for (var i = 0; i < parameters.Length; i++) { + arguments[i] = BuildArgumentInfo (parameters[i], name, stateVariables); + } + var return_argument = BuildArgumentInfo (method.ReturnParameter, name, stateVariables, true); + return new ServiceAction (name, Combine (arguments, return_argument), args => + { + Trace (name, args.Values); + + var argument_array = new object[arguments.Length]; + for (var i = 0; i < arguments.Length; i++) { + var argument = arguments[i]; + if (argument.Argument.Direction == ArgumentDirection.Out) { + continue; + } + + string value; + if (args.TryGetValue (argument.Argument.Name, out value)) { + var parameter_type = argument.ParameterInfo.ParameterType; + if (parameter_type.IsEnum) { + // TODO handle attributes + foreach (var enum_value in Enum.GetValues (parameter_type)) { + if (Enum.GetName (parameter_type, enum_value) == value) { + argument_array[i] = enum_value; + break; + } + } + } else { + argument_array[i] = Convert.ChangeType (value, parameter_type); + } + } else { + // TODO throw + } + } + + object result; + try { + result = method.Invoke (service, argument_array); + } catch (TargetInvocationException e) { + if (e.InnerException is UpnpControlException) { + throw e.InnerException; + } else { + throw new UpnpControlException ( + UpnpError.Unknown (), "Unexpected exception.", e.InnerException); + } + } + + var out_arguments = new Dictionary (); + for (var i = 0; i < arguments.Length; i++) { + if (arguments[i].Argument.Direction == ArgumentDirection.In) { + continue; + } + var value = argument_array[i]; + out_arguments.Add (arguments[i].Argument.Name, value != null ? value.ToString () : ""); + } + if (return_argument != null) { + out_arguments.Add (return_argument.Argument.Name, result.ToString ()); + } + + Trace (name, out_arguments); + + return out_arguments; + }); + } else { + return null; + } + } + + [Conditional ("TRACE")] + static void Trace (string name, IEnumerable values) + { + var builder = new StringBuilder (); + builder.Append (name); + builder.Append (" ("); + foreach (var value in values) { + if (name == null) { + builder.Append (", "); + } else { + name = null; + } + builder.Append (value); + } + builder.Append (')'); + Log.Trace (builder.ToString ()); + } + + [Conditional ("TRACE")] + static void Trace (string name, IDictionary results) + { + var builder = new StringBuilder (); + builder.Append (name); + builder.Append (": "); + foreach (var result in results) { + builder.Append ("\n\t"); + builder.Append (result.Key); + builder.Append (": "); + builder.Append (result.Value); + } + Log.Trace (builder.ToString ()); + } + + static ArgumentInfo BuildArgumentInfo (ParameterInfo parameterInfo, + string actionName, + Dictionary stateVariables) + { + return BuildArgumentInfo (parameterInfo, actionName, stateVariables, false); + } + + static ArgumentInfo BuildArgumentInfo (ParameterInfo parameterInfo, + string actionName, + Dictionary stateVariables, + bool isReturnValue) + { + if (parameterInfo.ParameterType == typeof (void)) { + return null; + } + + var attributes = parameterInfo.GetCustomAttributes (typeof (UpnpArgumentAttribute), false); + var attribute = attributes.Length != 0 + ? (UpnpArgumentAttribute)attributes[0] + : null; + var name = attribute != null && !string.IsNullOrEmpty (attribute.Name) + ? attribute.Name + : (string.IsNullOrEmpty (parameterInfo.Name) + ? "result" + : parameterInfo.Name); + var related_state_variable = BuildRelatedStateVariable ( + parameterInfo, actionName, name, stateVariables); + var direction = parameterInfo.IsRetval || parameterInfo.ParameterType.IsByRef || isReturnValue + ? ArgumentDirection.Out + : ArgumentDirection.In; + return new ArgumentInfo + { + ParameterInfo = parameterInfo, + Argument = new Argument ( + name, related_state_variable.StateVariable.Name, direction, parameterInfo.IsRetval || isReturnValue) + }; + } + + static StateVariableInfo BuildRelatedStateVariable (ParameterInfo parameterInfo, + string actionName, + string argumentName, + Dictionary stateVariables) + { + var attributes = parameterInfo.GetCustomAttributes (typeof (UpnpRelatedStateVariableAttribute), false); + var attribute = attributes.Length != 0 + ? (UpnpRelatedStateVariableAttribute)attributes[0] + : null; + var name = attribute != null && !string.IsNullOrEmpty (attribute.Name) + ? attribute.Name + : CreateRelatedStateVariableName (argumentName); + var data_type = attribute != null && !string.IsNullOrEmpty (attribute.DataType) + ? attribute.DataType + : GetDataType (parameterInfo.ParameterType); + var default_value = attribute != null && !string.IsNullOrEmpty (attribute.DefaultValue) + ? attribute.DefaultValue + : null; + var allowed_values = parameterInfo.ParameterType.IsEnum + ? BuildAllowedValues (parameterInfo.ParameterType) + : null; + var allowed_value_range = attribute != null && !string.IsNullOrEmpty (attribute.MinimumValue) + ? new AllowedValueRange (attribute.MinimumValue, attribute.MaximumValue, attribute.StepValue) + : null; + + StateVariableInfo state_variable_info; + if (stateVariables.TryGetValue (name, out state_variable_info)) { + var state_variable = state_variable_info.StateVariable; + if (state_variable.DataType != data_type || + state_variable.DefaultValue != default_value || + state_variable.AllowedValueRange != allowed_value_range || + ((state_variable.AllowedValues != null || allowed_values != null) && + state_variable_info.Type != parameterInfo.ParameterType)) { + if (attribute == null || string.IsNullOrEmpty (attribute.Name)) { + name = CreateRelatedStateVariableName (actionName, parameterInfo.Name); + } + } else { + return state_variable_info; + } + } + + if (allowed_values != null) { + state_variable_info = new StateVariableInfo + { + StateVariable = new StateVariable (name, allowed_values, + new StateVariableOptions { DefaultValue = default_value }), + Type = parameterInfo.ParameterType + }; + } else if (allowed_value_range != null) { + state_variable_info = new StateVariableInfo + { + StateVariable = new StateVariable (name, data_type, allowed_value_range, + new StateVariableOptions { DefaultValue = default_value }) + }; + } else { + state_variable_info = new StateVariableInfo + { + StateVariable = new StateVariable (name, data_type, + new StateVariableOptions { DefaultValue = default_value }) + }; + } + stateVariables[name] = state_variable_info; + return state_variable_info; + } + + static string CreateRelatedStateVariableName (string name) + { + return string.Format ("A_ARG_{0}", name); + } + + static string CreateRelatedStateVariableName (string actionName, string argumentName) + { + return string.Format ("A_ARG_{0}_{1}", actionName, argumentName); + } + + static string GetDataType (Type type) + { + if (type.IsByRef) { + type = type.GetElementType (); + } + if (type.IsEnum || type == typeof (string)) + return "string"; + if (type == typeof (int)) + return "i4"; + if (type == typeof (byte)) + return "ui1"; + if (type == typeof (ushort)) + return "ui2"; + if (type == typeof (uint)) + return "ui4"; + if (type == typeof (sbyte)) + return "i1"; + if (type == typeof (short)) + return "i2"; + if (type == typeof (long)) + return "int"; // TODO Is this right? The UPnP docs are vague + if (type == typeof (float)) + return "r4"; + if (type == typeof (double)) + return "r8"; + if (type == typeof (char)) + return "char"; + if (type == typeof (DateTime)) + return "date"; // TODO what about "time"? + if (type == typeof (bool)) + return "boolean"; + if (type == typeof (byte[])) + return "bin"; + if (type == typeof (Uri)) + return "uri"; + throw new UpnpServiceDefinitionException (string.Format ("The data type {0} is unsupported.", type)); + } + + static IEnumerable BuildAllowedValues (Type type) + { + foreach (var field in type.GetFields (BindingFlags.Public | BindingFlags.Static)) { + var attributes = field.GetCustomAttributes (typeof (UpnpEnumAttribute), false); + var attribute = attributes.Length != 0 ? (UpnpEnumAttribute)attributes[0] : null; + if (attribute != null && !string.IsNullOrEmpty (attribute.Name)) { + yield return attribute.Name; + } else { + yield return field.Name; + } + } + } + + static IEnumerable BuildStateVariables (T service, + Dictionary stateVariables) + { + foreach (var state_variable_info in stateVariables.Values) { + yield return state_variable_info.StateVariable; + } + + foreach (var property_info in typeof (T).GetProperties (BindingFlags.Public | BindingFlags.Instance)) { + var state_variable = BuildStateVariable (property_info, service, stateVariables); + if (state_variable != null) + yield return state_variable; + } + + foreach (var event_info in typeof (T).GetEvents (BindingFlags.Public | BindingFlags.Instance)) { + var state_variable = BuildStateVariable (event_info, service, stateVariables); + if (state_variable != null) { + yield return state_variable; + } + } + } + + static StateVariable BuildStateVariable (EventInfo eventInfo, + object service, + Dictionary stateVariables) + { + var attributes = eventInfo.GetCustomAttributes (typeof (UpnpStateVariableAttribute), false); + if (attributes.Length == 0) { + return null; + } + + var type = eventInfo.EventHandlerType; + if (!type.IsGenericType || type.GetGenericTypeDefinition () != typeof (EventHandler<>)) { + throw new UpnpServiceDefinitionException ( + "A event must be handled by the type EventHandler>."); + } + + type = type.GetGenericArguments ()[0]; + if (!type.IsGenericType || type.GetGenericTypeDefinition () != typeof (StateVariableChangedArgs<>)) { + throw new UpnpServiceDefinitionException ( + "A event must be handled by the type EventHandler>."); + } + + var attribute = (UpnpStateVariableAttribute)attributes[0]; + if (Omit (eventInfo.DeclaringType, service, attribute.OmitUnless)) { + return null; + } + + type = type.GetGenericArguments ()[0]; + var eventer = new EventHandlerEventer (); + var method = EventHandlerEventer.HandlerMethod.MakeGenericMethod (new[] { type }); + var handler = Delegate.CreateDelegate (eventInfo.EventHandlerType, eventer, method); + eventInfo.AddEventHandler (service, handler); + var name = string.IsNullOrEmpty (attribute.Name) ? eventInfo.Name : attribute.Name; + var data_type = string.IsNullOrEmpty (attribute.DataType) ? GetDataType (type) : attribute.DataType; + StateVariableInfo info; + if (stateVariables.TryGetValue (name, out info)) { + if (info.StateVariable.Eventer != null) + throw new UpnpServiceDefinitionException ( + string.Format ("An event handler is declared multiple times for {0}.", + name)); + + // TODO check type + info.StateVariable.SetEventer (eventer, attribute.IsMulticast); + return null; + } else { + return new StateVariable (name, data_type, + new StateVariableOptions { Eventer = eventer, IsMulticast = attribute.IsMulticast }); + } + } + + static StateVariable BuildStateVariable (PropertyInfo propertyInfo, + object service, + Dictionary stateVariables) + { + var attributes = propertyInfo.GetCustomAttributes (typeof (UpnpStateVariableAttribute), false); + if (attributes.Length == 0) { + return null; + } + + var attribute = (UpnpStateVariableAttribute)attributes[0]; + if (Omit (propertyInfo.DeclaringType, service, attribute.OmitUnless)) { + return null; + } + + PropertyChangedEventer eventer = null; + var notifyService = service as INotifyPropertyChanged; + if (notifyService != null) { + eventer = new PropertyChangedEventer (propertyInfo); + notifyService.PropertyChanged += eventer.OnPropertyChanged; + } + + var type = propertyInfo.PropertyType; + var name = string.IsNullOrEmpty (attribute.Name) ? propertyInfo.Name : attribute.Name; + var data_type = string.IsNullOrEmpty (attribute.DataType) ? GetDataType (type) : attribute.DataType; + StateVariableInfo info; + if (stateVariables.TryGetValue (name, out info)) { + if (info.StateVariable.Eventer != null) + throw new UpnpServiceDefinitionException ( + string.Format ("An event handler is declared multiple times for {0}.", + name)); + + // set initial value of state variable + var value = propertyInfo.GetValue (service, null); + info.StateVariable.Value = value != null ? value.ToString () : null; + + info.StateVariable.SetEventer (eventer, attribute.IsMulticast); + return null; + } else { + return new StateVariable (name, data_type, + new StateVariableOptions { Eventer = eventer, IsMulticast = attribute.IsMulticast }); + } + } + + static IEnumerable Combine (IEnumerable arguments, ArgumentInfo return_argument) + { + foreach (var argument in arguments) { + yield return argument.Argument; + } + if (return_argument != null) { + yield return return_argument.Argument; + } + } + } +}