001/* 002 * Copyright 2014 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.odf.export.pdf; 017 018import java.io.IOException; 019import java.net.MalformedURLException; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.List; 023import java.util.Map; 024import java.util.stream.Collectors; 025 026import org.apache.avalon.framework.parameters.ParameterException; 027import org.apache.avalon.framework.service.ServiceException; 028import org.apache.avalon.framework.service.ServiceManager; 029import org.apache.cocoon.ProcessingException; 030import org.apache.cocoon.components.source.impl.SitemapSource; 031import org.apache.cocoon.environment.ObjectModelHelper; 032import org.apache.cocoon.generation.ServiceableGenerator; 033import org.apache.cocoon.xml.AttributesImpl; 034import org.apache.cocoon.xml.XMLUtils; 035import org.apache.commons.lang3.StringUtils; 036import org.xml.sax.SAXException; 037 038import org.ametys.cms.contenttype.ContentAttributeDefinition; 039import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 040import org.ametys.cms.repository.Content; 041import org.ametys.cms.repository.ContentTypeExpression; 042import org.ametys.cms.repository.LanguageExpression; 043import org.ametys.core.util.IgnoreRootHandler; 044import org.ametys.core.util.LambdaUtils; 045import org.ametys.odf.NoLiveVersionException; 046import org.ametys.odf.ODFHelper; 047import org.ametys.odf.ProgramItem; 048import org.ametys.odf.catalog.Catalog; 049import org.ametys.odf.catalog.CatalogsManager; 050import org.ametys.odf.enumeration.OdfReferenceTableEntry; 051import org.ametys.odf.enumeration.OdfReferenceTableHelper; 052import org.ametys.odf.orgunit.OrgUnit; 053import org.ametys.odf.program.AbstractProgram; 054import org.ametys.odf.program.Program; 055import org.ametys.odf.program.ProgramFactory; 056import org.ametys.odf.program.ProgramPart; 057import org.ametys.odf.program.SubProgram; 058import org.ametys.odf.program.SubProgramFactory; 059import org.ametys.odf.schedulable.CatalogPDFExportSchedulable; 060import org.ametys.plugins.repository.AmetysObjectIterable; 061import org.ametys.plugins.repository.AmetysObjectResolver; 062import org.ametys.plugins.repository.AmetysRepositoryException; 063import org.ametys.plugins.repository.CollectionIterable; 064import org.ametys.plugins.repository.UnknownAmetysObjectException; 065import org.ametys.plugins.repository.query.QueryHelper; 066import org.ametys.plugins.repository.query.SortCriteria; 067import org.ametys.plugins.repository.query.expression.AndExpression; 068import org.ametys.plugins.repository.query.expression.Expression; 069import org.ametys.plugins.repository.query.expression.Expression.Operator; 070import org.ametys.plugins.repository.query.expression.OrExpression; 071import org.ametys.plugins.repository.query.expression.StringExpression; 072import org.ametys.runtime.model.View; 073 074/** 075 * Generator producing the SAX events for the catalogue summary 076 */ 077public class FOProgramsGenerator extends ServiceableGenerator 078{ 079 private static final String __EXPORT_MODE = "catalog"; 080 081 /** The Ametys object resolver */ 082 protected AmetysObjectResolver _resolver; 083 /** The content type extension point */ 084 protected ContentTypeExtensionPoint _ctypeEP; 085 /** The ODf helper */ 086 protected ODFHelper _odfHelper; 087 /** The ODf enumeration helper */ 088 protected OdfReferenceTableHelper _odfTableRefHelper; 089 /** The catalog manager */ 090 protected CatalogsManager _catalogManager; 091 /** The query helper */ 092 protected org.ametys.plugins.queriesdirectory.QueryHelper _queryHelper; 093 094 @Override 095 public void service(ServiceManager sManager) throws ServiceException 096 { 097 super.service(sManager); 098 _resolver = (AmetysObjectResolver) sManager.lookup(AmetysObjectResolver.ROLE); 099 _ctypeEP = (ContentTypeExtensionPoint) sManager.lookup(ContentTypeExtensionPoint.ROLE); 100 _odfHelper = (ODFHelper) sManager.lookup(ODFHelper.ROLE); 101 _odfTableRefHelper = (OdfReferenceTableHelper) sManager.lookup(OdfReferenceTableHelper.ROLE); 102 _catalogManager = (CatalogsManager) sManager.lookup(CatalogsManager.ROLE); 103 _queryHelper = (org.ametys.plugins.queriesdirectory.QueryHelper) sManager.lookup(org.ametys.plugins.queriesdirectory.QueryHelper.ROLE); 104 } 105 106 public void generate() throws IOException, SAXException, ProcessingException 107 { 108 contentHandler.startDocument(); 109 XMLUtils.startElement(contentHandler, "programs"); 110 111 Catalog catalog = null; 112 if ("_default".equals(source)) 113 { 114 catalog = _catalogManager.getDefaultCatalog(); 115 } 116 else 117 { 118 catalog = _catalogManager.getCatalog(source); 119 } 120 121 if (catalog == null) 122 { 123 throw new IllegalArgumentException ("Failed to generated PDF of unknown catalog '" + source + "'"); 124 } 125 126 String lang; 127 try 128 { 129 lang = parameters.getParameter("lang"); 130 } 131 catch (ParameterException e) 132 { 133 throw new IllegalArgumentException ("Missing lang parameter", e); 134 } 135 136 // Catalog 137 AttributesImpl attrs = new AttributesImpl(); 138 attrs.addCDATAAttribute("name", source); 139 XMLUtils.createElement(contentHandler, "catalog", attrs, catalog.getTitle()); 140 141 Map parentContext = (Map) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); 142 143 boolean queryMode = CatalogPDFExportSchedulable.MODE_QUERY.equals(parentContext.get(CatalogPDFExportSchedulable.JOBDATAMAP_MODE_KEY)); 144 145 // Orgunits 146 List<OrgUnit> orgUnits = new ArrayList<>(); 147 if (parentContext.containsKey(CatalogPDFExportSchedulable.JOBDATAMAP_ORGUNIT_KEY)) 148 { 149 Object[] ouIds = (Object[]) parentContext.get(CatalogPDFExportSchedulable.JOBDATAMAP_ORGUNIT_KEY); 150 151 orgUnits = Arrays.stream(ouIds) 152 .map(String.class::cast) 153 .filter(StringUtils::isNotEmpty) 154 .map(LambdaUtils.wrap(_resolver::<OrgUnit>resolveById)) 155 .collect(Collectors.toList()); 156 } 157 158 for (OrgUnit orgUnit : orgUnits) 159 { 160 attrs.clear(); 161 attrs.addCDATAAttribute("id", orgUnit.getId()); 162 attrs.addCDATAAttribute("uaiCode", orgUnit.getUAICode()); 163 XMLUtils.createElement(contentHandler, "orgunit", attrs, orgUnit.getTitle()); 164 } 165 166 // Degrees 167 List<OdfReferenceTableEntry> degrees = new ArrayList<>(); 168 if (parentContext.containsKey(CatalogPDFExportSchedulable.JOBDATAMAP_DEGREE_KEY)) 169 { 170 Object[] degreeIds = (Object[]) parentContext.get(CatalogPDFExportSchedulable.JOBDATAMAP_DEGREE_KEY); 171 172 degrees = Arrays.stream(degreeIds) 173 .map(String.class::cast) 174 .filter(StringUtils::isNotEmpty) 175 .map(_odfTableRefHelper::getItem) 176 .collect(Collectors.toList()); 177 } 178 179 for (OdfReferenceTableEntry degree : degrees) 180 { 181 attrs.clear(); 182 attrs.addCDATAAttribute("id", degree.getId()); 183 attrs.addCDATAAttribute("code", degree.getCode()); 184 attrs.addCDATAAttribute("order", String.valueOf(degree.getOrder())); 185 XMLUtils.createElement(contentHandler, "degree", attrs, degree.getLabel(lang)); 186 } 187 188 String queryId = (String) parentContext.get(CatalogPDFExportSchedulable.JOBDATAMAP_QUERY_KEY); 189 boolean includeSubPrograms = (Boolean) parentContext.get(CatalogPDFExportSchedulable.JOBDATAMAP_INCLUDE_SUBPROGRAMS); 190 191 Map<String, ContentAttributeDefinition> tableRefAttributeDefs = _odfTableRefHelper.getTableRefAttributeDefinitions(ProgramFactory.PROGRAM_CONTENT_TYPE); 192 193 AmetysObjectIterable<Program> programs = queryMode ? _getPrograms(catalog.getName(), lang, queryId) : _getPrograms(catalog.getName(), lang, orgUnits, degrees); 194 _saxPrograms(programs, includeSubPrograms); 195 196 // SAX entries of table references 197 XMLUtils.startElement(contentHandler, "enumerated-metadata"); 198 for (ContentAttributeDefinition attributeDef : tableRefAttributeDefs.values()) 199 { 200 _odfTableRefHelper.saxItems(contentHandler, attributeDef, lang); 201 } 202 XMLUtils.endElement(contentHandler, "enumerated-metadata"); 203 204 XMLUtils.endElement(contentHandler, "programs"); 205 contentHandler.endDocument(); 206 } 207 208 /** 209 * Get programs from catalog, lang, orgunit and degree. Orgunit and degree can be null. 210 * @param catalog The catalog 211 * @param lang The content language 212 * @param orgUnits The restricted orgunits. Can be empty to not filter by orgunits 213 * @param degrees The restricted degrees. Can be empty to not filter by degrees 214 * @return An iterable of programs corresponding to the query with catalog, lang, orgunit and degree. 215 */ 216 protected AmetysObjectIterable<Program> _getPrograms(String catalog, String lang, List<OrgUnit> orgUnits, List<OdfReferenceTableEntry> degrees) 217 { 218 List<Expression> exprs = new ArrayList<>(); 219 exprs.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE)); 220 exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 221 exprs.add(new LanguageExpression(Operator.EQ, lang)); 222 223 if (!degrees.isEmpty()) 224 { 225 Expression[] degreeExprs = degrees.stream() 226 .map(d -> new StringExpression(AbstractProgram.DEGREE, Operator.EQ, d.getId())) 227 .toArray(Expression[]::new); 228 229 exprs.add(new OrExpression(degreeExprs)); 230 } 231 232 if (!orgUnits.isEmpty()) 233 { 234 Expression[] ouExprs = orgUnits.stream() 235 .map(_odfHelper::getSubOrgUnitIds) 236 .flatMap(List::stream) 237 .distinct() 238 .map(orgunitId -> new StringExpression(AbstractProgram.ORG_UNITS_REFERENCES, Operator.EQ, orgunitId)) 239 .toArray(Expression[]::new); 240 241 exprs.add(new OrExpression(ouExprs)); 242 } 243 244 Expression programsExpression = new AndExpression(exprs.toArray(Expression[]::new)); 245 246 SortCriteria sortCriteria = new SortCriteria(); 247 sortCriteria.addCriterion(Content.ATTRIBUTE_TITLE, true, true); 248 249 String programsQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, programsExpression, sortCriteria); 250 AmetysObjectIterable<Program> programs = _resolver.query(programsQuery); 251 252 return programs; 253 } 254 255 /** 256 * Get programs from catalog, lang and a query 257 * @param catalog The catalog 258 * @param lang The content language 259 * @param queryId The query id 260 * @return An iterable of programs corresponding to the query. Results are filtered 261 * @throws ProcessingException if failed to execute query 262 */ 263 protected AmetysObjectIterable<Program> _getPrograms(String catalog, String lang, String queryId) throws ProcessingException 264 { 265 try 266 { 267 AmetysObjectIterable<Content> contents = _queryHelper.executeQuery(queryId); 268 269 List<Program> programs = contents.stream() 270 .filter(Program.class::isInstance) 271 .map(Program.class::cast) 272 .filter(p -> catalog.equals(p.getCatalog())) 273 .filter(p -> lang.equals(p.getLanguage())) 274 .toList(); 275 276 return new CollectionIterable<>(programs); 277 } 278 catch (Exception e) 279 { 280 throw new ProcessingException("Failed to execute query '" + queryId + "' to generate PDF catalog", e); 281 } 282 } 283 284 /** 285 * Sax programs 286 * @param programs the programs to sax 287 * @param includeSubPrograms true to include subprograms 288 * @throws MalformedURLException if an error occurred 289 * @throws IOException if an error occurred 290 * @throws SAXException if an error occurred 291 */ 292 protected void _saxPrograms(AmetysObjectIterable<Program> programs, boolean includeSubPrograms) throws MalformedURLException, IOException, SAXException 293 { 294 Map<String, ContentAttributeDefinition> tableRefAttributeDefs = _odfTableRefHelper.getTableRefAttributeDefinitions(ProgramFactory.PROGRAM_CONTENT_TYPE); 295 296 for (AbstractProgram program : programs) 297 { 298 _saxAbstractProgram("program", program, tableRefAttributeDefs, includeSubPrograms); 299 } 300 } 301 302 /** 303 * SAX a program or subprogram 304 * @param tagName the XML root tag 305 * @param program The abstract program to sax 306 * @param tableRefAttributeDefs The table reference attribute definitions 307 * @param includeSubprograms true to include subprograms 308 * @throws MalformedURLException if an error occurred 309 * @throws IOException if an error occurred 310 * @throws SAXException if an error occurred 311 */ 312 protected void _saxAbstractProgram(String tagName, AbstractProgram program, Map<String, ContentAttributeDefinition> tableRefAttributeDefs, boolean includeSubprograms) throws MalformedURLException, IOException, SAXException 313 { 314 try 315 { 316 _odfHelper.switchToLiveVersionIfNeeded(program); 317 SitemapSource src = null; 318 319 try 320 { 321 AttributesImpl attrs = new AttributesImpl(); 322 attrs.addCDATAAttribute("id", program.getId()); 323 attrs.addCDATAAttribute("name", program.getName()); 324 attrs.addCDATAAttribute("title", program.getTitle()); 325 326 XMLUtils.startElement(contentHandler, tagName, attrs); 327 328 _saxTableRefAttributeValues(program, tableRefAttributeDefs); 329 330 XMLUtils.startElement(contentHandler, "fo"); 331 332 String uri = "cocoon://_plugins/odf/_content/" + program.getName() + ".fo"; 333 src = (SitemapSource) resolver.resolveURI(uri, null, Map.of("exportMode", __EXPORT_MODE)); 334 src.toSAX(new IgnoreRootHandler(contentHandler)); 335 336 XMLUtils.endElement(contentHandler, "fo"); 337 338 if (includeSubprograms) 339 { 340 Map<String, ContentAttributeDefinition> subProgramTableRefAttributeDefs = _odfTableRefHelper.getTableRefAttributeDefinitions(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE); 341 List<ProgramPart> programPartChildren = program.getProgramPartChildren(); 342 for (ProgramPart programPart : programPartChildren) 343 { 344 if (programPart instanceof SubProgram) 345 { 346 _saxAbstractProgram("subprogram", (SubProgram) programPart, subProgramTableRefAttributeDefs, includeSubprograms); 347 } 348 } 349 } 350 XMLUtils.endElement(contentHandler, tagName); 351 352 } 353 catch (UnknownAmetysObjectException e) 354 { 355 // The content may be archived 356 } 357 finally 358 { 359 resolver.release(src); 360 } 361 362 } 363 catch (NoLiveVersionException e) 364 { 365 getLogger().info("No live version found for program item " + program.getTitle() + " (" + program.getCode() + "). The program item will not appear in the PDF export.", e); 366 } 367 } 368 369 /** 370 * SAX enumerated values of an attribute 371 * @param program The program 372 * @param tableRefAttributeDefs The table reference attribute definitions 373 * @throws AmetysRepositoryException if an error occurred 374 * @throws SAXException if an error occurred 375 * @throws IOException if an error occurred 376 */ 377 protected void _saxTableRefAttributeValues(AbstractProgram program, Map<String, ContentAttributeDefinition> tableRefAttributeDefs) throws AmetysRepositoryException, SAXException, IOException 378 { 379 // Build a view containing all the reference tables attributes 380 View view = View.of(program.getModel(), tableRefAttributeDefs.keySet().toArray(new String[tableRefAttributeDefs.size()])); 381 382 // Generate SAX events for the built view 383 XMLUtils.startElement(contentHandler, "metadata"); 384 program.dataToSAX(contentHandler, view); 385 XMLUtils.endElement(contentHandler, "metadata"); 386 } 387}