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