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