searching._pike
Go to the documentation of this file.
1 /* Copyright (C) 2000-2004 Thomas Bopp, Thorsten Hampel, Ludger Merkens
2  *
3  * This program is free software; you can redistribute it and/or modify
4  * it under the terms of the GNU General Public License as published by
5  * the Free Software Foundation; either version 2 of the License, or
6  * (at your option) any later version.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program; if not, write to the Free Software
15  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16  *
17  * $Id: searching.pike,v 1.9 2010/02/01 23:35:23 nicke Exp $
18  */
19 inherit "/kernel/db_searching";
20 #include <database.h>
21 #include <macros.h>
22 #include <attributes.h>
23 #include <classes.h>
24 #include <access.h>
25 class searching : public db_searching{
26 public:
27 
28 
29 
30 string get_identifier() { return "searching"; }
31 
32 #define DEBUG_QUERY 1
33 
34 #ifdef DEBUG_QUERY
35 #define LOG_QUERY(a, b...) werror(a,b);
36 #else
37 #define LOG_QUERY(a, b...)
38 #endif
39 
40  int searches = 0;
41  object service;
42 
43 class SearchToken {
44 public:
45  string storage;
46  string query;
47  string andor;
48  string key;
49  mixed value;
50 
51  void create(string store, string k, mixed v, string ao) {
52  storage = store;
53  key = k;
54  if ( objectp(v) ) {
55  value = "%" + v->get_object_id();
56  }
57  value = v;
58  andor = ao;
59  }
60  mapping get() {
61  return ([
62  "storage": storage,
63  "key": key,
64  "value": value,
65  ]);
66  }
67 }
68 
69 string compose_value_expression(SearchToken st)
70 {
71  string query;
72  if ( st->storage == STORE_ATTRIB )
73  query = " (ob_ident = 'attrib' and ob_attr='"+st->key+"' and ob_data "+
74  st->value[0]+" '"+st->value[1]+"') ";
75  else if ( st->storage == "doc_data" )
76  query = " (match(doc_data) against (\""+st->value[1]+"\") ";
77  else
78  query = " (ob_data "+ st->value[0]+" '" + st->value[1]+"') ";
79  return query;
80 }
81 
82 string compose_value_expressions(array tokens)
83 {
84  string query = "";
85 
86  SearchToken last = tokens[0];
87  string exp = compose_value_expression(last);
88 
89  for ( int i = 1; i < sizeof(tokens); i++ ) {
90  SearchToken token = tokens[i];
91  query += exp + " or ";
92  exp = compose_value_expression(token);
93  }
94  query += exp;
95 
96  return query;
97 }
98 
99 string get_table_name() { return "ob_data"; }
100 
101 class Result {
102 public:
103 
104  function f;
105  mixed args;
106  void create(function func, mixed a) { f = func; args = a; }
107 
108  void asyncResult(mixed id, mixed result) {
109  f(args, result);
110  }
111 }
112 
113  mapping results = ([ ]);
114 
115 class Search {
116 public:
117 
118  array eq(string|int value) {
119  return ({ "=",
120  intp(value) ? (string)value : fDb()->quote(value)
121  });
122  }
123  array gt(string|int value) {
124  return ({ ">", intp(value) ? (string)value : fDb()->quote(value)});
125  }
126  array lt(string|int value) {
127  return ({ "<", intp(value) ? (string)value : fDb()->quote(value)});
128  }
129  array like(string value) {
130  return ({ "like", intp(value) ? (string)value : fDb()->quote(value)});
131  }
132  array lte(string|int value) {
133  return ({ "<=", intp(value) ? (string)value : fDb()->quote(value)});
134  }
135  array gte(string|int value) {
136  return ({ ">=", intp(value) ? (string)value : fDb()->quote(value)});
137  }
138  array btw(string|int low, string|int up) {
139  return and(gte(low), lte(up));
140  }
141  array or(array a, array b) {
142  return ({ "or", ({a, b}) });
143  }
144  array and(array a, array b) {
145  return ({ "and", ({a, b}) });
146  }
147 
148  array extends = ({ });
149  array limits = ({ });
150  array fulltext = ({ });
151  array classes;
152  int search_id;
153 
154  void create(int sid, array cl) {
155  classes = cl;
156  search_id = sid;
157  service = get_module("ServiceManager");
158  }
159 
160  void search_attribute(string key, string value) {
161  extends += ({ search(STORE_ATTRIB, key, like(value), "or") });
162  }
163 
164  void first_query(string store, string key, mixed value, mixed filter) {
165  extends += ({ search(store, key, value, "or") });
166  }
167 
168  void limit(string store, string key, mixed value) {
169  limits += ({ search(store, key, value, "and") });
170  }
171  void extend(string store, string key, mixed value) {
172  extends += ({ search(store, key, value, "or") });
173  }
174 
175  void extend_ft(string pattern) {
176  fulltext += ({ SearchToken("doc_ft", "doc_data", pattern, "or") });
177  }
178 
179  SearchToken search(string store, string key, mixed value, string andor) {
180  return SearchToken(store, key, value, andor);
181  }
182  mapping serialize_token(SearchToken s) {
183  return s->get();
184  }
185  void execute() { run(); }
186  void run() {
187  if ( !service->is_service("search") ) {
188  FATAL("Unable to locate Search Service - running locally !");
189  string _query = "select distinct ob_id from ( ob_data ";
190 
191  if ( arrayp(classes) && sizeof(classes) > 0 ) {
192  _query += "INNER JOIN ob_class on ob_class.ob_id=ob_data.ob_id and ("+
193  ( "ob_class = "+classes*" or ob_class=")+")";
194  }
195  _query += ") where";
196 
197 
198  _query += compose_value_expressions(extends);
199  LOG_QUERY("Query is: %s", _query);
200  array res = query(_query);
201  array sresult = ({ });
202  foreach(res, mixed r)
203  if ( mappingp(r) )
204  sresult += ({ r->ob_id });
205 
206  handle_service(search_id, sresult);
207  }
208  else {
209  object result =
210  service->call_service_async("search",
211  "search_id": search_id,
212  "classes": classes,
213  "extends": map(extends, serialize_token),
214  "limits": map(limits, serialize_token),
215  "fulltext": map(fulltext, serialize_token), ]));
216 
217  // process result;
218  result->vars = ([ "id": search_id, ]);
219  result->processFunc = handle_result;
220  result->resultFunc = results[search_id]->asyncResult;
221  }
222  }
223  object run_async() {
224  if ( !service->is_service("search") )
225  steam_error("Unable to locate search service !");
226  return
227  service->call_service_async("search",
228  "search_id": search_id,
229  "classes": classes,
230  "extends": map(extends, serialize_token),
231  "limits": map(limits, serialize_token),
232  "fulltext": map(fulltext, serialize_token), ]));
233  }
234 }
235 
236 array handle_result(array res)
237 {
238  int size = sizeof(res);
239  array result = ({ });
240 
241  for (int i =0; i<size; i++) {
242  object o = find_object((int)res[i]);
243  if ( objectp(o) && o->status() >= 0 )
244  result += ({ o });
245  }
246  return result;
247 }
248 
249 void handle_service(int id, array result)
250 {
251  result = handle_result(result);
252  Result r = results[id];
253  r->f(r->args, result);
254 }
255 
256 Search searchQuery(function result_cb, mixed result_args, mixed ... params)
257 {
258  results[++searches] = Result(result_cb, result_args);
259  return Search(searches, @params);
260 }
261 
262 object searchAsyncAttribute(string key, mixed val, mixed ... params)
263 {
264  Async.Return r = Async.Return();
265  results[++searches] = Result(r->asyncResult, 0);
266  Search s = Search(searches, @params);
267  s->search_attribute(key, val);
268  s->run();
269  return r;
270 }
271 
272 object searchAsync(array extends, array limits, array fulltext, void|int cBits)
273 {
274  array classlist = ({ });
275  int i = (1<<0);
276  while ( (cBits > 0) && (i < (1<<31)) )
277  {
278  if ( (cBits & i) == cBits)
279  classlist += ({ _Database->get_class_string(i) });
280  i = i << 1;
281  }
282  object aResult = get_module("ServiceManager")->call_service_async("search",
283  "search_id": searches,
284  "classes": classlist,
285  "extends": extends,
286  "limits": limits,
287  "fulltext": fulltext, ]));
288  aResult->processFunc = handle_result;
289  return aResult;
290 }
291 
292 array search_simple(string searchTerm, void|int classBit)
293 {
294  object handle = _Database->get_db_handle();
295  string query = "SELECT distinct ob_id from ob_class WHERE obkeywords";
296  if (search(searchTerm, "%") >= 0)
297  query += " like '"+ handle->quote(searchTerm) + "'";
298  else
299  query += "='"+handle->quote(searchTerm) +"'";
300 
301  if (classBit)
302  query += " AND ob_class='"+_Database->get_class_string(classBit)+"'";
303  object result = handle->big_query(query);
304  array resultArr = allocate(result->num_rows());
305  for (int i=0; i < result->num_rows(); i++) {
306  mixed row = result->fetch_row();
307  resultArr[i] = find_object((int)row[0]);
308  }
309  return resultArr;
310 }
311 
312 string object_to_dc(object obj)
313 {
314  string rdf = "<rdf:RDF\n"+
315  "xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n"+
316  "xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n";
317 
318  object creator = obj->get_creator() || USER("root");
319 
320  rdf += sprintf("<rdf:Description rdf:about=\"%s\">\n"+
321  "<dc:creator>%s</dc:creator>\n"+
322  "<dc:title>%s</dc:title>\n"+
323  "<dc:description>%s</dc:description>\n"+
324  "<dc:subject>%s</dc:subject>\n"+
325  "<dc:source>%s</dc:source>\n"+
326  "<dc:date>%s</dc:date>\n"+
327  "<dc:type>%s</dc:type>\n"+
328  "<dc:identifier>%s</dc:identifier>\n"+
329  "</rdf:Description>\n",
330  get_module("filepath:tree")->object_to_filename(obj),
331  creator->get_name(),
332  obj->get_identifier(),
333  obj->query_attribute(OBJ_DESC),
334  "",
335  "",
336  (string)obj->query_attribute(OBJ_CREATION_TIME),
337  obj->query_attribute(DOC_MIME_TYPE) || "",
338  (string)obj->get_object_id()
339  );
340  return rdf;
341 }
342 
343 object searchKeyword(string keyword)
344 {
345  object aResult = get_module("ServiceManager")->call_service_async(
346  "search",
347  "search_id": searches,
348  "classes": ({ }),
349  "extends": (["keyword": keyword]),
350  "limits": ({ }),
351  "fulltext": ({ }), ]));
352  aResult->processFunc = handle_result;
353  return aResult;
354 }
355 
356 
357 
358 /**
359  * Filters an array of objects and returns those objects matching specified
360  * filter rules, sorted in a specified order and with optional pagination.
361  *
362  * The filter entries are applied in the order in which they are given. Each
363  * filter must be an array like this:
364  * ({ +/-, "class", class-bitmask })
365  * ({ +/-, "attribute", attribute-name, condition, value/values })
366  * ({ +/-, "function", function-name, condition, value/values, [params] })
367  * ({ +/-, "access", access-bitmask })
368  * If an object doesn't match any filter rule, it will be excluded by
369  * default, so if you would like to include any objects that didn't match any
370  * filter, append ({ "+", "class", CLASS_ALL }) to the end of your filter list.
371  * If the second parameter of a filter is prefixed by an exclamation mark, then
372  * the filter rule will match if the condition is not met. E.g.:
373  * ({ -, "!access", access-bitmask })
374  *
375  * * +/- must be either "+" (include) or "-" (exclude) as a string.
376  * * class-bitmask must be either a CLASS_* constant or a combination (binary
377  * OR) of CLASS_* constants (e.g. CLASS_CONTAINER|CLASS_DOCUMENT).
378  * * attribute-name must be the name (key) of an attribute, e.g. "OBJ_NAME".
379  * * condition must be one of the following strings: "==", "!=", "<",
380  * "<=", ">", ">=", "prefix", "suffix". Note that some conditions will only
381  * match for certain attribute types, e.g. int, float or string.
382  * * value/values must be either a simple value like int, float, string,
383  * object, or an array of simple values, in which case the condition will
384  * try to match at least one of these values.
385  * * params is optional and must be an array of parameters to pass to the
386  * function if specified.
387  * * access-bitmask must be either a SANCTION_* constant or a combination
388  * (binary OR) of SANCTION_* constants (e.g. SANCTION_READ|SANCTION_ANNOTATE).
389  *
390  * The sort entries are applied in the order in which they are given in case
391  * some entries are considered equal regarding the previous sort rule. Each
392  * sort entry must be an array like this:
393  * ({ >/<, "class", class-order })
394  * ({ >/<, "attribute", attribute-name })
395  *
396  * * >/< must be "<" (ascending) or ">" (descending)
397  * * class-order is optional and can be an array of CLASS_* constants. The
398  * result will be sorted in the specified order by the objects that match
399  * the specified classes. All classes that were not specified will be
400  * considered equal for this sort entry.
401  * * attribute-name must be the name (key) of an attribute, e.g. "OBJ_NAME".
402  *
403  * Example:
404  * Return all documents and containers (no users) that the user can read,
405  * sorted by type and then
406  * name:
407  * filter_objects_array(
408  * ({ // filters:
409  * ({ "-", "!access", SANCTION_READ }),
410  * ({ "-", "class", CLASS_USER }),
411  * ({ "+", "class", CLASS_DOCUMENT|CLASS_CONTAINER })
412  * }),
413  * ({ // sort:
414  * ({ "<", "class", ({ CLASS_CONTAINER, CLASS_DOCUMENT }) }),
415  * ({ "<", "attribute", "OBJ_NAME" })
416  * }) );
417  *
418  * Example:
419  * Return all documents with keywords "urgent" or "important" that the user
420  * has read access to, that are no wikis and that have been changed in the
421  * last 24 hours, sort them by modification date (newest first) and return
422  * only the first 10 results:
423  * filter_objects_array(
424  * ({ // filters:
425  * ({ "-", "!access", SANCTION_READ }),
426  * ({ "-", "attribute", "OBJ_TYPE", "prefix", "container_wiki" }),
427  * ({ "-", "attribute", "DOC_LAST_MODIFIED", "<", time()-86400 }),
428  * ({ "-", "attribute", "OBJ_KEYWORDS", "!=", ({ "urgent", "important" }) }),
429  * ({ "+", "class", CLASS_DOCUMENT })
430  * }),
431  * ({ // sort:
432  * ({ ">", "attribute", "DOC_LAST_MODIFIED" })
433  * }), 0, 10 );
434  *
435  * @param objects the array of which to retrieve a filtered selection
436  * @param filters (optional) an array of filters (each an array as described
437  * above) that specify which objects to return
438  * @param sort (optional) an array of sort entries (each an array as described
439  * above) that specify the order of the items (before pagination)
440  * @param offset (optional) only return the objects starting at (and including)
441  * this index
442  * @param length (optional) only return a maximum of this many objects
443  * @return a mapping ([ "objects":({...}), "total":nr, "length":nr,
444  * "start":nr, "page":nr ]), where the "objects" value is an array of
445  * objects that match the specified filters, sort order and pagination.
446  * The other indices contain pagination information ("total" is the total
447  * number of objects after filtering but before applying "length", "length"
448  * is the requested number of items to return (as in the parameter list),
449  * "start" is the start index of the result in the total number of objects,
450  * and "page" is the page number (starting with 1) of pages with "length"
451  * objects each, or 0 if invalid).
452  */
453 mapping paginate_object_array ( array objects, array|void filters, array|void sort, int|void offset, int|void length )
454 {
455  array result;
456  if ( !arrayp(filters) || sizeof(filters) == 0 )
457  result = objects;
458  else {
459  // filter objects:
460  result = ({ });
461  foreach ( objects, object obj ) {
462  if ( !objectp(obj) ) continue;
463  foreach ( filters, mixed filter ) {
464  if ( !arrayp(filter) || sizeof(filter) < 2 )
465  THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
466  int done;
467  int invert = 0;
468  string filter_type = filter[1];
469  if ( has_prefix( filter_type, "!" ) ) {
470  invert = 1;
471  filter_type = filter_type[1..];
472  }
473  switch ( filter_type ) {
474 
475  case "class":
476  if ( sizeof(filter) < 3 )
477  THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
478  int obj_class = obj->get_object_class();
479  if ( invert != ((filter[2] & obj_class) != 0) ) { // matches class
480  if ( filter[0] == "+" ) { // include class
481  result += ({ obj });
482  done = 1;
483  }
484  else done = 1; // exclude class
485  }
486  break; // class
487 
488  case "access": {
489  if ( sizeof(filter) < 3 )
490  THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
491  if ( invert != (get_module( "security" )->check_user_access( obj,
492  this_user(), filter[2], 0, false ) != 0) ) {
493  if ( filter[0] == "+" ) { // include
494  result += ({ obj });
495  done = 1;
496  }
497  else done = 1; // exclude
498  }
499  } break; // access
500 
501  case "attribute":
502  case "function":
503  if ( sizeof(filter) < 5 )
504  THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
505  mixed value;
506  if ( filter_type == "function" ) {
507  mixed func_name = filter[2];
508  if ( sizeof(filter) > 5 ) {
509  mixed func_params = filter[5];
510  if ( !arrayp(func_params) ) func_params = ({ func_params });
511  catch( value = obj->find_function(func_name)( @func_params ) );
512  }
513  else {
514  catch( value = obj->find_function(func_name)() );
515  }
516  }
517  else
518  value = obj->query_attribute( filter[2] );
519  if ( mappingp(value) )
520  break; // ignore mappings
521  mixed target_value = filter[4];
522  switch ( filter[3] ) { // condition:
523  case "==":
524  if ( !arrayp(value) ) {
525  if ( invert != (value == target_value) ) done = 1;
526  break;
527  }
528  foreach ( value, mixed subvalue ) {
529  if ( subvalue == target_value ) {
530  done = 1;
531  break;
532  }
533  }
534  if ( invert ) done = !done;
535  break; // ==
536  case "!=":
537  if ( !arrayp(value) ) {
538  if ( invert != (value != target_value) ) done = 1;
539  break;
540  }
541  int found_equal = 0;
542  foreach ( value, mixed subvalue ) {
543  if ( subvalue == target_value ) {
544  found_equal = 1;
545  break;
546  }
547  }
548  done = !found_equal;
549  if ( invert ) done = !done;
550  break; // !=
551  case "<=":
552  if ( !arrayp(value) ) {
553  if ( invert != (value <= target_value) ) done = 1;
554  break;
555  }
556  foreach ( value, mixed subvalue ) {
557  if ( subvalue <= target_value ) {
558  done = 1;
559  break;
560  }
561  }
562  if ( invert ) done = !done;
563  break; // <=
564  case ">=":
565  if ( !arrayp(value) ) {
566  if ( invert != (value >= target_value) ) done = 1;
567  break;
568  }
569  foreach ( value, mixed subvalue ) {
570  if ( subvalue >= target_value ) {
571  done = 1;
572  break;
573  }
574  }
575  if ( invert ) done = !done;
576  break; // >=
577  case "<":
578  if ( !arrayp(value) ) {
579  if ( invert != (value < target_value) ) done = 1;
580  break;
581  }
582  foreach ( value, mixed subvalue ) {
583  if ( subvalue < target_value ) {
584  done = 1;
585  break;
586  }
587  }
588  if ( invert ) done = !done;
589  break; // <
590  case ">":
591  if ( !arrayp(value) ) {
592  if ( invert != (value > target_value) ) done = 1;
593  break;
594  }
595  foreach ( value, mixed subvalue ) {
596  if ( subvalue > target_value ) {
597  done = 1;
598  break;
599  }
600  }
601  if ( invert ) done = !done;
602  break; // >
603  case "prefix":
604  if ( stringp(value) ) {
605  if ( invert != has_prefix( value, target_value ) ) done = 1;
606  break;
607  }
608  if ( !arrayp(value) ) {
609  if ( invert ) done = 1;
610  break;
611  }
612  foreach ( value, mixed subvalue ) {
613  if ( !stringp(subvalue) ) continue;
614  if ( invert != has_prefix( subvalue, target_value ) ) {
615  done = 1;
616  break;
617  }
618  }
619  if ( invert ) done = !done;
620  break; // prefix
621  case "suffix":
622  if ( stringp(value) ) {
623  if ( invert != has_suffix( value, target_value ) ) done = 1;
624  break;
625  }
626  if ( !arrayp(value) ) {
627  if ( invert ) done = 1;
628  break;
629  }
630  foreach ( value, mixed subvalue ) {
631  if ( !stringp(subvalue) ) continue;
632  if ( has_suffix( subvalue, target_value ) ) {
633  done = 1;
634  break;
635  }
636  }
637  if ( invert ) done = !done;
638  break; // suffix
639  }
640  if ( done && filter[0] == "+" ) { // condition matches
641  result += ({ obj });
642  } // otherwise "done" is set and the object will be excluded
643  break; // attribute
644 
645  }
646  if ( done ) break;
647  } // foreach filters
648  } // foreach objects
649  } // if has filters
650 
651  // sort result:
652  if ( arrayp(sort) && sizeof(sort) > 0 ) {
653  result = Array.sort_array( result, sort_objects_filter, sort );
654  }
655 
656  mapping info = ([ "total":sizeof(result), "length":length, "start":offset,
657  "page":0, "objects":({ }) ]);
658  if ( offset >= sizeof(result) ) return info;
659  if ( offset != 0 || ( length > 0 && length < sizeof(result) ) ) {
660  if ( length < 1 || (offset + length >= sizeof(result)) )
661  length = sizeof(result) - offset;
662  result = result[ offset .. (offset+length-1) ];
663  }
664  if ( result == objects )
665  result = copy_value( objects );
666  info["objects"] = result;
667  info["page"] = (int)ceil( (float)length / (float)info["total"] );
668  return info;
669 }
670 
671 /**
672  * Filters an object array according to filter rules, sorting, offset and
673  * length. This returns the same as the "objects" index in the result of
674  * paginate_object_array() and is here for compatibility reasons and ease of
675  * use (if you don't need pagination information).
676  *
677  * @see paginate_object_array
678  */
679 array filter_object_array ( array objects, array|void filters, array|void sort, int|void offset, int|void length )
680 {
681  return paginate_object_array( objects, filters, sort, offset, length )["objects"];
682 }
683 
684 object paginate_search_async ( array|void filters, array|void sort, int|void offset, int|void length )
685 {
686  array limits = ({ });
687  array extends = ({ });
688  array fulltext = ({ });
689  int classes;
690  int classes_not;
691  array remaining_filters = ({ });
692 
693  foreach ( filters, mixed filter ) {
694  if ( !arrayp(filter) || sizeof(filter) < 2 )
695  THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
696  int invert = 0;
697  string filter_type = filter[1];
698  if ( has_prefix( filter_type, "!" ) ) {
699  // inverted filters are currently not handled in the search itself,
700  // they will be applied after the search.
701  remaining_filters += ({ filter });
702  continue;
703  }
704 
705  switch ( filter_type ) {
706 
707  case "class":
708  if ( sizeof(filter) < 3 )
709  THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
710  if ( filter[0] == "+" ) classes |= filter[2];
711  else classes_not |= filter[2];
712  break;
713 
714  case "access":
715  if ( sizeof(filter) < 3 )
716  THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
717  // access filters are currently applied after the search, they are not
718  // handled by the search itself
719  remaining_filters += ({ filter });
720  continue;
721  break;
722 
723  case "function":
724  if ( sizeof(filter) < 5 )
725  THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
726  // functions are currently not handled by the search itself, these
727  // filters will be applied after the search
728  remaining_filters += ({ filter });
729  continue;
730  break;
731 
732  case "attribute":
733  if ( sizeof(filter) < 5 )
734  THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
735  string attrib = filter[2];
736  string condition = filter[3];
737  mixed target_value = filter[4];
738  switch ( condition ) {
739  case "==":
740  condition = "=";
741  break;
742  case "<":
743  case ">":
744  case "<=":
745  case ">=":
746  case "!=":
747  case "like":
748  // condition is okay for search
749  break;
750  case "prefix":
751  case "suffix":
752  condition = "like";
753  // like doesn't check strictly enough, so use it for a first
754  // selection and then run the filter after the search
755  remaining_filters += ({ filter });
756  break;
757  default:
758  THROW( "Invalid condition for search: " + condition, E_ERROR );
759  }
760  if ( !arrayp( target_value ) ) target_value = ({ target_value });
761  foreach ( target_value, mixed value ) {
762  if ( filter[0] == "+" ) extends += ({ ([ "storage":"attrib",
763  "key":attrib, "value":({ condition, value }) ]) });
764  else limits += ({ ([ "storage":"attrib",
765  "key":attrib, "value":({ condition, value }) ]) });
766  }
767  break;
768 
769  case "content":
770  if ( sizeof(filter) < 3 )
771  THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
772  // limiting by content is not supported at the moment:
773  if ( filter[0] != "+" ) break;
774  fulltext += ({ ([ "storage":"doc_ft", "value":filter[2] ]) });
775  break;
776  }
777  }
778 
779  if ( classes == 0 ) classes = CLASS_ALL & (~classes_not);
780  array classlist = ({ });
781  if ( classes != CLASS_ALL ) {
782  int cBits = classes;
783  while ( cBits ) {
784  mixed class_string = _Database->get_class_string(cBits);
785  if ( class_string != "/classes/Object" )
786  classlist += ({ class_string });
787  cBits = cBits >> 1;
788  }
789  }
790 
791  object async_result = get_module("ServiceManager")->call_service_async(
792  "extends":extends, "limits":limits, "fulltext":fulltext ])
793  );
794  async_result->processFunc = handle_paginate_search_result;
795  async_result->userData = ([ "filters":remaining_filters, "sort":sort,
796  "offset":offset, "length":length ]);
797  return async_result;
798 }
799 
800 protected:
801  mapping handle_paginate_search_result ( array res, mapping data )
802 {
803  array objects = ({ });
804  foreach ( res, mixed obj ) {
805  if ( stringp(obj) ) obj = (int)obj;
806  if ( intp(obj) ) obj = find_object( obj );
807  if ( objectp(obj) ) objects += ({ obj });
808  }
809  return paginate_object_array( objects, data->filters, data->sort,
810  data->offset, data->length );
811 }
812 
813 public:
814 
815 object filter_search_async ( array|void filters, array|void sort, int|void offset, int|void length )
816 {
817  object async_result = paginate_search_async( filters, sort, offset, length );
818  async_result->processFunc = handle_filter_search_result;
819  return async_result;
820 }
821 
822 protected:
823  array handle_filter_search_result ( array res, mapping data )
824 {
825  array objects = ({ });
826  foreach ( res, mixed obj ) {
827  if ( stringp(obj) ) obj = (int)obj;
828  if ( intp(obj) ) obj = find_object( obj );
829  if ( objectp(obj) ) objects += ({ obj });
830  }
831  return filter_object_array( objects, data->filters, data->sort,
832  data->offset, data->length );
833 }
834 
835 public:
836 
837 protected:
838  int sort_objects_filter ( object obj1, object obj2, array rules )
839 {
840  foreach ( rules, array rule ) {
841  int reverse = 0;
842  if ( rule[0] == ">" ) reverse = 1;
843  switch ( rule[1] ) {
844  case "class":
845  int obj_class1 = obj1->get_object_class();
846  int obj_class2 = obj2->get_object_class();
847  if ( sizeof(rule) < 3 ) {
848  if ( obj_class1 > obj_class2 ) return 1 ^ reverse;
849  else if ( obj_class1 < obj_class2 ) return 0 ^ reverse;
850  continue;
851  }
852  int index1 = -1;
853  int index2 = -1;
854  int index = 0;
855  foreach ( rule[2], int obj_class ) {
856  if ( obj_class & obj_class1 ) index1 = index;
857  if ( obj_class & obj_class2 ) index2 = index;
858  if ( index1 >= 0 && index2 >= 0 ) break;
859  index++;
860  }
861  if ( index1 > index2 ) return 1 ^ reverse;
862  else if ( index1 < index2 ) return 0 ^ reverse;
863  break;
864 
865  case "attribute":
866  mixed value1;
867  mixed value2;
868  if ( arrayp( rule[2] ) ) {
869  foreach ( rule[2], mixed key ) {
870  if ( !value1 ) value1 = obj1->query_attribute( key );
871  if ( !value2 ) value2 = obj2->query_attribute( key );
872  }
873  }
874  else {
875  value1 = obj1->query_attribute( rule[2] );
876  value2 = obj2->query_attribute( rule[2] );
877  }
878  if ( value1 == 0 && value2 == 0 ) continue;
879  if ( stringp(value1) && value2 == 0 ) return 1 ^ reverse;
880  else if ( value1 == 0 && stringp(value2) ) return 0 ^ reverse;
881  else if ( stringp(value1) && stringp(value2) ) {
882  if ( value1 > value2 ) return 1 ^ reverse;
883  else if ( value1 < value2 ) return 0 ^ reverse;
884  else break;
885  }
886  else if ( (intp(value1) || floatp(value1)) &&
887  (intp(value2) || floatp(value2)) ) {
888  if ( value1 > value2 ) return 1 ^ reverse;
889  else if ( value1 < value2 ) return 0 ^ reverse;
890  else break;
891  }
892  break;
893  }
894  }
895  return 0;
896 }
897 
898 public:
899 
900 
901 
902 private array test_objects = ({ });
903 
904 {
905  // first create some objects to search
906  object obj = get_factory(CLASS_DOCUMENT)->execute((["name": "document.doc", ]));
907  obj->set_attribute(OBJ_KEYWORDS, ({ "Mistel", "approach" }));
908  test_objects += ({ obj });
909  // test external search service
910  Test.add_test_function(test_search, 20, 1, ([ ]));
911 }
912 
913 void test_cleanup()
914 {
915  if ( arrayp(test_objects) ) {
916  foreach ( test_objects, object obj )
917  catch ( obj->delete() );
918  }
919 }
920 
921 void search_test_finished(object result, array results)
922 {
923  if ( sizeof(results) == 0 ) {
924  Test.failed(result->userData->name,
925  "Search %s finished with %d results in %d ms",
926  result->userData->name,
927  sizeof(results),
928  get_time_millis()-result->userData->time);
929  }
930  else {
931  Test.succeeded(result->userData->name,
932  "Search %s finished with %d results in %d ms",
933  result->userData->name,
934  sizeof(results),
935  get_time_millis()-result->userData->time);
936  }
937  result->userData->tests[result->userData->name] = 1;
938 }
939 
940 protected:
941  void test_search(int nr_tries, mapping tests)
942 {
943  object serviceManager = get_module("ServiceManager");
944 
945  if ( !Test.test("Service Manager",
946  objectp(serviceManager), "Failed to find ServiceManager!") )
947  return;
948 
949  if ( !serviceManager->is_service("search") ) {
950  if ( nr_tries > 20 )
951  Test.failed("search service",
952  "failed to locate search services after %d tries",
953  nr_tries);
954  else
955  Test.add_test_function(test_search, 10, nr_tries+1, tests);
956  return;
957  }
958  if ( sizeof(tests) != 0 ) {
959  foreach(values(tests), int i) {
960  if ( i == 0 ) {
961  werror("Waiting for tests to finish!");
962  Test.add_test_function(test_search, 10, nr_tries+1, tests);
963  }
964  }
965  return;
966  }
967 
968  // now search for common queries
969  object result, query;
970 
971  // a single search
972  tests["simple1"] = 0;
973  query = searchQuery(search_test_finished, ([ ]), ({ }));
974  query->extend(STORE_ATTRIB, OBJ_NAME, query->like("steam"));
975  result = query->run_async();
976  result->resultFunc = search_test_finished;
977  result->userData = ([ "name": "simple1",
978  "tests":tests,
979  "time":get_time_millis(),]);
980 
981  tests["simple2"] = 0;
982  query = searchQuery(search_test_finished, ([ ]), ({ }));
983  query->extend(STORE_ATTRIB, OBJ_NAME, query->like("steam"));
984  query->extend(STORE_ATTRIB, OBJ_DESC, query->like("steam"));
985  query->extend(STORE_ATTRIB, OBJ_KEYWORDS, query->like("steam"));
986  result = query->run_async();
987  result->resultFunc = search_test_finished;
988  result->userData = ([ "name": "simple2",
989  "tests":tests,
990  "time":get_time_millis(),]);
991 
992 
993  tests["keywords"] = 0;
994  query = searchQuery(search_test_finished, ([ ]), ({ }));
995  query->extend(STORE_ATTRIB, OBJ_KEYWORDS, query->like("Mistel"));
996  result = query->run_async();
997  result->resultFunc = search_test_finished;
998  result->userData = ([ "name": "keywords",
999  "tests":tests,
1000  "time":get_time_millis(),]);
1001 
1002  tests["target room"] = 0;
1003  query = searchQuery(search_test_finished, ([ ]), ({ "\"/classes/Room\"" }));
1004  query->extend(STORE_ATTRIB, OBJ_NAME, query->like("coder%"));
1005  result = query->run_async();
1006  result->resultFunc = search_test_finished;
1007  result->userData = ([ "name": "target room",
1008  "tests":tests,
1009  "time":get_time_millis(),]);
1010 
1011  tests["user 1"] = 0;
1012  query = searchQuery(search_test_finished, ([ ]), ({ "\"/classes/User\"" }));
1013  query->extend(STORE_ATTRIB, OBJ_NAME, query->like("service"));
1014  result = query->run_async();
1015  result->resultFunc = search_test_finished;
1016  result->userData = ([ "name": "user 1",
1017  "tests":tests,
1018  "time":get_time_millis(),]);
1019 
1020  Test.add_test_function(test_search, 10, 0, tests);
1021 }
1022 
1023 public:
1024 
1025 
1026 };