package com.airlock.iam.servicecontainer.app.application.configuration.task;

import com.airlock.iam.base.misc.file.FileName;
import com.airlock.iam.common.api.domain.model.persistency.exception.NotFoundException;
import com.airlock.iam.common.api.domain.model.persistency.exception.PersisterException;
import com.airlock.iam.core.misc.persistency.TokenListIterator;
import com.airlock.iam.core.misc.persistency.TokenListPersister;
import com.airlock.iam.core.misc.persistency.TokenUserStructure;
import com.airlock.iam.core.misc.renderer.TokenListRenderer;
import com.airlock.iam.core.misc.renderer.TokenRendererException;
import com.airlock.iam.core.misc.renderer.util.TokenOrderInfoUtils;
import com.airlock.iam.core.misc.tanserver.LogTypes;
import com.airlock.iam.core.misc.tanserver.core.HashedTokenList;
import com.airlock.iam.core.misc.tanserver.core.PlainTokenList;
import com.airlock.iam.core.misc.tanserver.core.TokenListGenerator;
import com.airlock.iam.core.misc.tanserver.core.TokenListHasher;
import com.airlock.iam.core.misc.tanserver.core.TokenType;
import com.airlock.iam.core.misc.util.password.hash.PasswordHash;
import com.airlock.iam.core.misc.util.report.ReportRendererException;
import com.airlock.iam.core.misc.util.report.aggregation.AggregateReport;
import com.airlock.iam.core.misc.util.report.aggregation.ReportData;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.log.audit.AuditLogger;
import com.airlock.iam.log.usertrail.UserTrailLogger;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.license.PluginLicenseRestriction;
import com.airlock.iam.plugin.api.annotation.lifecycle.AfterConfigActivation;
import com.airlock.iam.plugin.api.annotation.property.BooleanProperty;
import com.airlock.iam.plugin.api.annotation.property.EnumProperty;
import com.airlock.iam.plugin.api.annotation.property.FileProperty;
import com.airlock.iam.plugin.api.annotation.property.IntegerProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.Environment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.InitializeException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.exception.PropertyTypeMismatchException;
import com.airlock.iam.plugin.api.valuereader.IntegerPropertyValueReader;
import com.airlock.iam.servicecontainer.api.application.configuration.task.TaskConfig;
import com.airlock.iam.servicecontainer.api.domain.model.task.AbstractTask;
import com.airlock.iam.servicecontainer.api.domain.model.task.Task;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.lang3.time.FastDateFormat;

@Plugin(
   description = "This plug-in implements a task that is used in the TAN authentication server. It performs tasks, which are not triggered by user a user action (e.g. by a user attempting to log in) but must be done by a batch job regularly. <p>In order to understand the following task description, it is important to understand, that a user can have zero, one, or two token lists or matrix cards at the same time: The active token list is the one being used for authentication. The next token list is a new token list waiting to become active. It is generated some time before the current active token list expires so it can be sent to the user. The new token list becomes active, if the current token list expires or is removed. Depending on configuration, just using the new token list will also activate it. </p><p>When executed, this task looks at all users with the configured token list iterator plug-in and does the following tasks. The process is done only for active users returned by the token list iterator: <ul><li>Remove expired token lists or matrix cards.</li><li>Remove empty token lists. </li><li>If the user has no active list but a valid new token list, the new token list is made the active token list.</li><li>Generate new token lists or new matrix cards where necessary. This is the case if at least one of the following conditions is true:<br/>- If the current token list or matrix card of the user is about to expire and there is no \"next token list\". <br/>- If there is no token list at all. <br/>- If there is no \"next token list\" and the corresponding flag (\"order new list flag\") is set.</li><li>Newly generated token lists are rendered by calling the configured token list renderer.</li></ul></p>",
   displayName = "TAN Batch Task"
)
@PluginLicenseRestriction(moduleAffiliation = "Matrixcard")
public class TanBatchTask extends AbstractTask implements TaskConfig {
   private static final long HOURS_PER_DAY = 24L;
   private static final long MINUTES_PER_HOUR = 60L;
   private static final long SECONDS_PER_MINUTE = 60L;
   private static final long MILLIS_PER_SECOND = 1000L;
   private static final int VALIDITY_DAYS_EXAMPLE = 30;
   private static final int VALIDITY_DAYS_REFERENCE = 30;
   private static final int MIN_TOKEN_LENGTH = 1;
   private static final int MAX_TOKEN_LENGTH = 500;
   private static final int MIN_TOKENS_PER_LIST = 1;
   private static final int MAX_TOKENS_PER_LIST = 10000;
   private static final int DEFAULT_MATRIX_TOKENS_PER_LIST = 130;
   private static final int DEFAULT_TANLIST_TOKENS_PER_LIST = 84;
   private static final int DEFAULT_INDEXEDLIST_TOKENS_PER_LIST = 70;
   private static final FastDateFormat TAN_LETTER_DATE_FORMAT = FastDateFormat.getInstance("dd.MM.yyyy");
   private static final FastDateFormat DATE_TIME_FORMAT = FastDateFormat.getInstance("yyyy/MM/dd HH:mm:ss");
   private static final ELogger LOG = ELoggerFactory.getLogger(TanBatchTask.class);
   @PluginProperty(description = "The token list persister plugin used to read and store token list structures.", optional = false)
   private TokenListPersister tokenListPersister;
   @PluginProperty(
      description = "The token list iterator plugin used to iterate over all token users. Usually this is the same as the token list persister.",
      optional = false
   )
   private TokenListIterator tokenListIterator;
   @IntegerProperty(
      description = "Specifies the number of days a token list is valid after its generation. After the period has elapsed, the list can no longer be used and is replaced by the new token list (if there is any). If there is no new list, the list is deleted. If the value -1 is used, the token list does not expire.",
      optional = true,
      defaultValue = -1,
      exampleValues = {-1, 30, 30},
      minValue = -1
   )
   private Integer maximumValidityDays;
   private static final String MAXIMUM_VALIDITY_DAYS_FIELD = "maximumValidityDays";
   @IntegerProperty(
      description = "If a list is about to expire, generates an event and/or sets the flag to generate a new list. This property specifies the number of remaining validity days that trigger this event.<br/>The value is only relevant when the property <prop>maximum-validity-days</prop> is not -1.",
      optional = true,
      exampleValues = {10, 30, 30},
      minValue = -1
   )
   private Integer remainingDaysThreshold;
   @IntegerProperty(
      description = "In order to avoid sending more than one credential to a user at the same time, this task inspects the delivery times of other credentials of the same user. The value of this property indicates the minimum number of days between the latest delivery of another token and the generation of a token list.<br/>Setting this property to zero (0) disables this feature.",
      optional = true,
      defaultValue = 0,
      exampleValues = {10, 30, 30},
      minValue = -1
   )
   private Integer deliverySecurityGap;
   @StringProperty(
      description = "This property is used when new token lists are generated. The type of tokens (for new token list generation).",
      optional = false,
      allowedValues = {"DIGITS", "DIGITS_CAPITAL_LETTERS", "DIGITS_LETTERS"}
   )
   private String tokenTypeName;
   @IntegerProperty(
      description = "This property is used when new token lists are generated. The length of each generated token (number of characters).",
      optional = false,
      minValue = 1,
      maxValue = 500,
      exampleValues = {3, 4, 6}
   )
   private Integer tokenLength;
   @IntegerProperty(
      description = "This property is used when new token lists are generated. The number of tokens in each generated list. Setting for standard matrix card (credit-card-format)",
      optional = false,
      minValue = 1,
      maxValue = 10000,
      exampleValues = {70, 84, 130}
   )
   private Integer tokensPerList;
   @PluginProperty(
      description = "This property is used when new token lists are generated.<br/>The hash function used to hash the generated tokens. It must be the same (or hash value compatible) as used when generating the token lists.",
      optional = false
   )
   private PasswordHash hashFunctionPlugin;
   @PluginProperty(
      description = "Tells the tan batch task which token list renderer to use for the rendering of newly generated token lists.",
      optional = false
   )
   private TokenListRenderer tokenListRenderer;
   @StringProperty(
      description = "Tells the TAN batch task which attribute in the context data container contains the language to be used for rendering the password. If this property is configured and if the context data container of the user has a value for this attribute, it is used when calling the password renderer plugin.<p>If this property is not defined, the user's language is not taken into account when rendering token lists!</p>",
      optional = true,
      suggestedValues = "language"
   )
   private String languageAttributeName;
   @BooleanProperty(
      description = "Deletes old rendered token lists of a user from the file system when a new one is rendered. Setting this to TRUE results in at most one rendered token list per user.<br/>If this property is set to TRUE, the plugin must have permission to delete files from the directory.",
      optional = true,
      defaultValue = "false"
   )
   private boolean deleteOldTokenLists;
   @FileProperty(
      description = "A writable directory used to store partial reports.<br/>If this property is defined, the token lists are not directly generated into the output directory (see other property) but they are generated into this working directory and are moved to the output directory once they are done.<br/>This helps to solve problems with processes automatically reading the rendered token lists and reading partial token lists during the generation process. Make sure that the working directory and the output directory reside in the same file system (if not the moving of the generated file will not be atomic).<br/>The directory is either absolute or relative to the JVMs current directory.",
      optional = true,
      isDirectory = true,
      assureExistence = true,
      assureWritable = true,
      exampleValues = {"tmp", "under-construction", "working"}
   )
   private File workingDirectory;
   @FileProperty(
      description = "Directory in the file system to put the rendered token lists in. The directory is either absolute or relative to the JVMs current directory.<p>This property is not required if the renderer plugin (see separate property) does not write on the outputstream (e.g. sends it somewhere else). It is required otherwise.</p><p><b>Note:</b> If this property is not defined and the used renderer plugin writes on the output stream, then the result (e.g. a PDF file) is lost.</p>",
      optional = true,
      isDirectory = true,
      assureExistence = true,
      assureWritable = true,
      exampleValues = {"output/", "directory/out/"}
   )
   private File outputDirectory;
   @StringProperty(
      description = "Filename prefix for rendered report files. It is important to set this to a unique value for the kind of reports generated by this task. When this task deletes old reports, it looks at this prefix (and the user id) in order to find out what files to delete. Thus, if this prefix is the same as for other reports and the reside in the same directory, other reports may be deleted.<p>Do not use the prefix \"pwd-\" if password- reports are stored in the same directory. This prefix is the default for password letters (and not configurable in older plugin versions).</p><p>This property is optional to be backwards compatible. It is strongly recommended to define a prefix.</p>",
      optional = true,
      suggestedValues = {"matrix-", "gridcard-"}
   )
   private String fileNamePrefix;
   @StringProperty(description = "Filename suffix for rendered token list files.", optional = true, suggestedValues = {".pdf", ".docx", ".txt"})
   private String fileNameSuffix;
   @StringProperty(
      description = "Name of a context property (make sure it is persisted in the used token list persister plugin) used to store the generation date of the current token list.<p>The generation date of the current token list is extracted from the token list data during the task and stored as date object (date and time) using the configured persister.</p>",
      optional = true,
      exampleValues = "tok_list_inf_gen"
   )
   private String generationDateExportProperty;
   @StringProperty(
      description = "Name of a context property (make sure it is persisted in the used token list persister plugin) used to store the validity date of the current token list.<p>The validity date of the current token list is computed using the generation date and the configured validity of the token list and stored as date object (date and time) using the configured persister.</p>",
      optional = true,
      exampleValues = "tok_list_inf_val"
   )
   private String validityDateExportProperty;
   @StringProperty(
      description = "Name of a context property (make sure it is persisted in the used token list persister plugin) used to store serial number of the current token list.<p>The serial number of the current token list is extracted from the token list data during the task and stored as string using the configured persister.</p>",
      optional = true,
      exampleValues = "tok_list_inf_val"
   )
   private String serialNumberExportProperty;
   @StringProperty(
      description = "Name of a context property (make sure it is persisted in the used token list persister plugin) used to store remaining number of tokens on the current token list.<p>The remaining number of tokens on the  current token list is extracted from the token list data during the task and stored as integer number using the configured persister.</p>",
      optional = true,
      exampleValues = "tok_list_inf_val"
   )
   private String remainingTokensExportProperty;
   @PluginProperty(
      description = "Optional property to describe an aggregate report over all generated reports in a batch. If none is configured, no aggregate report will be generated.",
      optional = true
   )
   private AggregateReport aggregateReport;
   @IntegerProperty(
      description = "When set, this property limits the number of matrix cards that are printed per day by shifting their generation date at creation time. The property can only be set, if a 'maximumValidityDays' has been configured.<br/>Example: if the property is set to 500 but 1000 new cards are to be produced, the generation dates of the cards are set in a way, that the cards expire in blocks of 500 on 2 different days.<p>Please note:<ul><li>Setting this property doubles the runtime of the task. Consider to only set it before bulk generation of matrix cards.</li><li>The actual amount of cards to be printed on a given day could exceed the value of this property due to: 'delivery security gap' configuration, explicitly (manually) ordered cards or used up cards.</li><li>The mechanism does not work retroactive, i.e. the expiration or print date of existing cards will not be altered.</li></ul></p>",
      optional = true,
      minValue = 1,
      exampleValues = {100, 500, 1000}
   )
   private Integer maxNoOfCardsToPrintPerDay;
   private static final String MAX_NO_OF_CARDS_TO_PRINT_PER_DAY_FIELD = "maxNoOfCardsToPrintPerDay";
   @EnumProperty(
      description = "This property determines whether the generation date should be shifted into the future or past when 'maxNoOfCardsToPrintPerDay' is exceeded for the prospective print date.<p>Shifting into the past reduces the validity period, shifting into the future extends the validity period.</p>",
      optional = true,
      defaultValue = "PAST"
   )
   private TanBatchTask.ShiftDirection shiftDirection;
   @SuppressFBWarnings("UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR")
   private TanBatchTask.ConfigBean config;

   public TanBatchTask(
      TokenListPersister tokenListPersister,
      TokenListIterator tokenListIterator,
      Integer maximumValidityDays,
      Integer remainingDaysThreshold,
      Integer deliverySecurityGap,
      String tokenTypeName,
      Integer tokenLength,
      Integer tokensPerList,
      PasswordHash hashFunctionPlugin,
      TokenListRenderer tokenListRenderer,
      String languageAttributeName,
      boolean deleteOldTokenLists,
      File workingDirectory,
      File outputDirectory,
      String fileNamePrefix,
      String fileNameSuffix,
      String generationDateExportProperty,
      String validityDateExportProperty,
      String serialNumberExportProperty,
      String remainingTokensExportProperty,
      AggregateReport aggregateReport,
      Integer maxNoOfCardsToPrintPerDay,
      TanBatchTask.ShiftDirection shiftDirection
   ) {
      this.tokenListPersister = tokenListPersister;
      this.tokenListIterator = tokenListIterator;
      this.maximumValidityDays = maximumValidityDays;
      this.remainingDaysThreshold = remainingDaysThreshold;
      this.deliverySecurityGap = deliverySecurityGap;
      this.tokenTypeName = tokenTypeName;
      this.tokenLength = tokenLength;
      this.tokensPerList = tokensPerList;
      this.hashFunctionPlugin = hashFunctionPlugin;
      this.tokenListRenderer = tokenListRenderer;
      this.languageAttributeName = languageAttributeName;
      this.deleteOldTokenLists = deleteOldTokenLists;
      this.workingDirectory = workingDirectory;
      this.outputDirectory = outputDirectory;
      this.fileNamePrefix = fileNamePrefix;
      this.fileNameSuffix = fileNameSuffix;
      this.generationDateExportProperty = generationDateExportProperty;
      this.validityDateExportProperty = validityDateExportProperty;
      this.serialNumberExportProperty = serialNumberExportProperty;
      this.remainingTokensExportProperty = remainingTokensExportProperty;
      this.aggregateReport = aggregateReport;
      this.maxNoOfCardsToPrintPerDay = maxNoOfCardsToPrintPerDay;
      this.shiftDirection = shiftDirection;
   }

   @AfterConfigActivation
   void afterConfigActivation() throws InitializeException {
      TanBatchTask.ConfigBean cb = new TanBatchTask.ConfigBean();
      if (this.deliverySecurityGap != 0 && this.deliverySecurityGap != -1) {
         cb.deliverySecurityGapMillis = this.deliverySecurityGap.intValue() * 24L * 60L * 60L * 1000L;
      } else {
         cb.deliverySecurityGapMillis = 0L;
      }

      cb.maxNoOfCardsToPrintPerDay = this.maxNoOfCardsToPrintPerDay;
      cb.shiftDirection = this.shiftDirection;
      if (this.maximumValidityDays == -1) {
         cb.maximumValidityDaysMillis = -1L;
         cb.remainingValidityMillisUntilOrderNewList = -1L;
      } else {
         cb.maximumValidityDaysMillis = this.maximumValidityDays.intValue() * 24L * 60L * 60L * 1000L;
         if (this.remainingDaysThreshold != -1) {
            cb.remainingValidityMillisUntilOrderNewList = this.remainingDaysThreshold.intValue() * 24L * 60L * 60L * 1000L;
            if (cb.remainingValidityMillisUntilOrderNewList >= cb.maximumValidityDaysMillis) {
               LOG.warn(
                  LogTypes.CONFIGURATION_ERROR,
                  "The property \"remainingDaysThreshold\" is set to a larger value than property \"maximumValidityDays\"! Thus the lists expires before a new list is ordered."
               );
            }
         }
      }

      TokenType tokenType = TokenType.getFromName(this.tokenTypeName);
      cb.tokenListGenerator = new TokenListGenerator(this.tokensPerList, tokenType, this.tokenLength);
      cb.workingDirectory = this.workingDirectory;
      cb.outputDirectory = this.outputDirectory;
      if (this.fileNameSuffix != null) {
         if (!this.fileNameSuffix.startsWith(".")) {
            cb.fileNameSuffix = "." + this.fileNameSuffix;
         } else {
            cb.fileNameSuffix = this.fileNameSuffix;
         }
      } else {
         cb.fileNameSuffix = "";
      }

      if (this.fileNamePrefix != null) {
         cb.fileNamePrefix = this.fileNamePrefix.trim();
      }

      cb.tokenListRenderer = this.tokenListRenderer;
      cb.tokenListIterator = this.tokenListIterator;
      cb.tokenListPersister = this.tokenListPersister;
      cb.deleteOldTokenLists = this.deleteOldTokenLists;
      cb.hashFunctionPlugin = this.hashFunctionPlugin;
      cb.languageAttributeName = this.languageAttributeName;
      this.config = cb;
   }

   @ConfigurationValidator
   static void validateConfiguration(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment) throws PropertyConstraintViolationException, PropertyTypeMismatchException {
      Integer maxNoOfCardsToPrintPerDay = IntegerPropertyValueReader.effectiveIntegerConfigValue(config, "maxNoOfCardsToPrintPerDay", env, context);
      Integer maximumValidityDays = IntegerPropertyValueReader.effectiveIntegerConfigValue(config, "maximumValidityDays", env, context);
      if (maxNoOfCardsToPrintPerDay != null && (maximumValidityDays == null || maximumValidityDays <= 0)) {
         throw new PropertyConstraintViolationException(
            "maxNoOfCardsToPrintPerDay", config, env, context, "This property can only be set if a maximum validity is set."
         );
      }
   }

   @Override
   public void executeInternal() throws RuntimeException {
      List<ReportData> reports = Lists.newArrayList();

      try {
         long startTime = System.currentTimeMillis();
         TanBatchTask.ConfigBean current = this.config;

         List<String> uids;
         try {
            if (LOG.isDebugEnabled()) {
               LOG.debug("Getting all user ids for TAN batch task");
            }

            uids = current.tokenListIterator.getAllUserIds();
         } catch (PersisterException e) {
            LOG.error(this.getLogType(), "Error getting all token user ids. Aborting batch job.");
            throw new RuntimeException("Error getting all token user ids. Aborting batch job.", e);
         }

         PrintDateMap printDateMap = null;
         if (current.maxNoOfCardsToPrintPerDay != null) {
            try {
               printDateMap = this.calculatePrintDateMap(uids, current);
            } catch (Exception e) {
               LOG.error(this.getLogType(), "Error while calculation print date map. Aborting batch execution.", e);
               return;
            }
         }

         int numOfUsers = uids.size();
         int numOfHandled = 0;
         if (LOG.isDebugEnabled()) {
            LOG.debug("Found " + numOfUsers + " user ids for TAN batch task");
         }

         for (String uid : uids) {
            if (this.checkShouldStop()) {
               LOG.info(this.getLogType(), "Job aborted incomplete.");
               return;
            }

            try {
               Optional<ReportData> reportData = this.handleUid(uid, startTime, current, printDateMap);
               if (reportData.isPresent()) {
                  reports.add(reportData.get());
               }
            } catch (PersisterException e) {
               LOG.error(this.getLogType(), "Error in TAN batch task while handling uid \"" + uid + "\" (" + e.getMessage() + "). Skipping user. ", e);
            }

            numOfHandled++;
            this.setProgressPercentage(numOfHandled * 100 / numOfUsers);
         }

         this.setProgressPercentage(100);
      } finally {
         try {
            this.handleAggregateReport(reports, this.config);
         } catch (ReportRendererException e) {
            LOG.error(this.getLogType(), "Error in TAN batch task while rendering aggregate report (" + e.getMessage() + ").", e);
         } catch (IOException e) {
            LOG.error(this.getLogType(), "Error in TAN batch task while generating aggregate report (" + e.getMessage() + ").", e);
         }
      }
   }

   @VisibleForTesting
   PrintDateMap calculatePrintDateMap(List<String> uids, TanBatchTask.ConfigBean current) throws PersisterException {
      Validate.isTrue(current.maximumValidityDaysMillis != -1L);
      PrintDateMap map = new PrintDateMap();

      for (String uid : uids) {
         TokenUserStructure tus = current.tokenListPersister.getTokenUserStructure(uid);
         if (tus != null
            && tus.isActive()
            && tus.getTokenList() != null
            && (tus.getNewTokenList() == null || this.hasListExpired(tus.getNewTokenList(), current))) {
            Date generationDate = tus.getTokenList().getGenerationDate();
            Date printDate = this.calculatePrintDate(generationDate, current);
            map.put(printDate);
         }
      }

      return map;
   }

   private Date calculatePrintDate(Date generationDate, TanBatchTask.ConfigBean cb) {
      Validate.isTrue(cb.maximumValidityDaysMillis > 0L);
      long temp = generationDate.getTime() + cb.maximumValidityDaysMillis;
      return cb.remainingValidityMillisUntilOrderNewList > 0L ? new Date(temp - cb.remainingValidityMillisUntilOrderNewList) : new Date(temp);
   }

   private void handleAggregateReport(List<ReportData> reports, TanBatchTask.ConfigBean currentConfig) throws ReportRendererException, IOException {
      if (this.aggregateReport != null && reports != null && !reports.isEmpty()) {
         ByteArrayOutputStream bos = new ByteArrayOutputStream();
         this.aggregateReport.renderReport(reports, bos);
         if (bos.size() > 0) {
            File selectedWorkingDir = this.selectWorkingDirectory(currentConfig);
            FileName filename = this.aggregateReport.generateFileName();
            File outputFile = new File(selectedWorkingDir, filename.value());
            if (outputFile.exists()) {
               throw new IOException(
                  "File \"" + filename + "\" already exists in directory \"" + selectedWorkingDir.getAbsolutePath() + "\" when storing rendered password."
               );
            }

            this.writeToFile(currentConfig.getWorkingDirectory(), currentConfig.getOutputDirectory(), bos, outputFile.getName(), outputFile);
         }
      }
   }

   private File selectWorkingDirectory(TanBatchTask.ConfigBean currentConfig) {
      return currentConfig.getWorkingDirectory() == null ? currentConfig.getOutputDirectory() : currentConfig.getWorkingDirectory();
   }

   private void writeToFile(File workingDir, File configuredOutputDir, ByteArrayOutputStream bos, String filename, File outputFile) throws IOException {
      if (configuredOutputDir != null && bos.size() > 0) {
         try (OutputStream out = Files.newOutputStream(outputFile.toPath())) {
            bos.writeTo(out);
            out.flush();
         }

         if (workingDir != null) {
            File targetFile = new File(configuredOutputDir, filename);
            if (targetFile.exists()) {
               throw new IOException(
                  "Cannot move generated file \""
                     + filename
                     + "\" from working directory ("
                     + workingDir.getAbsolutePath()
                     + ") to output directory ("
                     + configuredOutputDir.getAbsolutePath()
                     + ") because it already exists in the output directory."
               );
            }

            try {
               com.airlock.iam.base.misc.io.Files.replaceFile(outputFile, targetFile);
            } catch (IOException e) {
               throw new IOException(
                  "Moving generated file \""
                     + filename
                     + "\" from working directory ("
                     + workingDir.getAbsolutePath()
                     + ") to output directory ("
                     + configuredOutputDir
                     + ") failed. Cause: "
                     + e.getMessage()
               );
            }
         }
      }
   }

   private Optional<ReportData> handleUid(String uid, long now, TanBatchTask.ConfigBean current, PrintDateMap printDateMap) throws PersisterException {
      try {
         assert uid != null;

         TokenUserStructure tus;
         try {
            tus = current.tokenListPersister.getTokenUserStructure(uid);
         } catch (NotFoundException e) {
            LOG.warn(
               this.getLogType(),
               "The token user structure with uid \""
                  + uid
                  + "\" could not be found in the batch job. User might have been deleted during batch job. Skipping this user"
            );
            return Optional.empty();
         }

         if (!tus.isActive()) {
            if (LOG.isDebugEnabled()) {
               LOG.debug("Skipping token user structure with uid \"" + uid + "\" in tan batch task because: not active");
            }

            return Optional.empty();
         }

         if (LOG.isDebugEnabled()) {
            if (tus.getTokenList() == null) {
               LOG.debug("User \"" + tus.getUid() + "\" has no current token list");
            } else {
               HashedTokenList l = tus.getTokenList();
               LOG.debug(
                  "Details about current token list of user \""
                     + tus.getUid()
                     + "\": ID="
                     + l.getIdentification()
                     + ", generation date="
                     + DATE_TIME_FORMAT.format(l.getGenerationDate())
                     + ", list expires="
                     + (
                        current.maximumValidityDaysMillis == -1L
                           ? "never"
                           : DATE_TIME_FORMAT.format(new Date(l.getGenerationDate().getTime() + current.maximumValidityDaysMillis))
                     )
                     + ", total tokens="
                     + l.getNumberOfTokens()
                     + ", unused tokens="
                     + l.getNumberOfUnusedTokens()
               );
            }

            if (tus.getNewTokenList() == null) {
               LOG.debug("User \"" + tus.getUid() + "\" has no new token list");
            } else {
               HashedTokenList l = tus.getNewTokenList();
               LOG.debug(
                  "Details about new token list of user \""
                     + tus.getUid()
                     + "\": ID="
                     + l.getIdentification()
                     + ", generation date="
                     + DATE_TIME_FORMAT.format(l.getGenerationDate())
                     + ", list expires="
                     + (
                        current.maximumValidityDaysMillis == -1L
                           ? "never"
                           : DATE_TIME_FORMAT.format(new Date(l.getGenerationDate().getTime() + current.maximumValidityDaysMillis))
                     )
                     + ", total tokens="
                     + l.getNumberOfTokens()
                     + ", unused tokens="
                     + l.getNumberOfUnusedTokens()
               );
            }
         }

         boolean dirty = false;
         if (StringUtils.isNotEmpty(this.generationDateExportProperty)) {
            Object contextObj = tus.getValueFromContextData(this.generationDateExportProperty);
            if (contextObj != null && !(contextObj instanceof Date)) {
               LOG.warn(this.getLogType(), "generation-date-export-property value is not of a supported date type: " + contextObj.getClass().getCanonicalName());
            } else {
               Date genDateFromDb = (Date)contextObj;
               if (tus.getTokenList() == null && genDateFromDb != null) {
                  tus.setValueToContextData(this.generationDateExportProperty, null);
                  dirty = true;
               } else if (tus.getTokenList() != null) {
                  Date genDateFromWithinlist = tus.getTokenList().getGenerationDate();
                  if (!genDateFromWithinlist.equals(genDateFromDb)) {
                     tus.setValueToContextData(this.generationDateExportProperty, genDateFromWithinlist);
                  }

                  dirty = true;
               }
            }
         }

         if (StringUtils.isNotEmpty(this.validityDateExportProperty)) {
            Object contextObj = tus.getValueFromContextData(this.validityDateExportProperty);
            if (contextObj != null && !(contextObj instanceof Date)) {
               LOG.warn(this.getLogType(), "validity-date-export-property value is not of a supported date type: " + contextObj.getClass().getCanonicalName());
            } else {
               Date valDateFromDb = (Date)contextObj;
               if (tus.getTokenList() == null && valDateFromDb != null) {
                  tus.setValueToContextData(this.validityDateExportProperty, null);
                  dirty = true;
               } else if (tus.getTokenList() != null) {
                  Date valDateFromWithinList;
                  if (this.maximumValidityDays == -1) {
                     valDateFromWithinList = null;
                  } else {
                     valDateFromWithinList = DateUtils.addDays(tus.getTokenList().getGenerationDate(), this.maximumValidityDays);
                  }

                  if (ObjectUtils.notEqual(valDateFromWithinList, valDateFromDb)) {
                     tus.setValueToContextData(this.validityDateExportProperty, valDateFromWithinList);
                     dirty = true;
                  }
               }
            }
         }

         if (StringUtils.isNotEmpty(this.serialNumberExportProperty)) {
            Object contextObj = tus.getValueFromContextData(this.serialNumberExportProperty);
            if (contextObj != null && !(contextObj instanceof String)) {
               LOG.warn(this.getLogType(), "serial-number-export-property value is not of a supported string type: " + contextObj.getClass().getCanonicalName());
            } else {
               String serialFromDb = (String)tus.getValueFromContextData(this.serialNumberExportProperty);
               if (tus.getTokenList() == null && serialFromDb != null) {
                  tus.setValueToContextData(this.serialNumberExportProperty, null);
                  dirty = true;
               } else if (tus.getTokenList() != null && !tus.getTokenList().getIdentification().equals(serialFromDb)) {
                  tus.setValueToContextData(this.serialNumberExportProperty, tus.getTokenList().getIdentification());
                  dirty = true;
               }
            }
         }

         if (StringUtils.isNotEmpty(this.remainingTokensExportProperty)) {
            Object contextObj = tus.getValueFromContextData(this.remainingTokensExportProperty);
            if (contextObj != null && !(contextObj instanceof Number)) {
               LOG.warn(
                  this.getLogType(), "remaining-tokens-export-property value is not of a supported number type: " + contextObj.getClass().getCanonicalName()
               );
            } else {
               Number unusedTokensFromDb = (Number)contextObj;
               Number unusedTokensFromTus = tus.getTokenList() == null ? null : tus.getTokenList().getNumberOfUnusedTokens();
               if (!this.numberEquals(unusedTokensFromTus, unusedTokensFromDb)) {
                  tus.setValueToContextData(this.remainingTokensExportProperty, unusedTokensFromTus);
                  dirty = true;
               }
            }
         }

         if (tus.getTokenList() == null && tus.getNewTokenList() != null) {
            AuditLogger.log(
               TanBatchTask.class,
               this.getLogType(),
               "Token user structure with uid \""
                  + uid
                  + "\" has no current but a new token list. Making new token list "
                  + tus.getNewTokenList().getIdentification()
                  + " the current list"
            );
            UserTrailLogger.log(
               uid,
               "Activated new matrix card/TAN list (ID=" + tus.getNewTokenList().getIdentification() + ") because there is no current matrix card/TAN list."
            );
            tus.setTokenList(tus.getNewTokenList());
            tus.setNewTokenList(null);
            dirty = true;
         }

         if (tus.getNewTokenList() != null && this.hasListExpired(tus.getNewTokenList(), current)) {
            AuditLogger.log(
               TanBatchTask.class,
               this.getLogType(),
               "New token list of user \"" + uid + "\" " + tus.getNewTokenList().getIdentification() + " has expired. Deleting list."
            );
            UserTrailLogger.log(uid, "Deleted new matrix card/TAN list (ID=" + tus.getNewTokenList().getIdentification() + ") because it has expired");
            tus.setNewTokenList(null);
            dirty = true;
         }

         if (tus.getTokenList() != null && this.hasListExpired(tus.getTokenList(), current)) {
            if (tus.getNewTokenList() != null) {
               AuditLogger.log(
                  TanBatchTask.class,
                  this.getLogType(),
                  "Current token list of user \""
                     + uid
                     + "\" "
                     + tus.getTokenList().getIdentification()
                     + " has expired. Replacing with new list "
                     + tus.getNewTokenList().getIdentification()
                     + "."
               );
               UserTrailLogger.log(
                  uid,
                  "Deleted current matrix card/TAN list (ID="
                     + tus.getTokenList().getIdentification()
                     + ") because it has expired. Activated new matrix card/TAN list (ID="
                     + tus.getNewTokenList().getIdentification()
                     + ")."
               );
               tus.setTokenList(tus.getNewTokenList());
               tus.setNewTokenList(null);
               dirty = true;
            } else {
               AuditLogger.log(
                  TanBatchTask.class,
                  this.getLogType(),
                  "Current token list of user \""
                     + uid
                     + "\" "
                     + tus.getTokenList().getIdentification()
                     + " has expired. Deleting list (there is no new list)."
               );
               UserTrailLogger.log(
                  uid,
                  "Deleted current matrix card/TAN list (ID="
                     + tus.getTokenList().getIdentification()
                     + ") because it has expired. There is no more matrix card/TAN list now."
               );
               tus.setTokenList(null);
               dirty = true;
            }
         }

         boolean generateNewList = false;
         if (tus.getOrderListFlag()) {
            if (tus.getNewTokenList() != null) {
               AuditLogger.log(
                  TanBatchTask.class,
                  this.getLogType(),
                  "Token user structure with uid \""
                     + uid
                     + "\" has the order-new-list-flag set but has already a valid new list: "
                     + tus.getNewTokenList().getIdentification()
                     + ". Resetting the flag."
               );
               tus.resetOrderedList();
               dirty = true;
            } else {
               generateNewList = true;
               AuditLogger.log(
                  TanBatchTask.class,
                  this.getLogType(),
                  "Planning to generate new token list for token user structure with uid \"" + uid + "\" because order-new-list-flag is set"
               );
            }
         }

         if (!generateNewList) {
            if (tus.getNewTokenList() == null && tus.getTokenList() == null) {
               generateNewList = true;
               AuditLogger.log(
                  TanBatchTask.class,
                  this.getLogType(),
                  "Planning to generate new token list for token user structure with uid \"" + uid + "\" because user has no token list at all"
               );
            } else if (current.maximumValidityDaysMillis > -1L && tus.getNewTokenList() == null && tus.getTokenList() != null) {
               long generationDate = tus.getTokenList().getGenerationDate().getTime();
               long expirationDate = generationDate + current.maximumValidityDaysMillis;
               if (expirationDate - current.remainingValidityMillisUntilOrderNewList < now) {
                  generateNewList = true;
                  AuditLogger.log(
                     TanBatchTask.class,
                     this.getLogType(),
                     "Planning to generate new token list for token user structure with uid \""
                        + uid
                        + "\" because token list "
                        + tus.getTokenList().getIdentification()
                        + " is expiring soon (at "
                        + DATE_TIME_FORMAT.format(new Date(expirationDate))
                        + ")"
                  );
               }
            }
         }

         if (generateNewList) {
            Date[] otherCredentialsDeliveryDates = tus.getOtherCredentialsDeliveryDates();
            if (current.deliverySecurityGapMillis > 0L && otherCredentialsDeliveryDates != null) {
               for (Date otherDate : otherCredentialsDeliveryDates) {
                  if (otherDate != null && otherDate.getTime() + current.deliverySecurityGapMillis > now) {
                     AuditLogger.log(
                        TanBatchTask.class,
                        this.getLogType(),
                        "Not generating token list for token user stucture with uid  \""
                           + uid
                           + "\" because the delivery date of some other credential ("
                           + DATE_TIME_FORMAT.format(otherDate)
                           + ") is too close (Delivery Security Gap)"
                     );
                     generateNewList = false;
                     break;
                  }
               }
            }
         }

         Optional<ReportData> reportData;
         if (generateNewList) {
            dirty = true;
            Date generationDate = this.determineGenerationDate(printDateMap, current);
            PlainTokenList plainList = this.generateNewTokenList(current.tokenListGenerator, generationDate);
            if (LOG.isDebugEnabled()) {
               AuditLogger.log(
                  TanBatchTask.class,
                  this.getLogType(),
                  "Generated new token list for uid \""
                     + uid
                     + "\": "
                     + plainList.getIdentification()
                     + "("
                     + plainList.getNumberOfTokens()
                     + " tokens of length "
                     + plainList.getTokenLength()
                     + " on list)"
               );
            }

            HashedTokenList hashedList = this.hashTokenList(plainList, current.hashFunctionPlugin);
            if (tus.getTokenList() == null) {
               tus.setTokenList(hashedList);
            } else {
               tus.setNewTokenList(hashedList);
            }

            tus.setOrderListFlag(false);
            tus.setDeliveryDate(new Date(now));
            String language;
            if (current.languageAttributeName != null) {
               Object o = tus.getValueFromContextData(current.languageAttributeName);
               language = o == null ? null : o.toString();
            } else {
               language = null;
            }

            reportData = Optional.of(this.renderTokenList(tus, plainList, language, current));
            UserTrailLogger.log(uid, "Generated new matrix card/TAN list (ID=" + plainList.getIdentification() + ").");
            tus.resetOrderedList();
         } else {
            reportData = Optional.empty();
         }

         if (dirty) {
            current.tokenListPersister.makeTokenUserStructurePersistent(tus);
         }

         return reportData;
      } catch (TokenRendererException e) {
         LOG.warn(this.getLogType(), "Error rendering token list for uid \"" + uid + "\". List not generated or rendered.", e);
      } catch (IOException e) {
         LOG.warn(this.getLogType(), "Error storing rendered token list for uid \"" + uid + "\". List not generated or rendered.", e);
      }

      return Optional.empty();
   }

   @VisibleForTesting
   Date determineGenerationDate(PrintDateMap printDateMap, TanBatchTask.ConfigBean cb) {
      Date generationDate = new Date();
      if (cb.maxNoOfCardsToPrintPerDay != null) {
         Date printDate;
         for (printDate = this.calculatePrintDate(generationDate, cb);
            printDateMap.getCountByDate(printDate) >= cb.maxNoOfCardsToPrintPerDay;
            printDate = this.calculatePrintDate(generationDate, cb)
         ) {
            if (cb.shiftDirection.equals(TanBatchTask.ShiftDirection.PAST)) {
               generationDate = DateUtils.addDays(generationDate, -1);
            } else {
               generationDate = DateUtils.addDays(generationDate, 1);
            }
         }

         printDateMap.put(printDate);
      }

      return generationDate;
   }

   private PlainTokenList generateNewTokenList(TokenListGenerator tokenListGenerator, Date generationDate) {
      synchronized (tokenListGenerator) {
         return tokenListGenerator.generatePlainTokenList(generationDate);
      }
   }

   private HashedTokenList hashTokenList(PlainTokenList plainList, PasswordHash hashFunction) {
      TokenListHasher hasher = new TokenListHasher(hashFunction);
      return hasher.hashList(plainList);
   }

   protected ReportData renderTokenList(TokenUserStructure tokenUserStructure, PlainTokenList list, String language, TanBatchTask.ConfigBean current) throws TokenRendererException, IOException {
      String uid = tokenUserStructure.getUid();
      File localOutputDirectory = current.outputDirectory;
      FileName filename = null;
      File tokenListFile = null;
      if (localOutputDirectory != null) {
         if (current.deleteOldTokenLists) {
            File[] oldLists = current.outputDirectory.listFiles(new TanBatchTask.ReportDataFilenameFilter(uid, current, localOutputDirectory));
            if (oldLists != null) {
               for (File oldList : oldLists) {
                  AuditLogger.log(
                     TanBatchTask.class,
                     this.getLogType(),
                     "Deleting old rendered token list because generated new one for this user. UID = \""
                        + uid
                        + "\", file = \""
                        + oldList.getAbsolutePath()
                        + "\""
                  );
                  if (!oldList.delete()) {
                     LOG.warn(this.getLogType(), "Cannot delete old rendered token list. UID = \"" + uid + "\", file = \"" + oldList.getAbsolutePath() + "\"");
                  }
               }
            }
         }

         filename = this.createFileName(tokenUserStructure, list, current);
         File targetDir = current.workingDirectory == null ? current.outputDirectory : current.workingDirectory;
         tokenListFile = new File(targetDir, filename.value());
         if (tokenListFile.exists()) {
            throw new IOException(
               "File \"" + filename + "\" already exists in directory \"" + targetDir.getAbsolutePath() + "\" when storing rendered token list."
            );
         }
      }

      Map<String, Object> map = new HashMap<>(tokenUserStructure.getContextDataContainer());
      map.put("ListId", list.getIdentification());
      map.put("ExpirationDate", TAN_LETTER_DATE_FORMAT.format(new Date(list.getGenerationDate().getTime() + current.maximumValidityDaysMillis)));
      map.put("ExpirationDateObject", new Date(list.getGenerationDate().getTime() + current.maximumValidityDaysMillis));
      map.put("DateString", TAN_LETTER_DATE_FORMAT.format(new Date()));
      TokenOrderInfoUtils.addOrderInfo(map, tokenUserStructure.getListOrderedUser(), tokenUserStructure.getListOrderedDate());
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      current.tokenListRenderer.renderTokenList(list, tokenUserStructure, language, map, bos);
      if (localOutputDirectory != null && bos.size() > 0) {
         try (OutputStream out = Files.newOutputStream(tokenListFile.toPath())) {
            bos.writeTo(out);
            out.flush();
         }

         if (current.workingDirectory != null) {
            File targetFile = new File(current.outputDirectory, filename.value());
            if (targetFile.exists()) {
               throw new IOException(
                  "Cannot move generated file \""
                     + filename
                     + "\" from working directory ("
                     + current.workingDirectory.getAbsolutePath()
                     + ") to output directory ("
                     + current.outputDirectory
                     + ") because it already exists in the output directory."
               );
            }

            try {
               com.airlock.iam.base.misc.io.Files.replaceFile(tokenListFile, targetFile);
            } catch (IOException e) {
               throw new IOException(
                  "Moving generated file \""
                     + filename
                     + "\" from working directory ("
                     + current.workingDirectory.getAbsolutePath()
                     + ") to output directory ("
                     + current.outputDirectory
                     + ") failed. Cause: "
                     + e.getMessage()
               );
            }
         }
      }

      return new ReportData(language, map, filename);
   }

   private FileName createFileName(TokenUserStructure tokenUserStructure, PlainTokenList list, TanBatchTask.ConfigBean current) {
      return FileName.builder()
         .prefix(current.fileNamePrefix)
         .element(tokenUserStructure.getUid())
         .element(list.getIdentification())
         .suffix(current.fileNameSuffix)
         .build();
   }

   private boolean numberEquals(Number number1, Number number2) {
      if (number1 == null && number2 == null) {
         return true;
      } else {
         return number1 != null && number2 != null ? Long.valueOf(number1.longValue()).equals(number2.longValue()) : false;
      }
   }

   private boolean hasListExpired(HashedTokenList list, TanBatchTask.ConfigBean current) {
      if (current.maximumValidityDaysMillis == -1L) {
         return false;
      }

      assert list != null;
      long listGenerationDate = list.getGenerationDate().getTime();
      long expiryDate = listGenerationDate + current.maximumValidityDaysMillis;
      return expiryDate < System.currentTimeMillis();
   }

   @Override
   public Task getLogic() {
      return this;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static TanBatchTask.TanBatchTaskBuilder builder() {
      return new TanBatchTask.TanBatchTaskBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public TanBatchTask() {
   }

   @SuppressFBWarnings("UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR")
   public static class ConfigBean {
      private long deliverySecurityGapMillis;
      private long maximumValidityDaysMillis = -1L;
      private long remainingValidityMillisUntilOrderNewList = -1L;
      private TokenListGenerator tokenListGenerator;
      private String fileNamePrefix = "";
      private String fileNameSuffix;
      private File outputDirectory;
      private File workingDirectory;
      private String languageAttributeName;
      private boolean deleteOldTokenLists;
      private PasswordHash hashFunctionPlugin;
      private TokenListRenderer tokenListRenderer;
      private TokenListIterator tokenListIterator;
      private TokenListPersister tokenListPersister;
      private Integer maxNoOfCardsToPrintPerDay;
      private TanBatchTask.ShiftDirection shiftDirection;

      public File getOutputDirectory() {
         return this.outputDirectory;
      }

      public File getWorkingDirectory() {
         return this.workingDirectory;
      }

      @VisibleForTesting
      public void setMaximumValidityDaysMillis(long maximumValidityDaysMillis) {
         this.maximumValidityDaysMillis = maximumValidityDaysMillis;
      }

      @VisibleForTesting
      public void setMaxNoOfCardsToPrintPerDay(Integer maxNoOfCardsToPrintPerDay) {
         this.maxNoOfCardsToPrintPerDay = maxNoOfCardsToPrintPerDay;
      }

      @VisibleForTesting
      public void setTokenListPersister(TokenListPersister tokenListPersister) {
         this.tokenListPersister = tokenListPersister;
      }

      @VisibleForTesting
      public void setShiftDirection(TanBatchTask.ShiftDirection shiftDirection) {
         this.shiftDirection = shiftDirection;
      }
   }

   private static final class ReportDataFilenameFilter implements FilenameFilter {
      private final String uid;
      private final TanBatchTask.ConfigBean current;
      private final File localOutputDirectory;

      private ReportDataFilenameFilter(String uid, TanBatchTask.ConfigBean current, File localOutputDirectory) {
         this.uid = uid;
         this.current = current;
         this.localOutputDirectory = localOutputDirectory;
      }

      @Override
      public boolean accept(File dir, String name) {
         FileName fileNameBase = FileName.builder().prefix(this.current.fileNamePrefix).element(this.uid).build().append("-");
         return dir.equals(this.localOutputDirectory) && name.startsWith(fileNameBase.value()) && name.endsWith(this.current.fileNameSuffix);
      }
   }

   enum ShiftDirection {
      FUTURE,
      PAST;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class TanBatchTaskBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private TokenListPersister tokenListPersister;
      @SuppressFBWarnings(justification = "generated code")
      private TokenListIterator tokenListIterator;
      @SuppressFBWarnings(justification = "generated code")
      private Integer maximumValidityDays;
      @SuppressFBWarnings(justification = "generated code")
      private Integer remainingDaysThreshold;
      @SuppressFBWarnings(justification = "generated code")
      private Integer deliverySecurityGap;
      @SuppressFBWarnings(justification = "generated code")
      private String tokenTypeName;
      @SuppressFBWarnings(justification = "generated code")
      private Integer tokenLength;
      @SuppressFBWarnings(justification = "generated code")
      private Integer tokensPerList;
      @SuppressFBWarnings(justification = "generated code")
      private PasswordHash hashFunctionPlugin;
      @SuppressFBWarnings(justification = "generated code")
      private TokenListRenderer tokenListRenderer;
      @SuppressFBWarnings(justification = "generated code")
      private String languageAttributeName;
      @SuppressFBWarnings(justification = "generated code")
      private boolean deleteOldTokenLists;
      @SuppressFBWarnings(justification = "generated code")
      private File workingDirectory;
      @SuppressFBWarnings(justification = "generated code")
      private File outputDirectory;
      @SuppressFBWarnings(justification = "generated code")
      private String fileNamePrefix;
      @SuppressFBWarnings(justification = "generated code")
      private String fileNameSuffix;
      @SuppressFBWarnings(justification = "generated code")
      private String generationDateExportProperty;
      @SuppressFBWarnings(justification = "generated code")
      private String validityDateExportProperty;
      @SuppressFBWarnings(justification = "generated code")
      private String serialNumberExportProperty;
      @SuppressFBWarnings(justification = "generated code")
      private String remainingTokensExportProperty;
      @SuppressFBWarnings(justification = "generated code")
      private AggregateReport aggregateReport;
      @SuppressFBWarnings(justification = "generated code")
      private Integer maxNoOfCardsToPrintPerDay;
      @SuppressFBWarnings(justification = "generated code")
      private TanBatchTask.ShiftDirection shiftDirection;

      @SuppressFBWarnings(justification = "generated code")
      TanBatchTaskBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder tokenListPersister(TokenListPersister tokenListPersister) {
         this.tokenListPersister = tokenListPersister;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder tokenListIterator(TokenListIterator tokenListIterator) {
         this.tokenListIterator = tokenListIterator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder maximumValidityDays(Integer maximumValidityDays) {
         this.maximumValidityDays = maximumValidityDays;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder remainingDaysThreshold(Integer remainingDaysThreshold) {
         this.remainingDaysThreshold = remainingDaysThreshold;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder deliverySecurityGap(Integer deliverySecurityGap) {
         this.deliverySecurityGap = deliverySecurityGap;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder tokenTypeName(String tokenTypeName) {
         this.tokenTypeName = tokenTypeName;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder tokenLength(Integer tokenLength) {
         this.tokenLength = tokenLength;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder tokensPerList(Integer tokensPerList) {
         this.tokensPerList = tokensPerList;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder hashFunctionPlugin(PasswordHash hashFunctionPlugin) {
         this.hashFunctionPlugin = hashFunctionPlugin;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder tokenListRenderer(TokenListRenderer tokenListRenderer) {
         this.tokenListRenderer = tokenListRenderer;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder languageAttributeName(String languageAttributeName) {
         this.languageAttributeName = languageAttributeName;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder deleteOldTokenLists(boolean deleteOldTokenLists) {
         this.deleteOldTokenLists = deleteOldTokenLists;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder workingDirectory(File workingDirectory) {
         this.workingDirectory = workingDirectory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder outputDirectory(File outputDirectory) {
         this.outputDirectory = outputDirectory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder fileNamePrefix(String fileNamePrefix) {
         this.fileNamePrefix = fileNamePrefix;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder fileNameSuffix(String fileNameSuffix) {
         this.fileNameSuffix = fileNameSuffix;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder generationDateExportProperty(String generationDateExportProperty) {
         this.generationDateExportProperty = generationDateExportProperty;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder validityDateExportProperty(String validityDateExportProperty) {
         this.validityDateExportProperty = validityDateExportProperty;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder serialNumberExportProperty(String serialNumberExportProperty) {
         this.serialNumberExportProperty = serialNumberExportProperty;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder remainingTokensExportProperty(String remainingTokensExportProperty) {
         this.remainingTokensExportProperty = remainingTokensExportProperty;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder aggregateReport(AggregateReport aggregateReport) {
         this.aggregateReport = aggregateReport;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder maxNoOfCardsToPrintPerDay(Integer maxNoOfCardsToPrintPerDay) {
         this.maxNoOfCardsToPrintPerDay = maxNoOfCardsToPrintPerDay;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask.TanBatchTaskBuilder shiftDirection(TanBatchTask.ShiftDirection shiftDirection) {
         this.shiftDirection = shiftDirection;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TanBatchTask build() {
         return new TanBatchTask(
            this.tokenListPersister,
            this.tokenListIterator,
            this.maximumValidityDays,
            this.remainingDaysThreshold,
            this.deliverySecurityGap,
            this.tokenTypeName,
            this.tokenLength,
            this.tokensPerList,
            this.hashFunctionPlugin,
            this.tokenListRenderer,
            this.languageAttributeName,
            this.deleteOldTokenLists,
            this.workingDirectory,
            this.outputDirectory,
            this.fileNamePrefix,
            this.fileNameSuffix,
            this.generationDateExportProperty,
            this.validityDateExportProperty,
            this.serialNumberExportProperty,
            this.remainingTokensExportProperty,
            this.aggregateReport,
            this.maxNoOfCardsToPrintPerDay,
            this.shiftDirection
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "TanBatchTask.TanBatchTaskBuilder(tokenListPersister="
            + this.tokenListPersister
            + ", tokenListIterator="
            + this.tokenListIterator
            + ", maximumValidityDays="
            + this.maximumValidityDays
            + ", remainingDaysThreshold="
            + this.remainingDaysThreshold
            + ", deliverySecurityGap="
            + this.deliverySecurityGap
            + ", tokenTypeName="
            + this.tokenTypeName
            + ", tokenLength="
            + this.tokenLength
            + ", tokensPerList="
            + this.tokensPerList
            + ", hashFunctionPlugin="
            + this.hashFunctionPlugin
            + ", tokenListRenderer="
            + this.tokenListRenderer
            + ", languageAttributeName="
            + this.languageAttributeName
            + ", deleteOldTokenLists="
            + this.deleteOldTokenLists
            + ", workingDirectory="
            + this.workingDirectory
            + ", outputDirectory="
            + this.outputDirectory
            + ", fileNamePrefix="
            + this.fileNamePrefix
            + ", fileNameSuffix="
            + this.fileNameSuffix
            + ", generationDateExportProperty="
            + this.generationDateExportProperty
            + ", validityDateExportProperty="
            + this.validityDateExportProperty
            + ", serialNumberExportProperty="
            + this.serialNumberExportProperty
            + ", remainingTokensExportProperty="
            + this.remainingTokensExportProperty
            + ", aggregateReport="
            + this.aggregateReport
            + ", maxNoOfCardsToPrintPerDay="
            + this.maxNoOfCardsToPrintPerDay
            + ", shiftDirection="
            + this.shiftDirection
            + ")";
      }
   }
}
