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