001// --------------------------------------------------------------------------------
002// Copyright 2002-2025 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017package com.echothree.util.server.control;
018
019import com.echothree.control.user.party.common.spec.PartySpec;
020import com.echothree.model.control.core.common.CommandMessageTypes;
021import com.echothree.model.control.core.common.ComponentVendors;
022import com.echothree.model.control.core.common.EventTypes;
023import com.echothree.model.control.core.server.control.CommandControl;
024import com.echothree.model.control.core.server.control.ComponentControl;
025import com.echothree.model.control.core.server.control.CoreControl;
026import com.echothree.model.control.core.server.control.EntityTypeControl;
027import com.echothree.model.control.core.server.control.EventControl;
028import com.echothree.model.control.license.server.logic.LicenseCheckLogic;
029import com.echothree.model.control.party.common.PartyTypes;
030import com.echothree.model.control.security.server.logic.SecurityRoleLogic;
031import com.echothree.model.control.user.server.control.UserControl;
032import com.echothree.model.control.user.server.logic.UserSessionLogic;
033import com.echothree.model.data.accounting.server.entity.Currency;
034import com.echothree.model.data.core.server.entity.EntityInstance;
035import com.echothree.model.data.core.server.entity.Event;
036import com.echothree.model.data.party.common.pk.PartyPK;
037import com.echothree.model.data.party.server.entity.DateTimeFormat;
038import com.echothree.model.data.party.server.entity.Language;
039import com.echothree.model.data.party.server.entity.Party;
040import com.echothree.model.data.party.server.entity.PartyType;
041import com.echothree.model.data.party.server.entity.TimeZone;
042import com.echothree.model.data.user.common.pk.UserVisitPK;
043import com.echothree.model.data.user.server.entity.UserSession;
044import com.echothree.model.data.user.server.entity.UserVisit;
045import com.echothree.model.data.user.server.factory.UserVisitFactory;
046import com.echothree.util.common.command.BaseResult;
047import com.echothree.util.common.command.CommandResult;
048import com.echothree.util.common.command.ExecutionResult;
049import com.echothree.util.common.command.SecurityResult;
050import com.echothree.util.common.exception.BaseException;
051import com.echothree.util.common.form.ValidationResult;
052import com.echothree.util.common.message.Message;
053import com.echothree.util.common.message.Messages;
054import com.echothree.util.common.message.SecurityMessages;
055import com.echothree.util.common.persistence.BasePK;
056import com.echothree.util.common.transfer.BaseTransfer;
057import com.echothree.util.server.message.ExecutionErrorAccumulator;
058import com.echothree.util.server.message.ExecutionWarningAccumulator;
059import com.echothree.util.server.message.MessageUtils;
060import com.echothree.util.server.message.SecurityMessageAccumulator;
061import com.echothree.util.server.persistence.EntityPermission;
062import com.echothree.util.server.persistence.Session;
063import com.echothree.util.server.persistence.ThreadSession;
064import com.echothree.util.server.persistence.ThreadUtils;
065import java.nio.charset.StandardCharsets;
066import java.util.concurrent.Future;
067import javax.ejb.AsyncResult;
068import javax.inject.Inject;
069import org.apache.commons.logging.Log;
070import org.apache.commons.logging.LogFactory;
071
072public abstract class BaseCommand
073        implements ExecutionWarningAccumulator, ExecutionErrorAccumulator, SecurityMessageAccumulator {
074
075    private Log log;
076
077    private final CommandSecurityDefinition commandSecurityDefinition;
078
079    private ThreadUtils.PreservedState preservedState;
080    protected Session session;
081
082    private UserVisitPK userVisitPK;
083    private UserVisit userVisit;
084    private UserSession userSession;
085    
086    private Party party;
087    
088    private Messages executionWarnings;
089    private Messages executionErrors;
090    private Messages securityMessages;
091    
092    private String componentVendorName;
093    private String commandName;
094    
095    private boolean checkIdentityVerifiedTime = true;
096    private boolean updateLastCommandTime = true;
097    private boolean logCommand = true;
098
099    @Inject
100    protected UserControl userControl;
101
102    @Inject
103    protected CoreControl coreControl;
104
105    @Inject
106    protected ComponentControl componentControl;
107
108    @Inject
109    protected EntityTypeControl entityTypeControl;
110
111    @Inject
112    protected EventControl eventControl;
113
114    @Inject
115    protected CommandControl commandControl;
116
117//    @Inject
118//    protected LicenseCheckLogic licenseCheckLogic;
119
120    @Inject
121    protected SecurityRoleLogic securityRoleLogic;
122
123    protected BaseCommand(CommandSecurityDefinition commandSecurityDefinition) {
124        if(ControlDebugFlags.LogBaseCommands) {
125            getLog().info("BaseCommand()");
126        }
127
128        this.commandSecurityDefinition = commandSecurityDefinition;
129    }
130
131    protected Log getLog() {
132        if(log == null) {
133            log = LogFactory.getLog(this.getClass());
134        }
135        
136        return log;
137    }
138    
139    private void setupNames() {
140        Class<? extends BaseCommand> c = this.getClass();
141        var className = c.getName();
142        var nameOffset = className.lastIndexOf('.');
143        
144        componentVendorName = ComponentVendors.ECHO_THREE.name();
145        commandName = new String(className.getBytes(StandardCharsets.UTF_8), nameOffset + 1, className.length() - nameOffset - 8, StandardCharsets.UTF_8);
146    }
147    
148    public String getComponentVendorName() {
149        if(componentVendorName == null) {
150            setupNames();
151        }
152        
153        return componentVendorName;
154    }
155    
156    public String getCommandName() {
157        if(commandName == null) {
158            setupNames();
159        }
160        
161        return commandName;
162    }
163    
164    public Party getCompanyParty() {
165        Party companyParty = null;
166        var partyRelationship = userSession.getPartyRelationship();
167        
168        if(partyRelationship != null) {
169            companyParty = partyRelationship.getFromParty();
170        }
171        
172        return companyParty;
173    }
174    
175    public PartyPK getPartyPK() {
176        if(party == null) {
177            getParty();
178        }
179        
180        return party == null? null: party.getPrimaryKey();
181    }
182    
183    public Party getParty() {
184        if(party == null) {
185            party = userControl.getPartyFromUserVisitPK(userVisitPK);
186        }
187        
188        return party;
189    }
190    
191    public PartyType getPartyType() {
192        PartyType partyType = null;
193        
194        if(getParty() != null) {
195            partyType = party.getLastDetail().getPartyType();
196        }
197        
198        return partyType;
199    }
200    
201    public String getPartyTypeName() {
202        var partyType = getPartyType();
203
204        return partyType == null ? null : partyType.getPartyTypeName();
205    }
206    
207    public UserVisitPK getUserVisitPK() {
208        return userVisitPK;
209    }
210
211    public void setUserVisitPK(UserVisitPK userVisitPK) {
212        this.userVisitPK = userVisitPK;
213        userVisit = null;
214    }
215
216    private UserVisit getUserVisit(EntityPermission entityPermission) {
217        if(userVisitPK != null) {
218            if(userVisit == null) {
219                userVisit = UserVisitFactory.getInstance().getEntityFromPK(entityPermission, userVisitPK);
220            } else {
221                if(entityPermission.equals(EntityPermission.READ_WRITE)) {
222                    if(!userVisit.getEntityPermission().equals(EntityPermission.READ_WRITE)) {
223                        userVisit = UserVisitFactory.getInstance().getEntityFromPK(EntityPermission.READ_WRITE, userVisitPK);
224                    }
225                }
226            }
227        }
228
229        return userVisit;
230    }
231    
232    public UserVisit getUserVisit() {
233        return getUserVisit(EntityPermission.READ_ONLY);
234    }
235    
236    public UserVisit getUserVisitForUpdate() {
237        return getUserVisit(EntityPermission.READ_WRITE);
238    }
239    
240    public UserSession getUserSession() {
241        return userSession;
242    }
243    
244    public Session getSession() {
245        return session;
246    }
247    
248    public UserControl getUserControl() {
249        return userControl;
250    }
251    
252    public Language getPreferredLanguage() {
253        return userControl.getPreferredLanguageFromUserVisit(getUserVisit());
254    }
255    
256    public Language getPreferredLanguage(Party party) {
257        return userControl.getPreferredLanguageFromParty(party);
258    }
259    
260    public Currency getPreferredCurrency() {
261        return userControl.getPreferredCurrencyFromUserVisit(getUserVisit());
262    }
263    
264    public Currency getPreferredCurrency(Party party) {
265        return userControl.getPreferredCurrencyFromParty(party);
266    }
267    
268    public TimeZone getPreferredTimeZone() {
269        return userControl.getPreferredTimeZoneFromUserVisit(getUserVisit());
270    }
271    
272    public TimeZone getPreferredTimeZone(Party party) {
273        return userControl.getPreferredTimeZoneFromParty(party);
274    }
275    
276    public DateTimeFormat getPreferredDateTimeFormat() {
277        return userControl.getPreferredDateTimeFormatFromUserVisit(getUserVisit());
278    }
279    
280    public DateTimeFormat getPreferredDateTimeFormat(Party party) {
281        return userControl.getPreferredDateTimeFormatFromParty(party);
282    }
283    
284    public boolean getCheckIdentityVerifiedTime() {
285        return checkIdentityVerifiedTime;
286    }
287
288    public void setCheckIdentityVerifiedTime(boolean checkIdentityVerifiedTime) {
289        this.checkIdentityVerifiedTime = checkIdentityVerifiedTime;
290    }
291
292    public boolean getUpdateLastCommandTime() {
293        return updateLastCommandTime;
294    }
295
296    public void setUpdateLastCommandTime(boolean updateLastCommandTime) {
297        this.updateLastCommandTime = updateLastCommandTime;
298    }
299
300    public boolean getLogCommand() {
301        return logCommand;
302    }
303
304    public void setLogCommand(boolean logCommand) {
305        this.logCommand = logCommand;
306    }
307
308    private void checkUserVisit() {
309        if(getUserVisit() != null) {
310            userSession = userControl.getUserSessionByUserVisit(userVisit);
311
312            if(userSession != null && checkIdentityVerifiedTime) {
313                var identityVerifiedTime = userSession.getIdentityVerifiedTime();
314
315                if(identityVerifiedTime != null) {
316                    var timeSinceLastCommand = session.START_TIME - userVisit.getLastCommandTime();
317
318                    // If it has been > 15 minutes since their last command, invalidate the UserSession.
319                    if(timeSinceLastCommand > 15 * 60 * 1000) {
320                        userSession = UserSessionLogic.getInstance().invalidateUserSession(userSession);
321                    }
322                }
323            }
324        }
325    }
326
327    protected CommandSecurityDefinition getCommandSecurityDefinition() {
328        return commandSecurityDefinition;
329    }
330    
331    // Returns true if everything passes.
332    protected boolean checkCommandSecurityDefinition() {
333        var passed = true;
334        var myCommandSecurityDefinition = getCommandSecurityDefinition();
335        
336        if(myCommandSecurityDefinition != null) {
337            var partyTypeName = getParty() == null ? null : party.getLastDetail().getPartyType().getPartyTypeName();
338            var foundPartyType = false;
339            var foundPartySecurityRole = false;
340
341            for(var partyTypeDefinition : myCommandSecurityDefinition.getPartyTypeDefinitions()) {
342                if(partyTypeName == null) {
343                    if(partyTypeDefinition.getPartyTypeName() == null) {
344                        foundPartyType = true;
345                        foundPartySecurityRole = true;
346                        break;
347                    }
348                } else {
349                    if(partyTypeDefinition.getPartyTypeName().equals(partyTypeName)) {
350                        var securityRoleDefinitions = partyTypeDefinition.getSecurityRoleDefinitions();
351
352                        if(securityRoleDefinitions == null) {
353                            foundPartySecurityRole = true;
354                        } else {
355                            for(var securityRoleDefinition : securityRoleDefinitions) {
356                                var securityRoleGroupName = securityRoleDefinition.getSecurityRoleGroupName();
357                                var securityRoleName = securityRoleDefinition.getSecurityRoleName();
358
359                                if(securityRoleGroupName != null && securityRoleName != null) {
360                                    foundPartySecurityRole = securityRoleLogic.hasSecurityRoleUsingNames(this, party, securityRoleGroupName,
361                                            securityRoleName);
362                                }
363
364                                if(foundPartySecurityRole) {
365                                    break;
366                                }
367                            }
368                        }
369
370                        foundPartyType = true;
371                        break;
372                    }
373                }
374            }
375
376            if(!foundPartyType || !foundPartySecurityRole) {
377                passed = false;
378            }
379        }
380        
381        return passed;
382    }
383
384    // Returns true if everything passes.
385    protected boolean checkOptionalSecurityRoles() {
386        return true;
387    }
388
389    protected SecurityResult security() {
390        if(!(checkCommandSecurityDefinition() && checkOptionalSecurityRoles())) {
391            addSecurityMessage(SecurityMessages.InsufficientSecurity.name());
392        }
393
394        return securityMessages == null ? null : new SecurityResult(securityMessages);
395    }
396    
397    @Override
398    public void addSecurityMessage(Message message) {
399        if(securityMessages == null) {
400            securityMessages = new Messages();
401        }
402        
403        securityMessages.add(Messages.SECURITY_MESSAGE, message);
404    }
405    
406    @Override
407    public void addSecurityMessage(String key, Object... values) {
408        addSecurityMessage(new Message(key, values));
409    }
410    
411    @Override
412    public Messages getSecurityMessages() {
413        return securityMessages;
414    }
415    
416    @Override
417    public boolean hasSecurityMessages() {
418        return securityMessages != null && securityMessages.size(Messages.SECURITY_MESSAGE) != 0;
419    }
420    
421    protected ValidationResult validate() {
422        if(ControlDebugFlags.LogBaseCommands) {
423            log.info("validate()");
424        }
425        
426        return null;
427    }
428    
429    protected abstract BaseResult execute();
430    
431    @Override
432    public void addExecutionWarning(Message message) {
433        if(executionWarnings == null) {
434            executionWarnings = new Messages();
435        }
436        
437        executionWarnings.add(Messages.EXECUTION_WARNING, message);
438    }
439    
440    @Override
441    public void addExecutionWarning(String key, Object... values) {
442        addExecutionWarning(new Message(key, values));
443    }
444    
445    @Override
446    public Messages getExecutionWarnings() {
447        return executionWarnings;
448    }
449    
450    @Override
451    public boolean hasExecutionWarnings() {
452        return executionWarnings != null && executionWarnings.size(Messages.EXECUTION_WARNING) != 0;
453    }
454    
455    @Override
456    public void addExecutionError(Message message) {
457        if(executionErrors == null) {
458            executionErrors = new Messages();
459        }
460        
461        executionErrors.add(Messages.EXECUTION_ERROR, message);
462    }
463    
464    @Override
465    public void addExecutionError(String key, Object... values) {
466        addExecutionError(new Message(key, values));
467    }
468    
469    @Override
470    public Messages getExecutionErrors() {
471        return executionErrors;
472    }
473    
474    @Override
475    public boolean hasExecutionErrors() {
476        return executionErrors != null && executionErrors.size(Messages.EXECUTION_ERROR) != 0;
477    }
478    
479    protected BaseResult getBaseResultAfterErrors() {
480        return null;
481    }
482
483    protected void setupSession() {
484        preservedState = ThreadUtils.preserveState();
485        initSession();
486    }
487
488    // Called by setupSession() and canQueryByGraphQl()
489    protected void initSession() {
490        session = ThreadSession.currentSession();
491    }
492
493    protected void teardownSession() {
494        ThreadUtils.close();
495        session = null;
496
497        ThreadUtils.restoreState(preservedState);
498        preservedState = null;
499    }
500
501    public Future<CommandResult> runAsync(UserVisitPK userVisitPK) {
502        return new AsyncResult<>(run(userVisitPK));
503    }
504
505    public CommandResult run(UserVisitPK userVisitPK)
506            throws BaseException {
507        if(ControlDebugFlags.LogBaseCommands) {
508            log.info(">>> run()");
509        }
510
511        this.userVisitPK = userVisitPK;
512
513        setupSession();
514
515        SecurityResult securityResult;
516        ValidationResult validationResult = null;
517        ExecutionResult executionResult;
518        CommandResult commandResult;
519
520        try {
521            BaseResult baseResult = null;
522
523//            if(licenseCheckLogic.permitExecution(session)) {
524                checkUserVisit();
525                securityResult = security();
526
527                if(securityResult == null || !securityResult.getHasMessages()) {
528                    validationResult = validate();
529
530                    if(validationResult == null || !validationResult.getHasErrors()) {
531                        baseResult = execute();
532                    }
533                }
534//            } else {
535//                addExecutionError(ExecutionErrors.LicenseCheckFailed.name());
536//            }
537
538            executionResult = new ExecutionResult(executionWarnings, executionErrors, baseResult == null ? getBaseResultAfterErrors() : baseResult);
539
540            // Don't waste time getting the preferredLanguage if we don't need to.
541            if((securityResult != null && securityResult.getHasMessages())
542                    || (executionResult.getHasWarnings() || executionResult.getHasErrors())
543                    || (validationResult != null && validationResult.getHasErrors())) {
544                var preferredLanguage = getPreferredLanguage();
545
546                if(securityResult != null) {
547                    MessageUtils.getInstance().fillInMessages(preferredLanguage, CommandMessageTypes.Security.name(), securityResult.getSecurityMessages());
548                }
549
550                MessageUtils.getInstance().fillInMessages(preferredLanguage, CommandMessageTypes.Warning.name(), executionResult.getExecutionWarnings());
551                MessageUtils.getInstance().fillInMessages(preferredLanguage, CommandMessageTypes.Error.name(), executionResult.getExecutionErrors());
552
553                if(validationResult != null) {
554                    MessageUtils.getInstance().fillInMessages(preferredLanguage, CommandMessageTypes.Validation.name(), validationResult.getValidationMessages());
555                }
556            }
557
558            if(updateLastCommandTime) {
559                if(getUserVisitForUpdate() == null) {
560                    getLog().error("Command not logged, unknown userVisit");
561                } else {
562                    userVisit.setLastCommandTime(Math.max(session.START_TIME, userVisit.getLastCommandTime()));
563
564                    // TODO: Check PartyTypeAuditPolicy to see if the command should be logged
565                    if(logCommand) {
566                        var componentVendor = componentControl.getComponentVendorByName(getComponentVendorName());
567
568                        if(componentVendor != null) {
569                            getCommandName();
570                            getParty(); // TODO: should only use if UserSession.IdentityVerifiedTime != null
571
572                            if(ControlDebugFlags.CheckCommandNameLength) {
573                                if(commandName.length() > 80) {
574                                    getLog().error("commandName length > 80 characters, " + commandName);
575                                    commandName = commandName.substring(0, 79);
576                                }
577                            }
578
579                            var command = commandControl.getCommandByName(componentVendor, commandName);
580
581                            if(command == null) {
582                                command = commandControl.createCommand(componentVendor, commandName, 1, party == null ? null : party.getPrimaryKey());
583                            }
584
585                            if(command != null) {
586                                var userVisitStatus = userControl.getUserVisitStatusForUpdate(userVisit);
587
588                                if(userVisitStatus != null) {
589                                    Integer userVisitCommandSequence = userVisitStatus.getUserVisitCommandSequence() + 1;
590                                    var hadSecurityErrors = securityResult == null ? null : securityResult.getHasMessages();
591                                    var hadValidationErrors = validationResult == null ? null : validationResult.getHasErrors();
592                                    var hasExecutionErrors = executionResult.getHasErrors();
593
594                                    userVisitStatus.setUserVisitCommandSequence(userVisitCommandSequence);
595
596                                    userControl.createUserVisitCommand(userVisit, userVisitCommandSequence, party, command, session.START_TIME_LONG,
597                                            System.currentTimeMillis(), hadSecurityErrors, hadValidationErrors, hasExecutionErrors);
598                                } else {
599                                    getLog().error("Command not logged, unknown userVisitStatus for " + userVisit.getPrimaryKey());
600                                }
601                            } else {
602                                getLog().error("Command not logged, unknown (and could not create) commandName = " + commandName);
603                            }
604                        } else {
605                            getLog().error("Command not logged, unknown componentVendorName = " + componentVendorName);
606                        }
607                    }
608                }
609            }
610        } finally {
611            teardownSession();
612        }
613
614        // The Session for this Thread must NOT be utilized by anything after teardownSession() has been called.
615        commandResult = new CommandResult(securityResult, validationResult, executionResult);
616
617        if(commandResult.hasSecurityMessages() || commandResult.hasValidationErrors()) {
618            getLog().info("commandResult = " + commandResult);
619        }
620
621        if(ControlDebugFlags.LogBaseCommands) {
622            if(commandResult.hasExecutionErrors()) {
623                log.info("<<< run(), returning executionResult = " + commandResult.getExecutionResult());
624            } else {
625                log.info("<<< run()");
626            }
627        }
628
629        return commandResult;
630    }
631
632    // --------------------------------------------------------------------------------
633    //   Security Utilities
634    // --------------------------------------------------------------------------------
635
636    protected boolean canSpecifyParty() {
637        var partyType = getPartyType();
638        var result = false; // Default to most restrictive result.
639
640        if(partyType != null) {
641            var partyTypeName = partyType.getPartyTypeName();
642
643            // Of PartyTypes that may login only EMPLOYEEs or UTILITYs may specify another Party. CUSTOMERs and
644            // VENDORs may not.
645            result = partyTypeName.equals(PartyTypes.EMPLOYEE.name())
646                    || partyTypeName.equals(PartyTypes.UTILITY.name());
647        }
648
649        return result;
650    }
651
652    protected SecurityResult selfOnly(PartySpec spec) {
653        var hasInsufficientSecurity = !canSpecifyParty() && spec.getPartyName() != null;
654
655        return hasInsufficientSecurity ? getInsufficientSecurityResult() : null;
656    }
657
658    protected SecurityResult getInsufficientSecurityResult() {
659        return new SecurityResult(new Messages().add(Messages.SECURITY_MESSAGE, new Message(SecurityMessages.InsufficientSecurity.name())));
660    }
661
662    // --------------------------------------------------------------------------------
663    //   Event Utilities
664    // --------------------------------------------------------------------------------
665
666    protected Event sendEvent(final BasePK basePK, final EventTypes eventType, final BasePK relatedBasePK,
667            final EventTypes relatedEventType, final BasePK createdByBasePK) {
668        var entityInstance = coreControl.getEntityInstanceByBasePK(basePK);
669        var relatedEntityInstance = relatedBasePK == null ? null : coreControl.getEntityInstanceByBasePK(relatedBasePK);
670        
671        return sendEvent(entityInstance, eventType, relatedEntityInstance, relatedEventType, createdByBasePK);
672    }
673    
674    protected Event sendEvent(final EntityInstance entityInstance, final EventTypes eventType, final BasePK relatedBasePK,
675            final EventTypes relatedEventType, final BasePK createdByBasePK) {
676        var relatedEntityInstance = relatedBasePK == null ? null : coreControl.getEntityInstanceByBasePK(relatedBasePK);
677
678        return sendEvent(entityInstance, eventType, relatedEntityInstance, relatedEventType, createdByBasePK);
679    }
680    
681    protected Event sendEvent(final EntityInstance entityInstance, final EventTypes eventType, final EntityInstance relatedEntityInstance,
682            final EventTypes relatedEventType, final BasePK createdByBasePK) {
683        Event event = null;
684        
685        if(createdByBasePK != null) {
686            event = eventControl.sendEvent(entityInstance, eventType, relatedEntityInstance, relatedEventType,
687                createdByBasePK);
688        }
689        
690        return event;
691    }
692    
693    // --------------------------------------------------------------------------------
694    //   Option Utilities
695    // --------------------------------------------------------------------------------
696
697    /** This should only be called an override of setupSession(). After that, TransferCaches may have cached knowledge
698     * that specific options were set.
699     * @param option The option to remove.
700     */
701    protected void removeOption(String option) {
702        session.getOptions().remove(option);
703    }
704
705    // --------------------------------------------------------------------------------
706    //   Transfer Property Utilities
707    // --------------------------------------------------------------------------------
708
709    /** This should only be called an override of setupSession(). After that, TransferCaches may have cached knowledge
710     * that specific properties were filtered.
711     * @param clazz The Class whose properties should be examined.
712     * @param property The property to remove.
713     */
714    protected void removeFilteredTransferProperty(Class<? extends BaseTransfer> clazz, String property) {
715        var transferProperties = session.getTransferProperties();
716
717        if(transferProperties != null) {
718            var properties = transferProperties.getProperties(clazz);
719
720            if(properties != null) {
721                properties.remove(property);
722            }
723        }
724    }
725    
726}