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 <u.plonus@gmail.com>
*/
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;
}
}