CodeGenerator.java

/*
 * Copyright (C) 2019 sw4j.org
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.sw4j.tool.barcode.random.generator;

import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.SequenceWriter;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.sw4j.tool.barcode.random.codedata.CodeData;
import org.sw4j.tool.barcode.random.config.EncodingConfig;
import org.sw4j.tool.barcode.random.config.GenerationConfig;
import org.sw4j.tool.barcode.random.input.Identifier;
import org.sw4j.tool.barcode.random.input.Predefined;

/**
 * <p>
 * This class takes the configuration (from the package {@link org.sw4j.tool.barcode.random.config}) and uses this
 * configuration to generate encoded random numbers and barcodes.
 * </p>
 * <p>
 * This class is not thread safe.
 * </p>
 * @author Uwe Plonus &lt;u.plonus@gmail.com&gt;
 */
public class CodeGenerator {

    /**
     * <p>
     * The logger of this class.
     * </p>
     */
    private final Logger logger = Logger.getLogger(CodeGenerator.class.getName());

    /**
     * <p>
     * The configuration data for the random number generation and encoding.
     * </p>
     */
    private final GenerationConfig config;

    /**
     * <p>
     * The factories for the input and output stream.
     * </p>
     */
    private final CodeData codeData;

    /**
     * <p>
     * The random number generator used.
     * </p>
     */
    private final Random random;

    /**
     * <p>
     * The constructor for a new {@code CodeGenerator}. The code generator is initialized with the random configuration
     * and a factory for the input and output streams.
     * </p>
     * <p>
     * The code generator is initialized with a {@link java.security.SecureRandom SecureRandom} random number generator.
     * </p>
     * @param config the random number configuration.
     * @param codeData the factories for the input and output streams.
     */
    public CodeGenerator(final GenerationConfig config, final CodeData codeData) {
        this(config, codeData, new SecureRandom());
    }

    /**
     * <p>
     * The constructor for a new {@code CodeGenerator}. The code generator is initialized with the random configuration
     * and a factory for the input and output streams.
     * </p>
     * <p>
     * The code generator uses the random number generator supplied during construction.
     * </p>
     * <p>
     * <em>Attention:</em> if you do not know the security issues involved with using your own random number generator
     * please use the constructor {@link #CodeGenerator(RandomConfig,CodeData)} which creates a secure random number
     * generator for you.
     * </p>
     * @param config the random number configuration.
     * @param codeData the factories for the input and output streams.
     * @param random the random number generator to use.
     */
    public CodeGenerator(final GenerationConfig config, final CodeData codeData, final Random random) {
        this.config = config;
        this.codeData = codeData;
        this.random = random;
    }

    /**
     * <p>
     * Create the random numbers (and encoded representations) for the idents and writes the codes to the output csv
     * file.
     * </p>
     * @throws IOException if the reading or writing of the data fails.
     * @throws IllegalArgumentException if a duplicate ident is found in the input.
     */
    public void createCodes() throws IOException {
        Set<EncodingConfig> encodings = new HashSet<>();
        config.getCodes().forEach((codeConfig) -> {
            encodings.add(codeConfig.getEncoding());
        });
        config.getEncodings().forEach((encoding) -> {
            encodings.add(encoding);
        });

        CsvMapper csvMapper = new CsvMapper();
        Set<IdentValue> values;

        if (config.getEncoding() != null) {
            Map<String, String> inputValues = new LinkedHashMap<>();
            MappingIterator<Predefined> mappingIterator = csvMapper.readerWithSchemaFor(Predefined.class)
                    .readValues(codeData.getInput());
            while (mappingIterator.hasNext()) {
                Predefined ident = mappingIterator.next();
                inputValues.put(ident.getIdent(), ident.getCode());
            }
            values = convertCodes(inputValues, config.getEncoding(), encodings);
        } else {
            Set<String> inputValues = new LinkedHashSet<>();
            MappingIterator<Identifier> mappingIterator = csvMapper.readerWithSchemaFor(Identifier.class)
                    .readValues(codeData.getInput());
            while (mappingIterator.hasNext()) {
                Identifier ident = mappingIterator.next();
                if (!inputValues.add(ident.getIdent())) {
                    throw new IllegalArgumentException(
                            String.format("Duplicated input value '%s' found", ident.getIdent()));
                }
            }
            values = createCodes(inputValues, encodings);
        }

        config.getCodes().forEach(codeConfig -> {
            values.parallelStream().forEach(new BarcodeWriter(codeConfig, codeData));
        });
        CsvSchema.Builder schemaBuilder = CsvSchema.builder().addColumn("ident");
        // TODO Check column header
        encodings.forEach(encoding -> schemaBuilder.addColumn(encoding.toString()));
        CsvSchema schema = schemaBuilder.build().withHeader();
        CsvMapper mapper = new CsvMapper();
        List<Map<String, String>> outData = new LinkedList<>();
        values.stream().map((value) -> {
            Map<String, String> dataValues = new HashMap<>();
            dataValues.put("ident", value.getIdent());
            encodings.forEach((encoding) -> {
                dataValues.put(encoding.toString(), value.getEncoded(encoding));
            });
            return dataValues;
        }).forEachOrdered((dataValues) -> {
            outData.add(dataValues);
        });
        try (SequenceWriter writer = mapper.writer(schema).writeValues(codeData.getOutput())) {
            writer.writeAll(outData);
        }
    }

    /**
     * <p>
     * Create the encoded representation for all {@code inputValues}.
     * </p>
     * <p>
     * This method is thread safe.
     * </p>
     * @param inputValues the inputValues (idents) for which the random numbers should be created.
     * @param encoding the encoding of the source value.
     * @param encodings the encodings that should be created for the random numbers.
     * @return a set of {@link org.sw4j.tool.barcode.random.generator.RandomIdent RandomIdent} instances with the random
     *   numbers and the encoded random numbers.
     */
    public Set<IdentValue> convertCodes(final Map<String, String> inputValues, final EncodingConfig encoding,
            final Set<EncodingConfig> encodings) {
        Map<EncodingConfig, Set<String>> encoded = new HashMap<>();
        encodings.forEach(enc -> encoded.put(enc, new HashSet<>()));
        return inputValues.keySet().stream()
                .map(ident -> {
                    PredefinedIdent pi = new PredefinedIdent(ident, inputValues.get(ident), encoding, encodings);
                    for (EncodingConfig enc: encodings) {
                        encoded.get(enc).add(pi.getEncoded(enc));
                    }
                    return pi;
                })
                .collect(Collectors.toSet());
    }

    /**
     * <p>
     * Create random values for all {@code inputValues}. Additionally the encoded representation for the random values
     * are created.
     * </p>
     * <p>
     * This method is thread safe.
     * </p>
     * @param inputValues the inputValues (idents) for which the random numbers should be created.
     * @param encodings the encodings that should be created for the random numbers.
     * @return a set of {@link org.sw4j.tool.barcode.random.generator.RandomIdent RandomIdent} instances with the random
     *   numbers and the encoded random numbers.
     */
    public Set<IdentValue> createCodes(final Collection<String> inputValues, final Set<EncodingConfig> encodings) {
        Map<EncodingConfig, Set<String>> encodedRandoms = new HashMap<>();
        encodings.forEach(encoding -> encodedRandoms.put(encoding, new HashSet<>()));
        // The following stream may not be a parallel stream, because we check there for duplicates
        return inputValues.stream()
                .map(ident -> {
                    RandomIdent ri = new RandomIdent(ident, config.getSize(), random, encodings);
                    while (encodedRandomExists(ri, encodedRandoms)) {
                        ri = new RandomIdent(ident, config.getSize(), random, encodings);
                    }
                    for (EncodingConfig encoding: encodings) {
                        encodedRandoms.get(encoding).add(ri.getEncoded(encoding));
                    }
                    return ri;
                })
                .collect(Collectors.toSet());
    }

    /**
     * <p>
     * Check is the random number of the given {@code randomIdent} already exists in the encoded randoms. This checks
     * for each encoding is the representation already exists.
     * </p>
     * @param randomIdent the random ident to check.
     * @param encodedRandoms a map with the encoding as key and the already generated encoded random numbers as values.
     * @return {@code true} if the given random ident already exists.
     */
    private boolean encodedRandomExists(final RandomIdent randomIdent,
            final Map<EncodingConfig, Set<String>> encodedRandoms) {
        boolean valueExists = false;
        for (Map.Entry<EncodingConfig, Set<String>> entry: encodedRandoms.entrySet()) {
            valueExists |= entry.getValue().contains(randomIdent.getEncoded(entry.getKey()));
        }
        return valueExists;
    }

}