001/* 002 * Copyright 2017 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.extraction.component; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.Collections; 021import java.util.LinkedHashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025import java.util.StringJoiner; 026 027import org.apache.avalon.framework.configuration.Configuration; 028import org.apache.avalon.framework.configuration.ConfigurationException; 029import org.apache.avalon.framework.container.ContainerUtil; 030import org.apache.avalon.framework.context.Context; 031import org.apache.avalon.framework.context.ContextException; 032import org.apache.avalon.framework.context.Contextualizable; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.cocoon.xml.AttributesImpl; 036import org.apache.cocoon.xml.XMLUtils; 037import org.xml.sax.ContentHandler; 038 039import org.ametys.cms.repository.Content; 040import org.ametys.cms.search.Sort; 041import org.ametys.cms.search.Sort.Order; 042import org.ametys.cms.search.cocoon.ContentResultSetHelper; 043import org.ametys.cms.search.cocoon.GroupSearchContent; 044import org.ametys.cms.search.content.ContentSearcherFactory.SimpleContentSearcher; 045import org.ametys.cms.search.model.ResultField; 046import org.ametys.cms.search.query.QuerySyntaxException; 047import org.ametys.cms.search.solr.CriteriaSearchUIModelWrapper; 048import org.ametys.cms.search.ui.model.ColumnHelper; 049import org.ametys.cms.search.ui.model.ColumnHelper.Column; 050import org.ametys.cms.search.ui.model.SearchUIModel; 051import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint; 052import org.ametys.core.util.AvalonLoggerAdapter; 053import org.ametys.core.util.StringUtils; 054import org.ametys.plugins.extraction.ExtractionConstants; 055import org.ametys.plugins.extraction.execution.ExtractionExecutionContext; 056import org.ametys.plugins.extraction.execution.ExtractionExecutionContextHierarchyElement; 057import org.ametys.runtime.model.ModelItem; 058 059/** 060 * This class represents a query component of the extraction module 061 */ 062public class QueryExtractionComponent extends AbstractGroupExtractionComponent implements TwoStepsExecutingExtractionComponent, Contextualizable 063{ 064 private boolean _overrideColumns; 065 private List<ExtractionColumn> _columns = new ArrayList<>(); 066 private boolean _overrideSorts; 067 private Map<String, Order> _sortMap = new LinkedHashMap<>(); 068 private List<Sort> _sorts = new ArrayList<>(); 069 070 private Collection< ? extends ResultField> _resultFields; 071 072 private Context _context; 073 private ServiceManager _serviceManager; 074 private SearchUIModelExtensionPoint _searchModelExtensionPoint; 075 private ContentResultSetHelper _contentResultSetHelper; 076 private ColumnHelper _columnHelper; 077 078 public void contextualize(Context context) throws ContextException 079 { 080 _context = context; 081 } 082 083 @Override 084 public void service(ServiceManager serviceManager) throws ServiceException 085 { 086 super.service(serviceManager); 087 _serviceManager = serviceManager; 088 _searchModelExtensionPoint = (SearchUIModelExtensionPoint) serviceManager.lookup(SearchUIModelExtensionPoint.ROLE); 089 _contentResultSetHelper = (ContentResultSetHelper) serviceManager.lookup(ContentResultSetHelper.ROLE); 090 _columnHelper = (ColumnHelper) serviceManager.lookup(ColumnHelper.ROLE); 091 } 092 093 @Override 094 public void configure(Configuration query) throws ConfigurationException 095 { 096 super.configure(query); 097 098 Configuration columnsConfiguration = query.getChild("columns"); 099 _overrideColumns = columnsConfiguration.getAttributeAsBoolean("override", false); 100 for (Configuration columnConfiguration : columnsConfiguration.getChildren("column")) 101 { 102 ExtractionColumn column = new ExtractionColumn(); 103 column.setFieldPath(columnConfiguration.getValue()); 104 column.setDisplayOptionalName(columnConfiguration.getAttribute("optional", null)); 105 _columns.add(column); 106 } 107 108 Configuration sorts = query.getChild("sorts"); 109 _overrideSorts = sorts.getAttributeAsBoolean("override", false); 110 for (Configuration sort : sorts.getChildren("sort")) 111 { 112 String orderAsString = sort.getAttribute("order", "ASC"); 113 Order order = orderAsString.equalsIgnoreCase("ASC") ? Order.ASC : Order.DESC; 114 String fieldPath = sort.getValue(); 115 _sortMap.put(fieldPath, order); 116 } 117 } 118 119 @Override 120 public void prepareComponentExecution(ExtractionExecutionContext context) throws Exception 121 { 122 super.prepareComponentExecution(context); 123 124 List<String> columnFieldPaths = _getColumnsToDisplay(context); 125 _resultFields = _buildResulFields(columnFieldPaths); 126 127 // Replace attribute separator for sorts coming from configuration 128 for (Map.Entry<String, Order> entry : _sortMap.entrySet()) 129 { 130 String fieldPath = entry.getKey().replaceAll(EXTRACTION_ITEM_PATH_SEPARATOR, ModelItem.ITEM_PATH_SEPARATOR); 131 _sorts.add(new Sort(fieldPath, entry.getValue())); 132 } 133 134 // Sorts can come from configuration or from referenced query 135 for (Sort sort : _sorts) 136 { 137 // get attribute type just to throw an Exception if the field is not available for content types 138 this._getAttributeTypeId(sort.getField(), _contentTypes); 139 } 140 } 141 142 private List<String> _getColumnsToDisplay(ExtractionExecutionContext context) 143 { 144 Map<String, Boolean> displayOptionalColumns = context.getDisplayOptionalColumns(); 145 List<String> columnFieldPaths = new ArrayList<>(); 146 for (ExtractionColumn column : _columns) 147 { 148 String displayOptionalColumnName = column.getDisplayOptionalName(); 149 if (!displayOptionalColumns.containsKey(displayOptionalColumnName) || displayOptionalColumns.get(displayOptionalColumnName)) 150 { 151 columnFieldPaths.add(column.getFieldPath()); 152 } 153 } 154 return columnFieldPaths; 155 } 156 157 private List<ResultField> _buildResulFields(List<String> columnFieldPaths) throws Exception 158 { 159 // FIXME Use a simpler API to build ResultFields. Blocked by CMS-8633. 160 SearchUIModel model = _searchModelExtensionPoint.getExtension("search-ui.solr"); 161 162 CriteriaSearchUIModelWrapper modelWrapper = new CriteriaSearchUIModelWrapper(model, _serviceManager, _context, new AvalonLoggerAdapter(getLogger())); 163 ContainerUtil.service(modelWrapper, _serviceManager); 164 165 List<ResultField> resultFields = new ArrayList<>(); 166 167 Set<String> contentTypeIds = _contentTypesHelper.getCommonAncestors(_contentTypes); 168 List<Column> columns = _columnHelper.getColumns(columnFieldPaths, contentTypeIds); 169 modelWrapper.setResultColumns(contentTypeIds, columns, Collections.emptyMap()); 170 resultFields.addAll(modelWrapper.getResultFields(Collections.emptyMap()).values()); 171 return resultFields; 172 } 173 174 @SuppressWarnings("unchecked") 175 @Override 176 protected void computeReferencedQueryInfos(String refQueryContent) throws QuerySyntaxException 177 { 178 super.computeReferencedQueryInfos(refQueryContent); 179 180 Map<String, Object> contentMap = _jsonUtils.convertJsonToMap(refQueryContent); 181 Map<String, Object> exportParams = (Map<String, Object>) contentMap.get("exportParams"); 182 183 if (!_overrideColumns) 184 { 185 Map<String, Object> values = (Map<String, Object>) exportParams.get("values"); 186 Object columnFieldPathsAsObject = values.get("columns"); 187 188 Collection<String> columnFieldPaths; 189 if (columnFieldPathsAsObject instanceof String) 190 { 191 columnFieldPaths = StringUtils.stringToCollection((String) columnFieldPathsAsObject); 192 } 193 else 194 { 195 columnFieldPaths = (List<String>) columnFieldPathsAsObject; 196 } 197 198 for (String fieldPath : columnFieldPaths) 199 { 200 ExtractionColumn column = new ExtractionColumn(); 201 column.setFieldPath(fieldPath); 202 _columns.add(column); 203 } 204 } 205 206 if (!_overrideSorts) 207 { 208 _sorts.addAll(0, _getQueryFromJSONHelper.getSort(exportParams)); 209 } 210 } 211 212 @Override 213 protected SimpleContentSearcher getContentSearcher() 214 { 215 return super.getContentSearcher().withSort(_sorts); 216 } 217 218 @Override 219 protected void processContents(Iterable<Content> contents, ContentHandler contentHandler, ExtractionExecutionContext context) throws Exception 220 { 221 if (contents.iterator().hasNext()) 222 { 223 XMLUtils.startElement(contentHandler, _tagName); 224 225 GroupSearchContent rootGroup = organizeContentsInGroups(contents, context.getDefaultLocale()); 226 saxGroup(contentHandler, rootGroup, 0, context, _resultFields); 227 228 XMLUtils.endElement(contentHandler, _tagName); 229 } 230 } 231 232 @Override 233 public Iterable<Content> computeFirstLevelResults(ExtractionExecutionContext context) throws Exception 234 { 235 return getContents(context); 236 } 237 238 @Override 239 public void executeFor(ContentHandler contentHandler, Iterable<Content> firstLevelResults, ExtractionExecutionContext context) throws Exception 240 { 241 processContents(firstLevelResults, contentHandler, context); 242 } 243 244 @Override 245 protected void saxContents(ContentHandler contentHandler, ExtractionExecutionContext context, Collection< ? extends ResultField> resultFields, List<Content> contents) throws Exception 246 { 247 for (int currentContentIndex = 0; currentContentIndex < contents.size(); currentContentIndex++) 248 { 249 if (getLogger().isDebugEnabled()) 250 { 251 getLogger().debug(getLogsPrefix() + "executing content " + (currentContentIndex + 1) + "/" + contents.size()); 252 } 253 254 Content content = contents.get(currentContentIndex); 255 256 AttributesImpl attributes = new AttributesImpl(); 257 attributes.addCDATAAttribute("id", content.getId()); 258 attributes.addCDATAAttribute("name", content.getName()); 259 attributes.addCDATAAttribute("title", content.getTitle(context.getDefaultLocale())); 260 if (content.getLanguage() != null) 261 { 262 attributes.addCDATAAttribute("language", content.getLanguage()); 263 } 264 265 XMLUtils.startElement(contentHandler, "content", attributes); 266 _contentResultSetHelper.saxResultFields(contentHandler, content, resultFields, context.getDefaultLocale()); 267 ExtractionExecutionContextHierarchyElement currectContext = new ExtractionExecutionContextHierarchyElement(this, Collections.singleton(content)); 268 executeSubComponents(contentHandler, context, currectContext); 269 XMLUtils.endElement(contentHandler, "content"); 270 } 271 } 272 273 @Override 274 public Map<String, Object> getComponentDetailsForTree() 275 { 276 Map<String, Object> details = super.getComponentDetailsForTree(); 277 details.put("tag", ExtractionConstants.QUERY_COMPONENT_TAG); 278 279 @SuppressWarnings("unchecked") 280 Map<String, Object> data = (Map<String, Object>) details.get("data"); 281 282 StringJoiner columns = new StringJoiner(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELIMITER); 283 for (ExtractionColumn column : this.getColumns()) 284 { 285 StringBuilder builder = new StringBuilder(); 286 builder.append(column.getFieldPath()); 287 if (null != column.getDisplayOptionalName()) 288 { 289 builder.append(" (").append(column.getDisplayOptionalName()).append(")"); 290 } 291 columns.add(builder); 292 } 293 data.put("columns", columns.toString()); 294 data.put("overrideColumns", this.overrideColumns()); 295 296 StringJoiner sorts = new StringJoiner(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELIMITER); 297 for (Map.Entry<String, Order> sort : this.getSorts().entrySet()) 298 { 299 StringBuilder builder = new StringBuilder(); 300 builder.append(sort.getKey()).append(" ("); 301 if (Order.DESC.equals(sort.getValue())) 302 { 303 builder.append("DESC"); 304 } 305 else 306 { 307 builder.append("ASC"); 308 } 309 builder.append(")"); 310 sorts.add(builder); 311 } 312 data.put("sorts", sorts.toString()); 313 data.put("overrideSorts", this.overrideSorts()); 314 315 details.put("iconCls", "ametysicon-query-search"); 316 317 return details; 318 } 319 320 @Override 321 protected String getDefaultTagName() 322 { 323 return "query"; 324 } 325 326 @Override 327 protected String getLogsPrefix() 328 { 329 return "Query component '" + _tagName + "': "; 330 } 331 332 /** 333 * the referenced query's columns 334 * @return <code>true</code> if the referenced query's columns are overridden on this component, <code>false</code> otherwise 335 */ 336 public boolean overrideColumns() 337 { 338 return _overrideColumns; 339 } 340 341 /** 342 * Set the boolean to override the referenced query's columns or not 343 * @param overrideColumns <code>true</code> to override columns, <code>false</code> otherwise 344 */ 345 public void setOverrideColumns(boolean overrideColumns) 346 { 347 _overrideColumns = overrideColumns; 348 } 349 350 /** 351 * Retrieves the component columns 352 * @return The component columns 353 */ 354 public List<ExtractionColumn> getColumns() 355 { 356 return _columns; 357 } 358 359 /** 360 * Add columns to the components. Do not manage optional columns' variables 361 * @param fieldPaths Array of columns' field paths to add 362 */ 363 public void addColumns(String... fieldPaths) 364 { 365 for (String fieldPath : fieldPaths) 366 { 367 ExtractionColumn column = new ExtractionColumn(); 368 column.setFieldPath(fieldPath); 369 _columns.add(column); 370 } 371 } 372 373 /** 374 * Add an optional column to the component 375 * @param fieldPath The column's field path 376 * @param displayOptionalColumnName The name of the variable that manage the display of this optional column 377 */ 378 public void addColumn(String fieldPath, String displayOptionalColumnName) 379 { 380 ExtractionColumn column = new ExtractionColumn(); 381 column.setFieldPath(fieldPath); 382 column.setDisplayOptionalName(displayOptionalColumnName); 383 _columns.add(column); 384 } 385 386 /** 387 * the referenced query's sorts 388 * @return <code>true</code> if the referenced query's sorts are overridden on this component, <code>false</code> otherwise 389 */ 390 public boolean overrideSorts() 391 { 392 return _overrideSorts; 393 } 394 395 /** 396 * Set the boolean to override the referenced query's sorts or not 397 * @param overrideSorts <code>true</code> to override sorts, <code>false</code> otherwise 398 */ 399 public void setOverrideSorts(boolean overrideSorts) 400 { 401 _overrideSorts = overrideSorts; 402 } 403 404 /** 405 * Retrieves the component sorts 406 * @return The component sorts 407 */ 408 public Map<String, Order> getSorts() 409 { 410 return _sortMap; 411 } 412 413 /** 414 * Add sort to the component 415 * @param filedPath Field on which apply sort 416 * @param order sort order to apply for field 417 */ 418 public void addSort(String filedPath, Order order) 419 { 420 _sortMap.put(filedPath, order); 421 } 422}