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}