CWIS Developer Documentation
SearchParameterSet.php
Go to the documentation of this file.
1 <?PHP
2 #
3 # FILE: SearchParameterSet.php
4 #
5 # Part of the ScoutLib application support library
6 # Copyright 2015 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu
8 #
9 
14 
15  # ---- SETUP / CONFIGURATION ---------------------------------------------
16  /*@(*/
17 
25  function __construct($Data = NULL)
26  {
27  # if set data supplied
28  if ($Data !== NULL)
29  {
30  # set internal values from data
31  $this->LoadFromData($Data);
32  }
33  }
34 
42  static function SetCanonicalFieldFunction($Func)
43  {
44  if (is_callable($Func))
45  {
46  self::$CanonicalFieldFunction = $Func;
47  }
48  else
49  {
50  throw new InvalidArgumentException("Invalid function supplied.");
51  }
52  }
53 
61  static function SetPrintableFieldFunction($Func)
62  {
63  if (is_callable($Func))
64  {
65  self::$PrintableFieldFunction = $Func;
66  }
67  else
68  {
69  throw new InvalidArgumentException("Invalid function supplied.");
70  }
71  }
72 
78  /*@)*/
79  # ---- SET CONSTRUCTION ---------------------------------------------------
80  /*@(*/
81 
92  function AddParameter($SearchStrings, $Field = NULL)
93  {
94  # normalize field value if supplied
95  if (($Field !== NULL) && isset(self::$CanonicalFieldFunction))
96  {
97  $Field = call_user_func(self::$CanonicalFieldFunction, $Field);
98  }
99 
100  # make sure search strings are an array
101  if (!is_array($SearchStrings))
102  { $SearchStrings = array($SearchStrings); }
103 
104  # for each search string
105  foreach ($SearchStrings as $String)
106  {
107  # if field specified
108  if ($Field !== NULL)
109  {
110  # add strings to search values for field
111  $this->SearchStrings[$Field][] = $String;
112  }
113  else
114  {
115  # add strings to keyword search values
116  $this->KeywordSearchStrings[] = $String;
117  }
118  }
119  }
120 
127  function Logic($NewValue = NULL)
128  {
129  # if new value supplied
130  if ($NewValue !== NULL)
131  {
132  # normalize value
133  $NormValue = strtoupper($NewValue);
134 
135  # error out if value appears invalid
136  if (($NormValue !== "AND") && ($NormValue !== "OR"))
137  {
138  throw new InvalidArgumentException("New logic setting"
139  ." is invalid (".$NewValue.").");
140  }
141 
142  # save new setting
143  $this->Logic = $NormValue;
144  }
145 
146  # return current logic setting to caller
147  return $this->Logic;
148  }
149 
154  function AddSet(SearchParameterSet $Set)
155  {
156  # add subgroup to privilege set
157  $this->Subgroups[] = $Set;
158  }
159 
160 
161  /*@)*/
162  # ---- DATA TRANSLATION ---------------------------------------------------
163  /*@(*/
164 
175  function Data($NewValue = NULL)
176  {
177  # if new data supplied
178  if ($NewValue !== NULL)
179  {
180  # unpack set data and load
181  $this->LoadFromData($NewValue);
182  }
183 
184  # serialize current data and return to caller
185  $Data = array();
186  if ($this->Logic !== "AND") { $Data["Logic"] = $this->Logic; }
187  if (count($this->SearchStrings))
188  { $Data["SearchStrings"] = $this->SearchStrings; }
189  if (count($this->KeywordSearchStrings))
190  {
191  $Data["KeywordSearchStrings"] = $this->KeywordSearchStrings;
192  }
193  if (count($this->Subgroups))
194  {
195  foreach ($this->Subgroups as $Subgroup)
196  {
197  $Data["Subgroups"][] = $Subgroup->Data();
198  }
199  }
200  return serialize($Data);
201  }
202 
203  /*
204  * Get/set search parameter set, in the form of URL parameters.
205  * @param string $NewValue New parameter set in the form of a URL
206  * parameter string. (OPTIONAL)
207  * @return array URL parameter values, with parameter names for the index..
208  */
209  function UrlParameters($NewValue = NULL)
210  {
211  # if new value supplied
212  if ($NewValue !== NULL)
213  {
214  # set new parameters
215  $this->SetFromUrlParameters($NewValue);
216  }
217 
218  # get existing search parameters as URL parameters
219  $Params = $this->GetAsUrlParameters();
220 
221  # sort parameters by parameter name to normalize result
222  ksort($Params);
223 
224  # return parameters to caller
225  return $Params;
226  }
227 
228  /*
229  * Get/set search parameter set, in the form of an URL parameter string.
230  * @param string $NewValue New parameter set in the form of a URL
231  * parameter string. (OPTIONAL)
232  * @return string URL parameter string.
233  */
234  function UrlParameterString($NewValue = NULL)
235  {
236  # get/set parameters
237  $Params = $this->UrlParameters($NewValue);
238 
239  # combine values into string
240  $ParamString = "";
241  $Separator = "";
242  foreach ($Params as $Index => $Value)
243  {
244  $ParamString .= $Separator.$Index."=".urlencode($Value);
245  $Separator = "&";
246  }
247 
248  # return string to caller
249  return $ParamString;
250  }
251 
252  /*
253  * Get text description of search parameter set.
254  * @param bool $IncludeHtml Whether to include HTML tags for formatting.
255  * (OPTIONAL, defaults to TRUE)
256  * @param bool $StartWithBreak Whether to start string with BR tag.
257  * (OPTIONAL, defaults to TRUE)
258  * @param int $TruncateLongWordsTo Number of characters to truncate long
259  * words to (use 0 for no truncation). (OPTIONAL, defaults to 0)
260  * @param string $Indent (for internal (recursive) use only)
261  * @return string Text description of search parameters.
262  */
263  function TextDescription($IncludeHtml = TRUE, $StartWithBreak = TRUE,
264  $TruncateLongWordsTo = 0, $Indent = "")
265  {
266  # define list of phrases used to represent logical operators
267  $OperatorPhrases = array(
268  "=" => "is",
269  "==" => "is",
270  ">" => "is greater than",
271  "<" => "is less than",
272  ">=" => "is at least",
273  "<=" => "is no more than",
274  "!" => "is not",
275  "!=" => "is not",
276  );
277 
278  # set characters used to indicate literal strings
279  $LiteralStart = $IncludeHtml ? "<i>" : "\"";
280  $LiteralEnd = $IncludeHtml ? "</i>" : "\"";
281  $LiteralBreak = $IncludeHtml ? "<br>\n" : "\n";
282  $Indent .= $IncludeHtml ? "&nbsp;&nbsp;&nbsp;&nbsp;" : " ";
283 
284  # for each keyword search string
285  $Descriptions = array();
286  foreach ($this->KeywordSearchStrings as $SearchString)
287  {
288  # escape search string if appropriate
289  if ($IncludeHtml)
290  {
291  $SearchString = defaulthtmlentities($SearchString);
292  }
293 
294  # add string to list of descriptions
295  $Descriptions[] = $LiteralStart.$SearchString.$LiteralEnd;
296  }
297 
298  # for each field with search strings
299  foreach ($this->SearchStrings as $FieldId => $SearchStrings)
300  {
301  # retrieve field name
302  $FieldName = call_user_func(self::$PrintableFieldFunction, $FieldId);
303 
304  # for each search string
305  foreach ($SearchStrings as $SearchString)
306  {
307  # extract operator from search string
308  $MatchResult = preg_match("/^([=><!]+)(.+)/",
309  $SearchString, $Matches);
310 
311  # determine operator phrase
312  if (($MatchResult == 1) && isset($OperatorPhrases[$Matches[1]]))
313  {
314  $OpPhrase = $OperatorPhrases[$Matches[1]];
315  $SearchString = $Matches[2];
316  }
317  else
318  {
319  $OpPhrase = "contains";
320  }
321 
322  # escape field name and search string if appropriate
323  if ($IncludeHtml)
324  {
325  $FieldName = defaulthtmlentities($FieldName);
326  $SearchString = defaulthtmlentities($SearchString);
327  }
328 
329  # assemble field and operator and value into description
330  $Descriptions[] = $FieldName." ".$OpPhrase." "
331  .$LiteralStart.$SearchString.$LiteralEnd;
332  }
333  }
334 
335  # for each subgroup
336  foreach ($this->Subgroups as $Subgroup)
337  {
338  # retrieve description for subgroup
339  $Descriptions[] = "(".$Subgroup->TextDescription($IncludeHtml,
340  $StartWithBreak, $TruncateLongWordsTo, $Indent).")";
341  }
342 
343  # join descriptions with appropriate conjunction
344  $Descrip = join($LiteralBreak.$Indent." ".strtolower($this->Logic)." ",
345  $Descriptions);
346 
347  # if caller requested that long words be truncated
348  if ($TruncateLongWordsTo > 4)
349  {
350  # break description into words
351  $Words = explode(" ", $Descrip);
352 
353  # for each word
354  $NewDescrip = "";
355  foreach ($Words as $Word)
356  {
357  # if word is longer than specified length
358  if (strlen(strip_tags($Word)) > $TruncateLongWordsTo)
359  {
360  # truncate word and add ellipsis
361  $Word = NeatlyTruncateString($Word, $TruncateLongWordsTo - 3);
362  }
363 
364  # add word to new description
365  $NewDescrip .= " ".$Word;
366  }
367 
368  # set description to new description
369  $Descrip = $NewDescrip;
370  }
371 
372  # return description to caller
373  return $Descrip;
374  }
375 
376 
377  /*@)*/
378  # ---- BACKWARD COMPATIBILITY ---------------------------------------------
379  /*@(*/
380 
389  function GetAsLegacyArray()
390  {
391  # set logic for search group
392  $Group["Logic"] = ($this->Logic == "OR")
394 
395  # for each set of search strings
396  foreach ($this->SearchStrings as $Field => $Strings)
397  {
398  # get text name of field
399  $FieldName = call_user_func(self::$PrintableFieldFunction, $Field);
400 
401  # add set to group
402  $Group["SearchStrings"][$FieldName] = $Strings;
403  }
404 
405  # for each keyword search string
406  foreach ($this->KeywordSearchStrings as $String)
407  {
408  # add string to keyword entry in group
409  $Group["SearchStrings"]["XXXKeywordXXX"][] = $String;
410  }
411 
412  # add group to array
413  $Legacy[] = $Group;
414 
415  # for each subgroup
416  foreach ($this->Subgroups as $Subgroup)
417  {
418  # retrieve legacy array for subgroup
419  $SubLegacy = $Subgroup->GetAsLegacyArray();
420 
421  # add groups from legacy array to our array
422  $Legacy = array_merge($Legacy, $SubLegacy);
423  }
424 
425  # set logic for whole array
426  $Legacy["Logic"] = $Group["Logic"];
427 
428  # return array to caller
429  return $Legacy;
430  }
431 
432 
433  /*@)*/
434  # ---- PRIVATE INTERFACE -------------------------------------------------
435 
436  private $KeywordSearchStrings = array();
437  private $Logic = self::DEFAULT_LOGIC;
438  private $SearchStrings = array();
439  private $Subgroups = array();
440 
441  static private $CanonicalFieldFunction;
442  static private $PrintableFieldFunction;
443  static private $UrlParameterPrefix = "F";
444 
445  const DEFAULT_LOGIC = "AND";
446  const URL_KEYWORDFREE_RANGE = "A-JL-Z";
448  const URL_LOGIC_INDICATOR = "00";
449 
455  private function LoadFromData($Serialized)
456  {
457  # unpack new data
458  $Data = unserialize($Serialized);
459  if (!is_array($Data))
460  {
461  throw new InvalidArgumentException("Incoming set data"
462  ." appears invalid.");
463  }
464 
465  # load logic
466  $this->Logic = isset($Data["Logic"]) ? $Data["Logic"] : "AND";
467 
468  # load search strings
469  $this->SearchStrings = isset($Data["SearchStrings"])
470  ? $Data["SearchStrings"] : array();
471  $this->KeywordSearchStrings = isset($Data["KeywordSearchStrings"])
472  ? $Data["KeywordSearchStrings"] : array();
473 
474  # load any subgroups
475  $this->Subgroups = array();
476  if (isset($Data["Subgroups"]))
477  {
478  foreach ($Data["Subgroups"] as $SubgroupData)
479  {
480  $this->Subgroups[] = new SearchParameterSet($SubgroupData);
481  }
482  }
483  }
484 
485  /*
486  * Get the search set parameter set as an URL parameter string.
487  * @param string $SetPrefix Prefix to use after the URL parameter prefix.
488  * @return array Array of URL parameters.
489  */
490  private function GetAsUrlParameters($SetPrefix = "")
491  {
492  # for each search string group in set
493  $Params = array();
494  foreach ($this->SearchStrings as $FieldId => $Values)
495  {
496  # get numeric version of field ID if not already numeric
497  if (!is_numeric($FieldId))
498  {
499  $FieldId = call_user_func(self::$CanonicalFieldFunction, $FieldId);
500  }
501 
502  # for each search string in group
503  $ParamSuffix = "";
504  foreach ($Values as $Value)
505  {
506  # check for too many search strings for this field
507  if ($ParamSuffix == "Z")
508  {
509  throw new Exception("Maximum search parameter complexity"
510  ." exceeded: more than 26 search parameters for"
511  ." field ID ".$FieldId.".");
512  }
513 
514  # add search string to URL
515  $Params[self::$UrlParameterPrefix.$SetPrefix
516  .$FieldId.$ParamSuffix] = $Value;
517  $ParamSuffix = ($ParamSuffix == "") ? "A"
518  : chr(ord($ParamSuffix) + 1);
519  }
520  }
521 
522  # for each keyword search string
523  $ParamSuffix = "";
524  foreach ($this->KeywordSearchStrings as $Value)
525  {
526  # check for too many keyword search strings
527  if ($ParamSuffix == "Z")
528  {
529  throw new Exception("Maximum search parameter complexity"
530  ." exceeded: more than 26 keyword search parameters.");
531  }
532 
533  # add search string to URL
534  $Params[self::$UrlParameterPrefix.$SetPrefix
535  .self::URL_KEYWORD_INDICATOR.$ParamSuffix] = $Value;
536  $ParamSuffix = ($ParamSuffix == "") ? "A"
537  : chr(ord($ParamSuffix) + 1);
538  }
539 
540  # add logic if not default
541  if ($this->Logic != self::DEFAULT_LOGIC)
542  {
543  $Params[self::$UrlParameterPrefix.$SetPrefix
544  .self::URL_LOGIC_INDICATOR] = $this->Logic;
545  }
546 
547  # for each search parameter subgroup
548  $SetLetter = "A";
549  foreach ($this->Subgroups as $Subgroup)
550  {
551  # check for too many subgroups
552  if ($SetLetter == "Z")
553  {
554  throw new Exception("Maximum search parameter complexity"
555  ." exceeded: more than 24 search parameter subgroups.");
556  }
557 
558  # retrieve URL string for subgroup and add it to URL
559  $Params = array_merge($Params, $Subgroup->GetAsUrlParameters(
560  $SetPrefix.$SetLetter));
561 
562  # move to next set letter
563  $SetLetter = ($SetLetter == chr(ord(self::URL_KEYWORD_INDICATOR) - 1))
564  ? chr(ord(self::URL_KEYWORD_INDICATOR) + 1)
565  : chr(ord($SetLetter) + 1);
566  }
567 
568  # return constructed URL parameter string to caller
569  return $Params;
570  }
571 
572  /*
573  * Set search parameter from URL parameters in the same format as
574  * produced by GetAsUrlParameters().
575  * @param string $UrlParameter URL parameter string or array.
576  * @see SearchParameterSet::GetAsUrlParameters()
577  */
578  private function SetFromUrlParameters($UrlParameters)
579  {
580  # if string was passed in
581  if (is_string($UrlParameters))
582  {
583  # split string into parameter array
584  $Params = explode("&", $UrlParameters);
585 
586  # pare down parameter array to search parameter elements
587  # and strip off search parameter prefix
588  $NewUrlParameters = array();
589  foreach ($Params as $Param)
590  {
591  if (strpos($Param, self::$UrlParameterPrefix) === 0)
592  {
593  list($Index, $Value) = explode("=", $Param);
594  $NewUrlParameters[$Index] = urldecode($Value);
595  }
596  }
597  $UrlParameters = $NewUrlParameters;
598  }
599 
600  # for each search parameter
601  foreach ($UrlParameters as $ParamName => $SearchString)
602  {
603  # strip off standard search parameter prefix
604  $ParamName = substr($ParamName, strlen(self::$UrlParameterPrefix));
605 
606  # split parameter into component parts
607  $SplitResult = preg_match("/^([".self::URL_KEYWORDFREE_RANGE."]*)"
608  ."([0-9".self::URL_KEYWORD_INDICATOR."]+)([A-Z]*)$/",
609  $ParamName, $Matches);
610 
611  # if split was successful
612  if ($SplitResult === 1)
613  {
614  # pull components from split pieces
615  $SetPrefix = $Matches[1];
616  $FieldId = $Matches[2];
617  $ParamSuffix = $Matches[3];
618 
619  # if set prefix indicates parameter is part of our set
620  if ($SetPrefix == "")
621  {
622  switch ($FieldId)
623  {
624  case self::URL_LOGIC_INDICATOR:
625  # set logic
626  $this->Logic($SearchString);
627  break;
628 
629  case self::URL_KEYWORD_INDICATOR:
630  # add string to keyword searches
631  $this->KeywordSearchStrings[] = $SearchString;
632  break;
633 
634  default:
635  # add string to searches for appropriate field
636  $this->SearchStrings[$FieldId][] = $SearchString;
637  break;
638  }
639  }
640  else
641  {
642  # add parameter to array for subgroup
643  $SubgroupIndex = $SetPrefix[0];
644  $SubgroupPrefix = (strlen($SetPrefix) > 1)
645  ? substr($SetPrefix, 1) : "";
646  $SubgroupParamIndex = self::$UrlParameterPrefix
647  .$SubgroupPrefix.$FieldId.$ParamSuffix;
648  $SubgroupParameters[$SubgroupIndex][$SubgroupParamIndex]
649  = $SearchString;
650  }
651  }
652  }
653 
654  # if subgroups were found
655  if (isset($SubgroupParameters))
656  {
657  # for each identified subgroup
658  foreach ($SubgroupParameters as $SubgroupIndex => $Parameters)
659  {
660  # create subgroup and set parameters
661  $Subgroup = new SearchParameterSet();
662  $Subgroup->SetFromUrlParameters($Parameters);
663 
664  # add subgroup to our set
665  $this->Subgroups[] = $Subgroup;
666  }
667  }
668  }
669 }
TextDescription($IncludeHtml=TRUE, $StartWithBreak=TRUE, $TruncateLongWordsTo=0, $Indent="")
Data($NewValue=NULL)
Get/set search parameter set data, in the form of an opaque string.
static SetCanonicalFieldFunction($Func)
Register function used to retrieve a canonical value for a field.
Set of parameters used to perform a search.
AddSet(SearchParameterSet $Set)
Add subgroup of search parameters to set.
UrlParameterString($NewValue=NULL)
UrlParameters($NewValue=NULL)
Logic($NewValue=NULL)
Get/set logic for set.
static SetPrintableFieldFunction($Func)
Register function used to retrieve a printable value for a field.
AddParameter($SearchStrings, $Field=NULL)
Add search parameter to set.
GetAsLegacyArray()
Retrieve search parameters in legacy array format.
__construct($Data=NULL)
Class constructor, used to create a new set or reload an existing set from previously-constructed dat...