DataTransferService.kt| Commit | Message |
|---|---|
9ebb88522 | 2016-07-18 Initial commit |
@Service
class DataTransferService : DataTransferInterface {
@PostConstruct
private fun postConstruct() {
dataTransferBridge.register(this) // "I'm a DataTransfer provider!"
}
}override fun getPersonalBoxOfUserLink(userId: Long): String {
val personalBox = dataTransferAreaDao.ensurePersonalBox(userId)
?: throw IllegalStateException("Personal box not found for user with ID $userId.")
return getDataTransferAreaLink(personalBox.id)
}ensurePersonalBox() — the "ensure" prefix is a ProjectForge convention: create if missing, return existing if found. This is idempotent — calling it twice for the same user returns the same box. The DAO likely checks the database for an existing personal box and creates one if none exists.
The Elvis operator ?:: Kotlin's null-safety operator. If ensurePersonalBox returns null (unexpected — it should always succeed), the IllegalStateException fires. The exception message includes the user ID for debugging — a log grepper can find which user caused the failure.
The returned link points to DataTransferPageRest — a REST endpoint. The link is used in email notifications: "Your invoice has been generated. Download it here: https://projectforge.acme.com/rs/datatransfer/..."
override fun putFileInUsersInBox(
receiver: PFUserDO, // Who gets the file
filename: String, // Display name
content: ByteArray, // Raw file bytes
description: String? // Optional description
): Boolean { // Returns true on success, false on failure (no throw!)
val personalBox = dataTransferAreaDao.ensurePersonalBox(receiver.id)
attachmentsService.addAttachment(
dataTransferAreaPagesRest.jcrPath!!, // JCR node path for storage
fileInfo = FileInfo(filename, content.size.toLong(), description),
content = content,
baseDao = dataTransferAreaDao, // DAO for access control
obj = personalBox, // The entity being attached to
accessChecker = dataTransferAreaPagesRest.attachmentsAccessChecker
)
return true
} catch (ex: Exception) {
log.error("Can't put document '${filename}' of size ...", ex)
return false // Graceful failure — caller handles it
}
}attachmentsService.addAttachment() — the bridge to JCR (Java Content Repository). Files aren't stored as database BLOBs. They go into Apache Jackrabbit — a hierarchical content repository that handles versioning, access control, and binary storage. The JCR path comes from dataTransferAreaPagesRest.jcrPath — each data transfer area has its own node in the JCR tree.
The 6 parameters to addAttachment() reveal the attachment system's contract:
| Parameter | Purpose |
|---|---|
jcrPath | JCR node path — where in the content tree to store |
fileInfo | Metadata: filename, size, description. Wrapped in a FileInfo data class. |
content | Raw bytes — the actual file. ByteArray means the entire file is in memory. Large files (>100MB) would cause OOM. |
baseDao | The DAO responsible for the entity — used for history tracking and access control |
obj | The owning entity — the personal box that "owns" this file |
accessChecker | Determines who can read/write/delete this attachment |
Return type Boolean instead of Unit: This is a deliberate design choice. The method returns true on success, false on failure. It NEVER throws (the catch block catches Exception — the broadest possible). The caller can decide how to handle failure: show an error message, retry, or silently continue.
Logging with formatBytes(): content.size.formatBytes() is a Kotlin extension function (imported from org.projectforge.common.extensions). It converts raw byte counts to human-readable format ("1.5 MB", "340 KB"). Used in the log message: "Can't put document 'invoice.pdf' of size 1.5 MB into user 'Kai Reinhard' personal box" — this is actionable for support debugging.
The huge !! on line 94: dataTransferAreaPagesRest.jcrPath!! — this will throw NullPointerException if jcrPath is null. The !! operator means "I'm certain this is never null." If it IS null, the NPE is caught by the catch (Exception) block and logged. But ideally, this should have a proper null check with a descriptive error message.
| Feature | Line | What it does |
|---|---|---|
lateinit var | 46-58 | Spring-injected dependencies declared as lateinit — initialized after construction by Spring, guaranteed non-null after @PostConstruct |
by lazy | 60 | val jcrPath is computed on first access, then cached. Delegates to dataTransferAreaPagesRest.jcrPath. The !! on the delegate means the lazy val itself can throw NPE. |
private val log | 41 | KotlinLogging.logger {} from the mu library — creates a logger with the class name automatically. No need to pass DataTransferService::class.java. |
Elvis ?: | 69,91 | Null-coalescing — if left side is null, throw the exception on the right |
| String templates | 69,91,105,109 | "user with ID $userId" — Kotlin string interpolation. Variables and expressions inside ${} are evaluated inline. |
This service is the bridge between three subsystems:
dataTransferBridge.getProviders() → find DataTransferInterface → call putFileInUsersInBox(). They don't know about JCR, personal boxes, or access control.attachmentsService.addAttachment() stores files in Apache Jackrabbit. The JCR handles versioning, access control, and binary storage. Files aren't in the database.DataTransferPageRest and DataTransferAreaPagesRest expose download/upload endpoints. The getDataTransferAreaLink() method generates URLs for these endpoints.The design pattern is a facade: complex JCR operations are hidden behind a simple putFileInUsersInBox() interface.
This is the plugin self-registration pattern. When Spring creates this bean,
@PostConstructfires and the service registers itself withDataTransferBridge. The bridge maintains a collection of allDataTransferInterfaceimplementations. Other parts of ProjectForge (report generators, script executors, invoice exporters) query the bridge to find available data transfer providers — they don't need to know about this specific plugin. This is the Observer pattern applied to plugin discovery: plugins register themselves; consumers discover them through the bridge.Why not just @Autowired? Because there might be MULTIPLE data transfer providers (different plugins). The bridge pattern allows consumers to iterate over all providers or select one by criteria, whereas
@Autowiredwith a list would couple the consumer to Spring's injection mechanism.