Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2026 United Internet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.unitedinternet.cosmo.dao;

import java.util.List;

import org.unitedinternet.cosmo.model.CollectionModification;

/**
* <p>
* Persistent per-collection change log backing RFC 6578
* <code>DAV:sync-collection</code> incremental synchronization. Every
* membership change of a collection appends exactly one
* {@link CollectionModification} row whose server-assigned id is globally
* monotonic.
* </p>
*/
public interface ModificationDao {

/**
* Appends one change record to a collection's log.
*
* @param collectionUid uid of the collection whose membership changed
* @param memberUid uid of the affected member item
* @param memberName name of the affected member item at the time of the
* change (needed to report tombstones for deletions)
* @param modType one of {@link CollectionModification#MOD_TYPE_CREATED},
* {@link CollectionModification#MOD_TYPE_MODIFIED} or
* {@link CollectionModification#MOD_TYPE_DELETED}
*/
void log(String collectionUid, String memberUid, String memberName,
char modType);

/**
* Returns the change records of a collection that were appended
* <em>after</em> the given revision, ordered by ascending revision.
*
* @param collectionUid uid of the collection to query
* @param sinceRevision exclusive lower bound (a client's previously seen
* token value)
* @param limit maximum number of records to return; a negative value
* means no limit
*/
List<CollectionModification> findSince(String collectionUid,
long sinceRevision,
int limit);

/**
* The highest revision ever assigned (0 when the log is empty). This is
* the numeric part of the freshest sync token the server can issue.
*/
long currentRevision();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2026 United Internet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.unitedinternet.cosmo.model;

/**
* <p>
* A single entry in a collection's persistent change log. Each record captures
* one membership-affecting change ({@link #MOD_TYPE_CREATED creation},
* {@link #MOD_TYPE_MODIFIED modification} or {@link #MOD_TYPE_DELETED deletion}
* of a member) together with enough information to reconstruct the member's
* DAV:href even after deletion.
* </p>
*
* <p>
* The {@link #getId() id} is a globally monotonic server-assigned revision.
* It is the value encoded in RFC 6578 <code>DAV:sync-token</code>s issued by
* Cosmo ("urn:cosmo:sync-token:&lt;id&gt;") and orders all changes made to a
* collection.
* </p>
*/
public interface CollectionModification {

/** Change type: the member was created in the collection. */
char MOD_TYPE_CREATED = 'C';

/** Change type: an existing member of the collection changed. */
char MOD_TYPE_MODIFIED = 'M';

/** Change type (tombstone): the member was removed from the collection. */
char MOD_TYPE_DELETED = 'D';

/**
* Globally monotonic revision assigned by the server. Doubles as the
* numeric part of sync tokens and as the ordering key of the log.
*/
Long getId();

/** Uid of the collection whose membership changed. */
String getCollectionUid();

/** Uid of the affected member item. */
String getMemberUid();

/**
* Name of the affected member item at the time of the change. Kept for
* deleted members so their old DAV:href can still be reported as a
* tombstone.
*/
String getMemberName();

/** One of {@link #MOD_TYPE_CREATED}, {@link #MOD_TYPE_MODIFIED}, {@link #MOD_TYPE_DELETED}. */
char getModType();

/** Wall-clock timestamp of the change (informational; ordering uses the id). */
Long getTimestamp();
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
*/
package org.unitedinternet.cosmo.service;

import java.util.List;
import java.util.Set;
import java.util.SortedSet;

import org.unitedinternet.cosmo.model.CollectionModification;

import org.unitedinternet.cosmo.model.CollectionItem;
import org.unitedinternet.cosmo.model.ContentItem;
import org.unitedinternet.cosmo.model.HomeCollectionItem;
Expand Down Expand Up @@ -427,4 +430,24 @@ public void removeTicket(Item item,
*/
public void removeItemsFromCollection(CollectionItem collection);

/**
* Returns the change records of a collection's persistent change log that
* were appended after the given revision, ordered by ascending revision.
* Backs incremental DAV:sync-collection REPORT rounds (RFC 6578).
*
* @param collectionUid uid of the collection to query
* @param sinceRevision exclusive lower bound (client-held token value)
* @param limit maximum number of records; negative means no limit
* @return the matching {@link CollectionModification} records
*/
List<CollectionModification> findModificationsSince(String collectionUid,
long sinceRevision,
int limit);

/**
* The highest revision currently assigned in the change log. This is the
* numeric part of the freshest sync token the server can issue.
*/
long getModificationRevision();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2026 United Internet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.unitedinternet.cosmo.dao.hibernate;

import java.util.List;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;

import org.springframework.stereotype.Repository;
import org.unitedinternet.cosmo.dao.ModificationDao;
import org.unitedinternet.cosmo.model.CollectionModification;
import org.unitedinternet.cosmo.model.hibernate.HibCollectionModification;

/**
* <p>
* Hibernate implementation of the persistent collection change log. Rows are
* never mutated after insertion; the AUTO_INCREMENT primary key provides the
* global monotonic revision used by sync tokens.
* </p>
*/
@Repository
public class ModificationDaoImpl implements ModificationDao {

@PersistenceContext
protected EntityManager em;

public void log(String collectionUid, String memberUid,
String memberName, char modType) {
em.persist(new HibCollectionModification(collectionUid, memberUid,
memberName, modType));
em.flush();
}

public List<CollectionModification> findSince(String collectionUid,
long sinceRevision,
int limit) {
TypedQuery<HibCollectionModification> query = em.createQuery(
"select m from HibCollectionModification m "
+ "where m.collectionUid = :collectionUid and m.id > :since "
+ "order by m.id asc",
HibCollectionModification.class);
query.setParameter("collectionUid", collectionUid);
query.setParameter("since", Long.valueOf(sinceRevision));
if (limit >= 0) {
query.setMaxResults(limit);
}
List<HibCollectionModification> result = query.getResultList();
return List.copyOf(result);
}

public long currentRevision() {
List<Long> revisions = em.createQuery(
"select max(m.id) from HibCollectionModification m",
Long.class).getResultList();
if (revisions.isEmpty() || revisions.get(0) == null) {
return 0L;
}
return revisions.get(0).longValue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
import org.unitedinternet.cosmo.dav.caldav.report.FreeBusyReport;
import org.unitedinternet.cosmo.dav.caldav.report.MultigetReport;
import org.unitedinternet.cosmo.dav.caldav.report.QueryReport;
import org.unitedinternet.cosmo.dav.report.SyncCollectionReport;

import org.unitedinternet.cosmo.dav.property.ExcludeFreeBusyRollup;
import org.unitedinternet.cosmo.dav.property.WebDavProperty;
import org.unitedinternet.cosmo.model.CollectionItem;
Expand Down Expand Up @@ -98,6 +100,8 @@ public class DavCollectionBase extends DavItemResourceBase implements DavItemCol
REPORT_TYPES.add(PrincipalMatchReport.REPORT_TYPE_PRINCIPAL_MATCH);
REPORT_TYPES
.add(PrincipalPropertySearchReport.REPORT_TYPE_PRINCIPAL_PROPERTY_SEARCH);
REPORT_TYPES.add(SyncCollectionReport.REPORT_TYPE_DAV_SYNC_COLLECTION);


DEAD_PROPERTY_FILTER.add(CollectionItem.class.getName());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,11 +276,9 @@ public void report(DavRequest request, DavResponse response, WebDavResource reso
try {
ReportInfo info = request.getReportInfo();
if (info == null) {
if (resource.isCollection()) {
return;
} else {
throw new BadRequestException("REPORT requires entity body");
}
// RFC 3253/6578: a REPORT without an entity body is invalid;
// silently ignoring it would leave the response unanswered.
throw new BadRequestException("REPORT requires entity body");
}
/*
* Since the report type could not be determined in the security filter in order to check ticket permissions
Expand Down
Loading