From c24783c4ebdb9abe95b9b0e6be026bfda908da47 Mon Sep 17 00:00:00 2001 From: Divine Threepwood Date: Tue, 13 Feb 2024 22:20:01 +0100 Subject: [PATCH] port to latest bco version, implement feature message registry. --- pom.xml | 86 ++++- .../bco/registry/editor/RegistryEditor.java | 1 + .../struct/RegistryMessageTreeItem.java | 327 ------------------ .../editor/struct/RegistryMessageTreeItem.kt | 313 +++++++++++++++++ .../struct/preset/UserMessageTreeItem.kt | 37 ++ 5 files changed, 427 insertions(+), 337 deletions(-) delete mode 100644 src/main/java/org/openbase/bco/registry/editor/struct/RegistryMessageTreeItem.java create mode 100644 src/main/java/org/openbase/bco/registry/editor/struct/RegistryMessageTreeItem.kt create mode 100644 src/main/java/org/openbase/bco/registry/editor/struct/preset/UserMessageTreeItem.kt diff --git a/pom.xml b/pom.xml index c36f818..233ebe8 100644 --- a/pom.xml +++ b/pom.xml @@ -65,15 +65,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - ${java.source.version} - ${java.target.version} - - org.codehaus.mojo appassembler-maven-plugin @@ -197,6 +188,69 @@ + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + src/main/java + target/generated-sources/annotations + + + + + test-compile + test-compile + + test-compile + + + + + 1.8 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + default-compile + none + + + default-testCompile + none + + + compile + compile + + compile + + + + testCompile + test-compile + + testCompile + + + + + ${java.source.version} + ${java.target.version} + + @@ -277,9 +331,10 @@ UTF-8 17 17 - 3.3-SNAPSHOT + 3.6-SNAPSHOT 3.1-SNAPSHOT [5.10,5.11-alpha) + 1.9.22 @@ -335,6 +390,17 @@ javafx-swing 21.0.1 + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-test + ${kotlin.version} + test + diff --git a/src/main/java/org/openbase/bco/registry/editor/RegistryEditor.java b/src/main/java/org/openbase/bco/registry/editor/RegistryEditor.java index 1a34a6f..8af17bc 100644 --- a/src/main/java/org/openbase/bco/registry/editor/RegistryEditor.java +++ b/src/main/java/org/openbase/bco/registry/editor/RegistryEditor.java @@ -94,6 +94,7 @@ public void start(Stage primaryStage) { globalTabPane.getTabs().add(new RegistryRemoteTab<>(Registries.getClassRegistry(), getClassRegistryGrouping())); globalTabPane.getTabs().add(new RegistryRemoteTab<>(Registries.getTemplateRegistry())); globalTabPane.getTabs().add(new RegistryRemoteTab<>(Registries.getActivityRegistry())); + globalTabPane.getTabs().add(new RegistryRemoteTab<>(Registries.getMessageRegistry())); final StackPane stackPane = new StackPane(); // make login panel appear in the top right with small margins at the top and right diff --git a/src/main/java/org/openbase/bco/registry/editor/struct/RegistryMessageTreeItem.java b/src/main/java/org/openbase/bco/registry/editor/struct/RegistryMessageTreeItem.java deleted file mode 100644 index 1db1b75..0000000 --- a/src/main/java/org/openbase/bco/registry/editor/struct/RegistryMessageTreeItem.java +++ /dev/null @@ -1,327 +0,0 @@ -package org.openbase.bco.registry.editor.struct; - -/*- - * #%L - * BCO Registry Editor - * %% - * Copyright (C) 2014 - 2024 openbase.org - * %% - * This program 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, either version 3 of the - * License, or (at your option) any later version. - * - * This program 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 - * . - * #L% - */ - -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.Message; -import javafx.application.Platform; -import javafx.scene.Node; -import javafx.scene.control.*; -import javafx.scene.control.Alert.AlertType; -import javafx.scene.layout.HBox; -import org.openbase.bco.registry.editor.visual.RequiredFieldAlert; -import org.openbase.bco.registry.remote.Registries; -import org.openbase.jul.exception.CouldNotPerformException; -import org.openbase.jul.exception.ExceptionProcessor; -import org.openbase.jul.exception.InitializationException; -import org.openbase.jul.exception.NotAvailableException; -import org.openbase.jul.exception.printer.ExceptionPrinter; -import org.openbase.jul.exception.printer.LogLevel; -import org.openbase.jul.extension.protobuf.processing.ProtoBufFieldProcessor; -import org.openbase.jul.extension.type.processing.LabelProcessor; -import org.openbase.jul.iface.Identifiable; -import org.openbase.jul.schedule.GlobalCachedExecutorService; - -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; - -/** - * @author Tamino Huxohl - */ -public class RegistryMessageTreeItem extends BuilderTreeItem { - - private final FieldDescriptor idField, labelField; - - private boolean inUpdate; - private boolean changed; - private Future registryTask; - - public RegistryMessageTreeItem(FieldDescriptor fieldDescriptor, MB builder, Boolean editable) throws InitializationException { - super(fieldDescriptor, builder, editable); - try { - idField = ProtoBufFieldProcessor.getFieldDescriptor(builder, Identifiable.TYPE_FIELD_ID); - labelField = ProtoBufFieldProcessor.getFieldDescriptor(builder, org.openbase.type.language.LabelType.Label.class.getSimpleName().toLowerCase()); - - changed = false; - inUpdate = false; - - this.addEventHandler(valueChangedEvent(), event -> { - // this is triggered when the value of this node or one of its children changes - updateDescriptionGraphic(); - - if (!inUpdate && !(event.getSource().equals(RegistryMessageTreeItem.this))) { - logger.trace("Set changed"); - changed = true; - } - - updateValueGraphic(); - }); - } catch (CouldNotPerformException ex) { - throw new InitializationException(this, ex); - } - } - - public String getId() { - return (String) getBuilder().getField(idField); - } - - /** - * Match a builder by comparing their ids. - * - * @param builder {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - protected boolean matchesBuilder(final MB builder) { - final Object id1 = getBuilder().getField(idField); - final Object id2 = builder.getField(idField); - return id1.equals(id2); - } - - @Override - protected Set getUneditableFields() { - Set uneditableFieldSet = new HashSet<>(); - uneditableFieldSet.add(idField.getNumber()); - return uneditableFieldSet; - } - - @Override - protected String createDescriptionText() { - try { - return LabelProcessor.getBestMatch((org.openbase.type.language.LabelType.Label) getBuilder().getField(labelField)); - } catch (NotAvailableException e) { - return super.createDescriptionText(); - } - } - - @Override - protected Node createValueGraphic() { - // task is not done yet so display loading graphic - if (registryTask != null && !registryTask.isDone()) { - final Label label = new Label("Waiting for registry update..."); - final ProgressIndicator progressIndicator = new ProgressIndicator(); - progressIndicator.setMaxHeight(16); - - final HBox hBox = new HBox(); - hBox.setSpacing(5); - hBox.getChildren().addAll(progressIndicator, label); - return hBox; - } - - final Label errorLabel = new Label(); - // task is done but not reset meaning it failed, so generate an error label - if (registryTask != null) { - errorLabel.setStyle("-fx-text-background-color: rgb(255,0,0); -fx-font-weight: bold;"); - // done but failed - try { - registryTask.get(); - } catch (InterruptedException ex) { - // this should not because the task should already be done - } catch (ExecutionException ex) { - errorLabel.setText(ExceptionProcessor.getInitialCause(ex).getMessage()); - } - } - - if (changed) { - logger.trace("Create buttons"); - final Button applyButton, cancelButton; - final HBox buttonLayout; - applyButton = new Button("Apply"); - applyButton.setOnAction(event -> handleApplyEvent()); - cancelButton = new Button("Cancel"); - cancelButton.setOnAction(event -> handleCancelEvent()); - buttonLayout = new HBox(applyButton, cancelButton); - if (registryTask != null) { - // if task failed add error to button layout - buttonLayout.getChildren().add(errorLabel); - } - return buttonLayout; - } - - if (registryTask != null) { - return errorLabel; - } - - return super.createValueGraphic(); - } - - @Override - public void update(MB value) throws CouldNotPerformException { - //TODO handle local changes but global update - - inUpdate = true; - try { - changed = false; - if (registryTask != null) { - registryTask = null; - } - - super.update(value); - } finally { - inUpdate = false; - } - } - - private void handleCancelEvent() { - final String id = (String) getBuilder().getField(idField); - if (id.isEmpty()) { - getParent().getChildren().remove(RegistryMessageTreeItem.this); - } else { - try { - final MB oldBuilder = (MB) Registries.getById(id, getBuilder()).toBuilder(); - try { - update(oldBuilder); - } catch (CouldNotPerformException ex) { - logger.error("Could not update tree item with old builder from registry", ex); - } - } catch (CouldNotPerformException ex) { - ExceptionPrinter.printHistory("Could not retrieve message with id[" + id + "] for type[" + getBuilder().getClass().getName() + "] from registry", ex, logger, LogLevel.WARN); - } - } - } - - private void handleApplyEvent() { - logger.info("Apply button pressed"); - handleRequiredFields(); - - try { - Message message; - try { - message = getBuilder().build(); - } catch (Throwable ex) { - logger.info("Build failed", ex); - throw ex; - } - - if (Registries.contains(message)) { - // save original value from model - final Message original = Registries.getById(ProtoBufFieldProcessor.getId(message)); - changed = false; - registryTask = Registries.update(message); - GlobalCachedExecutorService.submit(() -> { - final Message update; - try { - update = registryTask.get(); - - // check if update and original are the same, then the changed values where reset and a registry update is not triggered - if (original.equals(update)) { - // reset the container to its old value - Platform.runLater(() -> { - try { - RegistryMessageTreeItem.this.update((MB) original.toBuilder()); - } catch (CouldNotPerformException e) { - logger.error("Could not reset tree item", e); - } - }); - } - } catch (InterruptedException e) { - // just let the thread finish - } catch (ExecutionException e) { - changed = true; - // update the current value which will trigger an update of the displayed graphics, - // this will display the exception to the user - setValue(getValueCasted().createNew(getBuilder())); - } - }); - - } else { - registryTask = Registries.register(message); - GlobalCachedExecutorService.submit(() -> { - try { - registryTask.get(); - Platform.runLater(() -> getParent().getChildren().remove(this)); - } catch (InterruptedException e) { - // just let the thread finish - } catch (ExecutionException e) { - // update the current value which will trigger an update of the displayed graphics, - // this will display the exception to the user - setValue(getValueCasted().createNew(getBuilder())); - } - }); - } - setValue(getValueCasted().createNew(getBuilder())); - } catch (CouldNotPerformException ex) { - logger.error("Error while applying event", ex); - } - } - - private void handleRequiredFields() { - if (!getBuilder().isInitialized()) { - if (ProtoBufFieldProcessor.checkIfSomeButNotAllRequiredFieldsAreSet(getBuilder())) { - new RequiredFieldAlert(getBuilder()); - } else { - ProtoBufFieldProcessor.clearRequiredFields(getBuilder()); - } - } - } - - void handleRemoveEvent() { - final String id = (String) getBuilder().getField(idField); - try { - logger.debug("Removing message with Id [" + id + "]"); - if (!"".equals(id) && Registries.containsById(id, getBuilder())) { - - final Alert alert = new Alert(AlertType.CONFIRMATION); - alert.setTitle("Remove Action"); - alert.setHeaderText("Attention! Irreversible Action!"); - alert.setContentText("Do you really want to remove " + getDescriptionText() + "?"); - - final Optional result = alert.showAndWait(); - if (result.isPresent() && result.get() != ButtonType.OK) { - return; - } - - // always remove by id to ignore not initialized fields of the builder - registryTask = Registries.remove(Registries.getById(id, getBuilder())); - setValue(getValueCasted().createNew(getBuilder())); - GlobalCachedExecutorService.submit(() -> { - try { - registryTask.get(); - } catch (InterruptedException e) { - // just let the thread finish - } catch (ExecutionException e) { - // update the current value which will trigger an update of the displayed graphics, - // this will display the exception to the user - setValue(getValueCasted().createNew(getBuilder())); - } - }); - } else { - Platform.runLater(() -> RegistryMessageTreeItem.this.getParent().getChildren().remove(RegistryMessageTreeItem.this)); - } - } catch (CouldNotPerformException ex) { - //TODO: handle better - ExceptionPrinter.printHistory(ex, logger); - } - } - - public boolean isChanged() { - return changed; - } - - public void setChanged(final Boolean changed) { - this.changed = changed; - } -} diff --git a/src/main/java/org/openbase/bco/registry/editor/struct/RegistryMessageTreeItem.kt b/src/main/java/org/openbase/bco/registry/editor/struct/RegistryMessageTreeItem.kt new file mode 100644 index 0000000..98545f1 --- /dev/null +++ b/src/main/java/org/openbase/bco/registry/editor/struct/RegistryMessageTreeItem.kt @@ -0,0 +1,313 @@ +package org.openbase.bco.registry.editor.struct + +import com.google.protobuf.Descriptors +import com.google.protobuf.Message +import javafx.application.Platform +import javafx.event.ActionEvent +import javafx.event.EventHandler +import javafx.scene.Node +import javafx.scene.control.* +import javafx.scene.control.Alert.AlertType +import javafx.scene.layout.HBox +import org.openbase.bco.registry.editor.visual.RequiredFieldAlert +import org.openbase.bco.registry.remote.Registries +import org.openbase.jul.exception.CouldNotPerformException +import org.openbase.jul.exception.ExceptionProcessor.getInitialCause +import org.openbase.jul.exception.InitializationException +import org.openbase.jul.exception.NotAvailableException +import org.openbase.jul.exception.printer.ExceptionPrinter +import org.openbase.jul.exception.printer.LogLevel +import org.openbase.jul.extension.protobuf.processing.ProtoBufFieldProcessor +import org.openbase.jul.extension.type.processing.LabelProcessor +import org.openbase.jul.iface.Identifiable +import org.openbase.jul.schedule.GlobalCachedExecutorService +import org.openbase.type.language.LabelType +import java.util.* +import java.util.concurrent.ExecutionException +import java.util.concurrent.Future +import kotlin.collections.HashSet + +/** + * @author [Tamino Huxohl](mailto:pleminoq@openbase.org) + */ +open class RegistryMessageTreeItem( + fieldDescriptor: Descriptors.FieldDescriptor, + builder: MB, + editable: Boolean?, + labelProvider: (() -> String)? = null +) : BuilderTreeItem(fieldDescriptor, builder, editable) { + private var idField: Descriptors.FieldDescriptor? = null //, labelField; + private val labelProvider: () -> String = labelProvider ?: { + try { + ProtoBufFieldProcessor.getFieldDescriptor( + builder, + LabelType.Label::class.java.getSimpleName().lowercase() + ).let { defaultLabelField -> + LabelProcessor.getBestMatch( + getBuilder().getField(defaultLabelField) as LabelType.Label, + super.createDescriptionText() + ) + ?: super.createDescriptionText() + } + } catch (e: NotAvailableException) { + super.createDescriptionText() + } + } + + private var inUpdate = false + var isChanged: Boolean = false + private var registryTask: Future? = null + + constructor( + fieldDescriptor: Descriptors.FieldDescriptor, + builder: MB, + editable: Boolean? + ) : this(fieldDescriptor, builder, editable, null) + + init { + try { + idField = ProtoBufFieldProcessor.getFieldDescriptor(builder, Identifiable.TYPE_FIELD_ID) + isChanged = false + inUpdate = false + + this.addEventHandler( + valueChangedEvent(), + EventHandler> { event: TreeModificationEvent -> + // this is triggered when the value of this node or one of its children changes + updateDescriptionGraphic() + + if (!inUpdate && event.source != this@RegistryMessageTreeItem) { + logger.trace("Set changed") + isChanged = true + } + updateValueGraphic() + }) + } catch (ex: CouldNotPerformException) { + throw InitializationException(this, ex) + } + } + + val id: String + get() = builder!!.getField(idField) as String + + /** + * Match a builder by comparing their ids. + * + * @param builder {@inheritDoc} + * @return {@inheritDoc} + */ + override fun matchesBuilder(builder: MB): Boolean { + val id1 = getBuilder()!!.getField(idField) + val id2 = builder!!.getField(idField) + return id1 == id2 + } + + override fun getUneditableFields(): Set { + val uneditableFieldSet: MutableSet = HashSet() + uneditableFieldSet.add(idField!!.number) + return uneditableFieldSet + } + + override fun createDescriptionText(): String = labelProvider() + + override fun createValueGraphic(): Node? { + // task is not done yet so display loading graphic + if (registryTask != null && !registryTask!!.isDone) { + val label = Label("Waiting for registry update...") + val progressIndicator = ProgressIndicator() + progressIndicator.maxHeight = 16.0 + + val hBox = HBox() + hBox.spacing = 5.0 + hBox.children.addAll(progressIndicator, label) + return hBox + } + + val errorLabel = Label() + // task is done but not reset meaning it failed, so generate an error label + if (registryTask != null) { + errorLabel.style = "-fx-text-background-color: rgb(255,0,0); -fx-font-weight: bold;" + // done but failed + try { + registryTask!!.get() + } catch (ex: InterruptedException) { + // this should not because the task should already be done + } catch (ex: ExecutionException) { + errorLabel.text = getInitialCause(ex).message + } + } + + if (isChanged) { + logger.trace("Create buttons") + val buttonLayout: HBox + val applyButton = Button("Apply") + applyButton.onAction = EventHandler { event: ActionEvent? -> handleApplyEvent() } + val cancelButton = Button("Cancel") + cancelButton.onAction = EventHandler { event: ActionEvent? -> handleCancelEvent() } + buttonLayout = HBox(applyButton, cancelButton) + if (registryTask != null) { + // if task failed add error to button layout + buttonLayout.children.add(errorLabel) + } + return buttonLayout + } + + if (registryTask != null) { + return errorLabel + } + + return super.createValueGraphic() + } + + @Throws(CouldNotPerformException::class) + override fun update(value: MB) { + //TODO handle local changes but global update + + inUpdate = true + try { + isChanged = false + if (registryTask != null) { + registryTask = null + } + + super.update(value) + } finally { + inUpdate = false + } + } + + private fun handleCancelEvent() { + val id = builder!!.getField(idField) as String + if (id.isEmpty()) { + parent.children.remove(this@RegistryMessageTreeItem) + } else { + try { + val oldBuilder = Registries.getById(id, builder).toBuilder() as MB + try { + update(oldBuilder) + } catch (ex: CouldNotPerformException) { + logger.error("Could not update tree item with old builder from registry", ex) + } + } catch (ex: CouldNotPerformException) { + ExceptionPrinter.printHistory( + "Could not retrieve message with id[" + id + "] for type[" + builder.javaClass.name + "] from registry", + ex, + logger, + LogLevel.WARN + ) + } + } + } + + private fun handleApplyEvent() { + logger.info("Apply button pressed") + handleRequiredFields() + + try { + val message: Message + try { + message = builder!!.build() + } catch (ex: Throwable) { + logger.info("Build failed", ex) + throw ex + } + + if (Registries.contains(message)) { + // save original value from model + val original = Registries.getById(ProtoBufFieldProcessor.getId(message)) + isChanged = false + registryTask = Registries.update(message) + GlobalCachedExecutorService.submit { + val update: Message? + try { + update = registryTask?.get() + + // check if update and original are the same, then the changed values where reset and a registry update is not triggered + if (original == update) { + // reset the container to its old value + Platform.runLater { + try { + this@RegistryMessageTreeItem.update(original.toBuilder() as MB) + } catch (e: CouldNotPerformException) { + logger.error("Could not reset tree item", e) + } + } + } + } catch (e: InterruptedException) { + // just let the thread finish + } catch (e: ExecutionException) { + isChanged = true + // update the current value which will trigger an update of the displayed graphics, + // this will display the exception to the user + setValue(valueCasted.createNew(builder)) + } + } + } else { + registryTask = Registries.register(message) + GlobalCachedExecutorService.submit { + try { + registryTask?.get() + Platform.runLater { parent.children.remove(this) } + } catch (e: InterruptedException) { + // just let the thread finish + } catch (e: ExecutionException) { + // update the current value which will trigger an update of the displayed graphics, + // this will display the exception to the user + setValue(valueCasted.createNew(builder)) + } + } + } + setValue(valueCasted.createNew(builder)) + } catch (ex: CouldNotPerformException) { + logger.error("Error while applying event", ex) + } + } + + private fun handleRequiredFields() { + if (!builder!!.isInitialized) { + if (ProtoBufFieldProcessor.checkIfSomeButNotAllRequiredFieldsAreSet(builder)) { + RequiredFieldAlert(builder) + } else { + ProtoBufFieldProcessor.clearRequiredFields(builder) + } + } + } + + fun handleRemoveEvent() { + val id = builder!!.getField(idField) as String + try { + logger.debug("Removing message with Id [$id]") + if ("" != id && Registries.containsById(id, builder)) { + val alert = Alert(AlertType.CONFIRMATION) + alert.title = "Remove Action" + alert.headerText = "Attention! Irreversible Action!" + alert.contentText = "Do you really want to remove $descriptionText?" + + val result = alert.showAndWait() + if (result.isPresent && result.get() != ButtonType.OK) { + return + } + + // always remove by id to ignore not initialized fields of the builder + registryTask = Registries.remove(Registries.getById(id, builder)) + value = valueCasted.createNew(builder) + GlobalCachedExecutorService.submit { + try { + registryTask?.get() + } catch (e: InterruptedException) { + // just let the thread finish + } catch (e: ExecutionException) { + // update the current value which will trigger an update of the displayed graphics, + // this will display the exception to the user + value = valueCasted.createNew(builder) + } + } + } else { + Platform.runLater { parent.children.remove(this@RegistryMessageTreeItem) } + } + } catch (ex: CouldNotPerformException) { + //TODO: handle better + ExceptionPrinter.printHistory(ex, logger) + } + } +} diff --git a/src/main/java/org/openbase/bco/registry/editor/struct/preset/UserMessageTreeItem.kt b/src/main/java/org/openbase/bco/registry/editor/struct/preset/UserMessageTreeItem.kt new file mode 100644 index 0000000..cedc962 --- /dev/null +++ b/src/main/java/org/openbase/bco/registry/editor/struct/preset/UserMessageTreeItem.kt @@ -0,0 +1,37 @@ +package org.openbase.bco.registry.editor.struct.preset + +import com.google.protobuf.Descriptors +import org.openbase.bco.registry.editor.struct.RegistryMessageTreeItem +import org.openbase.bco.registry.remote.Registries +import org.openbase.jul.exception.NotAvailableException +import org.openbase.jul.extension.protobuf.processing.ProtoBufFieldProcessor +import org.openbase.jul.extension.type.processing.LabelProcessor +import org.openbase.type.domotic.communication.UserMessageType.UserMessage +import org.openbase.type.language.LabelType + +/** + * @author [Tamino Huxohl](mailto:pleminoq@openbase.org) + */ +class UserMessageTreeItem( + fieldDescriptor: Descriptors.FieldDescriptor?, + builder: UserMessage.Builder, + editable: Boolean?, +) : RegistryMessageTreeItem( + fieldDescriptor!!, builder, editable, { + "${builder.messageType.takeIf { it != UserMessage.MessageType.UNKNOWN } ?: "Message"}${ + try { + Registries.getUnitRegistry().getUnitConfigById(builder.senderId) + .let { LabelProcessor.getBestMatch(it.label, "") } + } catch (e: NotAvailableException) { + builder.senderId + }?.trim().takeIf { !it.isNullOrBlank() }?.let { " from $it" } ?: "" + } ${ + try { + Registries.getUnitRegistry().getUnitConfigById(builder.recipientId) + .let { LabelProcessor.getBestMatch(it.label, "") } + } catch (e: NotAvailableException) { + builder.recipientId + }?.trim().takeIf { !it.isNullOrBlank() }?.let { " to $it" } ?: "" + }" + } +)