diff --git a/ToDo.adoc b/ToDo.adoc
index 65af4a55d9..416c9c74ea 100644
--- a/ToDo.adoc
+++ b/ToDo.adoc
@@ -1,6 +1,6 @@
Aktuell:
- Scripting: Ergebnis Unresolved reference 'memo', 'todo'.: line 94 to 94 (add only activated plugins)
-
+- JsonValidatorTest anpassen.
- Groovy-scripts: remove or fix.
- AG-Grid: setColumnStates wird nicht in den UserPrefs gespeichert.
- Wicket: Auftragsbuch: org.apache.wicket.core.request.mapper.StalePageException: A request to page '[Page class = org.projectforge.web.fibu.AuftragEditPage, id = 9, render count = 3]' has been made with stale 'renderCount'. The page will be re-rendered.
diff --git a/plugins/org.projectforge.plugins.banking/src/main/kotlin/org/projectforge/plugins/banking/BankAccountBalanceDO.kt b/plugins/org.projectforge.plugins.banking/src/main/kotlin/org/projectforge/plugins/banking/BankAccountBalanceDO.kt
index d4c7eae1dd..f23006d6a1 100644
--- a/plugins/org.projectforge.plugins.banking/src/main/kotlin/org/projectforge/plugins/banking/BankAccountBalanceDO.kt
+++ b/plugins/org.projectforge.plugins.banking/src/main/kotlin/org/projectforge/plugins/banking/BankAccountBalanceDO.kt
@@ -23,12 +23,14 @@
package org.projectforge.plugins.banking
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
import org.projectforge.Constants
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.common.props.PropertyType
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import java.math.BigDecimal
import java.time.LocalDate
@@ -53,6 +55,7 @@ open class BankAccountBalanceDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "banking_account_fk", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var bankAccount: BankAccountDO? = null
@PropertyInfo(i18nKey = "plugins.banking.account.record.amount", type = PropertyType.CURRENCY)
diff --git a/plugins/org.projectforge.plugins.banking/src/main/kotlin/org/projectforge/plugins/banking/BankAccountRecordDO.kt b/plugins/org.projectforge.plugins.banking/src/main/kotlin/org/projectforge/plugins/banking/BankAccountRecordDO.kt
index cc54d48c31..5e6269ee61 100644
--- a/plugins/org.projectforge.plugins.banking/src/main/kotlin/org/projectforge/plugins/banking/BankAccountRecordDO.kt
+++ b/plugins/org.projectforge.plugins.banking/src/main/kotlin/org/projectforge/plugins/banking/BankAccountRecordDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.plugins.banking
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.apache.commons.codec.digest.DigestUtils
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
@@ -31,6 +32,7 @@ import org.projectforge.Constants
import org.projectforge.common.StringHelper
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.common.props.PropertyType
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.time.PFDay
import java.math.BigDecimal
@@ -56,6 +58,7 @@ open class BankAccountRecordDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "banking_account_fk", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var bankAccount: BankAccountDO? = null
@PropertyInfo(i18nKey = "plugins.banking.account.record.amount", type = PropertyType.CURRENCY)
diff --git a/plugins/org.projectforge.plugins.datatransfer/src/main/kotlin/org/projectforge/plugins/datatransfer/DataTransferAuditDO.kt b/plugins/org.projectforge.plugins.datatransfer/src/main/kotlin/org/projectforge/plugins/datatransfer/DataTransferAuditDO.kt
index bba79f9728..6d1216f46b 100644
--- a/plugins/org.projectforge.plugins.datatransfer/src/main/kotlin/org/projectforge/plugins/datatransfer/DataTransferAuditDO.kt
+++ b/plugins/org.projectforge.plugins.datatransfer/src/main/kotlin/org/projectforge/plugins/datatransfer/DataTransferAuditDO.kt
@@ -24,6 +24,7 @@
package org.projectforge.plugins.datatransfer
import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed
import org.projectforge.Constants
import org.projectforge.framework.i18n.TimeAgo
@@ -32,6 +33,7 @@ import org.projectforge.framework.jcr.AttachmentsEventType
import org.projectforge.framework.persistence.user.entities.PFUserDO
import java.util.*
import jakarta.persistence.*
+import org.projectforge.framework.json.IdOnlySerializer
/**
* @author Kai Reinhard (k.reinhard@micromata.de)
@@ -84,6 +86,7 @@ open class DataTransferAuditDO {
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "by_user_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var byUser: PFUserDO? = null
@get:Column(name = "by_external_user", length = 4000)
@@ -109,6 +112,7 @@ open class DataTransferAuditDO {
*/
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "upload_by_user_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var uploadByUser: PFUserDO? = null
@get:Column(length = 1000)
diff --git a/plugins/org.projectforge.plugins.marketing/src/main/kotlin/org/projectforge/plugins/marketing/AddressCampaignValueDO.kt b/plugins/org.projectforge.plugins.marketing/src/main/kotlin/org/projectforge/plugins/marketing/AddressCampaignValueDO.kt
index 403f40b21a..1a1c1b17be 100644
--- a/plugins/org.projectforge.plugins.marketing/src/main/kotlin/org/projectforge/plugins/marketing/AddressCampaignValueDO.kt
+++ b/plugins/org.projectforge.plugins.marketing/src/main/kotlin/org/projectforge/plugins/marketing/AddressCampaignValueDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.plugins.marketing
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmbedded
import org.projectforge.business.address.AddressDO
@@ -32,6 +33,7 @@ import org.projectforge.framework.persistence.entities.DefaultBaseDO
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency
+import org.projectforge.framework.json.IdOnlySerializer
/**
* A marketing campaign.
@@ -68,12 +70,14 @@ open class AddressCampaignValueDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "address_campaign_fk", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var addressCampaign: AddressCampaignDO? = null
@IndexedEmbedded(includeDepth = 1)
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "address_fk", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var address: AddressDO? = null
@PropertyInfo(i18nKey = "value")
diff --git a/plugins/org.projectforge.plugins.memo/src/main/kotlin/org/projectforge/plugins/memo/MemoDO.kt b/plugins/org.projectforge.plugins.memo/src/main/kotlin/org/projectforge/plugins/memo/MemoDO.kt
index 9425ec5d2f..d9957fd895 100644
--- a/plugins/org.projectforge.plugins.memo/src/main/kotlin/org/projectforge/plugins/memo/MemoDO.kt
+++ b/plugins/org.projectforge.plugins.memo/src/main/kotlin/org/projectforge/plugins/memo/MemoDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.plugins.memo
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.Constants
@@ -30,6 +31,7 @@ import org.projectforge.framework.persistence.entities.AbstractBaseDO
import org.projectforge.framework.persistence.user.entities.PFUserDO
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField
+import org.projectforge.framework.json.IdOnlySerializer
/**
* This data object is the Java representation of a data-base entry of a memo.
@@ -59,6 +61,7 @@ open class MemoDO : AbstractBaseDO() {
@PropertyInfo(i18nKey = "plugins.memo.owner")
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "owner_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var owner: PFUserDO? = null
@PropertyInfo(i18nKey = "plugins.memo.memo")
diff --git a/plugins/org.projectforge.plugins.skillmatrix/src/main/kotlin/org/projectforge/plugins/skillmatrix/SkillEntryDO.kt b/plugins/org.projectforge.plugins.skillmatrix/src/main/kotlin/org/projectforge/plugins/skillmatrix/SkillEntryDO.kt
index d68e38ab89..cec9558eb5 100644
--- a/plugins/org.projectforge.plugins.skillmatrix/src/main/kotlin/org/projectforge/plugins/skillmatrix/SkillEntryDO.kt
+++ b/plugins/org.projectforge.plugins.skillmatrix/src/main/kotlin/org/projectforge/plugins/skillmatrix/SkillEntryDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.plugins.skillmatrix
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.projectforge.common.StringHelper
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.Constants
@@ -31,6 +32,7 @@ import org.projectforge.framework.persistence.user.entities.PFUserDO
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
+import org.projectforge.framework.json.IdOnlySerializer
/**
* @author Kai Reinhard (k.reinhard@micromata.de)
@@ -77,6 +79,7 @@ open class SkillEntryDO : AbstractBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "owner_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var owner: PFUserDO? = null
/**
diff --git a/plugins/org.projectforge.plugins.todo/src/main/kotlin/org/projectforge/plugins/todo/ToDoDO.kt b/plugins/org.projectforge.plugins.todo/src/main/kotlin/org/projectforge/plugins/todo/ToDoDO.kt
index cc58205f14..2761ea64e9 100644
--- a/plugins/org.projectforge.plugins.todo/src/main/kotlin/org/projectforge/plugins/todo/ToDoDO.kt
+++ b/plugins/org.projectforge.plugins.todo/src/main/kotlin/org/projectforge/plugins/todo/ToDoDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.plugins.todo
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.projectforge.business.task.TaskDO
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.common.i18n.Priority
@@ -35,6 +36,7 @@ import java.time.LocalDate
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.history.NoHistory
/**
@@ -57,6 +59,7 @@ open class ToDoDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "reporter_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var reporter: PFUserDO? = null
/**
@@ -69,6 +72,7 @@ open class ToDoDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "assignee_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var assignee: PFUserDO? = null
@PropertyInfo(i18nKey = "task")
@@ -77,6 +81,7 @@ open class ToDoDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "task_id", nullable = true)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var task: TaskDO? = null
/**
@@ -88,6 +93,7 @@ open class ToDoDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "group_id", nullable = true)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var group: GroupDO? = null
@PropertyInfo(i18nKey = "description")
diff --git a/projectforge-application/src/main/resources/i18nKeys.json b/projectforge-application/src/main/resources/i18nKeys.json
index 87909f6189..df93f0b96b 100644
--- a/projectforge-application/src/main/resources/i18nKeys.json
+++ b/projectforge-application/src/main/resources/i18nKeys.json
@@ -1372,7 +1372,7 @@
{"i18nKey":"hr.planning.weekend","bundleName":"I18nResources","translation":"Week-end","translationDE":"Wochenende","usedInClasses":["org.projectforge.business.humanresources.HRPlanningEntryDO","org.projectforge.web.humanresources.HRPlanningListPage"],"usedInFiles":[]},
{"i18nKey":"hr.planning.workdays","bundleName":"I18nResources","translation":"Workdays","translationDE":"Arbeitstage","usedInClasses":[],"usedInFiles":[]},
{"i18nKey":"ibanvalidator.wronglength.de","bundleName":"I18nResources","translation":"The field ''${label}'' starts with ''DE'', but a german IBAN must have 22 characters.","translationDE":"Das Feld ''${label}'' beginnt mit ''DE''. Eine deutsche IBAN muss jedoch aus 22 Zeichen bestehen.","usedInClasses":["org.projectforge.web.common.IbanValidator"],"usedInFiles":[]},
- {"i18nKey":"id","bundleName":"I18nResources","translation":"Id","translationDE":"Id","usedInClasses":["org.apache.batik.util.XMLConstants","org.projectforge.business.address.AddressExport","org.projectforge.business.address.PersonalAddressDao","org.projectforge.business.book.BookDao","org.projectforge.business.fibu.AuftragDao","org.projectforge.business.fibu.AuftragsCacheService","org.projectforge.business.fibu.EingangsrechnungDO","org.projectforge.business.fibu.EingangsrechnungsPositionDO","org.projectforge.business.fibu.EmployeeDO","org.projectforge.business.fibu.EmployeeSalaryDao","org.projectforge.business.fibu.EmployeeServiceSupport","org.projectforge.business.fibu.InvoiceService","org.projectforge.business.fibu.ProjektDO","org.projectforge.business.fibu.RechnungDO","org.projectforge.business.fibu.RechnungDao","org.projectforge.business.fibu.RechnungService","org.projectforge.business.fibu.RechnungsPositionDO","org.projectforge.business.fibu.kost.Kost1DO","org.projectforge.business.fibu.kost.Kost1Dao","org.projectforge.business.fibu.kost.Kost2ArtDao","org.projectforge.business.fibu.kost.Kost2DO","org.projectforge.business.fibu.kost.Kost2Dao","org.projectforge.business.fibu.kost.KostZuweisungDO","org.projectforge.business.gantt.GanttChart","org.projectforge.business.gantt.GanttChartDao","org.projectforge.business.gantt.GanttTaskImpl","org.projectforge.business.humanresources.HRPlanningDO","org.projectforge.business.humanresources.HRPlanningDao","org.projectforge.business.humanresources.HRPlanningEntryDO","org.projectforge.business.orga.ContractDao","org.projectforge.business.orga.VisitorbookDO","org.projectforge.business.orga.VisitorbookEntryDO","org.projectforge.business.task.TaskDao","org.projectforge.business.task.TaskNode","org.projectforge.business.task.formatter.WicketTaskFormatter","org.projectforge.business.timesheet.TimesheetDao","org.projectforge.business.timesheet.TimesheetExport","org.projectforge.business.user.GroupDao","org.projectforge.business.user.UserDao","org.projectforge.business.user.UserPrefDao","org.projectforge.business.vacation.model.VacationDO","org.projectforge.excel.ExcelUtils","org.projectforge.framework.ToStringUtil","org.projectforge.framework.access.AccessDao","org.projectforge.framework.access.AccessEntryDO","org.projectforge.framework.access.GroupTaskAccessDO","org.projectforge.framework.jobs.AbstractJob","org.projectforge.framework.json.HibernateProxySerializer","org.projectforge.framework.persistence.api.BaseDao","org.projectforge.framework.persistence.candh.CandHMaster","org.projectforge.framework.persistence.database.DatabaseService","org.projectforge.framework.persistence.database.ReindexerRegistry","org.projectforge.framework.persistence.database.ReindexerStrategy","org.projectforge.framework.persistence.entities.DefaultBaseDO","org.projectforge.framework.persistence.jpa.PfPersistenceContext","org.projectforge.framework.persistence.search.HibernateSearchDependentObjectsReindexer","org.projectforge.framework.persistence.user.entities.PFUserDO","org.projectforge.framework.persistence.user.entities.UserPrefEntryDO","org.projectforge.framework.persistence.user.entities.UserRightDO","org.projectforge.framework.persistence.xstream.ProxyIdRefMarshaller","org.projectforge.menu.builder.FavoritesMenuReaderWriter","org.projectforge.plugins.banking.BankingServicesRest","org.projectforge.plugins.datatransfer.DataTransferAreaDO","org.projectforge.plugins.datatransfer.rest.DataTransferAuditPageRest","org.projectforge.plugins.datatransfer.rest.DataTransferPageRest","org.projectforge.plugins.datatransfer.restPublic.DataTransferPublicAttachmentPageRest","org.projectforge.plugins.datatransfer.restPublic.DataTransferPublicPageRest","org.projectforge.plugins.datatransfer.restPublic.DataTransferPublicServicesRest","org.projectforge.plugins.ihk.IHKExporter","org.projectforge.plugins.memo.MemoDO","org.projectforge.plugins.merlin.MerlinTemplateDO","org.projectforge.plugins.merlin.rest.MerlinExecutionPageRest","org.projectforge.plugins.merlin.rest.MerlinVariablePageRest","org.projectforge.plugins.skillmatrix.SkillEntryDO","org.projectforge.rest.AddressImageServicesRest","org.projectforge.rest.AddressServicesRest","org.projectforge.rest.AddressViewPageRest","org.projectforge.rest.AttachmentPageRest","org.projectforge.rest.AttachmentsServicesRest","org.projectforge.rest.TimesheetFavoritesRest","org.projectforge.rest.TimesheetMultiSelectedPageRest","org.projectforge.rest.TimesheetPagesRest","org.projectforge.rest.VacationAccountPageRest","org.projectforge.rest.admin.LogViewerPageRest","org.projectforge.rest.calendar.CalendarFilterServicesRest","org.projectforge.rest.calendar.CalendarSettingsPageRest","org.projectforge.rest.calendar.TeamEventPagesRest","org.projectforge.rest.config.IdObjectDeserializer","org.projectforge.rest.config.JacksonConfiguration","org.projectforge.rest.core.AbstractPagesRest","org.projectforge.rest.dvelop.DvelopClient","org.projectforge.rest.fibu.EmployeeValidSinceAttrPageRest","org.projectforge.rest.fibu.kost.Kost2ArtPagesRest","org.projectforge.rest.importer.AbstractImportPageRest","org.projectforge.rest.json.UISelectTypeSerializer","org.projectforge.rest.my2fa.My2FAServicesRest","org.projectforge.rest.my2fa.My2FASetupPageRest","org.projectforge.rest.my2fa.WebAuthnEntryPageRest","org.projectforge.rest.orga.VisitorbookEntryPageRest","org.projectforge.rest.orga.VisitorbookPagesRest","org.projectforge.rest.poll.PollPageRest","org.projectforge.rest.scripting.MyScriptExecutePageRest","org.projectforge.rest.scripting.ScriptExecutePageRest","org.projectforge.rest.scripting.ScriptPagesRest","org.projectforge.rest.task.TaskFavoritesRest","org.projectforge.rest.task.TaskServicesRest","org.projectforge.security.dto.WebAuthnPublicKeyCredentialCreationOptions","org.projectforge.security.webauthn.WebAuthnEntryDao","org.projectforge.ui.UISelect","org.projectforge.web.OrphanedLinkFilter","org.projectforge.web.fibu.EingangsrechnungEditPage","org.projectforge.web.fibu.Kost2ArtEditForm","org.projectforge.web.fibu.Kost2ArtListPage","org.projectforge.web.fibu.RechnungEditPage","org.projectforge.web.gantt.GanttTreeTableNode","org.projectforge.web.user.NewGroupSelectPanel","org.projectforge.web.wicket.AbstractEditPage","org.projectforge.web.wicket.components.TabPanel"],"usedInFiles":["./projectforge-rest/src/main/kotlin/org/projectforge/rest/json/Deserializers.kt"]},
+ {"i18nKey":"id","bundleName":"I18nResources","translation":"Id","translationDE":"Id","usedInClasses":["org.apache.batik.util.XMLConstants","org.projectforge.business.address.AddressExport","org.projectforge.business.address.PersonalAddressDao","org.projectforge.business.book.BookDao","org.projectforge.business.fibu.AuftragDao","org.projectforge.business.fibu.AuftragsCacheService","org.projectforge.business.fibu.EingangsrechnungsPositionDO","org.projectforge.business.fibu.EmployeeSalaryDao","org.projectforge.business.fibu.EmployeeServiceSupport","org.projectforge.business.fibu.InvoiceService","org.projectforge.business.fibu.ProjektDO","org.projectforge.business.fibu.RechnungDao","org.projectforge.business.fibu.RechnungService","org.projectforge.business.fibu.RechnungsPositionDO","org.projectforge.business.fibu.kost.Kost1DO","org.projectforge.business.fibu.kost.Kost1Dao","org.projectforge.business.fibu.kost.Kost2ArtDao","org.projectforge.business.fibu.kost.Kost2DO","org.projectforge.business.fibu.kost.Kost2Dao","org.projectforge.business.fibu.kost.KostZuweisungDO","org.projectforge.business.gantt.GanttChart","org.projectforge.business.gantt.GanttChartDao","org.projectforge.business.gantt.GanttTaskImpl","org.projectforge.business.humanresources.HRPlanningDao","org.projectforge.business.humanresources.HRPlanningEntryDO","org.projectforge.business.orga.ContractDao","org.projectforge.business.orga.VisitorbookDO","org.projectforge.business.task.TaskDao","org.projectforge.business.task.TaskNode","org.projectforge.business.task.formatter.WicketTaskFormatter","org.projectforge.business.timesheet.TimesheetDao","org.projectforge.business.timesheet.TimesheetExport","org.projectforge.business.user.GroupDao","org.projectforge.business.user.UserDao","org.projectforge.business.user.UserPrefDao","org.projectforge.business.vacation.model.VacationDO","org.projectforge.excel.ExcelUtils","org.projectforge.framework.ToStringUtil","org.projectforge.framework.access.AccessDao","org.projectforge.framework.access.AccessEntryDO","org.projectforge.framework.access.GroupTaskAccessDO","org.projectforge.framework.jobs.AbstractJob","org.projectforge.framework.json.HibernateProxySerializer","org.projectforge.framework.json.IdOnlySerializer","org.projectforge.framework.persistence.api.BaseDao","org.projectforge.framework.persistence.candh.CandHMaster","org.projectforge.framework.persistence.database.DatabaseService","org.projectforge.framework.persistence.database.ReindexerRegistry","org.projectforge.framework.persistence.database.ReindexerStrategy","org.projectforge.framework.persistence.entities.DefaultBaseDO","org.projectforge.framework.persistence.jpa.PfPersistenceContext","org.projectforge.framework.persistence.search.HibernateSearchDependentObjectsReindexer","org.projectforge.framework.persistence.user.entities.UserPrefEntryDO","org.projectforge.framework.persistence.user.entities.UserRightDO","org.projectforge.framework.persistence.xstream.ProxyIdRefMarshaller","org.projectforge.menu.builder.FavoritesMenuReaderWriter","org.projectforge.plugins.banking.BankingServicesRest","org.projectforge.plugins.datatransfer.DataTransferAreaDO","org.projectforge.plugins.datatransfer.rest.DataTransferAuditPageRest","org.projectforge.plugins.datatransfer.rest.DataTransferPageRest","org.projectforge.plugins.datatransfer.restPublic.DataTransferPublicAttachmentPageRest","org.projectforge.plugins.datatransfer.restPublic.DataTransferPublicPageRest","org.projectforge.plugins.datatransfer.restPublic.DataTransferPublicServicesRest","org.projectforge.plugins.ihk.IHKExporter","org.projectforge.plugins.memo.MemoDO","org.projectforge.plugins.merlin.MerlinTemplateDO","org.projectforge.plugins.merlin.rest.MerlinExecutionPageRest","org.projectforge.plugins.merlin.rest.MerlinVariablePageRest","org.projectforge.plugins.skillmatrix.SkillEntryDO","org.projectforge.rest.AddressImageServicesRest","org.projectforge.rest.AddressServicesRest","org.projectforge.rest.AddressViewPageRest","org.projectforge.rest.AttachmentPageRest","org.projectforge.rest.AttachmentsServicesRest","org.projectforge.rest.TimesheetFavoritesRest","org.projectforge.rest.TimesheetMultiSelectedPageRest","org.projectforge.rest.TimesheetPagesRest","org.projectforge.rest.VacationAccountPageRest","org.projectforge.rest.admin.LogViewerPageRest","org.projectforge.rest.calendar.CalendarFilterServicesRest","org.projectforge.rest.calendar.CalendarSettingsPageRest","org.projectforge.rest.calendar.TeamEventPagesRest","org.projectforge.rest.config.IdObjectDeserializer","org.projectforge.rest.config.JacksonConfiguration","org.projectforge.rest.core.AbstractPagesRest","org.projectforge.rest.dvelop.DvelopClient","org.projectforge.rest.fibu.EmployeeValidSinceAttrPageRest","org.projectforge.rest.fibu.kost.Kost2ArtPagesRest","org.projectforge.rest.importer.AbstractImportPageRest","org.projectforge.rest.json.UISelectTypeSerializer","org.projectforge.rest.my2fa.My2FAServicesRest","org.projectforge.rest.my2fa.My2FASetupPageRest","org.projectforge.rest.my2fa.WebAuthnEntryPageRest","org.projectforge.rest.orga.VisitorbookEntryPageRest","org.projectforge.rest.orga.VisitorbookPagesRest","org.projectforge.rest.poll.PollPageRest","org.projectforge.rest.scripting.MyScriptExecutePageRest","org.projectforge.rest.scripting.ScriptExecutePageRest","org.projectforge.rest.scripting.ScriptPagesRest","org.projectforge.rest.task.TaskFavoritesRest","org.projectforge.rest.task.TaskServicesRest","org.projectforge.security.dto.WebAuthnPublicKeyCredentialCreationOptions","org.projectforge.security.webauthn.WebAuthnEntryDao","org.projectforge.ui.UISelect","org.projectforge.web.OrphanedLinkFilter","org.projectforge.web.fibu.EingangsrechnungEditPage","org.projectforge.web.fibu.Kost2ArtEditForm","org.projectforge.web.fibu.Kost2ArtListPage","org.projectforge.web.fibu.RechnungEditPage","org.projectforge.web.gantt.GanttTreeTableNode","org.projectforge.web.user.NewGroupSelectPanel","org.projectforge.web.wicket.AbstractEditPage","org.projectforge.web.wicket.components.TabPanel"],"usedInFiles":["./projectforge-rest/src/main/kotlin/org/projectforge/rest/json/Deserializers.kt"]},
{"i18nKey":"imageFile","bundleName":"I18nResources","translation":"Image","translationDE":"Bild","usedInClasses":[],"usedInFiles":[]},
{"i18nKey":"import","bundleName":"I18nResources","translation":"Import","translationDE":"Importieren","usedInClasses":["org.projectforge.business.scripting.KotlinScriptExecutor","org.projectforge.rest.importer.AbstractImportPageRest","org.projectforge.web.admin.SetupImportForm","org.projectforge.web.fibu.ReportObjectivesForm","org.projectforge.web.teamcal.admin.TeamCalEditPage"],"usedInFiles":["./projectforge-wicket/src/main/java/org/projectforge/web/admin/SetupPage.html"]},
{"i18nKey":"import.confirmMessage","bundleName":"I18nResources","translation":"Would you like to import the selected entries now? This option isn't undoable.","translationDE":"Sollen nun alle ausgewählten Einträge importiert werden? Diese Aktion kann nicht rückgängig gemacht werden.","usedInClasses":["org.projectforge.rest.importer.AbstractImportPageRest"],"usedInFiles":[]},
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressDO.kt
index 79909eafff..10a740ab4c 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.business.address
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import mu.KotlinLogging
import org.apache.commons.lang3.StringUtils
@@ -35,6 +36,7 @@ import org.projectforge.common.StringHelper
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.framework.DisplayNameCapable
import org.projectforge.framework.i18n.translate
+import org.projectforge.framework.json.IdsOnlySerializer
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.persistence.user.api.ThreadLocalUserContext
import org.projectforge.framework.utils.LabelValueBean
@@ -293,6 +295,7 @@ open class AddressDO : DefaultBaseDO(), DisplayNameCapable {
columnList = "addressbook_id"
)]
)
+ @JsonSerialize(using = IdsOnlySerializer::class)
open var addressbookList: MutableSet? = null
fun add(addressbook: AddressbookDO) {
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressImageDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressImageDO.kt
index 14f3402cf3..778dd1ecb4 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressImageDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressImageDO.kt
@@ -23,7 +23,9 @@
package org.projectforge.business.address
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.api.IdObject
import java.util.Date
@@ -61,6 +63,7 @@ open class AddressImageDO : IdObject {
@get:OneToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "address_fk", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var address: AddressDO? = null
@get:Column(columnDefinition = "BLOB")
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressbookDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressbookDO.kt
index 761a68fb40..380c17acaa 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressbookDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressbookDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.business.address
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.apache.commons.lang3.builder.HashCodeBuilder
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
@@ -33,6 +34,7 @@ import org.projectforge.business.common.BaseUserGroupRightsDO
import org.projectforge.business.teamcal.admin.model.HibernateSearchUsersGroupsTypeBinder
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.framework.DisplayNameCapable
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.search.ClassBridge
import org.projectforge.framework.persistence.user.entities.PFUserDO
import java.util.*
@@ -62,6 +64,7 @@ open class AddressbookDO : BaseUserGroupRightsDO(), DisplayNameCapable {
@get:ManyToOne(fetch = FetchType.LAZY)
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:JoinColumn(name = "owner_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
override var owner: PFUserDO? = null
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/PersonalAddressDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/PersonalAddressDO.kt
index 9557267792..39c6817e26 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/PersonalAddressDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/PersonalAddressDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.business.address
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed
import org.projectforge.business.address.PersonalAddressDO.Companion.DELETE_ALL_BY_ADDRESS_ID
import org.projectforge.business.address.PersonalAddressDO.Companion.FIND_BY_OWNER
@@ -33,6 +34,7 @@ import org.projectforge.business.address.PersonalAddressDO.Companion.FIND_JOINED
import org.projectforge.framework.persistence.entities.AbstractBaseDO
import org.projectforge.framework.persistence.user.entities.PFUserDO
import jakarta.persistence.*
+import org.projectforge.framework.json.IdOnlySerializer
/**
* Every user has his own address book (a subset of all addresses). For every address a user can define which phone
@@ -86,6 +88,7 @@ class PersonalAddressDO : AbstractBaseDO() {
*/
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "address_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
var address: AddressDO? = null
/**
@@ -99,6 +102,7 @@ class PersonalAddressDO : AbstractBaseDO() {
*/
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "owner_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
var owner: PFUserDO? = null
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/book/BookDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/book/BookDO.kt
index d1e2ee7792..5606b094ff 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/book/BookDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/book/BookDO.kt
@@ -24,6 +24,7 @@
package org.projectforge.business.book
import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.apache.commons.lang3.StringUtils
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
@@ -36,6 +37,7 @@ import java.time.LocalDate
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.history.NoHistory
/**
@@ -73,6 +75,7 @@ open class BookDO : DefaultBaseDO(), DisplayNameCapable, AttachmentsInfo {
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:Fetch(FetchMode.SELECT)
@get:JoinColumn(name = "lend_out_by")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var lendOutBy: PFUserDO? = null
@PropertyInfo(i18nKey = "date")
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragDO.kt
index 2ee2139ca7..72a60d10e5 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragDO.kt
@@ -24,6 +24,7 @@
package org.projectforge.business.fibu
import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.apache.commons.lang3.StringUtils
import org.hibernate.annotations.ListIndexBase
@@ -34,6 +35,8 @@ import org.projectforge.common.anots.PropertyInfo
import org.projectforge.framework.DisplayNameCapable
import org.projectforge.framework.i18n.I18nHelper
import org.projectforge.framework.jcr.AttachmentsInfo
+import org.projectforge.framework.json.IdOnlySerializer
+import org.projectforge.framework.json.IdsOnlySerializer
import org.projectforge.framework.persistence.candh.CandHIgnore
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.persistence.history.NoHistory
@@ -136,6 +139,7 @@ open class AuftragDO : DefaultBaseDO(), DisplayNameCapable, AttachmentsInfo {
@get:ManyToOne(fetch = FetchType.LAZY)
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:JoinColumn(name = "contact_person_fk", nullable = true)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var contactPerson: PFUserDO? = null
@PropertyInfo(i18nKey = "fibu.kunde")
@@ -143,6 +147,7 @@ open class AuftragDO : DefaultBaseDO(), DisplayNameCapable, AttachmentsInfo {
@get:ManyToOne(fetch = FetchType.LAZY)
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:JoinColumn(name = "kunde_fk", nullable = true)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var kunde: KundeDO? = null
/**
@@ -158,6 +163,7 @@ open class AuftragDO : DefaultBaseDO(), DisplayNameCapable, AttachmentsInfo {
@get:ManyToOne(fetch = FetchType.LAZY)
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:JoinColumn(name = "projekt_fk", nullable = true)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var projekt: ProjektDO? = null
@PropertyInfo(i18nKey = "fibu.auftrag.title")
@@ -254,6 +260,7 @@ open class AuftragDO : DefaultBaseDO(), DisplayNameCapable, AttachmentsInfo {
@get:ManyToOne(fetch = FetchType.LAZY)
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:JoinColumn(name = "projectmanager_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var projectManager: PFUserDO? = null
@PropertyInfo(i18nKey = "fibu.headOfBusinessManager")
@@ -261,6 +268,7 @@ open class AuftragDO : DefaultBaseDO(), DisplayNameCapable, AttachmentsInfo {
@get:ManyToOne(fetch = FetchType.LAZY)
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:JoinColumn(name = "headofbusinessmanager_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var headOfBusinessManager: PFUserDO? = null
@PropertyInfo(i18nKey = "fibu.salesManager")
@@ -268,6 +276,7 @@ open class AuftragDO : DefaultBaseDO(), DisplayNameCapable, AttachmentsInfo {
@get:ManyToOne(fetch = FetchType.LAZY)
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:JoinColumn(name = "salesmanager_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var salesManager: PFUserDO? = null
@JsonIgnore
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragsPositionDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragsPositionDO.kt
index b56782714d..82fa4979f8 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragsPositionDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragsPositionDO.kt
@@ -24,6 +24,7 @@
package org.projectforge.business.fibu
import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.builder.HashCodeBuilder
import org.projectforge.business.task.TaskDO
@@ -39,6 +40,7 @@ import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextFi
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.TypeBinding
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.search.ClassBridge
/**
@@ -78,10 +80,12 @@ open class AuftragsPositionDO : DefaultBaseDO(), DisplayNameCapable {
// @ContainedIn
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "auftrag_fk", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var auftrag: AuftragDO? = null
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "task_fk", nullable = true)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var task: TaskDO? = null
@PropertyInfo(i18nKey = "fibu.auftrag.position.art")
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EingangsrechnungDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EingangsrechnungDO.kt
index b97ecc307b..8247a0ed7d 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EingangsrechnungDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EingangsrechnungDO.kt
@@ -23,9 +23,7 @@
package org.projectforge.business.fibu
-import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.JsonManagedReference
-import com.fasterxml.jackson.annotation.ObjectIdGenerators
import jakarta.persistence.*
import org.hibernate.annotations.ListIndexBase
import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.ValueBridgeRef
@@ -58,7 +56,6 @@ import org.projectforge.framework.utils.StringComparator
query = "select min(datum), max(datum) from EingangsrechnungDO"
)
)
-@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
open class EingangsrechnungDO : AbstractRechnungDO(), Comparable, DisplayNameCapable {
override val displayName: String
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EingangsrechnungsPositionDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EingangsrechnungsPositionDO.kt
index 18e441d727..14f6f3459b 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EingangsrechnungsPositionDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EingangsrechnungsPositionDO.kt
@@ -24,12 +24,12 @@
package org.projectforge.business.fibu
import com.fasterxml.jackson.annotation.JsonBackReference
-import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.JsonManagedReference
-import com.fasterxml.jackson.annotation.ObjectIdGenerators
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed
import org.projectforge.business.fibu.kost.KostZuweisungDO
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.history.PersistenceBehavior
/**
@@ -46,12 +46,12 @@ import org.projectforge.framework.persistence.history.PersistenceBehavior
columnList = "eingangsrechnung_fk"
)]
)
-@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
open class EingangsrechnungsPositionDO : AbstractRechnungsPositionDO() {
@JsonBackReference
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "eingangsrechnung_fk", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var eingangsrechnung: EingangsrechnungDO? = null
override val rechnungId: Long?
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EmployeeDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EmployeeDO.kt
index 98fd316bdc..bcceed06c1 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EmployeeDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EmployeeDO.kt
@@ -23,8 +23,7 @@
package org.projectforge.business.fibu
-import com.fasterxml.jackson.annotation.JsonIdentityInfo
-import com.fasterxml.jackson.annotation.ObjectIdGenerators
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import mu.KotlinLogging
import org.apache.commons.lang3.StringUtils
@@ -35,6 +34,7 @@ import org.projectforge.business.fibu.kost.Kost1DO
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.common.anots.StringAlphanumericSort
import org.projectforge.framework.DisplayNameCapable
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.api.AUserRightId
import org.projectforge.framework.persistence.api.BaseDO
import org.projectforge.framework.persistence.api.EntityCopyStatus
@@ -72,7 +72,6 @@ private val log = KotlinLogging.logger {}
query = "from EmployeeDO where user.lastname=:lastname and user.firstname=:firstname"
)
)
-@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
open class EmployeeDO : DefaultBaseDO(), Comparable, DisplayNameCapable {
// The class must be declared as open for mocking in VacationServiceTest.
@@ -84,10 +83,14 @@ open class EmployeeDO : DefaultBaseDO(), Comparable, DisplayNameCapable {
* The ProjectForge user assigned to this employee.
*/
@PropertyInfo(i18nKey = "fibu.employee.user")
- @IndexedEmbedded(includeDepth = 1, includePaths = ["username", "firstname", "lastname", "description", "organization"])
+ @IndexedEmbedded(
+ includeDepth = 1,
+ includePaths = ["username", "firstname", "lastname", "description", "organization"]
+ )
@get:ManyToOne(fetch = FetchType.LAZY)
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:JoinColumn(name = "user_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var user: PFUserDO? = null
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EmployeeSalaryDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EmployeeSalaryDO.kt
index 8e22ab01b6..a7cb666c67 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EmployeeSalaryDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EmployeeSalaryDO.kt
@@ -23,12 +23,14 @@
package org.projectforge.business.fibu
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
import org.projectforge.Constants
import org.projectforge.common.StringHelper
import org.projectforge.common.anots.PropertyInfo
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.time.PFDayUtils
import java.math.BigDecimal
@@ -66,6 +68,7 @@ open class EmployeeSalaryDO : DefaultBaseDO() {
@get:ManyToOne(fetch = FetchType.LAZY)
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:JoinColumn(name = "employee_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var employee: EmployeeDO? = null
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EmployeeValidSinceAttrDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EmployeeValidSinceAttrDO.kt
index 690a947965..29afc0679d 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EmployeeValidSinceAttrDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/EmployeeValidSinceAttrDO.kt
@@ -25,9 +25,11 @@ package org.projectforge.business.fibu
import com.fasterxml.jackson.annotation.JsonIdentityReference
import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.projectforge.Constants
import org.projectforge.common.anots.PropertyInfo
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.json.JsonUtils
import org.projectforge.framework.persistence.candh.CandHHistoryEntryICustomizer
import org.projectforge.framework.persistence.candh.CandHIgnore
@@ -73,6 +75,7 @@ open class EmployeeValidSinceAttrDO : Serializable, AbstractBaseDO(), Cand
@JsonIdentityReference(alwaysAsId = true)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "employee_fk", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var employee: EmployeeDO? = null
@get:Enumerated(EnumType.STRING)
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/KundeDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/KundeDO.kt
index b7e96da3a7..61f0848374 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/KundeDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/KundeDO.kt
@@ -24,6 +24,7 @@
package org.projectforge.business.fibu
import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.apache.commons.lang3.StringUtils
import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.ValueBridgeRef
@@ -31,6 +32,7 @@ import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
import org.projectforge.business.common.NumberToStringValueBridge
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.framework.DisplayNameCapable
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.AbstractHistorizableBaseDO
/**
@@ -109,6 +111,7 @@ open class KundeDO : AbstractHistorizableBaseDO(), DisplayNameCapable {
@PropertyInfo(i18nKey = "fibu.konto")
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "konto_id")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var konto: KontoDO? = null
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/PaymentScheduleDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/PaymentScheduleDO.kt
index 4c0e73c47b..144d8f9779 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/PaymentScheduleDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/PaymentScheduleDO.kt
@@ -24,6 +24,7 @@
package org.projectforge.business.fibu
import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.builder.HashCodeBuilder
import org.projectforge.common.anots.PropertyInfo
@@ -32,6 +33,7 @@ import org.projectforge.framework.persistence.entities.DefaultBaseDO
import java.math.BigDecimal
import java.time.LocalDate
import jakarta.persistence.*
+import org.projectforge.framework.json.IdOnlySerializer
/**
* @author Werner Feder (werner.feder@t-online.de)
@@ -55,6 +57,7 @@ open class PaymentScheduleDO : DefaultBaseDO(), DisplayNameCapable {
@JsonIgnore
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "auftrag_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var auftrag: AuftragDO? = null
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/ProjektDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/ProjektDO.kt
index 159c4c37e4..f85d021495 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/ProjektDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/ProjektDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.business.fibu
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.apache.commons.lang3.StringUtils
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
@@ -33,6 +34,7 @@ import org.projectforge.business.common.NumberToStringValueBridge
import org.projectforge.business.task.TaskDO
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.framework.DisplayNameCapable
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.persistence.search.ClassBridge
import org.projectforge.framework.persistence.user.entities.GroupDO
@@ -110,6 +112,7 @@ open class ProjektDO : DefaultBaseDO(), DisplayNameCapable {
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "kunde_id")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var kunde: KundeDO? = null
/**
@@ -139,6 +142,7 @@ open class ProjektDO : DefaultBaseDO(), DisplayNameCapable {
@get:JoinColumn(name = "projektmanager_group_fk")
@IndexedEmbedded(includeDepth = 1)
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var projektManagerGroup: GroupDO? = null
@PropertyInfo(i18nKey = "fibu.projectManager")
@@ -146,6 +150,7 @@ open class ProjektDO : DefaultBaseDO(), DisplayNameCapable {
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "projectmanager_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var projectManager: PFUserDO? = null
@PropertyInfo(i18nKey = "fibu.headOfBusinessManager")
@@ -153,6 +158,7 @@ open class ProjektDO : DefaultBaseDO(), DisplayNameCapable {
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "headofbusinessmanager_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var headOfBusinessManager: PFUserDO? = null
@PropertyInfo(i18nKey = "fibu.salesManager")
@@ -160,11 +166,13 @@ open class ProjektDO : DefaultBaseDO(), DisplayNameCapable {
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "salesmanager_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var salesManager: PFUserDO? = null
@PropertyInfo(i18nKey = "task")
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "task_fk", nullable = true)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var task: TaskDO? = null
/**
@@ -176,6 +184,7 @@ open class ProjektDO : DefaultBaseDO(), DisplayNameCapable {
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "konto_id")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var konto: KontoDO? = null
@get:PropertyInfo(i18nKey = "fibu.kost2")
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/RechnungDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/RechnungDO.kt
index da3caef5ae..3d6aea693c 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/RechnungDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/RechnungDO.kt
@@ -23,14 +23,14 @@
package org.projectforge.business.fibu
-import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.JsonManagedReference
-import com.fasterxml.jackson.annotation.ObjectIdGenerators
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.hibernate.annotations.ListIndexBase
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
import org.projectforge.common.anots.PropertyInfo
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.history.PersistenceBehavior
import java.time.LocalDate
@@ -59,7 +59,6 @@ import java.time.LocalDate
NamedQuery(name = RechnungDO.SELECT_MIN_MAX_DATE, query = "select min(datum), max(datum) from RechnungDO"),
NamedQuery(name = RechnungDO.FIND_OTHER_BY_NUMMER, query = "from RechnungDO where nummer=:nummer and id!=:id")
)
-@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
open class RechnungDO : AbstractRechnungDO(), Comparable {
@PropertyInfo(i18nKey = "fibu.rechnung.nummer")
@@ -75,6 +74,7 @@ open class RechnungDO : AbstractRechnungDO(), Comparable {
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "kunde_id", nullable = true)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var kunde: KundeDO? = null
/**
@@ -90,6 +90,7 @@ open class RechnungDO : AbstractRechnungDO(), Comparable {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "projekt_id", nullable = true)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var projekt: ProjektDO? = null
@PropertyInfo(i18nKey = "fibu.rechnung.status")
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/RechnungsPositionDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/RechnungsPositionDO.kt
index 388017fa1e..3ff03032fb 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/RechnungsPositionDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/RechnungsPositionDO.kt
@@ -24,14 +24,14 @@
package org.projectforge.business.fibu
import com.fasterxml.jackson.annotation.JsonBackReference
-import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.JsonManagedReference
-import com.fasterxml.jackson.annotation.ObjectIdGenerators
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
import org.projectforge.business.fibu.kost.KostZuweisungDO
import org.projectforge.common.anots.PropertyInfo
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.history.PersistenceBehavior
import java.time.LocalDate
@@ -50,13 +50,13 @@ import java.time.LocalDate
columnList = "auftrags_position_fk"
), jakarta.persistence.Index(name = "idx_fk_t_fibu_rechnung_position_rechnung_fk", columnList = "rechnung_fk")]
)
-@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
open class RechnungsPositionDO : AbstractRechnungsPositionDO() {
@JsonBackReference
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "rechnung_fk", nullable = false)
@get:AssociationInverseSide(inversePath = ObjectPath(PropertyValue(propertyName = "rechnung")))
@get:IndexingDependency(derivedFrom = [ObjectPath(PropertyValue(propertyName = "positionen"))])
+ @JsonSerialize(using = IdOnlySerializer::class)
open var rechnung: RechnungDO? = null
override val rechnungId: Long?
@@ -68,6 +68,7 @@ open class RechnungsPositionDO : AbstractRechnungsPositionDO() {
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "auftrags_position_fk")
@get:IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var auftragsPosition: AuftragsPositionDO? = null
@PropertyInfo(i18nKey = "fibu.periodOfPerformance.type")
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/kost/BuchungssatzDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/kost/BuchungssatzDO.kt
index 9b9c1852d3..1f27fe6930 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/kost/BuchungssatzDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/kost/BuchungssatzDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.business.fibu.kost
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.apache.commons.lang3.StringUtils
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
@@ -30,6 +31,7 @@ import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
import org.projectforge.business.fibu.KontoDO
import org.projectforge.common.StringHelper
import org.projectforge.common.anots.PropertyInfo
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.time.PFDayUtils
import org.slf4j.LoggerFactory
@@ -113,6 +115,7 @@ open class BuchungssatzDO : DefaultBaseDO(), Comparable {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "konto_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var konto: KontoDO? = null
@PropertyInfo(i18nKey = "fibu.buchungssatz.gegenKonto")
@@ -120,6 +123,7 @@ open class BuchungssatzDO : DefaultBaseDO(), Comparable {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "gegenkonto_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var gegenKonto: KontoDO? = null
@GenericField // was: @FullTextField(analyze = Analyze.NO)
@@ -148,6 +152,7 @@ open class BuchungssatzDO : DefaultBaseDO(), Comparable {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "kost1_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var kost1: Kost1DO? = null
@PropertyInfo(i18nKey = "fibu.kost2")
@@ -155,6 +160,7 @@ open class BuchungssatzDO : DefaultBaseDO(), Comparable {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "kost2_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var kost2: Kost2DO? = null
@PropertyInfo(i18nKey = "comment")
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/kost/Kost2DO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/kost/Kost2DO.kt
index 659f9d4b08..34d25f24e7 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/kost/Kost2DO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/kost/Kost2DO.kt
@@ -24,6 +24,7 @@
package org.projectforge.business.fibu.kost
import com.fasterxml.jackson.annotation.JsonCreator
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.apache.commons.lang3.builder.HashCodeBuilder
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
@@ -33,6 +34,7 @@ import org.projectforge.business.fibu.OldKostFormatter
import org.projectforge.business.fibu.ProjektDO
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.framework.DisplayNameCapable
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import java.math.BigDecimal
@@ -111,6 +113,7 @@ open class Kost2DO : DefaultBaseDO(), Comparable, DisplayNameCapable {
@PropertyInfo(i18nKey = "fibu.kost2.art")
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "kost2_art_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var kost2Art: Kost2ArtDO? = null
@PropertyInfo(i18nKey = "fibu.kost2.workFraction")
@@ -139,6 +142,7 @@ open class Kost2DO : DefaultBaseDO(), Comparable, DisplayNameCapable {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "projekt_id")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var projekt: ProjektDO? = null
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/kost/KostZuweisungDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/kost/KostZuweisungDO.kt
index 7aa31b0c6f..eb84a31af8 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/kost/KostZuweisungDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/kost/KostZuweisungDO.kt
@@ -24,11 +24,14 @@
package org.projectforge.business.fibu.kost
import com.fasterxml.jackson.annotation.JsonBackReference
-import com.fasterxml.jackson.annotation.JsonIdentityInfo
-import com.fasterxml.jackson.annotation.ObjectIdGenerators
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
+import jakarta.persistence.*
import org.apache.commons.lang3.builder.HashCodeBuilder
+import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
+import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmbedded
+import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency
import org.projectforge.Constants
import org.projectforge.business.fibu.AbstractRechnungsPositionDO
import org.projectforge.business.fibu.EingangsrechnungsPositionDO
@@ -36,14 +39,11 @@ import org.projectforge.business.fibu.EmployeeSalaryDO
import org.projectforge.business.fibu.RechnungsPositionDO
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.framework.DisplayNameCapable
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.utils.CurrencyHelper
-import java.math.BigDecimal
-import jakarta.persistence.*
-import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
-import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField
-import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency
import org.projectforge.framework.utils.NumberFormatter
+import java.math.BigDecimal
/**
* Rechnungen (Ein- und Ausgang) sowie Gehaltssonderzahlungen werden auf Kost1 und Kost2 aufgeteilt. Einer Rechnung
@@ -55,208 +55,212 @@ import org.projectforge.framework.utils.NumberFormatter
@Entity
@Indexed
@Table(
- name = "T_FIBU_KOST_ZUWEISUNG",
- uniqueConstraints = [UniqueConstraint(columnNames = ["index", "rechnungs_pos_fk", "kost1_fk", "kost2_fk"]), UniqueConstraint(
- columnNames = ["index", "eingangsrechnungs_pos_fk", "kost1_fk", "kost2_fk"]
- ), UniqueConstraint(columnNames = ["index", "employee_salary_fk", "kost1_fk", "kost2_fk"])],
- indexes = [Index(
- name = "idx_fk_t_fibu_kost_zuweisung_eingangsrechnungs_pos_fk",
- columnList = "eingangsrechnungs_pos_fk"
- ), Index(
- name = "idx_fk_t_fibu_kost_zuweisung_employee_salary_fk",
- columnList = "employee_salary_fk"
- ), Index(
- name = "idx_fk_t_fibu_kost_zuweisung_kost1_fk",
- columnList = "kost1_fk"
- ), Index(
- name = "idx_fk_t_fibu_kost_zuweisung_kost2_fk",
- columnList = "kost2_fk"
- ), Index(name = "idx_fk_t_fibu_kost_zuweisung_rechnungs_pos_fk", columnList = "rechnungs_pos_fk")]
+ name = "T_FIBU_KOST_ZUWEISUNG",
+ uniqueConstraints = [UniqueConstraint(columnNames = ["index", "rechnungs_pos_fk", "kost1_fk", "kost2_fk"]), UniqueConstraint(
+ columnNames = ["index", "eingangsrechnungs_pos_fk", "kost1_fk", "kost2_fk"]
+ ), UniqueConstraint(columnNames = ["index", "employee_salary_fk", "kost1_fk", "kost2_fk"])],
+ indexes = [Index(
+ name = "idx_fk_t_fibu_kost_zuweisung_eingangsrechnungs_pos_fk",
+ columnList = "eingangsrechnungs_pos_fk"
+ ), Index(
+ name = "idx_fk_t_fibu_kost_zuweisung_employee_salary_fk",
+ columnList = "employee_salary_fk"
+ ), Index(
+ name = "idx_fk_t_fibu_kost_zuweisung_kost1_fk",
+ columnList = "kost1_fk"
+ ), Index(
+ name = "idx_fk_t_fibu_kost_zuweisung_kost2_fk",
+ columnList = "kost2_fk"
+ ), Index(name = "idx_fk_t_fibu_kost_zuweisung_rechnungs_pos_fk", columnList = "rechnungs_pos_fk")]
)
-@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
open class KostZuweisungDO : DefaultBaseDO(), DisplayNameCapable {
- override val displayName: String
- @Transient
- get() = "$index:${kost1?.displayName}|${kost2?.displayName}:${NumberFormatter.formatCurrency(netto)}"
+ override val displayName: String
+ @Transient
+ get() = "$index:${kost1?.displayName}|${kost2?.displayName}:${NumberFormatter.formatCurrency(netto)}"
- /**
- * Die Kostzuweisungen sind als Array organisiert. Dies stellt den Index der Kostzuweisung dar. Der Index ist für
- * Gehaltszahlungen ohne Belang.
- *
- * @return
- */
- @get:Column
- open var index: Short = 0
+ /**
+ * Die Kostzuweisungen sind als Array organisiert. Dies stellt den Index der Kostzuweisung dar. Der Index ist für
+ * Gehaltszahlungen ohne Belang.
+ *
+ * @return
+ */
+ @get:Column
+ open var index: Short = 0
- @PropertyInfo(i18nKey = "fibu.common.netto")
- @get:Column(scale = 2, precision = 12)
- open var netto: BigDecimal? = null
+ @PropertyInfo(i18nKey = "fibu.common.netto")
+ @get:Column(scale = 2, precision = 12)
+ open var netto: BigDecimal? = null
- @PropertyInfo(i18nKey = "fibu.kost1")
- @IndexedEmbedded(includeDepth = 1)
- @IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
- @get:ManyToOne(fetch = FetchType.LAZY)
- @get:JoinColumn(name = "kost1_fk")
- open var kost1: Kost1DO? = null
+ @PropertyInfo(i18nKey = "fibu.kost1")
+ @IndexedEmbedded(includeDepth = 1)
+ @IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
+ @get:ManyToOne(fetch = FetchType.LAZY)
+ @get:JoinColumn(name = "kost1_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
+ open var kost1: Kost1DO? = null
- @PropertyInfo(i18nKey = "fibu.kost2")
- @IndexedEmbedded(includeDepth = 1)
- @IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
- @get:ManyToOne(fetch = FetchType.LAZY)
- @get:JoinColumn(name = "kost2_fk")
- open var kost2: Kost2DO? = null
+ @PropertyInfo(i18nKey = "fibu.kost2")
+ @IndexedEmbedded(includeDepth = 1)
+ @IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
+ @get:ManyToOne(fetch = FetchType.LAZY)
+ @get:JoinColumn(name = "kost2_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
+ open var kost2: Kost2DO? = null
- @IndexedEmbedded(includeDepth = 1)
- @get:ManyToOne(fetch = FetchType.LAZY)
- @get:JoinColumn(name = "rechnungs_pos_fk", nullable = true)
- @JsonBackReference
- open var rechnungsPosition: RechnungsPositionDO? = null
- set(rechnungsPosition) {
- if (rechnungsPosition != null && (this.eingangsrechnungsPosition != null || this.employeeSalary != null)) {
- throw IllegalStateException("eingangsRechnung or employeeSalary already given!")
- }
- field = rechnungsPosition
- }
+ @IndexedEmbedded(includeDepth = 1)
+ @get:ManyToOne(fetch = FetchType.LAZY)
+ @get:JoinColumn(name = "rechnungs_pos_fk", nullable = true)
+ @JsonBackReference
+ @JsonSerialize(using = IdOnlySerializer::class)
+ open var rechnungsPosition: RechnungsPositionDO? = null
+ set(rechnungsPosition) {
+ if (rechnungsPosition != null && (this.eingangsrechnungsPosition != null || this.employeeSalary != null)) {
+ throw IllegalStateException("eingangsRechnung or employeeSalary already given!")
+ }
+ field = rechnungsPosition
+ }
+
+ @IndexedEmbedded(includeDepth = 1)
+ @get:ManyToOne(fetch = FetchType.LAZY)
+ @get:JoinColumn(name = "eingangsrechnungs_pos_fk", nullable = true)
+ @JsonBackReference
+ @JsonSerialize(using = IdOnlySerializer::class)
+ open var eingangsrechnungsPosition: EingangsrechnungsPositionDO? = null
+ set(eingangsrechnungsPosition) {
+ if (eingangsrechnungsPosition != null && (this.rechnungsPosition != null || this.employeeSalary != null)) {
+ throw IllegalStateException("rechnungsPosition or employeeSalary already given!")
+ }
+ field = eingangsrechnungsPosition
+ }
- @IndexedEmbedded(includeDepth = 1)
- @get:ManyToOne(fetch = FetchType.LAZY)
- @get:JoinColumn(name = "eingangsrechnungs_pos_fk", nullable = true)
- @JsonBackReference
- open var eingangsrechnungsPosition: EingangsrechnungsPositionDO? = null
- set(eingangsrechnungsPosition) {
- if (eingangsrechnungsPosition != null && (this.rechnungsPosition != null || this.employeeSalary != null)) {
- throw IllegalStateException("rechnungsPosition or employeeSalary already given!")
- }
- field = eingangsrechnungsPosition
+ fun setAbstractRechnungsPosition(position: AbstractRechnungsPositionDO) {
+ if (position is RechnungsPositionDO)
+ rechnungsPosition = position
+ else
+ eingangsrechnungsPosition = position as EingangsrechnungsPositionDO
}
- fun setAbstractRechnungsPosition(position: AbstractRechnungsPositionDO) {
- if (position is RechnungsPositionDO)
- rechnungsPosition = position
- else
- eingangsrechnungsPosition = position as EingangsrechnungsPositionDO
- }
+ @IndexedEmbedded(includeDepth = 1)
+ @IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
+ @get:ManyToOne(fetch = FetchType.LAZY)
+ @get:JoinColumn(name = "employee_salary_fk", nullable = true)
+ @JsonSerialize(using = IdOnlySerializer::class)
+ open var employeeSalary: EmployeeSalaryDO? = null
+ set(employeeSalary) {
+ if (employeeSalary != null && (this.eingangsrechnungsPosition != null || this.rechnungsPosition != null)) {
+ throw IllegalStateException("eingangsRechnung or rechnungsPosition already given!")
+ }
+ field = employeeSalary
+ }
- @IndexedEmbedded(includeDepth = 1)
- @IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
- @get:ManyToOne(fetch = FetchType.LAZY)
- @get:JoinColumn(name = "employee_salary_fk", nullable = true)
- open var employeeSalary: EmployeeSalaryDO? = null
- set(employeeSalary) {
- if (employeeSalary != null && (this.eingangsrechnungsPosition != null || this.rechnungsPosition != null)) {
- throw IllegalStateException("eingangsRechnung or rechnungsPosition already given!")
- }
- field = employeeSalary
- }
+ @FullTextField
+ @get:Column(length = Constants.COMMENT_LENGTH)
+ open var comment: String? = null
- @FullTextField
- @get:Column(length = Constants.COMMENT_LENGTH)
- open var comment: String? = null
+ /**
+ * Calculates gross amount using the vat from the invoice position.
+ *
+ * @return Gross amount if vat found otherwise net amount.
+ * @see .getRechnungsPosition
+ * @see .getEingangsrechnungsPosition
+ */
+ val brutto: BigDecimal
+ @Transient
+ get() {
+ val vat: BigDecimal?
+ when {
+ this.rechnungsPosition != null -> vat = this.rechnungsPosition!!.vat
+ this.eingangsrechnungsPosition != null -> vat = this.eingangsrechnungsPosition!!.vat
+ else -> vat = null
+ }
+ return CurrencyHelper.getGrossAmount(this.netto, vat)
+ }
- /**
- * Calculates gross amount using the vat from the invoice position.
- *
- * @return Gross amount if vat found otherwise net amount.
- * @see .getRechnungsPosition
- * @see .getEingangsrechnungsPosition
- */
- val brutto: BigDecimal
- @Transient
- get() {
- val vat: BigDecimal?
- when {
- this.rechnungsPosition != null -> vat = this.rechnungsPosition!!.vat
- this.eingangsrechnungsPosition != null -> vat = this.eingangsrechnungsPosition!!.vat
- else -> vat = null
- }
- return CurrencyHelper.getGrossAmount(this.netto, vat)
- }
+ val kost1Id: Long?
+ @Transient
+ get() = if (this.kost1 == null) {
+ null
+ } else kost1!!.id
- val kost1Id: Long?
- @Transient
- get() = if (this.kost1 == null) {
- null
- } else kost1!!.id
+ val kost2Id: Long?
+ @Transient
+ get() = if (this.kost2 == null) {
+ null
+ } else kost2!!.id
- val kost2Id: Long?
- @Transient
- get() = if (this.kost2 == null) {
- null
- } else kost2!!.id
+ /**
+ * @return true if betrag is zero or not given.
+ */
+ val isEmpty: Boolean
+ @Transient
+ get() = netto == null || netto!!.compareTo(BigDecimal.ZERO) == 0
- /**
- * @return true if betrag is zero or not given.
- */
- val isEmpty: Boolean
+ /**
+ * If empty then no error will be returned.
+ *
+ * @return error message (i18n key) or null if no error is given.
+ */
@Transient
- get() = netto == null || netto!!.compareTo(BigDecimal.ZERO) == 0
-
- /**
- * If empty then no error will be returned.
- *
- * @return error message (i18n key) or null if no error is given.
- */
- @Transient
- fun hasErrors(): String? {
- if (isEmpty) {
- return null
- }
- var counter = 0
- if (rechnungsPosition?.id != null) {
- counter++
- }
- if (eingangsrechnungsPosition?.id != null) {
- counter++
+ fun hasErrors(): String? {
+ if (isEmpty) {
+ return null
+ }
+ var counter = 0
+ if (rechnungsPosition?.id != null) {
+ counter++
+ }
+ if (eingangsrechnungsPosition?.id != null) {
+ counter++
+ }
+ if (employeeSalary?.id != null) {
+ counter++
+ }
+ return if (counter != 1) {
+ "fibu.kostZuweisung.error.genauEinFinanzobjektErwartet" // i18n key
+ } else null
}
- if (employeeSalary?.id != null) {
- counter++
- }
- return if (counter != 1) {
- "fibu.kostZuweisung.error.genauEinFinanzobjektErwartet" // i18n key
- } else null
- }
- override fun equals(other: Any?): Boolean {
- if (other is KostZuweisungDO) {
- val o = other as KostZuweisungDO?
- if (this.index != o!!.index) {
- return false
- }
- if (this.rechnungsPosition?.id != o.rechnungsPosition?.id) {
- return false
- }
- if (this.eingangsrechnungsPosition?.id != o.eingangsrechnungsPosition?.id) {
+ override fun equals(other: Any?): Boolean {
+ if (other is KostZuweisungDO) {
+ val o = other as KostZuweisungDO?
+ if (this.index != o!!.index) {
+ return false
+ }
+ if (this.rechnungsPosition?.id != o.rechnungsPosition?.id) {
+ return false
+ }
+ if (this.eingangsrechnungsPosition?.id != o.eingangsrechnungsPosition?.id) {
+ return false
+ }
+ return this.employeeSalary?.id == o.employeeSalary?.id
+ }
return false
- }
- return this.employeeSalary?.id == o.employeeSalary?.id
}
- return false
- }
- override fun hashCode(): Int {
- val hcb = HashCodeBuilder()
- hcb.append(index)
- if (rechnungsPosition != null) {
- hcb.append(rechnungsPosition?.id)
+ override fun hashCode(): Int {
+ val hcb = HashCodeBuilder()
+ hcb.append(index)
+ if (rechnungsPosition != null) {
+ hcb.append(rechnungsPosition?.id)
+ }
+ if (eingangsrechnungsPosition != null) {
+ hcb.append(eingangsrechnungsPosition?.id)
+ }
+ if (employeeSalary != null) {
+ hcb.append(employeeSalary?.id)
+ }
+ return hcb.toHashCode()
}
- if (eingangsrechnungsPosition != null) {
- hcb.append(eingangsrechnungsPosition?.id)
- }
- if (employeeSalary != null) {
- hcb.append(employeeSalary?.id)
- }
- return hcb.toHashCode()
- }
- /**
- * Clones this cost assignment (without id's).
- *
- * @return
- */
- fun newClone(): KostZuweisungDO {
- val kostZuweisung = KostZuweisungDO()
- kostZuweisung.copyValuesFrom(this, "id")
- return kostZuweisung
- }
+ /**
+ * Clones this cost assignment (without id's).
+ *
+ * @return
+ */
+ fun newClone(): KostZuweisungDO {
+ val kostZuweisung = KostZuweisungDO()
+ kostZuweisung.copyValuesFrom(this, "id")
+ return kostZuweisung
+ }
}
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/gantt/GanttChartDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/gantt/GanttChartDO.kt
index b52e8720db..106bc4672d 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/gantt/GanttChartDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/gantt/GanttChartDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.business.gantt
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmbedded
import org.projectforge.business.task.TaskDO
@@ -33,6 +34,7 @@ import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency
+import org.projectforge.framework.json.IdOnlySerializer
/**
* @author Kai Reinhard (k.reinhard@micromata.de)
@@ -60,6 +62,7 @@ class GanttChartDO : AbstractBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "task_fk", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
var task: TaskDO? = null
@get:Transient
@@ -105,6 +108,7 @@ class GanttChartDO : AbstractBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "owner_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
var owner: PFUserDO? = null
val taskId: Long?
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/humanresources/HRPlanningDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/humanresources/HRPlanningDO.kt
index f18d9295de..31ca5e1fa7 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/humanresources/HRPlanningDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/humanresources/HRPlanningDO.kt
@@ -23,14 +23,17 @@
package org.projectforge.business.humanresources
-import com.fasterxml.jackson.annotation.JsonIdentityInfo
-import com.fasterxml.jackson.annotation.ObjectIdGenerators
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import mu.KotlinLogging
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
-import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
+import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField
+import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed
+import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmbedded
+import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency
import org.projectforge.business.fibu.ProjektDO
import org.projectforge.common.anots.PropertyInfo
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.persistence.history.PersistenceBehavior
import org.projectforge.framework.persistence.user.entities.PFUserDO
@@ -66,7 +69,6 @@ private val log = KotlinLogging.logger {}
query = "from HRPlanningDO where user.id=:userId and week=:week and id!=:id"
)
)
-@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
open class HRPlanningDO : DefaultBaseDO() {
/**
@@ -77,6 +79,7 @@ open class HRPlanningDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "user_fk", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var user: PFUserDO? = null
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/humanresources/HRPlanningEntryDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/humanresources/HRPlanningEntryDO.kt
index 1d622787bb..359fa3e8f2 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/humanresources/HRPlanningEntryDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/humanresources/HRPlanningEntryDO.kt
@@ -23,30 +23,35 @@
package org.projectforge.business.humanresources
-import com.fasterxml.jackson.annotation.JsonIdentityInfo
-import com.fasterxml.jackson.annotation.ObjectIdGenerators
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
+import jakarta.persistence.*
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.builder.HashCodeBuilder
+import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
+import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
import org.projectforge.business.fibu.ProjektDO
import org.projectforge.business.fibu.ProjektFormatter
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.common.i18n.Priority
import org.projectforge.framework.DisplayNameCapable
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.persistence.user.api.ThreadLocalUserContext
import org.projectforge.framework.utils.ObjectHelper
import java.math.BigDecimal
-import jakarta.persistence.*
-import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
-import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
/**
* @author Mario Groß (m.gross@micromata.de)
*/
@Entity
@Indexed
-@Table(name = "T_HR_PLANNING_ENTRY", indexes = [jakarta.persistence.Index(name = "idx_fk_t_hr_planning_entry_planning_fk", columnList = "planning_fk"), jakarta.persistence.Index(name = "idx_fk_t_hr_planning_entry_projekt_fk", columnList = "projekt_fk")])
-@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
+@Table(
+ name = "T_HR_PLANNING_ENTRY",
+ indexes = [jakarta.persistence.Index(
+ name = "idx_fk_t_hr_planning_entry_planning_fk",
+ columnList = "planning_fk"
+ ), jakarta.persistence.Index(name = "idx_fk_t_hr_planning_entry_projekt_fk", columnList = "projekt_fk")]
+)
open class HRPlanningEntryDO : DefaultBaseDO(), DisplayNameCapable {
override val displayName: String
@@ -57,6 +62,7 @@ open class HRPlanningEntryDO : DefaultBaseDO(), DisplayNameCapable {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "planning_fk", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var planning: HRPlanningDO? = null
@PropertyInfo(i18nKey = "fibu.projekt")
@@ -64,6 +70,7 @@ open class HRPlanningEntryDO : DefaultBaseDO(), DisplayNameCapable {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "projekt_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var projekt: ProjektDO? = null
@FullTextField
@@ -194,9 +201,11 @@ open class HRPlanningEntryDO : DefaultBaseDO(), DisplayNameCapable {
val isEmpty: Boolean
@Transient
- get() = ObjectHelper.isEmpty(this.description, this.mondayHours, this.tuesdayHours, this.wednesdayHours,
- this.thursdayHours,
- this.fridayHours, this.weekendHours, this.priority, this.probability, this.projekt)
+ get() = ObjectHelper.isEmpty(
+ this.description, this.mondayHours, this.tuesdayHours, this.wednesdayHours,
+ this.thursdayHours,
+ this.fridayHours, this.weekendHours, this.priority, this.probability, this.projekt
+ )
override fun equals(other: Any?): Boolean {
if (other is HRPlanningEntryDO) {
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/orga/VisitorbookDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/orga/VisitorbookDO.kt
index dfb964971e..97f7c8e968 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/orga/VisitorbookDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/orga/VisitorbookDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.business.orga
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import mu.KotlinLogging
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
@@ -34,6 +35,7 @@ import org.projectforge.Constants
import org.projectforge.business.fibu.EmployeeDO
import org.projectforge.business.vacation.model.VacationDO
import org.projectforge.common.anots.PropertyInfo
+import org.projectforge.framework.json.IdsOnlySerializer
import org.projectforge.framework.persistence.api.AUserRightId
import org.projectforge.framework.persistence.api.BaseDO
import org.projectforge.framework.persistence.api.EntityCopyStatus
@@ -86,6 +88,7 @@ open class VisitorbookDO : DefaultBaseDO() {
columnList = "visitorbook_id"
), jakarta.persistence.Index(name = "idx_fk_t_orga_employee_employee_id", columnList = "employee_id")]
)
+ @JsonSerialize(using = IdsOnlySerializer::class)
open var contactPersons: Set? = null
@PropertyInfo(i18nKey = "comment")
@@ -105,6 +108,7 @@ open class VisitorbookDO : DefaultBaseDO() {
)
@NoHistory
// @HistoryProperty(converter = TimependingHistoryPropertyConverter::class)
+ @JsonSerialize(using = IdsOnlySerializer::class)
open var entries: MutableList? = null
fun addEntry(entry: VisitorbookEntryDO) {
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/orga/VisitorbookEntryDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/orga/VisitorbookEntryDO.kt
index 92e6aea1c1..96795c96d3 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/orga/VisitorbookEntryDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/orga/VisitorbookEntryDO.kt
@@ -23,12 +23,12 @@
package org.projectforge.business.orga
-import com.fasterxml.jackson.annotation.JsonIdentityInfo
-import com.fasterxml.jackson.annotation.ObjectIdGenerators
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField
import org.projectforge.Constants
import org.projectforge.common.anots.PropertyInfo
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.AbstractBaseDO
import org.projectforge.framework.persistence.history.WithHistory
import java.io.Serializable
@@ -47,7 +47,6 @@ import java.time.LocalDate
)]
)
@WithHistory
-@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
open class VisitorbookEntryDO : Serializable, AbstractBaseDO() {
@get:Id
@get:GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "hibernate_sequence")
@@ -56,6 +55,7 @@ open class VisitorbookEntryDO : Serializable, AbstractBaseDO() {
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "visitorbook_fk", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var visitorbook: VisitorbookDO? = null
@PropertyInfo(i18nKey = "calendar.day")
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/scripting/ScriptDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/scripting/ScriptDO.kt
index 3e9c649ac0..c0d5b127c1 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/scripting/ScriptDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/scripting/ScriptDO.kt
@@ -24,6 +24,7 @@
package org.projectforge.business.scripting
import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.apache.commons.lang3.StringUtils
import org.hibernate.annotations.JdbcTypeCode
@@ -32,6 +33,7 @@ import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
import org.hibernate.type.SqlTypes
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.framework.jcr.AttachmentsInfo
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.persistence.history.NoHistory
import org.projectforge.framework.persistence.user.entities.PFUserDO
@@ -76,6 +78,7 @@ open class ScriptDO : DefaultBaseDO(), AttachmentsInfo {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "execute_as_user_id", nullable = true)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var executeAsUser: PFUserDO? = null
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/sipgate/SipgateContactSyncDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/sipgate/SipgateContactSyncDO.kt
index 113778e2e8..b01a1b7ad6 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/sipgate/SipgateContactSyncDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/sipgate/SipgateContactSyncDO.kt
@@ -24,10 +24,12 @@
package org.projectforge.business.sipgate
import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.projectforge.business.address.AddressDO
import org.projectforge.framework.json.JsonUtils
import java.util.*
import jakarta.persistence.*
+import org.projectforge.framework.json.IdOnlySerializer
/**
* @author K. Reinhard (k.reinhard@micromata.de)
@@ -149,6 +151,7 @@ open class SipgateContactSyncDO {
@get:ManyToOne
@get:JoinColumn(name = "address_id")
@get:JsonIgnore
+ @JsonSerialize(using = IdOnlySerializer::class)
open var address: AddressDO? = null
@get:Column(name = "last_sync")
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/task/TaskDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/task/TaskDO.kt
index 4e1d7111c9..c51873d791 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/task/TaskDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/task/TaskDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.business.task
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.apache.commons.lang3.builder.HashCodeBuilder
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
@@ -36,6 +37,7 @@ import org.projectforge.common.i18n.Priority
import org.projectforge.common.task.TaskStatus
import org.projectforge.common.task.TimesheetBookingStatus
import org.projectforge.framework.DisplayNameCapable
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.persistence.search.ClassBridge
import org.projectforge.framework.persistence.user.entities.PFUserDO
@@ -79,6 +81,7 @@ open class TaskDO : DefaultBaseDO(), Cloneable, DisplayNameCapable // , GanttObj
@PropertyInfo(i18nKey = "task.parentTask")
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "parent_task_id")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var parentTask: TaskDO? = null
@PropertyInfo(i18nKey = "task.title")
@@ -162,6 +165,7 @@ open class TaskDO : DefaultBaseDO(), Cloneable, DisplayNameCapable // , GanttObj
@PropertyInfo(i18nKey = "task.assignedUser")
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "responsible_user_id")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var responsibleUser: PFUserDO? = null
/**
@@ -274,6 +278,7 @@ open class TaskDO : DefaultBaseDO(), Cloneable, DisplayNameCapable // , GanttObj
fetch = FetchType.LAZY
)
@get:JoinColumn(name = "gantt_predecessor_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var ganttPredecessor: TaskDO? = null
/** -> Gantt */
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/teamcal/admin/model/TeamCalDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/teamcal/admin/model/TeamCalDO.kt
index 47c15b9a73..cf36925cd2 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/teamcal/admin/model/TeamCalDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/teamcal/admin/model/TeamCalDO.kt
@@ -25,6 +25,7 @@ package org.projectforge.business.teamcal.admin.model
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.builder.HashCodeBuilder
import org.hibernate.annotations.Type
@@ -38,6 +39,7 @@ import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.TypeBinderRef
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
import org.hibernate.type.SqlTypes
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.history.NoHistory
import org.projectforge.framework.persistence.search.ClassBridge
@@ -75,6 +77,7 @@ open class TeamCalDO : BaseUserGroupRightsDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "owner_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
override var owner: PFUserDO? = null
@PropertyInfo(i18nKey = "plugins.teamcal.description")
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/teamcal/event/model/TeamEventAttendeeDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/teamcal/event/model/TeamEventAttendeeDO.kt
index 00b07a446e..3cc6d18950 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/teamcal/event/model/TeamEventAttendeeDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/teamcal/event/model/TeamEventAttendeeDO.kt
@@ -23,12 +23,14 @@
package org.projectforge.business.teamcal.event.model
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.apache.commons.lang3.StringUtils
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed
import org.projectforge.business.address.AddressDO
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.persistence.user.entities.PFUserDO
import jakarta.persistence.*
+import org.projectforge.framework.json.IdOnlySerializer
/**
* @author Kai Reinhard (k.reinhard@micromata.de)
@@ -65,6 +67,7 @@ open class TeamEventAttendeeDO : DefaultBaseDO(), Comparable? = null
+ @JsonSerialize(using = IdOnlySerializer::class)
open var creator: PFUserDO? = null
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_event_fk_creator")
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/timesheet/TimesheetDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/timesheet/TimesheetDO.kt
index 79c1f1e76b..dda8568036 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/timesheet/TimesheetDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/timesheet/TimesheetDO.kt
@@ -24,6 +24,7 @@
package org.projectforge.business.timesheet
import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.apache.commons.lang3.StringUtils
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
@@ -32,6 +33,7 @@ import org.projectforge.business.fibu.kost.Kost2DO
import org.projectforge.business.task.TaskDO
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.framework.calendar.DurationUtils
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.persistence.user.api.UserPrefParameter
import org.projectforge.framework.persistence.user.entities.PFUserDO
@@ -81,6 +83,7 @@ open class TimesheetDO : DefaultBaseDO(), Comparable {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "task_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var task: TaskDO? = null
@PropertyInfo(i18nKey = "user")
@@ -89,6 +92,7 @@ open class TimesheetDO : DefaultBaseDO(), Comparable {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "user_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var user: PFUserDO? = null
@get:Column(name = "time_zone", length = 100)
@@ -140,6 +144,7 @@ open class TimesheetDO : DefaultBaseDO(), Comparable {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "kost2_id", nullable = true)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var kost2: Kost2DO? = null
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/user/UserPasswordDao.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/user/UserPasswordDao.kt
index ec104b2aa8..5887c6991c 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/user/UserPasswordDao.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/user/UserPasswordDao.kt
@@ -244,7 +244,7 @@ open class UserPasswordDao : BaseDao(UserPasswordDO::class.java)
val saltPepper = (pepper ?: "") + (salt ?: "")
val saltedAndPepperedPassword = ArrayUtils.addAll(saltPepper.toCharArray(), *password)
val encryptedPassword = digest(saltedAndPepperedPassword)
- LoginHandler.clearPassword(saltedAndPepperedPassword) // Clear array to to security reasons.
+ LoginHandler.clearPassword(saltedAndPepperedPassword) // Clear array due to security reasons.
return encryptedPassword
}
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/user/UserPrefDao.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/user/UserPrefDao.kt
index 84007f6d16..b5c9e3052a 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/user/UserPrefDao.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/user/UserPrefDao.kt
@@ -60,7 +60,6 @@ import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.persistence.metamodel.HibernateMetaModel.getPropertyLength
import org.projectforge.framework.persistence.user.api.ThreadLocalUserContext.loggedInUser
import org.projectforge.framework.persistence.user.api.ThreadLocalUserContext.loggedInUserId
-import org.projectforge.framework.persistence.user.api.ThreadLocalUserContext.requiredLoggedInUser
import org.projectforge.framework.persistence.user.api.ThreadLocalUserContext.requiredLoggedInUserId
import org.projectforge.framework.persistence.user.api.UserPrefArea
import org.projectforge.framework.persistence.user.api.UserPrefParameter
@@ -662,7 +661,7 @@ class UserPrefDao : BaseDao(UserPrefDO::class.java) {
fun serialize(value: Any?, compressBigContent: Boolean = true): String {
val json = try {
- MAGIC_JSON_START + getObjectMapper().writeValueAsString(value)
+ MAGIC_JSON_START + objectMapper.writeValueAsString(value)
} catch (ex: JsonProcessingException) {
log.error("Error while trying to serialize object as json: " + ex.message, ex)
""
@@ -679,54 +678,47 @@ class UserPrefDao : BaseDao(UserPrefDO::class.java) {
return StringUtils.startsWith(value, MAGIC_JSON_START)
}*/
- private fun fromJson(json: String, classOfT: Class): T? {
+ internal fun fromJson(json: String, classOfT: Class): T? {
var useJson = getUncompressed(json)
useJson = useJson.removePrefix(MAGIC_JSON_START)
// if (!isJsonObject(useJson)) return null
try {
- return getObjectMapper().readValue(useJson, classOfT)
+ return objectMapper.readValue(useJson, classOfT)
} catch (ex: IOException) {
- log.error(
- "Can't deserialize json object (may-be incompatible ProjectForge versions): " + ex.message + " json=" + useJson,
- ex
- )
+ log.error { "Can't deserialize json object (may-be incompatible ProjectForge versions): ${ex.message}, json=$useJson" }
return null
}
}
- private var objectMapper: ObjectMapper? = null
-
- fun getObjectMapper(): ObjectMapper {
- objectMapper?.let { return it }
- val mapper = ObjectMapper()
- mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
- mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
- mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
- mapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT)
- mapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false)
- mapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
- mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
-
- val module = SimpleModule()
- module.addSerializer(LocalDate::class.java, LocalDateSerializer())
- module.addDeserializer(LocalDate::class.java, LocalDateDeserializer())
-
- module.addSerializer(PFDateTime::class.java, PFDateTimeSerializer())
- module.addDeserializer(PFDateTime::class.java, PFDateTimeDeserializer())
-
- module.addSerializer(
- java.util.Date::class.java,
- UtilDateSerializer(UtilDateFormat.ISO_DATE_TIME_SECONDS)
- )
- module.addDeserializer(java.util.Date::class.java, UtilDateDeserializer())
+ val objectMapper: ObjectMapper by lazy {
+ ObjectMapper().also { mapper ->
+ mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
+ mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
+ mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
+ mapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT)
+ mapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false)
+ mapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
+ mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+
+ val module = SimpleModule()
+ module.addSerializer(LocalDate::class.java, LocalDateSerializer())
+ module.addDeserializer(LocalDate::class.java, LocalDateDeserializer())
+
+ module.addSerializer(PFDateTime::class.java, PFDateTimeSerializer())
+ module.addDeserializer(PFDateTime::class.java, PFDateTimeDeserializer())
+
+ module.addSerializer(
+ java.util.Date::class.java,
+ UtilDateSerializer(UtilDateFormat.ISO_DATE_TIME_SECONDS)
+ )
+ module.addDeserializer(java.util.Date::class.java, UtilDateDeserializer())
- module.addSerializer(Date::class.java, SqlDateSerializer())
- module.addDeserializer(Date::class.java, SqlDateDeserializer())
+ module.addSerializer(Date::class.java, SqlDateSerializer())
+ module.addDeserializer(Date::class.java, SqlDateDeserializer())
- mapper.registerModule(module)
- mapper.registerModule(KotlinModule.Builder().build())
- objectMapper = mapper
- return mapper
+ mapper.registerModule(module)
+ mapper.registerModule(KotlinModule.Builder().build())
+ }
}
internal fun getUncompressed(content: String?): String {
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/user/UserXmlPreferencesDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/user/UserXmlPreferencesDO.kt
index cbb8de7fd8..3f937cb5c1 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/user/UserXmlPreferencesDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/user/UserXmlPreferencesDO.kt
@@ -23,9 +23,11 @@
package org.projectforge.business.user
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.projectforge.framework.persistence.user.entities.PFUserDO
import java.util.*
import jakarta.persistence.*
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.user.entities.UserPrefDO.Companion.FIND_BY_USER_ID_AND_AREA
/**
@@ -51,6 +53,7 @@ class UserXmlPreferencesDO : IUserPref {
*/
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "user_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
override var user: PFUserDO? = null
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/vacation/model/LeaveAccountEntryDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/vacation/model/LeaveAccountEntryDO.kt
index 336b393ea6..c2d8fc8e59 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/vacation/model/LeaveAccountEntryDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/vacation/model/LeaveAccountEntryDO.kt
@@ -23,11 +23,13 @@
package org.projectforge.business.vacation.model
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*
import org.projectforge.business.fibu.EmployeeDO
import org.projectforge.common.anots.PropertyInfo
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import java.math.BigDecimal
import java.time.LocalDate
@@ -64,6 +66,7 @@ open class LeaveAccountEntryDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "employee_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var employee: EmployeeDO? = null
@PropertyInfo(i18nKey = "date", required = true)
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/vacation/model/RemainingLeaveDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/vacation/model/RemainingLeaveDO.kt
index dd222479cb..630cdb97ca 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/vacation/model/RemainingLeaveDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/vacation/model/RemainingLeaveDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.business.vacation.model
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmbedded
import org.projectforge.business.fibu.EmployeeDO
@@ -32,6 +33,7 @@ import java.math.BigDecimal
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency
+import org.projectforge.framework.json.IdOnlySerializer
/**
* Remaining leave entries for employees per year.
@@ -60,6 +62,7 @@ open class RemainingLeaveDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "employee_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var employee: EmployeeDO? = null
@PropertyInfo(i18nKey = "calendar.year")
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/vacation/model/VacationDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/vacation/model/VacationDO.kt
index dc583c8f74..249f7b8424 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/vacation/model/VacationDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/vacation/model/VacationDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.business.vacation.model
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField
@@ -32,6 +33,8 @@ import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDe
import org.projectforge.business.PfCaches
import org.projectforge.business.fibu.EmployeeDO
import org.projectforge.common.anots.PropertyInfo
+import org.projectforge.framework.json.IdOnlySerializer
+import org.projectforge.framework.json.IdsOnlySerializer
import org.projectforge.framework.persistence.api.AUserRightId
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.persistence.user.api.ThreadLocalUserContext
@@ -77,6 +80,7 @@ open class VacationDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "employee_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var employee: EmployeeDO? = null
@PropertyInfo(i18nKey = "vacation.startdate")
@@ -95,6 +99,7 @@ open class VacationDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "replacement_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var replacement: EmployeeDO? = null
/**
@@ -115,6 +120,7 @@ open class VacationDO : DefaultBaseDO() {
columnList = "employee_id",
)]
)
+ @JsonSerialize(using = IdsOnlySerializer::class)
open var otherReplacements: MutableSet? = null
/**
@@ -143,6 +149,7 @@ open class VacationDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "manager_id", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var manager: EmployeeDO? = null
@PropertyInfo(i18nKey = "vacation.status")
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/ToStringUtil.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/ToStringUtil.kt
index d7d42c9ad9..66e0830f0a 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/framework/ToStringUtil.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/ToStringUtil.kt
@@ -64,11 +64,21 @@ import java.time.format.DateTimeFormatter
/**
* Helper method to serialize objects as json strings and to use it in toString method.
* @param obj Object to serialize as json string.
- * @param ignoreEmbeddedSerializers Most embedded objects of type [DefaultBaseDO] are serialized in short form (id and short info field).
- * If this param contains a class of a [DefaultBaseDO], this object will be serialized with all fields.
+ * @param preferEmbeddedSerializers If true [IdOnlySerializer] and [IdsOnlySerializer] uses configured serializers instead of writing id only. Default is false.
+ * @param ignoreIdOnlySerializers If true [IdOnlySerializer] and [IdsOnlySerializer] are ignored. Default is false.
*/
-fun toJsonString(obj: Any, vararg ignoreEmbeddedSerializers: Class): String {
- return ToStringUtil.toJsonString(obj, ignoreEmbeddedSerializers, null)
+fun toJsonString(
+ obj: Any,
+ preferEmbeddedSerializers: Boolean = true,
+ ignoreIdOnlySerializers: Boolean = false,
+): String {
+ return ToStringUtil.toJsonString(
+ obj,
+ ToStringUtil.Configuration(
+ preferEmbeddedSerializers = preferEmbeddedSerializers,
+ ignoreIdOnlySerializers = ignoreIdOnlySerializers,
+ )
+ )
}
private val log = KotlinLogging.logger {}
@@ -76,6 +86,12 @@ private val log = KotlinLogging.logger {}
class ToStringUtil {
class Serializer(val clazz: Class, val serializer: JsonSerializer)
+ class Configuration(
+ var preferEmbeddedSerializers: Boolean = false,
+ var ignoreIdOnlySerializers: Boolean = false,
+ val additionalSerializers: Array>? = null
+ )
+
/**
* Helper class for having data classes with to json functionality (e. g. for logging).
*/
@@ -98,15 +114,14 @@ class ToStringUtil {
private val mapperMap = mutableMapOf()
+
/**
* Helper method to serialize objects as json strings and to use it in toString method.
* @param obj Object to serialize as json string.
- * @param ignoreEmbeddedSerializers Most embedded objects of type [DefaultBaseDO] are serialized in short form (id and short info field).
- * If this param contains a class of a [DefaultBaseDO], this object will be serialized with all fields.
*/
@JvmStatic
- fun toJsonString(obj: Any, vararg ignoreEmbeddedSerializers: Class): String {
- return toJsonString(obj, ignoreEmbeddedSerializers, null)
+ fun toJsonString(obj: Any): String {
+ return toJsonString(obj, Configuration())
}
/**
@@ -117,15 +132,21 @@ class ToStringUtil {
*/
@JvmStatic
fun toJsonStringExtended(obj: Any, vararg additionalSerializers: Serializer): String {
- return toJsonString(obj, null, additionalSerializers = additionalSerializers)
+ return toJsonString(obj, Configuration(additionalSerializers = additionalSerializers))
}
internal fun toJsonString(
- obj: Any, ignoreEmbeddedSerializers: Array>?,
- additionalSerializers: Array>?
+ obj: Any,
+ configuration: Configuration,
): String {
try {
- val mapper = getObjectMapper(obj::class.java, ignoreEmbeddedSerializers, additionalSerializers)
+ val mapper = getObjectMapper(obj::class.java, configuration)
+ if (configuration.preferEmbeddedSerializers || configuration.ignoreIdOnlySerializers) {
+ JsonThreadLocalContext.set(
+ preferEmbeddedSerializers = configuration.preferEmbeddedSerializers,
+ ignoreIdOnlySerializers = configuration.ignoreIdOnlySerializers,
+ )
+ }
return mapper.writeValueAsString(obj)
} catch (ex: Exception) {
val id = System.currentTimeMillis()
@@ -134,6 +155,8 @@ class ToStringUtil {
ex
)
return "[*** Exception while serializing object of type '${obj::class.java.simpleName}', see log files #$id for more details.]"
+ } finally {
+ JsonThreadLocalContext.clear()
}
}
@@ -142,16 +165,10 @@ class ToStringUtil {
clazz: Class,
serializer: EmbeddedDOSerializer,
objClass: Class<*>,
- ignoreEmbeddedSerializers: Array>?
) {
if (objClass.equals(clazz)) {
return // Don't use embedded serializer for current object itself.
}
- if (!ignoreEmbeddedSerializers.isNullOrEmpty()) {
- ignoreEmbeddedSerializers.forEach {
- if (it == clazz) return
- }
- }
module.addSerializer(clazz, serializer)
}
@@ -168,13 +185,13 @@ class ToStringUtil {
}
private fun getObjectMapper(
- objClass: Class<*>?, ignoreEmbeddedSerializers: Array>?,
- additionalSerializers: Array>?
+ objClass: Class<*>?,
+ configuration: Configuration,
): ObjectMapper {
val key = if (objClass != null && embeddedSerializerClasses.any { it.isAssignableFrom(objClass) }) {
- ObjectMapperKey(objClass, ignoreEmbeddedSerializers, additionalSerializers)
+ ObjectMapperKey(objClass, configuration)
} else {
- ObjectMapperKey(null, ignoreEmbeddedSerializers, additionalSerializers)
+ ObjectMapperKey(null, configuration)
}
var mapper = mapperMap[key]
if (mapper != null) {
@@ -197,18 +214,18 @@ class ToStringUtil {
module.addSerializer(AddressbookDO::class.java, AddressbookSerializer())
module.addSerializer(AbstractLazyInitializer::class.java, HibernateProxySerializer())
- additionalSerializers?.forEach {
+ configuration.additionalSerializers?.forEach {
module.addSerializer(it.clazz, it.serializer)
}
if (objClass != null) {
- register(module, GroupDO::class.java, GroupSerializer(), objClass, ignoreEmbeddedSerializers)
- register(module, Kost1DO::class.java, Kost1Serializer(), objClass, ignoreEmbeddedSerializers)
- register(module, Kost2DO::class.java, Kost2Serializer(), objClass, ignoreEmbeddedSerializers)
- register(module, KundeDO::class.java, KundeSerializer(), objClass, ignoreEmbeddedSerializers)
- register(module, PFUserDO::class.java, UserSerializer(), objClass, ignoreEmbeddedSerializers)
- register(module, EmployeeDO::class.java, EmployeeSerializer(), objClass, ignoreEmbeddedSerializers)
- register(module, ProjektDO::class.java, ProjektSerializer(), objClass, ignoreEmbeddedSerializers)
- register(module, TaskDO::class.java, TaskSerializer(), objClass, ignoreEmbeddedSerializers)
+ register(module, GroupDO::class.java, GroupSerializer(), objClass)
+ register(module, Kost1DO::class.java, Kost1Serializer(), objClass)
+ register(module, Kost2DO::class.java, Kost2Serializer(), objClass)
+ register(module, KundeDO::class.java, KundeSerializer(), objClass)
+ register(module, PFUserDO::class.java, UserSerializer(), objClass)
+ register(module, EmployeeDO::class.java, EmployeeSerializer(), objClass)
+ register(module, ProjektDO::class.java, ProjektSerializer(), objClass)
+ register(module, TaskDO::class.java, TaskSerializer(), objClass)
}
mapper.registerModule(module)
mapper.registerModule(KotlinModule.Builder().build())
@@ -316,23 +333,22 @@ class ToStringUtil {
private class ObjectMapperKey(
var objClass: Class<*>?,
- var ignoreEmbeddedSerializers: Array>?,
- var additionalSerializers: Array>?
+ val configuration: Configuration,
) {
override fun equals(other: Any?): Boolean {
other as ObjectMapperKey
return EqualsBuilder()
.append(this.objClass, other.objClass)
- .append(this.ignoreEmbeddedSerializers, other.ignoreEmbeddedSerializers)
- .append(this.additionalSerializers, other.additionalSerializers)
+ //.append(this.configuration.preferEmbeddedSerializers, other.configuration.preferEmbeddedSerializers)
+ .append(this.configuration.additionalSerializers, other.configuration.additionalSerializers)
.isEquals
}
override fun hashCode(): Int {
return HashCodeBuilder()
.append(objClass)
- .append(ignoreEmbeddedSerializers)
- .append(additionalSerializers)
+ //.append(configuration.preferEmbeddedSerializers)
+ .append(configuration.additionalSerializers)
.toHashCode()
}
}
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/access/GroupTaskAccessDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/access/GroupTaskAccessDO.kt
index 59c03f19be..75aec4c1af 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/framework/access/GroupTaskAccessDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/access/GroupTaskAccessDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.framework.access
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.apache.commons.lang3.builder.HashCodeBuilder
import org.apache.commons.lang3.builder.ToStringBuilder
@@ -34,13 +35,13 @@ import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmb
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency
import org.projectforge.business.task.TaskDO
import org.projectforge.common.anots.PropertyInfo
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.api.BaseDO
import org.projectforge.framework.persistence.api.EntityCopyStatus
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.persistence.history.PersistenceBehavior
import org.projectforge.framework.persistence.user.entities.GroupDO
import java.io.Serializable
-import java.util.*
/**
* Represents an access entry with the permissions of one group to one task. The persistent data object of
@@ -70,6 +71,7 @@ open class GroupTaskAccessDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne
@get:JoinColumn(name = "group_id")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var group: GroupDO? = null
@PropertyInfo(i18nKey = "task")
@@ -77,6 +79,7 @@ open class GroupTaskAccessDO : DefaultBaseDO() {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne
@get:JoinColumn(name = "task_id")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var task: TaskDO? = null
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdOnlySerializer.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdOnlySerializer.kt
new file mode 100644
index 0000000000..bcd30d3720
--- /dev/null
+++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdOnlySerializer.kt
@@ -0,0 +1,67 @@
+/////////////////////////////////////////////////////////////////////////////
+//
+// 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.framework.json
+
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.databind.JsonSerializer
+import com.fasterxml.jackson.databind.SerializerProvider
+import org.projectforge.framework.persistence.api.BaseDO
+import org.projectforge.framework.persistence.api.IdObject
+import java.io.IOException
+
+class IdOnlySerializer : JsonSerializer>() {
+ @Throws(IOException::class)
+ override fun serialize(value: IdObject<*>?, gen: JsonGenerator, serializers: SerializerProvider) {
+ writeObject(value, gen, serializers)
+ }
+
+ companion object {
+ internal fun writeObject(value: IdObject<*>?, gen: JsonGenerator, serializers: SerializerProvider) {
+ if (value == null) {
+ gen.writeNull()
+ return
+ }
+
+ JsonThreadLocalContext.get()?.let { ctx ->
+ if (ctx.preferEmbeddedSerializers == true || ctx.ignoreIdOnlySerializers == true) {
+ // Check if another serializer exists
+ val existingSerializer = serializers.findValueSerializer(value.javaClass, null)
+ if (existingSerializer != null && existingSerializer.javaClass != IdOnlySerializer::class.java) {
+ existingSerializer.serialize(value, gen, serializers)
+ return
+ }
+ // Let Jackson serialize the value:
+ serializers.defaultSerializeValue(value, gen)
+ return
+ }
+ }
+ gen.writeStartObject()
+ value.id.let { id ->
+ JsonUtils.writeField(gen, "id", id)
+ }
+ gen.writeEndObject()
+
+ }
+ }
+}
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdsOnlySerializer.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdsOnlySerializer.kt
new file mode 100644
index 0000000000..ec29a6ff2f
--- /dev/null
+++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdsOnlySerializer.kt
@@ -0,0 +1,53 @@
+/////////////////////////////////////////////////////////////////////////////
+//
+// 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.framework.json
+
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.databind.JsonSerializer
+import com.fasterxml.jackson.databind.SerializerProvider
+import org.hibernate.Hibernate
+import org.projectforge.framework.persistence.api.IdObject
+import java.io.IOException
+
+class IdsOnlySerializer : JsonSerializer>() {
+ @Throws(IOException::class)
+ override fun serialize(value: Collection<*>?, gen: JsonGenerator, serializers: SerializerProvider) {
+ if (value == null) {
+ gen.writeNull()
+ } else if (Hibernate.isInitialized(value)) {
+ gen.writeStartArray()
+ value.forEach { item ->
+ if (item is IdObject<*>) {
+ IdOnlySerializer.writeObject(item, gen, serializers)
+ } else {
+ // Let Jackson serialize the value:
+ serializers.defaultSerializeValue(value, gen)
+ }
+ }
+ gen.writeEndArray()
+ } else {
+ // Do nothing, collection not available. Don't fetch it.
+ }
+ }
+}
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/json/JsonThreadLocalContext.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/json/JsonThreadLocalContext.kt
new file mode 100644
index 0000000000..98a2bac1ba
--- /dev/null
+++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/json/JsonThreadLocalContext.kt
@@ -0,0 +1,54 @@
+/////////////////////////////////////////////////////////////////////////////
+//
+// 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.framework.json
+
+/**
+ * Thread local context for JSON serialization.
+ * This context is used to control the behavior of the [IdOnlySerializer].
+ */
+class JsonThreadLocalContext(
+ val preferEmbeddedSerializers: Boolean = false,
+ val ignoreIdOnlySerializers: Boolean = false,
+) {
+ companion object {
+ private val threadLocal = ThreadLocal()
+
+ fun get(): JsonThreadLocalContext? {
+ return threadLocal.get()
+ }
+
+ fun set(preferEmbeddedSerializers: Boolean = false, ignoreIdOnlySerializers: Boolean = false) {
+ threadLocal.set(
+ JsonThreadLocalContext(
+ preferEmbeddedSerializers = preferEmbeddedSerializers,
+ ignoreIdOnlySerializers = ignoreIdOnlySerializers,
+ )
+ )
+ }
+
+ fun clear() {
+ threadLocal.remove()
+ }
+ }
+}
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/json/JsonUtils.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/json/JsonUtils.kt
index 3e86fa7d6d..032b4d8b8d 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/framework/json/JsonUtils.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/json/JsonUtils.kt
@@ -24,6 +24,7 @@
package org.projectforge.framework.json
import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.DeserializationFeature
@@ -45,82 +46,101 @@ private val log = KotlinLogging.logger {}
* @author Kai Reinhard (k.reinhard@micromata.de)
*/
object JsonUtils {
- private val typeAdapterMap: MutableMap, Any> = HashMap()
- private val objectMapper: ObjectMapper = ObjectMapper()
- private val objectMapperIgnoreNullableProps: ObjectMapper = ObjectMapper()
- private val objectMapperIgnoreUnknownProps: ObjectMapper = ObjectMapper()
-
- init {
- objectMapper.registerModule(KotlinModule.Builder().build())
- objectMapperIgnoreNullableProps.registerModule(KotlinModule.Builder().build())
- val module = SimpleModule()
- initializeMapper(module)
- objectMapper.registerModule(module)
- objectMapperIgnoreNullableProps.registerModule(module)
- objectMapperIgnoreNullableProps.setSerializationInclusion(JsonInclude.Include.NON_NULL)
- objectMapperIgnoreUnknownProps.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
- }
-
- fun add(cls: Class<*>, typeAdapter: Any) {
- typeAdapterMap[cls] = typeAdapter
- }
-
- @JvmStatic
- @JvmOverloads
- fun toJson(obj: Any?, ignoreNullableProps: Boolean = false): String {
- return try {
- if (ignoreNullableProps) {
- objectMapperIgnoreNullableProps.writeValueAsString(obj)
- } else {
- objectMapper.writeValueAsString(obj)
- }
- } catch (ex: JsonProcessingException) {
- log.error(ex.message, ex)
- ""
+ private val typeAdapterMap: MutableMap, Any> = HashMap()
+ private val objectMapper: ObjectMapper = ObjectMapper()
+ private val objectMapperIgnoreNullableProps: ObjectMapper = ObjectMapper()
+ private val objectMapperIgnoreUnknownProps: ObjectMapper = ObjectMapper()
+
+ init {
+ objectMapper.registerModule(KotlinModule.Builder().build())
+ objectMapperIgnoreNullableProps.registerModule(KotlinModule.Builder().build())
+ val module = SimpleModule()
+ initializeMapper(module)
+ objectMapper.registerModule(module)
+ objectMapperIgnoreNullableProps.registerModule(module)
+ objectMapperIgnoreNullableProps.setSerializationInclusion(JsonInclude.Include.NON_NULL)
+ objectMapperIgnoreUnknownProps.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
}
- }
-
- @JvmStatic
- @JvmOverloads
- @Throws(IOException::class)
- fun fromJson(json: String?, classOfT: Class?, failOnUnknownProps: Boolean = true): T? {
- return if (failOnUnknownProps) {
- objectMapper.readValue(json, classOfT)
- } else {
- objectMapperIgnoreUnknownProps.readValue(json, classOfT)
+
+ fun add(cls: Class<*>, typeAdapter: Any) {
+ typeAdapterMap[cls] = typeAdapter
}
- }
-
- @JvmStatic
- @JvmOverloads
- @Throws(IOException::class)
- fun fromJson(json: String?, typeReference: TypeReference?, failOnUnknownProps: Boolean = true): T? {
- return if (failOnUnknownProps) {
- objectMapper.readValue(json, typeReference)
- } else {
- objectMapperIgnoreUnknownProps.readValue(json, typeReference)
+
+ @JvmStatic
+ @JvmOverloads
+ fun toJson(obj: Any?, ignoreNullableProps: Boolean = false): String {
+ return try {
+ if (ignoreNullableProps) {
+ objectMapperIgnoreNullableProps.writeValueAsString(obj)
+ } else {
+ objectMapper.writeValueAsString(obj)
+ }
+ } catch (ex: JsonProcessingException) {
+ log.error(ex.message, ex)
+ ""
+ }
}
- }
- fun initializeMapper(module: SimpleModule) {
- module.addSerializer(LocalDate::class.java, LocalDateSerializer())
- module.addDeserializer(LocalDate::class.java, LocalDateDeserializer())
+ @JvmStatic
+ @JvmOverloads
+ @Throws(IOException::class)
+ fun fromJson(json: String?, classOfT: Class?, failOnUnknownProps: Boolean = true): T? {
+ return if (failOnUnknownProps) {
+ objectMapper.readValue(json, classOfT)
+ } else {
+ objectMapperIgnoreUnknownProps.readValue(json, classOfT)
+ }
+ }
- module.addSerializer(LocalTime::class.java, LocalTimeSerializer())
- module.addDeserializer(LocalTime::class.java, LocalTimeDeserializer())
+ @JvmStatic
+ @JvmOverloads
+ @Throws(IOException::class)
+ fun fromJson(json: String?, typeReference: TypeReference?, failOnUnknownProps: Boolean = true): T? {
+ return if (failOnUnknownProps) {
+ objectMapper.readValue(json, typeReference)
+ } else {
+ objectMapperIgnoreUnknownProps.readValue(json, typeReference)
+ }
+ }
- module.addSerializer(PFDateTime::class.java, PFDateTimeSerializer())
- module.addDeserializer(PFDateTime::class.java, PFDateTimeDeserializer())
+ fun initializeMapper(module: SimpleModule) {
+ module.addSerializer(LocalDate::class.java, LocalDateSerializer())
+ module.addDeserializer(LocalDate::class.java, LocalDateDeserializer())
- module.addSerializer(java.util.Date::class.java, UtilDateSerializer(UtilDateFormat.JS_DATE_TIME_MILLIS))
- module.addDeserializer(java.util.Date::class.java, UtilDateDeserializer())
+ module.addSerializer(LocalTime::class.java, LocalTimeSerializer())
+ module.addDeserializer(LocalTime::class.java, LocalTimeDeserializer())
- module.addSerializer(Timestamp::class.java, TimestampSerializer(UtilDateFormat.JS_DATE_TIME_MILLIS))
- module.addDeserializer(Timestamp::class.java, TimestampDeserializer())
+ module.addSerializer(PFDateTime::class.java, PFDateTimeSerializer())
+ module.addDeserializer(PFDateTime::class.java, PFDateTimeDeserializer())
- module.addSerializer(java.sql.Date::class.java, SqlDateSerializer())
- module.addDeserializer(java.sql.Date::class.java, SqlDateDeserializer())
+ module.addSerializer(java.util.Date::class.java, UtilDateSerializer(UtilDateFormat.JS_DATE_TIME_MILLIS))
+ module.addDeserializer(java.util.Date::class.java, UtilDateDeserializer())
+ module.addSerializer(Timestamp::class.java, TimestampSerializer(UtilDateFormat.JS_DATE_TIME_MILLIS))
+ module.addDeserializer(Timestamp::class.java, TimestampDeserializer())
- }
+ module.addSerializer(java.sql.Date::class.java, SqlDateSerializer())
+ module.addDeserializer(java.sql.Date::class.java, SqlDateDeserializer())
+ }
+
+ /**
+ * Writes the id by using methods [JsonGenerator.writeNullField], [JsonGenerator.writeNumberField] or
+ * [JsonGenerator.writeString] dependent on type of id.
+ * @param gen the json generator
+ * @param field the field name.
+ * @param value the id to write.
+ */
+ fun writeField(gen: JsonGenerator, field: String, value: Any?) {
+ if (value == null) {
+ gen.writeNullField(field)
+ } else if (value is Long) {
+ gen.writeNumberField(field, value)
+ } else if (value is Int) {
+ gen.writeNumberField(field, value)
+ } else if (value is String) {
+ gen.writeStringField(field, value)
+ } else {
+ gen.writeStringField(field, "$value")
+ }
+ }
}
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/MagicFilter.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/MagicFilter.kt
index 2650078854..b798910268 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/MagicFilter.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/MagicFilter.kt
@@ -131,7 +131,7 @@ class MagicFilter(
}
fun clone(): MagicFilter {
- val mapper = UserPrefDao.getObjectMapper()
+ val mapper = UserPrefDao.objectMapper
val json = mapper.writeValueAsString(this)
return mapper.readValue(json, MagicFilter::class.java)
}
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/history/HistoryEntryAttrDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/history/HistoryEntryAttrDO.kt
index 4e672d59a7..90ea8ba7eb 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/history/HistoryEntryAttrDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/history/HistoryEntryAttrDO.kt
@@ -24,8 +24,10 @@
package org.projectforge.framework.persistence.history
import com.fasterxml.jackson.annotation.JsonBackReference
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.api.HibernateUtils
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
@@ -75,6 +77,7 @@ class HistoryEntryAttrDO : HistoryEntryAttr {
@JsonBackReference
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "master_fk", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
var parent: HistoryEntryDO? = null
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/GroupDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/GroupDO.kt
index a6ab3b8014..6b2e778a2d 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/GroupDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/GroupDO.kt
@@ -23,6 +23,7 @@
package org.projectforge.framework.persistence.user.entities
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.apache.commons.lang3.builder.HashCodeBuilder
import org.hibernate.Hibernate
import org.projectforge.common.StringHelper
@@ -37,6 +38,8 @@ import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextFi
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmbedded
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency
+import org.projectforge.framework.json.IdOnlySerializer
+import org.projectforge.framework.json.IdsOnlySerializer
/**
* @author Kai Reinhard (k.reinhard@micromata.de)
@@ -131,6 +134,7 @@ open class GroupDO : DefaultBaseDO(), DisplayNameCapable {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToMany(targetEntity = PFUserDO::class, fetch = FetchType.LAZY)
@get:JoinTable(name = "T_GROUP_USER", joinColumns = [JoinColumn(name = "GROUP_ID")], inverseJoinColumns = [JoinColumn(name = "USER_ID")], indexes = [jakarta.persistence.Index(name = "idx_fk_t_group_user_group_id", columnList = "group_id"), jakarta.persistence.Index(name = "idx_fk_t_group_user_user_id", columnList = "user_id")])
+ @JsonSerialize(using = IdsOnlySerializer::class)
open var assignedUsers: MutableSet? = null
@PropertyInfo(i18nKey = "group.owner")
@@ -138,6 +142,7 @@ open class GroupDO : DefaultBaseDO(), DisplayNameCapable {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "group_owner_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var groupOwner: PFUserDO? = null
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/PFUserDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/PFUserDO.kt
index f4d934a121..3c2ba2aed3 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/PFUserDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/PFUserDO.kt
@@ -23,8 +23,7 @@
package org.projectforge.framework.persistence.user.entities
-import com.fasterxml.jackson.annotation.JsonIdentityInfo
-import com.fasterxml.jackson.annotation.ObjectIdGenerators
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.ValueBridgeRef
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField
@@ -34,6 +33,7 @@ import org.projectforge.business.common.HibernateSearchPhoneNumberBridge
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.framework.DisplayNameCapable
import org.projectforge.framework.configuration.Configuration
+import org.projectforge.framework.json.IdsOnlySerializer
import org.projectforge.framework.persistence.api.IUserRightId
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import org.projectforge.framework.persistence.history.NoHistory
@@ -59,7 +59,6 @@ import java.util.*
query = "from PFUserDO where username=:username and id<>:id"
)
)
-@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
open class PFUserDO : DefaultBaseDO(), DisplayNameCapable {
override val displayName: String
@@ -288,6 +287,7 @@ open class PFUserDO : DefaultBaseDO(), DisplayNameCapable {
orphanRemoval = false,
mappedBy = "user"
) // No cascade, because the rights are managed by the UserRightDao.
+ @JsonSerialize(using = IdsOnlySerializer::class)
open var rights: MutableSet? = mutableSetOf()
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserAuthenticationsDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserAuthenticationsDO.kt
index 0990dbaef6..517119c48e 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserAuthenticationsDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserAuthenticationsDO.kt
@@ -23,11 +23,13 @@
package org.projectforge.framework.persistence.user.entities
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.projectforge.business.user.UserTokenType
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import java.util.*
import jakarta.persistence.*
+import org.projectforge.framework.json.IdOnlySerializer
/**
* Users may have several authentication tokens, e. g. for CardDAV/CalDAV-Clients or other clients. ProjectForge shows the usage of this tokens and such tokens
@@ -98,6 +100,7 @@ open class UserAuthenticationsDO : DefaultBaseDO() {
@PropertyInfo(i18nKey = "user")
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "user_id")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var user: PFUserDO? = null
val userId: Long?
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserPasswordDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserPasswordDO.kt
index 320079f8a1..0a3a85748c 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserPasswordDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserPasswordDO.kt
@@ -24,10 +24,12 @@
package org.projectforge.framework.persistence.user.entities
import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import mu.KotlinLogging
import org.projectforge.common.anots.PropertyInfo
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import jakarta.persistence.*
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.history.NoHistory
private val log = KotlinLogging.logger {}
@@ -53,6 +55,7 @@ open class UserPasswordDO : DefaultBaseDO() {
@PropertyInfo(i18nKey = "user")
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "user_id")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var user: PFUserDO? = null
/**
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserPrefDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserPrefDO.kt
index 1e19d5c3dd..6cebef7dc0 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserPrefDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserPrefDO.kt
@@ -25,6 +25,7 @@
package org.projectforge.framework.persistence.user.entities
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import mu.KotlinLogging
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate
@@ -35,6 +36,7 @@ import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDe
import org.projectforge.business.user.IUserPref
import org.projectforge.business.user.UserPrefAreaRegistry
import org.projectforge.common.StringHelper
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.api.BaseDO
import org.projectforge.framework.persistence.api.EntityCopyStatus
import org.projectforge.framework.persistence.entities.AbstractBaseDO
@@ -102,6 +104,7 @@ class UserPrefDO : AbstractBaseDO(), IUserPref {
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "user_fk", nullable = false)
+ @JsonSerialize(using = IdOnlySerializer::class)
override var user: PFUserDO? = null
@FullTextField
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserRightDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserRightDO.kt
index 4bd75c303e..30549c97b1 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserRightDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/user/entities/UserRightDO.kt
@@ -23,8 +23,7 @@
package org.projectforge.framework.persistence.user.entities
-import com.fasterxml.jackson.annotation.JsonIdentityInfo
-import com.fasterxml.jackson.annotation.ObjectIdGenerators
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import jakarta.persistence.*
import org.apache.commons.lang3.builder.HashCodeBuilder
import org.apache.commons.lang3.builder.ToStringBuilder
@@ -34,6 +33,7 @@ import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmb
import org.projectforge.business.user.UserRightId
import org.projectforge.business.user.UserRightValue
import org.projectforge.framework.DisplayNameCapable
+import org.projectforge.framework.json.IdOnlySerializer
import org.projectforge.framework.persistence.api.IUserRightId
import org.projectforge.framework.persistence.entities.DefaultBaseDO
import java.io.Serializable
@@ -49,7 +49,6 @@ import java.io.Serializable
NamedQuery(name = UserRightDO.FIND_ALL_ORDERED, query = "from UserRightDO order by user.id, rightIdString"),
NamedQuery(name = UserRightDO.FIND_ALL_BY_USER_ID, query = "from UserRightDO where user.id=:userId"),
)
-@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
open class UserRightDO : DefaultBaseDO, Comparable, Serializable, DisplayNameCapable {
/**
* Only for storing the right id in the data base.
@@ -66,6 +65,7 @@ open class UserRightDO : DefaultBaseDO, Comparable, Serializable, D
@get:JoinColumn(name = "user_fk", nullable = false)
@get:ManyToOne(fetch = FetchType.LAZY)
@IndexedEmbedded(includeDepth = 1)
+ @JsonSerialize(using = IdOnlySerializer::class)
open var user: PFUserDO? = null
constructor()
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/security/webauthn/WebAuthnEntryDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/security/webauthn/WebAuthnEntryDO.kt
index c2c2b036eb..9699bc5b9d 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/security/webauthn/WebAuthnEntryDO.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/security/webauthn/WebAuthnEntryDO.kt
@@ -24,6 +24,7 @@
package org.projectforge.security.webauthn
import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.webauthn4j.authenticator.Authenticator
import com.webauthn4j.authenticator.AuthenticatorImpl
import com.webauthn4j.converter.AttestedCredentialDataConverter
@@ -36,6 +37,7 @@ import org.projectforge.common.anots.PropertyInfo
import org.projectforge.framework.persistence.user.entities.PFUserDO
import java.util.*
import jakarta.persistence.*
+import org.projectforge.framework.json.IdOnlySerializer
@Entity
@Indexed
@@ -78,6 +80,7 @@ open class WebAuthnEntryDO {
@get:ManyToOne(fetch = FetchType.LAZY)
@get:JoinColumn(name = "owner_fk")
+ @JsonSerialize(using = IdOnlySerializer::class)
open var owner: PFUserDO? = null
@get:Column(length = 4000, name = "credential_id")
diff --git a/projectforge-business/src/test/kotlin/org/projectforge/business/calendar/CalendarFilterFavoritesTest.kt b/projectforge-business/src/test/kotlin/org/projectforge/business/calendar/CalendarFilterFavoritesTest.kt
index 6ae36d1dac..ce09164766 100644
--- a/projectforge-business/src/test/kotlin/org/projectforge/business/calendar/CalendarFilterFavoritesTest.kt
+++ b/projectforge-business/src/test/kotlin/org/projectforge/business/calendar/CalendarFilterFavoritesTest.kt
@@ -47,7 +47,7 @@ class CalendarFilterFavoritesTest {
"{\"type\":\"org.projectforge.business.calendar.CalendarFilter\",\"name\":\"Standard\",\"id\":8,\"defaultCalendarId\":-1,\"showStatistics\":true,\"timesheetUserId\":2,\"showTimesheets\":true,\"showBreaks\":true,\"showPlanning\":true,\"calendarIds\":[1240526,1240528],\"invisibleCalendars\":[1240528]},"+
"{\"type\":\"org.projectforge.business.calendar.CalendarFilter\",\"name\":\"Stéphanie\",\"id\":9,\"defaultCalendarId\":-1,\"calendarIds\":[1245916,1245918]}," +
"{\"type\":\"org.projectforge.business.calendar.CalendarFilter\",\"name\":\"Urlaub\",\"id\":10,\"defaultCalendarId\":-1,\"showBreaks\":true,\"calendarIds\":[1240530]}]}"
- val favorites = UserPrefDao.getObjectMapper().readValue(json, Favorites::class.java)
+ val favorites = UserPrefDao.objectMapper.readValue(json, Favorites::class.java)
assertEquals(11, favorites.favoriteNames.size)
}
diff --git a/projectforge-business/src/test/kotlin/org/projectforge/business/user/UserPrefDaoTest.kt b/projectforge-business/src/test/kotlin/org/projectforge/business/user/UserPrefDaoTest.kt
new file mode 100644
index 0000000000..c01849923f
--- /dev/null
+++ b/projectforge-business/src/test/kotlin/org/projectforge/business/user/UserPrefDaoTest.kt
@@ -0,0 +1,77 @@
+/////////////////////////////////////////////////////////////////////////////
+//
+// 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.business.user
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import org.projectforge.business.test.AbstractTestBase
+import org.projectforge.business.vacation.model.VacationDO
+import org.projectforge.framework.json.JsonTestUtils
+
+class UserPrefDaoTest : AbstractTestBase() {
+ @Test
+ fun `test deserialization of vacation`() {
+ val json = """{
+ | "employee": {
+ | "id": 1
+ | },
+ | "replacement": {
+ | "id": 2
+ | },
+ | "manager": {
+ | "id": 2
+ | }
+ |}""".trimMargin()
+ UserPrefDao.fromJson(json, VacationDO::class.java).let { vacation ->
+ Assertions.assertNotNull(vacation)
+ Assertions.assertEquals(1, vacation!!.employee?.id)
+ Assertions.assertEquals(2, vacation.replacement?.id)
+ Assertions.assertEquals(2, vacation.manager?.id)
+
+ }
+ }
+
+ @Test
+ fun `test serialization of vacation`() {
+ val test = JsonTestUtils()
+ val json = UserPrefDao.serialize(test.vacation)
+ Assertions.assertTrue { json.contains("\"comment\":\"This is a comment\"") }
+ Assertions.assertTrue { json.contains("\"employee\":{\"id\":101}") }
+ Assertions.assertTrue { json.contains("\"replacement\":{\"id\":102}") }
+ Assertions.assertTrue { json.contains("\"manager\":{\"id\":102}") }
+ Assertions.assertTrue { json.contains("\"otherReplacements\":[{\"id\":102},{\"id\":103}]") }
+
+ UserPrefDao.fromJson(json, VacationDO::class.java).let {
+ Assertions.assertNotNull(it)
+ Assertions.assertEquals(5, it!!.id)
+ Assertions.assertEquals("This is a comment", it.comment)
+ Assertions.assertEquals(101, it.employee?.id)
+ Assertions.assertEquals(102, it.replacement?.id)
+ Assertions.assertEquals(102, it.manager?.id)
+ Assertions.assertEquals(2, it.otherReplacements!!.size)
+ Assertions.assertTrue(it.otherReplacements!!.any { it.id == 102L })
+ Assertions.assertTrue(it.otherReplacements!!.any { it.id == 103L })
+ }
+ }
+}
diff --git a/projectforge-business/src/test/kotlin/org/projectforge/framework/ToStringUtilTest.kt b/projectforge-business/src/test/kotlin/org/projectforge/framework/ToStringUtilTest.kt
new file mode 100644
index 0000000000..ebce179b02
--- /dev/null
+++ b/projectforge-business/src/test/kotlin/org/projectforge/framework/ToStringUtilTest.kt
@@ -0,0 +1,84 @@
+/////////////////////////////////////////////////////////////////////////////
+//
+// 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.framework
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import org.projectforge.business.test.AbstractTestBase
+import org.projectforge.business.vacation.model.VacationDO
+import org.projectforge.framework.json.JsonTestUtils
+import org.projectforge.framework.json.JsonUtils
+
+class ToStringUtilTest : AbstractTestBase() {
+ @Test
+ fun `test IdOnlySerializers`() {
+ val test = JsonTestUtils()
+ var json = toJsonString(test.vacation, preferEmbeddedSerializers = false, ignoreIdOnlySerializers = false)
+ JsonUtils.fromJson(json, test.vacation.javaClass).let { vacation ->
+ assertVacation(vacation)
+ }
+ Assertions.assertFalse(
+ json.contains("Abteilung"),
+ "Abteilung shouldn't be serialized, only id's of employee's expected."
+ )
+ Assertions.assertFalse(
+ json.contains("user"),
+ "usernames shouldn't be serialized, only id's of user's expected."
+ )
+ json = toJsonString(test.vacation, preferEmbeddedSerializers = true, ignoreIdOnlySerializers = false)
+ JsonUtils.fromJson(json, test.vacation.javaClass, failOnUnknownProps = false).let { vacation ->
+ assertVacation(vacation)
+ }
+ Assertions.assertFalse(
+ json.contains("Abteilung"),
+ "Abteilung shouldn't be serialized, only id's of employee's expected."
+ )
+ Assertions.assertTrue(
+ json.contains("user"),
+ "usernames should be serialized, because of PFUserDO serializer."
+ )
+ json = toJsonString(test.vacation, preferEmbeddedSerializers = false, ignoreIdOnlySerializers = true)
+ JsonUtils.fromJson(json, test.vacation.javaClass, failOnUnknownProps = false).let { vacation ->
+ assertVacation(vacation)
+ }
+ Assertions.assertFalse(
+ json.contains("Abteilung"),
+ "Abteilung shouldn't be serialized, only id's of employee's expected."
+ )
+ Assertions.assertTrue(
+ json.contains("user"),
+ "usernames should be serialized, because of PFUserDO serializer."
+ )
+ }
+
+ private fun assertVacation(vacation: VacationDO?) {
+ Assertions.assertNotNull(vacation)
+ Assertions.assertEquals("This is a comment", vacation!!.comment)
+ Assertions.assertEquals(101L, vacation.employee?.id)
+ Assertions.assertEquals(102L, vacation.manager?.id)
+ Assertions.assertEquals(102L, vacation.replacement?.id)
+ val others = vacation.otherReplacements
+ Assertions.assertEquals(2, others!!.size)
+ }
+}
diff --git a/projectforge-business/src/test/kotlin/org/projectforge/framework/json/JsonTestUtils.kt b/projectforge-business/src/test/kotlin/org/projectforge/framework/json/JsonTestUtils.kt
new file mode 100644
index 0000000000..96836364b4
--- /dev/null
+++ b/projectforge-business/src/test/kotlin/org/projectforge/framework/json/JsonTestUtils.kt
@@ -0,0 +1,63 @@
+/////////////////////////////////////////////////////////////////////////////
+//
+// 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.framework.json
+
+import org.projectforge.business.fibu.EmployeeDO
+import org.projectforge.business.vacation.model.VacationDO
+import org.projectforge.framework.persistence.user.entities.PFUserDO
+
+class JsonTestUtils {
+ val employee1 = EmployeeDO().also { employee ->
+ employee.id = 101
+ employee.abteilung = "Abteilung 1"
+ employee.user = PFUserDO().also {
+ it.id = 1
+ it.username = "user1"
+ }
+ }
+ val employee2 = EmployeeDO().also { employee ->
+ employee.id = 102
+ employee.abteilung = "Abteilung 2"
+ employee.user = PFUserDO().also {
+ it.id = 2
+ it.username = "user2"
+ }
+ }
+ val employee3 = EmployeeDO().also { employee ->
+ employee.id = 103
+ employee.abteilung = "Abteilung 3"
+ employee.user = PFUserDO().also {
+ it.id = 3
+ it.username = "user3"
+ }
+ }
+ val vacation = VacationDO().also {
+ it.id = 5
+ it.comment = "This is a comment"
+ it.employee = employee1
+ it.manager = employee2
+ it.replacement = employee2
+ it.otherReplacements = mutableSetOf(employee2, employee3)
+ }
+}
diff --git a/projectforge-business/src/test/kotlin/org/projectforge/framework/json/JsonValidatorTest.kt b/projectforge-business/src/test/kotlin/org/projectforge/framework/json/JsonValidatorTest.kt
index 14c19196f5..a3992c6b75 100644
--- a/projectforge-business/src/test/kotlin/org/projectforge/framework/json/JsonValidatorTest.kt
+++ b/projectforge-business/src/test/kotlin/org/projectforge/framework/json/JsonValidatorTest.kt
@@ -50,8 +50,9 @@ class JsonValidatorTest : AbstractTestBase() {
Assertions.assertEquals("kai", jsonValidator.get("responsibleUser.username"))
Assertions.assertNull(jsonValidator.get("responsibleUser.firstname"), "Firstname shouldn't be serialized.")
- jsonValidator = JsonValidator(toJsonString(task, PFUserDO::class.java))
- Assertions.assertEquals("Kai", jsonValidator.get("responsibleUser.firstname"), "Firstname shouldn't be ignored.")
+ jsonValidator =
+ JsonValidator(toJsonString(task, preferEmbeddedSerializers = false, ignoreIdOnlySerializers = true))
+ Assertions.assertEquals("kai", jsonValidator.get("responsibleUser.username"))
val timesheet = TimesheetDO()
timesheet.user = user
@@ -61,11 +62,16 @@ class JsonValidatorTest : AbstractTestBase() {
Assertions.assertEquals(42.0, jsonValidator.getDouble("user.id"))
Assertions.assertEquals("kai", jsonValidator.get("user.username"))
Assertions.assertNull(jsonValidator.get("user.firstname"), "Firstname shouldn't be serialized.")
+
+ jsonValidator = JsonValidator(toJsonString(timesheet, preferEmbeddedSerializers = false))
+ Assertions.assertEquals(42.0, jsonValidator.getDouble("user.id"))
+ Assertions.assertNull(jsonValidator.get("user.username"), "Username shouldn't be serialized.")
}
@Test
fun parseJsonTest() {
- val jsonValidator = JsonValidator("{'fruit1':'apple','fruit2':'orange','basket':{'fruit3':'cherry','fruit4':'banana'},'actions':[{'id':'cancel','title':'Abbrechen','style':'danger','type':'button','key':'el-20'},{'id':'create','title':'Anlegen','style':'primary','type':'button','key':'el-21'}]}")
+ val jsonValidator =
+ JsonValidator("{'fruit1':'apple','fruit2':'orange','basket':{'fruit3':'cherry','fruit4':'banana'},'actions':[{'id':'cancel','title':'Abbrechen','style':'danger','type':'button','key':'el-20'},{'id':'create','title':'Anlegen','style':'primary','type':'button','key':'el-21'}]}")
Assertions.assertEquals("apple", jsonValidator.get("fruit1"))
Assertions.assertEquals("orange", jsonValidator.get("fruit2"))
Assertions.assertEquals("cherry", jsonValidator.get("basket.fruit3"))
diff --git a/projectforge-business/src/test/kotlin/org/projectforge/framework/persistence/api/MagicFilterTest.kt b/projectforge-business/src/test/kotlin/org/projectforge/framework/persistence/api/MagicFilterTest.kt
index ae137cabdd..f32fd8ebc3 100644
--- a/projectforge-business/src/test/kotlin/org/projectforge/framework/persistence/api/MagicFilterTest.kt
+++ b/projectforge-business/src/test/kotlin/org/projectforge/framework/persistence/api/MagicFilterTest.kt
@@ -33,9 +33,9 @@ class MagicFilterTest {
fun serializationTest() {
val filter = MagicFilter()
filter.entries.add(MagicFilterEntry("zipCode", "12345"))
- val om = UserPrefDao.getObjectMapper()
- var json = om.writeValueAsString(filter)
- var obj = om.readValue(json, MagicFilter::class.java) as MagicFilter
+ val om = UserPrefDao.objectMapper
+ val json = om.writeValueAsString(filter)
+ val obj = om.readValue(json, MagicFilter::class.java) as MagicFilter
Assertions.assertEquals(1, obj.entries.size)
Assertions.assertEquals("zipCode", obj.entries[0].field)
Assertions.assertEquals("12345", obj.entries[0].value.value)
diff --git a/projectforge-business/src/testFixtures/kotlin/org/projectforge/framework/json/JsonValidator.kt b/projectforge-business/src/testFixtures/kotlin/org/projectforge/framework/json/JsonValidator.kt
index 3435e08394..53cde503e9 100644
--- a/projectforge-business/src/testFixtures/kotlin/org/projectforge/framework/json/JsonValidator.kt
+++ b/projectforge-business/src/testFixtures/kotlin/org/projectforge/framework/json/JsonValidator.kt
@@ -25,6 +25,7 @@ package org.projectforge.framework.json
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
+import org.jetbrains.kotlin.ir.types.IdSignatureValues.result
import kotlin.collections.get
class JsonValidator(val json: String) {
@@ -89,19 +90,20 @@ class JsonValidator(val json: String) {
}
fun getDouble(path: String): Double? {
- val result = getElement(path)
- if (result == null)
- return null
+ val result = getElement(path) ?: return null
if (result is Double) {
return result
}
throw java.lang.IllegalArgumentException("Requested element of path '${path}' isn't of type Double: '${result::class.java}'.")
}
+ fun getLong(path: String): Long? {
+ val double = getDouble(path) ?: return null
+ return double.toLong() // Gson uses Double for all numbers.
+ }
+
fun getBoolean(path: String): Boolean? {
- val result = getElement(path)
- if (result == null)
- return null
+ val result = getElement(path) ?: return null
if (result is Boolean) {
return result
}
@@ -109,9 +111,7 @@ class JsonValidator(val json: String) {
}
fun getList(path: String): List<*>? {
- val result = getElement(path)
- if (result == null)
- return null
+ val result = getElement(path) ?: return null
if (result is List<*>) {
return result
}
@@ -119,9 +119,7 @@ class JsonValidator(val json: String) {
}
fun getMap(path: String): Map? {
- val result = getElement(path)
- if (result == null)
- return null
+ val result = getElement(path) ?: return null
if (result is Map<*, *>) {
@Suppress("UNCHECKED_CAST")
return result as Map
@@ -137,12 +135,12 @@ class JsonValidator(val json: String) {
if (currentMap == null) {
throw IllegalArgumentException("Can't step so deep: '${path}'. '${it}' doesn't exist.")
}
- if (it.isNullOrBlank())
+ if (it.isBlank())
throw IllegalArgumentException("Illegal path: '${path}' contains empty attributes such as 'a..b'.")
- var idx: Int?;
+ val idx: Int?;
var attr = it
- var value: Any?
+ val value: Any?
if (it.indexOf('[') > 0) {
// Array found:
if (!it.matches(attrRegexWithIndex)) {
diff --git a/projectforge-common/src/main/java/org/projectforge/Version.java b/projectforge-common/src/main/java/org/projectforge/Version.java
index cb2cb6ce6e..1a6aeeba67 100644
--- a/projectforge-common/src/main/java/org/projectforge/Version.java
+++ b/projectforge-common/src/main/java/org/projectforge/Version.java
@@ -289,8 +289,12 @@ private void asString()
private int parseInt(final String version, final String str)
{
+ if (version != null && version.contains("gradle.version")) {
+ log.info("Not running in productive environment: version string is '?gradle.version?', assuming 0.");
+ return 0;
+ }
try {
- return Integer.valueOf(str);
+ return Integer.parseInt(str);
} catch (final NumberFormatException ex) {
log.error("Can't parse version string '" + version + "'. '" + str + "'isn't a number");
}
diff --git a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/NodeInfo.kt b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/NodeInfo.kt
index 98cec990f1..0dbb769243 100644
--- a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/NodeInfo.kt
+++ b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/NodeInfo.kt
@@ -40,27 +40,35 @@ class NodeInfo() {
node.parent.path
}
if (recursive) {
- node.nodes?.let {
- val nodes = mutableListOf()
- while (it.hasNext()) {
- val child = it.nextNode()
- if (PFJcrUtils.matchAnyPath(child, listOfIgnoredNodePaths)) {
- log.info { "Ignore path=${child.path} as configured." }
- continue
+ try {
+ node.nodes?.let {
+ val nodes = mutableListOf()
+ while (it.hasNext()) {
+ val child = it.nextNode()
+ if (PFJcrUtils.matchAnyPath(child, listOfIgnoredNodePaths)) {
+ log.info { "Ignore path=${child.path} as configured." }
+ continue
+ }
+ nodes.add(NodeInfo(child))
}
- nodes.add(NodeInfo(child))
+ children = nodes
}
- children = nodes
+ } catch(e: Exception) {
+ log.error { "Error while reading children of node '${node.path}': ${e.message}" }
}
}
- if (node.properties?.hasNext() == true) {
- val props = mutableListOf()
- properties = props
- node.properties.let {
- while (it.hasNext()) {
- props.add(PropertyInfo(it.nextProperty()))
+ try {
+ if (node.properties?.hasNext() == true) {
+ val props = mutableListOf()
+ properties = props
+ node.properties.let {
+ while (it.hasNext()) {
+ props.add(PropertyInfo(it.nextProperty()))
+ }
}
}
+ } catch (e: Exception) {
+ log.error { "Error while reading properties of node '${node.path}': ${e.message}" }
}
}
diff --git a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/RepoService.kt b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/RepoService.kt
index 71452b306f..b1f89a60f1 100644
--- a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/RepoService.kt
+++ b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/RepoService.kt
@@ -23,6 +23,7 @@
package org.projectforge.jcr
+import jakarta.annotation.PreDestroy
import mu.KotlinLogging
import org.apache.commons.codec.digest.DigestUtils
import org.apache.jackrabbit.oak.Oak
@@ -40,684 +41,688 @@ import java.io.InputStream
import java.io.OutputStream
import java.security.SecureRandom
import java.util.*
-import jakarta.annotation.PreDestroy
-import javax.jcr.Binary
-import javax.jcr.Node
-import javax.jcr.Repository
-import javax.jcr.Session
+import javax.jcr.*
import kotlin.concurrent.thread
private val log = KotlinLogging.logger {}
@Service
open class RepoService {
- internal lateinit var repository: Repository
-
- internal var fileStore: FileStore? = null
-
- var fileStoreLocation: File? = null
- internal set
-
- private var nodeStore: NodeStore? = null
-
- internal lateinit var mainNodeName: String
-
- @PreDestroy
- fun shutdown() {
- log.info { "Shutting down jcr repository..." }
- fileStore?.let {
- it.flush()
- it.compactFull()
- it.cleanup()
- log.info { "Jcr stats: ${FileStoreInfo(this)}" }
- it.close()
- }
- nodeStore?.let {
- //log.warn { "Method not yet implemented: ${it.javaClass}.dispose()" }
- /*if (it is DocumentNodeStore) {
- it.dispose()
- }*/
- }
- }
-
- /**
- * Should only be called by test cases if you need to initialize a repo multiple times.
- */
- fun internalResetForJunitTestCases() {
- nodeStore = null
- }
-
- /**
- * @param parentNodePath Path, nodes are separated by '/', e. g. "world/germany". The nodes of this path must already exist.
- * For creating top level nodes (direct child of main node), set parentNode to null, empty string or "/".
- * @param relPath Sub node parent node to create if not exists. Null value results in nop.
- */
- open fun ensureNode(parentNodePath: String?, relPath: String? = null): Node? {
- relPath ?: return null
- return runInSession { session ->
- val node = getNode(session, parentNodePath, relPath, true)
- session.save()
- node
- }
- }
-
- @JvmOverloads
- open fun storeProperty(
- parentNodePath: String?,
- relPath: String?,
- name: String,
- value: String,
- ensureRelNode: Boolean = true
- ) {
- runInSession { session ->
- val node = getNode(session, parentNodePath, relPath, ensureRelNode)
- node.setProperty(name, value)
- session.save()
- }
- }
-
- open fun retrievePropertyString(parentNodePath: String?, relPath: String?, name: String): String? {
- return runInSession { session ->
- getNodeOrNull(session, parentNodePath, relPath, false)?.getProperty(name)?.string
- }
- }
-
- /**
- * Content of file should be given as [FileObject.content].
- * @param password Optional password for encryption. The password will not be stored in any kind!
- */
- @JvmOverloads
- open fun storeFile(
- fileObject: FileObject, fileSizeChecker: FileSizeChecker,
- user: String? = null,
- password: String? = null,
- ) {
- val content = fileObject.content ?: ByteArray(0) // Assuming 0 byte file if no content is given.
- return storeFile(fileObject, content.inputStream(), fileSizeChecker, user, password = password)
- }
-
- /**
- * @param password Optional password for encryption. The password will not be stored in any kind!
- */
- @JvmOverloads
- open fun storeFile(
- fileObject: FileObject,
- content: InputStream,
- fileSizeChecker: FileSizeChecker,
- user: String? = null,
+ internal lateinit var repository: Repository
+
+ internal var fileStore: FileStore? = null
+
+ var fileStoreLocation: File? = null
+ internal set
+
+ private var nodeStore: NodeStore? = null
+
+ internal lateinit var mainNodeName: String
+
+ @PreDestroy
+ fun shutdown() {
+ log.info { "Shutting down jcr repository..." }
+ fileStore?.let {
+ it.flush()
+ it.compactFull()
+ it.cleanup()
+ log.info { "Jcr stats: ${FileStoreInfo(this)}" }
+ it.close()
+ }
+ nodeStore?.let {
+ //log.warn { "Method not yet implemented: ${it.javaClass}.dispose()" }
+ /*if (it is DocumentNodeStore) {
+ it.dispose()
+ }*/
+ }
+ }
+
+ /**
+ * Should only be called by test cases if you need to initialize a repo multiple times.
+ */
+ fun internalResetForJunitTestCases() {
+ nodeStore = null
+ }
+
+ /**
+ * @param parentNodePath Path, nodes are separated by '/', e. g. "world/germany". The nodes of this path must already exist.
+ * For creating top level nodes (direct child of main node), set parentNode to null, empty string or "/".
+ * @param relPath Sub node parent node to create if not exists. Null value results in nop.
+ */
+ open fun ensureNode(parentNodePath: String?, relPath: String? = null): Node? {
+ relPath ?: return null
+ return runInSession { session ->
+ val node = getNode(session, parentNodePath, relPath, true)
+ session.save()
+ node
+ }
+ }
+
+ @JvmOverloads
+ open fun storeProperty(
+ parentNodePath: String?,
+ relPath: String?,
+ name: String,
+ value: String,
+ ensureRelNode: Boolean = true
+ ) {
+ runInSession { session ->
+ val node = getNode(session, parentNodePath, relPath, ensureRelNode)
+ node.setProperty(name, value)
+ session.save()
+ }
+ }
+
+ open fun retrievePropertyString(parentNodePath: String?, relPath: String?, name: String): String? {
+ return runInSession { session ->
+ getNodeOrNull(session, parentNodePath, relPath, false)?.getProperty(name)?.string
+ }
+ }
+
+ /**
+ * Content of file should be given as [FileObject.content].
+ * @param password Optional password for encryption. The password will not be stored in any kind!
+ */
+ @JvmOverloads
+ open fun storeFile(
+ fileObject: FileObject, fileSizeChecker: FileSizeChecker,
+ user: String? = null,
+ password: String? = null,
+ ) {
+ val content = fileObject.content ?: ByteArray(0) // Assuming 0 byte file if no content is given.
+ return storeFile(fileObject, content.inputStream(), fileSizeChecker, user, password = password)
+ }
+
/**
- * Optional data e. g. for fileSizeChecker of data transfer area size.
+ * @param password Optional password for encryption. The password will not be stored in any kind!
*/
- data: Any? = null,
- password: String? = null,
- ) {
- if (fileObject.size != null) { // file size already known:
- fileSizeChecker.checkSize(fileObject, data)
- }
- val parentNodePath = fileObject.parentNodePath
- val relPath = fileObject.relPath
- if (parentNodePath == null || relPath == null) {
- throw IllegalArgumentException("Parent node path and relPath not given. Can't determine location of file to store: $fileObject")
- }
- var lazyCheckSumFileObject: FileObject? = null
- runInSession { session ->
- val node = getNode(session, parentNodePath, relPath, true)
- val filesNode = ensureNode(node, NODENAME_FILES)
- val fileId = fileObject.fileId ?: createRandomId
- fileObject.fileId = fileId
- log.info { "Storing file: $fileObject" }
- val fileNode = filesNode.addNode(fileId)
- val now = Date()
- if (fileObject.created == null) {
- // created should only be preset for test cases. So normally, use current date.
- fileObject.created = now
- }
- fileObject.createdByUser = user
- if (fileObject.lastUpdate == null) {
- // last update should only be preset for test cases. So normally, use current date.
- fileObject.lastUpdate = now
- }
- fileObject.lastUpdate = fileObject.created
- fileObject.lastUpdateByUser = user
- var bin: Binary? = null
- try {
- if (password.isNullOrBlank()) {
- bin = session.valueFactory.createBinary(content)
+ @JvmOverloads
+ open fun storeFile(
+ fileObject: FileObject,
+ content: InputStream,
+ fileSizeChecker: FileSizeChecker,
+ user: String? = null,
+ /**
+ * Optional data e. g. for fileSizeChecker of data transfer area size.
+ */
+ data: Any? = null,
+ password: String? = null,
+ ) {
+ if (fileObject.size != null) { // file size already known:
+ fileSizeChecker.checkSize(fileObject, data)
+ }
+ val parentNodePath = fileObject.parentNodePath
+ val relPath = fileObject.relPath
+ if (parentNodePath == null || relPath == null) {
+ throw IllegalArgumentException("Parent node path and relPath not given. Can't determine location of file to store: $fileObject")
+ }
+ var lazyCheckSumFileObject: FileObject? = null
+ runInSession { session ->
+ val node = getNode(session, parentNodePath, relPath, true)
+ val filesNode = ensureNode(node, NODENAME_FILES)
+ val fileId = fileObject.fileId ?: createRandomId
+ fileObject.fileId = fileId
+ log.info { "Storing file: $fileObject" }
+ val fileNode = filesNode.addNode(fileId)
+ val now = Date()
+ if (fileObject.created == null) {
+ // created should only be preset for test cases. So normally, use current date.
+ fileObject.created = now
+ }
+ fileObject.createdByUser = user
+ if (fileObject.lastUpdate == null) {
+ // last update should only be preset for test cases. So normally, use current date.
+ fileObject.lastUpdate = now
+ }
+ fileObject.lastUpdate = fileObject.created
+ fileObject.lastUpdateByUser = user
+ var bin: Binary? = null
+ try {
+ if (password.isNullOrBlank()) {
+ bin = session.valueFactory.createBinary(content)
+ } else {
+ val inputStream = CryptStreamUtils.pipeToEncryptedInputStream(content, password)
+ bin = session.valueFactory.createBinary(inputStream)
+ fileObject.aesEncrypted = true
+ }
+ fileNode.setProperty(PROPERTY_FILECONTENT, bin)
+ fileObject.size = bin?.size
+ Integer.MAX_VALUE
+ } finally {
+ bin?.dispose()
+ }
+ // Check size again for the case, the fileObject didn't contain file size before processing the stream.
+ try {
+ fileSizeChecker.checkSize(fileObject, data)
+ } catch (ex: Exception) {
+ fileNode.remove()
+ throw ex
+ }
+ if (fileObject.size ?: 0 > NumberOfBytes.MEGA_BYTES * 50) {
+ lazyCheckSumFileObject = fileObject
+ fileObject.checksum = "..."
+ } else {
+ checksum(fileNode, fileObject)
+ }
+ fileObject.copyTo(fileNode)
+ session.save()
+ }
+ lazyCheckSumFileObject?.let {
+ thread {
+ checksum(it)
+ }
+ }
+ }
+
+ private fun checksum(fileNode: Node, fileObject: FileObject) {
+ // Calculate checksum for files smaller than 50MB (it's fast enough).
+ val startTime = System.currentTimeMillis()
+ // Calculate checksum
+ getFileInputStream(fileNode, fileObject, useEncryptedFile = true).use { istream ->
+ fileObject.checksum = checksum(istream)
+ }
+ FileObject.setChecksum(fileNode, fileObject.checksum)
+ log.info {
+ "Checksum of '${fileObject.fileName}' of size ${FormatterUtils.formatBytes(fileObject.size)} calculated in ${
+ FormatterUtils.format(
+ (System.currentTimeMillis() - startTime) / 1000
+ )
+ }s."
+ }
+ }
+
+ open fun deleteFile(fileObject: FileObject): Boolean {
+ return runInSession { session ->
+ val node = getNode(session, fileObject.parentNodePath, fileObject.relPath, false)
+ if (!node.hasNode(NODENAME_FILES)) {
+ log.error { "Can't delete file, because '$NODENAME_FILES' not found for node '${node.path}': $fileObject" }
+ false
+ } else {
+ val filesNode = node.getNode(NODENAME_FILES)
+ val fileNode = findFile(filesNode, fileObject.fileId, fileObject.fileName)
+ if (fileNode == null) {
+ log.info { "Nothing to delete, file node doesn't exit: $fileObject" }
+ false
+ } else {
+ fileObject.copyFrom(fileNode)
+ log.info { "Deleting file: $fileObject" }
+ fileNode.remove()
+ session.save()
+ true
+ }
+ }
+ }
+ }
+
+ open fun deleteNode(nodeInfo: NodeInfo): Boolean {
+ return runInSession { session ->
+ val node = getNode(session, nodeInfo.path, nodeInfo.name, false)
+ log.info { "Deleting node: $nodeInfo" }
+ node.remove()
+ session.save()
+ true
+ }
+ }
+
+ /**
+ * @return list of file infos without content.
+ */
+ @JvmOverloads
+ open fun getFileInfos(parentNodePath: String?, relPath: String? = null): List? {
+ return runInSession { session ->
+ val filesNode = getFilesNode(session, parentNodePath, relPath)
+ getFileInfos(filesNode)
+ }
+ }
+
+ /**
+ * @return file info without content.
+ */
+ @JvmOverloads
+ open fun getFileInfo(
+ parentNodePath: String?,
+ relPath: String? = null,
+ fileId: String? = null,
+ fileName: String? = null
+ ): FileObject? {
+ return runInSession { session ->
+ val filesNode = getFilesNode(session, parentNodePath, relPath)
+ val node = findFile(filesNode, fileId, fileName)
+ if (node != null) {
+ FileObject(node, parentNodePath, relPath)
+ } else {
+ null
+ }
+ }
+ }
+
+ /**
+ * Change fileName and/or description if given.
+ * @param updateLastUpdateInfo If true (default),
+ * time stamp of last update and user of this update will be updated. Otherwise time stamp and user info will be left untouched.
+ * @return new file info without content.
+ */
+ @JvmOverloads
+ open fun changeFileInfo(
+ fileObject: FileObject,
+ user: String,
+ newFileName: String? = null,
+ newDescription: String? = null,
+ newZipMode: ZipMode? = null,
+ updateLastUpdateInfo: Boolean = true,
+ ): FileObject? {
+ return runInSession { session ->
+ val node = getNode(session, fileObject.parentNodePath, fileObject.relPath, false)
+ if (!node.hasNode(NODENAME_FILES)) {
+ log.error { "Can't change file info, because '$NODENAME_FILES' not found for node '${node.path}': $fileObject" }
+ null
+ } else {
+ val filesNode = node.getNode(NODENAME_FILES)
+ val fileNode = findFile(filesNode, fileObject.fileId, fileObject.fileName)
+ if (fileNode == null) {
+ log.error { "Can't change file info, file node doesn't exit: $fileObject" }
+ null
+ } else {
+ var modified = false
+ if (!newFileName.isNullOrBlank()) {
+ log.info { "Changing file name to '$newFileName' for: $fileObject" }
+ fileNode.setProperty(PROPERTY_FILENAME, newFileName)
+ modified = true
+ }
+ if (newDescription != null) {
+ log.info { "Changing file description to '$newDescription' for: $fileObject" }
+ fileNode.setProperty(PROPERTY_FILEDESC, newDescription)
+ modified = true
+ }
+ if (newZipMode != null) {
+ log.info { "Changing zip encryption algorithm to '$newZipMode' for: $fileObject" }
+ fileNode.setProperty(PROPERTY_ZIP_MODE, newZipMode.name)
+ modified = true
+ }
+ if (modified && updateLastUpdateInfo) {
+ fileNode.setProperty(PROPERTY_LAST_UPDATE_BY_USER, user)
+ fileNode.setProperty(PROPERTY_LAST_UPDATE, PFJcrUtils.convertToString(Date()) ?: "")
+ }
+ session.save()
+ FileObject(fileNode)
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the already calculated checksum or calculates it, if not given.
+ * @return new file info including checksum without content.
+ */
+ open fun checksum(fileObject: FileObject): String? {
+ return runInSession { session ->
+ val node = getNode(session, fileObject.parentNodePath, fileObject.relPath, false)
+ if (!node.hasNode(NODENAME_FILES)) {
+ log.error { "Can't change file info, because '$NODENAME_FILES' not found for node '${node.path}': $fileObject" }
+ null
+ } else {
+ val filesNode = node.getNode(NODENAME_FILES)
+ val fileNode = findFile(filesNode, fileObject.fileId, fileObject.fileName)
+ if (fileNode == null) {
+ log.error { "Can't get or calculate file info, file node doesn't exit: $fileObject" }
+ null
+ } else {
+ val storedFileObject = FileObject(fileNode)
+ checksum(fileNode, storedFileObject)
+ session.save()
+ fileObject.checksum = storedFileObject.checksum
+ fileObject.checksum
+ }
+ }
+ }
+ }
+
+ @JvmOverloads
+ open fun getNodeInfo(absPath: String, recursive: Boolean = false): NodeInfo {
+ return runInSession { session ->
+ log.info { "Getting node info of path '$absPath'..." }
+ val node = session.getNode(absPath)
+ NodeInfo(node, recursive)
+ }
+ }
+
+ private fun getFilesNode(
+ sessionWrapper: SessionWrapper,
+ parentNodePath: String?,
+ relPath: String?,
+ ensureFilesNode: Boolean = false
+ ): Node? {
+ val parentNode = getNodeOrNull(sessionWrapper, parentNodePath, relPath, false)
+ if (parentNode == null) {
+ log.info { "Parent node '${getAbsolutePath(parentNodePath, relPath)}' doesn't exist. No files found (OK)." }
+ return null
+ }
+ return if (ensureFilesNode || parentNode.hasNode(NODENAME_FILES)) {
+ ensureNode(parentNode, NODENAME_FILES)
} else {
- val inputStream = CryptStreamUtils.pipeToEncryptedInputStream(content, password)
- bin = session.valueFactory.createBinary(inputStream)
- fileObject.aesEncrypted = true
- }
- fileNode.setProperty(PROPERTY_FILECONTENT, bin)
- fileObject.size = bin?.size
- Integer.MAX_VALUE
- } finally {
- bin?.dispose()
- }
- // Check size again for the case, the fileObject didn't contain file size before processing the stream.
- try {
- fileSizeChecker.checkSize(fileObject, data)
- } catch (ex: Exception) {
- fileNode.remove()
- throw ex
- }
- if (fileObject.size ?: 0 > NumberOfBytes.MEGA_BYTES * 50) {
- lazyCheckSumFileObject = fileObject
- fileObject.checksum = "..."
- } else {
- checksum(fileNode, fileObject)
- }
- fileObject.copyTo(fileNode)
- session.save()
- }
- lazyCheckSumFileObject?.let {
- thread {
- checksum(it)
- }
- }
- }
-
- private fun checksum(fileNode: Node, fileObject: FileObject) {
- // Calculate checksum for files smaller than 50MB (it's fast enough).
- val startTime = System.currentTimeMillis()
- // Calculate checksum
- getFileInputStream(fileNode, fileObject, useEncryptedFile = true).use { istream ->
- fileObject.checksum = checksum(istream)
- }
- FileObject.setChecksum(fileNode, fileObject.checksum)
- log.info {
- "Checksum of '${fileObject.fileName}' of size ${FormatterUtils.formatBytes(fileObject.size)} calculated in ${
- FormatterUtils.format(
- (System.currentTimeMillis() - startTime) / 1000
+ null
+ }
+ }
+
+ internal fun getFileInfos(
+ filesNode: Node?,
+ parentNodePath: String? = null,
+ relPath: String? = null
+ ): List? {
+ filesNode ?: return null
+ var fileNodes: NodeIterator? = null
+ try {
+ fileNodes = filesNode.nodes
+ if (fileNodes == null || !fileNodes.hasNext()) {
+ return null
+ }
+ } catch (ex: Exception) {
+ log.error { "Error while reading file nodes of '${filesNode.path}': ${ex.message}" }
+ return null
+ }
+ val result = mutableListOf()
+ while (fileNodes.hasNext()) {
+ val node = fileNodes.nextNode()
+ try {
+ if (node.hasProperty(PROPERTY_FILENAME)) {
+ result.add(FileObject(node, parentNodePath ?: node.path, relPath))
+ }
+ } catch (ex: Exception) {
+ log.error { "Error while reading file node '${node.path}': ${ex.message}" }
+ }
+ }
+ return result
+ }
+
+ fun getFileInfos(nodeInfo: NodeInfo?): List? {
+ nodeInfo ?: return null
+ val fileNodes = nodeInfo.children
+ if (fileNodes.isNullOrEmpty()) {
+ return null
+ }
+ val result = mutableListOf()
+ fileNodes.forEach { node ->
+ if (node.hasProperty(PROPERTY_FILENAME)) {
+ result.add(FileObject(node))
+ }
+ }
+ return result
+ }
+
+ internal fun findFile(filesNode: Node?, fileId: String?, fileName: String? = null): Node? {
+ filesNode ?: return null
+ if (!filesNode.hasNodes()) {
+ return null
+ }
+ filesNode.nodes?.let {
+ while (it.hasNext()) {
+ val node = it.nextNode()
+ if (node.name == fileId || PFJcrUtils.getProperty(node, PROPERTY_FILENAME)?.string == fileName) {
+ return node
+ }
+ }
+ }
+ return null
+ }
+
+ @JvmOverloads
+ open fun retrieveFile(fileObject: FileObject, password: String? = null): Boolean {
+ return runInSession { session ->
+ val filesNode = getFilesNode(session, fileObject.parentNodePath, fileObject.relPath, false)
+ val node = findFile(filesNode, fileObject.fileId, fileObject.fileName)
+ if (node == null) {
+ log.warn { "File not found in repository: $fileObject" }
+ false
+ } else {
+ fileObject.copyFrom(node)
+ fileObject.content = getFileContent(node, fileObject, password)
+ true
+ }
+ }
+ }
+
+ open fun retrieveFileInputStream(fileObject: FileObject, password: String? = null): InputStream? {
+ return runInSession { session ->
+ val filesNode = getFilesNode(session, fileObject.parentNodePath, fileObject.relPath, false)
+ val node = findFile(filesNode, fileObject.fileId, fileObject.fileName)
+ if (node == null) {
+ log.warn { "File not found in repository: $fileObject" }
+ null
+ } else {
+ getFileInputStream(node, fileObject)
+ }
+ }
+ }
+
+ internal fun getFileContent(
+ node: Node?, fileObject: FileObject,
+ password: String? = null,
+ useEncryptedFile: Boolean = false,
+ ): ByteArray? {
+ return getFileInputStream(node, fileObject, password = password, useEncryptedFile = useEncryptedFile)?.use(
+ InputStream::readBytes
)
- }s."
- }
- }
-
- open fun deleteFile(fileObject: FileObject): Boolean {
- return runInSession { session ->
- val node = getNode(session, fileObject.parentNodePath, fileObject.relPath, false)
- if (!node.hasNode(NODENAME_FILES)) {
- log.error { "Can't delete file, because '$NODENAME_FILES' not found for node '${node.path}': $fileObject" }
- false
- } else {
- val filesNode = node.getNode(NODENAME_FILES)
- val fileNode = findFile(filesNode, fileObject.fileId, fileObject.fileName)
- if (fileNode == null) {
- log.info { "Nothing to delete, file node doesn't exit: $fileObject" }
- false
- } else {
- fileObject.copyFrom(fileNode)
- log.info { "Deleting file: $fileObject" }
- fileNode.remove()
- session.save()
- true
- }
- }
- }
- }
-
- open fun deleteNode(nodeInfo: NodeInfo): Boolean {
- return runInSession { session ->
- val node = getNode(session, nodeInfo.path, nodeInfo.name, false)
- log.info { "Deleting node: $nodeInfo" }
- node.remove()
- session.save()
- true
- }
- }
-
- /**
- * @return list of file infos without content.
- */
- @JvmOverloads
- open fun getFileInfos(parentNodePath: String?, relPath: String? = null): List? {
- return runInSession { session ->
- val filesNode = getFilesNode(session, parentNodePath, relPath)
- getFileInfos(filesNode)
- }
- }
-
- /**
- * @return file info without content.
- */
- @JvmOverloads
- open fun getFileInfo(
- parentNodePath: String?,
- relPath: String? = null,
- fileId: String? = null,
- fileName: String? = null
- ): FileObject? {
- return runInSession { session ->
- val filesNode = getFilesNode(session, parentNodePath, relPath)
- val node = findFile(filesNode, fileId, fileName)
- if (node != null) {
- FileObject(node, parentNodePath, relPath)
- } else {
- null
- }
- }
- }
-
- /**
- * Change fileName and/or description if given.
- * @param updateLastUpdateInfo If true (default),
- * time stamp of last update and user of this update will be updated. Otherwise time stamp and user info will be left untouched.
- * @return new file info without content.
- */
- @JvmOverloads
- open fun changeFileInfo(
- fileObject: FileObject,
- user: String,
- newFileName: String? = null,
- newDescription: String? = null,
- newZipMode: ZipMode? = null,
- updateLastUpdateInfo: Boolean = true,
- ): FileObject? {
- return runInSession { session ->
- val node = getNode(session, fileObject.parentNodePath, fileObject.relPath, false)
- if (!node.hasNode(NODENAME_FILES)) {
- log.error { "Can't change file info, because '$NODENAME_FILES' not found for node '${node.path}': $fileObject" }
- null
- } else {
- val filesNode = node.getNode(NODENAME_FILES)
- val fileNode = findFile(filesNode, fileObject.fileId, fileObject.fileName)
- if (fileNode == null) {
- log.error { "Can't change file info, file node doesn't exit: $fileObject" }
- null
- } else {
- var modified = false
- if (!newFileName.isNullOrBlank()) {
- log.info { "Changing file name to '$newFileName' for: $fileObject" }
- fileNode.setProperty(PROPERTY_FILENAME, newFileName)
- modified = true
- }
- if (newDescription != null) {
- log.info { "Changing file description to '$newDescription' for: $fileObject" }
- fileNode.setProperty(PROPERTY_FILEDESC, newDescription)
- modified = true
- }
- if (newZipMode != null) {
- log.info { "Changing zip encryption algorithm to '$newZipMode' for: $fileObject" }
- fileNode.setProperty(PROPERTY_ZIP_MODE, newZipMode.name)
- modified = true
- }
- if (modified && updateLastUpdateInfo) {
- fileNode.setProperty(PROPERTY_LAST_UPDATE_BY_USER, user)
- fileNode.setProperty(PROPERTY_LAST_UPDATE, PFJcrUtils.convertToString(Date()) ?: "")
- }
- session.save()
- FileObject(fileNode)
- }
- }
- }
- }
-
- /**
- * Returns the already calculated checksum or calculates it, if not given.
- * @return new file info including checksum without content.
- */
- open fun checksum(fileObject: FileObject): String? {
- return runInSession { session ->
- val node = getNode(session, fileObject.parentNodePath, fileObject.relPath, false)
- if (!node.hasNode(NODENAME_FILES)) {
- log.error { "Can't change file info, because '$NODENAME_FILES' not found for node '${node.path}': $fileObject" }
- null
- } else {
- val filesNode = node.getNode(NODENAME_FILES)
- val fileNode = findFile(filesNode, fileObject.fileId, fileObject.fileName)
- if (fileNode == null) {
- log.error { "Can't get or calculate file info, file node doesn't exit: $fileObject" }
- null
- } else {
- val storedFileObject = FileObject(fileNode)
- checksum(fileNode, storedFileObject)
- session.save()
- fileObject.checksum = storedFileObject.checksum
- fileObject.checksum
- }
- }
- }
- }
-
- @JvmOverloads
- open fun getNodeInfo(absPath: String, recursive: Boolean = false): NodeInfo {
- return runInSession { session ->
- log.info { "Getting node info of path '$absPath'..." }
- val node = session.getNode(absPath)
- NodeInfo(node, recursive)
- }
- }
-
- private fun getFilesNode(
- sessionWrapper: SessionWrapper,
- parentNodePath: String?,
- relPath: String?,
- ensureFilesNode: Boolean = false
- ): Node? {
- val parentNode = getNodeOrNull(sessionWrapper, parentNodePath, relPath, false)
- if (parentNode == null) {
- log.info { "Parent node '${getAbsolutePath(parentNodePath, relPath)}' doesn't exist. No files found (OK)." }
- return null
- }
- return if (ensureFilesNode || parentNode.hasNode(NODENAME_FILES)) {
- ensureNode(parentNode, NODENAME_FILES)
- } else {
- null
- }
- }
-
- internal fun getFileInfos(
- filesNode: Node?,
- parentNodePath: String? = null,
- relPath: String? = null
- ): List? {
- filesNode ?: return null
- val fileNodes = filesNode.nodes
- if (fileNodes == null || !fileNodes.hasNext()) {
- return null
- }
- val result = mutableListOf()
- while (fileNodes.hasNext()) {
- val node = fileNodes.nextNode()
- if (node.hasProperty(PROPERTY_FILENAME)) {
- result.add(FileObject(node, parentNodePath ?: node.path, relPath))
- }
- }
- return result
- }
-
- fun getFileInfos(nodeInfo: NodeInfo?): List? {
- nodeInfo ?: return null
- val fileNodes = nodeInfo.children
- if (fileNodes.isNullOrEmpty()) {
- return null
- }
- val result = mutableListOf()
- fileNodes.forEach { node ->
- if (node.hasProperty(PROPERTY_FILENAME)) {
- result.add(FileObject(node))
- }
- }
- return result
- }
-
- internal fun findFile(filesNode: Node?, fileId: String?, fileName: String? = null): Node? {
- filesNode ?: return null
- if (!filesNode.hasNodes()) {
- return null
- }
- filesNode.nodes?.let {
- while (it.hasNext()) {
- val node = it.nextNode()
- if (node.name == fileId || PFJcrUtils.getProperty(node, PROPERTY_FILENAME)?.string == fileName) {
- return node
- }
- }
- }
- return null
- }
-
- @JvmOverloads
- open fun retrieveFile(fileObject: FileObject, password: String? = null): Boolean {
- return runInSession { session ->
- val filesNode = getFilesNode(session, fileObject.parentNodePath, fileObject.relPath, false)
- val node = findFile(filesNode, fileObject.fileId, fileObject.fileName)
- if (node == null) {
- log.warn { "File not found in repository: $fileObject" }
- false
- } else {
- fileObject.copyFrom(node)
- fileObject.content = getFileContent(node, fileObject, password)
- true
- }
- }
- }
-
- open fun retrieveFileInputStream(fileObject: FileObject, password: String? = null): InputStream? {
- return runInSession { session ->
- val filesNode = getFilesNode(session, fileObject.parentNodePath, fileObject.relPath, false)
- val node = findFile(filesNode, fileObject.fileId, fileObject.fileName)
- if (node == null) {
- log.warn { "File not found in repository: $fileObject" }
- null
- } else {
- getFileInputStream(node, fileObject)
- }
- }
- }
-
- internal fun getFileContent(
- node: Node?, fileObject: FileObject,
- password: String? = null,
- useEncryptedFile: Boolean = false,
- ): ByteArray? {
- return getFileInputStream(node, fileObject, password = password, useEncryptedFile = useEncryptedFile)?.use(
- InputStream::readBytes
- )
- }
-
- /**
- * @param password Must be given for encrypted file to decrypt (if useEncryptedFile isn't true)
- * @param useEncryptedFile If true, work with encrypted file directly without password and decryption.
- * Used by internal checksum and backup functionality.
- */
- internal fun getFileInputStream(
- node: Node?,
- fileObject: FileObject,
- suppressLogInfo: Boolean = false,
- password: String? = null,
- useEncryptedFile: Boolean = false,
- ): InputStream? {
- node ?: return null
- if (!suppressLogInfo) {
- log.info { "Reading file from repository '${node.path}': $fileObject..." }
- }
- if (!useEncryptedFile && fileObject.aesEncrypted == true && password.isNullOrBlank()) {
- log.error { "File is encrypted, but no password given to decrypt in repository '${node.path}': $fileObject" }
- return null
- }
- var binary: Binary? = null
- try {
- binary = node.getProperty(PROPERTY_FILECONTENT)?.binary ?: return null
- return if (useEncryptedFile || password.isNullOrBlank()) {
- binary.stream
- } else {
+ }
+
+ /**
+ * @param password Must be given for encrypted file to decrypt (if useEncryptedFile isn't true)
+ * @param useEncryptedFile If true, work with encrypted file directly without password and decryption.
+ * Used by internal checksum and backup functionality.
+ */
+ internal fun getFileInputStream(
+ node: Node?,
+ fileObject: FileObject,
+ suppressLogInfo: Boolean = false,
+ password: String? = null,
+ useEncryptedFile: Boolean = false,
+ ): InputStream? {
+ node ?: return null
+ if (!suppressLogInfo) {
+ log.info { "Reading file from repository '${node.path}': $fileObject..." }
+ }
+ if (!useEncryptedFile && fileObject.aesEncrypted == true && password.isNullOrBlank()) {
+ log.error { "File is encrypted, but no password given to decrypt in repository '${node.path}': $fileObject" }
+ return null
+ }
+ var binary: Binary? = null
+ try {
+ binary = node.getProperty(PROPERTY_FILECONTENT)?.binary ?: return null
+ return if (useEncryptedFile || password.isNullOrBlank()) {
+ binary.stream
+ } else {
+ try {
+ CryptStreamUtils.pipeToDecryptedInputStream(binary.stream, password)
+ } catch (ex: Exception) {
+ if (CryptStreamUtils.wasWrongPassword(ex)) {
+ log.error { "Can't decrypt and retrieve file (wrong password) in repository '${node.path}': $fileObject" }
+ null
+ } else {
+ throw ex
+ }
+ }
+ }
+ } finally {
+ binary?.dispose()
+ }
+ }
+
+ internal fun getFileSize(node: Node?, fileObject: FileObject, suppressLogInfo: Boolean = false): Long? {
+ node ?: return null
+ if (!suppressLogInfo) {
+ log.info { "Determining size of file from repository '${node.path}': '${fileObject.fileName}'..." }
+ }
+ var binary: Binary? = null
try {
- CryptStreamUtils.pipeToDecryptedInputStream(binary.stream, password)
+ binary = node.getProperty(PROPERTY_FILECONTENT)?.binary
+ return binary?.size
+ } finally {
+ binary?.dispose()
+ }
+ }
+
+ internal fun getNode(
+ session: SessionWrapper,
+ parentNodePath: String?,
+ relPath: String? = null,
+ ensureRelNode: Boolean = true
+ ): Node {
+ return getNodeOrNull(session, parentNodePath, relPath, ensureRelNode)
+ ?: throw IllegalArgumentException("Can't find node ${getAbsolutePath(parentNodePath, relPath)}.")
+ }
+
+ internal fun getNodeOrNull(
+ session: SessionWrapper,
+ parentNodePath: String?,
+ relPath: String? = null,
+ ensureRelNode: Boolean = true
+ ): Node? {
+ val absolutePath = getAbsolutePath(parentNodePath)
+ if (!session.nodeExists(absolutePath)) {
+ return null
+ }
+ val parentNode = try {
+ session.getNode(absolutePath)
} catch (ex: Exception) {
- if (CryptStreamUtils.wasWrongPassword(ex)) {
- log.error { "Can't decrypt and retrieve file (wrong password) in repository '${node.path}': $fileObject" }
- null
- } else {
- throw ex
- }
- }
- }
- } finally {
- binary?.dispose()
- }
- }
-
- internal fun getFileSize(node: Node?, fileObject: FileObject, suppressLogInfo: Boolean = false): Long? {
- node ?: return null
- if (!suppressLogInfo) {
- log.info { "Determining size of file from repository '${node.path}': '${fileObject.fileName}'..." }
- }
- var binary: Binary? = null
- try {
- binary = node.getProperty(PROPERTY_FILECONTENT)?.binary
- return binary?.size
- } finally {
- binary?.dispose()
- }
- }
-
- internal fun getNode(
- session: SessionWrapper,
- parentNodePath: String?,
- relPath: String? = null,
- ensureRelNode: Boolean = true
- ): Node {
- return getNodeOrNull(session, parentNodePath, relPath, ensureRelNode)
- ?: throw IllegalArgumentException("Can't find node ${getAbsolutePath(parentNodePath, relPath)}.")
- }
-
- internal fun getNodeOrNull(
- session: SessionWrapper,
- parentNodePath: String?,
- relPath: String? = null,
- ensureRelNode: Boolean = true
- ): Node? {
- val absolutePath = getAbsolutePath(parentNodePath)
- if (!session.nodeExists(absolutePath)) {
- return null
- }
- val parentNode = try {
- session.getNode(absolutePath)
- } catch (ex: Exception) {
- log.error { "Can't get node '$absolutePath'. ${ex::class.java.name}: ${ex.message}." }
- return null
- }
- return when {
- ensureRelNode -> ensureNode(parentNode, relPath)
- parentNode.hasNode(relPath) -> parentNode.getNode(relPath)
- else -> null
- }
- }
-
- fun getAbsolutePath(nodePath: String?): String {
- val path = nodePath?.removePrefix("/")?.removePrefix(mainNodeName)?.removePrefix("/") ?: ""
- return "/$mainNodeName/$path"
- }
-
- private fun getAbsolutePath(parentNode: Node, relPath: String?): String? {
- val parentPath = parentNode.path
- return getAbsolutePath(parentPath, relPath)
- }
-
- fun cleanup() {
- log.info { "Cleaning JCR repository up..." }
- fileStore?.let {
- it.flush()
- it.compactFull()
- it.cleanup()
- }
- }
-
- internal fun ensureNode(parentNode: Node, relPath: String?): Node {
- relPath ?: return parentNode
- var current: Node = parentNode
- relPath.split("/").forEach {
- current = if (current.hasNode(it)) {
- current.getNode(it)
- } else {
- log.info { "Creating node ${getAbsolutePath(current, it)}." }
- current.addNode(it)
- }
- }
- return current
- }
-
- private val createRandomId: String
- get() {
- val random = SecureRandom()
- val bytes = ByteArray(PROPERTY_RANDOM_ID_LENGTH)
- random.nextBytes(bytes)
- val sb = StringBuilder()
- for (i in 0 until PROPERTY_RANDOM_ID_LENGTH) {
- sb.append(ALPHA_CHARSET[(bytes[i].toInt() and 0xFF) % PROPERTY_RANDOM_ID_LENGTH])
- }
- return sb.toString()
- }
-
- internal fun runInSession(method: (sessionWrapper: SessionWrapper) -> T): T {
- val session = SessionWrapper(this)
- try {
- return method(session)
- } finally {
- session.logout()
- }
- }
-
- /**
- * @param mainNodeName All activities (working with nodes) will done under topNode. TopNode should be given for backing up and
- * restoring. By default "ProjectForge" is used.
- */
- @JvmOverloads
- fun init(repositoryDir: File, mainNodeName: String = "ProjectForge") {
- synchronized(this) {
- if (nodeStore != null) {
- throw IllegalArgumentException("Can't initialize repo twice! repo=$this")
- }
- if (mainNodeName.isBlank()) {
- throw IllegalArgumentException("Top node shouldn't be empty!")
- }
- if (log.isDebugEnabled) {
- log.debug { "Setting system property: derby.stream.error.field=${DerbyUtil::class.java.name}.DEV_NULL" }
- }
- System.setProperty("derby.stream.error.field", "${DerbyUtil::class.java.name}.DEV_NULL")
- log.info { "Initializing JCR repository with main node '$mainNodeName' in: ${repositoryDir.absolutePath}" }
- this.mainNodeName = mainNodeName
-
- FileStoreBuilder.fileStoreBuilder(repositoryDir).build().let { fileStore ->
- this.fileStore = fileStore
- this.fileStoreLocation = repositoryDir
- nodeStore = SegmentNodeStoreBuilders.builder(fileStore).build()
- repository = Jcr(Oak(nodeStore)).createRepository()
- }
-
- runInSession { session ->
- if (!session.rootNode.hasNode(mainNodeName)) {
- log.info { "Creating top level node '$mainNodeName'." }
- session.rootNode.addNode(mainNodeName)
+ log.error { "Can't get node '$absolutePath'. ${ex::class.java.name}: ${ex.message}." }
+ return null
+ }
+ return when {
+ ensureRelNode -> ensureNode(parentNode, relPath)
+ parentNode.hasNode(relPath) -> parentNode.getNode(relPath)
+ else -> null
}
+ }
+
+ fun getAbsolutePath(nodePath: String?): String {
+ val path = nodePath?.removePrefix("/")?.removePrefix(mainNodeName)?.removePrefix("/") ?: ""
+ return "/$mainNodeName/$path"
+ }
+
+ private fun getAbsolutePath(parentNode: Node, relPath: String?): String? {
+ val parentPath = parentNode.path
+ return getAbsolutePath(parentPath, relPath)
+ }
+
+ fun cleanup() {
+ log.info { "Cleaning JCR repository up..." }
+ fileStore?.let {
+ it.flush()
+ it.compactFull()
+ it.cleanup()
+ }
+ }
+
+ internal fun ensureNode(parentNode: Node, relPath: String?): Node {
+ relPath ?: return parentNode
+ var current: Node = parentNode
+ relPath.split("/").forEach {
+ current = if (current.hasNode(it)) {
+ current.getNode(it)
+ } else {
+ log.info { "Creating node ${getAbsolutePath(current, it)}." }
+ current.addNode(it)
+ }
+ }
+ return current
+ }
+
+ private val createRandomId: String
+ get() {
+ val random = SecureRandom()
+ val bytes = ByteArray(PROPERTY_RANDOM_ID_LENGTH)
+ random.nextBytes(bytes)
+ val sb = StringBuilder()
+ for (i in 0 until PROPERTY_RANDOM_ID_LENGTH) {
+ sb.append(ALPHA_CHARSET[(bytes[i].toInt() and 0xFF) % PROPERTY_RANDOM_ID_LENGTH])
+ }
+ return sb.toString()
+ }
+
+ internal fun runInSession(method: (sessionWrapper: SessionWrapper) -> T): T {
+ val session = SessionWrapper(this)
+ try {
+ return method(session)
+ } finally {
+ session.logout()
+ }
+ }
+
+ /**
+ * @param mainNodeName All activities (working with nodes) will done under topNode. TopNode should be given for backing up and
+ * restoring. By default "ProjectForge" is used.
+ */
+ @JvmOverloads
+ fun init(repositoryDir: File, mainNodeName: String = "ProjectForge") {
+ synchronized(this) {
+ if (nodeStore != null) {
+ throw IllegalArgumentException("Can't initialize repo twice! repo=$this")
+ }
+ if (mainNodeName.isBlank()) {
+ throw IllegalArgumentException("Top node shouldn't be empty!")
+ }
+ log.debug { "Setting system property: derby.stream.error.field=${DerbyUtil::class.java.name}.DEV_NULL" }
+ System.setProperty("derby.stream.error.field", "${DerbyUtil::class.java.name}.DEV_NULL")
+ log.info { "Initializing JCR repository with main node '$mainNodeName' in: ${repositoryDir.absolutePath}" }
+ this.mainNodeName = mainNodeName
+
+ FileStoreBuilder.fileStoreBuilder(repositoryDir).build().let { fileStore ->
+ this.fileStore = fileStore
+ this.fileStoreLocation = repositoryDir
+ nodeStore = SegmentNodeStoreBuilders.builder(fileStore).build()
+ repository = Jcr(Oak(nodeStore)).createRepository()
+ }
+
+ runInSession { session ->
+ if (!session.rootNode.hasNode(mainNodeName)) {
+ log.info { "Creating top level node '$mainNodeName'." }
+ session.rootNode.addNode(mainNodeName)
+ }
+ session.save()
+ }
+ }
+ }
+
+ internal fun close(session: Session) {
session.save()
- }
- }
- }
-
- internal fun close(session: Session) {
- session.save()
- fileStore?.close()
- }
-
- companion object {
- const val NODENAME_FILES = "__FILES"
- internal const val PROPERTY_FILENAME = "fileName"
- internal const val PROPERTY_FILESIZE = "size"
- internal const val PROPERTY_FILECONTENT = "content"
- internal const val PROPERTY_CREATED = "created"
- internal const val PROPERTY_CREATED_BY_USER = "createdByUser"
- internal const val PROPERTY_FILEDESC = "fileDescription"
- internal const val PROPERTY_LAST_UPDATE = "lastUpdate"
- internal const val PROPERTY_LAST_UPDATE_BY_USER = "lastUpdateByUser"
- internal const val PROPERTY_CHECKSUM = "checksum"
- internal const val PROPERTY_AES_ENCRYPTED = "aesEncrypted"
- internal const val PROPERTY_ZIP_MODE = "zipMode"
- private const val PROPERTY_RANDOM_ID_LENGTH = 20
- private val ALPHA_CHARSET: Array = ('a'..'z').toList().toTypedArray()
-
- internal fun checksum(istream: InputStream?): String {
- istream ?: return ""
- return "SHA256: ${DigestUtils.sha256Hex(istream)}"
- }
-
- internal fun getAbsolutePath(parentPath: String?, relPath: String?): String? {
- if (parentPath == null && relPath == null) {
- return null
- }
- parentPath ?: return relPath
- relPath ?: return parentPath
- return if (parentPath.endsWith("/")) "$parentPath$relPath" else "$parentPath/$relPath"
+ fileStore?.close()
+ }
+
+ companion object {
+ const val NODENAME_FILES = "__FILES"
+ internal const val PROPERTY_FILENAME = "fileName"
+ internal const val PROPERTY_FILESIZE = "size"
+ internal const val PROPERTY_FILECONTENT = "content"
+ internal const val PROPERTY_CREATED = "created"
+ internal const val PROPERTY_CREATED_BY_USER = "createdByUser"
+ internal const val PROPERTY_FILEDESC = "fileDescription"
+ internal const val PROPERTY_LAST_UPDATE = "lastUpdate"
+ internal const val PROPERTY_LAST_UPDATE_BY_USER = "lastUpdateByUser"
+ internal const val PROPERTY_CHECKSUM = "checksum"
+ internal const val PROPERTY_AES_ENCRYPTED = "aesEncrypted"
+ internal const val PROPERTY_ZIP_MODE = "zipMode"
+ private const val PROPERTY_RANDOM_ID_LENGTH = 20
+ private val ALPHA_CHARSET: Array = ('a'..'z').toList().toTypedArray()
+
+ internal fun checksum(istream: InputStream?): String {
+ istream ?: return ""
+ return "SHA256: ${DigestUtils.sha256Hex(istream)}"
+ }
+
+ internal fun getAbsolutePath(parentPath: String?, relPath: String?): String? {
+ if (parentPath == null && relPath == null) {
+ return null
+ }
+ parentPath ?: return relPath
+ relPath ?: return parentPath
+ return if (parentPath.endsWith("/")) "$parentPath$relPath" else "$parentPath/$relPath"
+ }
}
- }
- // https://stackoverflow.com/questions/1004327/getting-rid-of-derby-log
- object DerbyUtil {
- @JvmField
- val DEV_NULL: OutputStream = object : OutputStream() {
- override fun write(b: Int) {}
+ // https://stackoverflow.com/questions/1004327/getting-rid-of-derby-log
+ object DerbyUtil {
+ @JvmField
+ val DEV_NULL: OutputStream = object : OutputStream() {
+ override fun write(b: Int) {}
+ }
}
- }
}
diff --git a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/RepoTreeWalker.kt b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/RepoTreeWalker.kt
index c389c095b0..576ce9517a 100644
--- a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/RepoTreeWalker.kt
+++ b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/RepoTreeWalker.kt
@@ -23,8 +23,11 @@
package org.projectforge.jcr
+import mu.KotlinLogging
import javax.jcr.Node
+private val log = KotlinLogging.logger {}
+
open class RepoTreeWalker(
val repoService: RepoService,
val absPath: String = "/${repoService.mainNodeName}"
@@ -54,10 +57,14 @@ open class RepoTreeWalker(
}
}
}
- node.nodes?.let {
- while (it.hasNext()) {
- walk(it.nextNode())
+ try {
+ node.nodes?.let {
+ while (it.hasNext()) {
+ walk(it.nextNode())
+ }
}
+ } catch (e: Exception) {
+ log.error { "Error while reading children of node '${node.path}': ${e.message}" }
}
}
diff --git a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/SanityCheckMain.kt b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/SanityCheckMain.kt
new file mode 100644
index 0000000000..fad1ccdda1
--- /dev/null
+++ b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/SanityCheckMain.kt
@@ -0,0 +1,44 @@
+/////////////////////////////////////////////////////////////////////////////
+//
+// 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.jcr
+
+import org.projectforge.jcr.BackupMain.Companion.checkRepoDir
+
+class SanityCheckMain {
+ companion object {
+ @JvmStatic
+ fun main(args: Array) {
+ if (args.size != 1) {
+ BackupMain.printHelp()
+ return
+ }
+ val repositoryLocation = checkRepoDir(args[0]) ?: return
+ val repoService = RepoService()
+ repoService.init(repositoryLocation)
+ val jcrCheckSanityJob = JCRCheckSanityJob()
+ jcrCheckSanityJob.repoService = repoService
+ jcrCheckSanityJob.execute()
+ }
+ }
+}
diff --git a/projectforge-jcr/src/main/resources/backupReadme.txt b/projectforge-jcr/src/main/resources/backupReadme.txt
index b0ba482c85..ad97d5607b 100644
--- a/projectforge-jcr/src/main/resources/backupReadme.txt
+++ b/projectforge-jcr/src/main/resources/backupReadme.txt
@@ -13,20 +13,48 @@ repository.json is used first to create the nodes and properties.
After restoring a sanity check will be done (comparing file sizes and checksums). You may run this sanity check at
any time via click on ProjectForge admin's web page: Administration -> System -> misc checks -> JCR sanity check.
+Prepare application
+-------------------
+You can't run the main classes directly from ProjectForge's jar file. Following steps are needed to prepare the application:
+1. Extract the ProjectForge application jar file.
+ mkdir extracted
+ cd extracted
+ jar xf ../projectforge-application-.jar
+2. Set CLASSPATH to the extracted directory.
+ CLASSPATH="BOOT-INF/classes:$(find BOOT-INF/lib -name '*.jar' | tr '\n' ':')"
+3. Run the main classes from the extracted directory.
+ java -cp "$CLASSPATH" org.projectforge.jcr.BackupMain
Usage Backup
------------
- java -cp projectforge-application-[version].jar org.projectforge.jcr.BackupMain [jcr-path]
- java -cp projectforge-application-[version].jar org.projectforge.jcr.BackupMain [jcr-path] [backup-dir]
+ # ./ is used for creating zip archive of backup:
+ java -cp "$CLASSPATH" org.projectforge.jcr.BackupMain [jcr-path]
+ # [backup-dir] is used for creating zip archive of backup:
+ java -cp "$CLASSPATH" org.projectforge.jcr.BackupMain [jcr-path] [backup-dir]
Examples:
- java -cp projectforge-application-[version].jar org.projectforge.jcr.BackupMain /home/kai/ProjectForge/jcr/
- java -cp projectforge-application-[version].jar org.projectforge.jcr.BackupMain /home/kai/ProjectForge/jcr/ /home/kai/backups/
+ java -cp "$CLASSPATH" org.projectforge.jcr.BackupMain /home/kai/ProjectForge/jcr/
+ java -cp "$CLASSPATH" org.projectforge.jcr.BackupMain /home/kai/ProjectForge/jcr/ /home/kai/backups/
Usage Restore
-------------
- java -cp projectforge-application-[version].jar org.projectforge.jcr.RestoreMain [jcr-path] [backup-zip]
+ mv [jcr-path] [jcr-path].bak # Move original jcr-path to jcr-path.bak
+ mkdir [jcr-path]
+ java -cp "$CLASSPATH" org.projectforge.jcr.RestoreMain [jcr-path] [backup-zip]
Example:
- java -cp projectforge-application-[version].jar org.projectforge.jcr.RestoreMain /home/kai/ProjectForge/jcr/ projectforge-jcr-backup.zip
+ java -cp "$CLASSPATH" org.projectforge.jcr.RestoreMain /home/kai/ProjectForge/jcr/ projectforge-jcr-backup.zip
+
+Check file integrity
+--------------------
+ java -cp "$CLASSPATH" org.projectforge.jcr.SanityCheckMain [jcr-path]
+
+Corrupted segments
+------------------
+Try to do a backup as described above. If the backup fails, you may have corrupted segments in your repository.
+
+You may also detect corrupted segments, if you see the following error message in the log file:
+ERROR org.apache.jackrabbit.oak.segment.SegmentNotFoundExceptionListener -- Segment not found: ...
+
+If you have corrupted segments, execute a backup and restore as described above.
diff --git a/site/_docs/adminguide.adoc b/site/_docs/adminguide.adoc
index ffe6de70ca..5eae541b1c 100644
--- a/site/_docs/adminguide.adoc
+++ b/site/_docs/adminguide.adoc
@@ -1067,14 +1067,9 @@ update t_pf_user SET password='SHA\{BC871652288E56E306CFA093BEFC3FFCD0ED8872}',
The JCR repository contains all uploaded files (contracts, orders etc.). The file of the data transfer boxes is not included (it's normally to large for backup). The data transfer boxes are used for exchanging very large
files between users and/or customers.
-For restoring a JCR backup you may use:
-[source,shell,linenums]
-----
-mv ./ProjectForge/jcr ./ProjectForge/jcr.old
-mkdir ./ProjectForge/jcr
-java -cp projectforge-application-version.jar -Dloader.main=org.projectforge.jcr.RestoreMain org.springframework.boot.loader.PropertiesLauncher /home/$user/ProjectForge/jcr/ projectforge-jcr-backup.zip
-----
+For restoring a JCR backup, please refer the file `backupReadme.txt` inside the backup zip file.
+If you have a corrupted JCR repository or if you want to make a full backup, refer `backupReadme.txt` as well. You will also find the readme here: `https://github.com/micromata/projectforge/blob/develop/projectforge-jcr/src/main/resources/backupReadme.txt`.
=== Automatical backup