001/*
002 *  Copyright 2013 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.Collection;
021import java.util.Collections;
022import java.util.List;
023import java.util.Locale;
024import java.util.Map;
025import java.util.Set;
026import java.util.stream.Collectors;
027
028import org.apache.avalon.framework.context.Context;
029import org.apache.avalon.framework.context.ContextException;
030import org.apache.avalon.framework.context.Contextualizable;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.cocoon.ProcessingException;
034import org.apache.cocoon.components.ContextHelper;
035import org.apache.cocoon.environment.ObjectModelHelper;
036import org.apache.cocoon.environment.Request;
037import org.apache.cocoon.generation.ServiceableGenerator;
038import org.apache.cocoon.xml.AttributesImpl;
039import org.apache.cocoon.xml.XMLUtils;
040import org.apache.commons.lang3.ArrayUtils;
041import org.apache.commons.lang3.StringUtils;
042import org.xml.sax.SAXException;
043
044import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
045import org.ametys.cms.contenttype.ContentTypesHelper;
046import org.ametys.cms.repository.Content;
047import org.ametys.cms.search.SearchResult;
048import org.ametys.cms.search.SearchResults;
049import org.ametys.cms.search.model.ResultField;
050import org.ametys.cms.search.model.SearchModel;
051import org.ametys.cms.search.ui.model.ColumnHelper;
052import org.ametys.cms.search.ui.model.ColumnHelper.Column;
053import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint;
054import org.ametys.core.util.ServerCommHelper;
055import org.ametys.plugins.repository.AmetysRepositoryException;
056import org.ametys.plugins.repository.version.VersionAwareAmetysObject;
057
058/**
059 * Generate contents returned by the {@link SearchAction}.
060 */
061public class SearchGenerator extends ServiceableGenerator implements Contextualizable
062{
063    /** Constant for getting content in specific version label */
064    public static final String CONTENT_VERSION_LABEL = "versionLabel";
065    
066    /** The server comm helper */
067    protected ServerCommHelper _serverCommHelper;
068    /** Helper for saxing result */
069    protected ContentResultSetHelper _contentRSH;
070    /** Context */
071    protected Context _context;
072    /** The search model manager */
073    protected SearchUIModelExtensionPoint _searchModelManager;
074    /** The content type helper. */
075    protected ContentTypesHelper _cTypeHelper;
076    /** The helper for columns */
077    protected ColumnHelper _columnHelper;
078    /** The content type extension point */
079    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
080    
081    @Override
082    public void service(ServiceManager smanager) throws ServiceException
083    {
084        super.service(smanager);
085        _serverCommHelper = (ServerCommHelper) smanager.lookup(ServerCommHelper.ROLE);
086        
087        _contentRSH = (ContentResultSetHelper) smanager.lookup(ContentResultSetHelper.ROLE);
088        _searchModelManager = (SearchUIModelExtensionPoint) smanager.lookup(SearchUIModelExtensionPoint.ROLE);
089        
090        _cTypeHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
091        _columnHelper = (ColumnHelper) smanager.lookup(ColumnHelper.ROLE);
092        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
093    }
094    
095    @Override
096    public void contextualize(Context context) throws ContextException
097    {
098        _context = context;
099    }
100    
101    @SuppressWarnings("unchecked")
102    @Override
103    public void generate() throws IOException, SAXException, ProcessingException
104    {
105        Request request = ObjectModelHelper.getRequest(objectModel);
106        
107        SearchResults<Content> results = (SearchResults<Content>) request.getAttribute(SearchAction.SEARCH_RESULTS);
108        SearchModel model = (SearchModel) request.getAttribute(SearchAction.SEARCH_MODEL);
109        
110        Map<String, Object> jsParameters = _serverCommHelper.getJsParameters();
111        
112        Map<String, Object> contextualParameters = (Map<String, Object>) jsParameters.get("contextualParameters");
113        if (contextualParameters == null)
114        {
115            contextualParameters = Collections.emptyMap();
116        }
117        
118        Locale defaultLocale = (Locale) request.getAttribute(SearchAction.SEARCH_LOCALE);
119        
120        Collection< ? extends ResultField> resultFields = model.getResultFields(contextualParameters).values();
121        
122        String versionLabel = (String) jsParameters.get(CONTENT_VERSION_LABEL);
123
124        contentHandler.startDocument();
125        saxContents(model, results, resultFields, versionLabel, jsParameters, defaultLocale, contextualParameters);
126        contentHandler.endDocument();
127    }
128    
129    /**
130     * Sax a set of contents
131     * @param model search model
132     * @param results contents
133     * @param resultFields fields to retreive
134     * @param versionLabel version label
135     * @param jsParameters parameters of the search
136     * @param defaultLocale The locale to use for localized values if content's language is null.
137     * @param contextualParameters The contextual parameters
138     * @throws SAXException if a error occurred during sax
139     * @throws AmetysRepositoryException if a error occurred
140     * @throws IOException if a error occurred
141     * @throws ProcessingException if a error occurred
142     */
143    protected void saxContents(SearchModel model, SearchResults<Content> results, Collection< ? extends ResultField> resultFields, String versionLabel, Map<String, Object> jsParameters, Locale defaultLocale, Map<String, Object> contextualParameters) throws SAXException, AmetysRepositoryException, IOException, ProcessingException
144    {
145        // Common content type
146        Set<String> commonContentTypeIds = getCommonContentTypeIds(jsParameters, model, contextualParameters);
147        
148        // Filter the resultFields, using columns if available
149        Collection<ResultField> resultFieldsFiltered = new ArrayList<>();
150        List<String> columns = getColumnsFromParameters(jsParameters, commonContentTypeIds)
151                .stream()
152                .map(Column::getId)
153                .collect(Collectors.toList());
154        for (ResultField resultField : resultFields)
155        {
156            if (columns.size() == 0 || columns.contains(resultField.getId()))
157            {
158                resultFieldsFiltered.add(resultField);
159            }
160        }
161
162        XMLUtils.startElement(contentHandler, "contents");
163        
164        Iterable<SearchResult<Content>> contents = results.getResults();
165        for (SearchResult<Content> result : contents)
166        {
167            Content content = result.getObject();
168            
169            if (StringUtils.isBlank(versionLabel) || switchToLabel(content, versionLabel))
170            {
171                if (resultFieldsFiltered.size() > 0)
172                {
173                    saxContent(content, resultFieldsFiltered, defaultLocale);
174                }
175                else
176                {
177                    saxContent(content, resultFields, defaultLocale);
178                }
179            }
180        }
181        
182        XMLUtils.endElement(contentHandler, "contents");
183    }
184    
185    /**
186     * Get the common content type
187     * @param jsParameters The JS parameters
188     * @param model The search model
189     * @param contextualParameters The contextual parameters
190     * @return The common content type
191     */
192    @SuppressWarnings("unchecked")
193    protected Set<String> getCommonContentTypeIds(Map<String, Object> jsParameters, SearchModel model, Map<String, Object> contextualParameters)
194    {
195        // Get content types from model...
196        Collection<String> contentTypes = model.getContentTypes(contextualParameters);
197        
198        // ...or from the JS parameters
199        Map<String, Object> values = (Map<String, Object>) jsParameters.get("values");
200        if (values != null && values.containsKey("contentTypes"))
201        {
202            contentTypes = (List<String>) values.get("contentTypes");
203            
204        }
205        
206        // Retrieves the common ancestors of the content types
207        return _cTypeHelper.getCommonAncestors(contentTypes).stream()
208                .filter(_contentTypeExtensionPoint::hasExtension)
209                .collect(Collectors.toSet());
210    }
211    
212    /**
213     * Get the columns from JS parameters
214     * @param jsParameters The JS parameters
215     * @param contentTypeIds The content type identifiers
216     * @return the requested columns
217     */
218    @SuppressWarnings("unchecked")
219    protected List<Column> getColumnsFromParameters(Map<String, Object> jsParameters, Set<String> contentTypeIds)
220    {
221        Map<String, Object> values = (Map<String, Object>) jsParameters.get("values");
222        
223        if (values != null && values.containsKey("columns"))
224        {
225            Object columnsAsObj = values.get("columns");
226            if (columnsAsObj instanceof String)
227            {
228                return _columnHelper.getColumns((String) columnsAsObj, contentTypeIds);
229            }
230            else
231            {
232                return _columnHelper.getColumns((List<String>) columnsAsObj, contentTypeIds);
233            }
234        }
235        
236        return List.of();
237    }
238    
239    /**
240     * Switch to the revision corresponding to the specified label.
241     * @param content The content
242     * @param label the label to switch to
243     * @return <code>true</code> if a revision with this label exists and the content was switch, <code>false</code> otherwise.
244     */
245    protected boolean switchToLabel(Content content, String label)
246    {
247        if (content instanceof VersionAwareAmetysObject)
248        {
249            String[] allLabels = ((VersionAwareAmetysObject) content).getAllLabels();
250            if (ArrayUtils.contains(allLabels, label))
251            {
252                ((VersionAwareAmetysObject) content).switchToLabel(label);
253                return true;
254            }
255        }
256        
257        return false;
258    }
259    
260    /**
261     * SAX the result content
262     * @param content the result
263     * @param resultFields the result fields
264     * @param defaultLocale The locale to use for localized values if content's language is null.
265     * @throws SAXException if a error occurred during sax
266     * @throws AmetysRepositoryException if a error occurred
267     * @throws IOException if a error occurred
268     */
269    protected void saxContent(Content content, Collection<? extends ResultField> resultFields, Locale defaultLocale) throws SAXException, AmetysRepositoryException, IOException
270    {
271        Request request = ContextHelper.getRequest(_context);
272        
273        try
274        {
275            request.setAttribute(Content.class.getName(), content);
276            
277            AttributesImpl attrs = new AttributesImpl();
278            attrs.addCDATAAttribute("id", content.getId());
279            attrs.addCDATAAttribute("name", content.getName());
280            attrs.addCDATAAttribute("title", content.getTitle(defaultLocale));
281            if (content.getLanguage() != null)
282            {
283                attrs.addCDATAAttribute("language", content.getLanguage());
284            }
285                
286            XMLUtils.startElement(contentHandler, "content", attrs);
287            _contentRSH.saxResultFields(contentHandler, content, resultFields, defaultLocale);
288            XMLUtils.endElement(contentHandler, "content");
289        }
290        finally
291        {
292            request.setAttribute(Content.class.getName(), null);
293        }
294    }
295}