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}