diff --git a/ln.http.api.demo/ln.http.api.demo.csproj b/ln.http.api.demo/ln.http.api.demo.csproj index 12e29b0..f32b3c3 100644 --- a/ln.http.api.demo/ln.http.api.demo.csproj +++ b/ln.http.api.demo/ln.http.api.demo.csproj @@ -3,7 +3,7 @@ - + diff --git a/ln.http.api/HttpResponseExtensions.cs b/ln.http.api/HttpResponseExtensions.cs index 9c4a853..ad3e833 100644 --- a/ln.http.api/HttpResponseExtensions.cs +++ b/ln.http.api/HttpResponseExtensions.cs @@ -8,7 +8,7 @@ namespace ln.http.api public static HttpResponse Content(this HttpResponse response, JSONValue json) { - response.SetHeader("Content-type", "application/json"); + response.ContentType("application/json"); response.ContentWriter.Write(json.ToString()); response.ContentWriter.Flush(); return response; diff --git a/ln.http.api/JSONEventWebSocketResponse.cs b/ln.http.api/JSONEventWebSocketResponse.cs new file mode 100644 index 0000000..a414a18 --- /dev/null +++ b/ln.http.api/JSONEventWebSocketResponse.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using ln.http.websocket; +using ln.json; +using ln.json.mapping; +using ln.logging; +using ln.type; + +namespace ln.http.api +{ + public class JSONEventWebSocketResponse : JSONWebSocketResponse, IDisposable + { + static List activeWebSockets = new List(); + public static void SendUpdates() + { + if (Monitor.TryEnter(activeWebSockets, 100)) + { + try + { + foreach (JSONEventWebSocketResponse websocket in activeWebSockets) + websocket.SendUpdate(); + } catch (Exception e) + { + Logging.Log(e); + } finally + { + Monitor.Exit(activeWebSockets); + } + } else { + Logging.Log(LogLevel.WARNING, "JSONEventWebSocketResponse: SendUpdates(): failed to enter critical section"); + } + } + + public string DefaultTargetName { get; set; } + + Dictionary publishedTargets = new Dictionary(); + + + public JSONEventWebSocketResponse() + { + OnWebSocketReceivedJSON += JSONReceived; + OnWebSocketStateChanged += (WebSocketResponse websocket, WebSocketState newState) => { + switch (newState) + { + case WebSocketState.CLOSED: + lock (activeWebSockets) + activeWebSockets.Remove(this); + break; + case WebSocketState.OPEN: + lock (activeWebSockets) + activeWebSockets.Add(this); + break; + } + }; + } + + public JSONEventWebSocketResponse(string defaultTargetName) + :this() + { + DefaultTargetName = defaultTargetName; + } + + public JSONEventWebSocketResponse(object defaultInstance) + :this() + { + AddTarget(null, defaultInstance, defaultInstance.GetType().GetCustomAttribute()?.PublishPublicMembers ?? false, out Target target); + DefaultTargetName = target.Name; + } + public JSONEventWebSocketResponse(object defaultInstance, bool publishPublicMembers) + :this() + { + AddTarget(null, defaultInstance, publishPublicMembers, out Target target); + DefaultTargetName = target.Name; + } + + public void AddTarget(object instance) => AddTarget(null, instance, instance.GetType().GetCustomAttribute()?.PublishPublicMembers ?? false); + public void AddTarget(object instance, bool publishPublicMembers) => AddTarget(null, instance, publishPublicMembers); + public void AddTarget(string targetName, object instance, bool publishPublicMembers) => AddTarget(targetName, instance, publishPublicMembers, out Target target); + public void AddTarget(string targetName, object instance, bool publishPublicMembers, out Target target) + { + target = new Target(targetName, instance, publishPublicMembers); + publishedTargets.Add(target.Name, target); + } + + + void JSONReceived(JSONWebSocketResponse sender, JSONValue json) + { + if (json is JSONObject jsonMessage) + { + string eventName = jsonMessage["event"].ToNative().ToString(); + string targetName = jsonMessage["target"].ToNative().ToString(); + JSONValue jsonValue = jsonMessage["value"]; + + Target target = publishedTargets[targetName ?? DefaultTargetName]; + + switch (eventName) + { + case "action": + target.Call(jsonValue); + break; + case "set": + target.SetProperty(jsonValue); + break; + } + + } + } + + public virtual void SendUpdate() + { + JSONObject jsonUpdateValue = new JSONObject(); + JSONObject jsonUpdateMessage = new JSONObject() + .Add("event", "update") + .Add("value", jsonUpdateValue); + + foreach (Target target in publishedTargets.Values) + jsonUpdateValue.Add(target.Name, target.CreateUpdateValue()); + + Send(jsonUpdateMessage); + } + + public void Dispose() + { + lock (activeWebSockets) + activeWebSockets.Remove(this); + } + + public class Target + { + public string Name { get; } + public object Instance { get; } + + Dictionary publishedMethods = new Dictionary(); + Dictionary publishedProperties = new Dictionary(); + Dictionary publishedFields = new Dictionary(); + + + public Target(object instance, bool publishPublicMembers) :this(instance.GetType().Name, instance, publishPublicMembers) { } + public Target(object instance) :this(instance.GetType().Name, instance, false) { } + public Target(string targetName, object instance) : this(targetName, instance, false) { } + public Target(string targetName, object instance, bool publishPublicMembers) + { + Instance = instance; + Name = targetName ?? Instance.GetType().Name; + + Initialize(publishPublicMembers); + } + + + void Initialize(bool publishPublicMembers) + { + foreach (MethodInfo methodInfo in Instance.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy)) + { + ESMethodAttribute methodAttribute = methodInfo.GetCustomAttribute(); + if ((methodAttribute != null) || (publishPublicMembers && methodInfo.IsPublic)) + publishedMethods.Add(methodAttribute?.Name ?? methodInfo.Name, methodInfo); + } + + foreach (FieldInfo fieldInfo in Instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy)) + { + ESPropertyAttribute propertyAttribute = fieldInfo.GetCustomAttribute(); + if ((propertyAttribute != null) || (publishPublicMembers && fieldInfo.IsPublic)) + publishedFields.Add(propertyAttribute?.Name ?? fieldInfo.Name, fieldInfo); + } + + foreach (PropertyInfo propertyInfo in Instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy)) + { + ESPropertyAttribute propertyAttribute = propertyInfo.GetCustomAttribute(); + if ((propertyAttribute != null) || (publishPublicMembers && propertyInfo.GetMethod.IsPublic)) + publishedProperties.Add(propertyAttribute?.Name ?? propertyInfo.Name, propertyInfo); + } + } + + public JSONValue CreateUpdateValue() + { + JSONObject jsonUpdate = new JSONObject(); + + foreach (PropertyInfo propertyInfo in publishedProperties.Values) + if (propertyInfo.CanRead) + jsonUpdate.Add(propertyInfo.Name, JSONMapper.DefaultMapper.ToJson(propertyInfo.GetValue(Instance))); + + foreach (FieldInfo fieldInfo in publishedFields.Values) + jsonUpdate.Add(fieldInfo.Name, JSONMapper.DefaultMapper.ToJson(fieldInfo.GetValue(Instance))); + + return jsonUpdate; + } + + public void Call(JSONValue callValue) + { + string methodName; + MethodInfo methodInfo; + object[] arguments; + + if (callValue is JSONString jsonMethodName) + { + methodName = jsonMethodName.Value; + methodInfo = publishedMethods[methodName]; + arguments = new object[0]; + } + else if (callValue is JSONObject callObject) + { + methodName = callObject["method"].ToNative().ToString(); + methodInfo = publishedMethods[methodName]; + if (!JSONMapper.DefaultMapper.MapMethodParameters(methodInfo, callObject["arguments"], out arguments)) + throw new ArgumentException(nameof(callValue)); + } + else + throw new ArgumentOutOfRangeException(nameof(callValue)); + + methodInfo.Invoke(Instance, arguments); + } + + public void Call(string methodName, object[] arguments) + { + MethodInfo methodInfo = publishedMethods[methodName]; + methodInfo.Invoke(Instance, arguments); + } + + public void SetProperty(JSONValue jsonValue) + { + if (jsonValue is JSONObject setObject) + { + string propertyName = setObject["name"].ToNative().ToString(); + if (publishedProperties.TryGetValue(propertyName, out PropertyInfo propertyInfo) && propertyInfo.CanWrite) + propertyInfo.SetValue(Instance, Cast.To(setObject["value"].ToNative(), propertyInfo.PropertyType)); + else if (publishedFields.TryGetValue(propertyName, out FieldInfo fieldInfo) && !fieldInfo.IsInitOnly) + fieldInfo.SetValue(Instance, Cast.To(setObject["value"].ToNative(), fieldInfo.FieldType)); + } + } + + } + } +} \ No newline at end of file diff --git a/ln.http.api/JSONWebSocketResponse.cs b/ln.http.api/JSONWebSocketResponse.cs new file mode 100644 index 0000000..26c0cbf --- /dev/null +++ b/ln.http.api/JSONWebSocketResponse.cs @@ -0,0 +1,39 @@ +using System; +using ln.http.websocket; +using ln.json; +using ln.logging; + +namespace ln.http.api +{ + + public delegate void WebSocketReceivedJSON(JSONWebSocketResponse sender, JSONValue json); + + public class JSONWebSocketResponse : WebSocketResponse + { + public event WebSocketReceivedJSON OnWebSocketReceivedJSON; + + public JSONWebSocketResponse(){ } + + public override void Received(string textMessage) + { + base.Received(textMessage); + try + { + JSONValue json = JSONParser.Parse(textMessage); + try { + OnWebSocketReceivedJSON?.Invoke(this, json); + } catch (Exception ex) + { + Logging.Log(ex); + } + + } catch (Exception e) + { + Logging.Log(LogLevel.ERROR, "JSONWebSocketResponse: received unparsable json message: {0}", textMessage); + Close(); + } + } + + public void Send(JSONValue json) => Send(json.ToString()); + } +} \ No newline at end of file diff --git a/ln.http.api/WebApiController.cs b/ln.http.api/WebApiController.cs index eab368b..4c8a83e 100644 --- a/ln.http.api/WebApiController.cs +++ b/ln.http.api/WebApiController.cs @@ -51,12 +51,14 @@ namespace ln.http.api return httpResponse; if (result is JSONValue jsonResult) - return defaultResponseFactory().Content(jsonResult); + return defaultResponseFactory() + .ContentType("application/json") + .Content(jsonResult); if (JSONMapper.DefaultMapper.Serialize(result, out JSONValue json)) return defaultResponseFactory() .ContentType("application/json") - .Content(result.ToString()) + .Content(json.ToString()) ; return HttpResponse.InternalServerError().Content("Method result could not be serialized"); diff --git a/ln.http.api/attributes/ESMethodAttribute.cs b/ln.http.api/attributes/ESMethodAttribute.cs new file mode 100644 index 0000000..7f51a19 --- /dev/null +++ b/ln.http.api/attributes/ESMethodAttribute.cs @@ -0,0 +1,10 @@ + +using System; + +namespace ln.http.api +{ + public class ESMethodAttribute : Attribute + { + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/ln.http.api/attributes/ESPropertyAttribute.cs b/ln.http.api/attributes/ESPropertyAttribute.cs new file mode 100644 index 0000000..9ef1c88 --- /dev/null +++ b/ln.http.api/attributes/ESPropertyAttribute.cs @@ -0,0 +1,11 @@ + +using System; + +namespace ln.http.api +{ + public class ESPropertyAttribute : Attribute + { + public string Name { get; set; } + public bool ReadOnly { get; set; } + } +} \ No newline at end of file diff --git a/ln.http.api/attributes/ESTargetAttribute.cs b/ln.http.api/attributes/ESTargetAttribute.cs new file mode 100644 index 0000000..e99e2b8 --- /dev/null +++ b/ln.http.api/attributes/ESTargetAttribute.cs @@ -0,0 +1,12 @@ + + +using System; +using System.Reflection; + +namespace ln.http.api +{ + public class ESTargetAttribute : Attribute + { + public bool PublishPublicMembers { get; set; } + } +} \ No newline at end of file diff --git a/ln.http.api/ln.http.api.csproj b/ln.http.api/ln.http.api.csproj index 19db803..072346f 100644 --- a/ln.http.api/ln.http.api.csproj +++ b/ln.http.api/ln.http.api.csproj @@ -4,7 +4,7 @@ netcoreapp3.1 true - 0.0.4 + 0.0.6 Harald Wolff-Thobaben l--n.de Framework to create REST like APIs @@ -13,8 +13,8 @@ - - + +