From 0237d5eba9c94d97444cc46873b5f77dce7817b2 Mon Sep 17 00:00:00 2001 From: Kai Reinhard Date: Mon, 30 Dec 2024 14:01:39 +0100 Subject: [PATCH 1/3] Json serialization refactored: IdOnlySerializer and IdsOnlySerializer introduced. --- ToDo.adoc | 2 +- .../plugins/banking/BankAccountBalanceDO.kt | 3 + .../plugins/banking/BankAccountRecordDO.kt | 3 + .../datatransfer/DataTransferAuditDO.kt | 4 + .../marketing/AddressCampaignValueDO.kt | 4 + .../org/projectforge/plugins/memo/MemoDO.kt | 3 + .../plugins/skillmatrix/SkillEntryDO.kt | 3 + .../org/projectforge/plugins/todo/ToDoDO.kt | 6 + .../business/address/AddressDO.kt | 3 + .../business/address/AddressImageDO.kt | 3 + .../business/address/AddressbookDO.kt | 3 + .../business/address/PersonalAddressDO.kt | 4 + .../org/projectforge/business/book/BookDO.kt | 3 + .../projectforge/business/fibu/AuftragDO.kt | 9 + .../business/fibu/AuftragsPositionDO.kt | 4 + .../business/fibu/EingangsrechnungDO.kt | 3 - .../fibu/EingangsrechnungsPositionDO.kt | 6 +- .../projectforge/business/fibu/EmployeeDO.kt | 11 +- .../business/fibu/EmployeeSalaryDO.kt | 3 + .../business/fibu/EmployeeValidSinceAttrDO.kt | 3 + .../org/projectforge/business/fibu/KundeDO.kt | 3 + .../business/fibu/PaymentScheduleDO.kt | 3 + .../projectforge/business/fibu/ProjektDO.kt | 9 + .../projectforge/business/fibu/RechnungDO.kt | 7 +- .../business/fibu/RechnungsPositionDO.kt | 7 +- .../business/fibu/kost/BuchungssatzDO.kt | 6 + .../business/fibu/kost/Kost2DO.kt | 4 + .../business/fibu/kost/KostZuweisungDO.kt | 376 +++++++++--------- .../business/gantt/GanttChartDO.kt | 4 + .../business/humanresources/HRPlanningDO.kt | 11 +- .../humanresources/HRPlanningEntryDO.kt | 29 +- .../business/orga/VisitorbookDO.kt | 4 + .../business/orga/VisitorbookEntryDO.kt | 6 +- .../business/scripting/ScriptDO.kt | 3 + .../business/sipgate/SipgateContactSyncDO.kt | 3 + .../org/projectforge/business/task/TaskDO.kt | 5 + .../business/teamcal/admin/model/TeamCalDO.kt | 3 + .../event/model/TeamEventAttendeeDO.kt | 4 + .../teamcal/event/model/TeamEventDO.kt | 4 + .../business/timesheet/TimesheetDO.kt | 5 + .../projectforge/business/user/UserPrefDao.kt | 70 ++-- .../business/user/UserXmlPreferencesDO.kt | 3 + .../vacation/model/LeaveAccountEntryDO.kt | 3 + .../vacation/model/RemainingLeaveDO.kt | 3 + .../business/vacation/model/VacationDO.kt | 7 + .../framework/access/GroupTaskAccessDO.kt | 5 +- .../framework/json/IdOnlySerializer.kt | 45 +++ .../framework/json/IdsOnlySerializer.kt | 54 +++ .../projectforge/framework/json/JsonUtils.kt | 154 +++---- .../framework/persistence/api/MagicFilter.kt | 2 +- .../persistence/history/HistoryEntryAttrDO.kt | 3 + .../persistence/user/entities/GroupDO.kt | 5 + .../persistence/user/entities/PFUserDO.kt | 6 +- .../user/entities/UserAuthenticationsDO.kt | 3 + .../user/entities/UserPasswordDO.kt | 3 + .../persistence/user/entities/UserPrefDO.kt | 3 + .../persistence/user/entities/UserRightDO.kt | 6 +- .../security/webauthn/WebAuthnEntryDO.kt | 3 + .../calendar/CalendarFilterFavoritesTest.kt | 2 +- .../business/user/UserPrefDaoTest.kt | 110 +++++ .../framework/json/JsonValidatorTest.kt | 8 +- .../persistence/api/MagicFilterTest.kt | 6 +- .../main/java/org/projectforge/Version.java | 6 +- 63 files changed, 746 insertions(+), 343 deletions(-) create mode 100644 projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdOnlySerializer.kt create mode 100644 projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdsOnlySerializer.kt create mode 100644 projectforge-business/src/test/kotlin/org/projectforge/business/user/UserPrefDaoTest.kt 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-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/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/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..2a6eb19cb9 --- /dev/null +++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdOnlySerializer.kt @@ -0,0 +1,45 @@ +///////////////////////////////////////////////////////////////////////////// +// +// 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 java.io.IOException + +class IdOnlySerializer : JsonSerializer>() { + @Throws(IOException::class) + override fun serialize(value: BaseDO<*>?, gen: JsonGenerator, serializers: SerializerProvider) { + if (value == null) { + gen.writeNull() + } else { + 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..b62bad79df --- /dev/null +++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdsOnlySerializer.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 + +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<*>) { + gen.writeStartObject() + JsonUtils.writeField(gen, "id", item.id) + gen.writeEndObject() + } else { + gen.writeNull() + } + } + gen.writeEndArray() + } else { + // Do nothing, collection not available. Don't fetch it. + } + } +} 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..655aa9c350 --- /dev/null +++ b/projectforge-business/src/test/kotlin/org/projectforge/business/user/UserPrefDaoTest.kt @@ -0,0 +1,110 @@ +///////////////////////////////////////////////////////////////////////////// +// +// 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.fibu.EmployeeDO +import org.projectforge.business.test.AbstractTestBase +import org.projectforge.business.vacation.model.VacationDO +import org.projectforge.framework.persistence.user.entities.PFUserDO + +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 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" + } + } + VacationDO().let { vacation -> + vacation.id = 5 + vacation.comment = "This is a comment" + vacation.employee = employee1 + vacation.replacement = employee2 + vacation.manager = employee2 + vacation.otherReplacements = mutableSetOf(employee2, employee3) + Assertions.assertEquals(vacation, vacation) + val json = UserPrefDao.serialize(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/json/JsonValidatorTest.kt b/projectforge-business/src/test/kotlin/org/projectforge/framework/json/JsonValidatorTest.kt index 14c19196f5..d49a64c111 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 @@ -47,11 +47,11 @@ class JsonValidatorTest : AbstractTestBase() { task.responsibleUser = user var jsonValidator = JsonValidator(toJsonString(task)) Assertions.assertEquals(42.0, jsonValidator.getDouble("responsibleUser.id")) - Assertions.assertEquals("kai", jsonValidator.get("responsibleUser.username")) + // 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, PFUserDO::class.java)) + //Assertions.assertEquals("Kai", jsonValidator.get("responsibleUser.firstname"), "Firstname shouldn't be ignored.") val timesheet = TimesheetDO() timesheet.user = user @@ -59,7 +59,7 @@ class JsonValidatorTest : AbstractTestBase() { jsonValidator = JsonValidator(toJsonString(timesheet)) Assertions.assertEquals(42.0, jsonValidator.getDouble("user.id")) - Assertions.assertEquals("kai", jsonValidator.get("user.username")) + // Assertions.assertEquals("kai", jsonValidator.get("user.username")) Assertions.assertNull(jsonValidator.get("user.firstname"), "Firstname shouldn't be serialized.") } 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-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"); } From 59519ee509f6e9bb306a5f3a88713340a6712bc4 Mon Sep 17 00:00:00 2001 From: Kai Reinhard Date: Mon, 30 Dec 2024 16:48:35 +0100 Subject: [PATCH 2/3] Json serialization refactored: IdOnlySerializer and IdsOnlySerializer introduced. --- .../src/main/resources/i18nKeys.json | 2 +- .../business/user/UserPasswordDao.kt | 2 +- .../projectforge/framework/ToStringUtil.kt | 90 +++++++++++-------- .../framework/json/IdOnlySerializer.kt | 32 +++++-- .../framework/json/IdsOnlySerializer.kt | 7 +- .../framework/json/JsonThreadLocalContext.kt | 54 +++++++++++ .../business/user/UserPrefDaoTest.kt | 69 ++++---------- .../framework/ToStringUtilTest.kt | 84 +++++++++++++++++ .../framework/json/JsonTestUtils.kt | 63 +++++++++++++ .../framework/json/JsonValidatorTest.kt | 16 ++-- .../framework/json/JsonValidator.kt | 28 +++--- 11 files changed, 328 insertions(+), 119 deletions(-) create mode 100644 projectforge-business/src/main/kotlin/org/projectforge/framework/json/JsonThreadLocalContext.kt create mode 100644 projectforge-business/src/test/kotlin/org/projectforge/framework/ToStringUtilTest.kt create mode 100644 projectforge-business/src/test/kotlin/org/projectforge/framework/json/JsonTestUtils.kt 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/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/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/json/IdOnlySerializer.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdOnlySerializer.kt index 2a6eb19cb9..bcd30d3720 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdOnlySerializer.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdOnlySerializer.kt @@ -27,19 +27,41 @@ 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>() { +class IdOnlySerializer : JsonSerializer>() { @Throws(IOException::class) - override fun serialize(value: BaseDO<*>?, gen: JsonGenerator, serializers: SerializerProvider) { - if (value == null) { - gen.writeNull() - } else { + 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 index b62bad79df..ec29a6ff2f 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdsOnlySerializer.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/json/IdsOnlySerializer.kt @@ -39,11 +39,10 @@ class IdsOnlySerializer : JsonSerializer>() { gen.writeStartArray() value.forEach { item -> if (item is IdObject<*>) { - gen.writeStartObject() - JsonUtils.writeField(gen, "id", item.id) - gen.writeEndObject() + IdOnlySerializer.writeObject(item, gen, serializers) } else { - gen.writeNull() + // Let Jackson serialize the value: + serializers.defaultSerializeValue(value, gen) } } gen.writeEndArray() 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/test/kotlin/org/projectforge/business/user/UserPrefDaoTest.kt b/projectforge-business/src/test/kotlin/org/projectforge/business/user/UserPrefDaoTest.kt index 655aa9c350..c01849923f 100644 --- a/projectforge-business/src/test/kotlin/org/projectforge/business/user/UserPrefDaoTest.kt +++ b/projectforge-business/src/test/kotlin/org/projectforge/business/user/UserPrefDaoTest.kt @@ -25,10 +25,9 @@ package org.projectforge.business.user import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test -import org.projectforge.business.fibu.EmployeeDO import org.projectforge.business.test.AbstractTestBase import org.projectforge.business.vacation.model.VacationDO -import org.projectforge.framework.persistence.user.entities.PFUserDO +import org.projectforge.framework.json.JsonTestUtils class UserPrefDaoTest : AbstractTestBase() { @Test @@ -55,56 +54,24 @@ class UserPrefDaoTest : AbstractTestBase() { @Test fun `test serialization of vacation`() { - 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" - } - } - VacationDO().let { vacation -> - vacation.id = 5 - vacation.comment = "This is a comment" - vacation.employee = employee1 - vacation.replacement = employee2 - vacation.manager = employee2 - vacation.otherReplacements = mutableSetOf(employee2, employee3) - Assertions.assertEquals(vacation, vacation) - val json = UserPrefDao.serialize(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}]") } + 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 }) - } + 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 d49a64c111..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 @@ -47,11 +47,12 @@ class JsonValidatorTest : AbstractTestBase() { task.responsibleUser = user var jsonValidator = JsonValidator(toJsonString(task)) Assertions.assertEquals(42.0, jsonValidator.getDouble("responsibleUser.id")) - // Assertions.assertEquals("kai", jsonValidator.get("responsibleUser.username")) + 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 @@ -59,13 +60,18 @@ class JsonValidatorTest : AbstractTestBase() { jsonValidator = JsonValidator(toJsonString(timesheet)) Assertions.assertEquals(42.0, jsonValidator.getDouble("user.id")) - // Assertions.assertEquals("kai", jsonValidator.get("user.username")) + 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/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)) { From 06a456e90cdda4db96e1ba90a247df52855d79a4 Mon Sep 17 00:00:00 2001 From: Kai Reinhard Date: Tue, 31 Dec 2024 00:38:17 +0100 Subject: [PATCH 3/3] JCR: backup jcr repositories with corrupted segments. --- .../kotlin/org/projectforge/jcr/NodeInfo.kt | 38 +- .../org/projectforge/jcr/RepoService.kt | 1331 +++++++++-------- .../org/projectforge/jcr/RepoTreeWalker.kt | 13 +- .../org/projectforge/jcr/SanityCheckMain.kt | 44 + .../src/main/resources/backupReadme.txt | 40 +- site/_docs/adminguide.adoc | 9 +- 6 files changed, 781 insertions(+), 694 deletions(-) create mode 100644 projectforge-jcr/src/main/kotlin/org/projectforge/jcr/SanityCheckMain.kt 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