Terms of Service | Privacy Policy | Cookie Policy

Verified Commit e5b7d62f authored by Uwe Plonus's avatar Uwe Plonus
Browse files

Documented and refactored code generator package

parent f1d61a1c
Loading
Loading
Loading
Loading
Loading
+132 −0
Original line number Diff line number Diff line
/*
 * 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.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.sw4j.tool.barcode.random.codedata.CodeData;
import org.sw4j.tool.barcode.random.config.CodeConfig;
import org.sw4j.tool.barcode.random.config.CodeType;

/**
 * <p>
 * This is a {@link java.util.function.Consumer Consumer} for
 * {@link org.sw4j.tool.barcode.random.generator.RandomIdent RandomIdent} which generates one barcode for a
 * {@code RandomIdent}.
 * </p>
 * <p>
 * This class is thread safe, as long as no barcodes for the same ident are generated.
 * </p>
 * @author Uwe Plonus &lt;u.plonus@gmail.com&gt;
 */
public class BarcodeWriter implements Consumer<RandomIdent> {

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

    /**
     * <p>
     * The configuration for the barcode that should me generated.
     * </p>
     */
    private final CodeConfig codeConfig;

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

    /**
     * <p>
     * Create a new instance with the given barcode config and output factories.
     * </p>
     * @param codeConfig the configuration for the barcode to generate.
     * @param codeData the factories for the output files.
     */
    public BarcodeWriter(final CodeConfig codeConfig, final CodeData codeData) {
        this.codeConfig = codeConfig;
        this.codeData = codeData;
    }

    /**
     * <p>
     * Generate the barcode for the given ident and configured barcode.
     * </p>
     * @param randomIdent the ident (and its random numbers) for output.
     */
    @Override
    public void accept(final RandomIdent randomIdent) {
        CodeType codeType = codeConfig.getType();
        String codeEncoding = codeConfig.getEncoding();
        String codeUrl = codeConfig.getUrl();
        codeUrl = codeUrl.replace("{code}", randomIdent.getEncoded(codeEncoding));
        try {
            OutputStream os = codeData.getOutputForIdent(codeType, codeEncoding, randomIdent.getIdent(),
                    codeConfig.getFiletype());
            MultiFormatWriter codeWriter = new MultiFormatWriter();
            Map<EncodeHintType, Object> encodingParameters = new HashMap<>();
            encodingParameters.put(EncodeHintType.CHARACTER_SET, "utf-8");
            setErrorCorrection(encodingParameters);
            BitMatrix matrix = codeWriter.encode(codeUrl, codeType.getFormat(), codeConfig.getWidth(),
                    codeConfig.getHeight());
            MatrixToImageWriter.writeToStream(matrix, codeConfig.getFiletype(), os);
        } catch (IOException | WriterException exc) {
            logger.log(Level.WARNING,
                    String.format("Writing of Code 'type: %s / encoding: %s / ident: %s' failed.",
                            codeType.getType(), codeEncoding, randomIdent.getIdent()), exc);
        }
    }

    /**
     * <p>
     * Configure the barcode error correction depending on the bacode type.
     * </p>
     * @param encodingParameters the encoding parameters where the error correction should be set in.
     */
    private void setErrorCorrection(final Map<EncodeHintType, Object> encodingParameters) {
        switch (codeConfig.getType()) {
            case QRCODE:
                try {
                    encodingParameters.put(EncodeHintType.ERROR_CORRECTION,
                            ErrorCorrectionLevel.valueOf(codeConfig.getErrorCorrection()));
                } catch (IllegalArgumentException | NullPointerException exc) {
                    encodingParameters.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
                }
                break;
            default:
                logger.warning("This point should never be reached.");
        }
    }

}
+92 −135
Original line number Diff line number Diff line
@@ -20,17 +20,8 @@ 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 com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
@@ -40,48 +31,97 @@ import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.sw4j.tool.barcode.random.codedata.CodeData;
import org.sw4j.tool.barcode.random.config.RandomConfig;
import org.sw4j.tool.barcode.random.encoder.ByteArrayEncoder;
import org.sw4j.tool.barcode.random.input.Identifier;
import org.sw4j.tool.barcode.random.codedata.CodeData;
import org.sw4j.tool.barcode.random.config.CodeConfig;
import org.sw4j.tool.barcode.random.config.CodeType;

/**
 *
 * <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 {

    private static final Logger logger = Logger.getLogger(CodeGenerator.class.getName());
    /**
     * <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 RandomConfig 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;

    public CodeGenerator(RandomConfig config, CodeData codeData) {
    /**
     * <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 RandomConfig config, final CodeData codeData) {
        this(config, codeData, new SecureRandom());
    }

    public CodeGenerator(RandomConfig config, CodeData codeData, 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 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 RandomConfig config, final CodeData codeData, final Random random) {
        this.config = config;
        this.codeData = codeData;
        this.random = random;
    }

    /**
     *
     * @throws IOException
     * @throws FileNotFoundException
     * @throws IllegalArgumentException
     * <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<String> encodings = new HashSet<>();
@@ -103,31 +143,9 @@ public class CodeGenerator {
                        String.format("Duplicated input value '%s' found", ident.getValue()));
            }
        }
        Set<RandomIdent> randomValues = createCodes(inputValues, encodings);
        randomValues.parallelStream()
                .forEach(randomIdent -> {
                    config.getCodes().forEach(code -> {
                        CodeType codeType = code.getType();
                        String codeEncoding = code.getEncoding();
                        String codeUrl = code.getUrl();
                        codeUrl = codeUrl.replace("{code}", randomIdent.getEncoded(codeEncoding));
                        try {
                            OutputStream os = codeData.getOutputForIdent(codeType, codeEncoding,
                                    randomIdent.getIdent(), code.getFiletype());
                            MultiFormatWriter codeWriter = new MultiFormatWriter();
                            Map<EncodeHintType, Object> encodingParameters = new HashMap<>();
                            encodingParameters.put(EncodeHintType.CHARACTER_SET, "utf-8");
                            setErrorCorrection(code, encodingParameters);
                            BitMatrix matrix = codeWriter.encode(codeUrl, codeType.getFormat(),
                                    code.getWidth(), code.getHeight());
                            MatrixToImageWriter.writeToStream(matrix, code.getFiletype(), os);
                        } catch (IOException | WriterException exc) {
                            logger.log(Level.WARNING,
                                    String.format("Writing of Code 'type: %s / encoding: %s / ident: %s' failed.",
                                            codeType.getType(), codeEncoding, randomIdent.getIdent()),
                                    exc);
                        }
                    });
        final Set<RandomIdent> randomValues = createCodes(inputValues, encodings);
        config.getCodes().forEach(codeConfig -> {
            randomValues.parallelStream().forEach(new BarcodeWriter(codeConfig, codeData));
        });
        CsvSchema.Builder schemaBuilder = CsvSchema.builder()
                .addColumn("ident");
@@ -150,22 +168,20 @@ public class CodeGenerator {
        }
    }

    private void setErrorCorrection(CodeConfig codeConfig, Map<EncodeHintType, Object> encodingParameters) {
        switch (codeConfig.getType()) {
            case QRCODE:
                try {
                    encodingParameters.put(EncodeHintType.ERROR_CORRECTION,
                            ErrorCorrectionLevel.valueOf(codeConfig.getErrorCorrection()));
                } catch (IllegalArgumentException | NullPointerException exc) {
                    encodingParameters.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
                }
            default:
                // Do nothing
        }
        encodingParameters.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
    }

    public Set<RandomIdent> createCodes(Collection<String> inputValues, Set<String> encodings) {
    /**
     * <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<RandomIdent> createCodes(final Collection<String> inputValues, final Set<String> encodings) {
        Map<String, 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
@@ -183,7 +199,16 @@ public class CodeGenerator {
                .collect(Collectors.toSet());
    }

    private boolean encodedRandomExists(RandomIdent randomIdent, Map<String, Set<String>> encodedRandoms) {
    /**
     * <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<String, Set<String>> encodedRandoms) {
        boolean valueExists = false;
        for (Map.Entry<String, Set<String>> entry: encodedRandoms.entrySet()) {
            valueExists |= entry.getValue().contains(randomIdent.getEncoded(entry.getKey()));
@@ -191,72 +216,4 @@ public class CodeGenerator {
        return valueExists;
    }


    public static class RandomIdent {

        private final String ident;

        private final byte[] random;

        private final Map<String, String> encoded;

        public RandomIdent(String ident, int randomSize, Random rng, Set<String> encodings) {
            this.ident = ident;
            random = new byte[randomSize / 8];
            rng.nextBytes(random);
            encoded = new HashMap<>();
            encodings.forEach((encoding) -> {
                String rawEncoding = encoding;
                int endIndex = -1;
                int startIndex = -1;
                boolean hasMinus = false;
                if (rawEncoding.matches(".+\\{\\d*-?\\d+\\}")) {
                    Pattern p = Pattern.compile("(.+)\\{((\\d*)-)?(\\d+)\\}");
                    Matcher m = p.matcher(rawEncoding);
                    String startIndexGroup = null;
                    String minusGroup = null;
                    m.matches();
                    rawEncoding = m.group(1);
                    if (m.group(2) != null && m.group(2).length() > 1) {
                        startIndex = Integer.parseInt(m.group(2).substring(0, m.group(2).length() - 1));
                    }
                    startIndexGroup = m.group(2);
                    hasMinus = m.group(3) != null;
                    endIndex = Integer.parseInt(m.group(4));
                }
                ByteArrayEncoder encoder = ByteArrayEncoder.forEncoding(rawEncoding);
                if (encoder == null) {
                    throw new IllegalArgumentException(
                            String.format("Cannot find an encoder for encoding %s", rawEncoding));
                }
                String encodedValue = encoder.encode(random);
                if (endIndex > 0) {
                    if (hasMinus) {
                        if (startIndex >= 0) {
                            encodedValue = encodedValue.substring(startIndex, endIndex);
                        } else {
                            encodedValue = encodedValue.substring(encodedValue.length() - endIndex);
                        }
                    } else {
                        encodedValue = encodedValue.substring(0, endIndex);
                    }
                }
                encoded.put(encoding, encodedValue);
            });
        }

        public String getIdent() {
            return ident;
        }

        public byte[] getRandom() {
            return Arrays.copyOf(random, random.length);
        }

        public String getEncoded(String encoding) {
            return encoded.get(encoding);
        }

    }

}
+151 −0
Original line number Diff line number Diff line
/*
 * 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 java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.sw4j.tool.barcode.random.encoder.ByteArrayEncoder;

/**
 * <p>
 * This class contains the random number and the encoded representations for a single ident.
 * </p>
 * <p>
 * This class is immutable.
 * </p>
 * @author Uwe Plonus &lt;u.plonus@gmail.com&gt;
 */
public class RandomIdent {

    /**
     * <p>
     * The ident number.
     * </p>
     */
    private final String ident;

    /**
     * <p>
     * The generated random number.
     * </p>
     */
    private final byte[] random;

    /**
     * <p>
     * A map containing all encoded values for the ident. The key is the encoding and the value the encoded value.
     * </p>
     */
    private final Map<String, String> encoded;

    /**
     * <p>
     * Create a new {@code RandomIdent} and the random number for the {@qcode ident}. The random number has a size of
     * {@code randomSize} bits (rounded down to the next whole byte). For the generation of the random number the given
     * {@code rng} will be used. The generated random number will be encoded in all given {@code encodings}.
     * </p>
     * <p>
     * For high quality random numbers use an appropriate random number generator
     * (e.g. {@link java.security.SecureRandom SecureRandom}).
     * </p>
     * @param ident the ident for which the random number will be generated for.
     * @param randomSize the size in bits (rounded down to the next byte).
     * @param rng the random number generator to use.
     * @param encodings the encodings that are used for the encoded representation of the random number.
     * @throws IllegalArgumentException if no encoder for a given encoding can be found.
     */
    public RandomIdent(final String ident, final int randomSize, final Random rng, final Set<String> encodings) {
        this.ident = ident;
        random = new byte[randomSize / 8];
        rng.nextBytes(random);
        encoded = new HashMap<>();
        encodings.forEach((encoding) -> {
            String rawEncoding = encoding;
            int endIndex = -1;
            int startIndex = -1;
            boolean hasMinus = false;
            if (rawEncoding.matches(".+\\{\\d*-?\\d+\\}")) {
                Pattern p = Pattern.compile("(.+)\\{((\\d*)-)?(\\d+)\\}");
                Matcher m = p.matcher(rawEncoding);
                m.matches();
                rawEncoding = m.group(1);
                if (m.group(2) != null && m.group(2).length() > 1) {
                    startIndex = Integer.parseInt(m.group(2).substring(0, m.group(2).length() - 1));
                }
                String startIndexGroup = m.group(2);
                hasMinus = m.group(3) != null;
                endIndex = Integer.parseInt(m.group(4));
            }
            ByteArrayEncoder encoder = ByteArrayEncoder.forEncoding(rawEncoding);
            if (encoder == null) {
                throw new IllegalArgumentException(
                        String.format("Cannot find an encoder for encoding %s", rawEncoding));
            }
            String encodedValue = encoder.encode(random);
            if (endIndex > 0) {
                if (hasMinus) {
                    if (startIndex >= 0) {
                        encodedValue = encodedValue.substring(startIndex, endIndex);
                    } else {
                        encodedValue = encodedValue.substring(encodedValue.length() - endIndex);
                    }
                } else {
                    encodedValue = encodedValue.substring(0, endIndex);
                }
            }
            encoded.put(encoding, encodedValue);
        });
    }

    /**
     * <p>
     * Return the ident number.
     * </p>
     * @return the ident number.
     */
    public String getIdent() {
        return ident;
    }

    /**
     * <p>
     * Return the raw random number generated.
     * </p>
     * @return the raw random number.
     */
    public byte[] getRandom() {
        return Arrays.copyOf(random, random.length);
    }

    /**
     * <p>
     * Return the random number in the given {@code encoding}. Only the encodings that were given during construction
     * can be returned.
     * </p>
     * @param encoding the encoding for which the encoded value should be returned.
     * @return the encoded value or {@code null} if the encoding is unknown.
     */
    public String getEncoded(final String encoding) {
        return encoded.get(encoding);
    }

}
+23 −0
Original line number Diff line number Diff line
/*
 * 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/>.
 */

/**
 * <p>
 * This package contains the generator of the barcodes and encoded random numbers.
 * </p>
 */
package org.sw4j.tool.barcode.random.generator;
+1 −1
Original line number Diff line number Diff line
@@ -33,7 +33,7 @@ import javax.xml.bind.DatatypeConverter;
import org.sw4j.tool.barcode.random.codedata.CodeData;
import org.sw4j.tool.barcode.random.config.CodeConfig;
import org.sw4j.tool.barcode.random.config.RandomConfig;
import org.sw4j.tool.barcode.random.generator.CodeGenerator.RandomIdent;
import org.sw4j.tool.barcode.random.generator.RandomIdent;
import org.sw4j.tool.barcode.random.support.PredictableRandom;
import org.sw4j.tool.barcode.random.support.TestCodeData;
import static org.testng.Assert.*;