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}