001/*
002 *  Copyright 2015 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.Collections;
021import java.util.HashMap;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Locale;
025import java.util.Map;
026
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.cocoon.ProcessingException;
030import org.apache.cocoon.environment.ObjectModelHelper;
031import org.apache.cocoon.environment.Request;
032import org.apache.cocoon.reading.ServiceableReader;
033import org.apache.commons.lang3.StringUtils;
034import org.xml.sax.SAXException;
035
036import org.ametys.cms.content.ContentHelper;
037import org.ametys.cms.contenttype.ContentTypesHelper;
038import org.ametys.cms.repository.Content;
039import org.ametys.cms.search.SearchField;
040import org.ametys.cms.search.SearchResult;
041import org.ametys.cms.search.SearchResults;
042import org.ametys.cms.search.content.ContentValuesExtractorFactory;
043import org.ametys.cms.search.content.ContentValuesExtractorFactory.ContentValuesExtractor;
044import org.ametys.cms.search.content.ContentValuesExtractorFactory.SearchModelContentValuesExtractor;
045import org.ametys.cms.search.model.SearchCriterion;
046import org.ametys.cms.search.model.SearchCriterionHelper;
047import org.ametys.cms.search.model.SearchModel;
048import org.ametys.core.util.JSONUtils;
049import org.ametys.core.util.ServerCommHelper;
050import org.ametys.plugins.repository.AmetysObjectResolver;
051import org.ametys.plugins.repository.UnknownAmetysObjectException;
052import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
053import org.ametys.runtime.i18n.I18nizableText;
054
055/**
056 * JSON reader for search for contents.
057 */
058public class SearchJsonReader extends ServiceableReader
059{
060    /** JSON utils */
061    protected JSONUtils _jsonUtils;
062    
063    /** The serverComm helper */
064    protected ServerCommHelper _serverCommHelper;
065    
066    /** The content types helper. */
067    protected ContentTypesHelper _contentTypesHelper;
068    
069    /** The content helper */
070    protected ContentHelper _contentHelper;
071    
072    /** The Ametys object resolver */
073    protected AmetysObjectResolver _resolver;
074    
075    /** The ContentGridComponent instance */
076    protected ContentGridComponent _contentGridComponent;
077    
078    /** The search criterion helper */
079    protected SearchCriterionHelper _searchCriterionHelper;
080    
081    private ContentValuesExtractorFactory _valuesExtractorFactory;
082
083
084    @Override
085    public void service(ServiceManager serviceManager) throws ServiceException
086    {
087        super.service(serviceManager);
088        
089        _jsonUtils = (JSONUtils) serviceManager.lookup(JSONUtils.ROLE);
090        _serverCommHelper = (ServerCommHelper) serviceManager.lookup(ServerCommHelper.ROLE);
091        _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
092        _contentHelper = (ContentHelper) serviceManager.lookup(ContentHelper.ROLE);
093        _valuesExtractorFactory = (ContentValuesExtractorFactory) serviceManager.lookup(ContentValuesExtractorFactory.ROLE);
094        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
095        _contentGridComponent = (ContentGridComponent) serviceManager.lookup(ContentGridComponent.ROLE);
096        _searchCriterionHelper = (SearchCriterionHelper) serviceManager.lookup(SearchCriterionHelper.ROLE);
097    }
098    
099    @Override
100    public String getMimeType()
101    {
102        return "application/json";
103    }
104    
105    @SuppressWarnings("unchecked")
106    @Override
107    public void generate() throws IOException, SAXException, ProcessingException
108    {
109        Request request = ObjectModelHelper.getRequest(objectModel);
110        
111        String workspaceName = parameters.getParameter("workspace", null);
112        String originalWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
113        try
114        {
115            if (StringUtils.isNotEmpty(workspaceName))
116            {
117                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
118            }
119            
120            SearchResults<Content> results = (SearchResults<Content>) request.getAttribute(SearchAction.SEARCH_RESULTS);
121            SearchModel model = (SearchModel) request.getAttribute(SearchAction.SEARCH_MODEL);
122            I18nizableText queryError = (I18nizableText) request.getAttribute(SearchAction.QUERY_ERROR);
123            
124            Locale defaultLocale = (Locale) request.getAttribute(SearchAction.SEARCH_LOCALE);
125            
126            Map<String, Object> jsonObject = new HashMap<>();
127            
128            if (queryError != null)
129            {
130                jsonObject.put("error", queryError);
131            }
132            else if (results != null)
133            {
134                convertResults2JsonObject(jsonObject, results, model, defaultLocale);
135            }
136            else
137            {
138                List<String> contentIds = (List<String>) request.getAttribute(SearchAction.SEARCH_CONTENTS);
139                convertContents2JsonObject(jsonObject, contentIds, model, defaultLocale);
140            }
141            
142            _jsonUtils.convertObjectToJson(out, jsonObject);
143        }
144        finally
145        {
146            if (StringUtils.isNotEmpty(workspaceName))
147            {
148                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, originalWorkspace);
149            }
150        }
151    }
152    
153    /**
154     * Convert the query results to a JSON object
155     * @param results2Json the JSON object to fill.
156     * @param results the query results
157     * @param model the search model
158     * @param defaultLocale the default locale for localized values if content's language is null. 
159     * @throws ProcessingException If an error occurs.
160     */
161    protected void convertResults2JsonObject(Map<String, Object> results2Json, SearchResults<Content> results, SearchModel model, Locale defaultLocale) throws ProcessingException
162    {
163        Map<String, Object> jsParameters = _serverCommHelper.getJsParameters();
164        @SuppressWarnings("unchecked")
165        Map<String, Object> values = (Map<String, Object>) jsParameters.get("values");
166        @SuppressWarnings("unchecked")
167        Map<String, Object> contextualParameters = (Map<String, Object>) jsParameters.get("contextualParameters");
168        if (contextualParameters == null)
169        {
170            contextualParameters = Collections.emptyMap();
171        }
172        
173        setContents(results2Json, results, model, defaultLocale, contextualParameters);
174        
175        setFacets(results2Json, results.getFacetResults(), model, defaultLocale, contextualParameters);
176        
177        results2Json.put("values", values);
178        results2Json.put("total", results.getTotalCount());
179    }
180    
181    /**
182     * Convert the query results to a JSON object
183     * @param results2Json the JSON object to fill.
184     * @param contentIds the ids of contents
185     * @param model the search model
186     * @param defaultLocale the default locale for localized values if content's language is null. 
187     * @throws ProcessingException If an error occurs.
188     */
189    protected void convertContents2JsonObject(Map<String, Object> results2Json, List<String> contentIds, SearchModel model, Locale defaultLocale) throws ProcessingException
190    {
191        Map<String, Object> jsParameters = _serverCommHelper.getJsParameters();
192        @SuppressWarnings("unchecked")
193        Map<String, Object> contextualParameters = (Map<String, Object>) jsParameters.get("contextualParameters");
194        if (contextualParameters == null)
195        {
196            contextualParameters = Collections.emptyMap();
197        }
198        
199        int size = setContents(results2Json, contentIds, model, defaultLocale, contextualParameters);
200        results2Json.put("total", size);
201    }
202    
203    /**
204     * Extract the desired contents from the {@link SearchResults} object and set them in the search results.
205     * @param searchResults the search results to populate.
206     * @param results the {@link SearchResults}.
207     * @param model the search model.
208     * @param defaultLocale the default locale for localized values if content's language is null. 
209     * @param contextualParameters the search contextual parameters.
210     */
211    protected void setContents(Map<String, Object> searchResults, SearchResults<Content> results, SearchModel model, Locale defaultLocale, Map<String, Object> contextualParameters)
212    {
213        List<Map<String, Object>> contents = new ArrayList<>();
214        searchResults.put("contents", contents);
215        
216        SearchModelContentValuesExtractor extractor = _valuesExtractorFactory.create(model);
217        
218        for (SearchResult<Content> result : results.getResults())
219        {
220            contents.add(getContentData(result.getObject(), extractor, defaultLocale, contextualParameters));
221        }
222    }
223    
224    /**
225     * Extract the desired contents and set them in the search results.
226     * @param searchResults the search results to populate.
227     * @param contentIds the id of desired contents
228     * @param model the search model.
229     * @param defaultLocale the default locale for localized values if content's language is null. 
230     * @param contextualParameters the search contextual parameters.
231     * @return the number of resolved contents without error 
232     */
233    protected int setContents(Map<String, Object> searchResults, List<String> contentIds, SearchModel model, Locale defaultLocale, Map<String, Object> contextualParameters)
234    { 
235        List<Map<String, Object>> contents = new ArrayList<>();
236        searchResults.put("contents", contents);
237        
238        SearchModelContentValuesExtractor extractor = _valuesExtractorFactory.create(model);
239        
240        int count = 0;
241        for (String contentId : contentIds)
242        {
243            try
244            {
245                Content content = _resolver.resolveById(contentId);
246                contents.add(getContentData(content, extractor, defaultLocale, contextualParameters));
247                count++;
248            }
249            catch (UnknownAmetysObjectException e)
250            {
251                getLogger().warn("The Ametys object with id '" + contentId + "' does not exist anymore.");
252            }
253        }
254        return count;
255    }
256    
257    /**
258     * Generate standard content data.
259     * @param content The content.
260     * @param extractor The content values extractor which generates.
261     * @param defaultLocale the default locale for localized values if content's language is null. 
262     * @param contextualParameters The search contextual parameters.
263     * @return A Map containing the content data.
264     */
265    protected Map<String, Object> getContentData(Content content, ContentValuesExtractor extractor, Locale defaultLocale, Map<String, Object> contextualParameters)
266    {
267        return _contentGridComponent.getContentData(content, extractor, defaultLocale, contextualParameters);
268    }
269    
270    /**
271     * Set the facet results in the search results.
272     * @param searchResults the search results.
273     * @param facetResults the facet results as a Map&lt;column name, Map&lt;value, result count&gt;&gt;.
274     * @param model the search model.
275     * @param locale the locale for localized values
276     * @param contextualParameters the search contextual parameters.
277     */
278    protected void setFacets(Map<String, Object> searchResults, Map<String, Map<String, Integer>> facetResults, SearchModel model, Locale locale, Map<String, Object> contextualParameters)
279    {
280        List<Object> facets = new ArrayList<>();
281        searchResults.put("facets", facets);
282        
283        // Index facets by search field name.
284        Map<String, SearchCriterion> facetByName = new HashMap<>();
285        for (SearchCriterion facet : model.getFacetedCriteria(contextualParameters).values())
286        {
287            SearchField searchField = facet.getSearchField();
288            if (searchField != null)
289            {
290                facetByName.put(searchField.getName(), facet);
291            }
292        }
293        
294        for (String criterionName : facetResults.keySet())
295        {
296            SearchCriterion searchCriterion = facetByName.get(criterionName);
297            
298            if (searchCriterion != null)
299            {
300                Map<String, Integer> values = facetResults.get(criterionName);
301                
302                Map<String, Object> column = new HashMap<>();
303                facets.add(column);
304                
305                List<Map<String, Object>> criterionFacets = new ArrayList<>();
306                column.put("children", criterionFacets);
307                column.put("name", criterionName);
308                column.put("label", searchCriterion.getLabel());
309                column.put("type", "criterion");
310                
311                for (String value : values.keySet())
312                {
313                    Map<String, Object> facet = new LinkedHashMap<>();
314                    criterionFacets.add(facet);
315                    
316                    Integer count = values.get(value);
317                    I18nizableText label = _searchCriterionHelper.getFacetLabel(searchCriterion, value, locale);
318                    
319                    facet.put("value", value);
320                    facet.put("count", count);
321                    facet.put("label", label);
322                    facet.put("type", "facet");
323                }
324            }
325        }
326    }
327}