Fix PR10: Gcm_input may loose its input message
[gnucomo.git] / src / gcm_input / message.cpp
1
2 /**************************************************************************
3 **  (c) Copyright 2002, Andromeda Technology & Automation
4 ** This is free software; you can redistribute it and/or modify it under the
5 ** terms of the GNU General Public License, see the file COPYING.
6 ***************************************************************************
7 ** MODULE INFORMATION *
8 ***********************
9 **      FILE NAME      : message.cpp
10 **      SYSTEM NAME    : Gnucomo - Gnu Computer Monitoring
11 **      VERSION NUMBER : $Revision: 1.19 $
12 **
13 **  DESCRIPTION      :  Implementation of the message handling classes
14 **
15 **  EXPORTED OBJECTS : 
16 **  LOCAL    OBJECTS : 
17 **  MODULES  USED    :
18 ***************************************************************************
19 **  ADMINISTRATIVE INFORMATION *
20 ********************************
21 **      ORIGINAL AUTHOR : Arjen Baart - arjen@andromeda.nl
22 **      CREATION DATE   : Sep 16, 2002
23 **      LAST UPDATE     : Nov 02, 2007
24 **      MODIFICATIONS   : 
25 **************************************************************************/
26
27 /*****************************
28    $Log: message.cpp,v $
29    Revision 1.19  2011-03-24 10:20:37  arjen
30    Added some debug info
31
32    Revision 1.18  2007/11/03 10:23:53  arjen
33    Handling of parameters is greatly improved.
34    When creating a new parameter from an XML report which is fed into
35    gcm_input, the class definition is used as a template to fill in
36    the default values for the properties.
37    The class is also used as a template when a new property is added
38    to an existing parameter.
39
40    DYNAMIC properties are now handled properly. Instead of making a
41    'changed property' notification, the value is checked against the
42    defined range for the property. An 'out of range' notification
43    is created when this condition is detected.
44
45    Revision 1.17  2005/05/31 05:51:41  arjen
46    Textual changes in parameter notifications
47
48    Revision 1.16  2003/12/04 10:38:09  arjen
49    Major redesign. All input is handled through XML. Raw input data is first
50    transformed into an XML document for further processing.
51    A collection of polymorphic classes handle the transformation of various
52    input formats into XML.
53    Classifying input data is done with a finite improbability calculation.
54
55    Revision 1.15  2003/10/27 11:28:27  arjen
56    Do not add another parameter_notification record is the notification
57    already exists for that parameter.
58
59    Revision 1.14  2003/09/01 06:57:14  arjen
60    Reject log entries that are found to be invalid.
61
62    Revision 1.13  2003/08/16 15:28:45  arjen
63    Fixed a namespace problem
64
65    Revision 1.12  2003/08/11 16:56:16  arjen
66    Different kinds of log files are parsed by a collection of objects
67    of different classes, derived from the base class line_cooker
68    Depending on the message content or the message_type element in
69    XML, one of these objects is selected.
70
71    Logrunner is integrated with gcm_input. Although its functionality
72    is still limited, a connection between logrunner and gcm_input
73    is beginning to form.
74
75    Revision 1.11  2003/08/05 08:15:00  arjen
76    Debug output to the log stream instead of cerr.
77    Fixed namespace problems in XPath searches of the DOM.
78    Moved string utility functions to a separate file.
79
80    Revision 1.10  2003/04/29 09:16:44  arjen
81    Read XML input,
82    Only cooked log entries for now.
83
84    Revision 1.9  2003/03/29 09:04:10  arjen
85    Extract the hostname out of the 'From:' or 'Message-Id:' line
86    of an email header.
87
88    Revision 1.8  2003/03/16 09:42:40  arjen
89    Read IRIX system logs.
90
91    Revision 1.7  2003/02/21 08:08:05  arjen
92    Gcm_input also detects packages that are removed from the system.
93    Determining the version number of a package in a RPM
94    list is improved. Only the last one or two parts of the string that
95    begin with a '-' and a number are considered the version.
96
97    Revision 1.6  2003/02/05 09:37:51  arjen
98    Create notifications when a new package is discovered
99    in a 'rpm -qa' list or when the version of a package is changed.
100
101    Revision 1.4  2002/12/06 22:26:28  arjen
102    Set the value of log.processed to FALSE when inserting a
103    new log entry into the database
104    When a syslog entry arrives from last year, gcm_input subtracts one from the
105    year of arrival to create the year of the log entry.
106    Read output from "rpm -qa" and enter packages in the parameter table.
107
108    Revision 1.3  2002/11/09 08:04:27  arjen
109    Added a reference to the GPL
110
111    Revision 1.2  2002/11/04 10:13:36  arjen
112    Use proper namespace for iostream classes
113
114    Revision 1.1  2002/10/05 10:25:49  arjen
115    Creation of gcm_input and a first approach to a web interface
116
117 *****************************/
118
119 #include <unistd.h>
120
121 #include <fstream>
122 #include <algorithm>
123 #include <libxml/xpath.h>
124 #include <libxml/debugXML.h>
125
126 #include "message.h"
127
128 //#define DEBUG
129
130 extern bool verbose;   /*  Defined in the main application */
131 extern bool testmode;
132 extern bool incremental;
133 extern std::ostream *Log;
134
135 /*   Utility functions   */
136
137 extern String SQL_Escape(String s);
138
139 /*=========================================================================
140 **  NAME           : operator >>
141 **  SYNOPSIS       : bool operator >> (message_buffer &, String &)
142 **  PARAMETERS     : 
143 **  RETURN VALUE   : True if input was available.
144 **
145 **  DESCRIPTION    : Input operator. Read the next line from the message.
146 **
147 **  VARS USED      :
148 **  VARS CHANGED   :
149 **  FUNCTIONS USED :
150 **  SEE ALSO       :
151 **  LAST MODIFIED  : Nov 04, 2002
152 **=========================================================================
153 */
154
155 bool operator >> (message_buffer &b, String &s)
156 {
157    bool   input_ok = false;
158
159    if (b.next_line == b.buffer.end())
160    {
161       String   l;
162
163       if (*(b.input) >> l)
164       {
165          b.buffer.push_back(l);
166
167          //   next_line keeps pointing to the end.
168
169          s = l;
170          input_ok = true;
171       }
172    }
173    else
174    {
175       s = *(b.next_line);
176       b.next_line++;
177       input_ok = true;
178    }
179    return input_ok;
180 }
181
182 /*=========================================================================
183 **  NAME           : client_message
184 **  SYNOPSIS       : client_message(std::istream *in, gnucomo_database db)
185 **  PARAMETERS     : 
186 **  RETURN VALUE   : None
187 **
188 **  DESCRIPTION    : Client message constructor.
189 **
190 **  VARS USED      :
191 **  VARS CHANGED   :
192 **  FUNCTIONS USED :
193 **  SEE ALSO       :
194 **  LAST MODIFIED  : Nov 04, 2002
195 **=========================================================================
196 */
197
198 client_message::client_message(std::istream *in, gnucomo_database db)
199 {
200    input.from(in);
201    database = db;
202
203    hostname = "";
204    mail_header   = false;
205    gpg_encrypted = false;
206    classification = UNKNOWN;
207    xmlDom         = NULL;
208    certainty      = 0.0;
209 }
210
211 static const String unix_date_re("[[:alpha:]]{3} [[:alpha:]]{3} [ 123][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2} [0-9]{4}");
212
213 static const regex re_PGP("-----BEGIN PGP MESSAGE-----");
214 static const regex re_dump("^ *DUMP: Date of this level");
215 static const regex re_rpm("[[:alnum:]+-]+-[0-9][[:alnum:].-]");
216
217 static const regex re_uxmail_from("^From [^ \t]+[ ]+" + unix_date_re);
218 static const regex re_xml_header("xml .*\?>$");
219
220 /*=========================================================================
221 **  NAME           : extractHeader
222 **  SYNOPSIS       : void extractHeader()
223 **  PARAMETERS     : 
224 **  RETURN VALUE   : True if the mandatory header elements are available.
225 **
226 **  DESCRIPTION    : Extract the header information from the XML DOM tree.
227 **
228 **  VARS USED      :
229 **  VARS CHANGED   :
230 **  FUNCTIONS USED :
231 **  SEE ALSO       :
232 **  LAST MODIFIED  : Nov 26, 2003
233 **=========================================================================
234 */
235
236 bool client_message::extractHeader()
237 {
238    xmlNodePtr       root, item;
239    xmlNsPtr         namespaces[1];
240
241    xmlXPathObjectPtr res;
242    xmlXPathContextPtr pathcontext;
243
244    bool header_OK = true;
245
246    root = xmlDocGetRootElement(xmlDom);
247    namespaces[0] = root->ns;
248
249    //TODO Ought to check  root->name and  root->ns->href 
250
251    pathcontext = xmlXPathNewContext(xmlDom);
252    pathcontext->node = xmlDocGetRootElement(xmlDom);
253    pathcontext->namespaces = namespaces;
254    pathcontext->nsNr       = 1;
255
256 #ifdef DEBUG
257    xmlDebugDumpNodeList(stdout, pathcontext->node, 0);
258 #endif
259
260    res = xmlXPathEval((const xmlChar *)"gcmt:header/gcmt:messagetype/text()", pathcontext);
261    if (res->nodesetval != NULL && res->nodesetval->nodeTab != NULL)
262    {
263       item = *res->nodesetval->nodeTab;
264
265       //  Select a line cooker based on the message type.
266
267 #ifdef DEBUG
268       *Log << "Looking for a line cooker for " << item->content << "\n";
269 #endif
270       pan.lc = 0;
271       std::list<xform>::iterator lci = kitchen.begin();
272       while (pan.lc == 0 && lci != kitchen.end())
273       {
274          if (lci->lc->message_type() == (const char *)(item->content))
275          {
276             pan.lc = lci->lc;
277          }
278          lci++;
279       }
280       if (pan.lc == 0)
281       {
282          *Log << "Can not find a line cooker for message type " << item->content << "\n";
283          header_OK = false;
284       }
285    }
286    else
287    {
288       *Log << "Message type not found in XML header.\n";
289       header_OK = false;
290    }
291
292    res = xmlXPathEval((const xmlChar *)"gcmt:header/gcmt:hostname/text()", pathcontext);
293    if (res->nodesetval != NULL && res->nodesetval->nodeTab != NULL)
294    {
295       item = *res->nodesetval->nodeTab;
296       hostname = (const char *)item->content;
297    }
298    else
299    {
300       *Log <<  "Can not determine the hostname where the message came from.\n";
301       header_OK = false;
302    }
303
304    res = xmlXPathEval((const xmlChar *)"gcmt:header/gcmt:service/text()", pathcontext);
305    if (res->nodesetval != NULL && res->nodesetval->nodeTab != NULL)
306    {
307       item = *res->nodesetval->nodeTab;
308       service = (const char *)item->content;
309    }
310    res = xmlXPathEval((const xmlChar *)"gcmt:header/gcmt:time/text()", pathcontext);
311    if (res->nodesetval != NULL && res->nodesetval->nodeTab != NULL)
312    {
313       item = *res->nodesetval->nodeTab;
314       arrival = String((char *)item->content);
315       if (!arrival.proper())
316       {
317          *Log << "Arrival time is not properly stated.\n";
318          header_OK = false;
319       }
320
321    }
322    //xmlDebugDumpNodeList(stdout, *res->nodesetval->nodeTab, 0);
323
324    return header_OK;
325 }
326
327
328 /*=========================================================================
329 **  NAME           : classify
330 **  SYNOPSIS       : double classify(String host, date arriv_d, hour arriv_t, String serv)
331 **  PARAMETERS     : 
332 **  RETURN VALUE   : The certainty with which the message is classified.
333 **
334 **  DESCRIPTION    : 
335 **
336 **  VARS USED      :
337 **  VARS CHANGED   :
338 **  FUNCTIONS USED :
339 **  SEE ALSO       :
340 **  LAST MODIFIED  : Nov 27, 2003
341 **=========================================================================
342 */
343
344 double client_message::classify(String host, UTC arriv, String serv)
345 {
346    String    line;
347
348    hostname    = host;
349    arrival     = arriv;
350    service     = serv;
351
352    const double   epsilon = 0.1;   //  Threshold for uncertainty
353    const double   P       = 0.5;   //  Probability of a wrong match
354
355    double uncertainty;
356
357 #ifdef DEBUG
358    *Log << "Checking for a mail header.\n";
359 #endif
360
361    /*  First, check if the message has a mail header. */
362
363    if (input >> line && line == re_uxmail_from)
364    {
365       String    from_address;
366
367       mail_header = true;
368
369       /*  Skip the mail header until the first empty line.  */
370
371       while (input >> line && line != "")
372       {
373       }
374    }
375    else
376    {
377       //  Push the first line back, we need to read it again.
378       --input;
379
380    }
381
382    /*
383     *  Now that we have the mail header out of the way, try to figure
384     *  out what the content of the message is.
385     */
386
387 #ifdef DEBUG
388    *Log << "Classifying message.\n";
389 #endif
390
391
392    uncertainty = 1.0;
393
394    while (input >> line && uncertainty > epsilon)
395    {
396       if (verbose)
397       {
398          *Log << "  testing: " << line << "\n";
399       }
400
401       if (line == re_PGP)
402       {
403          uncertainty = 0.0;
404          gpg_encrypted = true;
405          *Log << "The message is PGP/GnuPG encrypted.\n";
406       }
407       else
408       {
409          //  Scan the list of line cookers if there is anything familiar.
410
411          std::list<xform>::iterator lci = kitchen.begin();
412          uncertainty = 1.0;
413          while (lci != kitchen.end())
414          {
415             if (lci->lc->check_pattern(line))
416             {
417                //  We have a match; decrease the uncertainty
418
419                lci->uncertainty *= P;
420                if (uncertainty > lci->uncertainty)
421                {
422                   uncertainty = lci->uncertainty;
423                   pan = *lci;
424                }
425                if (verbose)
426                {
427                   *Log << lci->lc->message_type() << " detected with "
428                        << lci->uncertainty << " uncertainty.\n";
429                }
430             }
431             lci++;
432          }
433          classification = COOKER_OBJECT;
434       }
435    }
436
437    input.rewind();
438
439    certainty = 1.0 - uncertainty;
440
441    return certainty;
442 }
443
444 /*=========================================================================
445 **  NAME           : enterXML
446 **  SYNOPSIS       : int enterXML()
447 **  PARAMETERS     : 
448 **  RETURN VALUE   : None
449 **
450 **  DESCRIPTION    : Analyze the DOM tree from the XML input.
451 **                   The DOM tree was previously parsed by readXMLinput().
452 **
453 **  VARS USED      :
454 **  VARS CHANGED   :
455 **  FUNCTIONS USED :
456 **  SEE ALSO       :
457 **  LAST MODIFIED  : Nov 02, 2007
458 **=========================================================================
459 */
460
461 struct param_property
462 {
463    String name;
464    String value;
465    String type;
466    double minimum;
467    double maximum;
468 };
469
470 void client_message::enterXML()
471 {
472    //TODO : return the number of elements that are handled.
473
474    xmlXPathObjectPtr res;
475    xmlXPathContextPtr pathcontext;
476    xmlNsPtr           namespaces[1];
477
478    /*  Try to find the host in the database */
479
480    String objectid;
481    String remark;        //  For notifications
482
483    objectid = database.find_host(hostname);
484    if (objectid == "")
485    {
486       *Log << "Please define the host " << hostname << " in the database.\n";
487       return;
488    }
489    if (verbose)
490    {
491       *Log << "Object id for " << hostname << " is " << objectid << "\n";
492    }
493
494    pathcontext = xmlXPathNewContext(xmlDom);
495    pathcontext->node = xmlDocGetRootElement(xmlDom);
496    namespaces[0] = pathcontext->node->ns;
497    pathcontext->namespaces = namespaces;
498    pathcontext->nsNr       = 1;
499
500    res = xmlXPathEval((const xmlChar *)"gcmt:data/node()", pathcontext);
501
502    if (res->nodesetval != NULL)
503    {
504       //  Find the first child element of the <data> element.
505
506       xmlNodePtr  node = *res->nodesetval->nodeTab;
507       while (node->type != XML_ELEMENT_NODE)
508       {
509          node = node->next;
510       }
511       if (strcmp((char *)node->name, "log") == 0)
512       {
513          int log_entry_counter = 0;
514
515          //  Each child contains a log entry, raw or cooked.
516
517          node = node->children;
518          while (node != NULL)
519          {
520             if (node->type == XML_ELEMENT_NODE)
521             {
522                log_entry_counter++;
523
524                xmlNodePtr  item;
525                String      log_hostname;
526                UTC         log_date;
527                String      raw("");;
528                String      log_service;
529
530                if (verbose)
531                {
532                   *Log << "Log entry " << log_entry_counter << " is " << node->name << "\n";
533                }
534                if (strcmp((char *)node->name, "raw") == 0 && node->children != NULL)
535                {
536                   item = node->children;
537                   if (pan.lc == 0)
538                   {
539                      *Log << "Can not cook this type of <raw> log element.\n";
540                   }
541                   else
542                   {
543                      raw = String((const char *)item->content);
544                      if (pan.lc->cook_this(raw, arrival))
545                      {
546                         log_hostname = pan.lc->hostname();
547                         if (log_hostname == "")
548                         {
549                            log_hostname = hostname;
550                         }
551                         log_service = pan.lc->service();
552                         log_date    = pan.lc->timestamp();
553
554                         if (!log_date.proper())
555                         {
556                            *Log << log_date << " is not a valid timestamp.\n";
557                         }
558                      }
559                      else
560                      {
561                         *Log << "gcm_input WARNING: Not a valid line: " << raw << "\n";
562                         raw = "";
563                      }
564                   }
565                }
566                else if (strcmp((char *)node->name, "cooked") == 0)
567                {
568                   //  Find the parts of the log entry
569
570                   if (verbose)
571                   {
572                      *Log << "Analyzing cooked element.\n";
573                   }
574                   pathcontext->node = node;
575
576                   res = xmlXPathEval((const xmlChar *)"hostname/text()", pathcontext);
577                   if (res->nodesetval != NULL)
578                   {
579                      item = *res->nodesetval->nodeTab;
580                      log_hostname = (const char *)item->content;
581                      if (log_hostname != hostname(0, ~log_hostname))
582                      {
583                         *Log << "Hostname " << log_hostname << " does not match.\n";
584                         log_hostname = "";
585                      }
586                   }
587                   else
588                   {
589                      log_hostname = hostname;
590                   }
591
592                   res = xmlXPathEval((const xmlChar *)"service/text()", pathcontext);
593                   if (res->nodesetval != NULL)
594                   {
595                      item = *res->nodesetval->nodeTab;
596                      log_service = (const char *)item->content;
597                   }
598                   else
599                   {
600                      log_service = service;
601                   }
602
603                   res = xmlXPathEval((const xmlChar *)"timestamp/text()", pathcontext);
604                   if (res->nodesetval != NULL)
605                   {
606                      item = *res->nodesetval->nodeTab;
607                      log_date = String((const char *)item->content);
608                   }
609                   else
610                   {
611                      *Log << "<timestamp> missing from cooked log element nr " << log_entry_counter << ".\n";
612                   }
613
614                   res = xmlXPathEval((const xmlChar *)"raw/text()", pathcontext);
615                   if (res->nodesetval != NULL)
616                   {
617                      item = *res->nodesetval->nodeTab;
618                      raw = String((const char *)item->content);
619                   }
620                   else
621                   {
622                      *Log << "<raw> missing from cooked log element nr " << log_entry_counter << ".\n";
623                   }
624
625                }
626
627                //   Insert a new log record into the database.
628                if (raw != "" && log_hostname != "" && log_date.proper())
629                {
630                   String insertion("insert into log (objectid, servicecode,"
631                         " object_timestamp, timestamp, rawdata, processed) values (");
632
633                   /*   Insert a new record into the log table   */
634
635                   insertion += "'" + objectid + "',";
636                   insertion += "'" + log_service + "',";
637                   insertion += "'" + log_date.format("%Y-%m-%d %T") + "',";
638                   insertion += "'" + arrival.format("%Y-%m-%d %T") + "',";
639                   insertion += "'" + SQL_Escape(raw) + "',FALSE";
640                   insertion += ")";
641
642                   if (testmode)
643                   {
644                      *Log << insertion << "\n";
645                   }
646                   else
647                   {
648                      database.Query(insertion);
649                   }
650
651                }
652                else
653                {
654                   *Log << "Can not insert log element " << raw << ".\n";
655                }
656  
657             }
658             node = node->next;
659          }
660       }
661       else if (strcmp((char *)node->name, "parameters") == 0)
662       {
663          //  Each child contains a parameter entry, with at least one property
664
665          String   qry;
666          String   insertion;
667          String   change_notification("");
668          String   out_of_range_notification("");
669          String   create_notification("");
670          String   remove_notification("");
671          bool     initial_entry = false;
672          String   param_class((const char *)xmlGetProp(node, (const xmlChar *)"class"));
673
674          std::list<param_property>  parameter_template;
675          int                        nr_properties;
676
677          //  Obtain a list of properties from the parameter's class.
678          //  This list is used to create new parameters.
679
680          qry = "select * from parameter_class where name='";
681          qry += param_class + "'";
682          nr_properties = database.Query(qry);
683          for (int i = 0; i < nr_properties; i++)
684          {
685             param_property pp;
686
687             pp.name = database.Field(i, "property_name");
688             pp.type = database.Field(i, "property_type");
689             pp.minimum = database.Field(i, "min");
690             pp.maximum = database.Field(i, "max");
691
692             parameter_template.push_back(pp);
693          }
694
695 #ifdef DEBUG
696          *Log << "Entering a list of " << param_class << " parameters.\n";
697 #endif
698          pathcontext->node = node;
699
700          //  If we don't have any parameters of this class, this will be
701          //  an initial entry.
702
703          qry = "select name from parameter where objectid='";
704          qry += objectid + "' and class='" + param_class + "'";
705          initial_entry = database.Query(qry) == 0;
706
707          node = node->children;
708          while (node != NULL)
709          {
710             if (node->type == XML_ELEMENT_NODE &&
711                 strcmp((char *)node->name, "parameter") == 0)
712             {
713                String param_name((const char *)xmlGetProp(node, (const xmlChar *)"name"));
714
715 #ifdef DEBUG
716                *Log << "Parameter with name " << param_name << "\n";
717 #endif
718                std::list<param_property>  properties;
719                param_property             prop;
720                xmlNodePtr                 item;
721
722                String                     paramid;
723
724
725                //  Collect the parameter's properties.
726
727                item = node->children;
728                while (item != NULL)
729                {
730                   if (item->type == XML_ELEMENT_NODE &&
731                       strcmp((char *)item->name, "property") == 0)
732                   {
733                      prop.name = (const char *)xmlGetProp(item, (const xmlChar *)"name");
734                      if (item->children != NULL)
735                      {
736                         prop.value = (const char *)item->children->content;
737                         properties.push_back(prop);
738                      }
739                      else
740                      {
741                         *Log << "WARNING: Property " << prop.name << " has no value.\n";
742                      }
743                   }
744
745                   //  TODO: Hanlde description element
746
747                   item = item->next;
748                }
749
750                //  Check the parameter in the database.
751
752                std::list<param_property>::iterator pi = properties.begin();
753
754                qry = "select paramid from parameter where objectid='";
755                qry += objectid + "' and class='";
756                qry += param_class + "' and name='";
757                qry += param_name + "'";
758
759                if (database.Query(qry) == 1)
760                {
761                   //  The parameter exists in the database; check all properties.
762
763                   bool  param_changed = false;
764                   bool  out_of_range  = false;
765
766                   paramid = database.Field(0, "paramid");
767                   while (pi != properties.end())
768                   {
769                      qry = "select * from property where paramid='";
770                      qry += paramid + "' and name='";
771                      qry += pi->name + "'";
772                      if (database.Query(qry) == 0)
773                      {
774                         *Log << "Property " << pi->name << " of "
775                              << param_name << " does not exist.\n";
776                         //  Find the property in the template from the class.
777
778                         String property_type("STATIC");
779                         double property_minimum = 0.0;
780                         double property_maximum = 0.0;
781                         std::list<param_property>::iterator ti = parameter_template.begin();
782
783                         while (ti != parameter_template.end() && ti->name != pi->name)
784                         {
785                            ti++;
786                         }
787                         if (ti != parameter_template.end())
788                         {
789                            property_type    = ti->type;
790                            property_minimum = ti->minimum;
791                            property_maximum = ti->maximum;
792                         }
793
794                         insertion = "insert into property (paramid, name, value, type, min, max) values ('";
795                         insertion += paramid + "', '";
796                         insertion += pi->name + "', '";
797                         insertion += pi->value + "', '";
798                         insertion += property_type + "', '";
799                         insertion += String(property_minimum) + "', '";
800                         insertion += String(property_maximum) + "')";
801                         database.Query(insertion);
802
803                         insertion = "insert into history (paramid, modified,";
804                         insertion += " change_nature, changed_property, new_value)";
805                         insertion += " values ('";
806                         insertion += paramid + "', '" + arrival.format("%Y-%m-%d %T")
807                                              + "', 'CREATED', '";
808                         insertion += pi->name + "', '";
809                         insertion += pi->value + "')";
810                         database.Query(insertion);
811                      }
812                      else
813                      {
814                         param_property  stored_property;
815
816                         stored_property.value    = database.Field(0, "value");
817                         stored_property.type     = database.Field(0, "type");
818                         stored_property.minimum  = database.Field(0, "min");
819                         stored_property.maximum  = database.Field(0, "max");
820
821                         if (stored_property.value != pi->value)
822                         {
823                            *Log << "Property " << pi->name << " of "
824                                 << param_name << " is different.\n";
825
826                            insertion = "update property set value='";
827                            insertion += pi->value + "' where paramid='";
828                            insertion += paramid + "' and name='";
829                            insertion += pi->name + "'";
830
831                            database.Query(insertion);
832
833                            insertion = "insert into history (paramid, modified,";
834                            insertion += " change_nature, changed_property, new_value)";
835                            insertion += " values ('";
836                            insertion += paramid + "', '" + arrival.format("%Y-%m-%d %T")
837                                                 + "', 'MODIFIED', '";
838                            insertion += pi->name + "', '";
839                            insertion += pi->value + "')";
840
841                            database.Query(insertion);
842                            if (stored_property.type == "DYNAMIC")
843                            {
844                               // Check the value against the range of the property.
845
846                               double numeric_value = pi->value;
847                               if (numeric_value < stored_property.minimum || numeric_value > stored_property.maximum)
848                               {
849                                  out_of_range = true;
850                               }
851                            }
852                            else
853                            {
854                               //  A STATIC property changed.
855                               param_changed = true;
856                            }
857
858                         }
859                      }
860                      pi++;
861                   }
862
863                   if (param_changed)
864                   {
865                      if (change_notification == "")
866                      {
867                         remark = "Gnucomo detected a different property for parameter(s) ";
868                         change_notification = database.new_notification(objectid,
869                                                            "property modified", remark);
870                      }
871
872                      if (change_notification != "")
873                      {
874                         qry = "select * from parameter_notification where notificationid='";
875                         qry += change_notification + "' and paramid='";
876                         qry += paramid + "'";
877
878                         if (database.Query(qry) == 0)
879                         {
880                            insertion = "insert into parameter_notification";
881                            insertion += " (notificationid, paramid) values ('";
882                            insertion += change_notification + "', '";
883                            insertion += paramid + "')";
884
885                            database.Query(insertion);
886                         }
887                      }
888                      else
889                      {
890                         *Log << "gcm_input ERROR: Cannot create 'property modified' notification.\n";
891                      }
892                   }
893
894                   if (out_of_range)
895                   {
896                      if (out_of_range_notification == "")
897                      {
898                         remark = "Gnucomo detected that a property value is out of range.";
899                         out_of_range_notification = database.new_notification(objectid,
900                                                            "property out of range", remark);
901                      }
902
903                      if (out_of_range_notification != "")
904                      {
905                         qry = "select * from parameter_notification where notificationid='";
906                         qry += out_of_range_notification + "' and paramid='";
907                         qry += paramid + "'";
908
909                         if (database.Query(qry) == 0)
910                         {
911                            insertion = "insert into parameter_notification";
912                            insertion += " (notificationid, paramid) values ('";
913                            insertion += out_of_range_notification + "', '";
914                            insertion += paramid + "')";
915
916                            database.Query(insertion);
917                         }
918                      }
919                      else
920                      {
921                         *Log << "gcm_input ERROR: Cannot create 'property out of range' notification.\n";
922                      }
923                   }
924                }
925                else
926                {
927                   //   The parameter does not exist; create anew.
928
929                   // TODO: Insert description
930
931                   insertion = "insert into parameter (objectid, name, class, description) values ('";
932                   insertion += objectid + "', '" + param_name + "', '" + param_class + "', '')";
933                   database.Query(insertion);
934
935                   qry = "select paramid from parameter where objectid='";
936                   qry += objectid + "' and class='";
937                   qry += param_class + "' and name='";
938                   qry += param_name + "'";
939                   database.Query(qry);
940                   paramid = database.Field(0, "paramid");
941
942                   while (pi != properties.end())
943                   {
944                      //  Find the property in the template from the class.
945
946                      String property_type("STATIC");
947                      double property_minimum = 0.0;
948                      double property_maximum = 0.0;
949                      std::list<param_property>::iterator ti = parameter_template.begin();
950
951                      while (ti != parameter_template.end() && ti->name != pi->name)
952                      {
953                         ti++;
954                      }
955                      if (ti != parameter_template.end())
956                      {
957                         property_type    = ti->type;
958                         property_minimum = ti->minimum;
959                         property_maximum = ti->maximum;
960                      }
961
962                      insertion = "insert into property (paramid, name, value, type, min, max) values ('";
963                      insertion += paramid + "', '";
964                      insertion += pi->name + "', '";
965                      insertion += pi->value + "', '";
966                      insertion += property_type + "', '";
967                      insertion += String(property_minimum) + "', '";
968                      insertion += String(property_maximum) + "')";
969                      database.Query(insertion);
970
971                      insertion = "insert into history (paramid, modified,";
972                      insertion += " change_nature, changed_property, new_value)";
973                      insertion += " values ('";
974                      insertion += paramid + "', '" + arrival.format("%Y-%m-%d %T")
975                                           + "', 'CREATED', '";
976                      insertion += pi->name + "', '";
977                      insertion += pi->value + "')";
978                      database.Query(insertion);
979
980                      pi++;
981                   }
982
983                   if (!initial_entry)
984                   {
985                      if (create_notification == "")
986                      {
987                         remark = "Gnucomo detected new parameter(s) of class " + param_class;
988                         create_notification = database.new_notification(objectid,
989                                                          "parameter created", remark);
990                      }
991                      if (create_notification != "")
992                      {
993                         insertion = "insert into parameter_notification";
994                         insertion += " (notificationid, paramid) values ('";
995                         insertion += create_notification + "', '";
996                         insertion += paramid + "')";
997
998                         database.Query(insertion);
999                      }
1000                      else
1001                      {
1002                         *Log << "gcm_input ERROR: Cannot create 'parameter created' notification.\n";
1003                      }
1004                   }
1005                }
1006             }
1007
1008             node = node->next;
1009          }
1010
1011          if (!incremental)
1012          {
1013             //  Check if any parameters in this class have disappeared.
1014
1015 #ifdef DEBUG
1016             *Log << "Checking for disappeared parameters.\n";
1017 #endif
1018             qry = "select name, paramid from parameter where objectid='";
1019             qry += objectid + "' and class='" + param_class + "'";
1020
1021             int          nr_parameters = database.Query(qry);
1022             pqxx::result parameter_set = database.Result();
1023
1024 #ifdef DEBUG
1025             *Log << nr_parameters << " parameters of class " << param_class << " found in the database.\n";
1026 #endif
1027             for (int i = 0; i < nr_parameters; i++)
1028             {
1029                String XPath;
1030                String param_name, paramid;
1031
1032                param_name = database.Field(parameter_set, i, "name");
1033 #ifdef DEBUG
1034                *Log << "Looking for " << param_name << " in XML tree.\n";
1035 #endif
1036                XPath = "gcmt:parameter[@name='" + param_name + "']";
1037
1038                res = xmlXPathEval((const xmlChar *)(const char *)XPath, pathcontext);
1039 #ifdef DEBUG
1040                *Log << "XPATH result: " << res->type << ".\n";
1041                *Log << "Nr of nodes found: " << res->nodesetval->nodeNr << ".\n";
1042 #endif
1043                if (res->nodesetval->nodeNr == 0)
1044                {
1045                   // The parameter is in the database but not in the report
1046
1047 #ifdef DEBUG
1048                   *Log << "Could not find " << XPath << " in XML tree.\n";
1049 #endif
1050                   paramid = database.Field(parameter_set, i, "paramid");
1051                   qry ="select change_nature from history where paramid='";
1052                   qry += paramid + "' order by modified desc";
1053                   if (database.Query(qry) <= 0)
1054                   {
1055                      *Log << "Database ERROR: no history record for parameter "
1056                           << param_name << ".\n";
1057                   }
1058                   else if (database.Field(0, "change_nature") != "REMOVED")
1059                   {
1060                      if (verbose)
1061                      {
1062                        *Log << "Removing parameter " << param_name << ".\n";
1063                      }
1064
1065                      insertion = "insert into history (paramid, modified, change_nature)";
1066                      insertion += " values ('";
1067                      insertion += paramid + "', '" + arrival.format("%Y-%m-%d %T")
1068                                           + "', 'REMOVED')";
1069
1070                      database.Query(insertion);
1071
1072                      if (remove_notification == "")
1073                      {
1074                         remark = "Gnucomo detected that " + param_class
1075                                + " parameters(s) have disappeared ";
1076                         remove_notification = database.new_notification(objectid,
1077                                                          "parameter removed", remark);
1078                      }
1079
1080                      if (remove_notification != "")
1081                      {
1082                         insertion = "insert into parameter_notification";
1083                         insertion += " (notificationid, paramid) values ('";
1084                         insertion += remove_notification + "', '";
1085                         insertion += paramid + "')";
1086
1087                         database.Query(insertion);
1088                      }
1089                      else
1090                      {
1091                         *Log << "gcm_input ERROR: Cannot create 'parameter removed' notification.\n";
1092                      }
1093                   }
1094                }
1095                else
1096                {
1097 #ifdef DEBUG
1098                   *Log << XPath << " was found in XML tree.\n";
1099 #endif
1100                }
1101             }
1102          }
1103       }
1104       else
1105       {
1106          *Log << "Data element " << node->name << " is not supported.\n";
1107       }
1108    }
1109    else
1110    {
1111       *Log << "Data node not found.\n";
1112    }
1113 }
1114
1115 void client_message::saveXML(String filename)
1116 {
1117    std::ofstream    xmlfile;
1118
1119    xmlfile.open(filename);
1120    xmlfile << xmlBuffer.str();
1121 }
1122
1123 /*=========================================================================
1124 **  NAME           : enter
1125 **  SYNOPSIS       : int enter()
1126 **  PARAMETERS     : 
1127 **  RETURN VALUE   : The number of lines successfully parsed from the input
1128 **                   or a negative number if an error is encountered.
1129 **                   The error return values are:
1130 **                    -1  No database connection.
1131 **                    -2  XML parser error.
1132 **
1133 **  DESCRIPTION    : 
1134 **
1135 **  VARS USED      :
1136 **  VARS CHANGED   :
1137 **  FUNCTIONS USED :
1138 **  SEE ALSO       :
1139 **  LAST MODIFIED  : Sep 22, 2020
1140 **=========================================================================
1141 */
1142
1143 int client_message::enter()
1144 {
1145    int nr_lines = 0;
1146
1147    pan.mf->set_message_type(pan.lc->message_type());
1148
1149    pan.mf->construct_XML(input, xmlBuffer);
1150
1151 #ifdef DEBUG
1152    *Log << "Constructed XML document:\n\n";
1153    *Log << xmlBuffer.str();
1154    *Log << "\n";
1155 #endif
1156
1157    xmlDom = xmlParseMemory(xmlBuffer.str(), xmlBuffer.pcount());
1158
1159    if (database.is_connected())
1160    {
1161       if (xmlDom)
1162       {
1163          if (extractHeader())
1164          {
1165             enterXML();
1166          }
1167       }
1168       else
1169       {
1170          *Log << "XML parser FAILED.\n";
1171          nr_lines = -2;   //  XML parse error
1172       }
1173    }
1174    else
1175    {
1176       *Log << "Database connection FAILED.\n";
1177       nr_lines = -1;  // No database connection
1178    }
1179
1180    return nr_lines;
1181
1182 }