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