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.cms.search.cocoon;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.List;
021import java.util.Locale;
022import java.util.Map;
023import java.util.Set;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.container.ContainerUtil;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.cocoon.ProcessingException;
030import org.apache.cocoon.components.ContextHelper;
031import org.apache.cocoon.components.source.SourceUtil;
032import org.apache.cocoon.environment.Request;
033import org.apache.cocoon.xml.AttributesImpl;
034import org.apache.cocoon.xml.XMLUtils;
035import org.apache.commons.lang3.StringUtils;
036import org.apache.excalibur.source.Source;
037import org.xml.sax.SAXException;
038
039import org.ametys.cms.contenttype.ContentType;
040import org.ametys.cms.repository.Content;
041import org.ametys.cms.search.GroupSearchContentHelper;
042import org.ametys.cms.search.SearchResults;
043import org.ametys.cms.search.model.SearchModel;
044import org.ametys.cms.search.solr.CriteriaSearchModelWrapper;
045import org.ametys.cms.search.ui.model.ColumnHelper.Column;
046import org.ametys.cms.search.ui.model.SearchUIModel;
047import org.ametys.core.util.AvalonLoggerAdapter;
048import org.ametys.core.util.IgnoreRootHandler;
049import org.ametys.plugins.repository.AmetysRepositoryException;
050import org.ametys.runtime.model.ViewItemContainer;
051
052/**
053 * Generate contents returned by the {@link SearchAction}, groups by selected fields.
054 */
055public class DocSearchGenerator extends SearchGenerator
056{
057    private GroupSearchContentHelper _groupSearchContentHelper;
058    
059    @Override
060    public void service(ServiceManager serviceManager) throws ServiceException
061    {
062        super.service(serviceManager);
063        _groupSearchContentHelper = (GroupSearchContentHelper) serviceManager.lookup(GroupSearchContentHelper.ROLE);
064    }
065    
066    @Override
067    protected void saxContents(SearchModel model, SearchResults<Content> results, String versionLabel, Map<String, Object> jsParameters, Locale locale, Map<String, Object> contextualParameters) throws SAXException, AmetysRepositoryException, IOException, ProcessingException
068    {
069        String modelId = (String) jsParameters.get("model");
070        SearchUIModel uiModel = _searchModelManager.getExtension(modelId);
071        
072        Set<String> cTypeIds = getCommonContentTypeIds(jsParameters, uiModel, contextualParameters);
073        
074        List<String> groupingFields = _getGroupingFields(jsParameters);
075
076        // Columns
077        List<Column> columns = getColumnsFromParameters(jsParameters, cTypeIds);
078        List<String> columnIds = columns.stream()
079                                        .map(Column::getId)
080                                        .collect(Collectors.toList());
081
082        ViewItemContainer resultItems = model.getResultItems(contextualParameters);
083        ViewItemContainer filteredResultItems = (ViewItemContainer) SearchGeneratorHelper.copyAndFilterViewItemAccessor(resultItems, columnIds);
084        SearchGeneratorHelper.removeResultItems(filteredResultItems, groupingFields);
085        
086        CriteriaSearchModelWrapper modelWrapper = new CriteriaSearchModelWrapper(uiModel, manager, _context, new AvalonLoggerAdapter(getLogger()));
087        
088        try
089        {
090            ContainerUtil.service(modelWrapper, manager);
091            modelWrapper.setResultColumns(cTypeIds, columns, contextualParameters);
092        }
093        catch (Exception e)
094        {
095            throw new AmetysRepositoryException(e);
096        }
097        ViewItemContainer uiResultItems = modelWrapper.getResultItems(contextualParameters);
098        
099        Set<ContentType> contentTypes = cTypeIds.stream()
100                .map(_contentTypeExtensionPoint::getExtension)
101                .collect(Collectors.toSet());
102        GroupSearchContent groupSearchContent = _groupSearchContentHelper.organizeContentsInGroups(results.getObjects(), groupingFields, contentTypes, locale);
103        
104        // Set the request attributes from the contextual parameters
105        Request request = ContextHelper.getRequest(_context);
106        for (String name : contextualParameters.keySet())
107        {
108            request.setAttribute(name, contextualParameters.get(name));
109        }
110        
111        XMLUtils.startElement(contentHandler, "contents");
112        _saxGroup(groupSearchContent, 0, filteredResultItems, uiResultItems, locale);
113        XMLUtils.endElement(contentHandler, "contents");
114    }
115    
116    private List<String> _getGroupingFields(Map<String, Object> jsParameters)
117    {
118        @SuppressWarnings("unchecked")
119        List<String> groupingFields = (List<String>) jsParameters.get("groupingFields");
120        if (groupingFields == null || groupingFields.size() == 1 && StringUtils.isEmpty(groupingFields.get(0)))
121        {
122            groupingFields = new ArrayList<>();
123        }
124        
125        return groupingFields;
126    }
127    
128    /**
129     * Sax a group of content recursively.
130     * First level (level=0) will not create a xml group (usually, 1st group is just here to handle a list)
131     * @param group group to sax
132     * @param level current level (start with zero)
133     * @param resultItems result items to sax
134     * @param uiResultItems The columns to be exported
135     * @param locale The locale for search. Can be null.
136     * @throws SAXException if a error occurred during sax
137     * @throws AmetysRepositoryException if a error occurred
138     * @throws IOException if a error occurred
139     * @throws ProcessingException if a error occurred
140     */
141    private void _saxGroup(GroupSearchContent group, int level, ViewItemContainer resultItems, ViewItemContainer uiResultItems, Locale locale) throws SAXException, AmetysRepositoryException, IOException, ProcessingException
142    {
143        if (level > 0)
144        {
145            AttributesImpl attrs = new AttributesImpl();
146            attrs.addCDATAAttribute("level", String.valueOf(level));
147            attrs.addCDATAAttribute("fullModelPath", group.getGroupFieldPath());
148            attrs.addCDATAAttribute("value", group.getGroupName());
149            XMLUtils.startElement(contentHandler, "group", attrs);
150        }
151        
152        for (GroupSearchContent groupSearchContent : group.getSubList())
153        {
154            _saxGroup(groupSearchContent, level + 1, resultItems, uiResultItems, locale);
155        }
156        
157        for (Content content : group.getContents())
158        {
159            _saxContentForDoc(content, resultItems, uiResultItems, locale);
160        }
161        
162        if (level > 0)
163        {
164            XMLUtils.endElement(contentHandler, "group");
165        }
166    }
167    /**
168     * SAX the result content
169     * @param content the result
170     * @param resultItems the result items
171     * @param uiResultItems The columns to be exported
172     * @param locale The locale for search. Can be null.
173     * @throws SAXException if a error occurred during sax
174     * @throws AmetysRepositoryException if a error occurred
175     * @throws IOException if a error occurred
176     * @throws ProcessingException if a error occurred
177     */
178    private void _saxContentForDoc(Content content, ViewItemContainer resultItems, ViewItemContainer uiResultItems, Locale locale) throws SAXException, AmetysRepositoryException, IOException, ProcessingException
179    {
180        Request request = ContextHelper.getRequest(_context);
181        
182        try
183        {
184            request.setAttribute(Content.class.getName(), content);
185            request.setAttribute("resultItems", resultItems);
186            request.setAttribute("viewName", "export-word");
187            request.setAttribute("forceRemoteUrl", true);
188            
189            request.setAttribute("uiResultItems", uiResultItems);
190
191            AttributesImpl attrs = new AttributesImpl();
192            attrs.addCDATAAttribute("id", content.getId());
193            attrs.addCDATAAttribute("name", content.getName());
194            attrs.addCDATAAttribute("title", content.getTitle(locale));
195            if (content.getLanguage() != null)
196            {
197                attrs.addCDATAAttribute("language", content.getLanguage());
198            }
199                
200            XMLUtils.startElement(contentHandler, "content", attrs);
201            Source contentSource = resolver.resolveURI("cocoon:/export/content.doc" + (locale != null ? "?lang=" + locale.getLanguage() : ""));
202            SourceUtil.toSAX(contentSource, new IgnoreRootHandler(contentHandler));
203            
204            XMLUtils.endElement(contentHandler, "content");
205        }
206        finally
207        {
208            request.setAttribute(Content.class.getName(), null);
209            request.removeAttribute("forceRemoteUrl");
210        }
211    }
212    
213}