#1860: AddressService.kt
projectforge-carddav/src/main/kotlin/org/projectforge/carddav/service/AddressService.kt Kotlin Spring @Service — CardDAV address book service facade. Exposes ProjectForge contacts (favorites only) via the CardDAV protocol for synchronization with external address books (Apple Contacts, Thunderbird, DAVx⁵, etc.). Location: projectforge-carddav/src/main/kotlin/org/projectforge/carddav/service/AddressService.kt 101 lines · 66 code · 26 comments · 9 blank
Purpose: CardDAV is an address book extension to WebDAV (RFC 6352). This service acts as the bridge between the projectforge-carddav module's HTTP endpoints and the business-layer address data. It translates between ProjectForge's address domain objects and the CardDAV Contact model, enforcing the favorites-only access policy.
Architecture
The service follows a
read-only facade pattern. Create and Update operations are commented out and return empty
Contact() objects — the CardDAV implementation is read-only. The
deleteContact() method doesn't actually delete the address; it only
removes the address from the user's favorites (sets
isFavoriteCard = false in PersonalAddressDO). This is because:
- Deleting the actual address would affect other users who have also favorited it
- CardDAV clients expect to "delete" contacts from their personal address book, not the shared corporate directory
- Write operations are complex: vCard parsing, conflict resolution, and sync semantics would require significant additional work
Dependency Graph
AddressService (@Service)
├── AddressDAVCache (@Service + AbstractCache)
│ ├── AddressDao (core address persistence)
│ ├── AddressImageCache (contact photos)
│ └── VCardUtils (address → vCard conversion)
└── PersonalAddressDao (user's favorites list)
The
AddressDAVCache (
#844 context) wraps the expensive vCard generation (which involves base64-encoding contact photos, field mapping, and format serialization) in a TTL-based cache. Without this cache, every CardDAV client sync would regenerate vCards for all contacts — a CPU-intensive operation for address books with hundreds of entries and photos.
API Surface
| Method | Returns | Status | Behavior |
getContactList(userDO) | List<Contact> | Active | Returns all favorite addresses as vCard Contact objects, ordered by name |
getContact(id) | Contact? | Commented out | Would return single contact by ID if favorited |
createContact(ab, vcard) | Contact (empty) | Commented out | Contact creation not supported — returns empty Contact |
updateContact(contact, vcard) | Contact (empty) | Commented out | Contact updates not supported — returns empty Contact |
deleteContact(contactId) | Boolean | Active | Removes address from favorites (isFavoriteCard = false), returns true on success |
CardDAV Protocol Context
CardDAV clients (e.g., Apple Contacts on iOS/macOS, Thunderbird with CardBook, DAVx⁵ on Android) send HTTP requests to /{username}/addressBooks/ endpoints. The service is invoked by the CardDAV servlet/handler layer when:
1. PROPFIND / address-query REPORT: Client lists all contacts → getContactList()
2. GET /addressBooks/{uid}.vcf: Client fetches a single contact → getContact() (currently commented out)
3. DELETE /addressBooks/{uid}.vcf: Client removes from address book → deleteContact()
4. PUT: Create/update → not supported
The favorites-only policy means a user must explicitly mark contacts as favorites in the ProjectForge web UI for them to appear in their synced address book. This acts as an intentional gate — the full company directory is typically too large for personal address books (1000+ entries would flood a phone's contacts app).
Git History
868d6abb7 2025 → 2026 (copyright year update)
63081666f Source file headers: 2024→2025
49cddf9a0 WIP: Carddav (development iterations)
3527445db WIP: Carddav
d0aed59ed Module rename: projectforge-caldav → projectforge-carddav
8fc0c831f WIP: CardDav
27692785d CardDav (readonly) started; Milton library removed (license cost issue)
40e721b9c VCardUtils initial commit
685cfc9bf caldav: AddressCache renamed to AddressDAVCache
878cb7169 Compiler warning fixes
ab1776d2a CardDAV.AddressService: unused code removed
dc92ae34c CardDav: AddressCache implemented (vcard generation very slow, needed cache)
24b4d2e5f WIP: CalDAV (initial work on CardDAV)
562515f18 projectforge-caldav refactored; RestAuthenticationUtils introduced
Key Commit: dc92ae34c
This commit introduced the AddressDAVCache after discovering that vCard generation was "very slowly" — a critical performance fix. Generating vCards involves string manipulation, base64 image encoding, and date formatting for each contact. Without caching, every CardDAV sync request (which can happen every few minutes from mobile devices) would regenerate all vCards from scratch.
Key Commit: d0aed59ed — Module Rename
The module was originally named projectforge-caldav (Calendar + Contact together) but was renamed to projectforge-carddav when the CalDAV (calendar sync) and CardDAV (contact sync) implementations were separated into distinct modules. This reflects the evolution from a monolithic DAV module to separate, protocol-specific implementations.
Key Commit: 27692785d — Milton Removal
The initial implementation used the Milton Java WebDAV library, but it was removed due to license costs. The DAV protocol implementation was rewritten using a custom, lightweight approach. This is a common pain point in open-source WebDAV/CardDAV implementations — the available Java libraries (Milton, Bedework) either have restrictive licenses or insufficient CardDAV support.
Gotcha — Milton removal: The Milton library offered convenient annotations (@AddressBooks, @AddressBook) for CardDAV endpoint mapping. After its removal, the CardDAV endpoints had to be implemented manually using raw servlet handling or custom annotations. This increased implementation complexity for CardDAV protocol compliance (particularly addressbook-query REPORT requests and PROPFIND responses with addressbook-description properties).
Favorites-only policy rationale: CardDAV is primarily used for syncing contacts to mobile devices and desktop mail clients. Exposing the entire company directory (potentially thousands of contacts) to every device would be impractical. Users selectively favorite the contacts they need, creating a personalized subset. The deleteContact() method respects the multi-user nature of the address book by only removing the user's favorite marker, not the underlying address record.