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