updates for zmanagement x2
All checks were successful
Build Org Backend / build (push) Successful in 23s
All checks were successful
Build Org Backend / build (push) Successful in 23s
This commit is contained in:
@@ -26,6 +26,17 @@ COMBINED_RUN_LOG=/home/zaine/logs/combined.log
|
|||||||
|
|
||||||
PLAY_RPG_SAVE_DIR=/home/zaine/master-folder/org-platform/org_backend/data/rpg-saves
|
PLAY_RPG_SAVE_DIR=/home/zaine/master-folder/org-platform/org_backend/data/rpg-saves
|
||||||
|
|
||||||
|
# Calendar sync
|
||||||
|
NEXTCLOUD_CALDAV_URL=https://nextcloud.zainezq.com/remote.php/dav/calendars/zaine/
|
||||||
|
NEXTCLOUD_CALDAV_CALENDARS=personal,zxh
|
||||||
|
NEXTCLOUD_CALDAV_USERNAME=zaine
|
||||||
|
NEXTCLOUD_CALDAV_PASSWORD=change-me-to-a-nextcloud-app-password
|
||||||
|
CALENDAR_TIMEZONE=Europe/London
|
||||||
|
CALENDAR_SYNC_DAYS_BACK=30
|
||||||
|
CALENDAR_SYNC_DAYS_FORWARD=365
|
||||||
|
# Optional: external ICS feed for work calendar
|
||||||
|
WORK_ICS_URL=
|
||||||
|
|
||||||
# Leave unset on private zone — timesheet API read/write without credentials
|
# Leave unset on private zone — timesheet API read/write without credentials
|
||||||
# ORG_BACKEND_API_KEY=
|
# ORG_BACKEND_API_KEY=
|
||||||
TIMESHEET_AUTH_REQUIRED=false
|
TIMESHEET_AUTH_REQUIRED=false
|
||||||
|
|||||||
6
pom.xml
6
pom.xml
@@ -91,6 +91,12 @@
|
|||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mnode.ical4j</groupId>
|
||||||
|
<artifactId>ical4j</artifactId>
|
||||||
|
<version>4.2.5</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package org.zaine.app;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@EnableScheduling
|
||||||
public class Application {
|
public class Application {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(Application.class, args);
|
SpringApplication.run(Application.class, args);
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.zaine.app.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class CalendarSyncProperties {
|
||||||
|
|
||||||
|
private final String nextcloudBaseUrl;
|
||||||
|
private final String nextcloudUsername;
|
||||||
|
private final String nextcloudPassword;
|
||||||
|
private final List<String> nextcloudCalendarIds;
|
||||||
|
private final String workIcsUrl;
|
||||||
|
private final ZoneId zoneId;
|
||||||
|
private final int daysBack;
|
||||||
|
private final int daysForward;
|
||||||
|
|
||||||
|
public CalendarSyncProperties(
|
||||||
|
@Value("${calendar.nextcloud.base-url:${NEXTCLOUD_CALDAV_URL:}}") String nextcloudBaseUrl,
|
||||||
|
@Value("${calendar.nextcloud.username:${NEXTCLOUD_CALDAV_USERNAME:}}") String nextcloudUsername,
|
||||||
|
@Value("${calendar.nextcloud.password:${NEXTCLOUD_CALDAV_PASSWORD:}}") String nextcloudPassword,
|
||||||
|
@Value("${calendar.nextcloud.calendar-ids:${NEXTCLOUD_CALDAV_CALENDARS:personal,zxh}}") String nextcloudCalendarIds,
|
||||||
|
@Value("${calendar.work.ics-url:${WORK_ICS_URL:}}") String workIcsUrl,
|
||||||
|
@Value("${calendar.timezone:${CALENDAR_TIMEZONE:Europe/London}}") String timezone,
|
||||||
|
@Value("${calendar.sync.days-back:${CALENDAR_SYNC_DAYS_BACK:30}}") int daysBack,
|
||||||
|
@Value("${calendar.sync.days-forward:${CALENDAR_SYNC_DAYS_FORWARD:365}}") int daysForward) {
|
||||||
|
this.nextcloudBaseUrl = trim(nextcloudBaseUrl);
|
||||||
|
this.nextcloudUsername = trim(nextcloudUsername);
|
||||||
|
this.nextcloudPassword = trim(nextcloudPassword);
|
||||||
|
this.nextcloudCalendarIds = Arrays.stream(nextcloudCalendarIds.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(value -> !value.isBlank())
|
||||||
|
.toList();
|
||||||
|
this.workIcsUrl = trim(workIcsUrl);
|
||||||
|
this.zoneId = ZoneId.of(timezone);
|
||||||
|
this.daysBack = daysBack;
|
||||||
|
this.daysForward = daysForward;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNextcloudBaseUrl() { return nextcloudBaseUrl; }
|
||||||
|
public String getNextcloudUsername() { return nextcloudUsername; }
|
||||||
|
public String getNextcloudPassword() { return nextcloudPassword; }
|
||||||
|
public List<String> getNextcloudCalendarIds() { return nextcloudCalendarIds; }
|
||||||
|
public String getWorkIcsUrl() { return workIcsUrl; }
|
||||||
|
public ZoneId getZoneId() { return zoneId; }
|
||||||
|
public int getDaysBack() { return daysBack; }
|
||||||
|
public int getDaysForward() { return daysForward; }
|
||||||
|
|
||||||
|
public boolean hasNextcloudCredentials() {
|
||||||
|
return !nextcloudBaseUrl.isBlank() && !nextcloudUsername.isBlank() && !nextcloudPassword.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trim(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.zaine.app.dto.CalendarEventDTO;
|
import org.zaine.app.dto.CalendarEventDTO;
|
||||||
|
import org.zaine.app.dto.CalendarSyncResultDTO;
|
||||||
import org.zaine.app.dto.CreateCalendarEventDTO;
|
import org.zaine.app.dto.CreateCalendarEventDTO;
|
||||||
import org.zaine.app.service.CalendarService;
|
import org.zaine.app.service.CalendarService;
|
||||||
|
import org.zaine.app.service.CalendarSyncService;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -24,9 +26,11 @@ import java.util.List;
|
|||||||
public class CalendarController {
|
public class CalendarController {
|
||||||
|
|
||||||
private final CalendarService calendarService;
|
private final CalendarService calendarService;
|
||||||
|
private final CalendarSyncService calendarSyncService;
|
||||||
|
|
||||||
public CalendarController(CalendarService calendarService) {
|
public CalendarController(CalendarService calendarService, CalendarSyncService calendarSyncService) {
|
||||||
this.calendarService = calendarService;
|
this.calendarService = calendarService;
|
||||||
|
this.calendarSyncService = calendarSyncService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "Get today's calendar events")
|
@Operation(summary = "Get today's calendar events")
|
||||||
@@ -62,4 +66,10 @@ public class CalendarController {
|
|||||||
public ResponseEntity<CalendarEventDTO> createEvent(@RequestBody CreateCalendarEventDTO dto) {
|
public ResponseEntity<CalendarEventDTO> createEvent(@RequestBody CreateCalendarEventDTO dto) {
|
||||||
return ResponseEntity.ok(calendarService.createEvent(dto));
|
return ResponseEntity.ok(calendarService.createEvent(dto));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Fetch configured external calendars now")
|
||||||
|
@PostMapping("/sync")
|
||||||
|
public ResponseEntity<CalendarSyncResultDTO> syncCalendar() {
|
||||||
|
return ResponseEntity.ok(calendarSyncService.sync());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/main/java/org/zaine/app/dto/CalendarSyncResultDTO.java
Normal file
12
src/main/java/org/zaine/app/dto/CalendarSyncResultDTO.java
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package org.zaine.app.dto;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record CalendarSyncResultDTO(
|
||||||
|
OffsetDateTime syncedAt,
|
||||||
|
int sourceCount,
|
||||||
|
int eventCount,
|
||||||
|
List<String> sources,
|
||||||
|
List<String> errors) {
|
||||||
|
}
|
||||||
@@ -10,4 +10,6 @@ public interface CalendarEventRepository extends JpaRepository<CalendarEvent, Lo
|
|||||||
List<CalendarEvent> findByStartsAtLessThanAndEndsAtGreaterThanOrderByStartsAtAsc(
|
List<CalendarEvent> findByStartsAtLessThanAndEndsAtGreaterThanOrderByStartsAtAsc(
|
||||||
OffsetDateTime rangeEnd,
|
OffsetDateTime rangeEnd,
|
||||||
OffsetDateTime rangeStart);
|
OffsetDateTime rangeStart);
|
||||||
|
|
||||||
|
void deleteBySource(String source);
|
||||||
}
|
}
|
||||||
|
|||||||
285
src/main/java/org/zaine/app/service/CalendarSyncService.java
Normal file
285
src/main/java/org/zaine/app/service/CalendarSyncService.java
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
package org.zaine.app.service;
|
||||||
|
|
||||||
|
import net.fortuna.ical4j.data.CalendarBuilder;
|
||||||
|
import net.fortuna.ical4j.data.ParserException;
|
||||||
|
import net.fortuna.ical4j.model.Calendar;
|
||||||
|
import net.fortuna.ical4j.model.Component;
|
||||||
|
import net.fortuna.ical4j.model.Period;
|
||||||
|
import net.fortuna.ical4j.model.Property;
|
||||||
|
import net.fortuna.ical4j.model.component.CalendarComponent;
|
||||||
|
import net.fortuna.ical4j.model.component.VEvent;
|
||||||
|
import net.fortuna.ical4j.model.property.DateProperty;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.zaine.app.config.CalendarSyncProperties;
|
||||||
|
import org.zaine.app.dto.CalendarSyncResultDTO;
|
||||||
|
import org.zaine.app.model.CalendarEvent;
|
||||||
|
import org.zaine.app.repositories.CalendarEventRepository;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.temporal.Temporal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CalendarSyncService {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(CalendarSyncService.class.getName());
|
||||||
|
|
||||||
|
private final CalendarEventRepository calendarEventRepository;
|
||||||
|
private final CalendarSyncProperties properties;
|
||||||
|
private final HttpClient httpClient;
|
||||||
|
|
||||||
|
public CalendarSyncService(
|
||||||
|
CalendarEventRepository calendarEventRepository,
|
||||||
|
CalendarSyncProperties properties) {
|
||||||
|
this.calendarEventRepository = calendarEventRepository;
|
||||||
|
this.properties = properties;
|
||||||
|
this.httpClient = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(20))
|
||||||
|
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(initialDelayString = "${calendar.sync.initial-delay-ms:15000}",
|
||||||
|
fixedDelayString = "${calendar.sync.fixed-delay-ms:1800000}")
|
||||||
|
public void scheduledSync() {
|
||||||
|
try {
|
||||||
|
CalendarSyncResultDTO result = sync();
|
||||||
|
logger.info("Calendar sync complete: " + result.eventCount() + " events from " + result.sourceCount() + " sources");
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.log(Level.WARNING, "Calendar sync failed", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public CalendarSyncResultDTO sync() {
|
||||||
|
List<CalendarSource> sources = configuredSources();
|
||||||
|
List<String> syncedSources = new ArrayList<>();
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
int eventCount = 0;
|
||||||
|
|
||||||
|
for (CalendarSource source : sources) {
|
||||||
|
try {
|
||||||
|
String ics = fetch(source);
|
||||||
|
List<CalendarEvent> events = parse(source.name(), ics);
|
||||||
|
calendarEventRepository.deleteBySource(source.name());
|
||||||
|
calendarEventRepository.saveAll(events);
|
||||||
|
syncedSources.add(source.name());
|
||||||
|
eventCount += events.size();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.log(Level.WARNING, "Failed to sync calendar source " + source.name(), ex);
|
||||||
|
errors.add(source.name() + ": " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CalendarSyncResultDTO(OffsetDateTime.now(), syncedSources.size(), eventCount, syncedSources, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CalendarSource> configuredSources() {
|
||||||
|
List<CalendarSource> sources = new ArrayList<>();
|
||||||
|
|
||||||
|
if (properties.hasNextcloudCredentials()) {
|
||||||
|
for (String calendarId : properties.getNextcloudCalendarIds()) {
|
||||||
|
sources.add(new CalendarSource(
|
||||||
|
"nextcloud-" + calendarId,
|
||||||
|
nextcloudExportUrl(calendarId),
|
||||||
|
properties.getNextcloudUsername(),
|
||||||
|
properties.getNextcloudPassword()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!properties.getWorkIcsUrl().isBlank()) {
|
||||||
|
sources.add(new CalendarSource("work", properties.getWorkIcsUrl(), "", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String nextcloudExportUrl(String calendarId) {
|
||||||
|
String baseUrl = properties.getNextcloudBaseUrl().endsWith("/")
|
||||||
|
? properties.getNextcloudBaseUrl()
|
||||||
|
: properties.getNextcloudBaseUrl() + "/";
|
||||||
|
return baseUrl + calendarId + "?export";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fetch(CalendarSource source) throws IOException, InterruptedException {
|
||||||
|
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(URI.create(source.url()))
|
||||||
|
.GET()
|
||||||
|
.timeout(Duration.ofSeconds(60))
|
||||||
|
.header("Accept", "text/calendar,*/*");
|
||||||
|
|
||||||
|
if (!source.username().isBlank() || !source.password().isBlank()) {
|
||||||
|
String credentials = source.username() + ":" + source.password();
|
||||||
|
String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
|
||||||
|
requestBuilder.header("Authorization", "Basic " + encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||||
|
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||||
|
throw new IOException("Calendar fetch returned HTTP " + response.statusCode());
|
||||||
|
}
|
||||||
|
return response.body();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CalendarEvent> parse(String source, String ics) throws IOException, ParserException {
|
||||||
|
CalendarBuilder builder = new CalendarBuilder();
|
||||||
|
Calendar calendar = builder.build(new ByteArrayInputStream(ics.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
ZoneId zoneId = properties.getZoneId();
|
||||||
|
ZonedDateTime from = LocalDate.now(zoneId).minusDays(properties.getDaysBack()).atStartOfDay(zoneId);
|
||||||
|
ZonedDateTime to = LocalDate.now(zoneId).plusDays(properties.getDaysForward()).plusDays(1).atStartOfDay(zoneId);
|
||||||
|
Period<ZonedDateTime> syncWindow = new Period<>(from, to);
|
||||||
|
|
||||||
|
List<CalendarEvent> events = new ArrayList<>();
|
||||||
|
for (CalendarComponent component : calendar.getComponents()) {
|
||||||
|
if (component instanceof VEvent event) {
|
||||||
|
events.addAll(toCalendarEvents(source, event, syncWindow, zoneId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events.stream()
|
||||||
|
.filter(event -> event.getStartsAt().isBefore(to.toOffsetDateTime())
|
||||||
|
&& event.getEndsAt().isAfter(from.toOffsetDateTime()))
|
||||||
|
.sorted(Comparator.comparing(CalendarEvent::getStartsAt))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CalendarEvent> toCalendarEvents(
|
||||||
|
String source,
|
||||||
|
VEvent event,
|
||||||
|
Period<ZonedDateTime> syncWindow,
|
||||||
|
ZoneId zoneId) {
|
||||||
|
String uid = event.getUid().map(Property::getValue).orElseGet(() -> BigInteger.valueOf(event.hashCode()).abs().toString());
|
||||||
|
String title = propertyValue(event, Property.SUMMARY, "Untitled event");
|
||||||
|
String description = propertyValue(event, Property.DESCRIPTION, null);
|
||||||
|
String location = propertyValue(event, Property.LOCATION, null);
|
||||||
|
boolean allDay = isAllDay(event);
|
||||||
|
OffsetDateTime originalStart = event.getStartDate()
|
||||||
|
.map(DateProperty::getDate)
|
||||||
|
.map(value -> toOffsetDateTime(value, zoneId, false))
|
||||||
|
.orElse(null);
|
||||||
|
OffsetDateTime originalEnd = event.getEndDate()
|
||||||
|
.map(DateProperty::getDate)
|
||||||
|
.map(value -> toOffsetDateTime(value, zoneId, allDay))
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (originalStart == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration duration = originalEnd == null || !originalEnd.isAfter(originalStart)
|
||||||
|
? Duration.ofHours(1)
|
||||||
|
: Duration.between(originalStart, originalEnd);
|
||||||
|
|
||||||
|
Set<Period<Temporal>> periods;
|
||||||
|
try {
|
||||||
|
periods = event.calculateRecurrenceSet(syncWindow);
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
logger.log(Level.WARNING, "Failed to expand recurrence for " + uid + "; using original event only", ex);
|
||||||
|
periods = Set.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (periods.isEmpty()) {
|
||||||
|
CalendarEvent calendarEvent = buildEvent(
|
||||||
|
source,
|
||||||
|
uid,
|
||||||
|
originalStart,
|
||||||
|
originalStart.plus(duration),
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
location,
|
||||||
|
allDay);
|
||||||
|
return List.of(calendarEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return periods.stream()
|
||||||
|
.map(period -> {
|
||||||
|
OffsetDateTime startsAt = toOffsetDateTime(period.getStart(), zoneId, false);
|
||||||
|
OffsetDateTime endsAt = toOffsetDateTime(period.getEnd(), zoneId, allDay);
|
||||||
|
if (!endsAt.isAfter(startsAt)) {
|
||||||
|
endsAt = startsAt.plus(duration);
|
||||||
|
}
|
||||||
|
return buildEvent(source, uid, startsAt, endsAt, title, description, location, allDay);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CalendarEvent buildEvent(
|
||||||
|
String source,
|
||||||
|
String uid,
|
||||||
|
OffsetDateTime startsAt,
|
||||||
|
OffsetDateTime endsAt,
|
||||||
|
String title,
|
||||||
|
String description,
|
||||||
|
String location,
|
||||||
|
boolean allDay) {
|
||||||
|
CalendarEvent event = new CalendarEvent();
|
||||||
|
event.setExternalId(source + ":" + uid + ":" + startsAt.toInstant());
|
||||||
|
event.setTitle(title);
|
||||||
|
event.setDescription(description);
|
||||||
|
event.setLocation(location);
|
||||||
|
event.setStartsAt(startsAt);
|
||||||
|
event.setEndsAt(endsAt);
|
||||||
|
event.setAllDay(allDay);
|
||||||
|
event.setSource(source);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String propertyValue(VEvent event, String propertyName, String fallback) {
|
||||||
|
return event.<Property>getProperty(propertyName)
|
||||||
|
.map(Property::getValue)
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(value -> !value.isBlank())
|
||||||
|
.orElse(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAllDay(VEvent event) {
|
||||||
|
return event.getStartDate()
|
||||||
|
.map(DateProperty::getDate)
|
||||||
|
.map(value -> value instanceof LocalDate)
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OffsetDateTime toOffsetDateTime(Temporal temporal, ZoneId zoneId, boolean allDayEnd) {
|
||||||
|
if (temporal instanceof ZonedDateTime value) {
|
||||||
|
return value.toOffsetDateTime();
|
||||||
|
}
|
||||||
|
if (temporal instanceof OffsetDateTime value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (temporal instanceof LocalDateTime value) {
|
||||||
|
return value.atZone(zoneId).toOffsetDateTime();
|
||||||
|
}
|
||||||
|
if (temporal instanceof LocalDate value) {
|
||||||
|
LocalDate date = allDayEnd ? value : value;
|
||||||
|
return date.atStartOfDay(zoneId).toOffsetDateTime();
|
||||||
|
}
|
||||||
|
if (temporal instanceof Instant value) {
|
||||||
|
return value.atZone(zoneId).toOffsetDateTime();
|
||||||
|
}
|
||||||
|
return ZonedDateTime.from(temporal).withZoneSameInstant(zoneId).toOffsetDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record CalendarSource(String name, String url, String username, String password) {
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user