001/* 002 * Copyright 2013 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.Collection; 021import java.util.Collections; 022import java.util.List; 023import java.util.Locale; 024import java.util.Map; 025import java.util.Optional; 026import java.util.Set; 027import java.util.stream.Collectors; 028 029import org.apache.avalon.framework.context.Context; 030import org.apache.avalon.framework.context.ContextException; 031import org.apache.avalon.framework.context.Contextualizable; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.cocoon.ProcessingException; 035import org.apache.cocoon.components.ContextHelper; 036import org.apache.cocoon.environment.ObjectModelHelper; 037import org.apache.cocoon.environment.Request; 038import org.apache.cocoon.generation.ServiceableGenerator; 039import org.apache.cocoon.xml.AttributesImpl; 040import org.apache.cocoon.xml.XMLUtils; 041import org.apache.commons.lang3.ArrayUtils; 042import org.apache.commons.lang3.StringUtils; 043import org.xml.sax.SAXException; 044 045import org.ametys.cms.contenttype.ContentType; 046import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 047import org.ametys.cms.contenttype.ContentTypesHelper; 048import org.ametys.cms.repository.Content; 049import org.ametys.cms.search.SearchResult; 050import org.ametys.cms.search.SearchResults; 051import org.ametys.cms.search.content.ContentValuesExtractorFactory; 052import org.ametys.cms.search.model.ResultField; 053import org.ametys.cms.search.model.SearchModel; 054import org.ametys.cms.search.ui.model.ColumnHelper; 055import org.ametys.cms.search.ui.model.ColumnHelper.Column; 056import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint; 057import org.ametys.core.util.ServerCommHelper; 058import org.ametys.plugins.repository.AmetysRepositoryException; 059import org.ametys.plugins.repository.version.VersionAwareAmetysObject; 060 061/** 062 * Generate contents returned by the {@link SearchAction}. 063 */ 064public class SearchGenerator extends ServiceableGenerator implements Contextualizable 065{ 066 /** Constant for getting content in specific version label */ 067 public static final String CONTENT_VERSION_LABEL = "versionLabel"; 068 069 /** The server comm helper */ 070 protected ServerCommHelper _serverCommHelper; 071 /** The content values extractor factory. */ 072 protected ContentValuesExtractorFactory _valuesExtractorFactory; // FIXME not used 073 /** Helper for saxing result */ 074 protected ContentResultSetHelper _contentRSH; 075 /** Context */ 076 protected Context _context; 077 /** The search model manager */ 078 protected SearchUIModelExtensionPoint _searchModelManager; 079 /** The content type helper. */ 080 protected ContentTypesHelper _cTypeHelper; 081 /** The helper for columns */ 082 protected ColumnHelper _columnHelper; 083 /** The content type extension point */ 084 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 085 086 @Override 087 public void service(ServiceManager smanager) throws ServiceException 088 { 089 super.service(smanager); 090 _serverCommHelper = (ServerCommHelper) smanager.lookup(ServerCommHelper.ROLE); 091 _valuesExtractorFactory = (ContentValuesExtractorFactory) smanager.lookup(ContentValuesExtractorFactory.ROLE); 092 093 _contentRSH = (ContentResultSetHelper) smanager.lookup(ContentResultSetHelper.ROLE); 094 _searchModelManager = (SearchUIModelExtensionPoint) smanager.lookup(SearchUIModelExtensionPoint.ROLE); 095 096 _cTypeHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE); 097 _columnHelper = (ColumnHelper) smanager.lookup(ColumnHelper.ROLE); 098 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 099 } 100 101 @Override 102 public void contextualize(Context context) throws ContextException 103 { 104 _context = context; 105 } 106 107 @SuppressWarnings("unchecked") 108 @Override 109 public void generate() throws IOException, SAXException, ProcessingException 110 { 111 Request request = ObjectModelHelper.getRequest(objectModel); 112 113 SearchResults<Content> results = (SearchResults<Content>) request.getAttribute(SearchAction.SEARCH_RESULTS); 114 SearchModel model = (SearchModel) request.getAttribute(SearchAction.SEARCH_MODEL); 115 116 Map<String, Object> jsParameters = _serverCommHelper.getJsParameters(); 117 118 Map<String, Object> contextualParameters = (Map<String, Object>) jsParameters.get("contextualParameters"); 119 if (contextualParameters == null) 120 { 121 contextualParameters = Collections.emptyMap(); 122 } 123 124 Locale defaultLocale = (Locale) request.getAttribute(SearchAction.SEARCH_LOCALE); 125 126 Collection< ? extends ResultField> resultFields = model.getResultFields(contextualParameters).values(); 127 128 String versionLabel = (String) jsParameters.get(CONTENT_VERSION_LABEL); 129 130 saxContents(model, results, resultFields, versionLabel, jsParameters, defaultLocale, contextualParameters); 131 } 132 133 /** 134 * Sax a set of contents 135 * @param model search model 136 * @param results contents 137 * @param resultFields fields to retreive 138 * @param versionLabel version label 139 * @param jsParameters parameters of the search 140 * @param defaultLocale The locale to use for localized values if content's language is null. 141 * @param contextualParameters The contextual parameters 142 * @throws SAXException if a error occurred during sax 143 * @throws AmetysRepositoryException if a error occurred 144 * @throws IOException if a error occurred 145 * @throws ProcessingException if a error occurred 146 */ 147 protected void saxContents(SearchModel model, SearchResults<Content> results, Collection< ? extends ResultField> resultFields, String versionLabel, Map<String, Object> jsParameters, Locale defaultLocale, Map<String, Object> contextualParameters) throws SAXException, AmetysRepositoryException, IOException, ProcessingException 148 { 149 // Common content type 150 Optional<String> commonContentType = getCommonContentType(jsParameters, model, contextualParameters) 151 .map(ContentType::getId); 152 153 // Filter the resultFields, using columns if available 154 Collection<ResultField> resultFieldsFiltered = new ArrayList<>(); 155 List<String> columns = getColumnsFromParameters(jsParameters, commonContentType) 156 .stream() 157 .map(Column::getId) 158 .collect(Collectors.toList()); 159 for (ResultField resultField : resultFields) 160 { 161 if (columns.size() == 0 || columns.contains(resultField.getId())) 162 { 163 resultFieldsFiltered.add(resultField); 164 } 165 } 166 167 contentHandler.startDocument(); 168 XMLUtils.startElement(contentHandler, "contents"); 169 170 Iterable<SearchResult<Content>> contents = results.getResults(); 171 for (SearchResult<Content> result : contents) 172 { 173 Content content = result.getObject(); 174 175 if (StringUtils.isBlank(versionLabel) || switchToLabel(content, versionLabel)) 176 { 177 if (resultFieldsFiltered.size() > 0) 178 { 179 saxContent(content, resultFieldsFiltered, defaultLocale); 180 } 181 else 182 { 183 saxContent(content, resultFields, defaultLocale); 184 } 185 } 186 } 187 188 XMLUtils.endElement(contentHandler, "contents"); 189 contentHandler.endDocument(); 190 } 191 192 /** 193 * Get the common content type 194 * @param jsParameters The JS parameters 195 * @param model The search model 196 * @param contextualParameters The contextual parameters 197 * @return The common content type 198 */ 199 @SuppressWarnings("unchecked") 200 protected Optional<ContentType> getCommonContentType(Map<String, Object> jsParameters, SearchModel model, Map<String, Object> contextualParameters) 201 { 202 Map<String, Object> values = (Map<String, Object>) jsParameters.get("values"); 203 204 if (values != null && values.containsKey("contentTypes")) 205 { 206 List<String> contentTypes = (List<String>) values.get("contentTypes"); 207 String commonCTypeId = _cTypeHelper.getCommonAncestor(contentTypes); 208 if (commonCTypeId != null) 209 { 210 return Optional.ofNullable(_contentTypeExtensionPoint.getExtension(commonCTypeId)); 211 } 212 } 213 214 // Get the common content type from model 215 Set<String> contentTypes = model.getContentTypes(contextualParameters); 216 String commonCTypeId = _cTypeHelper.getCommonAncestor(contentTypes); 217 if (commonCTypeId != null) 218 { 219 return Optional.ofNullable(_contentTypeExtensionPoint.getExtension(commonCTypeId)); 220 } 221 222 return Optional.empty(); 223 } 224 225 /** 226 * Get the columns from JS parameters 227 * @param jsParameters The JS parameters 228 * @param contentType The content type 229 * @return the requested columns 230 */ 231 @SuppressWarnings("unchecked") 232 protected List<Column> getColumnsFromParameters(Map<String, Object> jsParameters, Optional<String> contentType) 233 { 234 Map<String, Object> values = (Map<String, Object>) jsParameters.get("values"); 235 236 if (values != null && values.containsKey("columns")) 237 { 238 Object columnsAsObj = values.get("columns"); 239 if (columnsAsObj instanceof String) 240 { 241 return _columnHelper.getColumns((String) columnsAsObj, contentType); 242 } 243 else 244 { 245 return _columnHelper.getColumns((List<String>) columnsAsObj, contentType); 246 } 247 } 248 return Collections.EMPTY_LIST; 249 } 250 251 /** 252 * Switch to the revision corresponding to the specified label. 253 * @param content The content 254 * @param label the label to switch to 255 * @return <code>true</code> if a revision with this label exists and the content was switch, <code>false</code> otherwise. 256 */ 257 protected boolean switchToLabel(Content content, String label) 258 { 259 if (content instanceof VersionAwareAmetysObject) 260 { 261 String[] allLabels = ((VersionAwareAmetysObject) content).getAllLabels(); 262 if (ArrayUtils.contains(allLabels, label)) 263 { 264 ((VersionAwareAmetysObject) content).switchToLabel(label); 265 return true; 266 } 267 } 268 269 return false; 270 } 271 272 /** 273 * SAX the result content 274 * @param content the result 275 * @param resultFields the result fields 276 * @param defaultLocale The locale to use for localized values if content's language is null. 277 * @throws SAXException if a error occurred during sax 278 * @throws AmetysRepositoryException if a error occurred 279 * @throws IOException if a error occurred 280 */ 281 protected void saxContent(Content content, Collection<? extends ResultField> resultFields, Locale defaultLocale) throws SAXException, AmetysRepositoryException, IOException 282 { 283 Request request = ContextHelper.getRequest(_context); 284 285 try 286 { 287 request.setAttribute(Content.class.getName(), content); 288 289 AttributesImpl attrs = new AttributesImpl(); 290 attrs.addCDATAAttribute("id", content.getId()); 291 attrs.addCDATAAttribute("name", content.getName()); 292 attrs.addCDATAAttribute("title", content.getTitle(defaultLocale)); 293 if (content.getLanguage() != null) 294 { 295 attrs.addCDATAAttribute("language", content.getLanguage()); 296 } 297 298 XMLUtils.startElement(contentHandler, "content", attrs); 299 _contentRSH.saxResultFields(contentHandler, content, resultFields, defaultLocale); 300 XMLUtils.endElement(contentHandler, "content"); 301 } 302 finally 303 { 304 request.setAttribute(Content.class.getName(), null); 305 } 306 } 307}