4 +--------------------------------------------------------------------+
5 | CiviCRM version 4.5 |
6 +--------------------------------------------------------------------+
7 | Copyright CiviCRM LLC (c) 2004-2014 |
8 +--------------------------------------------------------------------+
9 | This file is a part of CiviCRM. |
11 | CiviCRM is free software; you can copy, modify, and distribute it |
12 | under the terms of the GNU Affero General Public License |
13 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
15 | CiviCRM is distributed in the hope that it will be useful, but |
16 | WITHOUT ANY WARRANTY; without even the implied warranty of |
17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
18 | See the GNU Affero General Public License for more details. |
20 | You should have received a copy of the GNU Affero General Public |
21 | License and the CiviCRM Licensing Exception along |
22 | with this program; if not, contact CiviCRM LLC |
23 | at info[AT]civicrm[DOT]org. If you have questions about the |
24 | GNU Affero General Public License or the licensing of CiviCRM, |
25 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
26 +--------------------------------------------------------------------+
30 * Class CRM_Utils_QueryFormatter
32 * This class is a bad idea. It exists for the unholy reason that a single installation
33 * may have up to three query engines (MySQL LIKE, MySQL FTS, Solr) processing the same
34 * query-text. It labors* to take the user's search expression and provide similar search
35 * semantics in different contexts. It is unknown whether this labor will be fruitful
38 class CRM_Utils_QueryFormatter
{
39 const LANG_SQL_LIKE
= 'like';
40 const LANG_SQL_FTS
= 'fts';
41 const LANG_SQL_FTSBOOL
= 'ftsbool';
42 const LANG_SOLR
= 'solr';
45 * Attempt to leave the text as-is.
47 const MODE_NONE
= 'simple';
50 * Attempt to treat the input text as a phrase
52 const MODE_PHRASE
= 'phrase';
55 * Attempt to treat the input text as a phrase with
56 * wildcards on each end.
58 const MODE_WILDPHRASE
= 'wildphrase';
61 * Attempt to treat individual word as if it
62 * had wildcards at the start and end.
64 const MODE_WILDWORDS
= 'wildwords';
67 * Attempt to treat individual word as if it
68 * had a wildcard at the end.
70 const MODE_WILDWORDS_SUFFIX
= 'wildwords-suffix';
72 static protected $singleton;
76 * @return CRM_Utils_QueryFormatter
78 public static function singleton($fresh = FALSE) {
79 if ($fresh || self
::$singleton === NULL) {
80 $mode = CRM_Core_BAO_Setting
::getItem(CRM_Core_BAO_Setting
::SEARCH_PREFERENCES_NAME
, 'fts_query_mode', NULL, self
::MODE_NONE
);
81 self
::$singleton = new CRM_Utils_QueryFormatter($mode);
83 return self
::$singleton;
87 * @var string eg MODE_NONE
92 * @param string $mode eg MODE_NONE
94 function __construct($mode) {
101 public function setMode($mode) {
108 public function getMode() {
113 * @param string $text
114 * @param string $language eg LANG_SQL_LIKE, LANG_SQL_FTS, LANG_SOLR
115 * @throws CRM_Core_Exception
118 public function format($text, $language) {
122 case self
::LANG_SOLR
:
123 case self
::LANG_SQL_FTS
:
124 $text = $this->_formatFts($text, $this->mode
);
126 case self
::LANG_SQL_FTSBOOL
:
127 $text = $this->_formatFtsBool($text, $this->mode
);
129 case self
::LANG_SQL_LIKE
:
130 $text = $this->_formatLike($text, $this->mode
);
136 if ($text === NULL) {
137 throw new CRM_Core_Exception("Unrecognized combination: language=[{$language}] mode=[{$this->mode}]");
143 protected function _formatFts($text, $mode) {
146 // normalize user-inputted wildcards
147 $text = str_replace('%', '*', $text);
152 elseif (strpos($text, '*') !== FALSE) {
153 // if user supplies their own wildcards, then don't do any sophisticated changes
158 case self
::MODE_NONE
:
162 case self
::MODE_PHRASE
:
163 $result = '"' . $text . '"';
166 case self
::MODE_WILDPHRASE
:
167 $result = '"*' . $text . '*"';
170 case self
::MODE_WILDWORDS
:
171 $result = $this->mapWords($text, '*word*');
174 case self
::MODE_WILDWORDS_SUFFIX
:
175 $result = $this->mapWords($text, 'word*');
183 return $this->dedupeWildcards($result, '%');
186 protected function _formatFtsBool($text, $mode) {
189 // normalize user-inputted wildcards
190 $text = str_replace('%', '*', $text);
195 elseif (strpos($text, '+') !== FALSE ||
strpos($text, '-') !== FALSE) {
196 // if user supplies their own include/exclude operators, use text as is (with trailing wildcard)
197 $result = $this->mapWords($text, 'word*');
199 elseif (strpos($text, '*') !== FALSE) {
200 // if user supplies their own wildcards, then don't do any sophisticated changes
201 $result = $this->mapWords($text, '+word');
203 elseif (preg_match('/^(["\']).*\1$/m', $text)) {
204 // if surrounded by quotes, use term as is
209 case self
::MODE_NONE
:
210 $result = $this->mapWords($text, '+word');
213 case self
::MODE_PHRASE
:
214 $result = '+"' . $text . '"';
217 case self
::MODE_WILDPHRASE
:
218 $result = '+"*' . $text . '*"';
221 case self
::MODE_WILDWORDS
:
222 $result = $this->mapWords($text, '+*word*');
225 case self
::MODE_WILDWORDS_SUFFIX
:
226 $result = $this->mapWords($text, '+word*');
234 return $this->dedupeWildcards($result, '%');
237 protected function _formatLike($text, $mode) {
243 elseif (strpos($text, '%') !== FALSE) {
244 // if user supplies their own wildcards, then don't do any sophisticated changes
249 case self
::MODE_NONE
:
250 case self
::MODE_PHRASE
:
251 case self
::MODE_WILDPHRASE
:
252 $result = "%" . $text . "%";
255 case self
::MODE_WILDWORDS
:
256 case self
::MODE_WILDWORDS_SUFFIX
:
257 $result = "%" . preg_replace('/[ \r\n]+/', '%', $text) . '%';
265 return $this->dedupeWildcards($result, '%');
269 * @param string $text user-supplied query string
270 * @param string $template a prototypical description of each word, eg "word%" or "word*" or "*word*"
273 protected function mapWords($text, $template) {
275 foreach ($this->parseWords($text) as $word) {
276 $result[] = str_replace('word', $word, $template);
278 return implode(' ', $result);
285 protected function parseWords($text) {
286 return explode(' ', preg_replace('/[ \r\n\t]+/', ' ', trim($text)));
294 protected function dedupeWildcards($text, $wildcard) {
295 if ($text === NULL) {
299 // don't use preg_replace because $wildcard might be special char
300 while (strpos($text, "{$wildcard}{$wildcard}") !== FALSE) {
301 $text = str_replace("{$wildcard}{$wildcard}", "{$wildcard}", $text);
306 public static function getModes() {
310 self
::MODE_WILDPHRASE
,
311 self
::MODE_WILDWORDS
,
312 self
::MODE_WILDWORDS_SUFFIX
,
316 public static function getLanguages() {
320 self
::LANG_SQL_FTSBOOL
,
328 * Ex: drush eval 'civicrm_initialize(); CRM_Utils_QueryFormatter::dumpExampleTable("firstword secondword");'
330 public static function dumpExampleTable($text) {
331 $width = strlen($text) +
8;
334 $buf .= sprintf("%-{$width}s", 'mode');
335 foreach (self
::getLanguages() as $lang) {
336 $buf .= sprintf("%-{$width}s", $lang);
340 foreach (self
::getModes() as $mode) {
341 $formatter = new CRM_Utils_QueryFormatter($mode);
342 $buf .= sprintf("%-{$width}s", $mode);
343 foreach (self
::getLanguages() as $lang) {
344 $buf .= sprintf("%-{$width}s", $formatter->format($text, $lang));