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 contentData.put("isSimple", _contentHelper.isSimple(content)); 284 285 contentData.put("properties", extractor.getValues(content, defaultLocale, contextualParameters)); 286 287 return contentData; 288 } 289 290 /** 291 * Set the facet results in the search results. 292 * @param searchResults the search results. 293 * @param facetResults the facet results as a Map<column name, Map<value, result count>>. 294 * @param model the search model. 295 * @param locale the locale for localized values 296 * @param contextualParameters the search contextual parameters. 297 */ 298 protected void setFacets(Map<String, Object> searchResults, Map<String, Map<String, Integer>> facetResults, SearchUIModel model, Locale locale, Map<String, Object> contextualParameters) 299 { 300 List<Object> facets = new ArrayList<>(); 301 searchResults.put("facets", facets); 302 303 // Index facets by search field name. 304 Map<String, SearchUICriterion> facetByName = new HashMap<>(); 305 for (SearchUICriterion facet : model.getFacetedCriteria(contextualParameters).values()) 306 { 307 SearchField searchField = facet.getSearchField(); 308 if (searchField != null) 309 { 310 facetByName.put(searchField.getName(), facet); 311 } 312 } 313 314 for (String criterionName : facetResults.keySet()) 315 { 316 SearchUICriterion searchCriterion = facetByName.get(criterionName); 317 318 if (searchCriterion != null) 319 { 320 Map<String, Integer> values = facetResults.get(criterionName); 321 322 Map<String, Object> column = new HashMap<>(); 323 facets.add(column); 324 325 List<Map<String, Object>> criterionFacets = new ArrayList<>(); 326 column.put("children", criterionFacets); 327 column.put("name", criterionName); 328 column.put("label", searchCriterion.getLabel()); 329 column.put("type", "criterion"); 330 331 for (String value : values.keySet()) 332 { 333 Map<String, Object> facet = new LinkedHashMap<>(); 334 criterionFacets.add(facet); 335 336 Integer count = values.get(value); 337 I18nizableText label = searchCriterion.getFacetLabel(value, locale); 338 339 facet.put("value", value); 340 facet.put("count", count); 341 facet.put("label", label); 342 facet.put("type", "facet"); 343 } 344 } 345 } 346 } 347}