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