Wiki-Quellcode von Live-Table Ergebnismakros

Zuletzt geändert von Daniel Herrmann am 2026/02/07 23:25

Zeige letzte Bearbeiter
1 {{velocity output="false"}}
2 #template('hierarchy_macros.vm')
3
4 #**
5 * Macro to get the results of a livetable data call.
6 * This page is called from live grids via Ajax with the argument xpage=plain. It returns a
7 * set of results serialized in JSON.
8 *#
9 #macro(gridresult $className $collist)
10 #gridresultwithfilter($className $collist '' '' {})
11 #end
12
13
14 #**
15 * Computes the query used to retrieve the results of a live table data call.
16 * NOTE: This macro is not fully encapsulated because it defines a few Velocity variables that are used in subsequent macros.
17 *#
18 #macro(gridresultwithfilter_buildQuery $className $collist $filterfrom $filterwhere $filterParams)
19 ## Additional columns; should *not* contain raw parameters, all added column names must be filtered
20 #set($fromSql = '')
21 ## Parametrized filter part of the query
22 #set($whereSql = '')
23 ## List of values to use with $whereSql
24 #if (!$filterParams)
25 #set($filterParams = {})
26 #end
27 #if ($filterParams.entrySet())
28 #set($whereParams = {})
29 #else
30 #set($whereParams = [])
31 #end
32 #set($class = $xwiki.getDocument($className).getxWikiClass())
33 ##
34 ## Add the columns needed for the actual data
35 ##
36 #set($tablelist = [])
37 #foreach($colname in $collist)
38 ## If a classname is defined and the class field corresponding to the column name,
39 ## we check the type of the field and skip it if it's Password.
40 #livetable_getPropertyClassAndType($colname)
41 #if ($propType != '')
42 #set ($isPasswordType = $propClass.get($colname).classType == 'Password')
43 #set ($isEmailType = $propClass.get($colname).classType == 'Email')
44 #set ($emailObfuscated = $services.mail.general.shouldObfuscate())
45 #if (!($isPasswordType || ($isEmailType && $emailObfuscated)))
46 #livetable_addColumnToQuery($colname)
47 #end
48 #else
49 #livetable_addColumnToQuery($colname)
50 #end
51 #end
52 ##
53 ## Tag filtering
54 ##
55 #if($request.tag)
56 #set($fromSql = "${fromSql} , BaseObject as tobject, DBStringListProperty as tagprop")
57 #set($whereSql = "${whereSql} and tobject.className='XWiki.TagClass' and tobject.name=doc.fullName and tobject.id=tagprop.id.id and tagprop.id.name='tags' and (")
58 #foreach($tag in $request.getParameterValues('tag'))
59 #if($foreach.count > 1) #set($whereSql = "${whereSql} and ") #end
60 ## Tags are case insensitive but they are stored unchanged which means we have to normalize them when performing
61 ## a query. Unfortunately there's no simple way to match multiple tags (AND operator). If we join the list of
62 ## tags in the FROM clause then we match at least one of the tags (OR operator). The only remaining option is to
63 ## check that the list of tags contains each of the desired tags. HQL doesn't help us to lower-case the entire
64 ## list of tags so we use an inner select for this.
65 #if ($whereParams.entrySet())
66 #set($whereSql = "${whereSql} lower(:wikitag${foreach.count}) in (select lower(tag) from tagprop.list tag)")
67 #set($discard = $whereParams.put("wikitag$foreach.count", "${tag}"))
68 #else
69 #set($whereSql = "${whereSql} lower(?) in (select lower(tag) from tagprop.list tag)")
70 #set($discard = $whereParams.add("${tag}"))
71 #end
72 #end
73 #set($whereSql = "${whereSql})")
74 #end
75 ##
76 ##
77 ## Order
78 ##
79 ## if the object for the classname of the order column is not already in the from sql, put it
80 #macro(addObjectClause $objectAlias)
81 #if($fromSql.indexOf($objectAlias) < 0)
82 #set($fromSql = "${fromSql}, BaseObject $objectAlias")
83 #if ($whereParams.entrySet())
84 #set($whereSql = "${whereSql} and ${objectAlias}.name = doc.fullName and ${objectAlias}.className = :${objectAlias}_className")
85 #set($discard = $whereParams.put("${objectAlias}_className", $propClassName))
86 #else
87 #set($whereSql = "${whereSql} and ${objectAlias}.name = doc.fullName and ${objectAlias}.className = ?")
88 #set($discard = $whereParams.add($propClassName))
89 #end
90 #end
91 #end
92 ## Set the order clause for a field. We first ignore the case using the lower function (so that e.g. 'aaa' equals 'AAA')
93 ## but then consider it only for equal values (so that e.g. 'AAA' comes before 'aaa').
94 #macro(setOrderClause $fieldName $direction $useRawValue)
95 #if ($useRawValue)
96 #set($orderSql = " order by ${fieldName} ${direction}")
97 #else
98 #set($orderSql = " order by lower(${fieldName}) ${direction}, ${fieldName} ${direction}")
99 #end
100 #end
101 #set($order = "$!request.sort")
102 #if ($order == 'doc.location')
103 #set ($order = 'doc.fullName')
104 #elseif ($order == 'email' && $services.mail.general.shouldObfuscate())
105 #set ($order = '')
106 #end
107 #set ($orderSql = '')
108 #if($order != '')
109 #set($orderDirection = "$!{request.get('dir').toLowerCase()}")
110 #if("$!orderDirection" != '' && "$!orderDirection" != 'asc')
111 #set($orderDirection = 'desc')
112 #end
113 #livetable_getTableAlias($order)
114 #if($order.startsWith('doc.'))
115 ## The column is a document field.
116 ##
117 ## These document fields need to be ordered as raw values and not as strings.
118 #set($rawDocumentFields = ['translation', 'date', 'contentUpdateDate', 'creationDate', 'elements',
119 'minorEdit1', 'hidden', 'enforceRequiredRights'])
120 #set($documentField = $stringtool.removeStart($order, 'doc.'))
121 #setOrderClause(${safe_tableAlias.replace('_','.')}, ${orderDirection}, $rawDocumentFields.contains($documentField))
122 #else
123 ## The column is an object property.
124 ##
125 ## Resolve the property.
126 #livetable_getPropertyClassAndType($order)
127 #set ($multiselect = "$!{propClass.get($order).getProperty('multiSelect').getValue()}")
128 ## We can only handle single values, not multiselect ones.
129 #if ($multiselect != '1')
130 ## Some property types do not need lowercasing since they have unique values by design, so we use the raw values to order.
131 #set($rawPropertyTypes = ['NumberClass', 'BooleanClass', 'DateClass', 'LevelsClass'])
132 ## If the order column is also a filer column, this means that it was already added to the query and all we need to do is to add it to the order clause.
133 #if(!$tablelist.contains($order))
134 ## The order column is not also a filter column, so not yet defined in the query.
135 ## We need to first define it (to the from and where clauses) before we can add it to the order clause.
136 ##
137 ## Resolve the table name of the property to be used in the from clause below.
138 #livetable_getTableName($order)
139 ## If the sort column has a _class specified, join that object in
140 #set($orderObjectAlias = 'obj')
141 #if($propClassName != '' && "$!propClass" != '')
142 ## prepare the alias of the BaseObject table that corresponds to the class of this column
143 #set($orderObjectAlias = "$!{propClassName.replaceAll('[^a-zA-Z0-9_]', '')}_obj")
144 #addObjectClause($orderObjectAlias)
145 #end
146 #set($fromSql = "${fromSql}, ${tableName} ${safe_tableAlias}")
147 ## FIXME: Check if this is indeed a property of the class. Hint: $propType can be used.
148 ## Conditions are put on the object coresponding to the column of the order ($orderObjectAlias), which depends on which is the class of the $order
149 #if ($whereParams.entrySet())
150 #set($whereSql = "${whereSql} and ${orderObjectAlias}.id=${safe_tableAlias}.id.id and ${safe_tableAlias}.name = :${safe_tableAlias}_name")
151 #set($discard = $whereParams.put("${safe_tableAlias}_name", "${order}"))
152 #else
153 #set($whereSql = "${whereSql} and ${orderObjectAlias}.id=${safe_tableAlias}.id.id and ${safe_tableAlias}.name = ?")
154 #set($discard = $whereParams.add("${order}"))
155 #end
156 #end
157 ## Add the column to the order clause.
158 #setOrderClause("${safe_tableAlias}.value", ${orderDirection}, $rawPropertyTypes.contains($propType))
159 #end
160 #end
161 #end
162 ##
163 ##
164 ## Compute the final queries
165 ##
166 #if ($filterParams.entrySet())
167 #set($sqlParams = {})
168 #set($tagsMatchingParams = {})
169 #set($allMatchingParams = {})
170 #else
171 #set($sqlParams = [])
172 #set($tagsMatchingParams = [])
173 #set($allMatchingParams = [])
174 #end
175 #if("$!className" != '')
176 ## Class query
177 #if ($sqlParams.entrySet())
178 #set($sql = ", BaseObject as obj $!fromSql $!filterfrom where obj.name=doc.fullName and obj.className = :className and doc.fullName not in (:classTemplate1, :classTemplate2) $!whereSql $!filterwhere")
179 #set($discard = $sqlParams.put('className', "${className}"))
180 #set($discard = $sqlParams.put('classTemplate1', "${className}Template"))
181 #set($discard = $sqlParams.put('classTemplate2', ${className.replaceAll('Class$', 'Template')}))
182 #set($discard = $sqlParams.putAll($whereParams))
183 #else
184 #set($sql = ", BaseObject as obj $!fromSql $!filterfrom where obj.name=doc.fullName and obj.className = ? and doc.fullName not in (?, ?) $!whereSql $!filterwhere")
185 #set($discard = $sqlParams.addAll(["${className}", "${className}Template", ${className.replaceAll('Class$', 'Template')}]))
186 #set($discard = $sqlParams.addAll($whereParams))
187 #end
188 ##
189 #set($tagsMatchingFiltersFrom = ", BaseObject as obj $!fromSql $!filterfrom")
190 #if ($tagsMatchingParams.entrySet())
191 #set($tagsMatchingFiltersWhere = "obj.name=doc.fullName and obj.className = :className and doc.fullName not in (:classTemplate1, :classTemplate2) $!whereSql $!filterwhere")
192 #set($discard = $tagsMatchingParams.put('className', "${className}"))
193 #set($discard = $tagsMatchingParams.put('classTemplate1', "${className}Template"))
194 #set($discard = $tagsMatchingParams.put('classTemplate2', ${className.replaceAll('Class$', 'Template')}))
195 #set($discard = $tagsMatchingParams.putAll($whereParams))
196 #else
197 #set($tagsMatchingFiltersWhere = "obj.name=doc.fullName and obj.className = ? and doc.fullName not in (?, ?) $!whereSql $!filterwhere")
198 #set($discard = $tagsMatchingParams.addAll(["${className}", "${className}Template", ${className.replaceAll('Class$', 'Template')}]))
199 #set($discard = $tagsMatchingParams.addAll($whereParams))
200 #end
201 ##
202 #set($allMatchingTagsFrom = ", BaseObject as obj $!filterfrom")
203 #if ($allMatchingParams.entrySet())
204 #set($allMatchingTagsWhere = "obj.name=doc.fullName and obj.className = :className and doc.fullName not in (:classTemplate1, :classTemplate2) $!filterwhere")
205 #set($discard = $allMatchingParams.put('className', "${className}"))
206 #set($discard = $allMatchingParams.put('classTemplate1', "${className}Template"))
207 #set($discard = $allMatchingParams.put('classTemplate2', ${className.replaceAll('Class$', 'Template')}))
208 #else
209 #set($allMatchingTagsWhere = "obj.name=doc.fullName and obj.className = ? and doc.fullName not in (?, ?) $!filterwhere")
210 #set($discard = $allMatchingParams.addAll(["${className}", "${className}Template", ${className.replaceAll('Class$', 'Template')}]))
211 #end
212 ##
213 #if($filterParams)
214 #if ($filterParams.entrySet())
215 #set($discard = $sqlParams.putAll($filterParams))
216 #set($discard = $tagsMatchingParams.putAll($filterParams))
217 #set($discard = $allMatchingParams.putAll($filterParams))
218 #else
219 #set($discard = $sqlParams.addAll($filterParams))
220 #set($discard = $tagsMatchingParams.addAll($filterParams))
221 #set($discard = $allMatchingParams.addAll($filterParams))
222 #end
223 #end
224 #else
225 ## Document query
226 #set($sql = "$!fromSql $!filterfrom where 1=1 $!whereSql $!filterwhere")
227 #if ($whereParams.entrySet())
228 #set($discard = $sqlParams.putAll($whereParams))
229 #else
230 #set($discard = $sqlParams.addAll($whereParams))
231 #end
232 ##
233 #set($tagsMatchingFiltersFrom = "$!fromSql $!filterfrom")
234 #set($tagsMatchingFiltersWhere = "1=1 $!whereSql $!filterwhere")
235 #if ($whereParams.entrySet())
236 #set($discard = $tagsMatchingParams.putAll($whereParams))
237 #else
238 #set($discard = $tagsMatchingParams.addAll($whereParams))
239 #end
240 ##
241 #set($allMatchingTagsFrom = "$!filterfrom")
242 #set($allMatchingTagsWhere = "1=1 $!filterwhere")
243 ##
244 #if($filterParams)
245 #if ($filterParams.entrySet())
246 #set($discard = $sqlParams.putAll($filterParams))
247 #set($discard = $tagsMatchingParams.putAll($filterParams))
248 #set($discard = $allMatchingParams.putAll($filterParams))
249 #else
250 #set($discard = $sqlParams.addAll($filterParams))
251 #set($discard = $tagsMatchingParams.addAll($filterParams))
252 #set($discard = $allMatchingParams.addAll($filterParams))
253 #end
254 #end
255 #end
256 #if($orderSql != '')
257 #set($sql = "$sql $!{orderSql}")
258 #end
259 #end
260 #**
261 * Adds TagCloud information to the JSON returned by a live table data call.
262 * NOTE: This macro uses Velocity variables defined by gridresultwithfilter_buildQuery.
263 *
264 * @param $map stores the JSON in memory so that it can be adjusted before serialization
265 *#
266 #macro(gridresult_buildTagCloudJSON $map)
267 ##
268 ## TagCloud matching the current filters
269 ##
270 #set($tagsMatchingFilters = $xwiki.tag.getTagCountForQuery($tagsMatchingFiltersFrom, $tagsMatchingFiltersWhere, $tagsMatchingParams))
271 ## FIXME: We use a map just because the client expects an object, but all we really need is a list..
272 #set($matchingTags = {})
273 #foreach($tag in $tagsMatchingFilters.keySet())
274 ## NOTE: The value doesn't have a special meaning. I've used 1 just because it takes less space when serialized.
275 #set($discard = $matchingTags.put($tag, 1))
276 #end
277 #set($discard = $map.put('matchingtags', $matchingTags))
278 ##
279 ## TagCloud matching all the documents used by the live table
280 ##
281 ## If all the query parameters are the same as for $tagsMatchingFilters, no need to run the query again.
282 ## This optimization allows to divide the time to compute the tagcloud by 2 when the table has no filters applied.
283 #if ($allMatchingTagsFrom.trim() != $tagsMatchingFiltersFrom.trim() || $allMatchingTagsWhere.trim() != $tagsMatchingFiltersWhere.trim() || $tagsMatchingParams != $allMatchingParams)
284 #set($allMatchingTags = $xwiki.tag.getTagCountForQuery($allMatchingTagsFrom, $allMatchingTagsWhere, $allMatchingParams))
285 #else
286 #set($allMatchingTags = $tagsMatchingFilters)
287 #end
288 ## FIXME: We use a list of maps just because the client expects an array, but we should simply return $allMatchingTags..
289 #set($tags = [])
290 #foreach($tag in $allMatchingTags.keySet())
291 #set($discard = $tags.add({'tag': $tag, 'count': $allMatchingTags.get($tag)}))
292 #end
293 #set($discard = $map.put('tags', $tags))
294 #end
295
296
297 #**
298 * Adds information about each live table row to the JSON returned by a live table data call.
299 * NOTE: This macro uses Velocity variables defined by gridresultwithfilter_buildQuery.
300 *
301 * @param $map stores the JSON in memory so that it can be adjusted before serialization
302 *#
303 #macro(gridresult_buildRowsJSON $map)
304 #set($offset = $numbertool.toNumber($request.get('offset')).intValue())
305 ## Offset starts from 0 in velocity and 1 in javascript
306 #set($offset = $offset - 1)
307 #if(!$offset || $offset < 0)
308 #set($offset = 0)
309 #end
310 #getAndValidateQueryLimitFromRequest('limit', 15, $limit)
311 #set($query = $services.query.hql($sql))
312 ## Apply query filters if defined. Otherwise use default.
313 #foreach ($queryFilter in $stringtool.split($!request.queryFilters, ', '))
314 #set ($query = $query.addFilter($queryFilter))
315 #end
316 #set ($query = $query.setLimit($limit).setOffset($offset).bindValues($sqlParams))
317 #set($items = $query.execute())
318 #set($discard = $map.put('totalrows', $query.count()))
319 #if ($limit > 0)
320 #set($discard = $map.put('returnedrows', $mathtool.min($items.size(), $limit)))
321 #else
322 ## When the limit is 0, it's equivalent to no limit at all and the actual number of returned results can be used.
323 #set($discard = $map.put('returnedrows', $items.size()))
324 #end
325 #set($discard = $map.put('offset', $mathtool.add($offset, 1)))
326 #set($rows = [])
327 #foreach($item in $items)
328 #gridresult_buildRowJSON($item $rows)
329 #end
330 #set ($discard = $map.put('rows', $rows))
331 #livetable_filterObfuscated($map)
332 #end
333
334
335 #**
336 * Adds information about the specified live table row to the JSON returned by a live table data call.
337 * NOTE: This macro uses Velocity variables available in gridresult_buildRowsJSON.
338 *
339 * @param $item the name of the document that feeds this live table row
340 * @param $rows stores the JSON in memory so that it can be adjusted before serialization
341 *#
342 #macro(gridresult_buildRowJSON $item $rows)
343 ## Handle both the case where the "language" filter is used and thus languages are returned too and the case where
344 ## only the document name is returned. When more than the document name is returned the $item variable is a list.
345 #if($item.size())
346 ## Extract doc name and doc language from $item
347 #set($docName = $item[0])
348 #set($docLanguage = $item[1])
349 #else
350 #set($docName = $item)
351 #set($docLanguage = '')
352 #end
353 #set ($docReference = $services.model.resolveDocument($docName))
354 #set ($isViewable = $services.security.authorization.hasAccess('view', $docReference))
355 #if ($isViewable)
356 #set ($row = {
357 'doc_viewable': $isViewable,
358 'doc_fullName': $services.model.serialize($docReference, 'local'),
359 'doc_space': $services.model.serialize($docReference.parent, 'local'),
360 'doc_location': "#hierarchy($docReference, {'limit': 5, 'plain': false, 'local': true, 'displayTitle': false})",
361 'doc_url': $xwiki.getURL($docReference),
362 'doc_space_url': $xwiki.getURL($docReference.parent),
363 'doc_wiki': $docReference.wikiReference.name,
364 'doc_wiki_url': $xwiki.getURL($docReference.wikiReference),
365 'doc_hasadmin': $xwiki.hasAdminRights(),
366 'doc_hasedit': $services.security.authorization.hasAccess('edit', $docReference),
367 'doc_hasdelete': $services.security.authorization.hasAccess('delete', $docReference),
368 'doc_edit_url': $xwiki.getURL($docReference, 'edit'),
369 'doc_copy_url': $xwiki.getURL($docReference, 'view', 'xpage=copy'),
370 'doc_delete_url': $xwiki.getURL($docReference, 'delete'),
371 'doc_rename_url': $xwiki.getURL($docReference, 'view', 'xpage=rename&step=1')
372 })
373 #set ($isTranslation = "$!docLanguage" != '' && $xwiki.getLanguagePreference() != $docLanguage)
374 ## Display the language after the document name so that not all translated documents have the same name displayed.
375 #set ($row.doc_name = "$docReference.name#if ($isTranslation) ($docLanguage)#end")
376 #set ($row.doc_hascopy = $row.doc_viewable)
377 #set ($row.doc_hasrename = $row.doc_hasdelete)
378 #set ($row.doc_hasrights = $row.doc_hasedit && $isAdvancedUser)
379 #if ($docReference.name == 'WebHome')
380
381 ## For nested pages, use the page administration.
382 #set ($webPreferencesReference = $services.model.createDocumentReference(
383 'WebPreferences', $docReference.lastSpaceReference))
384 #set ($row.doc_rights_url = $xwiki.getURL($webPreferencesReference, 'admin',
385 'editor=spaceadmin&section=PageRights'))
386 #else
387 ## For terminal pages, use the old rights editor.
388 ## TODO: We should create a page administration for terminal pages too.
389 #set ($row.doc_rights_url = $xwiki.getURL($docReference, 'edit', 'editor=rights'))
390 #end
391 #if ($row.doc_viewable)
392 #set ($itemDoc = $xwiki.getDocument($docReference))
393 ## Handle translations. We need to make sure we display the data associated to the correct document if the returned
394 ## result is a translation.
395 #if ($isTranslation)
396 #set ($translatedDoc = $itemDoc.getTranslatedDocument($docLanguage))
397 #else
398 #set ($translatedDoc = $itemDoc.translatedDocument)
399 #end
400 #set($discard = $itemDoc.use($className))
401 #set($discard = $row.put('doc_objectCount', $itemDoc.getObjectNumbers($className)))
402 #set($discard = $row.put('doc_edit_url', $itemDoc.getURL($itemDoc.defaultEditMode)))
403 #set($discard = $row.put('doc_date', $xwiki.formatDate($translatedDoc.date)))
404 #set($discard = $row.put('doc_title', $translatedDoc.plainTitle))
405 #set($rawTitle = $translatedDoc.title)
406 #if($rawTitle != $row['doc_title'])
407 #set($discard = $row.put('doc_title_raw', $rawTitle))
408 #end
409 #set ($metadataAuthor = $translatedDoc.authors.originalMetadataAuthor)
410 #if ($metadataAuthor == $services.user.getGuestUserReference())
411 ## Special handling for guest so that it displays unknown user.
412 #set($discard = $row.put('doc_author', $xwiki.getPlainUserName($NULL)))
413 #else
414 #set($discard = $row.put('doc_author', $xwiki.getPlainUserName($metadataAuthor)))
415 #end
416
417 #set($discard = $row.put('doc_author_url', $xwiki.getURL($metadataAuthor)))
418 #set($discard = $row.put('doc_creationDate', $xwiki.formatDate($translatedDoc.creationDate)))
419 #set($discard = $row.put('doc_creator', $xwiki.getPlainUserName($translatedDoc.creatorReference)))
420 #set($discard = $row.put('doc_hidden', $translatedDoc.isHidden()))
421 #set($discard = $row.put('doc_enforceRequiredRights', $itemDoc.isEnforceRequiredRights()))
422 #foreach($colname in $collist)
423 #gridresult_buildColumnJSON($colname $row)
424 #end
425 #end
426 #else
427 #set ($row = {
428 'doc_viewable': $isViewable,
429 'doc_fullName': 'obfuscated'
430 })
431 #end
432 #set($discard = $rows.add($row))
433 #end
434
435
436 #**
437 * Adds information about the given column to the JSON returned by a live table data call.
438 * NOTE: This macro uses Velocity variables available in gridresult_buildRowJSON.
439 *
440 * @param $colname the name of the live table column for which to retrieve information
441 * @param $row stores the JSON in memory so that it can be adjusted before serialization
442 *#
443 #macro(gridresult_buildColumnJSON $colname $row)
444 #if($colname.startsWith('doc.'))
445 #elseif($colname == '_action')
446 #set($discard = $row.put($colname, $services.localization.render("${request.transprefix}actiontext")))
447 #elseif($colname == '_attachments')
448 #livetable_getAttachmentsList($translatedDoc)
449 #set($discard = $row.put($colname, $attachlist))
450 #elseif($colname == '_avatar')
451 #livetable_getAvatar($itemDoc)
452 #set($discard = $row.put($colname, $avatar))
453 #elseif($colname == '_images')
454 #livetable_getImagesList($itemDoc)
455 #set($discard = $row.put($colname, $imagesList))
456 ## Output likes if they are available.
457 #elseif($colname == '_likes' && "$!services.like" != "")
458 #set($likes = $services.like.getLikes($docReference))
459 #if ($likes.isPresent())
460 #set($discard = $row.put('_likes', $likes.get()))
461 #end
462 #else
463 #livetable_getPropertyClassAndType($colname)
464 #if(!$propClass.equals($class))
465 #set($discard = $itemDoc.use($propClassName))
466 #end
467 #set($fieldObject = $itemDoc.getFirstObject($colname))
468 #set($fieldProperty = $fieldObject.getProperty($colname))
469 #if ($fieldProperty.getPropertyClass().classType == 'Password')
470 #set($fieldValue = '********')
471 #elseif ($fieldProperty.getPropertyClass().classType == 'Email' && $services.mail.general.shouldObfuscate())
472 #set ($fieldValue = $services.mail.general.obfuscate("$!fieldProperty.getValue()"))
473 #else
474 #set($fieldValue = "$!fieldProperty.getValue()")
475 #end
476 #set($fieldDisplayValue = "#unwrapXPropertyDisplay($itemDoc.display($colname, 'view'))")
477 #if($fieldDisplayValue == '')
478 #set($fieldDisplayValue = $services.localization.render("${request.transprefix}emptyvalue"))
479 #end
480 #set($fieldUrl = '')
481 ## Only retrieve an URL for a DBListClass item
482 #if(($propType == 'DBListClass' || $propType == 'PageClass') && $propClass.get($colname).getProperty('multiSelect').value != 1)
483 #set($fieldUrl = $xwiki.getURL($fieldValue))
484 #if($fieldUrl == $xwiki.getURL($services.model.resolveDocument('', 'default', $doc.documentReference.extractReference('WIKI'))))
485 #set($fieldUrl = '')
486 #end
487 #end
488 #set($discard = $row.put($colname, $fieldDisplayValue))
489 #set($discard = $row.put("${colname}_value", $fieldValue))
490 #set($discard = $row.put("${colname}_url", $fieldUrl))
491 ## Reset to the default class
492 #set($discard = $itemDoc.use($className))
493 #end
494 #end
495
496
497 #**
498 * Builds the JSON response to a live table data call.
499 *
500 * @param $map stores the JSON in memory so that it can be adjusted before serialization
501 *#
502 #macro(gridresultwithfilter_buildJSON $className $collist $filterfrom $filterwhere $filterParams $map)
503 #gridresultwithfilter_buildQuery($className $collist $filterfrom $filterwhere $filterParams)
504 #if("$!request.sql" == '1')
505 #set($discard = $map.put('sql', $sql))
506 #set($discard = $map.put('params', $sqlParams))
507 #end
508 #set($discard = $map.put('reqNo', $numbertool.toNumber($request.reqNo).intValue()))
509 #gridresult_buildTagCloudJSON($map)
510 #gridresult_buildRowsJSON($map)
511 #end
512
513
514 #**
515 * Builds the JSON response to a live table data call.
516 *
517 * @param $map stores the JSON in memory so that it can be adjusted before serialization
518 *#
519 #macro(gridresult_buildJSON $className $collist $map)
520 #gridresultwithfilter_buildJSON($className $collist '' '' {} $map)
521 #end
522
523
524 #**
525 * Macro to get the results of a livetable data call.
526 * This page is called from live grids via Ajax with the argument xpage=plain. It returns a
527 * set of results serialized in JSON.
528 *#
529 #macro(gridresultwithfilter $className $collist $filterfrom $filterwhere $filterParams)
530 #if($xcontext.action == 'get' && "$!{request.outputSyntax}" == 'plain')
531 ## Build the JSON in memory (using basic Java data types) so that it can be adjusted before serialization.
532 #set($map = {})
533 #gridresultwithfilter_buildJSON($className $collist $filterfrom $filterwhere $filterParams $map)
534 #jsonResponse($map)
535 #end
536 #end
537
538
539 #**
540 * Get the name of the Property that should be used for a given livetable column.
541 * NOTE the resulting $tableName is safe to use inside SQL queries
542 *#
543 #macro(livetable_getTableName $colname)
544 #livetable_getPropertyClassAndType($colname)
545 #if($propType == 'NumberClass')
546 #set($numberType = $propClass.get($colname).getProperty('numberType').getValue())
547 #if($numberType == 'integer')
548 #set($tableName = 'IntegerProperty')
549 #elseif($numberType == 'float')
550 #set($tableName = 'FloatProperty')
551 #elseif($numberType == 'double')
552 #set($tableName = 'DoubleProperty')
553 #else
554 #set($tableName = 'LongProperty')
555 #end
556 #elseif($propType == 'BooleanClass')
557 #set($tableName = 'IntegerProperty')
558 #elseif($propType == 'DateClass')
559 #set($tableName = 'DateProperty')
560 #elseif($propType == 'TextAreaClass' || $propType == 'UsersClass' || $propType == 'GroupsClass')
561 #set($tableName = 'LargeStringProperty')
562 #elseif($propType == 'StaticListClass' || $propType == 'DBListClass' || $propType == 'DBTreeListClass' || $propType == 'PageClass')
563 ## The following logic is mirrored from ListClass and might need to be updated when the logic in ListClass changes.
564 #set($multiSelect = $propClass.get($colname).getProperty('multiSelect').getValue())
565 #set($relationalStorage = $propClass.get($colname).getProperty('relationalStorage').getValue())
566 #set($largeStorage = $propClass.get($colname).getProperty('largeStorage').getValue())
567 #if($multiSelect == 1)
568 #if($relationalStorage == 1)
569 #set($tableName = 'DBStringListProperty')
570 #else
571 #set($tableName = 'StringListProperty')
572 #end
573 #elseif($largeStorage == 1)
574 #set($tableName = 'LargeStringProperty')
575 #else
576 #set($tableName = 'StringProperty')
577 #end
578 #else
579 #set($tableName = 'StringProperty')
580 #end
581 #end
582
583 #**
584 * Get the property class and type for a given livetable column.
585 *#
586 #macro(livetable_getPropertyClassAndType $colname)
587 #set($propClassName = "$!request.get(${colname.concat('_class')})")
588 #if($propClassName != '')
589 #set($propClass = $xwiki.getDocument($propClassName).getxWikiClass())
590 #else
591 #set($propClass = $class)
592 #end
593 #set($propType = '')
594 #if($propClass.getPropertyNames().contains($colname))
595 #set($propType = "$!{propClass.get($colname).type}")
596 #end
597 #end
598
599 #**
600 * Old alias of the #livetable_getTableName macro.
601 * @deprecated since 2.2.3, use {@link #livetable_getTableName}
602 *#
603 #macro(grid_gettablename $colname)
604 #livetable_getTableName($colname)
605 #end
606
607
608
609 #**
610 * List attachments for a document, putting the result as HTML markup in the $attachlist variable.
611 *#
612 #macro(livetable_getAttachmentsList $itemDoc)
613 #set($attachlist = '')
614 #foreach($attachment in $itemDoc.attachmentList)
615 #set($attachmentUrl = $itemDoc.getAttachmentURL($attachment.filename))
616 #set($attachlist = "${attachlist}<a href='${attachmentUrl}'>$attachment.filename</a><br/>")
617 #end
618 #end
619
620 #**
621 * Old alias of the #livetable_getAttachmentsList macro.
622 * @deprecated since 2.2.3, use {@link #livetable_getAttachmentsList}
623 *#
624 #macro(grid_attachlist $itemDoc)
625 #livetable_getAttachmentsList($itemDoc)
626 #end
627
628
629
630 #**
631 * List image attachments for a document, putting the result as HTML markup in the $imagesList variable.
632 *#
633 #macro(livetable_getImagesList $itemDoc)
634 #set($imagesList = '')
635 #foreach ($attachment in $itemDoc.attachmentList)
636 #if($attachment.isImage())
637 ## Create a thumbnail by resizing the image on the server side, if needed, to fit inside a 50x50 pixel square.
638 #set($thumbnailURL = $itemDoc.getAttachmentURL($attachment.filename, 'download', "width=50&height=50&keepAspectRatio=true"))
639 #set($imageURL = $itemDoc.getAttachmentURL($attachment.filename))
640 #set($imagesList = "${imagesList}<a href=""$imageURL""><img src=""$thumbnailURL"" alt=""$attachment.filename"" title=""$attachment.filename"" /></a>")
641 #end
642 #end
643 #end
644
645 #**
646 * Old alias of the #livetable_getImagesList macro.
647 * @deprecated since 2.2.3, use {@link #livetable_getImagesList}
648 *#
649 #macro(grid_photolist $itemDoc)
650 #livetable_getImagesList($itemDoc)
651 #end
652
653
654 #**
655 * Generate the HTML code for a user avatar.
656 *#
657 #macro(livetable_getAvatar $itemDoc)
658 #set ($avatar = "#mediumUserAvatar($itemDoc.fullName)")
659 #set ($avatar = $avatar.trim())
660 #end
661
662 #**
663 * Old alias of the #livetable_getAvatar macro.
664 * @deprecated since 2.2.3, use {@link #livetable_getAvatar}
665 *#
666 #macro(grid_avatar $itemDoc)
667 #livetable_getAvatar($itemDoc)
668 #end
669
670
671
672 #**
673 * Macro to extend the query to select the properties for the livetable columns.
674 * NOTE $colName is filtered (all characters but [a-zA-Z0-9_.] are removed) before use
675 *#
676 #macro (livetable_addColumnToQuery $colName)
677 ## Safe because / is not allowed in property names
678 ## The $joinModeMarker is used in #livetable_filterDBStringListProperty.
679 #set ($joinModeMarker = "/join_mode")
680 #if (!$colName.endsWith($joinModeMarker))
681 #set ($filterValue = "$!request.getParameter($colName)")
682 #if ("$!filterValue" != '')
683 #set ($discard = $tablelist.add($colName))
684 ## Some columns may support filtering with multiple constraints (multiple filter values).
685 #set ($filterValues = $request.getParameterValues($colName))
686 #if ($colName.startsWith('doc.'))
687 #if ($colName == 'doc.location')
688 #set ($safeColName = 'doc.fullName')
689 ## Use filterLocation since addLivetableLocationFilter is buggy when called several times (it'll add the
690 ## same HQL binding name every time it's called! See https://jira.xwiki.org/browse/XWIKI-17463).
691 ## Also note that we don't call addLocationFilter since we use a Map for $params.
692 #filterLocation($whereSql, $whereParams, $filterValue, 'locationFilterValue2', true)
693 #elseif ($colName == 'doc.date' || $colName == 'doc.creationDate' || $colName == 'doc.contentUpdateDate')
694 #livetable_getTableAlias($colName)
695 #livetable_filterDateProperty()
696 #elseif ($colName == 'doc.hidden' || $colName == 'doc.minorEdit1' || $colName == 'doc.enforceRequiredRights')
697 ## Boolean document fields need special handling to work across all databases
698 ## (HSQLDB/PostgreSQL use true/false, MySQL/MariaDB/Oracle use 1/0).
699 ## Support both true/false and 1/0 as Live Data uses the former while LiveTable uses the latter.
700 #set ($booleanValue = ($filterValue.toLowerCase() == 'true' || $filterValue == '1'))
701 ## No need to clean the column name since it's only one of the given values.
702 #if ($whereParams.entrySet())
703 #set ($whereSql = "${whereSql} and $colName = :${colName.replace('.', '_')}_filter")
704 #set ($discard = $whereParams.put("${colName.replace('.', '_')}_filter", $booleanValue))
705 #else
706 #set ($whereSql = "${whereSql} and $colName = ?")
707 #set ($discard = $whereParams.add($booleanValue))
708 #end
709 #else
710 #set ($safeColName = $colName.replaceAll('[^a-zA-Z0-9_.]', '').replace('_', '.'))
711 #if ($whereParams.entrySet())
712 #set ($whereSql = "${whereSql} and upper(str($safeColName)) like upper(:${safeColName.replace('.', '_')}_filter)")
713 #set ($discard = $whereParams.put("${safeColName.replace('.', '_')}_filter", "%$filterValue%"))
714 #else
715 #set ($whereSql = "${whereSql} and upper(str($safeColName)) like upper(?)")
716 #set ($discard = $whereParams.add("%$filterValue%"))
717 #end
718 #end
719 #else
720 #livetable_filterProperty($colName)
721 #end
722 #end
723 #end
724 #end
725
726
727 #**
728 * Determine how the filter values should be matched against the stored values. This macro sets two variables:
729 * <ul>
730 * <li>$matchType: use this when the specified column supports only a single filter value</li>
731 * <li>$matchTypes: use this when the specified column supports multiple filter values.</li>
732 * </ul>
733 *
734 * @param column the column name; each column can have a different match type
735 * @param filterValueCount the number of filter values for which to determine the match type; each filter value can have
736 * a different match type
737 * @param defaultMatchType the default match type to use for the given column when the request doesn't specify one
738 *#
739 #macro (livetable_getMatchTypes $column $filterValueCount $defaultMatchType)
740 #set ($macro.matchTypes = $request.getParameterValues("${column}_match"))
741 #if (!$macro.matchTypes || $macro.matchTypes.isEmpty())
742 ## No match type specified for this column.
743 #set ($matchType = $defaultMatchType)
744 #set ($matchTypes = $stringtool.repeat($matchType, ',', $filterValueCount).split(','))
745 #else
746 ## At least one match type specified for this column.
747 #set ($matchType = $macro.matchTypes.get(0))
748 #set ($matchTypes = [])
749 #set ($discard = $matchTypes.addAll($macro.matchTypes.subList(0, $mathtool.min($macro.matchTypes.size(),
750 $filterValueCount))))
751 #if ($matchTypes.size() < $filterValueCount)
752 ## Add missing match types.
753 #set ($discard = $matchTypes.addAll($stringtool.repeat($matchType, ',', $mathtool.sub($filterValueCount,
754 $matchTypes.size())).split(',')))
755 #end
756 #end
757 #end
758
759
760 #macro (livetable_filterProperty $colname)
761 #livetable_getTableAlias($colname)
762 #livetable_getTableName($colname)
763 #set ($fromSql = "$fromSql, $tableName as $safe_tableAlias")
764 ##
765 ## If the column is not from $class, we need to make sure we join with the proper table.
766 #set ($filterObjectAlias = 'obj')
767 #set ($propClass = $class)
768 #set ($propClassName = $request.getParameter("${colname}_class"))
769 #if ("$!propClassName" != '')
770 #set ($propClass = $xwiki.getDocument($propClassName).getxWikiClass())
771 #if ("$!propClass" != '')
772 ## Prepare the alias of the BaseObject table that corresponds to the class of this column
773 ## Property table is to be joined with its object, determined depending on $propClassName.
774 #set ($filterObjectAlias = "$!{propClassName.replaceAll('[^a-zA-Z0-9_]', '')}_obj")
775 #addObjectClause($filterObjectAlias)
776 #end
777 #end
778 #if ($whereParams.entrySet())
779 #set ($joinObjectTable = "${filterObjectAlias}.id = ${safe_tableAlias}.id.id and ${safe_tableAlias}.id.name = :${safe_tableAlias}_id_name")
780 #set ($discard = $whereParams.put("${safe_tableAlias}_id_name", $colname))
781 #else
782 #set ($joinObjectTable = "${filterObjectAlias}.id = ${safe_tableAlias}.id.id and ${safe_tableAlias}.id.name = ?")
783 #set ($discard = $whereParams.add($colname))
784 #end
785 #set ($whereSql = "$whereSql and $joinObjectTable")
786 ##
787 ## We determine the default match type (when not specified) based on the property meta class (e.g. DateClass).
788 #set ($propMetaClass = $NULL)
789 #if ($propClass && $propClass.getPropertyNames().contains($colname))
790 #set ($propMetaClass = $propClass.get($colname).type)
791 #end
792 ##
793 #set ($numberProperties = ['IntegerProperty', 'LongProperty', 'FloatProperty', 'DoubleProperty'])
794 #if ($numberProperties.contains($tableName))
795 #livetable_filterNumberProperty()
796 #elseif ($tableName == 'DateProperty')
797 #livetable_filterDateProperty()
798 #elseif ($tableName == 'DBStringListProperty')
799 #livetable_filterDBStringListProperty()
800 #elseif ($tableName == 'StringListProperty')
801 #livetable_filterStringListProperty()
802 #else
803 ## StringProperty or LargeStringProperty
804 #livetable_filterStringProperty()
805 #end
806 #end
807
808
809 #**
810 * NOTE: This macro uses variables defined in livetable_filterProperty . It was not meant to be used alone.
811 *#
812 #macro (livetable_filterNumberProperty)
813 #set($numberValue = $numbertool.toNumber($filterValue))
814 #if($tableName == 'IntegerProperty' || $tableName == 'LongProperty')
815 #if($tableName == 'LongProperty')
816 #set($numberValue = $numberValue.longValue())
817 #else
818 ## IntegerProperty
819 #set($numberValue = $numberValue.intValue())
820 #end
821 #if ($whereParams.entrySet())
822 #set($whereSql = "${whereSql} and ${safe_tableAlias}.value = :${safe_tableAlias}_value")
823 #set($discard = $whereParams.put("${safe_tableAlias}_value", $numberValue))
824 #else
825 #set($whereSql = "${whereSql} and ${safe_tableAlias}.value = ?")
826 #set($discard = $whereParams.add($numberValue))
827 #end
828 #else
829 #if($tableName == 'FloatProperty')
830 #set($numberValue = $numberValue.floatValue())
831 #else
832 ## DoubleProperty
833 #set($numberValue = $numberValue.doubleValue())
834 #end
835 #set($precision = 0.000001)
836 #if ($whereParams.entrySet())
837 #set($whereSql = "${whereSql} and abs(:${safe_tableAlias}_value - ${safe_tableAlias}.value) <= ${precision}")
838 #set($discard = $whereParams.put("${safe_tableAlias}_value", $numberValue))
839 #else
840 #set($whereSql = "${whereSql} and abs(? - ${safe_tableAlias}.value) <= ${precision}")
841 #set($discard = $whereParams.add($numberValue))
842 #end
843 #end
844 #end
845
846
847 #**
848 * NOTE: This macro uses variables defined in livetable_filterProperty . It was not meant to be used alone.
849 *#
850 #macro (livetable_filterDateProperty)
851 #if ($safe_tableAlias.startsWith('doc.'))
852 #set ($dateProperty = $safe_tableAlias)
853 #else
854 #set ($dateProperty = "${safe_tableAlias}.value")
855 #end
856 #set ($safeDateProperty = $dateProperty.replace('.', '_'))
857 #set ($dateRange = {})
858 ## Perform partial string matching by default if no match type is specified.
859 ## Note that for the moment we support only one filter value (e.g. one date range) and thus only the first match type
860 ## is taken into account.
861 #livetable_getMatchTypes($colname $filterValues.size() 'partial')
862 #parseDateRange($matchType $filterValue $dateRange)
863 #if ($dateRange.start || $dateRange.end)
864 ## Date range.
865 #if ($dateRange.start)
866 #if ($whereParams.entrySet())
867 #set ($whereSql = "${whereSql} and $dateProperty >= :${safeDateProperty}1")
868 #set ($discard = $whereParams.put("${safeDateProperty}1", $dateRange.start))
869 #else
870 #set ($whereSql = "${whereSql} and $dateProperty >= ?")
871 #set ($discard = $whereParams.add($dateRange.start))
872 #end
873 #end
874 #if ($dateRange.end)
875 #if ($whereParams.entrySet())
876 #set ($whereSql = "${whereSql} and $dateProperty <= :${safeDateProperty}2")
877 #set ($discard = $whereParams.put("${safeDateProperty}2", $dateRange.end))
878 #else
879 #set ($whereSql = "${whereSql} and $dateProperty <= ?")
880 #set ($discard = $whereParams.add($dateRange.end))
881 #end
882 #end
883 #else
884 ## String matching (contains).
885 #if ($whereParams.entrySet())
886 #set ($whereSql = "${whereSql} and upper(str($dateProperty)) like upper(:$safeDateProperty)")
887 #set ($discard = $whereParams.put($safeDateProperty, "%$filterValue%"))
888 #else
889 #set ($whereSql = "${whereSql} and upper(str($dateProperty)) like upper(?)")
890 #set ($discard = $whereParams.add("%$filterValue%"))
891 #end
892 #end
893 #end
894
895
896 #**
897 * NOTE: This macro uses variables defined in livetable_filterProperty . It was not meant to be used alone.
898 *#
899 #macro (livetable_filterDBStringListProperty)
900 ## Perform exact matching by default if no match type is specified.
901 ## For DBStringList properties we still apply a single match type to all non-empty values, but we also allow
902 ## combining the special "empty" match type with other match types.
903 #livetable_getMatchTypes($colname $filterValues.size() 'exact')
904 #livetable_getJoinOperator($colname)
905
906 ## Collect non-empty filter values (those whose match type is not 'empty').
907 #set ($nonEmptyValues = [])
908 #set ($hasEmpty = false)
909 #set ($matchType = 'invalid')
910 #foreach ($filterValue in $filterValues)
911 #if ($matchTypes.get($foreach.index) == 'empty')
912 #set ($hasEmpty = true)
913 ## When we want to match empty values, we can't have other match types than exact for non-empty values as for
914 ## other match types, we need to join with the list of values, which is not compatible with checking for
915 ## emptiness.
916 #set ($matchType = 'exact')
917 #elseif ("$!filterValue" != '')
918 #set ($discard = $nonEmptyValues.add($filterValue))
919 ## Store the first non-empty match type.
920 #if ($matchType == 'invalid')
921 #set ($matchType = $matchTypes.get($foreach.index))
922 #end
923 #end
924 #end
925
926 ## 1) Apply the non-empty constraints.
927 #if (!$nonEmptyValues.isEmpty())
928 #if ($matchType == 'partial' || $matchType == 'prefix')
929 ## We need to join with the list of values in order to be able to use the LIKE operator.
930 #set ($matchTarget = "${safe_tableAlias}_item")
931 #if ($whereParams.entrySet())
932 #set ($paramPrefix = "${safe_tableAlias}_item_")
933 #else
934 #set ($paramPrefix = $NULL)
935 #end
936 #set ($joinPos = $mathtool.add($fromSql.lastIndexOf(" $safe_tableAlias"), $mathtool.add($safe_tableAlias.length(), 1)))
937 #set ($fromSql = "$fromSql.substring(0, $joinPos) join ${safe_tableAlias}.list as $matchTarget $fromSql.substring($joinPos)")
938 #else
939 ## Fall-back on exact matching even if the match type is specified, when its value is not supported.
940 #set ($matchType = 'exact')
941 #set ($matchTarget = "${safe_tableAlias}.list")
942 #if ($whereParams.entrySet())
943 #set ($paramPrefix = "${safe_tableAlias}_list_")
944 #else
945 #set ($paramPrefix = $NULL)
946 #end
947 #end
948
949 #set ($filterQuery = "#livetable_getFilterQuery($matchTarget $matchType true $nonEmptyValues.size() $paramPrefix $NULL)")
950 #if (!$hasEmpty)
951 ## Only non-empty values are used, combine directly with the existing constraints, otherwise, they will be
952 ## combined later together with the empty constraint.
953 #set ($whereSql = "$whereSql and ($filterQuery.trim())")
954 #end
955 #foreach ($filterValue in $nonEmptyValues)
956 #livetable_addFilterParam($filterValue $matchType $whereParams "${paramPrefix}${foreach.count}")
957 #end
958 #end
959
960 ## 2) Optionally add a single constraint if any match type is 'empty'.
961 #if ($hasEmpty)
962 ## "empty" means that there is no list item stored for this property on the filtered object.
963 ## The proper way to check for that would be "${safe_tableAlias}.list IS EMPTY", but JSQL cannot parse "IS EMPTY"
964 ## which means that we cannot use it without programming right.
965 #set ($emptyConstraint = "size(${safe_tableAlias}.list) = 0")
966 #if ($nonEmptyValues.isEmpty())
967 ## Only 'empty' is used, combine with the existing constraints.
968 #set ($whereSql = "${whereSql} and ${emptyConstraint}")
969 #else
970 ## Combine non-empty group and empty condition using the join operator.
971 #set ($whereSql = "${whereSql} and ($filterQuery.trim() ${joinOperator} ${emptyConstraint})")
972 #end
973 #end
974 #end
975
976
977 #**
978 * NOTE: This macro uses variables defined in livetable_filterProperty . It was not meant to be used alone.
979 *#
980 #macro (livetable_filterStringListProperty)
981 ## From the user point of view we support only exact matching for StringList properties, due to the way the values of
982 ## these properties are stored (concatenated). But when building the actual query, the match type is in fact partial
983 ## because we have to use the like operator in order to match the concatenated list of values.
984 #livetable_getMatchTypes($colname $filterValues.size() 'exact')
985 #set ($matchTarget = "concat('|', concat(${safe_tableAlias}.textValue, '|'))")
986 #if ($whereParams.entrySet())
987 #set ($paramPrefix = "${safe_tableAlias}_textValue_")
988 #else
989 #set ($paramPrefix = $NULL)
990 #end
991 ## As noted above, we have to use the like operator because the list of values is saved concatenated, so from the
992 ## point of view of the query the match type is always partial.
993 #set ($filterQuery = "#livetable_getFilterQuery($matchTarget 'partial' false $filterValues.size() $paramPrefix $NULL)")
994 #set ($whereSql = "${whereSql} and ($filterQuery.trim())")
995 #foreach ($filterValue in $filterValues)
996 #if ($matchTypes.get($foreach.index) == 'empty')
997 ## The client side cannot pass an empty filter value so it specifies that the value is empty using the match type.
998 #set ($filterValue = '')
999 #end
1000 ## As noted above, we can only perform exact matching due to the way the values are stored (concatenated).
1001 #livetable_addFilterParam("%|$filterValue|%" 'exact' $whereParams "${paramPrefix}${foreach.count}")
1002 #end
1003 #end
1004
1005
1006 #**
1007 * NOTE: This macro uses variables defined in livetable_filterProperty . It was not meant to be used alone.
1008 *#
1009 #macro (livetable_filterStringProperty)
1010 #if ($propMetaClass.endsWith('ListClass'))
1011 ## Perform exact matching by default for StaticListClass, DBListClass and DBTreeListClass
1012 ## when they are stored as StringProperty (i.e. single value and no relational storage).
1013 #set ($defaultStringMatchType = 'exact')
1014 #else
1015 ## Perform partial matching by default otherwise.
1016 #set ($defaultStringMatchType = 'partial')
1017 #end
1018 #livetable_getMatchTypes($colname $filterValues.size() $defaultStringMatchType)
1019 ## Group the filter values by match type so that we cann optimize the query.
1020 #livetable_groupFilterValuesByMatchType($matchTypes $filterValues)
1021 #if ($whereParams.entrySet())
1022 #set ($paramPrefix = "${safe_tableAlias}_value_")
1023 #else
1024 #set ($paramPrefix = $NULL)
1025 #end
1026 ## Note that unlike other property types, the String property supports different match types for different filter
1027 ## values. This means we have to call livetable_getFilterQuery for each filter value and then join the constraints
1028 ## ourselves.
1029 #set ($constraints = [])
1030 #set ($paramOffset = 1)
1031 #foreach ($entry in $filterValuesByMatchType.entrySet())
1032 #set ($matchType = $entry.key)
1033 #set ($filterValues = $entry.value)
1034 #set ($constraint = "#livetable_getFilterQuery(""${safe_tableAlias}.value"" $matchType false $filterValues.size() $paramPrefix $paramOffset)")
1035 #set ($discard = $constraints.add($constraint.trim()))
1036 #foreach ($filterValue in $filterValues)
1037 #livetable_addFilterParam($filterValue $matchType $whereParams
1038 "${paramPrefix}${mathtool.add($paramOffset, $foreach.index)}")
1039 #end
1040 #set ($paramOffset = $paramOffset + $filterValues.size())
1041 #end
1042 #set ($whereSql = "${whereSql} and ($stringtool.join($constraints, "" $joinOperator ""))")
1043 #end
1044
1045 #macro (livetable_groupFilterValuesByMatchType $matchTypes $filterValues)
1046 #set ($filterValuesByMatchType = {})
1047 #foreach ($matchType in $matchTypes)
1048 #set ($discard = $filterValuesByMatchType.putIfAbsent($matchType, []))
1049 #set ($discard = $filterValuesByMatchType.get($matchType).add($filterValues.get($foreach.index)))
1050 #end
1051 #end
1052
1053 #macro (livetable_getJoinOperator $colName)
1054 #set ($joinOperator = "$!{request.get(""${colName}${joinModeMarker}"").toUpperCase()}")
1055 #if ($joinOperator != 'AND' && $joinOperator != 'OR')
1056 #set ($joinOperator = 'AND')
1057 #end
1058 #end
1059
1060 #macro (livetable_getFilterQuery $column $matchType $isList $valueCount $paramPrefix $paramOffset)
1061 #livetable_getJoinOperator($colname)
1062 #if (!$paramOffset)
1063 #set ($paramOffset = 1)
1064 #end
1065 #if ($matchType == 'partial' || $matchType == 'prefix')
1066 #livetable_repeatParams("upper($column) like upper(?)", " $joinOperator ", $valueCount, $paramPrefix, $paramOffset)
1067 #elseif($matchType == 'empty')
1068 ## Check if the value of the column is like the empty parameter (which is often the empty string), or if the value
1069 ## of the column is null (to be compliant with Oracle which stores the empty string as a NULL value).
1070 #livetable_repeatParams("($column like ? or $column is null)", " $joinOperator ", $valueCount, $paramPrefix,
1071 $paramOffset)
1072 #elseif ($isList)
1073 #livetable_repeatParams("? in elements($column)", " $joinOperator ", $valueCount, $paramPrefix, $paramOffset)
1074 #elseif ($valueCount > 1 && $joinOperator == 'OR')
1075 $column in (#livetable_repeatParams('?', ', ', $valueCount, $paramPrefix, $paramOffset))
1076 #else
1077 #livetable_repeatParams("$column = ?", ' AND ', $valueCount, $paramPrefix, $paramOffset)
1078 #end
1079 #end
1080
1081 #macro (livetable_repeatParams $str $separator $valueCount $paramPrefix $paramOffset)
1082 #if ($valueCount > 0)
1083 #foreach ($count in [1..$valueCount])
1084 #if ($count > 1)
1085 $separator##
1086 #end
1087 #if ($paramPrefix)
1088 $str.replace('?', ":${paramPrefix}${mathtool.add($paramOffset, $foreach.index)}")##
1089 #else
1090 $str##
1091 #end
1092 #end
1093 #end
1094 #end
1095
1096 #macro (livetable_addFilterParam $filterValue $matchType $params $paramName)
1097 #if ($matchType == 'partial')
1098 #if ($params.entrySet())
1099 #set ($discard = $params.put($paramName, "%$!filterValue%"))
1100 #else
1101 #set ($discard = $params.add("%$!filterValue%"))
1102 #end
1103 #elseif ($matchType == 'prefix')
1104 #if ($params.entrySet())
1105 #set ($discard = $params.put($paramName, "$!filterValue%"))
1106 #else
1107 #set ($discard = $params.add("$!filterValue%"))
1108 #end
1109 #elseif ($matchType == 'empty')
1110 #if ($params.entrySet())
1111 #set ($discard = $params.put($paramName, ''))
1112 #else
1113 #set ($discard = $params.add(''))
1114 #end
1115 #else
1116 #if ($params.entrySet())
1117 #set ($discard = $params.put($paramName, $filterValue))
1118 #else
1119 #set ($discard = $params.add($filterValue))
1120 #end
1121 #end
1122 #end
1123
1124
1125 #**
1126 * Old alias of the #livetable_addColumnToQuery macro.
1127 * @deprecated since 2.2.3, use {@link #livetable_addColumnToQuery}
1128 *#
1129 #macro(grid_addcolumn $colname)
1130 #livetable_addColumnToQuery($colname)
1131 #end
1132
1133 #**
1134 * Generates a valid SQL table alias for the specified live table column.
1135 *#
1136 #macro (livetable_getTableAlias $columnName)
1137 #set ($prefix = 'doc.')
1138 #if ($columnName.startsWith($prefix))
1139 #set ($suffix = $stringtool.removeStart($columnName, $prefix))
1140 #else
1141 ## Force a prefix to avoid the cases when the column name is a reserved SQL keyword.
1142 #set ($prefix = 'prop_')
1143 #set ($suffix = $columnName)
1144 #end
1145 ## Remove non-word characters.
1146 #set ($safe_tableAlias = "$prefix$suffix.replaceAll('\W', '')")
1147 #end
1148 {{/velocity}}