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