From 0a4d3e900a1756dd3b4a4f3d5cb9f6cd6fdd9421 Mon Sep 17 00:00:00 2001 From: marzi333 Date: Mon, 15 Apr 2024 15:24:14 +0200 Subject: [PATCH 1/2] initial commit --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 603b19d..5b09f86 100644 --- a/README.md +++ b/README.md @@ -107,3 +107,5 @@ For a more detailed step-by-step explanation and documentation, feel free to vis - [ ] MQTT Consumer ... more in the future + + From 0f902273bdf31e3a8ca2e9c9d485c4c376c44abc Mon Sep 17 00:00:00 2001 From: marzi333 Date: Tue, 25 Jun 2024 12:26:17 +0200 Subject: [PATCH 2/2] protocol client abstraction; separated the protocol client sending requests and getting responses from the interaction affordances implemented by the ConsumedThing --- .gitignore | 3 +- SimpleHTTPConsumerTester/Program.cs | 33 +- WoT/SimpleHTTPConsumer.cs | 905 ------------------ WoT/WoT.csproj | 5 + WoT/binding-http/SimpleHTTPClient.cs | 125 +++ WoT/core/ConsumedThing.cs | 582 +++++++++++ WoT/core/Consumer.cs | 145 +++ .../Definitions.cs} | 15 +- WoT/{ => core}/Errors.cs | 0 WoT/core/InteractionOutput.cs | 163 ++++ WoT/core/ProtocolBindings.cs | 24 + WoT/core/Subscription.cs | 82 ++ WoT/{ => core}/TDHelpers.cs | 0 WoT/{ => core}/WoT.cs | 22 +- 14 files changed, 1189 insertions(+), 915 deletions(-) delete mode 100644 WoT/SimpleHTTPConsumer.cs create mode 100644 WoT/binding-http/SimpleHTTPClient.cs create mode 100644 WoT/core/ConsumedThing.cs create mode 100644 WoT/core/Consumer.cs rename WoT/{WoT-Definitions.cs => core/Definitions.cs} (99%) rename WoT/{ => core}/Errors.cs (100%) create mode 100644 WoT/core/InteractionOutput.cs create mode 100644 WoT/core/ProtocolBindings.cs create mode 100644 WoT/core/Subscription.cs rename WoT/{ => core}/TDHelpers.cs (100%) rename WoT/{ => core}/WoT.cs (95%) diff --git a/.gitignore b/.gitignore index 41aba75..1726fb1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ # Ignore documentation Generation Output WoT/docGeneration/api -WoT/docGeneration/_site \ No newline at end of file +WoT/docGeneration/_site +/WoT/binding-http/SimpleConsumedThing.cs diff --git a/SimpleHTTPConsumerTester/Program.cs b/SimpleHTTPConsumerTester/Program.cs index ba82d92..9e0d9e3 100644 --- a/SimpleHTTPConsumerTester/Program.cs +++ b/SimpleHTTPConsumerTester/Program.cs @@ -1,10 +1,33 @@ -// See https://aka.ms/new-console-template for more information +// See https://aka.ms/new-console-template for more informatio5n using WoT; using WoT.Definitions; using WoT.Implementation; -SimpleHTTPConsumer consumer = new SimpleHTTPConsumer(); -ThingDescription td = await consumer.RequestThingDescription("http://plugfest.thingweb.io:8083/smart-coffee-machine"); -SimpleConsumedThing? consumedThing = await consumer.Consume(td) as SimpleConsumedThing; -if(consumedThing != null) await consumedThing.WriteProperty("availableResourceLevel", 100 ,new InteractionOptions { uriVariables = new Dictionary { { "id", "water" } } }); +Consumer consumer = new Consumer(); +consumer.AddClient(new SimpleHTTPClient()); +ThingDescription coffeeMachineTD = await consumer.RequestThingDescription("http://plugfest.thingweb.io:8083/smart-coffee-machine"); +ThingDescription counterTD = await consumer.RequestThingDescription("http://plugfest.thingweb.io:8083/counter"); +ConsumedThing coffeeMachineConsumedThing = consumer.Consume(coffeeMachineTD) as ConsumedThing; +ConsumedThing counterConsumedThing = consumer.Consume(counterTD) as ConsumedThing; +Action> countListener = async (IInteractionOutput count) => { Console.WriteLine(await count.Value()); }; +if (coffeeMachineConsumedThing != null) +{ + //ReadProperty test + int count = await counterConsumedThing.ReadProperty("count", new InteractionOptions { uriVariables = new Dictionary { { "step", 5 } } }).Result.Value(); + Console.WriteLine("Count = " + count); + + + //ObserveProperty test + ISubscription countObserver = await counterConsumedThing.ObserveProperty("count", countListener); + + //InvokeAction test + IInteractionOutput actionOutput = await counterConsumedThing.InvokeAction("increment", options: new InteractionOptions { uriVariables = new Dictionary { { "step", 5 } } }) ; + + //WriteProperty + await coffeeMachineConsumedThing.WriteProperty("availableResourceLevel", 100, new InteractionOptions { uriVariables = new Dictionary { { "id", "water" } } }); + +} + + Console.WriteLine("Done"); +Console.ReadLine(); diff --git a/WoT/SimpleHTTPConsumer.cs b/WoT/SimpleHTTPConsumer.cs deleted file mode 100644 index 97ec74a..0000000 --- a/WoT/SimpleHTTPConsumer.cs +++ /dev/null @@ -1,905 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Schema; -using System; -using System.Text; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Tavis.UriTemplates; -using WoT.Definitions; -using WoT.Errors; -using System.Threading; - -namespace WoT.Implementation -{ - /// - /// A simple WoT Consumer that is capable of requesting TDs only from HTTP resources und consumes them to generate - /// - public class SimpleHTTPConsumer : IConsumer, IRequester - { - private readonly JsonSerializer _serializer; - public readonly HttpClient httpClient; - - /// - public SimpleHTTPConsumer() - { - httpClient = new HttpClient(); - _serializer = new JsonSerializer(); - - } - - public async Task Consume(ThingDescription td) - { - Task task = Task.Run(() => - { - return new SimpleConsumedThing(td, this); - }); - return await task; - } - - public async Task RequestThingDescription(string url) - { - Uri tdUrl = new Uri(url); - if (tdUrl.Scheme != "http") throw new Exception($"The protocol for accessing the TD url {url} is not HTTP"); - Console.WriteLine($"Info: Fetching TD from {url}"); - HttpResponseMessage tdResponse = await httpClient.GetAsync(tdUrl); - tdResponse.EnsureSuccessStatusCode(); - Console.WriteLine($"Info: Fetched TD from {url} successfully"); - Console.WriteLine($"Info: Parsing TD"); - HttpContent body = tdResponse.Content; - string tdData = await body.ReadAsStringAsync(); - TextReader reader = new StringReader(tdData); - ThingDescription td = _serializer.Deserialize(reader, typeof(ThingDescription)) as ThingDescription; - Console.WriteLine($"Info: Parsed TD successfully"); - return td; - } - - public async Task RequestThingDescription(Uri tdUrl) - { - if (tdUrl.Scheme != "http") throw new Exception($"The protocol for accessing the TD url {tdUrl.OriginalString} is not HTTP"); - Console.WriteLine($"Info: Fetching TD from {tdUrl.OriginalString}"); - HttpResponseMessage tdResponse = await httpClient.GetAsync(tdUrl); - tdResponse.EnsureSuccessStatusCode(); - Console.WriteLine($"Info: Fetched TD from {tdUrl.OriginalString} successfully"); - Console.WriteLine($"Info: Parsing TD"); - HttpContent body = tdResponse.Content; - string tdData = await body.ReadAsStringAsync(); - TextReader reader = new StringReader(tdData); - ThingDescription td = _serializer.Deserialize(reader, typeof(ThingDescription)) as ThingDescription; - Console.WriteLine($"Info: Parsed TD successfully"); - return td; - } - } - - /// - /// A representation of a consumed Thing that is capable of interacting only with HTTP resources. - /// - /// - /// This simple Consumed Thing class can only handle HTTP request and responses, "application/json" content type, and "observeproperty" and "subscribeevent" using long polling - /// - public class SimpleConsumedThing : IConsumedThing - { - - private readonly Dictionary _activeSubscriptions; - private readonly Dictionary _activeObservations; - private readonly ThingDescription _td; - private readonly SimpleHTTPConsumer _consumer; - public SimpleConsumedThing(ThingDescription td, SimpleHTTPConsumer consumer) - { - _td = td; - _consumer = consumer; - _activeSubscriptions = new Dictionary(); - _activeObservations = new Dictionary(); - } - - /****************************************************** Action Operations ******************************************************/ - public async Task InvokeAction(string actionName, InteractionOptions? options = null) - { - var actions = this._td.Actions; - Form form; - if (!actions.TryGetValue(actionName, out var actionAffordance)) - { - // 4. If interaction is undefined, reject promise with a NotFoundError and stop. - throw new Exception($"Property {actionName} not found in TD with ID {_td.Id}"); - } - else - { - // find suitable form - form = FindSuitableForm(actionAffordance.Forms, null, "http", options, "application/json"); - // Handle UriVariables - if (options.HasValue && options.Value.uriVariables != null) form = HandleUriVariables(form, options.Value.uriVariables); - - if (form == null) throw new Exception($"Could not find a form that allows reading property {actionName}"); - Console.Write(form.Href); - HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, form.Href); - var interactionResponse = await _consumer.httpClient.SendAsync(message); - interactionResponse.EnsureSuccessStatusCode(); - InteractionOutput output = new InteractionOutput(form); - return output; - } - } - - public async Task InvokeAction(string actionName, U parameters, InteractionOptions? options = null) - { - var actions = this._td.Actions; - Form form; - if (!actions.TryGetValue(actionName, out var actionAffordance)) - { - // 4. If interaction is undefined, reject promise with a NotFoundError and stop. - throw new Exception($"Property {actionName} not found in TD with ID {_td.Id}"); - } - else - { - // find suitable form - form = FindSuitableForm(actionAffordance.Forms, null, "http", options); - // Handle UriVariables - if (options.HasValue && options.Value.uriVariables != null) form = HandleUriVariables(form, options.Value.uriVariables); - - if (form == null) throw new Exception($"Could not find a form that allows reading property {actionName}"); - string payloadString = JsonConvert.SerializeObject(parameters); - StringContent payload = new StringContent(payloadString, Encoding.UTF8, "application/json"); - HttpResponseMessage interactionResponse = await _consumer.httpClient.PostAsync(form.Href, payload); - interactionResponse.EnsureSuccessStatusCode(); - InteractionOutput output = new InteractionOutput(form); - return output; - } - } - - public async Task> InvokeAction(string actionName, InteractionOptions? options = null) - { - var actions = this._td.Actions; - Form form; - if (!actions.TryGetValue(actionName, out var actionAffordance)) - { - // 4. If interaction is undefined, reject promise with a NotFoundError and stop. - throw new Exception($"Property {actionName} not found in TD with ID {_td.Id}"); - } - else - { - // find suitable form - form = FindSuitableForm(actionAffordance.Forms, null, "http", options); - // Handle UriVariables - if (options.HasValue && options.Value.uriVariables != null) form = HandleUriVariables(form, options.Value.uriVariables); - - if (form == null) throw new Exception($"Could not find a form that allows reading property {actionName}"); - Console.Write(form.Href); - HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, form.Href); - HttpResponseMessage interactionResponse = await _consumer.httpClient.SendAsync(message); - interactionResponse.EnsureSuccessStatusCode(); - Stream responseStream = await interactionResponse.Content.ReadAsStreamAsync(); - InteractionOutput output = new InteractionOutput(actionAffordance.Output, form, responseStream); - return output; - } - } - - public async Task> InvokeAction(string actionName, U parameters, InteractionOptions? options = null) - { - var actions = this._td.Actions; - Form form; - if (!actions.TryGetValue(actionName, out var actionAffordance)) - { - // 4. If interaction is undefined, reject promise with a NotFoundError and stop. - throw new Exception($"Property {actionName} not found in TD with ID {_td.Id}"); - } - else - { - // find suitable form - form = FindSuitableForm(actionAffordance.Forms, null, "http", options); - // Handle UriVariables - if (options.HasValue && options.Value.uriVariables != null) form = HandleUriVariables(form, options.Value.uriVariables); - - if (form == null) throw new Exception($"Could not find a form that allows reading property {actionName}"); - string payloadString = JsonConvert.SerializeObject(parameters); - StringContent payload = new StringContent(payloadString, Encoding.UTF8, "application/json"); - HttpResponseMessage interactionResponse = await _consumer.httpClient.PostAsync(form.Href, payload); - interactionResponse.EnsureSuccessStatusCode(); - Stream responseStream = await interactionResponse.Content.ReadAsStreamAsync(); - InteractionOutput output = new InteractionOutput(actionAffordance.Output, form, responseStream); - return output; - } - } - - /****************************************************** Property Operations ******************************************************/ - - public async Task> ReadProperty(string propertyName, InteractionOptions? options = null) - { - var properties = this._td.Properties; - Form form; - // 3. Let interaction be [[td]].properties.propertyName. - if (!properties.TryGetValue(propertyName, out var propertyAffordance)) - { - // 4. If interaction is undefined, reject promise with a NotFoundError and stop. - throw new NotFoundError($"Property {propertyName} not found in TD with ID {_td.Id}"); - } - else - { - if (propertyAffordance.WriteOnly == true) throw new NotAllowedError($"Cannot read writeOnly property {propertyName}"); - // find suitable form - form = FindSuitableForm(propertyAffordance.Forms, "readproperty", "http", options); - // Handle UriVariables - if (options.HasValue && options.Value.uriVariables != null) form = HandleUriVariables(form, options.Value.uriVariables); - - if (form == null) throw new NotFoundError($"Could not find a form that allows reading property {propertyName}"); - HttpResponseMessage interactionResponse = await _consumer.httpClient.GetAsync(form.Href); - interactionResponse.EnsureSuccessStatusCode(); - Stream responseStream = await interactionResponse.Content.ReadAsStreamAsync(); - InteractionOutput output = new InteractionOutput(propertyAffordance, form, responseStream); - return output; - } - } - - public async Task WriteProperty(string propertyName, T value, InteractionOptions? options = null) - { - var properties = this._td.Properties; - Form form; - // 3. Let interaction be [[td]].properties.propertyName. - if (!properties.TryGetValue(propertyName, out var propertyAffordance)) - { - // 4. If interaction is undefined, reject promise with a NotFoundError and stop. - throw new Exception($"Property {propertyName} not found in TD with ID {_td.Id}"); - } - else - { - if (propertyAffordance.ReadOnly == true) throw new Exception($"Cannot read writeOnly property {propertyName}"); - // find suitable form - form = FindSuitableForm(propertyAffordance.Forms, "readproperty", "http", options); - // Handle UriVariables - if (options.HasValue && options.Value.uriVariables != null) form = HandleUriVariables(form, options.Value.uriVariables); - - if (form == null) throw new Exception($"Could not find a form that allows reading property {propertyName}"); - string payloadString = JsonConvert.SerializeObject(value); - var payload = new StringContent(payloadString, Encoding.UTF8, "application/json"); - var ms = new HttpRequestMessage(HttpMethod.Put, form.Href); - ms.Content = payload; - HttpResponseMessage interactionResponse = await _consumer.httpClient.SendAsync(ms); - interactionResponse.EnsureSuccessStatusCode(); - } - } - - public async Task ObserveProperty(string propertyName, Action> listener, InteractionOptions? options = null) - { - Subscription subscription = null; - PropertyAffordance propertyAffordance = null; - try - { - if (listener.GetType() == typeof(Action)) - throw new TypeError("listener for event " + propertyName + " specified is not a function."); - - if (_activeObservations.ContainsKey(propertyName)) - throw new NotAllowedError("Event " + propertyName + " has an already active subscription."); - - if (!_td.Properties.TryGetValue(propertyName, out propertyAffordance)) - throw new NotFoundError("Property " + propertyName + " was not found in TD. TD Title:" + _td.Title + "."); - - SimpleConsumedThing simpleConsumedThing = this; - Form[] forms = propertyAffordance.Forms; - Form form = simpleConsumedThing.FindSuitableForm(forms, "observeproperty", "http", options, "application/json", "longpoll"); - - if (options.HasValue && options.Value.uriVariables != null) - form = HandleUriVariables(form, options.Value.uriVariables); - - subscription = new Subscription(Subscription.SubscriptionType.Observation, propertyName, propertyAffordance, form, this); - try - { - var task = Task.Run(async () => - { - while (subscription.Active) - { - subscription.StopObservation += (sender, args) => subscription.tokenSource.Cancel(); - HttpResponseMessage interactionResponse = await _consumer.httpClient.GetAsync(form.Href, subscription.CancellationToken); - interactionResponse.EnsureSuccessStatusCode(); - Stream responseStream = await interactionResponse.Content.ReadAsStreamAsync(); - InteractionOutput output = new InteractionOutput(propertyAffordance, form, responseStream); - listener.Invoke(output); - } - }, subscription.CancellationToken); - } - catch (TaskCanceledException) - { - Console.WriteLine("Unobserved to property: " + propertyName + " of TD " + _td.Title); - } - return subscription; - } - catch (TypeError e) - { - throw e; - } - catch (NotAllowedError e) - { - Console.WriteLine(e.ToString() + " The subscription is ignored"); - return null; - } - } - - public async Task ObserveProperty(string propertyName, Action> listener, Action onerror, InteractionOptions? options = null) - { - Subscription subscription = null; - PropertyAffordance propertyAffordance = null; - try - { - if (listener.GetType() == typeof(Action)) - throw new TypeError("listener for event " + propertyName + " specified is not a function."); - - if (_activeObservations.ContainsKey(propertyName)) - throw new NotAllowedError("Event " + propertyName + " has an already active subscription."); - - if (!_td.Properties.TryGetValue(propertyName, out propertyAffordance)) - throw new NotFoundError("Property " + propertyName + " was not found in TD. TD Title:" + _td.Title + "."); - - SimpleConsumedThing simpleConsumedThing = this; - Form[] forms = propertyAffordance.Forms; - Form form = simpleConsumedThing.FindSuitableForm(forms, "observeproperty", "http", options, "application/json", "longpoll"); - - if (options.HasValue && options.Value.uriVariables != null) - form = HandleUriVariables(form, options.Value.uriVariables); - - subscription = new Subscription(Subscription.SubscriptionType.Observation, propertyName, propertyAffordance, form, this); - try - { - var task = Task.Run(async () => - { - while (subscription.Active) - { - subscription.StopObservation += (sender, args) => subscription.tokenSource.Cancel(); - HttpResponseMessage interactionResponse = await _consumer.httpClient.GetAsync(form.Href, subscription.CancellationToken); - interactionResponse.EnsureSuccessStatusCode(); - Stream responseStream = await interactionResponse.Content.ReadAsStreamAsync(); - InteractionOutput output = new InteractionOutput(propertyAffordance, form, responseStream); - listener.Invoke(output); - } - }, subscription.CancellationToken); - } - catch (TaskCanceledException) - { - Console.WriteLine("Unobserved to property: " + propertyName + " of TD " + _td.Title); - } - catch (HttpRequestException e) - { - onerror(e); - } - return subscription; - } - catch (TypeError e) - { - throw e; - } - catch (NotAllowedError e) - { - Console.WriteLine(e.ToString() + " The observation is ignored"); - return null; - } - } - - /****************************************************** Event Operations ******************************************************/ - - public async Task SubscribeEvent(string eventName, Action listener, InteractionOptions? options = null) - { - Subscription subscription = null; - EventAffordance eventAffordance = null; - try - { - if (listener.GetType() == typeof(Action)) - throw new TypeError($"listener for event {eventName} specified is not a function."); - - if (_activeSubscriptions.ContainsKey(eventName)) - throw new NotAllowedError($"Event {eventName} has an already active subscription."); - - if (!_td.Events.TryGetValue(eventName, out eventAffordance)) - throw new NotFoundError($"Event {eventName} was not found in TD. TD Title: {_td.Title}."); - - Form[] forms = eventAffordance.Forms; - Form form = FindSuitableForm(forms, "subscribeevent", "http", options, "application/json", "longpoll"); - - if (options.HasValue && options.Value.uriVariables != null) - form = HandleUriVariables(form, options.Value.uriVariables); - - subscription = new Subscription(Subscription.SubscriptionType.Event, eventName, eventAffordance, form, this); - try - { - var task = Task.Run(async () => - { - while (subscription.Active) - { - subscription.StopObservation += (sender, args) => subscription.tokenSource.Cancel(); - HttpResponseMessage interactionResponse = await _consumer.httpClient.GetAsync(form.Href, subscription.CancellationToken); - interactionResponse.EnsureSuccessStatusCode(); - listener.Invoke(); - } - }, subscription.CancellationToken); - } - catch (TaskCanceledException) - { - Console.WriteLine($"Unsubscribed to event: {eventName} of TD {_td.Title}"); - } - - return subscription; - } - catch (TypeError e) - { - throw e; - } - catch (NotAllowedError e) - { - Console.WriteLine(e.ToString() + " The subscription is ignored"); - return null; - } - } - - public async Task SubscribeEvent(string eventName, Action listener, Action onerror, InteractionOptions? options = null) - { - Subscription subscription = null; - EventAffordance eventAffordance = null; - try - { - if (listener.GetType() == typeof(Action)) - throw new TypeError($"listener for event {eventName} specified is not a function."); - - if (_activeSubscriptions.ContainsKey(eventName)) - throw new NotAllowedError($"Event {eventName} has an already active subscription."); - - if (!_td.Events.TryGetValue(eventName, out eventAffordance)) - throw new NotFoundError($"Event {eventName} was not found in TD. TD Title: {_td.Title}."); - - Form[] forms = eventAffordance.Forms; - Form form = FindSuitableForm(forms, "subscribeevent", "http", options, "application/json", "longpoll"); - - if (options.HasValue && options.Value.uriVariables != null) - form = HandleUriVariables(form, options.Value.uriVariables); - - subscription = new Subscription(Subscription.SubscriptionType.Event, eventName, eventAffordance, form, this); - try - { - var task = Task.Run(async () => - { - while (subscription.Active) - { - subscription.StopObservation += (sender, args) => subscription.tokenSource.Cancel(); - HttpResponseMessage interactionResponse = await _consumer.httpClient.GetAsync(form.Href, subscription.CancellationToken); - interactionResponse.EnsureSuccessStatusCode(); - listener.Invoke(); - } - }, subscription.CancellationToken); - } - catch (TaskCanceledException) - { - Console.WriteLine($"Unsubscribed to event: {eventName} of TD {_td.Title}"); - } - catch (HttpRequestException e) - { - onerror(e); - } - return subscription; - } - catch (TypeError e) - { - throw e; - } - catch (NotAllowedError e) - { - Console.WriteLine(e.ToString() + " The subscription is ignored"); - return null; - } - } - - public async Task SubscribeEvent(string eventName, Action> listener, InteractionOptions? options = null) - { - Subscription subscription = null; - EventAffordance eventAffordance = null; - try - { - if (listener.GetType() == typeof(Action)) - throw new TypeError($"listener for event {eventName} specified is not a function."); - - if (_activeSubscriptions.ContainsKey(eventName)) - throw new NotAllowedError($"Event {eventName} has an already active subscription."); - - if (!_td.Events.TryGetValue(eventName, out eventAffordance)) - throw new NotFoundError($"Event {eventName} was not found in TD. TD Title: {_td.Title}."); - - Form[] forms = eventAffordance.Forms; - Form form = FindSuitableForm(forms, "subscribeevent", "http", options, "application/json", "longpoll"); - - if (options.HasValue && options.Value.uriVariables != null) - form = HandleUriVariables(form, options.Value.uriVariables); - - subscription = new Subscription(Subscription.SubscriptionType.Event, eventName, eventAffordance, form, this); - try - { - var task = Task.Run(async () => - { - while (subscription.Active) - { - subscription.StopObservation += (sender, args) => subscription.tokenSource.Cancel(); - HttpResponseMessage interactionResponse = await _consumer.httpClient.GetAsync(form.Href, subscription.CancellationToken); - interactionResponse.EnsureSuccessStatusCode(); - Stream responseStream = await interactionResponse.Content.ReadAsStreamAsync(); - InteractionOutput output = new InteractionOutput(eventAffordance.Data, form, responseStream); - listener.Invoke(output); - } - }, subscription.CancellationToken); - } - - catch (TaskCanceledException) - { - Console.WriteLine($"Unsubscribed to event: {eventName} of TD {_td.Title}"); - } - - return subscription; - } - catch (TypeError e) - { - throw e; - } - catch (NotAllowedError e) - { - Console.WriteLine(e.ToString() + " The subscription is ignored"); - return null; - } - } - - public async Task SubscribeEvent(string eventName, Action> listener, Action onerror, InteractionOptions? options = null) - { - Subscription subscription = null; - EventAffordance eventAffordance = null; - try - { - if (listener.GetType() == typeof(Action)) - throw new TypeError($"listener for event {eventName} specified is not a function."); - - if (_activeSubscriptions.ContainsKey(eventName)) - throw new NotAllowedError($"Event {eventName} has an already active subscription."); - - if (!_td.Events.TryGetValue(eventName, out eventAffordance)) - throw new NotFoundError($"Event {eventName} was not found in TD. TD Title: {_td.Title}."); - - Form[] forms = eventAffordance.Forms; - Form form = FindSuitableForm(forms, "subscribeevent", "http", options, "application/json", "longpoll"); - - if (options.HasValue && options.Value.uriVariables != null) - form = HandleUriVariables(form, options.Value.uriVariables); - - subscription = new Subscription(Subscription.SubscriptionType.Event, eventName, eventAffordance, form, this); - try - { - var task = Task.Run(async () => - { - while (subscription.Active) - { - subscription.StopObservation += (sender, args) => subscription.tokenSource.Cancel(); - HttpResponseMessage interactionResponse = await _consumer.httpClient.GetAsync(form.Href, subscription.CancellationToken); - interactionResponse.EnsureSuccessStatusCode(); - Stream responseStream = await interactionResponse.Content.ReadAsStreamAsync(); - InteractionOutput output = new InteractionOutput(eventAffordance.Data, form, responseStream); - listener.Invoke(output); - } - }, subscription.CancellationToken); - } - - catch (TaskCanceledException) - { - Console.WriteLine($"Unsubscribed to event: {eventName} of TD {_td.Title}"); - } - - catch (HttpRequestException e) - { - onerror(e); - } - - return subscription; - } - catch (TypeError e) - { - throw e; - } - catch (NotAllowedError e) - { - Console.WriteLine(e.ToString() + " The subscription is ignored"); - return null; - } - } - - /****************************************************** Utility Methods ******************************************************/ - public ThingDescription GetThingDescription() { return _td; } - - /// - /// Indicates if there are active subscriptions for observations or events - /// - /// there are active subscriptions - public bool HasActiveListeners { get => _activeObservations.Count > 0 || _activeSubscriptions.Count > 0; } - - - /// - /// Add credentials for a Thing with given ID - /// - /// Thing ID - /// - /// - public void AddCredentials(string id, string password) - { - throw new NotImplementedException(); - } - - /// - /// Removes credentials for a Thing with given ID - /// - /// Thing ID - /// - public void RemoveCredentials(string id) - { - throw new NotImplementedException (); - } - - protected Form HandleUriVariables(Form form, Dictionary uriVariavles) - { - var urlTemplate = new UriTemplate(form.Href.OriginalString); - foreach (var variable in uriVariavles) - { - urlTemplate.AddParameter(variable.Key, variable.Value); - } - string extendedUrl = urlTemplate.Resolve(); - form.Href = new Uri(extendedUrl); - Console.WriteLine(form.Href.AbsoluteUri); - return form; - } - - protected Form FindSuitableForm(Form[] forms, string op, string scheme, InteractionOptions? options = null, string contentType = "application/json", string subprotocol = null) - { - Form[] filteredForms = forms; - Form form = null; - if (options != null) - { - if (options.Value.formIndex != null) - { - uint index = options.Value.formIndex.Value; - if (index >= 0 && index < forms.Length) form = forms[index]; - if (op != null && form.Op.Contains(op) && - contentType != null && form.ContentType == contentType && - subprotocol != null && form.Subprotocol == subprotocol && - scheme != form.Href.Scheme && form.Href.Scheme == "scheme") return form; - } - } - - if (op != null) { filteredForms = filteredForms.Where((f) => f.Op.Contains(op)).ToArray(); } - if (scheme != null) { filteredForms = filteredForms.Where((f) => f.Href.Scheme == scheme).ToArray(); } - if (contentType != null) { filteredForms = filteredForms.Where((f) => f.ContentType == contentType).ToArray(); } - if (subprotocol != null) { filteredForms = filteredForms.Where((f) => f.Subprotocol == subprotocol).ToArray(); } - - if (filteredForms.Length > 0) form = filteredForms[0]; - return form; - } - - public void AddObservation(string name, Subscription sub) - { - _activeObservations.Add(name, sub); - } - - public void AddSubscription(string name, Subscription sub) - { - _activeSubscriptions.Add(name, sub); - } - - public void RemoveObservation(string name) - { - _activeObservations.Remove(name); - } - - public void RemoveSubscription(string name) - { - _activeSubscriptions.Remove(name); - } - } - - public class InteractionOutput : IInteractionOutput - { - private readonly Form _form; - public InteractionOutput(Form form) - { - _form = form; - } - public Stream Data => null; - - public bool DataUsed => false; - - public Form Form => _form; - - public IDataSchema Schema => null; - - public Task ArrayBuffer() - { - return null; - } - public Task Value() - { - return null; - } - } - - /// - /// An implementation of - /// - /// output data type - public class InteractionOutput : IInteractionOutput - { - private readonly Form _form; - private readonly Stream _data; - private bool _dataUsed; - private readonly T _value; - private bool _isValueSet; - private readonly IDataSchema _schema; - private readonly JSchema _parsedSchema; - private readonly JsonSerializer _serializer; - public InteractionOutput(DataSchema schema, Form form, Stream data) { - _form = form; - _data = data; - _dataUsed = false; - //_content = content; - _schema = schema; - string schemaString = JsonConvert.SerializeObject(schema, settings: new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); - _parsedSchema = JSchema.Parse(schemaString); - _serializer = new JsonSerializer(); - - } - public InteractionOutput(DataSchema schema, Form form, Stream data, T value) - { - _form = form; - _data = data; - _dataUsed = false; - _value = value; - _isValueSet = true; - _schema = schema; - string schemaString = JsonConvert.SerializeObject(schema, settings: new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); - _parsedSchema = JSchema.Parse(schemaString); - _serializer = new JsonSerializer(); - - } - public InteractionOutput(PropertyAffordance schema, Form form, Stream data) - { - _form = form; - _data = data; - //_content = content; - _schema = schema; - _parsedSchema = JSchema.Parse(schema.OriginalJson); - _serializer = new JsonSerializer(); - - } - public InteractionOutput(PropertyAffordance schema, Form form, Stream data, T value) - { - _form = form; - _data = data; - _value = value; - _isValueSet = true; - _schema = schema; - _parsedSchema = JSchema.Parse(schema.OriginalJson); - _serializer = new JsonSerializer(); - - } - public Stream Data => _data; - - public bool DataUsed => _dataUsed; - - public Form Form => _form; - - public IDataSchema Schema => _schema; - - public async Task ArrayBuffer() - { - Task task = Task.Run(() => - { - if (!_data.CanRead || _dataUsed) - { - throw new NotReadableError(); - } - MemoryStream ms = new MemoryStream(); - _data.CopyTo(ms); - _dataUsed = true; - byte[] arrayBuffer = ms.ToArray(); - return arrayBuffer; - }); - return await task; - } - - public async Task Value() - { - Task task = Task.Run(() => - { - if (!_data.CanRead || _dataUsed) - { - throw new NotReadableError(); - } - StreamReader sr = new StreamReader(_data, Encoding.UTF8); - string valueJson = sr.ReadToEnd(); - _dataUsed = true; - // Intialize validating schema - JsonTextReader reader = new JsonTextReader(new StringReader(valueJson)); - JSchemaValidatingReader validatingReader = new JSchemaValidatingReader(reader) - { - Schema = _parsedSchema - }; - //Add Error listener - IList messages = new List(); - validatingReader.ValidationEventHandler += (o, a) => messages.Add(a.Message); - //Deserialize - T value = _serializer.Deserialize(validatingReader); - - bool isValid = (messages.Count == 0); - if (isValid) - { - return value; - } - else - { - throw new Exception("Schema Validation failed for value of readProperty"); - } - }); - return await task; - } - } - - /// - /// An implementation of - /// - public class Subscription : ISubscription - { - private readonly SubscriptionType _type; - private readonly string _name; - private readonly InteractionAffordance _interaction; - private readonly Form _form; - private readonly SimpleConsumedThing _thing; - private bool _active; - private CancellationToken _cancellationToken; - public CancellationTokenSource tokenSource; - - public event EventHandler StopEvent; - public event EventHandler StopObservation; - - /// - /// Subscription Types - /// - public enum SubscriptionType - { - Event, - Observation - } - - public Subscription(SubscriptionType type, string name, InteractionAffordance interaction, Form form, SimpleConsumedThing thing) - { - _type = type; - _name = name; - _interaction = interaction; - _thing = thing; - _active = true; - tokenSource = new CancellationTokenSource(); - _cancellationToken = tokenSource.Token; - switch(_type) - { - case SubscriptionType.Event: - _thing.AddSubscription(_name, this); - break; - case SubscriptionType.Observation: - _thing.AddObservation(_name, this); - break; - } - - } - - public bool Active => _active; - public CancellationToken CancellationToken => _cancellationToken; - - public async Task Stop(InteractionOptions? options = null) - { - _active = false; - if (_type == SubscriptionType.Event) - { - _thing.RemoveSubscription(_name); - this.StopEvent?.Invoke(this, EventArgs.Empty); - } - if (_type == SubscriptionType.Observation) - { - _thing.RemoveObservation(_name); - this.StopObservation?.Invoke(this, EventArgs.Empty); - } - } - } -} diff --git a/WoT/WoT.csproj b/WoT/WoT.csproj index e3bc739..a94780d 100644 --- a/WoT/WoT.csproj +++ b/WoT/WoT.csproj @@ -48,4 +48,9 @@ \ + + + + + diff --git a/WoT/binding-http/SimpleHTTPClient.cs b/WoT/binding-http/SimpleHTTPClient.cs new file mode 100644 index 0000000..bd27dee --- /dev/null +++ b/WoT/binding-http/SimpleHTTPClient.cs @@ -0,0 +1,125 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Schema; +using System; +using System.Text; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Tavis.UriTemplates; +using WoT.Definitions; +using WoT.Errors; +using System.Threading; +using WoT.ProtocolBindings; + +namespace WoT.Implementation +{ + /// + /// A simple WoT Consumer that is capable of requesting TDs only from HTTP resources und consumes them to generate + /// + public class SimpleHTTPClient : IProtocolClient + { + private readonly JsonSerializer _serializer; + public readonly HttpClient httpClient; + + public string Scheme { get; } = "http"; + + /// + public SimpleHTTPClient() + { + httpClient = new HttpClient(); + _serializer = new JsonSerializer(); + + } + + + public async Task SendGetRequest(Form form) + { + + HttpResponseMessage interactionResponse = await httpClient.GetAsync(form.Href); + interactionResponse.EnsureSuccessStatusCode(); + Stream responseStream = await interactionResponse.Content.ReadAsStreamAsync(); + return responseStream; + + } + + + + public async Task SendGetRequest(Form form, CancellationToken cancellationToken) + { + HttpResponseMessage interactionResponse = await httpClient.GetAsync(form.Href, cancellationToken); + interactionResponse.EnsureSuccessStatusCode(); + Stream responseStream = await interactionResponse.Content.ReadAsStreamAsync(); + return responseStream; + + } + public async Task SendPostRequest(Form form) + { + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, form.Href); + var interactionResponse = await httpClient.SendAsync(message); + interactionResponse.EnsureSuccessStatusCode(); + Stream responseStream = await interactionResponse.Content.ReadAsStreamAsync(); + return responseStream; + } + public async Task SendPostRequest(Form form, U parameters) + { + + string payloadString = JsonConvert.SerializeObject(parameters); + StringContent payload = new StringContent(payloadString, Encoding.UTF8, "application/json"); + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, form.Href); + message.Content = payload; + var interactionResponse = await httpClient.SendAsync(message); + interactionResponse.EnsureSuccessStatusCode(); + Stream responseStream = await interactionResponse.Content.ReadAsStreamAsync(); + return responseStream; + } + + public async Task SendPutRequest(Form form, T value) + { + string payloadString = JsonConvert.SerializeObject(value); + var payload = new StringContent(payloadString, Encoding.UTF8, "application/json"); + var message = new HttpRequestMessage(HttpMethod.Put, form.Href); + message.Content = payload; + HttpResponseMessage interactionResponse = await httpClient.SendAsync(message); + interactionResponse.EnsureSuccessStatusCode(); + + } + + + + + public async Task RequestThingDescription(string url) + { + Uri tdUrl = new Uri(url); + HttpResponseMessage tdResponse = await httpClient.GetAsync(tdUrl); + tdResponse.EnsureSuccessStatusCode(); + Console.WriteLine($"Info: Fetched TD from {url} successfully"); + Console.WriteLine($"Info: Parsing TD"); + HttpContent body = tdResponse.Content; + string tdData = await body.ReadAsStringAsync(); + TextReader reader = new StringReader(tdData); + ThingDescription td = _serializer.Deserialize(reader, typeof(ThingDescription)) as ThingDescription; + Console.WriteLine($"Info: Parsed TD successfully"); + return td; + } + + public async Task RequestThingDescription(Uri tdUrl) + { + if (tdUrl.Scheme != "http") throw new Exception($"The protocol for accessing the TD url {tdUrl.OriginalString} is not HTTP"); + Console.WriteLine($"Info: Fetching TD from {tdUrl.OriginalString}"); + HttpResponseMessage tdResponse = await httpClient.GetAsync(tdUrl); + tdResponse.EnsureSuccessStatusCode(); + Console.WriteLine($"Info: Fetched TD from {tdUrl.OriginalString} successfully"); + Console.WriteLine($"Info: Parsing TD"); + HttpContent body = tdResponse.Content; + string tdData = await body.ReadAsStringAsync(); + TextReader reader = new StringReader(tdData); + ThingDescription td = _serializer.Deserialize(reader, typeof(ThingDescription)) as ThingDescription; + Console.WriteLine($"Info: Parsed TD successfully"); + return td; + } + } + + +} diff --git a/WoT/core/ConsumedThing.cs b/WoT/core/ConsumedThing.cs new file mode 100644 index 0000000..9c6f8ae --- /dev/null +++ b/WoT/core/ConsumedThing.cs @@ -0,0 +1,582 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Schema; +using System; +using System.Text; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Tavis.UriTemplates; +using WoT.Definitions; +using WoT.Errors; +using System.Threading; +using WoT.ProtocolBindings; +using System.Runtime.CompilerServices; +namespace WoT.Implementation +{ + public class ConsumedThing:IConsumedThing + { + + private readonly ThingDescription _td; + private readonly Consumer _consumer; + private readonly Dictionary _activeSubscriptions; + private readonly Dictionary _activeObservations; + + public ConsumedThing(ThingDescription thingDescription, Consumer consumer) + { + this._td = thingDescription; + this._consumer = consumer; + _activeObservations = new Dictionary(); + _activeSubscriptions = new Dictionary(); + + } + + #region property operations + public async Task> ReadProperty(string propertyName, InteractionOptions? options = null) + { + var properties = this._td.Properties; + IProtocolClient protocolClient; + Form form; + // 3. Let interaction be [[td]].properties.propertyName. + if (!properties.TryGetValue(propertyName, out var propertyAffordance)) + { + // 4. If interaction is undefined, reject promise with a NotFoundError and stop. + throw new NotFoundError($"Property {propertyName} not found in TD with ID {_td.Id}"); + } + else + { + if (propertyAffordance.WriteOnly == true) throw new NotAllowedError($"Cannot read writeOnly property {propertyName}"); + ClientAndForm clientAndForm = _consumer.GetClientFor(propertyAffordance.Forms, "readproperty", options); + protocolClient = clientAndForm.protocolClient; + form = clientAndForm.form; + if (options.HasValue && options.Value.uriVariables != null) + form = HandleUriVariables(form, options.Value.uriVariables); + if (form == null || protocolClient == null) throw new NotFoundError($"Could not find form/client that allows reading property {propertyName}"); + Stream responseStream = await protocolClient.SendGetRequest(form); + InteractionOutput output = new InteractionOutput(propertyAffordance, form, responseStream); + return output; + } + } + + public async Task WriteProperty(string propertyName, T value, InteractionOptions? options = null) + { + var properties = this._td.Properties; + IProtocolClient protocolClient; + Form form; + // 3. Let interaction be [[td]].properties.propertyName. + if (!properties.TryGetValue(propertyName, out var propertyAffordance)) + { + // 4. If interaction is undefined, reject promise with a NotFoundError and stop. + throw new Exception($"Property {propertyName} not found in TD with ID {_td.Id}"); + } + else + { + if (propertyAffordance.ReadOnly == true) throw new Exception($"Cannot write readOnly property {propertyName}"); + ClientAndForm clientAndForm = _consumer.GetClientFor(propertyAffordance.Forms, "writeproperty", options); + protocolClient = clientAndForm.protocolClient; + form = clientAndForm.form; + // Handle UriVariables + if (options.HasValue && options.Value.uriVariables != null) form = HandleUriVariables(form, options.Value.uriVariables); + + if (form == null) throw new Exception($"Could not find a form that allows writing property {propertyName}"); + + await protocolClient.SendPutRequest(form, value); + + } + } + + public async Task ObserveProperty(string propertyName, Action> listener, InteractionOptions? options = null) + { + Subscription subscription = null; + PropertyAffordance propertyAffordance = null; + try + { + if (listener.GetType() == typeof(Action)) + throw new TypeError("Listener for property " + propertyName + " specified is not a function."); + + if (_activeObservations.ContainsKey(propertyName)) + throw new NotAllowedError("Property " + propertyName + " has an already active subscription."); + + if (!_td.Properties.TryGetValue(propertyName, out propertyAffordance)) + throw new NotFoundError("Property " + propertyName + " was not found in TD. TD Title:" + _td.Title + "."); + + ConsumedThing consumedThing = this; + Form[] forms = propertyAffordance.Forms; + ClientAndForm clientAndForm = consumedThing._consumer.GetClientFor(forms, "observeproperty", options, "application/json", "longpoll"); + Form form = clientAndForm.form; + IProtocolClient protocolClient = clientAndForm.protocolClient; + + if (options.HasValue && options.Value.uriVariables != null) + form = HandleUriVariables(form, options.Value.uriVariables); + + subscription = new Subscription(Subscription.SubscriptionType.Observation, propertyName, propertyAffordance, form, this); + try + { + var task = Task.Run(async () => + { + while (subscription.Active) + { + subscription.StopObservation += (sender, args) => subscription.tokenSource.Cancel(); + Stream responseStream = await protocolClient.SendGetRequest(form, subscription.CancellationToken); + InteractionOutput output = new InteractionOutput(propertyAffordance, form, responseStream); + listener.Invoke(output); + } + }, subscription.CancellationToken); + + } + catch (TaskCanceledException) + { + Console.WriteLine("Unobserved to property: " + propertyName + " of TD " + _td.Title); + } + return subscription; + } + catch (TypeError e) + { + throw e; + } + catch (NotAllowedError e) + { + Console.WriteLine(e.ToString() + " The subscription is ignored"); + return null; + } + } + + public async Task ObserveProperty(string propertyName, Action> listener, Action onerror, InteractionOptions? options = null) + { + Subscription subscription = null; + PropertyAffordance propertyAffordance = null; + try + { + if (listener.GetType() == typeof(Action)) + throw new TypeError("Listener for property " + propertyName + " specified is not a function."); + + if (_activeObservations.ContainsKey(propertyName)) + throw new NotAllowedError("Property " + propertyName + " has an already active subscription."); + + if (!_td.Properties.TryGetValue(propertyName, out propertyAffordance)) + throw new NotFoundError("Property " + propertyName + " was not found in TD. TD Title:" + _td.Title + "."); + + ConsumedThing consumedThing = this; + Form[] forms = propertyAffordance.Forms; + ClientAndForm clientAndForm = consumedThing._consumer.GetClientFor(forms, "observeproperty", options, "application/json", "longpoll"); + Form form = clientAndForm.form; + IProtocolClient protocolClient = clientAndForm.protocolClient; + + if (options.HasValue && options.Value.uriVariables != null) + form = HandleUriVariables(form, options.Value.uriVariables); + + subscription = new Subscription(Subscription.SubscriptionType.Observation, propertyName, propertyAffordance, form, this); + try + { + var task = Task.Run(async () => + { + while (subscription.Active) + { + subscription.StopObservation += (sender, args) => subscription.tokenSource.Cancel(); + Stream responseStream = await protocolClient.SendGetRequest(form, subscription.CancellationToken); + InteractionOutput output = new InteractionOutput(propertyAffordance, form, responseStream); + listener.Invoke(output); + } + }, subscription.CancellationToken); + } + catch (TaskCanceledException) + { + Console.WriteLine("Unobserved to property: " + propertyName + " of TD " + _td.Title); + } + catch (HttpRequestException e) + { + onerror(e); + } + return subscription; + } + catch (TypeError e) + { + throw e; + } + catch (NotAllowedError e) + { + Console.WriteLine(e.ToString() + " The observation is ignored"); + return null; + } + } + + + #endregion + #region action operations + public async Task InvokeAction(string actionName, InteractionOptions? options = null) + { + var actions = this._td.Actions; + + if (!actions.TryGetValue(actionName, out var actionAffordance)) + { + // 4. If interaction is undefined, reject promise with a NotFoundError and stop. + throw new Exception($"Property {actionName} not found in TD with ID {_td.Id}"); + } + else + { + ClientAndForm clientAndForm = _consumer.GetClientFor(actionAffordance.Forms, null, options, "application/json"); + Form form = clientAndForm.form; + IProtocolClient protocolClient = clientAndForm.protocolClient; + // Handle UriVariables + if (options.HasValue && options.Value.uriVariables != null) form = HandleUriVariables(form, options.Value.uriVariables); + + if (form == null) throw new Exception($"Could not find a form that allows invoking action {actionName}"); + Console.Write(form.Href); + await protocolClient.SendPostRequest(form); + + InteractionOutput output = new InteractionOutput(form); + return output; + } + } + + public async Task InvokeAction(string actionName, U parameters, InteractionOptions? options = null) + { + var actions = this._td.Actions; + if (!actions.TryGetValue(actionName, out var actionAffordance)) + { + // 4. If interaction is undefined, reject promise with a NotFoundError and stop. + throw new Exception($"Property {actionName} not found in TD with ID {_td.Id}"); + } + else + { + // find suitable form + ClientAndForm clientAndForm = _consumer.GetClientFor(actionAffordance.Forms, null, options, "application/json"); + Form form = clientAndForm.form; + IProtocolClient protocolClient = clientAndForm.protocolClient; + // Handle UriVariables + if (options.HasValue && options.Value.uriVariables != null) form = HandleUriVariables(form, options.Value.uriVariables); + + if (form == null) throw new Exception($"Could not find a form that allows reading property {actionName}"); + + await protocolClient.SendPostRequest(form, parameters); + + InteractionOutput output = new InteractionOutput(form); + return output; + } + } + + public async Task> InvokeAction(string actionName, InteractionOptions? options = null) + { + var actions = this._td.Actions; + if (!actions.TryGetValue(actionName, out var actionAffordance)) + { + // 4. If interaction is undefined, reject promise with a NotFoundError and stop. + throw new Exception($"Property {actionName} not found in TD with ID {_td.Id}"); + } + else + { + // find suitable form + ClientAndForm clientAndForm = _consumer.GetClientFor(actionAffordance.Forms, null, options, "application/json"); + Form form = clientAndForm.form; + IProtocolClient protocolClient = clientAndForm.protocolClient; + // Handle UriVariables + if (options.HasValue && options.Value.uriVariables != null) form = HandleUriVariables(form, options.Value.uriVariables); + + if (form == null) throw new Exception($"Could not find a form that allows reading property {actionName}"); + Console.Write(form.Href); + Stream responseStream = await protocolClient.SendPostRequest(form); + InteractionOutput output = new InteractionOutput(actionAffordance.Output, form, responseStream); + return output; + } + } + + public async Task> InvokeAction(string actionName, U parameters, InteractionOptions? options = null) + { + var actions = this._td.Actions; + if (!actions.TryGetValue(actionName, out var actionAffordance)) + { + // 4. If interaction is undefined, reject promise with a NotFoundError and stop. + throw new Exception($"Property {actionName} not found in TD with ID {_td.Id}"); + } + else + { + // find suitable form + ClientAndForm clientAndForm = _consumer.GetClientFor(actionAffordance.Forms, null, options, "application/json"); + Form form = clientAndForm.form; + IProtocolClient protocolClient = clientAndForm.protocolClient; + // Handle UriVariables + if (options.HasValue && options.Value.uriVariables != null) form = HandleUriVariables(form, options.Value.uriVariables); + + Stream responseStream = await protocolClient.SendPostRequest(form, parameters); + InteractionOutput output = new InteractionOutput(actionAffordance.Output, form, responseStream); + return output; + } + } + #endregion + #region event operations + public async Task SubscribeEvent(string eventName, Action listener, InteractionOptions? options = null) + { + Subscription subscription = null; + EventAffordance eventAffordance = null; + try + { + if (listener.GetType() == typeof(Action)) + throw new TypeError($"Listener for event {eventName} specified is not a function."); + + if (_activeSubscriptions.ContainsKey(eventName)) + throw new NotAllowedError($"Event {eventName} has an already active subscription."); + + if (!_td.Events.TryGetValue(eventName, out eventAffordance)) + throw new NotFoundError($"Event {eventName} was not found in TD. TD Title: {_td.Title}."); + + Form[] forms = eventAffordance.Forms; + ClientAndForm clientAndForm = _consumer.GetClientFor(forms, "subscribeevent", options, "application/json", "longpoll"); + Form form = clientAndForm.form; + IProtocolClient protocolClient = clientAndForm.protocolClient; + + if (options.HasValue && options.Value.uriVariables != null) + form = HandleUriVariables(form, options.Value.uriVariables); + + subscription = new Subscription(Subscription.SubscriptionType.Event, eventName, eventAffordance, form, this); + try + { + var task = Task.Run(async () => + { + while (subscription.Active) + { + subscription.StopObservation += (sender, args) => subscription.tokenSource.Cancel(); + await protocolClient.SendGetRequest(form, subscription.CancellationToken); + listener.Invoke(); + } + }, subscription.CancellationToken); + //TODO: await task to for proper use of async method, and test + //await task; + } + catch (TaskCanceledException) + { + Console.WriteLine($"Unsubscribed to event: {eventName} of TD {_td.Title}"); + } + + return subscription; + } + catch (TypeError e) + { + throw e; + } + catch (NotAllowedError e) + { + Console.WriteLine(e.ToString() + " The subscription is ignored"); + return null; + } + } + + public async Task SubscribeEvent(string eventName, Action listener, Action onerror, InteractionOptions? options = null) + { + Subscription subscription = null; + EventAffordance eventAffordance = null; + try + { + if (listener.GetType() == typeof(Action)) + throw new TypeError($"listener for event {eventName} specified is not a function."); + + if (_activeSubscriptions.ContainsKey(eventName)) + throw new NotAllowedError($"Event {eventName} has an already active subscription."); + + if (!_td.Events.TryGetValue(eventName, out eventAffordance)) + throw new NotFoundError($"Event {eventName} was not found in TD. TD Title: {_td.Title}."); + + Form[] forms = eventAffordance.Forms; + ClientAndForm clientAndForm = _consumer.GetClientFor(forms, "subscribeevent", options, "application/json", "longpoll"); + Form form = clientAndForm.form; + IProtocolClient protocolClient = clientAndForm.protocolClient; + + if (options.HasValue && options.Value.uriVariables != null) + form = HandleUriVariables(form, options.Value.uriVariables); + + subscription = new Subscription(Subscription.SubscriptionType.Event, eventName, eventAffordance, form, this); + try + { + var task = Task.Run(async () => + { + while (subscription.Active) + { + subscription.StopObservation += (sender, args) => subscription.tokenSource.Cancel(); + await protocolClient.SendGetRequest(form, subscription.CancellationToken); + listener.Invoke(); + } + }, subscription.CancellationToken); + } + catch (TaskCanceledException) + { + Console.WriteLine($"Unsubscribed to event: {eventName} of TD {_td.Title}"); + } + catch (HttpRequestException e) + { + onerror(e); + } + return subscription; + } + catch (TypeError e) + { + throw e; + } + catch (NotAllowedError e) + { + Console.WriteLine(e.ToString() + " The subscription is ignored"); + return null; + } + } + + public async Task SubscribeEvent(string eventName, Action> listener, InteractionOptions? options = null) + { + Subscription subscription = null; + EventAffordance eventAffordance = null; + try + { + if (listener.GetType() == typeof(Action)) + throw new TypeError($"Listener for event {eventName} specified is not a function."); + + if (_activeSubscriptions.ContainsKey(eventName)) + throw new NotAllowedError($"Event {eventName} has an already active subscription."); + + if (!_td.Events.TryGetValue(eventName, out eventAffordance)) + throw new NotFoundError($"Event {eventName} was not found in TD. TD Title: {_td.Title}."); + + Form[] forms = eventAffordance.Forms; + ClientAndForm clientAndForm = _consumer.GetClientFor(forms, "observeproperty", options, "application/json", "longpoll"); + Form form = clientAndForm.form; + IProtocolClient protocolClient = clientAndForm.protocolClient; + + if (options.HasValue && options.Value.uriVariables != null) + form = HandleUriVariables(form, options.Value.uriVariables); + + subscription = new Subscription(Subscription.SubscriptionType.Event, eventName, eventAffordance, form, this); + try + { + var task = Task.Run(async () => + { + while (subscription.Active) + { + subscription.StopObservation += (sender, args) => subscription.tokenSource.Cancel(); + Stream responseStream = await protocolClient.SendGetRequest(form, subscription.CancellationToken); + InteractionOutput output = new InteractionOutput(eventAffordance.Data, form, responseStream); + listener.Invoke(output); + } + }, subscription.CancellationToken); + } + + catch (TaskCanceledException) + { + Console.WriteLine($"Unsubscribed to event: {eventName} of TD {_td.Title}"); + } + + return subscription; + } + catch (TypeError e) + { + throw e; + } + catch (NotAllowedError e) + { + Console.WriteLine(e.ToString() + " The subscription is ignored"); + return null; + } + } + + public async Task SubscribeEvent(string eventName, Action> listener, Action onerror, InteractionOptions? options = null) + { + Subscription subscription = null; + EventAffordance eventAffordance = null; + try + { + if (listener.GetType() == typeof(Action)) + throw new TypeError($"listener for event {eventName} specified is not a function."); + + if (_activeSubscriptions.ContainsKey(eventName)) + throw new NotAllowedError($"Event {eventName} has an already active subscription."); + + if (!_td.Events.TryGetValue(eventName, out eventAffordance)) + throw new NotFoundError($"Event {eventName} was not found in TD. TD Title: {_td.Title}."); + + Form[] forms = eventAffordance.Forms; + ClientAndForm clientAndForm = _consumer.GetClientFor(forms, "observeproperty", options, "application/json", "longpoll"); + Form form = clientAndForm.form; + IProtocolClient protocolClient = clientAndForm.protocolClient; + + if (options.HasValue && options.Value.uriVariables != null) + form = HandleUriVariables(form, options.Value.uriVariables); + + subscription = new Subscription(Subscription.SubscriptionType.Event, eventName, eventAffordance, form, this); + try + { + var task = Task.Run(async () => + { + while (subscription.Active) + { + subscription.StopObservation += (sender, args) => subscription.tokenSource.Cancel(); + Stream responseStream = await protocolClient.SendGetRequest(form, subscription.CancellationToken); + InteractionOutput output = new InteractionOutput(eventAffordance.Data, form, responseStream); + listener.Invoke(output); + } + }, subscription.CancellationToken); + } + + catch (TaskCanceledException) + { + Console.WriteLine($"Unsubscribed to event: {eventName} of TD {_td.Title}"); + } + + catch (HttpRequestException e) + { + onerror(e); + } + + return subscription; + } + catch (TypeError e) + { + throw e; + } + catch (NotAllowedError e) + { + Console.WriteLine(e.ToString() + " The subscription is ignored"); + return null; + } + } + #endregion + public ThingDescription GetThingDescription() + { + return _td; + } + + + + protected Form HandleUriVariables(Form form, Dictionary uriVariables) + // {"id": "mariz"} + { + var urlTemplate = new UriTemplate(form.Href.OriginalString); + foreach (var variable in uriVariables) + { + urlTemplate.AddParameter(variable.Key, variable.Value); + } + string extendedUrl = urlTemplate.Resolve(); + form.Href = new Uri(extendedUrl); + Console.WriteLine(form.Href.AbsoluteUri); + return form; + } + + public void AddObservation(string name, Subscription sub) + { + _activeObservations.Add(name, sub); + } + + public void AddSubscription(string name, Subscription sub) + { + _activeSubscriptions.Add(name, sub); + } + + public void RemoveObservation(string name) + { + _activeObservations.Remove(name); + } + + public void RemoveSubscription(string name) + { + _activeSubscriptions.Remove(name); + } + + } + + +} \ No newline at end of file diff --git a/WoT/core/Consumer.cs b/WoT/core/Consumer.cs new file mode 100644 index 0000000..9b1ad50 --- /dev/null +++ b/WoT/core/Consumer.cs @@ -0,0 +1,145 @@ +using Newtonsoft.Json.Schema; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using WoT.Definitions; +using WoT.Errors; +using WoT.ProtocolBindings; +using System.Runtime.InteropServices.ComTypes; + +namespace WoT.Implementation +{ + public class Consumer : IConsumer, IRequester + { + /// + /// A simple WoT Consumer that according to the thing + /// + /// + private Dictionary _clients; + private IConsumedThing _consumedThing; + + public Consumer() + { + _clients = new Dictionary(); + } + public IConsumedThing Consume(ThingDescription td) + { + ConsumedThing consumedThing = new ConsumedThing(td, this); + _consumedThing = consumedThing; + return _consumedThing; + } + + + + + public Task RequestThingDescription(string url) + { + Uri tdURL = new Uri(url); + return RequestThingDescription(tdURL); + } + + public async Task RequestThingDescription(Uri tdUrl) + { + + Console.WriteLine($"Info: Fetching TD from {tdUrl.OriginalString}"); + Form tdForm = new Form(); + tdForm.Href = tdUrl; + Stream responseStream = await GetClientFor(tdUrl).SendGetRequest(tdForm); + Console.WriteLine($"Info: Fetched TD from {tdUrl.OriginalString} successfully"); + Console.WriteLine($"Info: Parsing TD"); + StreamReader streamReader = new StreamReader(responseStream); + JsonTextReader jsonReader = new JsonTextReader(streamReader); + JsonSerializer serializer = new JsonSerializer(); + ThingDescription td = serializer.Deserialize(jsonReader); + Console.WriteLine($"Info: Parsed TD successfully"); + return td; + + } + + + + public void AddClient(IProtocolClient protocolClient) + { + _clients.Add(protocolClient.Scheme, protocolClient); + } + + public void RemoveClient(string scheme) + { + _clients.Remove(scheme); + } + public ClientAndForm GetClientFor(Form[] forms, string op, InteractionOptions? options = null, string contentType = "application/json", string subprotocol = "null") + { + Form form = null; + IProtocolClient protocolClient = null; + + if (options != null && options.Value.formIndex.HasValue) + { + uint formIndex = options.Value.formIndex.Value; + if (formIndex < forms.Length) form = forms[formIndex]; + if ((op != null && !form.Op.Contains(op)) || + (contentType != null && form.ContentType != contentType) || + (subprotocol != "null" && form.Subprotocol != subprotocol)) + throw new NotFoundError($"Form at index {formIndex} does not support the given specifications."); + else if (!this._clients.TryGetValue(form.Href.Scheme, out var pc)) + { + throw new NotFoundError($"No Protocol Client for Form with href \"{form.Href}\"."); + } + else + { + protocolClient = pc; + } + } + else + { + Form[] filteredForms = forms; + if (op != null) { filteredForms = filteredForms.Where((f) => f.Op.Contains(op)).ToArray(); } + if (contentType != null) { filteredForms = filteredForms.Where((f) => f.ContentType == contentType).ToArray(); } + if (subprotocol != "null") { + filteredForms = filteredForms.Where((f) => f.Subprotocol == subprotocol).ToArray(); + } + if (filteredForms.Length == 0) + { + throw new NotFoundError($"No suitable form found for the given specifications."); + } + + bool foundMatchingScheme = false; + foreach (Form f in filteredForms) + { + if (this._clients.TryGetValue(f.Href.Scheme, out var pc)) + { + form = f; + protocolClient = pc; + foundMatchingScheme = true; + break; + } + } + if (!foundMatchingScheme) + { + throw new NotFoundError($"No Protocol Client found for the given input forms."); + } + } + + return new ClientAndForm(protocolClient, form); + } + + public IProtocolClient GetClientFor(Uri href) + { + if (!this._clients.TryGetValue(href.Scheme, out var pc)) + { + throw new NotFoundError($"No Protocol Client for href \"{href.OriginalString}\"."); + } + else + { + return pc; + } + } + + + } +} \ No newline at end of file diff --git a/WoT/WoT-Definitions.cs b/WoT/core/Definitions.cs similarity index 99% rename from WoT/WoT-Definitions.cs rename to WoT/core/Definitions.cs index 55e6966..83cd3a9 100644 --- a/WoT/WoT-Definitions.cs +++ b/WoT/core/Definitions.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WoT.TDHelpers; +using WoT.ProtocolBindings; namespace WoT.Definitions { @@ -430,7 +431,7 @@ public class DataSchema : IDataSchema { /// public DataSchema() { } - + [JsonProperty("@type")] public string[] AtType { get; set; } public string Title { get; set; } @@ -442,7 +443,7 @@ public DataSchema() { } public string Unit { get; set; } public IDataSchema[] OneOf { get; set; } public object[] Enum { get; set; } - public bool ReadOnly { get; set; } + public bool ReadOnly { get; set; } public bool WriteOnly { get; set; } public string Format { get; set; } public string Type { get; } @@ -500,7 +501,7 @@ public NumberSchema() { } public double? MultipleOf { get; set; } public new double? Const { get; set; } public new double? Default { get; set; } - public new double[] Enum { get; set; } + public new double[] Enum { get; set; } } /// @@ -948,6 +949,14 @@ public Form() { } } + public struct ClientAndForm + { + public IProtocolClient protocolClient { get; } + public Form form { get; } + + public ClientAndForm(IProtocolClient protocolClient, Form form) { this.protocolClient = protocolClient; this.form = form; } + } + /// /// A Helper Subclass of for properties used for serialization /// diff --git a/WoT/Errors.cs b/WoT/core/Errors.cs similarity index 100% rename from WoT/Errors.cs rename to WoT/core/Errors.cs diff --git a/WoT/core/InteractionOutput.cs b/WoT/core/InteractionOutput.cs new file mode 100644 index 0000000..aa1fada --- /dev/null +++ b/WoT/core/InteractionOutput.cs @@ -0,0 +1,163 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Schema; +using System; +using System.Text; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Tavis.UriTemplates; +using WoT.Definitions; +using WoT.Errors; +using System.Threading; +namespace WoT.Implementation +{ + public class InteractionOutput : IInteractionOutput + { + private readonly Form _form; + public InteractionOutput(Form form) + { + _form = form; + } + public Stream Data => null; + + public bool DataUsed => false; + + public Form Form => _form; + + public IDataSchema Schema => null; + + public Task ArrayBuffer() + { + return null; + } + public Task Value() + { + return null; + } + } + + /// + /// An implementation of + /// + /// output data type + public class InteractionOutput : IInteractionOutput + { + private readonly Form _form; + private readonly Stream _data; + private bool _dataUsed; + private readonly T _value; + private bool _isValueSet; + private readonly IDataSchema _schema; + private readonly JSchema _parsedSchema; + private readonly JsonSerializer _serializer; + public InteractionOutput(DataSchema schema, Form form, Stream data) + { + _form = form; + _data = data; + _dataUsed = false; + //_content = content; + _schema = schema; + string schemaString = JsonConvert.SerializeObject(schema, settings: new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + _parsedSchema = JSchema.Parse(schemaString); + _serializer = new JsonSerializer(); + + } + public InteractionOutput(DataSchema schema, Form form, Stream data, T value) + { + _form = form; + _data = data; + _dataUsed = false; + _value = value; + _isValueSet = true; + _schema = schema; + string schemaString = JsonConvert.SerializeObject(schema, settings: new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + _parsedSchema = JSchema.Parse(schemaString); + _serializer = new JsonSerializer(); + + } + public InteractionOutput(PropertyAffordance schema, Form form, Stream data) + { + _form = form; + _data = data; + //_content = content; + _schema = schema; + _parsedSchema = JSchema.Parse(schema.OriginalJson); + _serializer = new JsonSerializer(); + + } + public InteractionOutput(PropertyAffordance schema, Form form, Stream data, T value) + { + _form = form; + _data = data; + _value = value; + _isValueSet = true; + _schema = schema; + _parsedSchema = JSchema.Parse(schema.OriginalJson); + _serializer = new JsonSerializer(); + + } + public Stream Data => _data; + + public bool DataUsed => _dataUsed; + + public Form Form => _form; + + public IDataSchema Schema => _schema; + + public async Task ArrayBuffer() + { + Task task = Task.Run(() => + { + if (!_data.CanRead || _dataUsed) + { + throw new NotReadableError(); + } + MemoryStream ms = new MemoryStream(); + _data.CopyTo(ms); + _dataUsed = true; + byte[] arrayBuffer = ms.ToArray(); + return arrayBuffer; + }); + return await task; + } + + public async Task Value() + { + Task task = Task.Run(() => + { + if (!_data.CanRead || _dataUsed) + { + throw new NotReadableError(); + } + StreamReader sr = new StreamReader(_data, Encoding.UTF8); + string valueJson = sr.ReadToEnd(); + _dataUsed = true; + // Intialize validating schema + JsonTextReader reader = new JsonTextReader(new StringReader(valueJson)); + JSchemaValidatingReader validatingReader = new JSchemaValidatingReader(reader) + { + Schema = _parsedSchema + }; + //Add Error listener + IList messages = new List(); + validatingReader.ValidationEventHandler += (o, a) => messages.Add(a.Message); + //Deserialize + T value = _serializer.Deserialize(validatingReader); + + bool isValid = (messages.Count == 0); + if (isValid) + { + return value; + } + else + { + throw new Exception("Schema Validation failed for value of readProperty"); + } + }); + return await task; + } + } + +} \ No newline at end of file diff --git a/WoT/core/ProtocolBindings.cs b/WoT/core/ProtocolBindings.cs new file mode 100644 index 0000000..1278456 --- /dev/null +++ b/WoT/core/ProtocolBindings.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using WoT.Definitions; +namespace WoT.ProtocolBindings +{ + + public interface IProtocolClient + { + string Scheme { get; } + Task SendGetRequest(Form form); + Task SendGetRequest(Form form, CancellationToken cancellationToken); + Task SendPostRequest(Form form); + Task SendPostRequest(Form form, U parameters); + Task SendPutRequest(Form form, T value); + + Task RequestThingDescription(string url); + + } + + +} \ No newline at end of file diff --git a/WoT/core/Subscription.cs b/WoT/core/Subscription.cs new file mode 100644 index 0000000..c022d0a --- /dev/null +++ b/WoT/core/Subscription.cs @@ -0,0 +1,82 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Schema; +using System; +using System.Text; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Tavis.UriTemplates; +using WoT.Definitions; +using WoT.Errors; +using System.Threading; +namespace WoT.Implementation +{ + /// + /// An implementation of + /// + public class Subscription : ISubscription + { + private readonly SubscriptionType _type; + private readonly string _name; + private readonly InteractionAffordance _interaction; + private readonly Form _form; + private readonly ConsumedThing _thing; + private bool _active; + private CancellationToken _cancellationToken; + public CancellationTokenSource tokenSource; + + public event EventHandler StopEvent; + public event EventHandler StopObservation; + + /// + /// Subscription Types + /// + public enum SubscriptionType + { + Event, + Observation + } + + public Subscription(SubscriptionType type, string name, InteractionAffordance interaction, Form form, ConsumedThing thing) + { + _type = type; + _name = name; + _interaction = interaction; + _thing = thing; + _active = true; + tokenSource = new CancellationTokenSource(); + _cancellationToken = tokenSource.Token; + switch (_type) + { + case SubscriptionType.Event: + _thing.AddSubscription(_name, this); + break; + case SubscriptionType.Observation: + _thing.AddObservation(_name, this); + break; + } + + } + + public bool Active => _active; + public CancellationToken CancellationToken => _cancellationToken; + + public async Task Stop(InteractionOptions? options = null) + { + _active = false; + if (_type == SubscriptionType.Event) + { + _thing.RemoveSubscription(_name); + this.StopEvent?.Invoke(this, EventArgs.Empty); + } + if (_type == SubscriptionType.Observation) + { + _thing.RemoveObservation(_name); + this.StopObservation?.Invoke(this, EventArgs.Empty); + } + } + } + +} \ No newline at end of file diff --git a/WoT/TDHelpers.cs b/WoT/core/TDHelpers.cs similarity index 100% rename from WoT/TDHelpers.cs rename to WoT/core/TDHelpers.cs diff --git a/WoT/WoT.cs b/WoT/core/WoT.cs similarity index 95% rename from WoT/WoT.cs rename to WoT/core/WoT.cs index a4bd9ee..403bb65 100644 --- a/WoT/WoT.cs +++ b/WoT/core/WoT.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using WoT.Definitions; using WoT.Errors; +using WoT.ProtocolBindings; namespace WoT { @@ -18,7 +19,23 @@ public interface IConsumer /// TD of the client Thing /// Task that resolves with an object implementing interface /// WoT Scripting API - Task Consume(ThingDescription td); + + + //AddClientFactory(string scheme); + //RemoveClientFactory(string scheme); + /// + /// Returns the and available to the object + /// from the given forms, schemes and specifications. + /// + /// Protocol Client with matching Scheme + ClientAndForm GetClientFor(Form[] forms, string op, InteractionOptions? options = null, string contentType = "application/json", string subprotocol = "null"); + IProtocolClient GetClientFor(Uri href); + void AddClient(IProtocolClient protocolClient); + + void RemoveClient(string scheme); + + + IConsumedThing Consume(ThingDescription td); } /// @@ -304,6 +321,7 @@ public interface IConsumedThing /// TD of Consumed Thing ThingDescription GetThingDescription(); + } /// @@ -348,4 +366,6 @@ public struct InteractionOptions public object data; } + + }