Version 3.1 von Daniel Herrmann am 2026/02/04 20:23

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