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