Skip to content

Commit

Permalink
WIP: Carddav
Browse files Browse the repository at this point in the history
  • Loading branch information
kreinhard committed Dec 11, 2024
1 parent ef132a0 commit 167290b
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 86 deletions.
3 changes: 0 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,10 @@ org-hibernate-orm-core = { module = "org.hibernate.orm:hibernate-core", version.
org-hibernate-search-backend-lucene = { module = "org.hibernate.search:hibernate-search-backend-lucene", version.ref = "org-hibernate-search" }
org-hibernate-search-mapper-orm = { module = "org.hibernate.search:hibernate-search-mapper-orm", version.ref = "org-hibernate-search" }
org-hsqldb-hsqldb = { module = "org.hsqldb:hsqldb", version.ref = "org-hsqldb-hsqldb" }
org-jetbrains-intellij-deps-trove4j = { module = "org.jetbrains.intellij.deps:trove4j", version.ref = "org-jetbrains-intellij-deps-trove4j" }
org-jetbrains-kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-script-runtime = { module = "org.jetbrains.kotlin:kotlin-script-runtime", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-scripting-common = { module = "org.jetbrains.kotlin:kotlin-scripting-common", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-scripting-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-scripting-compiler-impl-embeddable = { module = "org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-scripting-jvm = { module = "org.jetbrains.kotlin:kotlin-scripting-jvm", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-scripting-jvm-host = { module = "org.jetbrains.kotlin:kotlin-scripting-jvm-host", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "org-jetbrains-kotlin" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,14 +276,17 @@ class PersonalAddressDao {
* @return the list of all PersonalAddressDO entries for the context user without any check access (addresses might be also deleted).
*/
get() {
val owner = requiredLoggedInUser
return persistenceService.executeNamedQuery(
PersonalAddressDO.FIND_FAVORITE_ADDRESS_IDS_BY_OWNER,
Long::class.java,
Pair("ownerId", owner.id)
)
return getFavoriteAddressIdList(requiredLoggedInUser)
}

fun getFavoriteAddressIdList(owner: PFUserDO): List<Long> {
return persistenceService.executeNamedQuery(
PersonalAddressDO.FIND_FAVORITE_ADDRESS_IDS_BY_OWNER,
Long::class.java,
Pair("ownerId", owner.id)
)
}

val personalAddressByAddressId: Map<Long, PersonalAddressDO>
/**
* @return the list of all address ids of personal address book for the context user (isFavorite() must be true).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ private val log = KotlinLogging.logger {}
class CardDavFilter : Filter {
private lateinit var springContext: WebApplicationContext

@Suppress("SpringJavaInjectionPointsAutowiringInspection")
@Autowired
private lateinit var cardDavService: CardDavService

Expand Down Expand Up @@ -125,7 +126,7 @@ class CardDavFilter : Filter {
return urlMatches(normalizedUri, "users", "principals")
}

"GET" -> {
"GET", "DELETE" -> {
log.debug { "GET call detected: $normalizedUri" }
// /carddav/users/admin/addressbooks/ProjectForge-129.vcf
return normalizedUri.matches(NORMALIZED_GET_REQUEST_REGEX)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ class CardDavService {
@Autowired
private lateinit var addressService: AddressService

@Autowired
private lateinit var deleteRequestHandler: DeleteRequestHandler

@Autowired
private lateinit var domainService: DomainService

Expand Down Expand Up @@ -136,6 +139,9 @@ class CardDavService {
// /carddav/users/admin/addressbooks/ProjectForge-129.vcf
writerContext.contactList = getContactList(userDO)
GetRequestHandler.handleGetCall(writerContext)
} else if (method == "DELETE") {
// /carddav/users/admin/addressbooks/ProjectForge-129.vcf
deleteRequestHandler.handleDeleteCall(writerContext)
} else {
log.warn { "Method not supported: $method" }
ResponseUtils.setValues(
Expand All @@ -146,9 +152,7 @@ class CardDavService {
}

private fun getContactList(userDO: PFUserDO): List<Contact> {
val user = User(userDO.username)
val addressBook = AddressBook(user)
return addressService.getContactList(addressBook)
return addressService.getContactList(userDO)
}

private fun authenticate(request: HttpServletRequest, response: HttpServletResponse): RestAuthenticationInfo {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
// www.projectforge.org
//
// Copyright (C) 2001-2024 Micromata GmbH, Germany (www.micromata.com)
//
// ProjectForge is dual-licensed.
//
// This community edition is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published
// by the Free Software Foundation; version 3 of the License.
//
// This community edition is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
// Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see http://www.gnu.org/licenses/.
//
/////////////////////////////////////////////////////////////////////////////

package org.projectforge.carddav

import mu.KotlinLogging
import org.projectforge.carddav.service.AddressService
import org.projectforge.rest.utils.ResponseUtils
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service

private val log = KotlinLogging.logger {}

@Service
internal class DeleteRequestHandler {
@Autowired
private lateinit var addressService: AddressService

/**
* Handles uri=/carddav/users/admin/addressbooks/ProjectForge-128.vcf, method=DELETE
* @param writerContext The writer context.
*/
fun handleDeleteCall(writerContext: WriterContext) {
val requestWrapper = writerContext.requestWrapper
val response = writerContext.response
val contactList = writerContext.contactList ?: emptyList()
log.debug { "handleDeleteCall: ${requestWrapper.request.method}: '${requestWrapper.requestURI}' body=[${requestWrapper.body}]" }
val requestedPath = requestWrapper.requestURI
val contactId = CardDavUtils.extractContactId(requestedPath)
val contact = contactList.find { it.id == contactId }
if (contactId == null) {
log.info { "Contact with id=$contactId not found in personal contact list. Can't delete it." }
ResponseUtils.setValues(response, content = "The resource does not exist.", status = HttpStatus.NOT_FOUND)
return
}
if (addressService.deleteContact(contactId, contact)) {
ResponseUtils.setValues(response, HttpStatus.NO_CONTENT)
} else {
ResponseUtils.setValues(response, content = "The resource does not exist.", status = HttpStatus.NOT_FOUND)
}
log.debug { "handleGetCall: response.content=[]" }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,16 @@

package org.projectforge.carddav

import jakarta.servlet.http.HttpServletResponse
import mu.KotlinLogging
import org.projectforge.carddav.model.Contact
import org.projectforge.rest.utils.ResponseUtils
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType

private val log = KotlinLogging.logger {}

internal object GetRequestHandler {
/**
* Handles /carddav/users/admin/addressbooks/ProjectForge-129.vcf, method=[GET]
* @param requestWrapper The request wrapper.
* @param response The response.
* Handles /carddav/users/admin/addressbooks/ProjectForge-129.vcf, method=GET
* @param writerContext The writer context.
*/
fun handleGetCall(writerContext: WriterContext) {
val requestWrapper = writerContext.requestWrapper
Expand All @@ -56,12 +52,7 @@ internal object GetRequestHandler {
// A unique identifier for this version of the resource. This allows clients to detect changes efficiently.
response.addHeader("ETag", contact.etag)
response.addHeader("Last-Modified", contact.lastModifiedAsHttpDate)
ResponseUtils.setValues(
response,
HttpStatus.OK,
contentType = "text/vcard",
content = content,
)
ResponseUtils.setValues(response, HttpStatus.OK, contentType = "text/vcard", content = content)
log.debug { "handleGetCall: response=${ResponseUtils.asJson(response)}, content=[$content]" }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal object PropFindRequestHandler {
log.debug { "handlePropFindCall: ${requestWrapper.request.method}: '${requestWrapper.requestURI}' body=[${requestWrapper.body}]" }
CardDavUtils.handleProps(requestWrapper, response) ?: return // No properties response is handled in handleProps.
val content = generatePropFindResponse(writerContext)
log.debug { "handlePropFindCall: response=[$content]" }
log.debug { "handlePropFindCall: response.content=[$content]" }
CardDavUtils.setMultiStatusResponse(response, content)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

package org.projectforge.carddav

import jakarta.servlet.http.HttpServletResponse
import mu.KotlinLogging
import org.projectforge.carddav.CardDavUtils.CARD
import org.projectforge.carddav.CardDavUtils.D
Expand All @@ -33,7 +32,6 @@ import org.projectforge.carddav.model.Contact
import org.projectforge.rest.utils.ResponseUtils
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import java.util.Date

private val log = KotlinLogging.logger {}

Expand All @@ -56,9 +54,7 @@ internal object ReportRequestHandler {
* </sync-collection>
* ```
*
* @param requestWrapper The request wrapper.
* @param response The response.
* @see CardDavXmlUtils.generatePropFindResponse
* @param writerContext The writer context.
*/
fun handleSyncReportCall(writerContext: WriterContext) {
val requestWrapper = writerContext.requestWrapper
Expand All @@ -72,7 +68,7 @@ internal object ReportRequestHandler {
if (rootElement == "sync-collection") {
val syncToken = props.find { it.type == PropType.SYNCTOKEN }?.value
val syncTokenMillis = CardDavUtils.getMillisFromSyncToken(syncToken)
val lastUpdated = CardDavUtils.getLastUpdated(contactList)?.time ?: 0L
val lastUpdated = CardDavUtils.getLastUpdated(contactList)?.time ?: 0L
val newSyncToken = CardDavUtils.getSyncToken()
if (syncTokenMillis != null && syncTokenMillis >= lastUpdated) {
// 1. No modifications since the last sync-token.
Expand Down Expand Up @@ -137,7 +133,7 @@ internal object ReportRequestHandler {
| <$D:href>${href}${CardDavUtils.getVcfFileName(contact)}</$D:href>
| <$D:propstat>
| <$D:prop>
| <$D:getetag>"${contact.etag}"</$D:getetag>
| <$D:getetag>${contact.etag}</$D:getetag>
""".trimMargin()
)
if (fullVCards) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ data class Contact(
/**
* A unique identifier for this version of the resource. This allows clients to detect changes efficiently.
* The hashcode of the vcard, embedded in quotes.
* Please note: the etag is embedded in quotes. Must be used in xml as well as in http response of GET call.
* @return The ETag.
*/
val etag: String by lazy {
vcardData?.let {
val digest = MessageDigest.getInstance("SHA-256")
val hashBytes = digest.digest(it.toByteArray())
hashBytes.joinToString("") { "%02x".format(it) }
"\"${hashBytes.joinToString("") { "%02x".format(it) }}\""
} ?: "\"null\""
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import org.projectforge.business.address.AddressDO
import org.projectforge.business.address.AddressDao
import org.projectforge.business.address.AddressImageDao
import org.projectforge.business.address.vcard.VCardUtils
import org.projectforge.carddav.model.AddressBook
import org.projectforge.carddav.model.Contact
import org.projectforge.framework.access.OperationType
import org.projectforge.framework.cache.AbstractCache
Expand All @@ -56,7 +55,7 @@ open class AddressDAVCache : AbstractCache(TICKS_PER_HOUR), BaseDOModifiedListen
return getCachedAddress(id)
}

fun getContacts(addressBook: AddressBook, ids: List<Long>): List<Contact> {
fun getContacts(ids: List<Long>): List<Contact> {
val result = mutableListOf<Contact>()
val missedInCache = mutableListOf<Long>()
ids.forEach {
Expand Down Expand Up @@ -116,7 +115,7 @@ open class AddressDAVCache : AbstractCache(TICKS_PER_HOUR), BaseDOModifiedListen
}

override fun refresh() {
org.projectforge.carddav.service.log.info("Clearing cache ${this::class.java.simpleName}.")
log.info("Clearing cache ${this::class.java.simpleName}.")
synchronized(contactMap) {
contactMap.clear()
}
Expand Down
Loading

0 comments on commit 167290b

Please sign in to comment.