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 XMLUtils.startElement(contentHandler, "contents"); 102 _saxGroup(groupSearchContent, 0, resultFieldsFiltered, uiResultFields, locale); 103 XMLUtils.endElement(contentHandler, "contents"); 104 } 105 106 private List<String> _getGroupingFields(Map<String, Object> jsParameters) 107 { 108 @SuppressWarnings("unchecked") 109 List<String> groupingFields = (List<String>) jsParameters.get("groupingFields"); 110 if (groupingFields == null || groupingFields.size() == 1 && StringUtils.isEmpty(groupingFields.get(0))) 111 { 112 groupingFields = new ArrayList<>(); 113 } 114 115 return groupingFields; 116 } 117 118 private Collection< ? extends ResultField> _filterResultFields(List<String> groupingFields, List<String> columns, Collection< ? extends ResultField> resultFields) 119 { 120 // Remove grouping fields from the result fields (they don't need to be displayed) and only keep columns requested 121 Collection<ResultField> resultFieldsFiltered = new ArrayList<>(); 122 for (ResultField resultField : resultFields) 123 { 124 if (!groupingFields.contains(resultField.getId()) && (columns.size() == 0 || columns.contains(resultField.getId()))) 125 { 126 resultFieldsFiltered.add(resultField); 127 } 128 } 129 130 return resultFieldsFiltered; 131 } 132 133 /** 134 * Sax a group of content recursively. 135 * First level (level=0) will not create a xml group (usually, 1st group is just here to handle a list) 136 * @param group group to sax 137 * @param level current level (start with zero) 138 * @param resultFields result fields to sax 139 * @param uiResultFields The columns to be exported 140 * @param locale The locale for search. Can be null. 141 * @throws SAXException if a error occurred during sax 142 * @throws AmetysRepositoryException if a error occurred 143 * @throws IOException if a error occurred 144 * @throws ProcessingException if a error occurred 145 */ 146 private void _saxGroup(GroupSearchContent group, int level, Collection< ? extends ResultField> resultFields, Map<String, SearchUIColumn> uiResultFields, Locale locale) throws SAXException, AmetysRepositoryException, IOException, ProcessingException 147 { 148 if (level > 0) 149 { 150 AttributesImpl attrs = new AttributesImpl(); 151 attrs.addCDATAAttribute("level", String.valueOf(level)); 152 attrs.addCDATAAttribute("fullModelPath", group.getGroupFieldPath()); 153 attrs.addCDATAAttribute("value", group.getGroupName()); 154 XMLUtils.startElement(contentHandler, "group", attrs); 155 } 156 if (group.getSubList() != null) 157 { 158 for (GroupSearchContent groupSearchContent : group.getSubList()) 159 { 160 _saxGroup(groupSearchContent, level + 1, resultFields, uiResultFields, locale); 161 } 162 } 163 if (group.getContents() != null) 164 { 165 for (Content content : group.getContents()) 166 { 167 _saxContentForDoc(content, resultFields, uiResultFields, locale); 168 } 169 } 170 171 if (level > 0) 172 { 173 XMLUtils.endElement(contentHandler, "group"); 174 } 175 } 176 /** 177 * SAX the result content 178 * @param content the result 179 * @param resultFields the result fields 180 * @param uiResultFields The columns to be exported 181 * @param locale The locale for search. Can be null. 182 * @throws SAXException if a error occurred during sax 183 * @throws AmetysRepositoryException if a error occurred 184 * @throws IOException if a error occurred 185 * @throws ProcessingException if a error occurred 186 */ 187 private void _saxContentForDoc(Content content, Collection<? extends ResultField> resultFields, Map<String, SearchUIColumn> uiResultFields, Locale locale) throws SAXException, AmetysRepositoryException, IOException, ProcessingException 188 { 189 Request request = ContextHelper.getRequest(_context); 190 191 try 192 { 193 request.setAttribute(Content.class.getName(), content); 194 request.setAttribute(ResultField.class.getName(), resultFields); 195 request.setAttribute("viewName", "export-word"); 196 request.setAttribute("forceRemoteUrl", true); 197 198 request.setAttribute("uiResultFields", uiResultFields); 199 200 AttributesImpl attrs = new AttributesImpl(); 201 attrs.addCDATAAttribute("id", content.getId()); 202 attrs.addCDATAAttribute("name", content.getName()); 203 attrs.addCDATAAttribute("title", content.getTitle(locale)); 204 if (content.getLanguage() != null) 205 { 206 attrs.addCDATAAttribute("language", content.getLanguage()); 207 } 208 209 XMLUtils.startElement(contentHandler, "content", attrs); 210 Source contentSource = resolver.resolveURI("cocoon:/export/content.doc" + (locale != null ? "?lang=" + locale.getLanguage() : "")); 211 SourceUtil.toSAX(contentSource, new IgnoreRootHandler(contentHandler)); 212 213 XMLUtils.endElement(contentHandler, "content"); 214 } 215 finally 216 { 217 request.setAttribute(Content.class.getName(), null); 218 request.removeAttribute("forceRemoteUrl"); 219 } 220 } 221 222}