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