Sort filter using xpath result for sort criteria
[metaproxy-moved-to-github.git] / src / filter_sort.cpp
1 /* This file is part of Metaproxy.
2    Copyright (C) 2005-2012 Index Data
3
4 Metaproxy is free software; you can redistribute it and/or modify it under
5 the terms of the GNU General Public License as published by the Free
6 Software Foundation; either version 2, or (at your option) any later
7 version.
8
9 Metaproxy is distributed in the hope that it will be useful, but WITHOUT ANY
10 WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17 */
18
19 #include "config.hpp"
20 #include "filter_sort.hpp"
21 #include <metaproxy/package.hpp>
22 #include <metaproxy/util.hpp>
23
24 #include <yaz/diagbib1.h>
25 #include <yaz/copy_types.h>
26 #include <yaz/log.h>
27 #include <yaz/oid_std.h>
28 #include <libxml/xpath.h>
29 #include <libxml/xpathInternals.h>
30
31 #include <boost/thread/mutex.hpp>
32 #include <boost/thread/condition.hpp>
33
34 #include <yaz/zgdu.h>
35
36 namespace mp = metaproxy_1;
37 namespace yf = mp::filter;
38
39 namespace metaproxy_1 {
40     namespace filter {
41         class Sort::Impl {
42             friend class Frontend;
43         public:
44             Impl();
45             ~Impl();
46             void process(metaproxy_1::Package & package);
47             void configure(const xmlNode * ptr, bool test_only,
48                            const char *path);
49         private:
50             int m_prefetch;
51             std::string m_xpath_expr;
52             std::string m_namespaces;
53             bool m_ascending;
54             boost::mutex m_mutex;
55             boost::condition m_cond_session_ready;
56             std::map<mp::Session, FrontendPtr> m_clients;
57             FrontendPtr get_frontend(mp::Package &package);
58             void release_frontend(mp::Package &package);
59         };
60         class Sort::Record {
61             friend class RecordList;
62             Z_NamePlusRecord *npr;
63             std::string score;
64             void get_xpath(xmlDoc *doc, const char *namespaces,
65                            const char *expr);
66             bool register_namespaces(xmlXPathContextPtr xpathCtx,
67                                      const char *nsList);
68         public:
69             Record(Z_NamePlusRecord *n, const char *namespaces,
70                    const char *expr);
71             ~Record();
72             bool operator < (const Record &rhs);
73         };
74         class Sort::RecordList : boost::noncopyable {
75             Odr_oid *syntax;
76             std::list<Record> npr_list;
77             mp::odr m_odr;
78             std::string namespaces;
79             std::string xpath_expr;
80         public:
81             void add(Z_NamePlusRecord *s);
82             Z_NamePlusRecord *get(int i);
83             void sort();
84             RecordList(Odr_oid *, std::string namespaces,
85                        std::string xpath_expr);
86             ~RecordList();
87         };
88         class Sort::ResultSet : boost::noncopyable {
89             friend class Frontend;
90             Odr_int hit_count;
91             std::list<RecordListPtr> record_lists;
92         };
93         class Sort::Frontend : boost::noncopyable {
94             friend class Impl;
95             Impl *m_p;
96             bool m_is_virtual;
97             bool m_in_use;
98             std::map<std::string, ResultSetPtr> m_sets;
99             typedef std::map<std::string, ResultSetPtr>::iterator Sets_it;
100             void handle_package(mp::Package &package);
101             void handle_search(mp::Package &package, Z_APDU *apdu_req);
102             void handle_present(mp::Package &package, Z_APDU *apdu_req);
103
104             void handle_records(mp::Package &package,
105                                 Z_APDU *apdu_reqq,
106                                 Z_Records *records,
107                                 Odr_int start_pos,
108                                 ResultSetPtr s,
109                                 Odr_oid *syntax,
110                                 const char *resultSetId);
111         public:
112             Frontend(Impl *impl);
113             ~Frontend();
114         };
115     }
116 }
117
118 static void print_xpath_nodes(xmlNodeSetPtr nodes, FILE* output)
119 {
120     xmlNodePtr cur;
121     int size;
122     int i;
123     
124     assert(output);
125     size = nodes ? nodes->nodeNr : 0;
126     
127     fprintf(output, "Result (%d nodes):\n", size);
128     for (i = 0; i < size; ++i) {
129         assert(nodes->nodeTab[i]);
130         
131         if (nodes->nodeTab[i]->type == XML_NAMESPACE_DECL)
132         {
133             xmlNsPtr ns = (xmlNsPtr)nodes->nodeTab[i];
134             cur = (xmlNodePtr)ns->next;
135             if (cur->ns)
136                 fprintf(output, "= namespace \"%s\"=\"%s\" for node %s:%s\n", 
137                         ns->prefix, ns->href, cur->ns->href, cur->name);
138             else
139                 fprintf(output, "= namespace \"%s\"=\"%s\" for node %s\n", 
140                         ns->prefix, ns->href, cur->name);
141         }
142         else if (nodes->nodeTab[i]->type == XML_ELEMENT_NODE)
143         {
144             cur = nodes->nodeTab[i];        
145             if (cur->ns)
146                 fprintf(output, "= element node \"%s:%s\"\n", 
147                         cur->ns->href, cur->name);
148             else
149                 fprintf(output, "= element node \"%s\"\n",  cur->name);
150         }
151         else
152         {
153             cur = nodes->nodeTab[i];    
154             fprintf(output, "= node \"%s\": type %d\n", cur->name, cur->type);
155         }
156     }
157 }
158
159 bool yf::Sort::Record::register_namespaces(xmlXPathContextPtr xpathCtx,
160                                            const char *nsList)
161 {
162     xmlChar* nsListDup;
163     xmlChar* prefix;
164     xmlChar* href;
165     xmlChar* next;
166     
167     assert(xpathCtx);
168     assert(nsList);
169
170     nsListDup = xmlStrdup((const xmlChar *) nsList);
171     if (!nsListDup)
172         return false;
173     
174     next = nsListDup; 
175     while (next)
176     {
177         /* skip spaces */
178         while (*next == ' ')
179             next++;
180         if (*next == '\0')
181             break;
182
183         /* find prefix */
184         prefix = next;
185         next = (xmlChar *) xmlStrchr(next, '=');
186         if (next == NULL)
187         {
188             xmlFree(nsListDup);
189             return false;
190         }
191         *next++ = '\0'; 
192         
193         /* find href */
194         href = next;
195         next = (xmlChar*)xmlStrchr(next, ' ');
196         if (next)
197             *next++ = '\0';     
198
199         /* do register namespace */
200         if (xmlXPathRegisterNs(xpathCtx, prefix, href) != 0)
201         {
202             xmlFree(nsListDup);
203             return false;
204         }
205     }
206     
207     xmlFree(nsListDup);
208     return true;
209 }
210
211
212
213 void yf::Sort::Record::get_xpath(xmlDoc *doc, const char *namespaces,
214                                  const char *expr)
215 {
216     xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
217     if (xpathCtx)
218     { 
219         register_namespaces(xpathCtx, namespaces);
220         xmlXPathObjectPtr xpathObj =
221             xmlXPathEvalExpression((const xmlChar *) expr, xpathCtx);
222         if (xpathObj)
223         {
224             xmlNodeSetPtr nodes = xpathObj->nodesetval;
225             if (nodes)
226             {
227                 int i;
228                 for (i = 0; i < nodes->nodeNr; i++)
229                 {
230                     std::string content;
231                     xmlNode *ptr = nodes->nodeTab[i];
232                     if (ptr->type == XML_ELEMENT_NODE ||
233                         ptr->type == XML_ATTRIBUTE_NODE)
234                     {
235                         content = mp::xml::get_text(ptr->children);
236                     }
237                     else if (ptr->type == XML_TEXT_NODE)
238                     {
239                         content = mp::xml::get_text(ptr);
240                     }
241                     if (content.c_str())
242                     {
243                         score = content;
244                         break;
245                     }
246                 }
247             }
248             xmlXPathFreeObject(xpathObj);
249         }
250         xmlXPathFreeContext(xpathCtx);
251     }
252 }
253
254 yf::Sort::Record::Record(Z_NamePlusRecord *n,
255                          const char *namespaces,
256                          const char *expr) : npr(n) 
257 {
258     if (npr->which == Z_NamePlusRecord_databaseRecord)
259     {
260         Z_External *ext = npr->u.databaseRecord;
261
262         if (ext->which == Z_External_octet &&
263             !oid_oidcmp(ext->direct_reference, yaz_oid_recsyn_xml))
264         {
265             xmlDoc *doc = xmlParseMemory(
266                 (const char *) ext->u.octet_aligned->buf,
267                 ext->u.octet_aligned->len);
268             if (doc)
269             {
270                 get_xpath(doc, namespaces, expr);
271                 xmlFreeDoc(doc);
272             }
273         }
274     }
275 }
276
277 yf::Sort::Record::~Record()
278 {
279 }
280
281 bool yf::Sort::Record::operator < (const Record &rhs)
282 {
283     if (strcmp(this->score.c_str(), rhs.score.c_str()) < 0)
284         return true;
285     return false;
286 }
287
288 yf::Sort::RecordList::RecordList(Odr_oid *syntax,
289                                  std::string a_namespaces,
290                                  std::string a_xpath_expr)
291     : namespaces(a_namespaces), xpath_expr(a_xpath_expr)
292
293 {
294     if (syntax)
295         this->syntax = odr_oiddup(m_odr, syntax);
296     else
297         this->syntax = 0;
298 }
299
300 yf::Sort::RecordList::~RecordList()
301 {
302     
303 }
304  
305 void yf::Sort::RecordList::add(Z_NamePlusRecord *s)
306 {
307     ODR oi = m_odr;
308     Record record(yaz_clone_z_NamePlusRecord(s, oi->mem),
309                   namespaces.c_str(),
310                   xpath_expr.c_str());
311     npr_list.push_back(record);
312 }
313
314 Z_NamePlusRecord *yf::Sort::RecordList::get(int i)
315 {
316     std::list<Record>::const_iterator it = npr_list.begin();
317     for (; it != npr_list.end(); it++, --i)
318         if (i <= 0)
319             return it->npr;
320     return 0;
321 }
322
323 void yf::Sort::RecordList::sort()
324 {
325     npr_list.sort();
326 }
327
328 yf::Sort::Sort() : m_p(new Impl)
329 {
330 }
331
332 yf::Sort::~Sort()
333 {  // must have a destructor because of boost::scoped_ptr
334 }
335
336 void yf::Sort::configure(const xmlNode *xmlnode, bool test_only,
337                          const char *path)
338 {
339     m_p->configure(xmlnode, test_only, path);
340 }
341
342 void yf::Sort::process(mp::Package &package) const
343 {
344     m_p->process(package);
345 }
346
347
348 yf::Sort::Frontend::Frontend(Impl *impl) : 
349     m_p(impl), m_is_virtual(false), m_in_use(true)
350 {
351 }
352
353 yf::Sort::Frontend::~Frontend()
354 {
355 }
356
357
358 yf::Sort::Impl::Impl() : m_prefetch(20), m_ascending(true)
359 {
360 }
361
362 yf::Sort::Impl::~Impl()
363
364 }
365
366 yf::Sort::FrontendPtr yf::Sort::Impl::get_frontend(mp::Package &package)
367 {
368     boost::mutex::scoped_lock lock(m_mutex);
369
370     std::map<mp::Session,yf::Sort::FrontendPtr>::iterator it;
371     
372     while(true)
373     {
374         it = m_clients.find(package.session());
375         if (it == m_clients.end())
376             break;
377         
378         if (!it->second->m_in_use)
379         {
380             it->second->m_in_use = true;
381             return it->second;
382         }
383         m_cond_session_ready.wait(lock);
384     }
385     FrontendPtr f(new Frontend(this));
386     m_clients[package.session()] = f;
387     f->m_in_use = true;
388     return f;
389 }
390
391 void yf::Sort::Impl::release_frontend(mp::Package &package)
392 {
393     boost::mutex::scoped_lock lock(m_mutex);
394     std::map<mp::Session,yf::Sort::FrontendPtr>::iterator it;
395     
396     it = m_clients.find(package.session());
397     if (it != m_clients.end())
398     {
399         if (package.session().is_closed())
400         {
401             m_clients.erase(it);
402         }
403         else
404         {
405             it->second->m_in_use = false;
406         }
407         m_cond_session_ready.notify_all();
408     }
409 }
410
411 void yf::Sort::Impl::configure(const xmlNode *ptr, bool test_only,
412                                const char *path)
413 {
414     for (ptr = ptr->children; ptr; ptr = ptr->next)
415     {
416         if (ptr->type != XML_ELEMENT_NODE)
417             continue;
418         if (!strcmp((const char *) ptr->name, "config"))
419         {            
420             const struct _xmlAttr *attr;
421             for (attr = ptr->properties; attr; attr = attr->next)
422             {
423                 if (!strcmp((const char *) attr->name, "prefetch"))
424                 {
425                     m_prefetch = mp::xml::get_int(attr->children, -1);
426                     if (m_prefetch < 0)
427                     {
428                         throw mp::filter::FilterException(
429                             "Bad or missing value for attribute " + 
430                             std::string((const char *) attr->name));
431                     }
432                 }
433                 else if (!strcmp((const char *) attr->name, "xpath"))
434                 {
435                     m_xpath_expr = mp::xml::get_text(attr->children);
436                 }
437                 else if (!strcmp((const char *) attr->name, "namespaces"))
438                 {
439                     m_namespaces = mp::xml::get_text(attr->children);
440                 }
441                 else if (!strcmp((const char *) attr->name, "sortorder"))
442                 {
443                     std::string t = mp::xml::get_text(attr->children);
444                     if (t == "ascending")
445                         m_ascending = true;
446                     else if (t == "descending")
447                         m_ascending = false;
448                     else
449                         throw mp::filter::FilterException(
450                             "Bad attribute value " + t + " for attribute " +
451                             std::string((const char *) attr->name));
452
453                 }
454                 else
455                     throw mp::filter::FilterException(
456                         "Bad attribute " +
457                         std::string((const char *) attr->name));
458             }
459         }
460         else
461         {
462             throw mp::filter::FilterException
463                 ("Bad element " 
464                  + std::string((const char *) ptr->name)
465                  + " in sort filter");
466         }
467     }
468     if (m_xpath_expr.length() == 0)
469     {
470         throw mp::filter::FilterException
471             ("Missing xpath attribute for config element in sort filter");
472     }
473
474 }
475
476 void yf::Sort::Frontend::handle_records(mp::Package &package,
477                                         Z_APDU *apdu_req,
478                                         Z_Records *records,
479                                         Odr_int start_pos,
480                                         ResultSetPtr s,
481                                         Odr_oid *syntax,
482                                         const char *resultSetId)
483 {
484     if (records && records->which == Z_Records_DBOSD && start_pos == 1)
485     {
486         Z_NamePlusRecordList *nprl = records->u.databaseOrSurDiagnostics;
487         int i;    // i is number of records fetched in last response
488
489         int pos = 1;
490         RecordListPtr rlp(new RecordList(syntax,
491                                          m_p->m_namespaces.c_str(),
492                                          m_p->m_xpath_expr.c_str()));
493         for (i = 0; i < nprl->num_records; i++, pos++)
494             rlp->add(nprl->records[i]);
495
496         int end_pos = m_p->m_prefetch;
497         if (end_pos > s->hit_count)
498             end_pos = s->hit_count;
499         while (i && pos <= end_pos)
500         {
501             mp::odr odr;
502             i = 0;
503
504             Package present_package(package.session(), package.origin());
505             present_package.copy_filter(package);
506
507             Z_APDU *p_apdu = zget_APDU(odr, Z_APDU_presentRequest);
508             Z_PresentRequest *p_req = p_apdu->u.presentRequest;
509
510             *p_req->resultSetStartPoint = pos;
511             *p_req->numberOfRecordsRequested = end_pos - pos + 1;
512             p_req->preferredRecordSyntax = syntax;
513             p_req->resultSetId = odr_strdup(odr, resultSetId);
514
515             present_package.request() = p_apdu;
516             present_package.move();
517
518             Z_GDU *gdu_res = present_package.response().get();
519             if (gdu_res && gdu_res->which == Z_GDU_Z3950 &&
520                 gdu_res->u.z3950->which == Z_APDU_presentResponse)
521             {
522                 Z_PresentResponse *res = gdu_res->u.z3950->u.presentResponse;
523                 Z_Records *records = res->records;
524                 if (records && records->which == Z_Records_DBOSD)
525                 {
526                     Z_NamePlusRecordList *nprl =
527                         records->u.databaseOrSurDiagnostics;
528                     for (i = 0; i < nprl->num_records; i++, pos++)
529                         rlp->add(nprl->records[i]);
530                 }
531             }
532         }
533         s->record_lists.push_back(rlp);
534         rlp->sort();
535
536         for (i = 0; i < nprl->num_records; i++)
537             if (m_p->m_ascending)
538                 nprl->records[i] = rlp->get(i);
539             else
540                 nprl->records[nprl->num_records - i - 1] = rlp->get(i);
541     }
542 }
543
544 void yf::Sort::Frontend::handle_search(mp::Package &package, Z_APDU *apdu_req)
545 {
546     Z_SearchRequest *req = apdu_req->u.searchRequest;    
547     std::string resultSetId = req->resultSetName;
548     Package b_package(package.session(), package.origin());
549     mp::odr odr;
550
551     b_package.copy_filter(package);
552     Sets_it sets_it = m_sets.find(req->resultSetName);
553     if (sets_it != m_sets.end())
554     {
555         // result set already exist 
556         // if replace indicator is off: we return diagnostic if
557         // result set already exist.
558         if (*req->replaceIndicator == 0)
559         {
560             Z_APDU *apdu = 
561                 odr.create_searchResponse(
562                     apdu_req,
563                     YAZ_BIB1_RESULT_SET_EXISTS_AND_REPLACE_INDICATOR_OFF,
564                     0);
565             package.response() = apdu;
566             return;
567         } 
568         m_sets.erase(resultSetId);
569     }
570     ResultSetPtr s(new ResultSet);
571     m_sets[resultSetId] = s;
572     package.move();
573     Z_GDU *gdu_res = package.response().get();
574     if (gdu_res && gdu_res->which == Z_GDU_Z3950 && gdu_res->u.z3950->which ==
575         Z_APDU_searchResponse)
576     {
577         Z_SearchResponse *res = gdu_res->u.z3950->u.searchResponse;
578         s->hit_count = *res->resultCount;
579         handle_records(b_package, apdu_req, res->records, 1, s,
580                        req->preferredRecordSyntax, resultSetId.c_str());
581     }
582 }
583
584 void yf::Sort::Frontend::handle_present(mp::Package &package, Z_APDU *apdu_req)
585 {
586     Z_PresentRequest *req = apdu_req->u.presentRequest;    
587     std::string resultSetId = req->resultSetId;
588     Package b_package(package.session(), package.origin());
589     mp::odr odr;
590
591     b_package.copy_filter(package);
592     Sets_it sets_it = m_sets.find(resultSetId);
593     if (sets_it == m_sets.end())
594     {
595         Z_APDU *apdu = 
596             odr.create_presentResponse(
597                 apdu_req,
598                 YAZ_BIB1_SPECIFIED_RESULT_SET_DOES_NOT_EXIST,
599                 resultSetId.c_str());
600         package.response() = apdu;
601         return;
602     }
603     package.move();
604     Z_GDU *gdu_res = package.response().get();
605     if (gdu_res && gdu_res->which == Z_GDU_Z3950 && gdu_res->u.z3950->which ==
606         Z_APDU_presentResponse)
607     {
608         Z_PresentResponse *res = gdu_res->u.z3950->u.presentResponse;
609         handle_records(b_package, apdu_req, res->records, 
610                        *req->resultSetStartPoint, sets_it->second,
611                        req->preferredRecordSyntax, resultSetId.c_str());
612     }
613 }
614
615 void yf::Sort::Frontend::handle_package(mp::Package &package)
616 {
617     Z_GDU *gdu = package.request().get();
618     if (gdu && gdu->which == Z_GDU_Z3950)
619     {
620         Z_APDU *apdu_req = gdu->u.z3950;
621         switch (apdu_req->which)
622         {
623         case Z_APDU_searchRequest:
624             handle_search(package, apdu_req);
625             return;
626         case Z_APDU_presentRequest:
627             handle_present(package, apdu_req);
628             return;
629         }
630     }
631     package.move();
632 }
633
634 void yf::Sort::Impl::process(mp::Package &package)
635 {
636     FrontendPtr f = get_frontend(package);
637     Z_GDU *gdu = package.request().get();
638
639     if (f->m_is_virtual)
640     {
641         f->handle_package(package);
642     }
643     else if (gdu && gdu->which == Z_GDU_Z3950 && gdu->u.z3950->which ==
644              Z_APDU_initRequest)
645     {
646         package.move();
647         f->m_is_virtual = true;
648     }
649     else
650         package.move();
651
652     release_frontend(package);
653 }
654
655
656 static mp::filter::Base* filter_creator()
657 {
658     return new mp::filter::Sort;
659 }
660
661 extern "C" {
662     struct metaproxy_1_filter_struct metaproxy_1_filter_sort = {
663         0,
664         "sort",
665         filter_creator
666     };
667 }
668
669
670 /*
671  * Local variables:
672  * c-basic-offset: 4
673  * c-file-style: "Stroustrup"
674  * indent-tabs-mode: nil
675  * End:
676  * vim: shiftwidth=4 tabstop=8 expandtab
677  */
678