Wiki source code of Extension Pages
Last modified by Eleni Cojocariu on 2026/10/08 09:27
Show last authors
| author | version | line-number | content |
|---|---|---|---|
| 1 | {{velocity}} | ||
| 2 | #set ($Q = '"') | ||
| 3 | #set ($idField = 'property.ExtensionCode.ExtensionClass.id_string') | ||
| 4 | #set ($extBase = "type:DOCUMENT AND wiki:extensions AND hidden:false AND class:${Q}ExtensionCode.ExtensionClass${Q}") | ||
| 5 | ## Only the fields the table actually reads. Without this, Solr ships every stored field of every | ||
| 6 | ## extension page -- the whole rendered content and every object property -- which is where most of | ||
| 7 | ## the page's time went. The first five are mandatory whatever the table shows: SolrQueryExecutor | ||
| 8 | ## resolves each result to a DocumentReference to check view rights, and a result it cannot resolve | ||
| 9 | ## is dropped, so an incomplete 'fl' empties the table while the total still looks right. | ||
| 10 | ## 'website' is a TextArea, so it is NOT indexed under a '_string' field like the id: TypedValue.TEXT | ||
| 11 | ## is null, so DocumentSolrMetadataExtractor suffixes it with the document's locale instead | ||
| 12 | ## (website_en here). A glob in 'fl' asks for it whatever that locale turns out to be, and the row | ||
| 13 | ## loop finds it back by prefix -- the sort twin website_sortString is stored="false" and never comes. | ||
| 14 | #set ($websitePrefix = 'property.ExtensionCode.ExtensionClass.website_') | ||
| 15 | #set ($fields = "wiki,spaces,name,type,doclocale,fullname,title_,${idField},${websitePrefix}*") | ||
| 16 | |||
| 17 | ## The project/pom scope reads a different population: pages carrying an ExtensionCode.ProjectClass | ||
| 18 | ## object (a project groups the modules of one codebase and is NOT an extension page -- it holds no | ||
| 19 | ## ExtensionClass object, so it appears in neither of the two scopes above) and pages carrying an | ||
| 20 | ## ExtensionCode.ExtensionProxyClass object (their fields are imported from a Maven repository, i.e. | ||
| 21 | ## they come from the pom). 19 pages are both, so the query ORs the two classes instead of summing. | ||
| 22 | ## 'class' is the multi-valued field holding every class the page has an object of -- it is what the | ||
| 23 | ## two check-mark columns are read from, so it has to be in 'fl' like any other field the table uses. | ||
| 24 | #set ($projIdField = 'property.ExtensionCode.ProjectClass.id_string') | ||
| 25 | #set ($pomBase = "type:DOCUMENT AND wiki:extensions AND hidden:false AND (class:${Q}ExtensionCode.ProjectClass${Q} OR class:${Q}ExtensionCode.ExtensionProxyClass${Q})") | ||
| 26 | #set ($pomFields = "wiki,spaces,name,type,doclocale,fullname,title_,class,${idField},${projIdField}") | ||
| 27 | |||
| 28 | ## Scopes, in display order; the first one is the default, so the page has something to show when it | ||
| 29 | ## is opened without a parameter -- same contract as the sibling 'details' page and its tree switcher. | ||
| 30 | #set ($scopes = []) | ||
| 31 | #set ($discard = $scopes.add({'key': 'noid', 'label': 'Without id', 'heading': 'Extension pages without id'})) | ||
| 32 | #set ($discard = $scopes.add({'key': 'withid', 'label': 'With id', 'heading': 'Extension pages with id, grouped by id prefix'})) | ||
| 33 | #set ($discard = $scopes.add({'key': 'project', 'label': 'Project / pom pages', 'heading': 'Project pages and pom-imported extension pages'})) | ||
| 34 | #set ($requested = "$!request.get('scope')") | ||
| 35 | #set ($scope = '') | ||
| 36 | #foreach ($candidate in $scopes) | ||
| 37 | #if ($candidate.key == $requested) | ||
| 38 | #set ($scope = $candidate) | ||
| 39 | #end | ||
| 40 | #end | ||
| 41 | #if ($scope == '') | ||
| 42 | #set ($scope = $scopes.get(0)) | ||
| 43 | #end | ||
| 44 | #set ($links = []) | ||
| 45 | #foreach ($candidate in $scopes) | ||
| 46 | #if ($candidate.key == $scope.key) | ||
| 47 | #set ($discard = $links.add("**$candidate.label**")) | ||
| 48 | #else | ||
| 49 | #set ($url = $doc.getURL('view', "scope=$candidate.key")) | ||
| 50 | #set ($discard = $links.add("[[$candidate.label>>path:$url]]")) | ||
| 51 | #end | ||
| 52 | #end | ||
| 53 | Scope: $stringtool.join($links, ' | ') | ||
| 54 | |||
| 55 | ## Groups, in display order. "others" receives whatever matches no prefix. | ||
| 56 | #set ($groups = []) | ||
| 57 | #set ($discard = $groups.add({'key': 'platform', 'heading': 'org.xwiki.platform extensions', 'prefix': 'org.xwiki.platform'})) | ||
| 58 | #set ($discard = $groups.add({'key': 'contrib', 'heading': 'org.xwiki.contrib extensions', 'prefix': 'org.xwiki.contrib'})) | ||
| 59 | #set ($discard = $groups.add({'key': 'rendering', 'heading': 'org.xwiki.rendering extensions', 'prefix': 'org.xwiki.rendering'})) | ||
| 60 | #set ($discard = $groups.add({'key': 'commons', 'heading': 'org.xwiki.commons extensions', 'prefix': 'org.xwiki.commons'})) | ||
| 61 | #set ($discard = $groups.add({'key': 'others', 'heading': 'Other extensions', 'prefix': ''})) | ||
| 62 | |||
| 63 | = $scope.heading = | ||
| 64 | |||
| 65 | ## Every extension page has the id field indexed; the ones "without an id" hold the empty string. | ||
| 66 | ## Splitting here rather than in Velocity means a scope only pays for the rows it shows. | ||
| 67 | #if ($scope.key == 'project') | ||
| 68 | #set ($statement = $pomBase) | ||
| 69 | #set ($fl = $pomFields) | ||
| 70 | #else | ||
| 71 | #if ($scope.key == 'noid') | ||
| 72 | #set ($statement = "${extBase} AND ${idField}:${Q}${Q}") | ||
| 73 | #else | ||
| 74 | #set ($statement = "${extBase} AND -${idField}:${Q}${Q}") | ||
| 75 | #end | ||
| 76 | #set ($fl = $fields) | ||
| 77 | #end | ||
| 78 | #set ($query = $services.query.createQuery($statement, 'solr')) | ||
| 79 | #set ($discard = $query.setLimit(2000)) | ||
| 80 | #set ($discard = $query.bindValue('fl', $fl)) | ||
| 81 | ## Alphabetical by the title the Page column shows. title_sort is the schema's dedicated sort field | ||
| 82 | ## for it (a copyField of title_*, of the case-insensitive 'lowercase' type); title_ itself is | ||
| 83 | ## analysed and cannot be sorted on. Solr sorts before the rights filter removes rows, so the order | ||
| 84 | ## survives filtering, and the buckets below keep it. | ||
| 85 | #set ($discard = $query.bindValue('sort', 'title_sort asc')) | ||
| 86 | #set ($results = $query.execute().get(0).results) | ||
| 87 | |||
| 88 | #if ($scope.key == 'noid') | ||
| 89 | (% class="table" %) | ||
| 90 | |=Page|=Website | ||
| 91 | #set ($withWebsite = 0) | ||
| 92 | #foreach ($solrDoc in $results) | ||
| 93 | #set ($title = "$!solrDoc.getFirstValue('title_')") | ||
| 94 | #if ($title == '') | ||
| 95 | #set ($title = "$!solrDoc.getFirstValue('name')") | ||
| 96 | #end | ||
| 97 | #set ($title = $title.replace('|', '~|')) | ||
| 98 | #set ($docWebsite = '') | ||
| 99 | #foreach ($fieldName in $solrDoc.getFieldNames()) | ||
| 100 | #if ($docWebsite == '' && $fieldName.startsWith($websitePrefix)) | ||
| 101 | #set ($docWebsite = "$!solrDoc.getFirstValue($fieldName)") | ||
| 102 | #end | ||
| 103 | #end | ||
| 104 | #if ($docWebsite == '') | ||
| 105 | #set ($website = '-') | ||
| 106 | #else | ||
| 107 | #set ($withWebsite = $withWebsite + 1) | ||
| 108 | #set ($website = $docWebsite.replace('|', '~|')) | ||
| 109 | #end | ||
| 110 | |[[$title>>doc:extensions:$solrDoc.getFirstValue('fullname')]]|$website | ||
| 111 | #end | ||
| 112 | |**Total: $results.size()**|**$withWebsite with a website** | ||
| 113 | #elseif ($scope.key == 'project') | ||
| 114 | ## Same buckets as the 'withid' scope, on the id resolved below. Sorting is done here rather than | ||
| 115 | ## in Solr because the id comes from either of two fields depending on the page, which no single | ||
| 116 | ## Solr 'sort' can express -- the bound 'title_sort' only decides the order inside a bucket when | ||
| 117 | ## two rows share an id. | ||
| 118 | #set ($buckets = {}) | ||
| 119 | #foreach ($g in $groups) | ||
| 120 | #set ($discard = $buckets.put($g.key, [])) | ||
| 121 | #end | ||
| 122 | #set ($bothCount = 0) | ||
| 123 | #foreach ($solrDoc in $results) | ||
| 124 | ## The project object's id when there is one, the extension object's otherwise. Both classes | ||
| 125 | ## index an unset id as the empty string rather than leaving the field out, so the first | ||
| 126 | ## non-empty value is what has to be looked for, not the first value. | ||
| 127 | #set ($extId = '') | ||
| 128 | #foreach ($values in [$solrDoc.getFieldValues($projIdField), $solrDoc.getFieldValues($idField)]) | ||
| 129 | #if ($values) | ||
| 130 | #foreach ($v in $values) | ||
| 131 | #if ($extId == '' && "$!v" != '') | ||
| 132 | #set ($extId = "$v") | ||
| 133 | #end | ||
| 134 | #end | ||
| 135 | #end | ||
| 136 | #end | ||
| 137 | ## The first matching prefix wins | ||
| 138 | #set ($docKey = 'others') | ||
| 139 | #foreach ($g in $groups) | ||
| 140 | #if ($docKey == 'others' && $g.key != 'others' && $extId.startsWith($g.prefix)) | ||
| 141 | #set ($docKey = $g.key) | ||
| 142 | #end | ||
| 143 | #end | ||
| 144 | ## 'class' is multi-valued: it holds every class the page carries an object of. Resolved to two | ||
| 145 | ## booleans here so the render loop below stays a straight render. | ||
| 146 | #set ($classes = $solrDoc.getFieldValues('class')) | ||
| 147 | #set ($isProject = false) | ||
| 148 | #set ($isPom = false) | ||
| 149 | #if ($classes) | ||
| 150 | #set ($isProject = $classes.contains('ExtensionCode.ProjectClass')) | ||
| 151 | #set ($isPom = $classes.contains('ExtensionCode.ExtensionProxyClass')) | ||
| 152 | #end | ||
| 153 | #if ($isProject && $isPom) | ||
| 154 | #set ($bothCount = $bothCount + 1) | ||
| 155 | #end | ||
| 156 | ## Sorted on 'sortid', not on 'id': $collectiontool.sort compares Strings case-sensitively, so a | ||
| 157 | ## single upper-case group id would otherwise sort ahead of every lower-case one. | ||
| 158 | #set ($discard = $buckets.get($docKey).add({'doc': $solrDoc, 'id': $extId, 'sortid': $extId.toLowerCase(), 'project': $isProject, 'pom': $isPom})) | ||
| 159 | #end | ||
| 160 | #set ($projectCount = 0) | ||
| 161 | #set ($pomCount = 0) | ||
| 162 | #foreach ($g in $groups) | ||
| 163 | #set ($list = $collectiontool.sort($buckets.get($g.key), 'sortid')) | ||
| 164 | |||
| 165 | == $g.heading: $list.size() == | ||
| 166 | |||
| 167 | (% class="table" %) | ||
| 168 | |=Extension|=Id|=Project page|=Pom page | ||
| 169 | #foreach ($item in $list) | ||
| 170 | #set ($title = "$!item.doc.getFirstValue('title_')") | ||
| 171 | #if ($title == '') | ||
| 172 | #set ($title = "$!item.doc.getFirstValue('name')") | ||
| 173 | #end | ||
| 174 | #set ($title = $title.replace('|', '~|')) | ||
| 175 | #if ($item.id == '') | ||
| 176 | #set ($extId = '-') | ||
| 177 | #else | ||
| 178 | #set ($extId = $item.id.replace('|', '~|')) | ||
| 179 | #end | ||
| 180 | #if ($item.project) | ||
| 181 | #set ($projectCell = '✅') | ||
| 182 | #set ($projectCount = $projectCount + 1) | ||
| 183 | #else | ||
| 184 | #set ($projectCell = '') | ||
| 185 | #end | ||
| 186 | #if ($item.pom) | ||
| 187 | #set ($pomCell = '✅') | ||
| 188 | #set ($pomCount = $pomCount + 1) | ||
| 189 | #else | ||
| 190 | #set ($pomCell = '') | ||
| 191 | #end | ||
| 192 | |[[$title>>doc:extensions:$item.doc.getFirstValue('fullname')]]|$extId|$projectCell|$pomCell | ||
| 193 | #end | ||
| 194 | #end | ||
| 195 | |||
| 196 | == Total == | ||
| 197 | |||
| 198 | **$results.size()** pages: **$projectCount** project pages and **$pomCount** pom pages, **$bothCount** of them both. | ||
| 199 | #else | ||
| 200 | ## Buckets: group key -> list of {doc, id} | ||
| 201 | #set ($buckets = {}) | ||
| 202 | #foreach ($g in $groups) | ||
| 203 | #set ($discard = $buckets.put($g.key, [])) | ||
| 204 | #end | ||
| 205 | #foreach ($solrDoc in $results) | ||
| 206 | ## First non-empty id of the page | ||
| 207 | #set ($docId = '') | ||
| 208 | #set ($values = $solrDoc.getFieldValues($idField)) | ||
| 209 | #if ($values) | ||
| 210 | #foreach ($v in $values) | ||
| 211 | #if ($docId == '' && "$!v" != '') | ||
| 212 | #set ($docId = "$v") | ||
| 213 | #end | ||
| 214 | #end | ||
| 215 | #end | ||
| 216 | ## The first matching prefix wins | ||
| 217 | #set ($docKey = 'others') | ||
| 218 | #foreach ($g in $groups) | ||
| 219 | #if ($docKey == 'others' && $g.key != 'others' && $docId.startsWith($g.prefix)) | ||
| 220 | #set ($docKey = $g.key) | ||
| 221 | #end | ||
| 222 | #end | ||
| 223 | ## The website URL, under whatever locale suffix it was indexed with | ||
| 224 | #set ($docWebsite = '') | ||
| 225 | #foreach ($fieldName in $solrDoc.getFieldNames()) | ||
| 226 | #if ($docWebsite == '' && $fieldName.startsWith($websitePrefix)) | ||
| 227 | #set ($docWebsite = "$!solrDoc.getFirstValue($fieldName)") | ||
| 228 | #end | ||
| 229 | #end | ||
| 230 | #set ($discard = $buckets.get($docKey).add({'doc': $solrDoc, 'id': $docId, 'website': $docWebsite})) | ||
| 231 | #end | ||
| 232 | #foreach ($g in $groups) | ||
| 233 | #set ($list = $buckets.get($g.key)) | ||
| 234 | |||
| 235 | == $g.heading: $list.size() == | ||
| 236 | |||
| 237 | (% class="table" %) | ||
| 238 | |=Page|=Id|=Website | ||
| 239 | #foreach ($item in $list) | ||
| 240 | #set ($title = "$!item.doc.getFirstValue('title_')") | ||
| 241 | #if ($title == '') | ||
| 242 | #set ($title = "$!item.doc.getFirstValue('name')") | ||
| 243 | #end | ||
| 244 | #set ($title = $title.replace('|', '~|')) | ||
| 245 | #if ($item.website == '') | ||
| 246 | #set ($website = '-') | ||
| 247 | #else | ||
| 248 | #set ($website = $item.website.replace('|', '~|')) | ||
| 249 | #end | ||
| 250 | |[[$title>>doc:extensions:$item.doc.getFirstValue('fullname')]]|$item.id|$website | ||
| 251 | #end | ||
| 252 | #end | ||
| 253 | |||
| 254 | == Total == | ||
| 255 | |||
| 256 | **$results.size()** extension pages with an id. | ||
| 257 | #end | ||
| 258 | {{/velocity}} |