From 943947aaae8734b471ce0cc53f05f8493b67bb9c Mon Sep 17 00:00:00 2001 From: Kai Reinhard Date: Tue, 24 Dec 2024 01:01:33 +0100 Subject: [PATCH] Addresses: support of gif and jpeg (also for carddav server). deprecated rest stuff (StorageClient, AddressDaoRest etc. removed). --- build.gradle.kts | 2 +- .../src/main/resources/i18nKeys.json | 10 +- .../business/address/AddressDO.kt | 4 - .../business/address/AddressDao.kt | 23 +- .../business/address/AddressImageCache.kt | 102 +++++++ .../business/address/AddressImageDO.kt | 10 +- .../business/address/AddressImageDao.kt | 49 +++- .../business/address/{vcard => }/ImageType.kt | 36 ++- .../business/address/ImagesResultFilter.kt | 2 +- .../business/address/vcard/VCardUtils.kt | 15 +- .../main/resources/I18nResources.properties | 2 +- .../resources/I18nResources_de.properties | 2 +- .../V8.0.3__RELEASE-AddressImageType.sql | 1 + .../business/address/vcard/VCardUtilsTest.kt | 2 +- .../org/projectforge/carddav/CardDavFilter.kt | 2 +- .../org/projectforge/carddav/CardDavUtils.kt | 2 +- .../projectforge/carddav/GetRequestHandler.kt | 5 +- .../org/projectforge/carddav/model/Contact.kt | 3 +- .../carddav/service/AddressDAVCache.kt | 25 +- .../projectforge/carddav/CarddavTestClient.kt | 36 ++- .../projectforge/model/rest/RestPaths.java | 20 -- .../org/projectforge/rest/AddressDaoRest.java | 276 ------------------ .../rest/converter/AddressDOConverter.java | 160 ---------- .../projectforge/storage/StorageClient.java | 133 --------- .../rest/AddressImageServicesRest.kt | 153 +++++----- .../org/projectforge/rest/AddressPagesRest.kt | 16 +- .../projectforge/rest/AddressViewPageRest.kt | 11 +- .../org/projectforge/rest/dto/Address.kt | 2 +- 28 files changed, 345 insertions(+), 759 deletions(-) create mode 100644 projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressImageCache.kt rename projectforge-business/src/main/kotlin/org/projectforge/business/address/{vcard => }/ImageType.kt (54%) create mode 100644 projectforge-business/src/main/resources/flyway/migrate/common/V8.0.3__RELEASE-AddressImageType.sql delete mode 100644 projectforge-rest/src/main/java/org/projectforge/rest/AddressDaoRest.java delete mode 100644 projectforge-rest/src/main/java/org/projectforge/rest/converter/AddressDOConverter.java delete mode 100644 projectforge-rest/src/main/java/org/projectforge/storage/StorageClient.java diff --git a/build.gradle.kts b/build.gradle.kts index de4787a010..640dad52ad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { allprojects { group = "org.projectforge" - version = "8.0" // Update version string here (nowhere else) + version = "8.1-SNAPSHOT" // Update version string here (nowhere else) repositories { mavenCentral() diff --git a/projectforge-application/src/main/resources/i18nKeys.json b/projectforge-application/src/main/resources/i18nKeys.json index 4d71f92d38..a4b8019ec7 100644 --- a/projectforge-application/src/main/resources/i18nKeys.json +++ b/projectforge-application/src/main/resources/i18nKeys.json @@ -281,8 +281,8 @@ {"i18nKey":"address.heading.privateAddress","bundleName":"I18nResources","translation":"Private address","translationDE":"Privatadresse","usedInClasses":["org.projectforge.rest.AddressPagesRest","org.projectforge.rest.AddressViewPageRest","org.projectforge.web.address.AddressPageSupport"],"usedInFiles":[]}, {"i18nKey":"address.heading.privateAddress2","bundleName":"I18nResources","translation":"Private address 2","translationDE":"Privatadresse 2","usedInClasses":[],"usedInFiles":[]}, {"i18nKey":"address.heading.privateContact","bundleName":"I18nResources","translation":"Private contakt","translationDE":"Privater Kontakt","usedInClasses":[],"usedInFiles":[]}, - {"i18nKey":"address.image","bundleName":"I18nResources","translation":"Image","translationDE":"Bild","usedInClasses":["org.projectforge.business.address.AddressDO","org.projectforge.rest.AddressPagesRest"],"usedInFiles":[]}, - {"i18nKey":"address.image.upload.error","bundleName":"I18nResources","translation":"Couln''t upload image. Supported format is only png for now with maximum size of {0}.","translationDE":"Bild kann nicht hochgeladen werden. Bisher wird nur das Bildformat PNG unterstützt mit der Maximalgröße von {0}.","usedInClasses":["org.projectforge.rest.AddressPagesRest"],"usedInFiles":[]}, + {"i18nKey":"address.image","bundleName":"I18nResources","translation":"Image","translationDE":"Bild","usedInClasses":["org.projectforge.rest.AddressPagesRest"],"usedInFiles":[]}, + {"i18nKey":"address.image.upload.error","bundleName":"I18nResources","translation":"Couldn''t upload image. Supported formats are png, gif and jpeg for now with maximum size of {0}.","translationDE":"Bild kann nicht hochgeladen werden. Es werden die Bildformate PNG, GIF und JPEG mit einer Maximalgröße von {0} unterstützt.","usedInClasses":["org.projectforge.rest.AddressPagesRest"],"usedInFiles":[]}, {"i18nKey":"address.mailing","bundleName":"I18nResources","translation":"Mailing","translationDE":"Mailing","usedInClasses":["org.projectforge.business.address.AddressExport"],"usedInFiles":[]}, {"i18nKey":"address.myCurrentCallerId","bundleName":"I18nResources","translation":"Caller id","translationDE":"Anruferkennung","usedInClasses":["org.projectforge.web.address.PhoneCallForm"],"usedInFiles":[]}, {"i18nKey":"address.myCurrentCallerId.tooltip.content","bundleName":"I18nResources","translation":"This caller id is displayed to the callee.","translationDE":"Diese Anruferkennung wird dem/der Angerufenen angezeigt.","usedInClasses":["org.projectforge.web.address.PhoneCallForm"],"usedInFiles":[]}, @@ -1447,7 +1447,7 @@ {"i18nKey":"label.sendEMailNotification","bundleName":"I18nResources","translation":"Send an e-mail notification?","translationDE":"E-Mail-Benachrichtigung versenden?","usedInClasses":["org.projectforge.plugins.todo.ToDoEditForm","org.projectforge.web.fibu.AuftragEditForm"],"usedInFiles":[]}, {"i18nKey":"label.sendShortMessage","bundleName":"I18nResources","translation":"Send the assignee a text message?","translationDE":"SMS an Bearbeiter versenden?","usedInClasses":["org.projectforge.plugins.todo.ToDoEditForm"],"usedInFiles":[]}, {"i18nKey":"language","bundleName":"I18nResources","translation":"Language","translationDE":"Sprache","usedInClasses":["org.projectforge.framework.persistence.search.MyAnalysisConfigurer","org.projectforge.web.address.AddressPageSupport"],"usedInFiles":[]}, - {"i18nKey":"lastUpdate","bundleName":"I18nResources","translation":"last modification","translationDE":"letzte Änderung","usedInClasses":["org.projectforge.business.address.AddressExport","org.projectforge.business.fibu.AuftragDO","org.projectforge.business.fibu.EingangsrechnungDO","org.projectforge.business.fibu.RechnungDO","org.projectforge.business.fibu.kost.KostZuweisungExport","org.projectforge.business.humanresources.HRPlanningDO","org.projectforge.business.teamcal.event.TeamEventDao","org.projectforge.business.teamcal.event.model.TeamEventDO","org.projectforge.business.timesheet.TimesheetDao","org.projectforge.business.timesheet.TimesheetExport","org.projectforge.framework.persistence.api.BaseDao","org.projectforge.framework.persistence.database.DatabaseService","org.projectforge.framework.persistence.database.ReindexerRegistry","org.projectforge.framework.persistence.database.ReindexerStrategy","org.projectforge.framework.persistence.entities.AbstractBaseDO","org.projectforge.framework.persistence.user.entities.UserAuthenticationsDO","org.projectforge.jcr.RepoService","org.projectforge.plugins.datatransfer.DataTransferAreaDao","org.projectforge.plugins.datatransfer.rest.DataTransferAreaPagesRest","org.projectforge.plugins.licensemanagement.LicenseListPage","org.projectforge.plugins.marketing.AddressCampaignListPage","org.projectforge.plugins.marketing.rest.AddressCampaignPagesRest","org.projectforge.plugins.memo.rest.MemoPagesRest","org.projectforge.plugins.merlin.MerlinTemplateDao","org.projectforge.plugins.skillmatrix.SkillEntryPagesRest","org.projectforge.plugins.todo.ToDoListPage","org.projectforge.rest.AddressBookPagesRest","org.projectforge.rest.AddressPagesRest","org.projectforge.rest.TeamCalPagesRest","org.projectforge.rest.importer.AbstractImportPageRest","org.projectforge.rest.my2fa.My2FASetupPageRest","org.projectforge.rest.scripting.MyScriptPagesRest","org.projectforge.rest.scripting.ScriptPagesRest","org.projectforge.security.webauthn.WebAuthnEntryDO","org.projectforge.ui.UIAgGridColumnDef","org.projectforge.ui.UIAttachmentList","org.projectforge.web.address.AddressListPage","org.projectforge.web.teamcal.admin.TeamCalListPage","org.projectforge.web.teamcal.event.TeamEventListPage","org.projectforge.web.user.UserPrefListPage"],"usedInFiles":[]}, + {"i18nKey":"lastUpdate","bundleName":"I18nResources","translation":"last modification","translationDE":"letzte Änderung","usedInClasses":["org.projectforge.business.address.AddressExport","org.projectforge.business.address.AddressImageCache","org.projectforge.business.address.AddressImageDao","org.projectforge.business.fibu.AuftragDO","org.projectforge.business.fibu.EingangsrechnungDO","org.projectforge.business.fibu.RechnungDO","org.projectforge.business.fibu.kost.KostZuweisungExport","org.projectforge.business.humanresources.HRPlanningDO","org.projectforge.business.teamcal.event.TeamEventDao","org.projectforge.business.teamcal.event.model.TeamEventDO","org.projectforge.business.timesheet.TimesheetDao","org.projectforge.business.timesheet.TimesheetExport","org.projectforge.framework.persistence.api.BaseDao","org.projectforge.framework.persistence.database.DatabaseService","org.projectforge.framework.persistence.database.ReindexerRegistry","org.projectforge.framework.persistence.database.ReindexerStrategy","org.projectforge.framework.persistence.entities.AbstractBaseDO","org.projectforge.framework.persistence.user.entities.UserAuthenticationsDO","org.projectforge.jcr.RepoService","org.projectforge.plugins.datatransfer.DataTransferAreaDao","org.projectforge.plugins.datatransfer.rest.DataTransferAreaPagesRest","org.projectforge.plugins.licensemanagement.LicenseListPage","org.projectforge.plugins.marketing.AddressCampaignListPage","org.projectforge.plugins.marketing.rest.AddressCampaignPagesRest","org.projectforge.plugins.memo.rest.MemoPagesRest","org.projectforge.plugins.merlin.MerlinTemplateDao","org.projectforge.plugins.skillmatrix.SkillEntryPagesRest","org.projectforge.plugins.todo.ToDoListPage","org.projectforge.rest.AddressBookPagesRest","org.projectforge.rest.AddressPagesRest","org.projectforge.rest.TeamCalPagesRest","org.projectforge.rest.importer.AbstractImportPageRest","org.projectforge.rest.my2fa.My2FASetupPageRest","org.projectforge.rest.scripting.MyScriptPagesRest","org.projectforge.rest.scripting.ScriptPagesRest","org.projectforge.security.webauthn.WebAuthnEntryDO","org.projectforge.ui.UIAgGridColumnDef","org.projectforge.ui.UIAttachmentList","org.projectforge.web.address.AddressListPage","org.projectforge.web.teamcal.admin.TeamCalListPage","org.projectforge.web.teamcal.event.TeamEventListPage","org.projectforge.web.user.UserPrefListPage"],"usedInFiles":[]}, {"i18nKey":"ldap","bundleName":"I18nResources","translation":"LDAP","translationDE":"LDAP","usedInClasses":["org.projectforge.business.ldap.LdapConnector","org.projectforge.framework.persistence.user.entities.GroupDO","org.projectforge.rest.GroupPagesRest","org.projectforge.rest.UserPagesRest","org.projectforge.web.user.GroupEditForm"],"usedInFiles":[]}, {"i18nKey":"ldap.gidNumber","bundleName":"I18nResources","translation":"GID number","translationDE":"GID number","usedInClasses":["org.projectforge.rest.GroupPagesRest","org.projectforge.rest.UserPagesRest","org.projectforge.web.user.GroupEditForm"],"usedInFiles":[]}, {"i18nKey":"ldap.gidNumber.alreadyInUse","bundleName":"I18nResources","translation":"GID number is already assigned to another group. The next free GID number is {0}.","translationDE":"Die GID-Nummer ist bereits an eine andere Gruppe vergeben. Die nächste freie GID-Nummer lautet: {0}.","usedInClasses":["org.projectforge.rest.GroupPagesRest","org.projectforge.web.user.GroupEditForm"],"usedInFiles":[]}, @@ -2136,7 +2136,7 @@ {"i18nKey":"scripting.title.edit","bundleName":"I18nResources","translation":"Edit script","translationDE":"Script bearbeiten","usedInClasses":["org.projectforge.rest.scripting.ScriptExecutePageRest"],"usedInFiles":[]}, {"i18nKey":"scripting.title.heading","bundleName":"I18nResources","translation":"Scripts","translationDE":"Scripte","usedInClasses":[],"usedInFiles":[]}, {"i18nKey":"scripting.title.list","bundleName":"I18nResources","translation":"Scripts","translationDE":"Scripte","usedInClasses":[],"usedInFiles":[]}, - {"i18nKey":"search","bundleName":"I18nResources","translation":"Search","translationDE":"Suchen","usedInClasses":["org.projectforge.rest.AddressDaoRest","org.projectforge.rest.AddressServicesRest","org.projectforge.rest.TimeZoneServicesRest","org.projectforge.rest.TimesheetPagesRest","org.projectforge.rest.admin.LogViewFilter","org.projectforge.rest.admin.LogViewerPageRest","org.projectforge.rest.calendar.VacationServicesRest","org.projectforge.rest.core.AbstractPagesRest","org.projectforge.rest.fibu.KontoPagesRest","org.projectforge.rest.task.TaskServicesRest","org.projectforge.ui.LayoutUtils","org.projectforge.ui.UIButton","org.projectforge.web.core.SearchForm","org.projectforge.web.registry.WebRegistry","org.projectforge.web.task.TaskTreeForm","org.projectforge.web.teamcal.integration.TeamCalCalendarForm","org.projectforge.web.wicket.AbstractListForm","org.projectforge.web.wicket.flowlayout.IconType"],"usedInFiles":[]}, + {"i18nKey":"search","bundleName":"I18nResources","translation":"Search","translationDE":"Suchen","usedInClasses":["org.projectforge.rest.AddressServicesRest","org.projectforge.rest.TimeZoneServicesRest","org.projectforge.rest.TimesheetPagesRest","org.projectforge.rest.admin.LogViewFilter","org.projectforge.rest.admin.LogViewerPageRest","org.projectforge.rest.calendar.VacationServicesRest","org.projectforge.rest.core.AbstractPagesRest","org.projectforge.rest.fibu.KontoPagesRest","org.projectforge.rest.task.TaskServicesRest","org.projectforge.ui.LayoutUtils","org.projectforge.ui.UIButton","org.projectforge.web.core.SearchForm","org.projectforge.web.registry.WebRegistry","org.projectforge.web.task.TaskTreeForm","org.projectforge.web.teamcal.integration.TeamCalCalendarForm","org.projectforge.web.wicket.AbstractListForm","org.projectforge.web.wicket.flowlayout.IconType"],"usedInFiles":[]}, {"i18nKey":"search.area","bundleName":"I18nResources","translation":"Area","translationDE":"Bereich","usedInClasses":["org.projectforge.web.core.SearchForm"],"usedInFiles":[]}, {"i18nKey":"search.durationOfSearch","bundleName":"I18nResources","translation":"duration of search","translationDE":"Suchdauer","usedInClasses":[],"usedInFiles":["./projectforge-wicket/src/main/java/org/projectforge/web/core/SearchAreaPanel.html"]}, {"i18nKey":"search.error","bundleName":"I18nResources","translation":"Unfortunately an internal error occured.","translationDE":"Es trat leider ein interner Fehler auf.","usedInClasses":["org.projectforge.web.core.SearchAreaPanel","org.projectforge.web.wicket.AbstractListPage"],"usedInFiles":[]}, @@ -2392,7 +2392,7 @@ {"i18nKey":"timeleft.weeks.one","bundleName":"I18nResources","translation":"in a week","translationDE":"in einer Woche","usedInClasses":[],"usedInFiles":[]}, {"i18nKey":"timeleft.years","bundleName":"I18nResources","translation":"in {0} years","translationDE":"in {0} Jahren","usedInClasses":[],"usedInFiles":[]}, {"i18nKey":"timeleft.years.one","bundleName":"I18nResources","translation":"in a year","translationDE":"in einem Jahr","usedInClasses":[],"usedInFiles":[]}, - {"i18nKey":"timesheet","bundleName":"I18nResources","translation":"Time-sheet","translationDE":"Zeitbericht","usedInClasses":["org.projectforge.Constants","org.projectforge.business.timesheet.TimesheetFavoritesService","org.projectforge.framework.persistence.DaoConst","org.projectforge.model.rest.RestPaths","org.projectforge.registry.Registry","org.projectforge.rest.calendar.CalendarServicesRest","org.projectforge.rest.calendar.FullCalendarEvent","org.projectforge.rest.calendar.TimesheetEventsProvider","org.projectforge.web.calendar.TimesheetEventsProvider","org.projectforge.web.timesheet.TimesheetEditPage","org.projectforge.web.timesheet.TimesheetListPage"],"usedInFiles":[]}, + {"i18nKey":"timesheet","bundleName":"I18nResources","translation":"Time-sheet","translationDE":"Zeitbericht","usedInClasses":["org.projectforge.Constants","org.projectforge.business.timesheet.TimesheetFavoritesService","org.projectforge.framework.persistence.DaoConst","org.projectforge.registry.Registry","org.projectforge.rest.calendar.CalendarServicesRest","org.projectforge.rest.calendar.FullCalendarEvent","org.projectforge.rest.calendar.TimesheetEventsProvider","org.projectforge.web.calendar.TimesheetEventsProvider","org.projectforge.web.timesheet.TimesheetEditPage","org.projectforge.web.timesheet.TimesheetListPage"],"usedInFiles":[]}, {"i18nKey":"timesheet.break","bundleName":"I18nResources","translation":"Break","translationDE":"leer","usedInClasses":["org.projectforge.rest.calendar.TimesheetEventsProvider","org.projectforge.web.calendar.TimesheetEventsProvider"],"usedInFiles":[]}, {"i18nKey":"timesheet.description","bundleName":"I18nResources","translation":"Activity report","translationDE":"Tätigkeitsbericht","usedInClasses":["org.projectforge.business.humanresources.HRPlanningExport","org.projectforge.business.timesheet.TimesheetDO","org.projectforge.rest.TimesheetPagesRest","org.projectforge.web.timesheet.TimesheetEditForm","org.projectforge.web.timesheet.TimesheetEditSelectRecentDialogPanel"],"usedInFiles":[]}, {"i18nKey":"timesheet.duration","bundleName":"I18nResources","translation":"Duration","translationDE":"Dauer","usedInClasses":["org.projectforge.business.timesheet.TimesheetExport","org.projectforge.renderer.custom.MicromataFormatter","org.projectforge.rest.TimesheetPagesRest","org.projectforge.web.calendar.CalendarForm","org.projectforge.web.teamcal.event.MyWicketEvent","org.projectforge.web.timesheet.TimesheetListPage"],"usedInFiles":[]}, diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressDO.kt index 571233cf65..380423acf3 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 @@ -272,10 +272,6 @@ open class AddressDO : DefaultBaseDO(), DisplayNameCapable { @get:Column open var birthday: LocalDate? = null - @PropertyInfo(i18nKey = "address.image") - @get:Column - open var image: Boolean? = null - /** * Time stamp of last image modification (or deletion). Usefull for history of changes. */ diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressDao.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressDao.kt index 263ea2efc2..147ce1e42f 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressDao.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressDao.kt @@ -278,7 +278,7 @@ open class AddressDao : BaseDao(AddressDO::class.java) { if (addressbookRight == null) { addressbookRight = userRights.getRight(UserRightId.MISC_ADDRESSBOOK) as AddressbookRight } - val addressbookList = addressbookCache.getAddressbooksForAddress(obj) ?: obj?.addressbookList + val addressbookList = addressbookCache.getAddressbooksForAddress(obj) ?: obj?.addressbookList if (addressbookList.isNullOrEmpty()) { return true } @@ -331,14 +331,6 @@ open class AddressDao : BaseDao(AddressDO::class.java) { } } - override fun onUpdate(obj: AddressDO, dbObj: AddressDO) { - // Don't modify the following fields: - if (obj.getTransientAttribute("Modify image modification data") !== "true") { - obj.image = dbObj.image - obj.imageLastUpdate = dbObj.imageLastUpdate - } - } - /** * On force deletion all personal address references has to be deleted. * @param obj The deleted object. @@ -363,19 +355,6 @@ open class AddressDao : BaseDao(AddressDO::class.java) { } } - /** - * Mark the given address, so the image fields (image and imageLastUpdate) will be updated. imageLastUpdate will be - * set to now. - * - * @param address - * @param hasImage Is there an image or not? - */ - fun internalModifyImageData(address: AddressDO, hasImage: Boolean) { - address.setTransientAttribute("Modify image modification data", "true") - address.image = hasImage - address.imageLastUpdate = Date() - } - override fun onInsert(obj: AddressDO) { // create uid if empty if (StringUtils.isBlank(obj.uid)) { diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressImageCache.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressImageCache.kt new file mode 100644 index 0000000000..16333bf4fe --- /dev/null +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressImageCache.kt @@ -0,0 +1,102 @@ +///////////////////////////////////////////////////////////////////////////// +// +// 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.address + +import jakarta.annotation.PostConstruct +import jakarta.persistence.Tuple +import mu.KotlinLogging +import org.projectforge.framework.cache.AbstractCache +import org.projectforge.framework.persistence.database.TupleUtils +import org.projectforge.framework.persistence.jpa.PfPersistenceService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component + +private val log = KotlinLogging.logger {} + +/** + * @author Kai Reinhard (k.reinhard@micromata.de) + */ +@Component +open class AddressImageCache : AbstractCache() { + @Autowired + private lateinit var persistenceService: PfPersistenceService + + /** + * Key is the address.id. Mustn't be synchronized, because it is only read. + */ + private var imageMap = mapOf() + + @PostConstruct + private fun postConstruct() { + instance = this + } + + /** + * Returns the image for the given addressId. + * The byte arrays of preview and image isn't loaded here. + * @param addressId The address.id. + * @return The image or null if not found. + */ + fun getImage(addressId: Long?): AddressImageDO? { + addressId ?: return null + checkRefresh() + return imageMap[addressId] + } + + /** + * This method will be called by CacheHelper and is synchronized via getData(); + */ + override fun refresh() { + log.info("Initializing AddressImageCache ...") + persistenceService.runIsolatedReadOnly(recordCallStats = true) { context -> + // This method must not be synchronized because it works with a new copy of maps. + val newMap = mutableMapOf() + val em = context.em + em.createQuery(SELECT_ADDRESS_IMAGE_INFO, Tuple::class.java).resultList + .forEach { tuple -> + val addressId = TupleUtils.getLong(tuple, "addressId")!! + newMap[addressId] = AddressImageDO().also { image -> + image.lastUpdate = TupleUtils.getDate(tuple, "lastUpdate") + image.imageType = tuple.get("imageType", ImageType::class.java) ?: ImageType.PNG + } + } + imageMap = newMap + log.info { + "Initializing of AddressImageCache done. ${ + context.formatStats( + true + ) + }" + } + } + } + + companion object { + lateinit var instance: AddressImageCache + private set + + private val SELECT_ADDRESS_IMAGE_INFO = + "SELECT address.id as addressId,lastUpdate as lastUpdate,imageType as imageType FROM ${AddressImageDO::class.simpleName}" + } +} 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 9e44152135..1934e542ad 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 @@ -33,15 +33,15 @@ import java.util.Date @NamedQueries( NamedQuery( name = AddressImageDO.SELECT_WITHOUT_IMAGES, - query = "select id,lastUpdate from AddressImageDO where address.id = :addressId" + query = "select id as id,lastUpdate as lastUpdate,imageType as imageType from AddressImageDO where address.id = :addressId" ), NamedQuery( name = AddressImageDO.SELECT_IMAGE_ONLY, - query = "select id,lastUpdate,image from AddressImageDO where address.id = :addressId" + query = "select id as id,lastUpdate as lastUpdate,imageType as imageType,image as image from AddressImageDO where address.id = :addressId" ), NamedQuery( name = AddressImageDO.SELECT_IMAGE_PREVIEW_ONLY, - query = "select id,lastUpdate,imagePreview from AddressImageDO where address.id = :addressId" + query = "select id as id,lastUpdate as lastUpdate,imageType as imageType,imagePreview as imagePreview from AddressImageDO where address.id = :addressId" ), NamedQuery( name = AddressImageDO.DELETE_ALL_IMAGES_BY_ADDRESS_ID, @@ -71,6 +71,10 @@ open class AddressImageDO : IdObject { @get:Basic(fetch = FetchType.LAZY) open var imagePreview: ByteArray? = null + @get:Column(name = "image_type", length = 5) + @get:Enumerated(EnumType.STRING) + open var imageType: ImageType? = null + companion object { internal const val SELECT_WITHOUT_IMAGES = "AddressImageDO.selectWithoutImages" internal const val SELECT_IMAGE_ONLY = "AddressImageDO.selectImage" diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressImageDao.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressImageDao.kt index 05bdc47ab2..03c01d5859 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressImageDao.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressImageDao.kt @@ -42,6 +42,9 @@ open class AddressImageDao { @Autowired private lateinit var addressDao: AddressDao + @Autowired + private lateinit var addressImageCache: AddressImageCache + @Autowired private lateinit var persistenceService: PfPersistenceService @@ -61,9 +64,9 @@ open class AddressImageDao { if (fetchImage && fetchPreviewImage) { // Fetch all: val res = persistenceService.find(AddressImageDO::class.java, addressId) - if (res != null && res.lastUpdate == null) { + if (res != null) { // Fix: lastUpdate is not set in the database for older entries. - res.lastUpdate = address?.imageLastUpdate ?: Date(0L) + res.lastUpdate = res.lastUpdate ?: Date(0L) } return res } @@ -77,15 +80,16 @@ open class AddressImageDao { } persistenceService.selectNamedSingleResult(namedQuery, Tuple::class.java, Pair("addressId", addressId))?.let { result.id = it[0] as? Long - result.lastUpdate = it[1] as? Date + result.lastUpdate = TupleUtils.getDate(it, "lastUpdate") + result.imageType = it.get("imageType", ImageType::class.java) ?: ImageType.PNG // Default image type for older images (<=8.0). if (fetchImage) { - result.image = it[2] as? ByteArray + result.image = it.get("image", ByteArray::class.java) } else if (fetchPreviewImage) { - result.imagePreview = it[2] as? ByteArray + result.imagePreview = it.get("imagePreview", ByteArray::class.java) } } // Fix: lastUpdate is not set in the database for older entries. - result.lastUpdate = result.lastUpdate ?: address?.imageLastUpdate ?: Date(0L) + result.lastUpdate = result.lastUpdate ?: Date(0L) return result } @@ -104,18 +108,40 @@ open class AddressImageDao { return findImage(addressId, fetchPreviewImage = true, checkAccess = checkAccess)?.imagePreview } + /** + * Extract the image type from the filename. Supported image types are PNG and JPEG. + * @param filename The filename. + * @return The image type, if the image type is supported. Otherwise, null. + * @see ImageType + */ + fun getSupportedImageType(filename: String?): ImageType? { + val imageType = ImageType.fromExtension(filename) + return if (isSupportedImageType(imageType)) { + imageType + } else { + null + } + } + + fun isSupportedImageType(imageType: ImageType?): Boolean { + return imageType != null + } + /** * Does the access checking. The user may only modify images, if he has the access to modify the given address. */ - open fun saveOrUpdate(addressId: Long, image: ByteArray): Boolean { + open fun saveOrUpdate(addressId: Long, image: ByteArray, imageType: ImageType): Boolean { + if (!isSupportedImageType(imageType)) { + log.error("Can't save or update immage of address. Unsupported image type: $imageType.") + return false + } val address = addressDao.find(addressId) if (address == null) { log.error("Can't save or update immage of address. Address #$addressId not found.") return false } - addressDao.internalModifyImageData(address, true) + address.imageLastUpdate = Date() addressDao.update(address) // Throws an exception if the logged-in user has now access. - persistenceService.runInTransaction { context -> val addressImage = context.selectSingleResult( "from ${AddressImageDO::class.java.name} t where t.address = :address", @@ -126,6 +152,7 @@ open class AddressImageDao { addressImage.address = address addressImage.image = image addressImage.imagePreview = imageService.resizeImage(image) + addressImage.imageType = imageType addressImage.lastUpdate = Date() if (addressImage.id != null) { // Update @@ -136,6 +163,7 @@ open class AddressImageDao { } } log.info("New image for address ${address.id} (${address.fullName}) saved.") + addressImageCache.setExpired() return true } @@ -148,7 +176,7 @@ open class AddressImageDao { log.error("Can't save or update immage of address. Address #$addressId not found.") return false } - addressDao.internalModifyImageData(address, false) + address.imageLastUpdate = null addressDao.update(address) // Throws an exception if the logged-in user has now access. var success = false persistenceService.runInTransaction { context -> @@ -164,6 +192,7 @@ open class AddressImageDao { success = true } } + addressImageCache.setExpired() return success } } diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/vcard/ImageType.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/ImageType.kt similarity index 54% rename from projectforge-business/src/main/kotlin/org/projectforge/business/address/vcard/ImageType.kt rename to projectforge-business/src/main/kotlin/org/projectforge/business/address/ImageType.kt index 31f8ad82b2..abbbdceb49 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/vcard/ImageType.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/ImageType.kt @@ -21,7 +21,7 @@ // ///////////////////////////////////////////////////////////////////////////// -package org.projectforge.business.address.vcard +package org.projectforge.business.address enum class ImageType(val extension: String, val mimeType: String) { JPEG("jpg", "image/jpeg"), PNG("png", "image/png"), GIF("gif", "image/gif"); @@ -33,4 +33,38 @@ enum class ImageType(val extension: String, val mimeType: String) { GIF -> ezvcard.parameter.ImageType.GIF } } + + companion object { + fun fromExtension(filename: String?): ImageType? { + filename ?: return null + return if (filename.endsWith(".jpg", ignoreCase = true) || filename.endsWith(".jpeg", ignoreCase = true)) { + JPEG + } else if (filename.endsWith(".png", ignoreCase = true)) { + PNG + } else if (filename.endsWith(".gif", ignoreCase = true)) { + GIF + } else { + null + } + } + + fun fromString(value: String?): ImageType? { + value ?: return null + return when (value.lowercase()) { + "jpg", "jpeg" -> JPEG + "png" -> PNG + "gif" -> GIF + else -> null + } + } + + fun from(imageType: ezvcard.parameter.ImageType): ImageType? { + return when (imageType) { + ezvcard.parameter.ImageType.JPEG -> JPEG + ezvcard.parameter.ImageType.PNG -> PNG + ezvcard.parameter.ImageType.GIF -> GIF + else -> null + } + } + } } diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/ImagesResultFilter.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/ImagesResultFilter.kt index 4dbaf42013..3447de6471 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/ImagesResultFilter.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/ImagesResultFilter.kt @@ -27,6 +27,6 @@ import org.projectforge.framework.persistence.api.impl.CustomResultFilter class ImagesResultFilter : CustomResultFilter { override fun match(list: MutableList, element: AddressDO): Boolean { - return element.image == true + return AddressImageCache.instance.getImage(element.id) != null } } diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/vcard/VCardUtils.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/vcard/VCardUtils.kt index 6d0e8e2921..ecb71c91e1 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/vcard/VCardUtils.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/vcard/VCardUtils.kt @@ -31,6 +31,8 @@ import ezvcard.parameter.TelephoneType import ezvcard.property.* import mu.KotlinLogging import org.projectforge.business.address.AddressDO +import org.projectforge.business.address.AddressImageDO +import org.projectforge.business.address.ImageType import org.projectforge.framework.time.PFDateTime import java.io.ByteArrayInputStream import java.io.IOException @@ -108,9 +110,9 @@ object VCardUtils { } vcard.addUrl(addressDO.website) vcard.addNote(addressDO.comment) - if (addressDO.image == true && imageUrl != null && imageType != null) { + if (imageUrl != null) { // embedded: Photo(addressImageDao.getImage(addressDO.id!!), ImageType.JPEG).let { vcard.addPhoto(it) } - Photo(imageUrl, imageType.asVCardImageType()).let { + Photo(imageUrl, (imageType ?: ImageType.PNG).asVCardImageType()).let { vcard.addPhoto(it) } } @@ -221,7 +223,14 @@ object VCardUtils { for (note in vcard.notes) { address.comment = if (address.comment != null) address.comment else "" + note.value + " " } - address.image = vcard.photos.isNullOrEmpty() + vcard.photos.firstOrNull { it.data != null }?.let { photo -> + address.setTransientAttribute("image", AddressImageDO().also { + it.image = photo.data + photo.contentType?.let { contentType -> + it.imageType = ImageType.from(contentType) ?: ImageType.PNG + } + }) + } if (vcard.organization?.values?.isNotEmpty() == true) { when (vcard.organization.values.size) { 3 -> { diff --git a/projectforge-business/src/main/resources/I18nResources.properties b/projectforge-business/src/main/resources/I18nResources.properties index 3f9eb2795e..abf85e7d06 100644 --- a/projectforge-business/src/main/resources/I18nResources.properties +++ b/projectforge-business/src/main/resources/I18nResources.properties @@ -446,7 +446,7 @@ address.heading.privateAddress=Private address address.heading.privateAddress2=Private address 2 address.heading.privateContact=Private contakt address.image=Image -address.image.upload.error=Couln''t upload image. Supported format is only png for now with maximum size of {0}. +address.image.upload.error=Couldn''t upload image. Supported formats are png, gif and jpeg for now with maximum size of {0}. address.mailing=Mailing address.myCurrentCallerId=Caller id address.myCurrentCallerId.tooltip.content=This caller id is displayed to the callee. diff --git a/projectforge-business/src/main/resources/I18nResources_de.properties b/projectforge-business/src/main/resources/I18nResources_de.properties index 6d52ce7b40..fb63bb1553 100644 --- a/projectforge-business/src/main/resources/I18nResources_de.properties +++ b/projectforge-business/src/main/resources/I18nResources_de.properties @@ -538,7 +538,7 @@ address.heading.privateAddress=Privatadresse address.heading.privateAddress2=Privatadresse 2 address.heading.privateContact=Privater Kontakt address.image=Bild -address.image.upload.error=Bild kann nicht hochgeladen werden. Bisher wird nur das Bildformat PNG unterstützt mit der Maximalgröße von {0}. +address.image.upload.error=Bild kann nicht hochgeladen werden. Es werden die Bildformate PNG, GIF und JPEG mit einer Maximalgröße von {0} unterstützt. address.mailing=Mailing address.myCurrentCallerId=Anruferkennung address.myCurrentCallerId.tooltip.content=Diese Anruferkennung wird dem/der Angerufenen angezeigt. diff --git a/projectforge-business/src/main/resources/flyway/migrate/common/V8.0.3__RELEASE-AddressImageType.sql b/projectforge-business/src/main/resources/flyway/migrate/common/V8.0.3__RELEASE-AddressImageType.sql new file mode 100644 index 0000000000..65449a5254 --- /dev/null +++ b/projectforge-business/src/main/resources/flyway/migrate/common/V8.0.3__RELEASE-AddressImageType.sql @@ -0,0 +1 @@ +ALTER TABLE T_ADDRESS_IMAGE ADD COLUMN image_type CHARACTER VARYING(5); diff --git a/projectforge-business/src/test/kotlin/org/projectforge/business/address/vcard/VCardUtilsTest.kt b/projectforge-business/src/test/kotlin/org/projectforge/business/address/vcard/VCardUtilsTest.kt index 298cbe8e23..0556a44cdd 100644 --- a/projectforge-business/src/test/kotlin/org/projectforge/business/address/vcard/VCardUtilsTest.kt +++ b/projectforge-business/src/test/kotlin/org/projectforge/business/address/vcard/VCardUtilsTest.kt @@ -26,6 +26,7 @@ package org.projectforge.business.address.vcard import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.projectforge.business.address.AddressDO +import org.projectforge.business.address.ImageType import org.projectforge.business.test.TestSetup import java.nio.charset.StandardCharsets import java.time.LocalDate @@ -39,7 +40,6 @@ class VCardUtilsTest { address.birthday = LocalDate.of(1992, Month.JULY, 11) address.firstName = "Joe" address.name = "Hill" - address.image = true val vcardString = VCardUtils.buildVCardString(address, VCardVersion.V_3_0, "https://www.projectforge.org/carddav/users/kai/photos/contact-1234.png", ImageType.JPEG) Assertions.assertTrue(vcardString.contains("BDAY:1992-07-11")) } diff --git a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavFilter.kt b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavFilter.kt index 0e819d50a7..b33fd27b60 100644 --- a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavFilter.kt +++ b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavFilter.kt @@ -104,7 +104,7 @@ class CardDavFilter : Filter { val NORMALIZED_GET_REQUEST_REGEX = """^users/([^/]+)/addressbooks/ProjectForge-(\d+)\.vcf$""".toRegex() // photos/contact-129.png - val NORMALIZED_GET_PHOTO_REQUEST_REGEX = """^photos/contact-(\d+).png$""".toRegex() + val NORMALIZED_GET_PHOTO_REQUEST_REGEX = """^photos/contact-(\d+).(png|gif|jpg|jpeg)$""".toRegex() /** * PROPFIND: /, /index.html, /carddav, /.well-known/carddav diff --git a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavUtils.kt b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavUtils.kt index a278c209da..f2f3468c75 100644 --- a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavUtils.kt +++ b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavUtils.kt @@ -24,7 +24,7 @@ package org.projectforge.carddav import jakarta.servlet.http.HttpServletResponse -import org.projectforge.business.address.vcard.ImageType +import org.projectforge.business.address.ImageType import org.projectforge.carddav.CardDavService.Companion.domain import org.projectforge.carddav.CardDavXmlUtils.EXTRACT_ADDRESS_ID_PHOTO_REGEX import org.projectforge.carddav.CardDavXmlUtils.EXTRACT_ADDRESS_ID_REGEX diff --git a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/GetRequestHandler.kt b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/GetRequestHandler.kt index be29d81346..4aa6518673 100644 --- a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/GetRequestHandler.kt +++ b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/GetRequestHandler.kt @@ -25,7 +25,7 @@ package org.projectforge.carddav import mu.KotlinLogging import org.projectforge.business.address.AddressImageDao -import org.projectforge.business.address.vcard.ImageType +import org.projectforge.business.address.ImageType import org.projectforge.framework.time.PFDateTime import org.projectforge.framework.time.PFDateTimeUtils import org.projectforge.rest.utils.ResponseUtils @@ -121,6 +121,7 @@ internal class GetRequestHandler { log.debug { "handleImageGetCall: ${writerContext.requestWrapper.request.method}: '${writerContext.requestWrapper.requestURI}'" } val imageDO = addressImageDao.findImage(contactId, fetchImage = true) val image = imageDO?.image + val imageType = imageDO?.imageType ?: ImageType.PNG if (image == null) { ResponseUtils.setValues(writerContext.response, status = HttpStatus.NOT_FOUND) return @@ -143,7 +144,7 @@ internal class GetRequestHandler { ResponseUtils.setByteArrayContent( writerContext.response, status = HttpStatus.OK, - contentType = ImageType.PNG.mimeType, + contentType = imageType.mimeType, content = image ) writerContext.response.let { response -> diff --git a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/model/Contact.kt b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/model/Contact.kt index 6be1e2527e..0b5734c2fe 100644 --- a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/model/Contact.kt +++ b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/model/Contact.kt @@ -23,6 +23,7 @@ package org.projectforge.carddav.model +import org.projectforge.business.address.ImageType import org.projectforge.carddav.CardDavUtils import org.projectforge.framework.time.PFDateTime import java.security.MessageDigest @@ -33,8 +34,8 @@ data class Contact( val firstName: String? = null, val lastName: String? = null, val lastUpdated: Date? = null, - val hasImage: Boolean = false, val imageLastUpdate: Date? = null, + val imageType: ImageType? = null, var vcardData: String? = null, ) { val displayName = "$lastName, $firstName" diff --git a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/service/AddressDAVCache.kt b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/service/AddressDAVCache.kt index 5b1484904c..2a0ebd3f82 100644 --- a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/service/AddressDAVCache.kt +++ b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/service/AddressDAVCache.kt @@ -25,10 +25,7 @@ package org.projectforge.carddav.service import jakarta.annotation.PostConstruct import mu.KotlinLogging -import org.projectforge.business.address.AddressDO -import org.projectforge.business.address.AddressDao -import org.projectforge.business.address.AddressImageDao -import org.projectforge.business.address.vcard.ImageType +import org.projectforge.business.address.* import org.projectforge.business.address.vcard.VCardUtils import org.projectforge.carddav.CardDavConfig import org.projectforge.carddav.CardDavUtils @@ -50,7 +47,7 @@ open class AddressDAVCache : AbstractCache(TICKS_PER_HOUR), BaseDOModifiedListen private lateinit var addressDao: AddressDao @Autowired - private lateinit var addressImageDao: AddressImageDao + private lateinit var addressImageCache: AddressImageCache @Autowired private lateinit var cardDavConfig: CardDavConfig @@ -75,21 +72,27 @@ open class AddressDAVCache : AbstractCache(TICKS_PER_HOUR), BaseDOModifiedListen log.info { "Got ${result.size} addresses from cache and must load ${missedInCache.size} from data base..." } if (missedInCache.size > 0) { addressDao.select(missedInCache, checkAccess = false)?.forEach { - val imageType = ImageType.PNG - val imageUrl = if(it.image == true) { - CardDavUtils.getImageUrl(it.id!!, imageType) + val image = addressImageCache.getImage(it.id!!) + val imageType = image?.imageType + val imageUrl = if (image != null) { + CardDavUtils.getImageUrl(it.id!!, imageType ?: ImageType.PNG) } else { null } - val vcard = VCardUtils.buildVCardString(it, cardDavConfig.vcardVersion, imageUrl = imageUrl, imageType = imageType) + val vcard = VCardUtils.buildVCardString( + it, + cardDavConfig.vcardVersion, + imageUrl = imageUrl, + imageType = imageType + ) val contact = Contact( it.id, firstName = it.firstName, lastName = it.name, lastUpdated = it.lastUpdate, - hasImage = it.image == true, - imageLastUpdate = it.imageLastUpdate, + imageType = imageType, + imageLastUpdate = image?.lastUpdate, vcardData = vcard, ) addCachedContact(it.id!!, contact) diff --git a/projectforge-carddav/src/test/kotlin/org/projectforge/carddav/CarddavTestClient.kt b/projectforge-carddav/src/test/kotlin/org/projectforge/carddav/CarddavTestClient.kt index 1ea0d3253c..3465b081e2 100644 --- a/projectforge-carddav/src/test/kotlin/org/projectforge/carddav/CarddavTestClient.kt +++ b/projectforge-carddav/src/test/kotlin/org/projectforge/carddav/CarddavTestClient.kt @@ -58,7 +58,7 @@ fun main(args: Array) { } val username = args[0] val davToken = args[1] - val baseUrl = if (args.size > 2) args[2] else "http://localhost:8080/carddav" + val baseUrl = "http://localhost:8080/carddav" // if (args.size > 2) args[2] else "http://localhost:8080/carddav" val lastSyncToken = if (args.size > 3) args[3] else null val client = CardDavTestClient(baseUrl, username = username, password = davToken, lastSyncToken = lastSyncToken) client.run() @@ -72,7 +72,7 @@ fun main(args: Array) { // REPORT for Milton (uri=/users/username/addressBooks/default/ class CardDavTestClient(private val baseUrl: String, username: String, password: String, val lastSyncToken: String?) { - private class ResponseData(val content: String, val headers: String) + private class ResponseData(val content: String, val status: Int, val headers: String) private val client: CloseableHttpClient = HttpClients.createDefault() private val authHeader: String = "Basic " + Base64.getEncoder().encodeToString("$username:$password".toByteArray()) @@ -80,7 +80,7 @@ class CardDavTestClient(private val baseUrl: String, username: String, password: private val responseHandler = HttpClientResponseHandler { response: ClassicHttpResponse -> val entity: HttpEntity? = response.entity val content = entity?.content?.bufferedReader(Charsets.UTF_8)?.use { it.readText() } ?: "" - ResponseData(content, response.headers.joinToString { "${it.name}=${it.value}" }) + ResponseData(content, response.code, response.headers.joinToString { "${it.name}=${it.value}" }) } /** @@ -101,9 +101,8 @@ class CardDavTestClient(private val baseUrl: String, username: String, password: """.trimIndent().let { body -> sendRequest("PROPFIND", requestBody = body, useAuthHeader = true) }*/ - sendRequest("GET", path = "/users/kai/photos/contact-825.png", useAuthHeader = true).let { response -> - println("Response GET: size=${response.length}") - } + sendRequest("GET", path = "/users/kai/addressbooks/ProjectForge-1970264.vcf", useAuthHeader = true) + sendRequest("GET", path = "/photos/contact-1970264.jpg", useAuthHeader = true, logResponseContent = false) /* """ @@ -184,6 +183,7 @@ class CardDavTestClient(private val baseUrl: String, username: String, password: path: String = "", requestBody: String? = null, useAuthHeader: Boolean = false, + logResponseContent: Boolean = true, ): String { val url = "$baseUrl$path" val context = HttpCoreContext.create() @@ -203,12 +203,12 @@ class CardDavTestClient(private val baseUrl: String, username: String, password: println(" ]") } client.execute(builder.build(), context, responseHandler).also { - logResponse(method, url, it) + logResponse(method, url, it, logResponseContent = logResponseContent) return it.content } } - private fun sendSyncReportRequest(path: String = "") { + private fun sendSyncReportRequest(path: String = "", logResponseContent: Boolean = true) { val url = "$baseUrl$path" val context = HttpCoreContext.create() val body = """ @@ -227,11 +227,11 @@ class CardDavTestClient(private val baseUrl: String, username: String, password: builder.setEntity(StringEntity(body, ContentType.APPLICATION_XML)) val response = client.execute(builder.build(), context, responseHandler).also { - logResponse("REPORT", url, it) + logResponse("REPORT", url, it, logResponseContent = logResponseContent) } } - private fun sendGetRequest(path: String = "", useAuthHeader: Boolean = false) { + private fun sendGetRequest(path: String = "", useAuthHeader: Boolean = false, logResponseContent: Boolean = true) { val url = "$baseUrl$path" val context = HttpCoreContext.create() val getRequest = HttpGet(url) @@ -239,16 +239,20 @@ class CardDavTestClient(private val baseUrl: String, username: String, password: getRequest.addHeader("Authorization", authHeader) } val result = client.execute(getRequest, context, responseHandler) - logResponse("GET", url, result) + logResponse("GET", url, result, logResponseContent = logResponseContent) } - private fun logResponse(method: String, endpoint: String, response: ResponseData) { + private fun logResponse(method: String, endpoint: String, response: ResponseData, logResponseContent: Boolean) { println("$method: $endpoint: response=[") - println(" headers=[${response.headers}], content-length=${response.content.length}") + println(" status=[${response.status}], headers=[${response.headers}], content-length=${response.content.length}") if (response.content.isNotEmpty()) { - println(" content=[") - println(response.content.abbreviate(2000)) - println(" ]") + if (logResponseContent) { + println(" content=[") + println(response.content.abbreviate(2000)) + println(" ]") + } else { + println(" content-size=${response.content.length}") + } } println("]") } diff --git a/projectforge-model/src/main/java/org/projectforge/model/rest/RestPaths.java b/projectforge-model/src/main/java/org/projectforge/model/rest/RestPaths.java index 3ffe9c8f16..d5feb7895c 100644 --- a/projectforge-model/src/main/java/org/projectforge/model/rest/RestPaths.java +++ b/projectforge-model/src/main/java/org/projectforge/model/rest/RestPaths.java @@ -36,22 +36,8 @@ public class RestPaths public static final String REST_START_MULTI_SELECTION = "startMultiSelection"; - public static final String ADDRESS = "address"; - - public static final String AUTHENTICATE = "authenticate"; - - public static final String AUTHENTICATE_GET_TOKEN_METHOD = "getToken"; - - public static final String AUTHENTICATE_GET_TOKEN = AUTHENTICATE + "/" + AUTHENTICATE_GET_TOKEN_METHOD; - - public static final String AUTHENTICATE_INITIAL_CONTACT_METHOD = "initialContact"; - - public static final String AUTHENTICATE_INITIAL_CONTACT = AUTHENTICATE + "/" + AUTHENTICATE_INITIAL_CONTACT_METHOD; - public static final String TASK = "task"; - public static final String TIMESHEET = "timesheet"; - public static final String TIMESHEET_TEMPLATE = "timesheetTemplate"; public static final String LIST = "list"; @@ -78,12 +64,6 @@ public class RestPaths public static final String CLONE = "clone"; - public static final String TREE = "tree"; - - public static final String TEAMCAL = "teamcal"; - - public static final String TEAMEVENTS = "teamevents"; - public static final String SET_COLUMN_STATES = "setColumnStates"; public static final String WATCH_FIELDS = "watchFields"; diff --git a/projectforge-rest/src/main/java/org/projectforge/rest/AddressDaoRest.java b/projectforge-rest/src/main/java/org/projectforge/rest/AddressDaoRest.java deleted file mode 100644 index 742f7bad3c..0000000000 --- a/projectforge-rest/src/main/java/org/projectforge/rest/AddressDaoRest.java +++ /dev/null @@ -1,276 +0,0 @@ -///////////////////////////////////////////////////////////////////////////// -// -// 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.rest; - -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.PredicateUtils; -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.projectforge.business.address.*; -import org.projectforge.business.user.ProjectForgeGroup; -import org.projectforge.framework.access.AccessChecker; -import org.projectforge.framework.configuration.Configuration; -import org.projectforge.framework.configuration.ConfigurationParam; -import org.projectforge.framework.json.JsonUtils; -import org.projectforge.framework.persistence.api.BaseSearchFilter; -import org.projectforge.framework.utils.NumberHelper; -import org.projectforge.model.rest.AddressObject; -import org.projectforge.model.rest.RestPaths; -import org.projectforge.rest.converter.AddressDOConverter; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; - -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import java.util.*; - -import static org.projectforge.framework.persistence.user.api.ThreadLocalUserContext.getLoggedInUserId; - -/** - * REST-Schnittstelle für {@link AddressDao} - * - * @author Kai Reinhard (k.reinhard@micromata.de) - */ -@Controller -@Path(RestPaths.ADDRESS) -public class AddressDaoRest { - private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AddressDaoRest.class); - - @Autowired - private AccessChecker accessChecker; - - @Autowired - private AddressDao addressDao; - - @Autowired - private AddressImageDao addressImageDao; - - @Autowired - private PersonalAddressDao personalAddressDao; - - @Autowired - private AddressbookDao addressbookDao; - - /** - * Rest-Call for {@link AddressDao#getFavoriteVCards()}.
- * If modifiedSince is given then only those addresses will be returned: - *
    - *
  1. The address was changed after the given modifiedSince date, or
  2. - *
  3. the address was added to the user's personal address book after the given modifiedSince date, or
  4. - *
  5. the address was removed from the user's personal address book after the given modifiedSince date.
  6. - *
- * - * @param searchTerm - * @param modifiedSince milliseconds since 1970 (UTC) - * @param all If true and the user is member of the ProjectForge's group {@link ProjectForgeGroup#FINANCE_GROUP} or - * {@link ProjectForgeGroup#MARKETING_GROUP} the export contains all addresses instead of only favorite - * addresses. - */ - @GET - @Path(RestPaths.LIST) - @Produces(MediaType.APPLICATION_JSON) - public Response getList(@QueryParam("search") final String searchTerm, - @QueryParam("modifiedSince") final Long modifiedSince, - @QueryParam("all") final Boolean all, - @QueryParam("disableImageData") final Boolean disableImageData, - @QueryParam("disableVCardData") final Boolean disableVCardData) { - log.info(RestPaths.LIST + "?search='" + searchTerm + "'&modifiedSince=" + modifiedSince + "&all=" + all + "disableImageDate=" + disableImageData + "&disableVCardData=" + disableVCardData); - final AddressFilter filter = new AddressFilter(new BaseSearchFilter()); - Date modifiedSinceDate = null; - if (modifiedSince != null) { - modifiedSinceDate = new Date(modifiedSince); - filter.setModifiedSince(modifiedSinceDate); - } - if (StringUtils.isNotBlank(searchTerm) && NumberHelper.matchesPhoneNumber(searchTerm)) { - filter.setSearchString("*" + NumberHelper.extractPhonenumber(searchTerm, Configuration.getInstance().getStringValue(ConfigurationParam.DEFAULT_COUNTRY_PHONE_PREFIX))); - } else { - filter.setSearchString(searchTerm); - } - - final List list = addressDao.select(filter); - - final List result = new ArrayList(); - if (modifiedSince == null && accessChecker.isLoggedInUserMemberOfGroup(ProjectForgeGroup.FINANCE_GROUP, - ProjectForgeGroup.MARKETING_GROUP)) { - for (AddressDO addressDO : list) { - final AddressObject address = AddressDOConverter.getAddressObject(addressDao, addressImageDao, addressDO, - BooleanUtils.isTrue(disableImageData), BooleanUtils.isTrue(disableVCardData)); - result.add(address); - } - } else { - boolean exportAll = false; - if (BooleanUtils.isTrue(all) - && accessChecker.isLoggedInUserMemberOfGroup(ProjectForgeGroup.FINANCE_GROUP, - ProjectForgeGroup.MARKETING_GROUP)) { - exportAll = true; - } - - List favorites = null; - Set favoritesSet = null; - if (!exportAll) { - favorites = personalAddressDao.getList(); - favoritesSet = new HashSet<>(); - if (favorites != null) { - for (final PersonalAddressDO personalAddress : favorites) { - if (personalAddress.isFavoriteCard() && !personalAddress.getDeleted()) { - favoritesSet.add(personalAddress.getAddressId()); - } - } - } - } - - if (list != null) { - for (final AddressDO addressDO : list) { - if (!exportAll && !favoritesSet.contains(addressDO.getId())) { - // Export only personal favorites due to data-protection. - continue; - } - final AddressObject address = AddressDOConverter.getAddressObject(addressDao, addressImageDao, addressDO, - BooleanUtils.isTrue(disableImageData), BooleanUtils.isTrue(disableVCardData)); - result.add(address); - } - } - if (!exportAll && modifiedSinceDate != null) { - // Add now personal address entries which were modified since the given date (deleted or added): - for (final PersonalAddressDO personalAddress : favorites) { - if (personalAddress.getLastUpdate() != null - && !personalAddress.getLastUpdate().before(modifiedSinceDate)) { - final AddressDO addressDO = addressDao.find(personalAddress.getAddressId()); - final AddressObject address = AddressDOConverter.getAddressObject(addressDao, addressImageDao, addressDO, - BooleanUtils.isTrue(disableImageData), BooleanUtils.isTrue(disableVCardData)); - if (!personalAddress.isFavorite()) { - // This address was may-be removed by the user from the personal address book, so add this address as deleted to the result - // list. - address.setDeleted(true); - } - result.add(address); - } - } - } - } - - @SuppressWarnings("unchecked") final List uniqResult = (List) CollectionUtils.select(result, - PredicateUtils.uniquePredicate()); - final String json = JsonUtils.toJson(uniqResult); - log.info("Rest call finished (" + result.size() + " addresses)..."); - return Response.ok(json).build(); - } - - @PUT - @Path(RestPaths.SAVE_OR_UDATE) - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public Response saveOrUpdateAddressObject(final AddressObject addressObject) { - String uid = addressObject.getUid() != null ? addressObject.getUid().replace("urn:uuid:", "") : UUID.randomUUID().toString(); - addressObject.setUid(uid); - AddressDO addressDORequest = AddressDOConverter.getAddressDO(addressObject); - byte[] image = AddressDOConverter.getAddressImageDO(addressObject); - AddressDO addressDOOrig = null; - boolean isNew = false; - try { - addressDOOrig = addressDao.findByUid(addressObject.getUid()); - } catch (jakarta.persistence.NoResultException e) { - log.info("No address with given uid found: " + uid); - log.info("Continoue creating new address."); - } - - if (addressDOOrig != null) { - //Metadata - addressDORequest.setId(addressDOOrig.getId()); - addressDORequest.setCreated(addressDOOrig.getCreated()); - //Data, which is not in vcard - addressDORequest.setAddressStatus(addressDOOrig.getAddressStatus()); - addressDORequest.setContactStatus(addressDOOrig.getContactStatus()); - addressDORequest.setForm(addressDOOrig.getForm()); - addressDORequest.setCommunicationLanguage(addressDOOrig.getCommunicationLanguage()); - - addressDORequest.setPostalAddressText(addressDOOrig.getPostalAddressText()); - addressDORequest.setPostalCity(addressDOOrig.getPostalCity()); - addressDORequest.setPostalCountry(addressDOOrig.getPostalCountry()); - addressDORequest.setPostalZipCode(addressDOOrig.getPostalZipCode()); - addressDORequest.setPostalState(addressDOOrig.getPostalState()); - - addressDORequest.setPublicKey(addressDOOrig.getPublicKey()); - addressDORequest.setFingerprint(addressDOOrig.getFingerprint()); - //Addressbooks - addressDORequest.setAddressbookList(addressDOOrig.getAddressbookList()); - } else { - addressDORequest.setAddressStatus(AddressStatus.UPTODATE); - addressDORequest.setContactStatus(ContactStatus.ACTIVE); - isNew = true; - } - - if (addressDORequest.getAddressbookList() == null || addressDORequest.getAddressbookList().size() < 1) { - Set addressbooks = new HashSet<>(); - addressbooks.add(addressbookDao.getGlobalAddressbook()); - addressDORequest.setAddressbookList(addressbooks); - addressDORequest.setForm(FormOfAddress.UNKNOWN); - } - - addressDao.insertOrUpdate(addressDORequest); - - addressImageDao.saveOrUpdate(addressDORequest.getId(), image); - - AddressDO dbAddress = addressDao.findByUid(uid); - - if (isNew) { - PersonalAddressDO personalAddress; - personalAddress = new PersonalAddressDO(); - personalAddress.setAddress(dbAddress); - personalAddress.setFavoriteCard(true); - personalAddressDao.setOwner(personalAddress, getLoggedInUserId()); - personalAddressDao.saveOrUpdate(personalAddress); - } - - final String json = JsonUtils.toJson(AddressDOConverter.getAddressObject(addressDao, addressImageDao, dbAddress, - false, true)); - log.info("Save or update address REST call finished."); - return Response.ok(json).build(); - } - - @DELETE - @Path(RestPaths.DELETE) - @Consumes(MediaType.APPLICATION_JSON) - public Response removeFavoriteAddressObject(final AddressObject addressObject) { - String uid = addressObject.getUid() != null ? addressObject.getUid().replace("urn:uuid:", "") : UUID.randomUUID().toString(); - addressObject.setUid(uid); - AddressDO addressDOOrig = null; - try { - addressDOOrig = addressDao.findByUid(addressObject.getUid()); - } catch (jakarta.persistence.NoResultException e) { - log.info("No address with given uid found: " + uid); - log.info("Serving error response."); - } - if (addressDOOrig == null) { - return Response.serverError().build(); - } - PersonalAddressDO personalAddress = personalAddressDao.getByAddressId(addressDOOrig.getId()); - if (personalAddress != null) { - personalAddress.setFavoriteCard(false); - personalAddressDao.saveOrUpdate(personalAddress); - } - return Response.ok().build(); - } -} diff --git a/projectforge-rest/src/main/java/org/projectforge/rest/converter/AddressDOConverter.java b/projectforge-rest/src/main/java/org/projectforge/rest/converter/AddressDOConverter.java deleted file mode 100644 index 7d76055e9b..0000000000 --- a/projectforge-rest/src/main/java/org/projectforge/rest/converter/AddressDOConverter.java +++ /dev/null @@ -1,160 +0,0 @@ -///////////////////////////////////////////////////////////////////////////// -// -// 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.rest.converter; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.lang3.StringUtils; -import org.projectforge.business.address.*; -import org.projectforge.business.converter.DOConverter; -import org.projectforge.framework.time.PFDay; -import org.projectforge.model.rest.AddressObject; - -import java.io.PrintWriter; -import java.io.StringWriter; - -/** - * For conversion of TaskDO to task object. - * - * @author Kai Reinhard (k.reinhard@micromata.de) - */ -public class AddressDOConverter { - public static AddressObject getAddressObject(final AddressDao addressDao, - final AddressImageDao addressImageDao, - final AddressDO addressDO, - boolean disableImageData, - boolean disableVCard) { - if (addressDO == null) { - return null; - } - final AddressObject address = new AddressObject(); - DOConverter.copyFields(address, addressDO); - address.setUid(addressDO.getUid()); - address.setAddressStatus(addressDO.getAddressStatus() != null ? addressDO.getAddressStatus().toString() : null); - address.setAddressText(addressDO.getAddressText()); - final PFDay birthday = PFDay.fromOrNull(addressDO.getBirthday()); - if (birthday != null) { - address.setBirthday(birthday.getSqlDate()); - } - address.setBusinessPhone(addressDO.getBusinessPhone()); - address.setCity(addressDO.getCity()); - address.setComment(addressDO.getComment()); - address.setCommunicationLanguage(addressDO.getCommunicationLanguage()); - address.setContactStatus(addressDO.getContactStatus() != null ? addressDO.getContactStatus().toString() : null); - address.setCountry(addressDO.getCountry()); - address.setDivision(addressDO.getDivision()); - address.setEmail(addressDO.getEmail()); - address.setFax(addressDO.getFax()); - address.setFingerprint(addressDO.getFingerprint()); - address.setFirstName(addressDO.getFirstName()); - address.setForm(addressDO.getForm() != null ? addressDO.getForm().toString() : null); - address.setMobilePhone(addressDO.getMobilePhone()); - address.setName(addressDO.getName()); - address.setOrganization(addressDO.getOrganization()); - address.setPositionText(addressDO.getPositionText()); - address.setPostalAddressText(addressDO.getPostalAddressText()); - address.setPostalCity(addressDO.getPostalCity()); - address.setPostalCountry(addressDO.getPostalCountry()); - address.setPostalState(addressDO.getPostalState()); - address.setPostalZipCode(addressDO.getPostalZipCode()); - address.setPrivateAddressText(addressDO.getPrivateAddressText()); - address.setPrivateCity(addressDO.getPrivateCity()); - address.setPrivateCountry(addressDO.getPrivateCountry()); - address.setPrivateEmail(addressDO.getPrivateEmail()); - address.setPrivateMobilePhone(addressDO.getPrivateMobilePhone()); - address.setPrivatePhone(addressDO.getPrivatePhone()); - address.setPrivateState(addressDO.getPrivateState()); - address.setPrivateZipCode(addressDO.getPrivateZipCode()); - address.setPublicKey(addressDO.getPublicKey()); - address.setState(addressDO.getState()); - address.setTitle(addressDO.getTitle()); - address.setWebsite(addressDO.getWebsite()); - address.setZipCode(addressDO.getZipCode()); - if (!disableImageData) { - address.setImage(Base64.encodeBase64String(addressImageDao.getImage(addressDO.getId()))); - } - if (!disableVCard) { - final StringWriter writer = new StringWriter(); - addressDao.exportVCard(new PrintWriter(writer), addressDO); - address.setVCardData(Base64.encodeBase64String(writer.toString().getBytes())); - } - return address; - } - - public static AddressDO getAddressDO(final AddressObject addressObject) { - if (addressObject == null) { - return null; - } - final AddressDO address = new AddressDO(); - DOConverter.copyFields(address, addressObject); - address.setUid(addressObject.getUid()); - address.setAddressStatus(addressObject.getAddressStatus() != null ? AddressStatus.valueOf(addressObject.getAddressStatus()) : null); - address.setAddressText(addressObject.getAddressText()); - final PFDay birthday = PFDay.fromOrNull(addressObject.getBirthday()); - if (birthday != null) { - address.setBirthday(birthday.getLocalDate()); - } - address.setBusinessPhone(addressObject.getBusinessPhone()); - address.setCity(addressObject.getCity()); - address.setComment(addressObject.getComment()); - address.setCommunicationLanguage(addressObject.getCommunicationLanguage()); - address.setContactStatus(addressObject.getContactStatus() != null ? ContactStatus.valueOf(addressObject.getContactStatus()) : null); - address.setCountry(addressObject.getCountry()); - address.setDivision(addressObject.getDivision()); - address.setEmail(addressObject.getEmail()); - address.setFax(addressObject.getFax()); - address.setFingerprint(addressObject.getFingerprint()); - address.setFirstName(addressObject.getFirstName()); - address.setForm(addressObject.getForm() != null ? FormOfAddress.valueOf(addressObject.getForm()) : null); - address.setMobilePhone(addressObject.getMobilePhone()); - address.setName(addressObject.getName()); - address.setOrganization(addressObject.getOrganization()); - address.setPositionText(addressObject.getPositionText()); - address.setPostalAddressText(addressObject.getPostalAddressText()); - address.setPostalCity(addressObject.getPostalCity()); - address.setPostalCountry(addressObject.getPostalCountry()); - address.setPostalState(addressObject.getPostalState()); - address.setPostalZipCode(addressObject.getPostalZipCode()); - address.setPrivateAddressText(addressObject.getPrivateAddressText()); - address.setPrivateCity(addressObject.getPrivateCity()); - address.setPrivateCountry(addressObject.getPrivateCountry()); - address.setPrivateEmail(addressObject.getPrivateEmail()); - address.setPrivateMobilePhone(addressObject.getPrivateMobilePhone()); - address.setPrivatePhone(addressObject.getPrivatePhone()); - address.setPrivateState(addressObject.getPrivateState()); - address.setPrivateZipCode(addressObject.getPrivateZipCode()); - address.setPublicKey(addressObject.getPublicKey()); - address.setState(addressObject.getState()); - address.setTitle(addressObject.getTitle()); - address.setWebsite(addressObject.getWebsite()); - address.setZipCode(addressObject.getZipCode()); - return address; - } - - public static byte[] getAddressImageDO(final AddressObject addressObject) { - if (addressObject == null || StringUtils.isEmpty(addressObject.getImage())) { - return null; - } - return Base64.decodeBase64(addressObject.getImage()); - } -} diff --git a/projectforge-rest/src/main/java/org/projectforge/storage/StorageClient.java b/projectforge-rest/src/main/java/org/projectforge/storage/StorageClient.java deleted file mode 100644 index de33ea4c4a..0000000000 --- a/projectforge-rest/src/main/java/org/projectforge/storage/StorageClient.java +++ /dev/null @@ -1,133 +0,0 @@ -///////////////////////////////////////////////////////////////////////////// -// -// 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.storage; - -import org.projectforge.business.configuration.ConfigurationServiceAccessor; -import org.projectforge.framework.configuration.ConfigXml; -import org.projectforge.framework.configuration.ConfigurationListener; -import org.projectforge.shared.storage.StorageConstants; -import org.springframework.stereotype.Component; - -import jakarta.annotation.PostConstruct; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.WebTarget; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -/** - * @author Kai Reinhard (k.reinhard@micromata.de) - */ -@Component -public class StorageClient implements ConfigurationListener -{ - private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(StorageClient.class); - - private StorageConfig config; - - private boolean initialized; - - @PostConstruct - public void init() - { - checkInitialized(); - ConfigXml.getInstance().register(this); - } - - private void checkInitialized() - { - synchronized (this) { - if (initialized) { - return; - } - this.config = ConfigXml.getInstance().getStorageConfig(); - if (this.config == null) { - log.info("No storageConfig given in config.xml. Storage not available."); - return; - } - final Client client = ClientBuilder.newClient(); - WebTarget webResource = client.target(getUrl("/initialization"))// - .queryParam(StorageConstants.PARAM_AUTHENTICATION_TOKEN, this.config.getAuthenticationToken())// - .queryParam(StorageConstants.PARAM_BASE_DIR, ConfigurationServiceAccessor.get().getApplicationHomeDir()); - Response response = webResource.request(MediaType.TEXT_PLAIN).get(); - if (response.getStatus() != Response.Status.OK.getStatusCode()) { - throw new RuntimeException("Failed : HTTP error code : " + response.getStatus()); - } - String output = null; - if (response.getEntity() instanceof String) { - output = (String) response.getEntity(); - } - if (!"OK".equals(output)) { - throw new RuntimeException("Initialization of ProjectForge's storage failed: " + output); - } - webResource = client.target(getUrl("/securityCheck")); - response = webResource.request(MediaType.TEXT_PLAIN).get(); - if (response.getStatus() == Response.Status.OK.getStatusCode()) { - final String message = "Security alert: storage is available without any authentication!!!!!!!!!!!!!!!!"; - log.error(message); - throw new RuntimeException(message); - } - webResource = client.target(getUrl("/securityCheck")); - addAuthenticationHeader(webResource); - response = webResource.request(MediaType.TEXT_PLAIN).get(); - if (response.getStatus() != Response.Status.OK.getStatusCode()) { - throw new RuntimeException("Failed : HTTP error code : " + response.getStatus()); - } - if (response.getEntity() instanceof String) { - output = (String) response.getEntity(); - } - if (!output.equals("authenticated")) { - final String message = "Authentication didn't work. Storage isn't available."; - log.error(message); - throw new RuntimeException(message); - } - initialized = true; - log.info("Initialization of ProjectForge's storage successfully done."); - } - } - - private String getUrl(final String service) - { - String url = this.config.getUrl(); - if (url == null) { - url = System.getProperty(StorageConstants.SYSTEM_PROPERTY_URL); - } - return url + "/" + service; - } - - private void addAuthenticationHeader(final WebTarget webResource) - { - webResource.request().header(StorageConstants.PARAM_AUTHENTICATION_TOKEN, config.getAuthenticationToken()); - } - - /** - * @see org.projectforge.framework.configuration.ConfigurationListener#afterRead() - */ - @Override - public void afterRead() - { - initialized = false; - checkInitialized(); - } -} diff --git a/projectforge-rest/src/main/kotlin/org/projectforge/rest/AddressImageServicesRest.kt b/projectforge-rest/src/main/kotlin/org/projectforge/rest/AddressImageServicesRest.kt index b095447ede..a395a8132a 100644 --- a/projectforge-rest/src/main/kotlin/org/projectforge/rest/AddressImageServicesRest.kt +++ b/projectforge-rest/src/main/kotlin/org/projectforge/rest/AddressImageServicesRest.kt @@ -23,8 +23,11 @@ package org.projectforge.rest +import jakarta.annotation.PostConstruct +import jakarta.servlet.http.HttpServletRequest import mu.KotlinLogging import org.projectforge.business.address.AddressImageDao +import org.projectforge.business.address.ImageType import org.projectforge.common.DataSizeConfig import org.projectforge.jcr.FileInfo import org.projectforge.jcr.FileSizeStandardChecker @@ -41,8 +44,6 @@ import org.springframework.util.unit.DataSize import org.springframework.util.unit.DataUnit import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile -import jakarta.annotation.PostConstruct -import jakarta.servlet.http.HttpServletRequest private val log = KotlinLogging.logger {} @@ -53,85 +54,91 @@ private val log = KotlinLogging.logger {} @RestController @RequestMapping("${Rest.URL}/address") class AddressImageServicesRest { + internal class SessionImage(val filename: String, val imageType: ImageType, val bytes: ByteArray) - @Value("\${${MAX_IMAGE_SIZE_SPRING_PROPERTY}:500KB}") - internal lateinit var maxImageSizeConfig: String - - lateinit var maxImageSize: DataSize - internal set + @Value("\${${MAX_IMAGE_SIZE_SPRING_PROPERTY}:500KB}") + internal lateinit var maxImageSizeConfig: String - private lateinit var fileSizeStandardChecker: FileSizeStandardChecker + lateinit var maxImageSize: DataSize + internal set - @PostConstruct - private fun postConstruct() { - maxImageSize = DataSizeConfig.init(maxImageSizeConfig, DataUnit.MEGABYTES) - log.info { "Maximum configured size of images: ${MAX_IMAGE_SIZE_SPRING_PROPERTY}=$maxImageSizeConfig." } - fileSizeStandardChecker = FileSizeStandardChecker(maxImageSize.toBytes(), MAX_IMAGE_SIZE_SPRING_PROPERTY) - } + private lateinit var fileSizeStandardChecker: FileSizeStandardChecker - @Autowired - private lateinit var addressImageDao: AddressImageDao + @PostConstruct + private fun postConstruct() { + maxImageSize = DataSizeConfig.init(maxImageSizeConfig, DataUnit.MEGABYTES) + log.info { "Maximum configured size of images: ${MAX_IMAGE_SIZE_SPRING_PROPERTY}=$maxImageSizeConfig." } + fileSizeStandardChecker = FileSizeStandardChecker(maxImageSize.toBytes(), MAX_IMAGE_SIZE_SPRING_PROPERTY) + } + @Autowired + private lateinit var addressImageDao: AddressImageDao + + + /** + * If given and greater 0, the image will be added to the address with the given id (pk), otherwise the image is + * stored in the user's session and will be used for the next update or save event. + */ + @PostMapping("uploadImage/{id}") + fun uploadFile( + @PathVariable("id") id: Long?, + @RequestParam("file") file: MultipartFile, + request: HttpServletRequest + ): + ResponseEntity<*> { + val filename = file.originalFilename + val imageType = addressImageDao.getSupportedImageType(filename) ?: run { + return ResponseEntity("Unsupported file: $filename. Only png files supported", HttpStatus.BAD_REQUEST) + } + fileSizeStandardChecker.checkSize(FileInfo(filename, fileSize = file.size), displayUserMessage = false) + val bytes = file.bytes + if (id == null || id < 0) { + val session = request.getSession(false) + val image = SessionImage(filename, imageType, bytes) + ExpiringSessionAttributes.setAttribute(session, SESSION_IMAGE_ATTR, image, 1) + } else { + addressImageDao.saveOrUpdate(id, bytes, imageType) + } + return ResponseEntity("OK", HttpStatus.OK) + } - /** - * If given and greater 0, the image will be added to the address with the given id (pk), otherwise the image is - * stored in the user's session and will be used for the next update or save event. - */ - @PostMapping("uploadImage/{id}") - fun uploadFile(@PathVariable("id") id: Long?, @RequestParam("file") file: MultipartFile, request: HttpServletRequest): - ResponseEntity<*> { - val filename = file.originalFilename - if (filename == null || !filename.endsWith(".png", true)) { - return ResponseEntity("Unsupported file: $filename. Only png files supported", HttpStatus.BAD_REQUEST) + /** + * @param id The id of the address the image is assigned to. + */ + @GetMapping("image/{id}") + fun getImage(@PathVariable("id") id: Long): ResponseEntity { + val image = addressImageDao.getImage(id) ?: return ResponseEntity(HttpStatus.NOT_FOUND) + val resource = ByteArrayResource(image) + return RestUtils.downloadFile("ProjectForge-addressImage_$id.png", resource) } - fileSizeStandardChecker.checkSize(FileInfo(filename, fileSize = file.size), displayUserMessage = false) - val bytes = file.bytes - if (id == null || id < 0) { - val session = request.getSession(false) - ExpiringSessionAttributes.setAttribute(session, SESSION_IMAGE_ATTR, bytes, 1) - } else { - addressImageDao.saveOrUpdate(id, bytes) + + /** + * @param id The id of the address the image is assigned to. + */ + @GetMapping("imagePreview/{id}") + fun getImagePreview(@PathVariable("id") id: Long): ResponseEntity { + val image = addressImageDao.getPreviewImage(id) ?: return ResponseEntity(HttpStatus.NOT_FOUND) + val resource = ByteArrayResource(image) + return RestUtils.downloadFile("ProjectForge-addressImagePreview_$id.png", resource) } - return ResponseEntity("OK", HttpStatus.OK) - } - - /** - * @param id The id of the address the image is assigned to. - */ - @GetMapping("image/{id}") - fun getImage(@PathVariable("id") id: Long): ResponseEntity { - val image = addressImageDao.getImage(id) ?: return ResponseEntity(HttpStatus.NOT_FOUND) - val resource = ByteArrayResource(image) - return RestUtils.downloadFile("ProjectForge-addressImage_$id.png", resource) - } - - /** - * @param id The id of the address the image is assigned to. - */ - @GetMapping("imagePreview/{id}") - fun getImagePreview(@PathVariable("id") id: Long): ResponseEntity { - val image = addressImageDao.getPreviewImage(id) ?: return ResponseEntity(HttpStatus.NOT_FOUND) - val resource = ByteArrayResource(image) - return RestUtils.downloadFile("ProjectForge-addressImagePreview_$id.png", resource) - } - - /** - * If given and greater 0, the image will be deleted from the address with the given id (pk), otherwise the image is - * removed from the user's session and will not be used for the next update or save event anymore. - * @param id The id of the address the image is assigned to. - */ - @DeleteMapping("deleteImage/{id}") - fun deleteImage(request: HttpServletRequest, @PathVariable("id") id: Long?): ResponseEntity { - val session = request.getSession(false) - ExpiringSessionAttributes.removeAttribute(session, SESSION_IMAGE_ATTR) - if (id != null && id > 0) { - addressImageDao.delete(id) + + /** + * If given and greater 0, the image will be deleted from the address with the given id (pk), otherwise the image is + * removed from the user's session and will not be used for the next update or save event anymore. + * @param id The id of the address the image is assigned to. + */ + @DeleteMapping("deleteImage/{id}") + fun deleteImage(request: HttpServletRequest, @PathVariable("id") id: Long?): ResponseEntity { + val session = request.getSession(false) + ExpiringSessionAttributes.removeAttribute(session, SESSION_IMAGE_ATTR) + if (id != null && id > 0) { + addressImageDao.delete(id) + } + return ResponseEntity("OK", HttpStatus.OK) } - return ResponseEntity("OK", HttpStatus.OK) - } - companion object { - internal const val SESSION_IMAGE_ATTR = "uploadedAddressImage" - private const val MAX_IMAGE_SIZE_SPRING_PROPERTY = "projectforge.address.maxImageSize" - } + companion object { + internal const val SESSION_IMAGE_ATTR = "uploadedAddressImage" + private const val MAX_IMAGE_SIZE_SPRING_PROPERTY = "projectforge.address.maxImageSize" + } } diff --git a/projectforge-rest/src/main/kotlin/org/projectforge/rest/AddressPagesRest.kt b/projectforge-rest/src/main/kotlin/org/projectforge/rest/AddressPagesRest.kt index 3dbfc4b80d..5cf318b1f3 100644 --- a/projectforge-rest/src/main/kotlin/org/projectforge/rest/AddressPagesRest.kt +++ b/projectforge-rest/src/main/kotlin/org/projectforge/rest/AddressPagesRest.kt @@ -88,6 +88,9 @@ class AddressPagesRest @Autowired private lateinit var addressbookDao: AddressbookDao + @Autowired + private lateinit var addressImageCache: AddressImageCache + @Autowired private lateinit var addressExport: AddressExport @@ -228,10 +231,10 @@ class AddressPagesRest personalAddressDao.saveOrUpdate(personalAddress) val session = request.getSession(false) - val bytes = ExpiringSessionAttributes.getAttribute(session, SESSION_IMAGE_ATTR) - if (bytes != null && bytes is ByteArray) { - // The user uploaded an image, so - addressImageDao.saveOrUpdate(obj.id!!, bytes) + val image = ExpiringSessionAttributes.getAttribute(session, SESSION_IMAGE_ATTR) + if (image != null && image is AddressImageServicesRest.SessionImage) { + // The user uploaded an image, so save it now. + addressImageDao.saveOrUpdate(obj.id!!, image.bytes, image.imageType) ExpiringSessionAttributes.removeAttribute(session, SESSION_IMAGE_ATTR) } } @@ -592,12 +595,13 @@ class AddressPagesRest magicFilter: MagicFilter, ): ResultSet<*> { val newList = resultSet.resultSet.map { + val image = addressImageCache.getImage(it.id) ListAddress( transformFromDB(it), id = it.id!!, deleted = it.deleted, - imageUrl = if (it.image == true) "address/image/${it.id}" else null, - previewImageUrl = if (it.image == true) "address/imagePreview/${it.id}" else null + imageUrl = if (image != null) "address/image/${it.id}" else null, + previewImageUrl = if (image != null) "address/imagePreview/${it.id}" else null ) } newList.forEach { diff --git a/projectforge-rest/src/main/kotlin/org/projectforge/rest/AddressViewPageRest.kt b/projectforge-rest/src/main/kotlin/org/projectforge/rest/AddressViewPageRest.kt index 6bea21b19e..114b771c91 100644 --- a/projectforge-rest/src/main/kotlin/org/projectforge/rest/AddressViewPageRest.kt +++ b/projectforge-rest/src/main/kotlin/org/projectforge/rest/AddressViewPageRest.kt @@ -24,10 +24,6 @@ package org.projectforge.rest import mu.KotlinLogging -import org.projectforge.business.address.AddressDO -import org.projectforge.business.address.AddressDao -import org.projectforge.business.address.PersonalAddressDO -import org.projectforge.business.address.PersonalAddressDao import org.projectforge.business.sipgate.SipgateConfiguration import org.projectforge.framework.persistence.user.api.ThreadLocalUserContext import org.projectforge.framework.utils.NumberHelper @@ -47,6 +43,7 @@ import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid +import org.projectforge.business.address.* private val log = KotlinLogging.logger {} @@ -56,6 +53,9 @@ class AddressViewPageRest : AbstractDynamicPageRest() { @Autowired private lateinit var addressDao: AddressDao + @Autowired + private lateinit var addressImageCache: AddressImageCache + @Autowired private lateinit var personalAddressDao: PersonalAddressDao @@ -98,7 +98,8 @@ class AddressViewPageRest : AbstractDynamicPageRest() { } val fieldSet = UIFieldset(12, title = "'${addressDO.fullNameWithTitleAndForm}$organization") layout.add(fieldSet) - if (addressDO.image == true) { + val image = addressImageCache.getImage(id) + if (image != null) { fieldSet.add( UICustomized( "image", diff --git a/projectforge-rest/src/main/kotlin/org/projectforge/rest/dto/Address.kt b/projectforge-rest/src/main/kotlin/org/projectforge/rest/dto/Address.kt index d08bc9654f..34c7ecb81f 100644 --- a/projectforge-rest/src/main/kotlin/org/projectforge/rest/dto/Address.kt +++ b/projectforge-rest/src/main/kotlin/org/projectforge/rest/dto/Address.kt @@ -87,7 +87,7 @@ class Address( override fun copyFrom(src: AddressDO) { super.copyFrom(src) - if (src.image == true) { + if (AddressImageCache.instance.getImage(src.id) != null) { imageData = byteArrayOf(1) // Marker for frontend for an available image. } // For new addresses no cache entry exist.