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