001/* 002 * Copyright 2012 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.plugins.calendar.events; 017 018import java.io.IOException; 019import java.time.LocalDate; 020import java.time.ZoneId; 021import java.time.ZonedDateTime; 022import java.time.format.DateTimeFormatter; 023import java.time.temporal.ChronoField; 024import java.util.ArrayList; 025import java.util.Calendar; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.Date; 029import java.util.HashMap; 030import java.util.Iterator; 031import java.util.LinkedHashSet; 032import java.util.List; 033import java.util.Map; 034import java.util.Map.Entry; 035import java.util.Set; 036import java.util.UUID; 037import java.util.stream.Collectors; 038 039import org.apache.avalon.framework.service.ServiceException; 040import org.apache.avalon.framework.service.ServiceManager; 041import org.apache.cocoon.ProcessingException; 042import org.apache.cocoon.environment.ObjectModelHelper; 043import org.apache.cocoon.environment.Request; 044import org.apache.cocoon.xml.AttributesImpl; 045import org.apache.cocoon.xml.XMLUtils; 046import org.apache.commons.lang.StringUtils; 047import org.apache.commons.lang.time.DateUtils; 048import org.apache.commons.lang3.tuple.ImmutablePair; 049import org.apache.commons.lang3.tuple.Pair; 050import org.xml.sax.ContentHandler; 051import org.xml.sax.SAXException; 052 053import org.ametys.cms.repository.Content; 054import org.ametys.cms.tag.Tag; 055import org.ametys.cms.tag.TagProviderExtensionPoint; 056import org.ametys.core.util.URIUtils; 057import org.ametys.plugins.calendar.icsreader.IcsReader; 058import org.ametys.plugins.calendar.icsreader.IcsReader.IcsEvents; 059import org.ametys.plugins.calendar.icsreader.LocalVEvent; 060import org.ametys.plugins.repository.AmetysObjectIterable; 061import org.ametys.plugins.repository.AmetysObjectResolver; 062import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 063import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeater; 064import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeaterEntry; 065import org.ametys.runtime.i18n.I18nizableText; 066import org.ametys.web.WebConstants; 067import org.ametys.web.content.GetSiteAction; 068import org.ametys.web.filter.WebContentFilter; 069import org.ametys.web.filter.WebContentFilter.AccessLimitation; 070import org.ametys.web.repository.page.Page; 071import org.ametys.web.repository.page.ZoneItem; 072 073import net.fortuna.ical4j.model.Property; 074import net.fortuna.ical4j.model.component.VEvent; 075 076/** 077 * Query and generate news according to many parameters. 078 */ 079public class EventsGenerator extends AbstractEventGenerator 080{ 081 /** The ametys object resolver. */ 082 protected AmetysObjectResolver _ametysResolver; 083 084 /** The events helper */ 085 protected EventsFilterHelper _eventsFilterHelper; 086 087 /** The ICS Reader */ 088 protected IcsReader _icsReader; 089 090 /** The tag provider extension point. */ 091 protected TagProviderExtensionPoint _tagProviderEP; 092 093 @Override 094 public void service(ServiceManager serviceManager) throws ServiceException 095 { 096 super.service(serviceManager); 097 _ametysResolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 098 _eventsFilterHelper = (EventsFilterHelper) serviceManager.lookup(EventsFilterHelper.ROLE); 099 _icsReader = (IcsReader) serviceManager.lookup(IcsReader.ROLE); 100 _tagProviderEP = (TagProviderExtensionPoint) manager.lookup(TagProviderExtensionPoint.ROLE); 101 } 102 103 @Override 104 public void generate() throws IOException, SAXException, ProcessingException 105 { 106 Request request = ObjectModelHelper.getRequest(objectModel); 107 @SuppressWarnings("unchecked") 108 Map<String, Object> parentContextAttrs = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); 109 if (parentContextAttrs == null) 110 { 111 parentContextAttrs = Collections.EMPTY_MAP; 112 } 113 114 LocalDate today = LocalDate.now(); 115 116 // Get site and language in sitemap parameters. Can not be null. 117 String siteName = parameters.getParameter("site", (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITE_NAME)); 118 String lang = parameters.getParameter("lang", (String) request.getAttribute("renderingLanguage")); 119 if (StringUtils.isEmpty(lang)) 120 { 121 lang = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME); 122 } 123 // Get the parameters. 124 int monthsBefore = parameters.getParameterAsInteger("months-before", 3); 125 int monthsAfter = parameters.getParameterAsInteger("months-after", 3); 126 // Type can be "calendar", "single-day" or "agenda". 127 String type = parameters.getParameter("type", "calendar"); 128 String view = parameters.getParameter("view", ""); 129 int year = parameters.getParameterAsInteger("year", today.getYear()); 130 int month = parameters.getParameterAsInteger("month", today.getMonthValue()); 131 int day = parameters.getParameterAsInteger("day", today.getDayOfMonth()); 132 // Select a single tag or "all". 133 String requestedTagsString = parameters.getParameter("tags", "all"); 134 135 136 Page page = (Page) request.getAttribute(WebConstants.REQUEST_ATTR_PAGE); 137 // Get the zone item, as a request attribute or from the ID in the 138 // parameters. 139 ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM); 140 String zoneItemId = parameters.getParameter("zoneItemId", ""); 141 if (zoneItem == null && StringUtils.isNotEmpty(zoneItemId)) 142 { 143 zoneItemId = URIUtils.decode(zoneItemId); 144 zoneItem = (ZoneItem) _ametysResolver.resolveById(zoneItemId); 145 } 146 147 if (page == null && zoneItem != null) 148 { 149 // Wrapped page such as _plugins/calendar/page/YEAR/MONTH/DAY/ZONEITEMID/events_1.3.html => get the page from its zone item 150 // The page is needed to get restriction 151 page = zoneItem.getZone().getPage(); 152 } 153 154 ZonedDateTime dateTime = ZonedDateTime.of(year, month, day, 0, 0, 0, 0, ZoneId.systemDefault()); 155 String title = _eventsFilterHelper.getTitle(zoneItem); 156 String rangeType = parameters.getParameter("rangeType", _eventsFilterHelper.getDefaultRangeType(zoneItem)); 157 boolean maskOrphan = _eventsFilterHelper.getMaskOrphan(zoneItem); 158 boolean pdfDownload = _eventsFilterHelper.getPdfDownload(zoneItem); 159 boolean icalDownload = _eventsFilterHelper.getIcalDownload(zoneItem); 160 String link = _eventsFilterHelper.getLink(zoneItem); 161 String linkTitle = _eventsFilterHelper.getLinkTitle(zoneItem); 162 163 boolean doRetrieveView = !StringUtils.equalsIgnoreCase("false", parameters.getParameter("do-retrieve-view", "true")); 164 165 // Get the search context to match, from the zone item or from the parameters. 166 @SuppressWarnings("unchecked") 167 List<Map<String, Object>> searchContexts = _eventsFilterHelper.getSearchContext(zoneItem, (List<Map<String, Object>>) parentContextAttrs.get("search")); 168 169 170 Set<String> tags = _eventsFilterHelper.getTags(zoneItem, searchContexts); 171 Set<Tag> categories = _eventsFilterHelper.getTagCategories(zoneItem, searchContexts, siteName); 172 Set<Tag> icsTags = _getIcsTags(zoneItem, siteName); 173 String pagePath = page != null ? page.getPathInSitemap() : ""; 174 175 Set<String> filteredCategories = _eventsFilterHelper.getFilteredCategories(null, requestedTagsString.split(","), zoneItem, siteName); 176 // Get the date range and deduce the expression (single day or month-before to month-after). 177 EventsFilterHelper.DateRange dateRange = _eventsFilterHelper.getDateRange(type, year, month, day, monthsBefore, monthsAfter, rangeType); 178 179 EventsFilter eventsFilter = _eventsFilterHelper.generateEventFilter(dateRange, zoneItem, view, type, filteredCategories, searchContexts); 180 181 // Get the corresponding contents. 182 AmetysObjectIterable<Content> eventContents = eventsFilter.getMatchingContents(siteName, lang, page); 183 184 // Read ICS threads 185 List<IcsEvents> parsedICS = _getICSEvents(zoneItem, siteName, dateRange); 186 187 // CAL-94 (same as CMS-2292 for filtered contents) 188 String currentSiteName = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITE_NAME); 189 String currentSkinName = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SKIN_ID); 190 String currentTemplateName = (String) request.getAttribute(WebConstants.REQUEST_ATTR_TEMPLATE_ID); 191 String currentLanguage = (String) request.getAttribute("renderingLanguage"); 192 request.setAttribute(GetSiteAction.OVERRIDE_SITE_REQUEST_ATTR, currentSiteName); 193 request.setAttribute(GetSiteAction.OVERRIDE_SKIN_REQUEST_ATTR, currentSkinName); 194 195 try 196 { 197 _sax(today, monthsBefore, monthsAfter, year, month, day, filteredCategories, page, zoneItem, dateTime, title, rangeType, maskOrphan, pdfDownload, icalDownload, link, linkTitle, doRetrieveView, tags, categories, icsTags, pagePath, eventsFilter, dateRange, eventContents, parsedICS); 198 } 199 finally 200 { 201 request.removeAttribute(GetSiteAction.OVERRIDE_SITE_REQUEST_ATTR); 202 request.removeAttribute(GetSiteAction.OVERRIDE_SKIN_REQUEST_ATTR); 203 request.setAttribute(WebConstants.REQUEST_ATTR_SITE_NAME, currentSiteName); 204 request.setAttribute("siteName", currentSiteName); 205 request.setAttribute(WebConstants.REQUEST_ATTR_SKIN_ID, currentSkinName); 206 request.setAttribute(WebConstants.REQUEST_ATTR_TEMPLATE_ID, currentTemplateName); 207 request.setAttribute("renderingLanguage", currentLanguage); 208 } 209 } 210 211 private Set<Tag> _getIcsTags(ZoneItem zoneItem, String currentSiteName) 212 { 213 ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters(); 214 Set<Tag> categories = new LinkedHashSet<>(); 215 216 // Add the categories defined in the ICS fields 217 if (serviceParameters.hasValue("ics")) 218 { 219 ModifiableModelAwareRepeater icsRepeater = zoneItem.getServiceParameters().getValue("ics"); 220 for (ModifiableModelAwareRepeaterEntry repeaterEntry : icsRepeater.getEntries()) 221 { 222 String categoryName = repeaterEntry.getValue("tag"); 223 Map<String, Object> contextualParameters = new HashMap<>(); 224 contextualParameters.put("siteName", currentSiteName); 225 Tag category = _tagProviderEP.getTag(categoryName, contextualParameters); 226 if (category != null) 227 { 228 categories.add(category); 229 } 230 } 231 } 232 return categories; 233 } 234 235 /** 236 * Read the configured distant ics sources into a list of {@link IcsEvents} 237 * @param zoneItem zoneItem where the configuration will be fetched 238 * @param siteName name of the current site 239 * @param dateRange range of dates to limit 240 * @return a list of {@link IcsEvents} 241 */ 242 private List<IcsEvents> _getICSEvents(ZoneItem zoneItem, String siteName, EventsFilterHelper.DateRange dateRange) 243 { 244 List<IcsEvents> icsEvents = new ArrayList<>(); 245 if (zoneItem != null && zoneItem.getServiceParameters().hasValue("ics")) 246 { 247 Long nbIcsEvent = zoneItem.getServiceParameters().getValue("nbEvents"); 248 Long maxIcsSize = zoneItem.getServiceParameters().getValue("maxSize"); 249 250 ModifiableModelAwareRepeater icsRepeater = zoneItem.getServiceParameters().getValue("ics"); 251 for (ModifiableModelAwareRepeaterEntry repeaterEntry : icsRepeater.getEntries()) 252 { 253 String url = repeaterEntry.getValue("url"); 254 IcsEvents eventList = _icsReader.getEventList(url, dateRange, nbIcsEvent, maxIcsSize); 255 256 String tagName = repeaterEntry.getValue("tag"); 257 Map<String, Object> tagParameters = new HashMap<>(); 258 tagParameters.put("siteName", siteName); 259 Tag tag = _tagProviderEP.getTag(tagName, tagParameters); 260 eventList.setTag(tag); 261 262 icsEvents.add(eventList); 263 } 264 } 265 return icsEvents; 266 } 267 268 /** 269 * Get a list of {@link LocalVEvent} form the list of {@link IcsEvents} 270 * @param icsEventsList the list of {@link IcsEvents} 271 * @param dateRange range of dates to limit 272 * @return a list of {@link LocalVEvent} 273 */ 274 private Pair<List<LocalVEvent>, String> _getLocalICSEvents(List<IcsEvents> icsEventsList, EventsFilterHelper.DateRange dateRange) 275 { 276 List<LocalVEvent> localICSEvents = new ArrayList<>(); 277 String fullICSDistantEvents = ""; 278 for (IcsEvents icsEvents : icsEventsList) 279 { 280 if (icsEvents.hasEvents()) 281 { 282 Tag tag = icsEvents.getTag(); 283 for (VEvent calendarComponent : icsEvents.getEvents()) 284 { 285 List<LocalVEvent> localCalendarComponents = _icsReader.getEventDates(calendarComponent, dateRange, tag); 286 localICSEvents.addAll(localCalendarComponents); 287 if (dateRange == null) 288 { 289 // To avoid to have the ICS events in double in some exotic non-anticipated cases, when there is a dateRange, we do not copy the ICS 290 fullICSDistantEvents += calendarComponent.toString() + "\n"; 291 } 292 } 293 } 294 } 295 296 return new ImmutablePair<>(localICSEvents, fullICSDistantEvents); 297 } 298 299 private void _sax(LocalDate today, int monthsBefore, int monthsAfter, int year, int month, int day, Set<String> filteredCategoryTags, Page page, ZoneItem zoneItem, ZonedDateTime dateTime, 300 String title, String rangeType, boolean maskOrphan, boolean pdfDownload, boolean icalDownload, String link, String linkTitle, boolean doRetrieveView, Set<String> tags, 301 Set<Tag> categories, Set<Tag> icsTags, String pagePath, EventsFilter eventsFilter, EventsFilterHelper.DateRange dateRange, AmetysObjectIterable<Content> eventContents, List<IcsEvents> icsEvents) 302 throws SAXException, IOException 303 { 304 AttributesImpl atts = new AttributesImpl(); 305 306 atts.addCDATAAttribute("page-path", pagePath); 307 atts.addCDATAAttribute("today", DateTimeFormatter.ISO_LOCAL_DATE.format(today)); 308 if (dateRange != null) 309 { 310 if (dateRange.getStartDate() != null) 311 { 312 atts.addCDATAAttribute("start", DateTimeFormatter.ISO_LOCAL_DATE.format(dateRange.getStartDate().toInstant().atZone(ZoneId.systemDefault()))); 313 } 314 if (dateRange.getEndDate() != null) 315 { 316 atts.addCDATAAttribute("end", DateTimeFormatter.ISO_LOCAL_DATE.format(dateRange.getEndDate().toInstant().atZone(ZoneId.systemDefault()))); 317 } 318 } 319 320 atts.addCDATAAttribute("year", Integer.toString(year)); 321 atts.addCDATAAttribute("month", String.format("%02d", month)); 322 atts.addCDATAAttribute("day", String.format("%02d", day)); 323 atts.addCDATAAttribute("months-before", Integer.toString(monthsBefore)); 324 atts.addCDATAAttribute("months-after", Integer.toString(monthsAfter)); 325 326 atts.addCDATAAttribute("title", title); 327 atts.addCDATAAttribute("mask-orphan", Boolean.toString(maskOrphan)); 328 atts.addCDATAAttribute("pdf-download", Boolean.toString(pdfDownload)); 329 atts.addCDATAAttribute("ical-download", Boolean.toString(icalDownload)); 330 atts.addCDATAAttribute("link", link); 331 atts.addCDATAAttribute("link-title", linkTitle); 332 333 if (zoneItem != null) 334 { 335 atts.addCDATAAttribute("zoneItemId", zoneItem.getId()); 336 } 337 if (StringUtils.isNotEmpty(rangeType)) 338 { 339 atts.addCDATAAttribute("range", rangeType); 340 } 341 342 if (!filteredCategoryTags.isEmpty()) 343 { 344 atts.addCDATAAttribute("requested-tags", String.join(",", filteredCategoryTags)); 345 } 346 347 contentHandler.startDocument(); 348 XMLUtils.startElement(contentHandler, "events", atts); 349 350 _saxRssUrl(zoneItem); 351 352 // Generate months (used in calendar mode) and days (used in full-page 353 // agenda mode). 354 _saxMonths(dateRange); 355 _saxDays(dateTime, rangeType); 356 357 _saxDaysNew(dateRange, rangeType); 358 359 // Generate tags and categories. 360 _saxTags(tags); 361 _saxCategories(categories, icsTags); 362 363 Pair<List<LocalVEvent>, String> parsedICSEvents = _getLocalICSEvents(icsEvents, dateRange); 364 List<LocalVEvent> localIcsEvents = parsedICSEvents.getLeft(); 365 String fullICSDistantEvents = parsedICSEvents.getRight(); 366 367 // Generate the matching contents. 368 XMLUtils.startElement(contentHandler, "contents"); 369 370 saxMatchingContents(contentHandler, eventsFilter, eventContents, page, doRetrieveView); 371 372 saxIcsEvents(contentHandler, localIcsEvents); 373 374 XMLUtils.endElement(contentHandler, "contents"); 375 376 XMLUtils.createElement(contentHandler, "rawICS", fullICSDistantEvents); 377 378 // Generate ICS events with errors 379 _saxErrorsICS(icsEvents); 380 381 XMLUtils.endElement(contentHandler, "events"); 382 contentHandler.endDocument(); 383 } 384 385 private void _saxErrorsICS(List<IcsEvents> icsEvents) throws SAXException 386 { 387 List<IcsEvents> errorsIcs = icsEvents.stream() 388 .filter(ics -> ics.getStatus() != IcsEvents.Status.OK) 389 .collect(Collectors.toList()); 390 391 if (!errorsIcs.isEmpty()) 392 { 393 XMLUtils.startElement(contentHandler, "errors-ics"); 394 for (IcsEvents ics : errorsIcs) 395 { 396 AttributesImpl attrs = new AttributesImpl(); 397 attrs.addCDATAAttribute("url", ics.getUrl()); 398 attrs.addCDATAAttribute("status", ics.getStatus().name()); 399 XMLUtils.createElement(contentHandler, "ics", attrs); 400 } 401 XMLUtils.endElement(contentHandler, "errors-ics"); 402 } 403 } 404 405 private void _saxRssUrl(ZoneItem zoneItem) throws SAXException 406 { 407 if (zoneItem != null) 408 { 409 ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters(); 410 // First check that there is a value because calendar service doesn't define the rss parameter 411 if (serviceParameters.hasValue("rss") && (boolean) serviceParameters.getValue("rss")) 412 { 413 // Only add RSS if there is a search context 414 if (serviceParameters.hasValue("search")) 415 { 416 ModifiableModelAwareRepeater searchRepeater = serviceParameters.getValue("search"); 417 if (searchRepeater.getSize() > 0) 418 { 419 // Split protocol and id 420 String[] zoneItemId = zoneItem.getId().split("://"); 421 String url = "_plugins/calendar/" + zoneItemId[1] + "/rss.xml"; 422 423 XMLUtils.createElement(contentHandler, "rssUrl", url); 424 } 425 } 426 } 427 } 428 } 429 430 /** 431 * SAX all contents matching the given filter 432 * 433 * @param handler The content handler to SAX into 434 * @param filter The filter 435 * @param contents iterator on the contents. 436 * @param currentPage The current page. 437 * @param saxContentItSelf true to sax the content, false will only sax some meta 438 * @throws SAXException If an error occurs while SAXing 439 * @throws IOException If an error occurs while retrieving content. 440 */ 441 public void saxMatchingContents(ContentHandler handler, WebContentFilter filter, AmetysObjectIterable<Content> contents, Page currentPage, boolean saxContentItSelf) throws SAXException, IOException 442 { 443 boolean checkUserAccess = filter.getAccessLimitation() == AccessLimitation.USER_ACCESS; 444 445 for (Content content : contents) 446 { 447 if (_filterHelper.isContentValid(content, currentPage, filter)) 448 { 449 saxContent(handler, content, saxContentItSelf, filter, checkUserAccess); 450 } 451 } 452 } 453 454 /** 455 * Sax a list of events coming from a distant ICS file 456 * @param handler The content handler to SAX into 457 * @param icsEvents the events to sax 458 * @throws SAXException Something went wrong 459 */ 460 public void saxIcsEvents(ContentHandler handler, List<LocalVEvent> icsEvents) throws SAXException 461 { 462 for (LocalVEvent icsEvent : icsEvents) 463 { 464 saxIcsEvent(handler, icsEvent); 465 } 466 } 467 468 /** 469 * Sax an event coming from a distant ICS file 470 * @param handler The content handler to SAX into 471 * @param icsEvent an event to sax 472 * @throws SAXException Something went wrong 473 */ 474 public void saxIcsEvent(ContentHandler handler, LocalVEvent icsEvent) throws SAXException 475 { 476 VEvent event = icsEvent.getEvent(); 477 AttributesImpl attrs = new AttributesImpl(); 478 479 String start = org.ametys.core.util.DateUtils.dateToString(icsEvent.getStart()); 480 String end = org.ametys.core.util.DateUtils.dateToString(icsEvent.getEnd()); 481 List<String> params = new ArrayList<>(); 482 483 String title = event.getProperty(Property.SUMMARY) != null ? event.getProperty(Property.SUMMARY).getValue() : ""; 484 String id = event.getProperty(Property.UID) != null ? event.getProperty(Property.UID).getValue() : UUID.randomUUID().toString(); 485 String eventAbstract = event.getProperty(Property.DESCRIPTION) != null ? event.getProperty(Property.DESCRIPTION).getValue() : ""; 486 487 params.add(title); 488 489 if (start != null) 490 { 491 String startAttr = org.ametys.core.util.DateUtils.asZonedDateTime(icsEvent.getStart(), null).format(DateTimeFormatter.ISO_LOCAL_DATE); 492 params.add(start); 493 attrs.addCDATAAttribute("start", startAttr); 494 } 495 496 if (end != null) 497 { 498 String endAttr = org.ametys.core.util.DateUtils.asZonedDateTime(icsEvent.getEnd(), null).format(DateTimeFormatter.ISO_LOCAL_DATE); 499 params.add(end); 500 attrs.addCDATAAttribute("end", endAttr); 501 } 502 503 XMLUtils.startElement(handler, "event", attrs); 504 505 String key = end == null ? "CALENDAR_SERVICE_AGENDA_EVENT_TITLE_SINGLE_DAY" : "CALENDAR_SERVICE_AGENDA_FROM_TO"; 506 I18nizableText description = new I18nizableText(null, key, params); 507 description.toSAX(handler, "description"); 508 509 attrs = new AttributesImpl(); 510 attrs.addCDATAAttribute("id", "ics://" + id); 511 attrs.addCDATAAttribute("title", title); 512 513 Date dtStamp = event.getDateStamp() != null ? event.getDateStamp().getDate() : new Date(); 514 Date createdAtDate = event.getCreated() != null ? event.getCreated().getDate() : dtStamp; 515 Date lastModifiedDate = event.getLastModified() != null ? event.getLastModified().getDate() : dtStamp; 516 517 String createdAt = org.ametys.core.util.DateUtils.asZonedDateTime(createdAtDate, null).format(DateTimeFormatter.ISO_INSTANT); 518 attrs.addCDATAAttribute("createdAt", createdAt); 519 520 String lastModified = org.ametys.core.util.DateUtils.asZonedDateTime(lastModifiedDate, null).format(DateTimeFormatter.ISO_INSTANT); 521 attrs.addCDATAAttribute("lastModifiedAt", lastModified); 522 523 XMLUtils.startElement(handler, "content", attrs); 524 525 XMLUtils.startElement(handler, "metadata"); 526 attrs = new AttributesImpl(); 527 attrs.addCDATAAttribute("typeId", "string"); 528 attrs.addCDATAAttribute("multiple", "false"); 529 XMLUtils.createElement(handler, "title", attrs, title); 530 XMLUtils.createElement(handler, "abstract", attrs, eventAbstract); 531 532 533 attrs = new AttributesImpl(); 534 attrs.addCDATAAttribute("typeId", "datetime"); 535 attrs.addCDATAAttribute("multiple", "false"); 536 XMLUtils.createElement(handler, "start-date", attrs, start); 537 XMLUtils.createElement(handler, "end-date", attrs, end); 538 539 XMLUtils.endElement(handler, "metadata"); 540 541 Tag tag = icsEvent.getTag(); 542 if (tag != null) 543 { 544 XMLUtils.startElement(handler, "tags"); 545 attrs = new AttributesImpl(); 546 attrs.addCDATAAttribute("parent", tag.getParentName()); 547 XMLUtils.startElement(handler, tag.getName(), attrs); 548 tag.getTitle().toSAX(handler); 549 XMLUtils.endElement(handler, tag.getName()); 550 XMLUtils.endElement(handler, "tags"); 551 } 552 553 XMLUtils.endElement(handler, "content"); 554 XMLUtils.endElement(handler, "event"); 555 } 556 557 /** 558 * SAX information on the months spanning the date range. 559 * @param dateRange the date range. 560 * @throws SAXException if a error occurs while saxing 561 */ 562 protected void _saxMonths(EventsFilterHelper.DateRange dateRange) throws SAXException 563 { 564 if (dateRange != null && dateRange.getStartDate() != null && dateRange.getEndDate() != null) 565 { 566 AttributesImpl atts = new AttributesImpl(); 567 568 XMLUtils.startElement(contentHandler, "months"); 569 570 ZonedDateTime date = dateRange.getStartDate().toInstant().atZone(ZoneId.systemDefault()); 571 ZonedDateTime end = dateRange.getEndDate().toInstant().atZone(ZoneId.systemDefault()); 572 573 while (date.isBefore(end)) 574 { 575 int year = date.getYear(); 576 int month = date.getMonthValue(); 577 578 String monthStr = String.format("%d-%02d", year, month); 579 String dateStr = org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(date); 580 581 atts.clear(); 582 atts.addCDATAAttribute("str", monthStr); 583 atts.addCDATAAttribute("raw", dateStr); 584 XMLUtils.startElement(contentHandler, "month", atts); 585 586 XMLUtils.endElement(contentHandler, "month"); 587 588 date = date.plusMonths(1); 589 } 590 591 XMLUtils.endElement(contentHandler, "months"); 592 } 593 } 594 595 /** 596 * Generate days to build a "calendar" view. 597 * 598 * @param dateRange a date belonging to the time span to generate. 599 * @param rangeType the range type, "month" or "week". 600 * @throws SAXException if an error occurs while saxing 601 */ 602 protected void _saxDaysNew(EventsFilterHelper.DateRange dateRange, String rangeType) throws SAXException 603 { 604 if (dateRange != null) 605 { 606 XMLUtils.startElement(contentHandler, "calendar-months"); 607 608 ZonedDateTime date = dateRange.getStartDate().toInstant().atZone(ZoneId.systemDefault()); 609 ZonedDateTime end = dateRange.getEndDate().toInstant().atZone(ZoneId.systemDefault()); 610 611 612 while (date.isBefore(end)) 613 { 614 int year = date.getYear(); 615 int month = date.getMonthValue(); 616 617 String monthStr = String.format("%d-%02d", year, month); 618 String dateStr = org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(date); 619 620 AttributesImpl attrs = new AttributesImpl(); 621 attrs.addCDATAAttribute("str", monthStr); 622 attrs.addCDATAAttribute("raw", dateStr); 623 attrs.addCDATAAttribute("year", Integer.toString(year)); 624 attrs.addCDATAAttribute("month", Integer.toString(month)); 625 XMLUtils.startElement(contentHandler, "month", attrs); 626 627 _saxDays(date, "month"); 628 629 XMLUtils.endElement(contentHandler, "month"); 630 631 date = date.plusMonths(1); 632 } 633 634 XMLUtils.endElement(contentHandler, "calendar-months"); 635 } 636 } 637 638 /** 639 * Generate days to build a "calendar" view. 640 * 641 * @param date a date belonging to the time span to generate. 642 * @param type the range type, "month" or "week". 643 * @throws SAXException if an error occurs while saxing 644 */ 645 protected void _saxDays(ZonedDateTime date, String type) throws SAXException 646 { 647 AttributesImpl attrs = new AttributesImpl(); 648 649 int rangeStyle = DateUtils.RANGE_MONTH_MONDAY; 650 ZonedDateTime previousDay = null; 651 ZonedDateTime nextDay = null; 652 653 // Week. 654 if ("week".equals(type)) 655 { 656 rangeStyle = DateUtils.RANGE_WEEK_MONDAY; 657 658 // Get the first day of the week. 659 previousDay = date.with(ChronoField.DAY_OF_WEEK, 1); 660 // First day of next week. 661 nextDay = previousDay.plusWeeks(1); 662 // First day of previous week. 663 previousDay = previousDay.minusWeeks(1); 664 } 665 else 666 { 667 rangeStyle = DateUtils.RANGE_MONTH_MONDAY; 668 669 // Get the first day of the month. 670 previousDay = date.with(ChronoField.DAY_OF_MONTH, 1); 671 // First day of previous month. 672 nextDay = previousDay.plusMonths(1); 673 // First day of next month. 674 previousDay = previousDay.minusMonths(1); 675 } 676 677 addNavAttributes(attrs, date, previousDay, nextDay); 678 679 // Get an iterator on the days to be present on the calendar. 680 681 Iterator<Calendar> days = DateUtils.iterator(Date.from(date.toInstant()), rangeStyle); 682 683 XMLUtils.startElement(contentHandler, "calendar", attrs); 684 685 ZonedDateTime previousWeekDay = date.minusWeeks(1); 686 ZonedDateTime nextWeekDay = date.plusWeeks(1); 687 688 AttributesImpl weekAttrs = new AttributesImpl(); 689 addNavAttributes(weekAttrs, date, previousWeekDay, nextWeekDay); 690 691 XMLUtils.startElement(contentHandler, "week", weekAttrs); 692 693 while (days.hasNext()) 694 { 695 Calendar dayCal = days.next(); 696 ZonedDateTime day = dayCal.toInstant().atZone(dayCal.getTimeZone().toZoneId()); 697 String rawDateStr = org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(day); 698 String dateStr = DateTimeFormatter.ISO_LOCAL_DATE.format(day); 699 String yearStr = Integer.toString(dayCal.get(Calendar.YEAR)); 700 String monthStr = Integer.toString(dayCal.get(Calendar.MONTH) + 1); 701 String dayStr = Integer.toString(dayCal.get(Calendar.DAY_OF_MONTH)); 702 703 AttributesImpl dayAttrs = new AttributesImpl(); 704 705 dayAttrs.addCDATAAttribute("raw", rawDateStr); 706 dayAttrs.addCDATAAttribute("date", dateStr); 707 dayAttrs.addCDATAAttribute("year", yearStr); 708 dayAttrs.addCDATAAttribute("month", monthStr); 709 dayAttrs.addCDATAAttribute("day", dayStr); 710 711 XMLUtils.createElement(contentHandler, "day", dayAttrs); 712 713 // Break on week on the last day of the week (but not on the last 714 // week). 715 if (dayCal.get(Calendar.DAY_OF_WEEK) == _eventsFilterHelper.getLastDayOfWeek(dayCal) && days.hasNext()) 716 { 717 previousWeekDay = day.minusDays(6); 718 nextWeekDay = day.plusDays(8); 719 weekAttrs.clear(); 720 721 addNavAttributes(weekAttrs, day, previousWeekDay, nextWeekDay); 722 723 XMLUtils.endElement(contentHandler, "week"); 724 XMLUtils.startElement(contentHandler, "week", weekAttrs); 725 } 726 } 727 728 XMLUtils.endElement(contentHandler, "week"); 729 XMLUtils.endElement(contentHandler, "calendar"); 730 } 731 732 /** 733 * Add nav attributes. 734 * 735 * @param attrs the attributes object to fill in. 736 * @param current the current date. 737 * @param previousDay the previous date. 738 * @param nextDay the next date. 739 */ 740 protected void addNavAttributes(AttributesImpl attrs, ZonedDateTime current, ZonedDateTime previousDay, ZonedDateTime nextDay) 741 { 742 attrs.addCDATAAttribute("current", org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(current)); 743 744 attrs.addCDATAAttribute("previous", org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(previousDay)); 745 attrs.addCDATAAttribute("previousYear", Integer.toString(previousDay.getYear())); 746 attrs.addCDATAAttribute("previousMonth", Integer.toString(previousDay.getMonthValue())); 747 attrs.addCDATAAttribute("previousDay", Integer.toString(previousDay.getDayOfMonth())); 748 749 attrs.addCDATAAttribute("next", org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(nextDay)); 750 attrs.addCDATAAttribute("nextYear", Integer.toString(nextDay.getYear())); 751 attrs.addCDATAAttribute("nextMonth", Integer.toString(nextDay.getMonthValue())); 752 attrs.addCDATAAttribute("nextDay", Integer.toString(nextDay.getDayOfMonth())); 753 } 754 755 /** 756 * Generate the list of selected tags. 757 * @param tags the list of tags. 758 * @throws SAXException if an error occurs while saxing 759 */ 760 protected void _saxTags(Collection<String> tags) throws SAXException 761 { 762 XMLUtils.startElement(contentHandler, "tags"); 763 for (String tag : tags) 764 { 765 AttributesImpl attrs = new AttributesImpl(); 766 attrs.addCDATAAttribute("name", tag); 767 XMLUtils.createElement(contentHandler, "tag", attrs); 768 } 769 XMLUtils.endElement(contentHandler, "tags"); 770 } 771 772 /** 773 * Generate the list of selected tags that act as categories and their descendant tags. 774 * @param categories the list of categories to generate. 775 * @param icsTags list of tags for the ICS feeds (tags, not parents) 776 * @throws SAXException if an error occurs while saxing 777 */ 778 protected void _saxCategories(Collection<Tag> categories, Collection<Tag> icsTags) throws SAXException 779 { 780 Map<Tag, Set<Tag>> icsTagsToAdd = new HashMap<>(); 781 782 // Only the tags that are not already in the ones from the search contexts 783 for (Tag tag : icsTags) 784 { 785 Tag parent = tag.getParent(); 786 if (icsTagsToAdd.containsKey(parent)) 787 { 788 icsTagsToAdd.get(parent).add(tag); 789 } 790 else if (categories == null || !categories.contains(tag.getParent())) 791 { 792 icsTagsToAdd.put(parent, new LinkedHashSet<>()); 793 icsTagsToAdd.get(parent).add(tag); 794 } 795 } 796 797 XMLUtils.startElement(contentHandler, "tag-categories"); 798 799 // Add the tags from the search contexts 800 if (categories != null) 801 { 802 for (Tag category : categories) 803 { 804 XMLUtils.startElement(contentHandler, "category"); 805 806 category.getTitle().toSAX(contentHandler, "title"); 807 808 _saxTags(_eventsFilterHelper.getAllTags(category)); 809 810 XMLUtils.endElement(contentHandler, "category"); 811 } 812 } 813 814 // Add the tags from the ICS feeds 815 for (Entry<Tag, Set<Tag>> entry : icsTagsToAdd.entrySet()) 816 { 817 Tag parent = entry.getKey(); 818 Set<Tag> tags = entry.getValue(); 819 820 XMLUtils.startElement(contentHandler, "category"); 821 822 // As in the ICS, we select directly a tag and not a category, it is possible that there are no parent. 823 // To keep the XML equivalent, the category is still created, possibly with an empty title 824 if (parent != null) 825 { 826 parent.getTitle().toSAX(contentHandler, "title"); 827 } 828 else 829 { 830 XMLUtils.createElement(contentHandler, "title"); 831 } 832 833 _saxTags(tags); 834 835 XMLUtils.endElement(contentHandler, "category"); 836 } 837 838 XMLUtils.endElement(contentHandler, "tag-categories"); 839 } 840 841 /** 842 * Sax a list of tags 843 * @param tags the list of tags to sax 844 * @throws SAXException if an error occurs while saxing 845 */ 846 protected void _saxTags(Set<Tag> tags) throws SAXException 847 { 848 XMLUtils.startElement(contentHandler, "tags"); 849 for (Tag tag : tags) 850 { 851 AttributesImpl tagAttrs = new AttributesImpl(); 852 tagAttrs.addCDATAAttribute("name", tag.getName()); 853 XMLUtils.startElement(contentHandler, "tag", tagAttrs); 854 855 tag.getTitle().toSAX(contentHandler); 856 857 XMLUtils.endElement(contentHandler, "tag"); 858 } 859 XMLUtils.endElement(contentHandler, "tags"); 860 } 861 862}