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