001/* 002 * Copyright 2017 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.Set; 026import java.util.stream.Collectors; 027 028import org.apache.avalon.framework.container.ContainerUtil; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.cocoon.ProcessingException; 032import org.apache.cocoon.components.ContextHelper; 033import org.apache.cocoon.components.source.SourceUtil; 034import org.apache.cocoon.environment.Request; 035import org.apache.cocoon.xml.AttributesImpl; 036import org.apache.cocoon.xml.XMLUtils; 037import org.apache.commons.lang3.StringUtils; 038import org.apache.excalibur.source.Source; 039import org.xml.sax.SAXException; 040 041import org.ametys.cms.contenttype.ContentType; 042import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 043import org.ametys.cms.contenttype.ContentTypesHelper; 044import org.ametys.cms.repository.Content; 045import org.ametys.cms.search.GroupSearchContentHelper; 046import org.ametys.cms.search.SearchResults; 047import org.ametys.cms.search.model.ResultField; 048import org.ametys.cms.search.model.SearchModel; 049import org.ametys.cms.search.solr.CriteriaSearchUIModelWrapper; 050import org.ametys.cms.search.solr.CriteriaSearchUIModelWrapper.Column; 051import org.ametys.cms.search.ui.model.SearchUIColumn; 052import org.ametys.cms.search.ui.model.SearchUIModel; 053import org.ametys.core.util.AvalonLoggerAdapter; 054import org.ametys.core.util.IgnoreRootHandler; 055import org.ametys.plugins.repository.AmetysRepositoryException; 056 057/** 058 * Generate contents returned by the {@link SearchAction}, groups by selected fields. 059 */ 060public class DocSearchGenerator extends SearchGenerator 061{ 062 private ContentTypesHelper _contentTypesHelper; 063 private ContentTypeExtensionPoint _contentTypeExtensionPoint; 064 private GroupSearchContentHelper _groupSearchContentHelper; 065 066 @Override 067 public void service(ServiceManager smanager) throws ServiceException 068 { 069 super.service(smanager); 070 _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE); 071 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 072 _groupSearchContentHelper = (GroupSearchContentHelper) smanager.lookup(GroupSearchContentHelper.ROLE); 073 } 074 075 @Override 076 @SuppressWarnings("unchecked") 077 protected void saxContents(SearchModel model, SearchResults<Content> results, Collection< ? extends ResultField> resultFields, String versionLabel, Map<String, Object> jsParameters, Locale locale) throws SAXException, AmetysRepositoryException, IOException, ProcessingException 078 { 079 String modelId = (String) jsParameters.get("model"); 080 SearchUIModel uiModel = _searchModelManager.getExtension(modelId); 081 Map<String, Object> contextualParameters = (Map<String, Object>) jsParameters.get("contextualParameters"); 082 if (contextualParameters == null) 083 { 084 contextualParameters = Collections.emptyMap(); 085 } 086 087 ContentType cType = _getCommonContentType(jsParameters, uiModel, contextualParameters); 088 089 List<String> groupingFields = _getGroupingFields(jsParameters); 090 091 List<Column> columns = getColumnsFromParameters(jsParameters); 092 093 Collection< ? extends ResultField> resultFieldsFiltered = _filterResultFields(groupingFields, columns.stream().map(Column::getId).collect(Collectors.toList()), resultFields); 094 095 CriteriaSearchUIModelWrapper modelWrapper = new CriteriaSearchUIModelWrapper(uiModel, manager, _context, new AvalonLoggerAdapter(getLogger())); 096 097 try 098 { 099 ContainerUtil.service(modelWrapper, manager); 100 modelWrapper.setResultColumns(cType != null ? cType.getId() : null, columns, contextualParameters); 101 } 102 catch (Exception e) 103 { 104 throw new AmetysRepositoryException(e); 105 } 106 Map<String, SearchUIColumn> uiResultFields = modelWrapper.getResultFields(contextualParameters); 107 108 GroupSearchContent groupSearchContent = _groupSearchContentHelper.organizeContentsInGroups(results.getObjects(), groupingFields, cType, locale); 109 110 contentHandler.startDocument(); 111 XMLUtils.startElement(contentHandler, "contents"); 112 113 _saxGroup(groupSearchContent, 0, resultFieldsFiltered, uiResultFields, locale); 114 115 XMLUtils.endElement(contentHandler, "contents"); 116 117 contentHandler.endDocument(); 118 } 119 120 @SuppressWarnings("unchecked") 121 private ContentType _getCommonContentType(Map<String, Object> jsParameters, SearchUIModel uiModel, Map<String, Object> contextualParameters) 122 { 123 Map<String, Object> values = (Map<String, Object>) jsParameters.get("values"); 124 125 if (values != null && values.containsKey("contentTypes")) 126 { 127 List<String> contentTypes = (List<String>) values.get("contentTypes"); 128 String commonCTypeId = _contentTypesHelper.getCommonAncestor(contentTypes); 129 if (commonCTypeId != null) 130 { 131 return _contentTypeExtensionPoint.getExtension(commonCTypeId); 132 } 133 } 134 135 // Get the common content type from model 136 Set<String> contentTypes = uiModel.getContentTypes(contextualParameters); 137 String commonCTypeId = _contentTypesHelper.getCommonAncestor(contentTypes); 138 if (commonCTypeId != null) 139 { 140 return _contentTypeExtensionPoint.getExtension(commonCTypeId); 141 } 142 143 return null; 144 } 145 146 private List<String> _getGroupingFields(Map<String, Object> jsParameters) 147 { 148 @SuppressWarnings("unchecked") 149 List<String> groupingFields = (List<String>) jsParameters.get("groupingFields"); 150 if (groupingFields == null || groupingFields.size() == 1 && StringUtils.isEmpty(groupingFields.get(0))) 151 { 152 groupingFields = new ArrayList<>(); 153 } 154 155 return groupingFields; 156 } 157 158 private Collection< ? extends ResultField> _filterResultFields(List<String> groupingFields, List<String> columns, Collection< ? extends ResultField> resultFields) 159 { 160 // Remove grouping fields from the result fields (they don't need to be displayed) and only keep columns requested 161 Collection<ResultField> resultFieldsFiltered = new ArrayList<>(); 162 for (ResultField resultField : resultFields) 163 { 164 if (!groupingFields.contains(resultField.getId()) && (columns.size() == 0 || columns.contains(resultField.getId()))) 165 { 166 resultFieldsFiltered.add(resultField); 167 } 168 } 169 170 return resultFieldsFiltered; 171 } 172 173 /** 174 * Sax a group of content recursively. 175 * First level (level=0) will not create a xml group (usually, 1st group is just here to handle a list) 176 * @param group group to sax 177 * @param level current level (start with zero) 178 * @param resultFields result fields to sax 179 * @param uiResultFields The columns to be exported 180 * @param locale The locale for search. Can be null. 181 * @throws SAXException if a error occurred during sax 182 * @throws AmetysRepositoryException if a error occurred 183 * @throws IOException if a error occurred 184 * @throws ProcessingException if a error occurred 185 */ 186 private void _saxGroup(GroupSearchContent group, int level, Collection< ? extends ResultField> resultFields, Map<String, SearchUIColumn> uiResultFields, Locale locale) throws SAXException, AmetysRepositoryException, IOException, ProcessingException 187 { 188 if (level > 0) 189 { 190 AttributesImpl attrs = new AttributesImpl(); 191 attrs.addCDATAAttribute("level", String.valueOf(level)); 192 attrs.addCDATAAttribute("fieldPath", group.getGroupFieldPath()); 193 attrs.addCDATAAttribute("value", group.getGroupName()); 194 XMLUtils.startElement(contentHandler, "group", attrs); 195 } 196 if (group.getSubList() != null) 197 { 198 for (GroupSearchContent groupSearchContent : group.getSubList()) 199 { 200 _saxGroup(groupSearchContent, level + 1, resultFields, uiResultFields, locale); 201 } 202 } 203 if (group.getContents() != null) 204 { 205 for (Content content : group.getContents()) 206 { 207 _saxContentForDoc(content, resultFields, uiResultFields, locale); 208 } 209 } 210 211 if (level > 0) 212 { 213 XMLUtils.endElement(contentHandler, "group"); 214 } 215 } 216 /** 217 * SAX the result content 218 * @param content the result 219 * @param resultFields the result fields 220 * @param uiResultFields The columns to be exported 221 * @param locale The locale for search. Can be null. 222 * @throws SAXException if a error occurred during sax 223 * @throws AmetysRepositoryException if a error occurred 224 * @throws IOException if a error occurred 225 * @throws ProcessingException if a error occurred 226 */ 227 private void _saxContentForDoc(Content content, Collection<? extends ResultField> resultFields, Map<String, SearchUIColumn> uiResultFields, Locale locale) throws SAXException, AmetysRepositoryException, IOException, ProcessingException 228 { 229 Request request = ContextHelper.getRequest(_context); 230 231 try 232 { 233 request.setAttribute(Content.class.getName(), content); 234 request.setAttribute(ResultField.class.getName(), resultFields); 235 request.setAttribute("metadataSetName", "export-word"); 236 request.setAttribute("forceRemoteUrl", true); 237 238 request.setAttribute("uiResultFields", uiResultFields); 239 240 AttributesImpl attrs = new AttributesImpl(); 241 attrs.addCDATAAttribute("id", content.getId()); 242 attrs.addCDATAAttribute("name", content.getName()); 243 attrs.addCDATAAttribute("title", content.getTitle(locale)); 244 if (content.getLanguage() != null) 245 { 246 attrs.addCDATAAttribute("language", content.getLanguage()); 247 } 248 249 XMLUtils.startElement(contentHandler, "content", attrs); 250 Source contentSource = resolver.resolveURI("cocoon:/export/content.doc" + (locale != null ? "?lang=" + locale.getLanguage() : "")); 251 SourceUtil.toSAX(contentSource, new IgnoreRootHandler(contentHandler)); 252 253 XMLUtils.endElement(contentHandler, "content"); 254 } 255 finally 256 { 257 request.setAttribute(Content.class.getName(), null); 258 request.removeAttribute("forceRemoteUrl"); 259 } 260 } 261 262}