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