Marco Oehler / Mbed OS Lab7
Revision:
0:7c29edfbb0bb
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/HTTPServer.cpp	Fri Jun 12 08:18:46 2020 +0000
@@ -0,0 +1,439 @@
+/*
+ * HTTPServer.cpp
+ * Copyright (c) 2020, ZHAW
+ * All rights reserved.
+ */
+
+#include <algorithm>
+#include "HTTPScript.h"
+#include "HTTPServer.h"
+
+using namespace std;
+
+inline string int2String(int i) {
+    
+    char buffer[32];
+    sprintf(buffer, "%d", i);
+    
+    return string(buffer);
+}
+
+const unsigned int HTTPServer::INPUT_BUFFER_SIZE = 1024;    // size of receive buffer, given in [bytes]
+const int HTTPServer::SOCKET_TIMEOUT = 1000;                // timeout of socket, given in [ms]
+
+/**
+ * Create and initialize an http server object.
+ * @param ethernet a reference to the embedded TCP/IP stack to use.
+ */
+HTTPServer::HTTPServer(EthernetInterface& ethernet) : ethernet(ethernet), thread(osPriorityNormal, STACK_SIZE) {
+    
+    // start thread
+    
+    thread.start(callback(this, &HTTPServer::run));
+}
+
+/**
+ * Delete the http server object.
+ */
+HTTPServer::~HTTPServer() {}
+
+/**
+ * Registers the given script with the http server.
+ * This allows to call a method of this script object
+ * through virtual cgi-bin requests from a remote system.
+ */
+void HTTPServer::add(string name, HTTPScript* httpScript) {
+    
+    httpScriptNames.push_back(name);
+    httpScripts.push_back(httpScript);
+}
+
+/**
+ * Decodes a given URL string into a standard text string.
+ */
+string HTTPServer::urlDecoder(string url) {
+    
+    size_t pos = 0;
+    while ((pos = url.find("+")) != string::npos) url = url.substr(0, pos)+" "+url.substr(pos+1);
+    while ((pos = url.find("%08")) != string::npos) url = url.substr(0, pos)+"\b"+url.substr(pos+3);
+    while ((pos = url.find("%09")) != string::npos) url = url.substr(0, pos)+"\t"+url.substr(pos+3);
+    while ((pos = url.find("%0A")) != string::npos) url = url.substr(0, pos)+"\n"+url.substr(pos+3);
+    while ((pos = url.find("%0D")) != string::npos) url = url.substr(0, pos)+"\r"+url.substr(pos+3);
+    while ((pos = url.find("%20")) != string::npos) url = url.substr(0, pos)+" "+url.substr(pos+3);
+    while ((pos = url.find("%22")) != string::npos) url = url.substr(0, pos)+"\""+url.substr(pos+3);
+    while ((pos = url.find("%23")) != string::npos) url = url.substr(0, pos)+"#"+url.substr(pos+3);
+    while ((pos = url.find("%24")) != string::npos) url = url.substr(0, pos)+"$"+url.substr(pos+3);
+    while ((pos = url.find("%25")) != string::npos) url = url.substr(0, pos)+"%"+url.substr(pos+3);
+    while ((pos = url.find("%26")) != string::npos) url = url.substr(0, pos)+"&"+url.substr(pos+3);
+    while ((pos = url.find("%2B")) != string::npos) url = url.substr(0, pos)+"+"+url.substr(pos+3);
+    while ((pos = url.find("%2C")) != string::npos) url = url.substr(0, pos)+","+url.substr(pos+3);
+    while ((pos = url.find("%2F")) != string::npos) url = url.substr(0, pos)+"/"+url.substr(pos+3);
+    while ((pos = url.find("%3A")) != string::npos) url = url.substr(0, pos)+":"+url.substr(pos+3);
+    while ((pos = url.find("%3B")) != string::npos) url = url.substr(0, pos)+";"+url.substr(pos+3);
+    while ((pos = url.find("%3C")) != string::npos) url = url.substr(0, pos)+"<"+url.substr(pos+3);
+    while ((pos = url.find("%3D")) != string::npos) url = url.substr(0, pos)+"="+url.substr(pos+3);
+    while ((pos = url.find("%3E")) != string::npos) url = url.substr(0, pos)+">"+url.substr(pos+3);
+    while ((pos = url.find("%3F")) != string::npos) url = url.substr(0, pos)+"?"+url.substr(pos+3);
+    while ((pos = url.find("%40")) != string::npos) url = url.substr(0, pos)+"@"+url.substr(pos+3);
+    
+    return url;
+}
+
+/**
+ * This <code>run()</code> method binds the TCP/IP server to a given port number
+ * and enters an infinite loop that waits for http requests and then processes
+ * these requests and returns a response.
+ */
+void HTTPServer::run() {
+    
+    // bind the server to a given port number
+    
+    server.open(&ethernet);
+    server.bind(PORT_NUMBER);
+    server.listen();
+    
+    // enter infinite loop
+    
+    while (true) {
+        
+        TCPSocket* client = server.accept();
+        if (client != NULL) {
+            
+            client->set_blocking(true);
+            client->set_timeout(SOCKET_TIMEOUT); // set timeout of socket
+            
+            // read input
+            
+            char buffer[INPUT_BUFFER_SIZE];
+            int size = client->recv(buffer, sizeof(buffer));
+            
+            if (size > 0) {
+                
+                string input(buffer, size);
+                string header;
+                string output;
+                
+                // parse input
+                
+                if ((input.find("GET") == 0) || (input.find("HEAD") == 0)) {
+                    
+                    if (input.find("cgi-bin") != string::npos) {
+                        
+                        // process script request with arguments
+                        
+                        string script = input.substr(input.find("cgi-bin/")+8, input.find(" ", input.find("cgi-bin/")+8)-input.find("cgi-bin/")-8);
+                        string name;
+                        vector<string> names;
+                        vector<string> values;
+                        
+                        if (script.find("?") != string::npos) {
+                            
+                            name = script.substr(0, script.find("?"));
+                            script = script.substr(script.find("?")+1);
+                            
+                            vector<string> arguments;
+                            while (script.find("&") != string::npos) {
+                                arguments.push_back(script.substr(0, script.find("&")));
+                                script = script.substr(script.find("&")+1);
+                            }
+                            arguments.push_back(script);
+                            
+                            for (int i = 0; i < arguments.size(); i++) {
+                                
+                                if (arguments[i].find("=") != string::npos) {
+                                    
+                                    names.push_back(arguments[i].substr(0, arguments[i].find("=")));
+                                    values.push_back(urlDecoder(arguments[i].substr(arguments[i].find("=")+1)));
+                                    
+                                } else {
+                                    
+                                    names.push_back(arguments[i]);
+                                    values.push_back("");
+                                }
+                            }
+                            
+                        } else {
+                            
+                            name = script;
+                        }
+                        
+                        // look for corresponding script
+                        
+                        for (int i = 0; i < min(httpScriptNames.size(), httpScripts.size()); i++) {
+                            
+                            if (httpScriptNames[i].compare(name) == 0) {
+                                
+                                output  = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n";
+                                output += "<!DOCTYPE html>\r\n";
+                                output += "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\r\n";
+                                output += "<body>\r\n";
+                                output += httpScripts[i]->call(names, values);
+                                output += "</body>\r\n";
+                                output += "</html>\r\n";
+                                
+                                header  = "HTTP/1.1 200 OK\r\n";
+                                header += "Content-Length: "+int2String(output.size())+"\r\n";
+                                header += "Content-Type: text/xml\r\n";
+                                header += "Expires: 0\r\n";
+                                header += "\r\n";
+                                
+                                output = header+output;
+                            }
+                        }
+                        
+                        // requested script was not found on this server
+                        
+                        if ((output).size() == 0) {
+                            
+                            output  = "<!DOCTYPE html>\r\n";
+                            output += "<html lang=\"en\">\r\n";
+                            output += "<head>\r\n";
+                            output += "  <title>404 Not Found</title>\r\n";
+                            output += "  <style type=\"text/css\">\r\n";
+                            output += "    h2 {font-family:Helvetica,Arial,sans-serif; font-size: 24; color:#FFFFFF;}\r\n";
+                            output += "    p {font-family:Helvetica,Arial,sans-serif; font-size: 14; color:#444444;}\r\n";
+                            output += "  </style>\r\n";
+                            output += "</head>\r\n";
+                            output += "<body leftmargin=\"0\" topmargin=\"0\" marginwidth=\"0\" marginheight=\"0\">\r\n";
+                            output += "  <table width=\"100%\" height=\"100%\" border=\"0\" frame=\"void\" cellspacing=\"0\" cellpadding=\"20\">\r\n";
+                            output += "    <tr>\r\n";
+                            output += "      <th width=\"100%\" height=\"30\" bgcolor=\"#0064A6\"><h2>400 Bad Request</h2></th>\r\n";
+                            output += "    </tr>\r\n";
+                            output += "    <tr>\r\n";
+                            output += "      <td valign=\"top\">\r\n";
+                            output += "      <p>The requested script could not be found on this server!</p>\r\n";
+                            output += "      </td>\r\n";
+                            output += "    </tr>\r\n";
+                            output += "  </table>\r\n";
+                            output += "</body>\r\n";
+                            output += "</html>\r\n";
+                            
+                            header  = "HTTP/1.1 404 Not Found\r\n";
+                            header += "Content-Length: "+int2String(output.size())+"\r\n";
+                            header += "Content-Type: text/html\r\n";
+                            header += "\r\n";
+                            
+                            output = header+output;
+                        }
+                        
+                        // write output
+                        
+                        void* address = (void*)(output).c_str();
+                        int offset = 0;
+                        while (offset < (output).size()) offset += client->send((void*)(static_cast<int>(reinterpret_cast<intptr_t>(address))+offset), (output).size()-offset);
+                        
+                    } else {
+                        
+                        // transmit static file
+                        
+                        output  = "<!DOCTYPE html>\r\n";
+                        output += "<html lang=\"en\">\r\n";
+                        output += "<head>\r\n";
+                        output += "  <meta charset=\"utf-8\"/>\r\n";
+                        output += "  <title>Sensor Fusion</title>\r\n";
+                        output += "  <style type=\"text/css\">\r\n";
+                        output += "    html {background-color: #223344;}\r\n";
+                        output += "    h2 {font-family:Helvetica,Arial,sans-serif; font-size: 24; color:#FFFFFF;}\r\n";
+                        output += "    p, td {font-family:Helvetica,Arial,sans-serif; font-size: 16; color:#EEEEEE;}\r\n";
+                        output += "  </style>\r\n";
+                        output += "</head>\r\n";
+                        output += "<body leftmargin=\"0\" topmargin=\"0\" marginwidth=\"0\" marginheight=\"0\">\r\n";
+                        output += "  <script type=\"text/javascript\">\r\n";
+                        output += "  var tiltAngleA = [];\r\n";
+                        output += "  var tiltAngleG = [];\r\n";
+                        output += "  var tiltAngleK = [];\r\n";
+                        output += "  var tiltAngleC = [];\r\n";
+                        output += "  var xmlhttp = null;\r\n";
+                        output += "  var task = window.setInterval(\"update()\", 50);\r\n";
+                        output += "  function update() {\r\n";
+                        output += "    if (window.XMLHttpRequest) {\r\n";
+                        output += "      xmlhttp = new XMLHttpRequest();\r\n";
+                        output += "    } else if (window.ActiveXObject) {\r\n";
+                        output += "      try {\r\n";
+                        output += "        xmlhttp = new ActiveXObject(\"Msxml2.XMLHTTP\");\r\n";
+                        output += "      } catch (exception) {\r\n";
+                        output += "        try {\r\n";
+                        output += "          xmlhttp = new ActiveXObject(\"Microsoft.XMLHTTP\");\r\n";
+                        output += "        } catch (exception) {}\r\n";
+                        output += "      }\r\n";
+                        output += "    }\r\n";
+                        output += "    xmlhttp.onreadystatechange = refresh;\r\n";
+                        output += "    xmlhttp.open(\"GET\", \"/cgi-bin/tiltAngle\");\r\n";
+                        output += "    xmlhttp.send(null);\r\n";
+                        output += "  }\r\n";
+                        output += "  function refresh() {\r\n";
+                        output += "    if (xmlhttp.readyState == 4) {\r\n";
+                        output += "      var xml = xmlhttp.responseXML;\r\n";
+                        output += "      var floatValues = xml.getElementsByTagName(\"float\");\r\n";
+                        output += "      tiltAngleA.push(floatValues[0].childNodes[0].nodeValue/Math.PI*180.0); if (tiltAngleA.length > 400) tiltAngleA.shift();\r\n";
+                        output += "      tiltAngleG.push(floatValues[1].childNodes[0].nodeValue/Math.PI*180.0); if (tiltAngleG.length > 400) tiltAngleG.shift();\r\n";
+                        output += "      tiltAngleK.push(floatValues[2].childNodes[0].nodeValue/Math.PI*180.0); if (tiltAngleK.length > 400) tiltAngleK.shift();\r\n";
+                        output += "      tiltAngleC.push(floatValues[3].childNodes[0].nodeValue/Math.PI*180.0); if (tiltAngleC.length > 400) tiltAngleC.shift();\r\n";
+                        output += "      drawPlot(\"tiltAngle\", tiltAngleA, tiltAngleG, tiltAngleK, tiltAngleC, \"°\");\r\n";
+                        output += "    }\r\n";
+                        output += "  }\r\n";
+                        output += "  function drawPlot(id, value0, value1, value2, value3, valueUnit) {\r\n";
+                        output += "    var canvas = document.getElementById(id);\r\n";
+                        output += "    var width = window.innerWidth-50;\r\n";
+                        output += "    var height = window.innerHeight-50;\r\n";
+                        output += "    canvas.width = 2*width;\r\n";
+                        output += "    canvas.height = 2*height;\r\n";
+                        output += "    canvas.style.width = width+\"px\";\r\n";
+                        output += "    canvas.style.height = height+\"px\";\r\n";
+                        output += "    var ctx = canvas.getContext(\"2d\");\r\n";
+                        output += "    ctx.scale(2,2);\r\n";
+                        output += "    ctx.fillStyle = \"#334455\";\r\n";
+                        output += "    ctx.fillRect(0.5, 0.5, width-1, height-1);\r\n";
+                        output += "    var valueMin = value0[0];\r\n";
+                        output += "    var valueMax = value0[0];\r\n";
+                        output += "    for (i = 0; i < Math.min(Math.min(value0.length, value1.length), value2.length); i++) {\r\n";
+                        output += "      valueMin = Math.min(valueMin, Math.min(value0[i], Math.min(value1[i], Math.min(value2[i], value3[i]))));\r\n";
+                        output += "      valueMax = Math.max(valueMax, Math.max(value0[i], Math.max(value1[i], Math.max(value2[i], value3[i]))));\r\n";
+                        output += "    }\r\n";
+                        output += "    if (valueMax-valueMin < 2.0/Math.pow(10.0, 3)) {\r\n";
+                        output += "      var valueMean = (valueMin+valueMax)/2.0;\r\n";
+                        output += "      valueMin = valueMean-1.0/Math.pow(10.0, 3);\r\n";
+                        output += "      valueMax = valueMean+1.0/Math.pow(10.0, 3);\r\n";
+                        output += "    }\r\n";
+                        output += "    var valueStep = (valueMax-valueMin)/(height/100); if (valueStep < 1.0/Math.pow(10.0, 3)) valueStep = 1.0/Math.pow(10.0, 3);\r\n";
+                        output += "    var valueGain = Math.pow(10.0, Math.floor(Math.log(valueStep)/Math.log(10.0)));\r\n";
+                        output += "    valueStep = valueStep/valueGain;\r\n";
+                        output += "    if (valueStep > 5.0) valueStep = 5.0*valueGain; else if (valueStep > 2.0) valueStep = 2.0*valueGain; else valueStep = valueGain;\r\n";
+                        output += "    valueMin = Math.floor(valueMin/valueStep-0.25)*valueStep;\r\n";
+                        output += "    valueMax = Math.ceil(valueMax/valueStep+0.25)*valueStep;\r\n";
+                        output += "    ctx.strokeStyle = \"#EEEEEE\";\r\n";
+                        output += "    ctx.lineWidth = 1;\r\n";
+                        output += "    ctx.beginPath();\r\n";
+                        output += "    for (valueCurrent = valueMin+valueStep; valueCurrent < valueMax-valueStep/2.0; valueCurrent += valueStep) {\r\n";
+                        output += "      ctx.moveTo(0, (valueMax-valueCurrent)/(valueMax-valueMin)*height+0.5);\r\n";
+                        output += "      ctx.lineTo(width, (valueMax-valueCurrent)/(valueMax-valueMin)*height+0.5);\r\n";
+                        output += "    }\r\n";
+                        output += "    ctx.stroke();\r\n";
+                        
+                        output += "    ctx.font = \"normal 16px sans-serif\";\r\n";
+                        output += "    ctx.textBaseline = \"bottom\";\r\n";
+                        output += "    ctx.fillStyle = \"white\";\r\n";
+                        output += "    for (valueCurrent = valueMin+valueStep; valueCurrent < valueMax-valueStep/2.0; valueCurrent += valueStep) {\r\n";
+                        output += "      ctx.fillText(valueCurrent.toFixed(2), 10.0, (valueMax-valueCurrent)/(valueMax-valueMin)*height-5.0);\r\n";
+                        output += "    }\r\n";
+                        output += "    ctx.fillStyle = \"#FF0000\";\r\n";
+                        output += "    ctx.fillText(\"Tilt Angle from Accelerometer: \"+(value0[value0.length-1]*1.0).toFixed(3)+\" [\"+valueUnit+\"]\", width/2.0, 40);\r\n";
+                        output += "    ctx.fillStyle = \"#FFFF00\";\r\n";
+                        output += "    ctx.fillText(\"Tilt Angle from Gyro: \"+(value1[value1.length-1]*1.0).toFixed(3)+\" [\"+valueUnit+\"]\", width/2.0, 65);\r\n";
+                        output += "    ctx.fillStyle = \"#00FF00\";\r\n";
+                        output += "    ctx.fillText(\"Tilt Angle from Kalman-Filter: \"+(value2[value2.length-1]*1.0).toFixed(3)+\" [\"+valueUnit+\"]\", width/2.0, 90);\r\n";
+                        output += "    ctx.fillStyle = \"#00AA00\";\r\n";
+                        output += "    ctx.fillText(\"Tilt Angle from Complementary Filter: \"+(value3[value3.length-1]*1.0).toFixed(3)+\" [\"+valueUnit+\"]\", width/2.0, 115);\r\n";
+                        
+                        output += "    ctx.strokeStyle = \"#FF0000\";\r\n";
+                        output += "    ctx.lineWidth = 2;\r\n";
+                        output += "    ctx.beginPath();\r\n";
+                        output += "    ctx.moveTo(0, (valueMax-value0[0])/(valueMax-valueMin)*height+0.5);\r\n";
+                        output += "    for (i = 1; i < value0.length; i++) {\r\n";
+                        output += "      ctx.lineTo(i/(value0.length-1)*width, (valueMax-value0[i])/(valueMax-valueMin)*height+0.5);\r\n";
+                        output += "    }\r\n";
+                        output += "    ctx.stroke();\r\n";
+                        
+                        output += "    ctx.strokeStyle = \"#FFFF00\";\r\n";
+                        output += "    ctx.lineWidth = 2;\r\n";
+                        output += "    ctx.beginPath();\r\n";
+                        output += "    ctx.moveTo(0, (valueMax-value1[0])/(valueMax-valueMin)*height+0.5);\r\n";
+                        output += "    for (i = 1; i < value1.length; i++) {\r\n";
+                        output += "      ctx.lineTo(i/(value1.length-1)*width, (valueMax-value1[i])/(valueMax-valueMin)*height+0.5);\r\n";
+                        output += "    }\r\n";
+                        output += "    ctx.stroke();\r\n";
+                        
+                        output += "    ctx.strokeStyle = \"#00FF00\";\r\n";
+                        output += "    ctx.lineWidth = 2;\r\n";
+                        output += "    ctx.beginPath();\r\n";
+                        output += "    ctx.moveTo(0, (valueMax-value2[0])/(valueMax-valueMin)*height+0.5);\r\n";
+                        output += "    for (i = 1; i < value2.length; i++) {\r\n";
+                        output += "      ctx.lineTo(i/(value2.length-1)*width, (valueMax-value2[i])/(valueMax-valueMin)*height+0.5);\r\n";
+                        output += "    }\r\n";
+                        output += "    ctx.stroke();\r\n";
+                        
+                        output += "    ctx.strokeStyle = \"#00AA00\";\r\n";
+                        output += "    ctx.lineWidth = 2;\r\n";
+                        output += "    ctx.beginPath();\r\n";
+                        output += "    ctx.moveTo(0, (valueMax-value3[0])/(valueMax-valueMin)*height+0.5);\r\n";
+                        output += "    for (i = 1; i < value3.length; i++) {\r\n";
+                        output += "      ctx.lineTo(i/(value3.length-1)*width, (valueMax-value3[i])/(valueMax-valueMin)*height+0.5);\r\n";
+                        output += "    }\r\n";
+                        output += "    ctx.stroke();\r\n";
+                        
+                        output += "    ctx.strokeStyle = \"white\";\r\n";
+                        output += "    ctx.lineWidth = 1;\r\n";
+                        output += "    ctx.strokeRect(0.5, 0.5, width-1, height-1);\r\n";
+                        output += "  }\r\n";
+                        output += "  </script>";
+                        output += "  <table width=\"100%\" height=\"100%\" border=\"0\" frame=\"void\" cellspacing=\"20\" cellpadding=\"0\">\r\n";
+                        output += "    <tr>\r\n";
+                        output += "      <th><canvas id=\"tiltAngle\"></canvas></th>\r\n";
+                        output += "    </tr>\r\n";
+                        output += "  </table>\r\n";
+                        output += "</body>\r\n";
+                        output += "</html>\r\n";
+                        
+                        header  = "HTTP/1.1 404 Not Found\r\n";
+                        header += "Content-Length: "+int2String(output.size())+"\r\n";
+                        header += "Content-Type: text/html\r\n";
+                        header += "\r\n";
+                        
+                        output = header+output;
+                        
+                        // write output
+                        
+                        void* address = (void*)output.c_str();
+                        int offset = 0;
+                        while (offset < output.size()) offset += client->send((void*)(static_cast<int>(reinterpret_cast<intptr_t>(address))+offset), output.size()-offset);
+                    }
+                    
+                } else {
+                    
+                    // the http method is not known
+                    
+                    output  = "<!DOCTYPE html>\r\n";
+                    output += "<html lang=\"en\">\r\n";
+                    output += "<head>\r\n";
+                    output += "  <title>400 Bad Request</title>\r\n";
+                    output += "  <style type=\"text/css\">\r\n";
+                    output += "    h2 {font-family:Helvetica,Arial,sans-serif; font-size: 24; color:#FFFFFF;}\r\n";
+                    output += "    p {font-family:Helvetica,Arial,sans-serif; font-size: 14; color:#444444;}\r\n";
+                    output += "  </style>\r\n";
+                    output += "</head>\r\n";
+                    output += "<body leftmargin=\"0\" topmargin=\"0\" marginwidth=\"0\" marginheight=\"0\">\r\n";
+                    output += "  <table width=\"100%\" height=\"100%\" border=\"0\" frame=\"void\" cellspacing=\"0\" cellpadding=\"20\">\r\n";
+                    output += "    <tr>\r\n";
+                    output += "      <th width=\"100%\" height=\"30\" bgcolor=\"#0064A6\"><h2>400 Bad Request</h2></th>\r\n";
+                    output += "    </tr>\r\n";
+                    output += "    <tr>\r\n";
+                    output += "      <td valign=\"top\">\r\n";
+                    output += "      <p>The requested method is not supported by this server!</p>\r\n";
+                    output += "      </td>\r\n";
+                    output += "    </tr>\r\n";
+                    output += "  </table>\r\n";
+                    output += "</body>\r\n";
+                    output += "</html>\r\n";
+                    
+                    header  = "HTTP/1.1 400 Bad Request\r\n";
+                    header += "Content-Length: "+int2String(output.size())+"\r\n";
+                    header += "Content-Type: text/html\r\n";
+                    header += "\r\n";
+                    
+                    output = header+output;
+                    
+                    // write output
+                    
+                    void* address = (void*)output.c_str();
+                    int offset = 0;
+                    while (offset < output.size()) offset += client->send((void*)(static_cast<int>(reinterpret_cast<intptr_t>(address))+offset), output.size()-offset);
+                }
+            }
+            
+            client->close();
+            
+        } // client != NULL
+        
+    } // infinite while loop
+}
+