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<column name, Map<value, result count>>. 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}