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 XMLUtils.startElement(contentHandler, _tagName); 222 223 GroupSearchContent rootGroup = organizeContentsInGroups(contents, context.getDefaultLocale()); 224 saxGroup(contentHandler, rootGroup, 0, context, _resultFields); 225 226 XMLUtils.endElement(contentHandler, _tagName); 227 } 228 229 @Override 230 public Iterable<Content> computeFirstLevelResults(ExtractionExecutionContext context) throws Exception 231 { 232 return getContents(context); 233 } 234 235 @Override 236 public void executeFor(ContentHandler contentHandler, Iterable<Content> firstLevelResults, ExtractionExecutionContext context) throws Exception 237 { 238 processContents(firstLevelResults, contentHandler, context); 239 } 240 241 @Override 242 protected void saxContents(ContentHandler contentHandler, ExtractionExecutionContext context, Collection< ? extends ResultField> resultFields, List<Content> contents) throws Exception 243 { 244 for (int currentContentIndex = 0; currentContentIndex < contents.size(); currentContentIndex++) 245 { 246 if (getLogger().isDebugEnabled()) 247 { 248 getLogger().debug(getLogsPrefix() + "executing content " + (currentContentIndex + 1) + "/" + contents.size()); 249 } 250 251 Content content = contents.get(currentContentIndex); 252 253 AttributesImpl attributes = new AttributesImpl(); 254 attributes.addCDATAAttribute("id", content.getId()); 255 attributes.addCDATAAttribute("name", content.getName()); 256 attributes.addCDATAAttribute("title", content.getTitle(context.getDefaultLocale())); 257 if (content.getLanguage() != null) 258 { 259 attributes.addCDATAAttribute("language", content.getLanguage()); 260 } 261 262 XMLUtils.startElement(contentHandler, "content", attributes); 263 _contentResultSetHelper.saxResultFields(contentHandler, content, resultFields, context.getDefaultLocale()); 264 ExtractionExecutionContextHierarchyElement currectContext = new ExtractionExecutionContextHierarchyElement(this, Collections.singleton(content)); 265 executeSubComponents(contentHandler, context, currectContext); 266 XMLUtils.endElement(contentHandler, "content"); 267 } 268 } 269 270 @Override 271 public Map<String, Object> getComponentDetailsForTree() 272 { 273 Map<String, Object> details = super.getComponentDetailsForTree(); 274 details.put("tag", ExtractionConstants.QUERY_COMPONENT_TAG); 275 276 @SuppressWarnings("unchecked") 277 Map<String, Object> data = (Map<String, Object>) details.get("data"); 278 279 StringJoiner columns = new StringJoiner(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELIMITER); 280 for (ExtractionColumn column : this.getColumns()) 281 { 282 StringBuilder builder = new StringBuilder(); 283 builder.append(column.getFieldPath()); 284 if (null != column.getDisplayOptionalName()) 285 { 286 builder.append(" (").append(column.getDisplayOptionalName()).append(")"); 287 } 288 columns.add(builder); 289 } 290 data.put("columns", columns.toString()); 291 data.put("overrideColumns", this.overrideColumns()); 292 293 StringJoiner sorts = new StringJoiner(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELIMITER); 294 for (Map.Entry<String, Order> sort : this.getSorts().entrySet()) 295 { 296 StringBuilder builder = new StringBuilder(); 297 builder.append(sort.getKey()).append(" ("); 298 if (Order.DESC.equals(sort.getValue())) 299 { 300 builder.append("DESC"); 301 } 302 else 303 { 304 builder.append("ASC"); 305 } 306 builder.append(")"); 307 sorts.add(builder); 308 } 309 data.put("sorts", sorts.toString()); 310 data.put("overrideSorts", this.overrideSorts()); 311 312 details.put("iconCls", "ametysicon-query-search"); 313 314 return details; 315 } 316 317 @Override 318 protected String getDefaultTagName() 319 { 320 return "query"; 321 } 322 323 @Override 324 protected String getLogsPrefix() 325 { 326 return "Query component '" + _tagName + "': "; 327 } 328 329 /** 330 * the referenced query's columns 331 * @return <code>true</code> if the referenced query's columns are overridden on this component, <code>false</code> otherwise 332 */ 333 public boolean overrideColumns() 334 { 335 return _overrideColumns; 336 } 337 338 /** 339 * Set the boolean to override the referenced query's columns or not 340 * @param overrideColumns <code>true</code> to override columns, <code>false</code> otherwise 341 */ 342 public void setOverrideColumns(boolean overrideColumns) 343 { 344 _overrideColumns = overrideColumns; 345 } 346 347 /** 348 * Retrieves the component columns 349 * @return The component columns 350 */ 351 public List<ExtractionColumn> getColumns() 352 { 353 return _columns; 354 } 355 356 /** 357 * Add columns to the components. Do not manage optional columns' variables 358 * @param fieldPaths Array of columns' field paths to add 359 */ 360 public void addColumns(String... fieldPaths) 361 { 362 for (String fieldPath : fieldPaths) 363 { 364 ExtractionColumn column = new ExtractionColumn(); 365 column.setFieldPath(fieldPath); 366 _columns.add(column); 367 } 368 } 369 370 /** 371 * Add an optional column to the component 372 * @param fieldPath The column's field path 373 * @param displayOptionalColumnName The name of the variable that manage the display of this optional column 374 */ 375 public void addColumn(String fieldPath, String displayOptionalColumnName) 376 { 377 ExtractionColumn column = new ExtractionColumn(); 378 column.setFieldPath(fieldPath); 379 column.setDisplayOptionalName(displayOptionalColumnName); 380 _columns.add(column); 381 } 382 383 /** 384 * the referenced query's sorts 385 * @return <code>true</code> if the referenced query's sorts are overridden on this component, <code>false</code> otherwise 386 */ 387 public boolean overrideSorts() 388 { 389 return _overrideSorts; 390 } 391 392 /** 393 * Set the boolean to override the referenced query's sorts or not 394 * @param overrideSorts <code>true</code> to override sorts, <code>false</code> otherwise 395 */ 396 public void setOverrideSorts(boolean overrideSorts) 397 { 398 _overrideSorts = overrideSorts; 399 } 400 401 /** 402 * Retrieves the component sorts 403 * @return The component sorts 404 */ 405 public Map<String, Order> getSorts() 406 { 407 return _sortMap; 408 } 409 410 /** 411 * Add sort to the component 412 * @param filedPath Field on which apply sort 413 * @param order sort order to apply for field 414 */ 415 public void addSort(String filedPath, Order order) 416 { 417 _sortMap.put(filedPath, order); 418 } 419}