All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.svws_nrw.davapi.api.ReportCalendarDispatcher Maven / Gradle / Ivy

Go to download

Diese Bibliothek enthält die Java-Server-Definition der CalDAV und CardDAV-Schnittstelle für die Schulverwaltungssoftware in NRW

There is a newer version: 1.0.1
Show newest version
package de.svws_nrw.davapi.api;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;

import de.svws_nrw.core.data.kalender.Kalender;
import de.svws_nrw.core.data.kalender.KalenderEintrag;
import de.svws_nrw.davapi.data.CollectionRessourceQueryParameters;
import de.svws_nrw.davapi.data.IKalenderRepository;
import de.svws_nrw.davapi.model.dav.Getcontenttype;
import de.svws_nrw.davapi.model.dav.Getetag;
import de.svws_nrw.davapi.model.dav.Multistatus;
import de.svws_nrw.davapi.model.dav.Prop;
import de.svws_nrw.davapi.model.dav.Propstat;
import de.svws_nrw.davapi.model.dav.Response;
import de.svws_nrw.davapi.model.dav.SyncCollection;
import de.svws_nrw.davapi.model.dav.cal.CalendarData;
import de.svws_nrw.davapi.model.dav.cal.CalendarMultiget;
import de.svws_nrw.davapi.model.dav.cal.CalendarQuery;
import de.svws_nrw.davapi.model.dav.cal.CompFilter;
import de.svws_nrw.davapi.model.dav.cal.TimeRange;
import de.svws_nrw.davapi.util.XmlUnmarshallingUtil;
import de.svws_nrw.davapi.util.icalendar.DateTimeUtil;
import de.svws_nrw.davapi.util.icalendar.VCalendar;
import jakarta.validation.constraints.NotNull;

/**
 * Dispatcher-Klasse für die Verarbeitung von Requests auf das DAV-API mittels
 * der HTTP-Methode REPORT auf die Ressource Kalender.
 */
public class ReportCalendarDispatcher extends DavDispatcher {

	/** Repository-Klasse zur Abfrage von Kalendern aus der SVWS-Datenbank */
	private final IKalenderRepository repository;

	/** URI-Parameter für die Erzeugung von URIs des Ergebnisobjekts */
	private final DavUriParameter uriParameter;

	/**
	 * Erstellt einen neuen Dispatcher mit dem angegebenen Repository und
	 * URI-Parameter
	 *
	 * @param kalenderRepository das Repository für Kalender
	 * @param uriParameter       die URI-Parameter für im Response verwendete URIs
	 */
	public ReportCalendarDispatcher(final IKalenderRepository kalenderRepository, final DavUriParameter uriParameter) {
		this.repository = kalenderRepository;
		this.uriParameter = uriParameter;
	}

	/**
	 * Verarbeitet den gegebenen Inputstream als CalDav-Protokollanfrage und
	 * versucht eine Anfrage als Multiget, SyncCollection oder CalendarQuery zu
	 * interpretieren und entsprechende Antwortobjekte zu erstellen.
	 *
	 * @param inputStream           der Request-Inputstream
	 * @param ressourceCollectionId die Ressourcensammlung-Id
	 * @return ein Antwortobjekt für die Reportanfrage, abhängig vom Request
	 * @throws IOException beim Verarbeiten des Inputstreams
	 */
	public Object dispatch(final InputStream inputStream, final String ressourceCollectionId) throws IOException {
		final Optional kalender = this.repository.getKalenderById(ressourceCollectionId,
				CollectionRessourceQueryParameters.INCLUDE_RESSOURCES_INCLUDE_PAYLOAD);
		if (kalender.isEmpty()) {
			return this.createResourceNotFoundError("Kalender mit der angegebenen Id wurde nicht gefunden!");
		}

		/*
		 * Ein REPORT-Request auf die Kalender-Resource kann als CalendarMultiget- und
		 * SyncCollection-Objekt formuliert werden. Der InputStream kann nur einmal
		 * gelesen werden. Daher erfolgt die Typ-Ermittlung nach dem try..error-Prinzip.
		 * Dazu wird zunächst ein Klon des InputStreams erstellt
		 */
		final ByteArrayOutputStream inputStreamAsByteArray = new ByteArrayOutputStream();
		inputStream.transferTo(inputStreamAsByteArray);
		try (InputStream inputStreamClone1 = new ByteArrayInputStream(inputStreamAsByteArray.toByteArray())) {
			//Versuche Multiget
			final CalendarMultiget multiget = XmlUnmarshallingUtil.unmarshal(inputStreamClone1,
					CalendarMultiget.class);
			inputStreamAsByteArray.close();
			return this.handleCalendarMultigetRequest(kalender.get(), multiget);
		} catch (final IOException e1) {
			// Kein Multiget, versuche Sync-Collection
			try (InputStream inputStreamClone2 = new ByteArrayInputStream(inputStreamAsByteArray.toByteArray())) {
				final SyncCollection syncCollection = XmlUnmarshallingUtil.unmarshal(inputStreamClone2,
						SyncCollection.class);
				inputStreamAsByteArray.close();
				return this.handleSyncCollectionRequest(kalender.get(), syncCollection);
			} catch (final Exception e2) {
				// Kein Sync-Collection, Versuche CalendarQuery
				try (InputStream inputStreamClone3 = new ByteArrayInputStream(inputStreamAsByteArray.toByteArray())) {
					final CalendarQuery calendarQuery = XmlUnmarshallingUtil.unmarshal(inputStreamClone3,
							CalendarQuery.class);
					inputStreamAsByteArray.close();
					return this.handleCalendarQueryRequest(kalender.get(), calendarQuery);
				} catch (final Exception e3) {
					throw new UnsupportedOperationException("Weder Multiget noch Sync-Collection noch CalendarQuery bei REPORT Calendar: " + e3.getMessage(),
							e3);
				}
			}
		}
	}


	/**
	 * Ermittelt das Ergebnisobjekt für den Fall, dass ein
	 * "calendar-multiget"-Request gestellt wurde.
	 *
	 * @param kalender Kalender, zu dem die Informationen abgerufen werden sollen
	 * @param multiget Parameter des Requests. Diese steuern, welche Informationen
	 *                 im Einzelnen zu der Ressource zurückgeliefert werden sollen.
	 * @return Multistatus-Objekt
	 */
	private Multistatus handleCalendarMultigetRequest(final Kalender kalender, final CalendarMultiget multiget) {
		final Multistatus ms = new Multistatus();
		final List eintraegeByHrefs = this.getEintraegeByHrefs(kalender, multiget.getHref());
		uriParameter.setResourceCollectionId(kalender.id);
		for (final KalenderEintrag eintrag : eintraegeByHrefs) {
			ms.getResponse().add(this.generateResponseEventLevel(eintrag, multiget.getProp()));
		}
		return ms;
	}

	/**
	 * Ermittelt das Ergebnisobjekt für den Fall, dass ein "sync-collection"-Request
	 * gestellt wurde.
	 *
	 * @param kalender       Kalender, zu dem die Informationen abgerufen werden
	 *                       sollen
	 * @param syncCollection Parameter des Requests. Diese steuern, welche
	 *                       Informationen im Einzelnen zu der Ressource
	 *                       zurückgeliefert werden sollen.
	 * @return Multistatus-Objekt
	 */
	private Multistatus handleSyncCollectionRequest(final Kalender kalender, final SyncCollection syncCollection) {
		final Multistatus ms = new Multistatus();
		uriParameter.setResourceCollectionId(kalender.id);
		final Long syncTokenMillis = syncCollection.getSyncToken().isBlank() ? 0
				: Long.valueOf(syncCollection.getSyncToken());
		for (final KalenderEintrag eintrag : this.getEintraegeBySyncToken(kalender.id, syncTokenMillis)) {
			ms.getResponse().add(this.generateResponseEventLevel(eintrag, syncCollection.getProp()));
		}
		final List deletedResourceUIDsSince = repository.getDeletedResourceUIDsSince(kalender.id, syncTokenMillis);
		for (final String deletedResourceUID : deletedResourceUIDsSince) {
			ms.getResponse().add(this.generateResponseResourceNotFound(deletedResourceUID));
		}
		ms.setSyncToken(Long.toString(kalender.synctoken));
		return ms;
	}

	private Response generateResponseResourceNotFound(final String deletedResourceUID) {
		final Response r = new Response();
		r.setStatus(Propstat.PROP_STATUS_404_NOT_FOUND);
		uriParameter.setResourceId(deletedResourceUID);
		r.getHref().add(DavUriBuilder.getCalendarEntryUri(uriParameter));
		return r;
	}

	/**
	 * Ermittelt das Ergebnisobjekt für den Fall, dass ein "sync-collection"-Request
	 * gestellt wurde.
	 *
	 * @param kalender      Kalender, zu dem die Informationen abgerufen werden
	 *                      sollen
	 * @param calendarQuery Parameter des Requests. Diese steuern, welche
	 *                      Informationen im Einzelnen zu der Ressource
	 *                      zurückgeliefert werden sollen.
	 * @return Multistatus-Objekt
	 */
	private Multistatus handleCalendarQueryRequest(final Kalender kalender, final CalendarQuery calendarQuery) {
		final Multistatus ms = new Multistatus();
		uriParameter.setResourceCollectionId(kalender.id);
		for (final KalenderEintrag eintrag : this.getEintraegeByFilter(kalender.id, calendarQuery)) {
			ms.getResponse().add(this.generateResponseEventLevel(eintrag, calendarQuery.getProp()));
		}
		return ms;
	}

	/**
	 * Ermittelt eine Liste von angefragten KalenderEintraegen zu einem Kalender.
	 * Welche Eintraege im Einzelnen zurückgegeben werden sollen, wird im Request
	 * über die Angabe von URIs der Eintrag-Ressourcen gesteuert.
	 *
	 * @param kalender     Kalender, aus dem KalenderEintraege ermittelt werden
	 *                     sollen.
	 * @param eintragHrefs List von URIs zu den angefragen Eintrag-Ressourcen im
	 *                     Kalender.
	 * @return Liste von Eintraegen.
	 */
	private List getEintraegeByHrefs(final @NotNull Kalender kalender, final List eintragHrefs) {
		uriParameter.setResourceCollectionId(kalender.id);
		return kalender.kalenderEintraege.stream().filter(k -> {
			uriParameter.setResourceId(k.uid);
			return eintragHrefs.isEmpty() || eintragHrefs.contains(DavUriBuilder.getCalendarEntryUri(uriParameter));
		}).toList();
	}

	/**
	 * Ermittelt eine Liste von geänderten Eintraegen zu einem Kalender ab einem
	 * bestimmten "Aufsetzpunkt" (Differenzdaten). Diese Funktion dient der
	 * Synchronisation von Kalendern mit dem aufrufenden Client. Die Methode liefert
	 * alle Eintraege eines Kalenders zurück, die sich nach dem angegebenen des
	 * Sync-Tokens serverseitig geändert haben oder neu hinzugekommen sind.
	 *
	 * @param kalenderId Id des Kalenders, aus dem Eintraege ermittelt werden
	 *                   sollen.
	 * @param syncToken  Sync-Token bzw. Aufsetzpunkt zur Abfrage von
	 *                   Differenzdaten.
	 * @return Liste von Eintraegen.
	 */
	private List getEintraegeBySyncToken(final String kalenderId, final Long syncToken) {
		final Optional kalenderById = this.repository.getKalenderById(kalenderId,
				CollectionRessourceQueryParameters.INCLUDE_RESSOURCES_INCLUDE_PAYLOAD);
		if (kalenderById.isEmpty()) {
			return Collections.emptyList();
		}
		return kalenderById.get().kalenderEintraege.stream().filter(e -> Long.valueOf(e.version) > syncToken).toList();
	}

	/**
	 * Ermittelt eine Liste von geänderten Eintraegen zu einem Kalender über einen
	 * dynamischen Filter, der im Calendar-Query Request definiert werden kann.
	 *
	 * @param kalenderId    Id des Kalenders, aus dem Einträge ermittelt werden
	 *                      sollen.
	 * @param calendarQuery Calendar-Query, enthält Filterkriterien für die Abfrage
	 *                      von Kalender-Einträgen (z.B. innerhalb eines
	 *                      Zeitintervalls)
	 * @return Liste von Einträgen.
	 */
	private List getEintraegeByFilter(final String kalenderId, final CalendarQuery calendarQuery) {
		final Optional kalenderById = this.repository.getKalenderById(kalenderId,
				CollectionRessourceQueryParameters.INCLUDE_RESSOURCES_INCLUDE_PAYLOAD);
		if (kalenderById.isEmpty()) {
			return Collections.emptyList();
		}
		final Predicate eintragFilter = getEintragFilter(calendarQuery);
		return kalenderById.get().kalenderEintraege.stream().filter(eintragFilter).toList();
	}

	/**
	 * gibt das Filterprädikat für die gegebene Kalenderquery zurück. vgl.
	 * https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1
	 *
	 * @param calendarQuery das CalendarQuery, für das der Filter erstellt werden
	 *                      soll
	 * @return ein Prädikat zum Filtern von Kalendereinträgen auf Basis des
	 *         CalendarQuery
	 */
	private static Predicate getEintragFilter(final CalendarQuery calendarQuery) {
		Predicate ressourceTypePredicate = e -> true;
		Predicate componentTypePredicate = e -> true;
		Predicate timeRangePredicate = e -> true;
		if ((calendarQuery != null) && (calendarQuery.getFilter() != null)
				&& (calendarQuery.getFilter().getCompFilter() != null)) {

			CompFilter filter = calendarQuery.getFilter().getCompFilter();
			// Filter für die Art der Ressource, bspw. VCALENDAR (andere
			// Einträge haben wir nicht)
			final String ressourceTypeFilter = filter.getName();
			if (ressourceTypeFilter != null) {
				ressourceTypePredicate = e -> (e.data != null) && e.data.startsWith("BEGIN:" + ressourceTypeFilter);
			}

			if (filter.getCompFilter() != null) {
				filter = filter.getCompFilter();
				final String componentTypeFilter = filter.getName();
				if (componentTypeFilter != null) {
					// component type, bspw VEVENT, VTODO, VFREEBUSY oder VTIMEZONE
					componentTypePredicate = e -> (e.data != null) && e.data.contains("BEGIN:" + componentTypeFilter);
				}
				if ((filter.getTimeRange() != null) && (filter.getTimeRange().getStart() != null)
						&& (filter.getTimeRange().getEnd() != null)) {
					final TimeRange timeRange = filter.getTimeRange();
					final Instant timeRangeMin = DateTimeUtil.parseCalDav(timeRange.getStart());
					final Instant timeRangeMax = DateTimeUtil.parseCalDav(timeRange.getEnd());
					timeRangePredicate = e -> DateTimeUtil.intersect(timeRangeMin, timeRangeMax,
							DateTimeUtil.fromSqlTimeStamp(e.kalenderStart),
							DateTimeUtil.fromSqlTimeStamp(e.kalenderEnde));
				}
			}
		}
		return ressourceTypePredicate.and(componentTypePredicate).and(timeRangePredicate);
	}

	/**
	 * Generiert ein Response-Objekt für einen angegebenen Eintrag.
	 *
	 * @param eintrag       Eintrag, zu dem Informationen zurückgeliefert werden
	 *                      sollen
	 * @param propRequested Prop aus dem Request. Definiert, welche Informationen
	 *                      zur Ressource zurückgeliefert werden sollen.
	 * @return Response-Objekt zum angegebenen KalenderEintrag
	 */
	private Response generateResponseEventLevel(final KalenderEintrag eintrag, final Prop propRequested) {
		final DynamicPropUtil dynamicPropUtil = new DynamicPropUtil(propRequested);
		uriParameter.setResourceId(eintrag.uid);

		final Prop prop200 = new Prop();
		if (dynamicPropUtil.getIsFieldRequested(CalendarData.class)) {
			final CalendarData calendarData = new CalendarData();
			final VCalendar vCalendar = VCalendar.createVCalendar(eintrag);
			calendarData.getContent().add(vCalendar.serialize());
			prop200.setCalendarData(calendarData);
		}

		if (dynamicPropUtil.getIsFieldRequested(Getetag.class)) {
			final Getetag getetag = new Getetag();
			getetag.getContent().add(eintrag.version);
			prop200.setGetetag(getetag);
		}
		if (dynamicPropUtil.getIsFieldRequested(Getcontenttype.class)) {
			final Getcontenttype getcontenttype = new Getcontenttype();
			getcontenttype.getContent().add("text/calendar");
			prop200.setGetcontenttype(getcontenttype);
		}

		final Response response = createResponse(propRequested, prop200);
		response.getHref().add(DavUriBuilder.getCalendarEntryUri(uriParameter));
		return response;
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy