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