Thursday, August 3, 2017

Generate Unique Code for Coupons or Vouchers in java

Please see the Maven Dependency
  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <scope>test</scope>
  </dependency>
  <dependency>
   <groupId>commons-lang</groupId>
   <artifactId>commons-lang</artifactId>
  </dependency>



import java.math.BigInteger;

public class Coupon {

    private byte numberOfChar;

    private BigInteger code;

    private BigInteger crc;

    public byte getNumberOfChar() {
        return numberOfChar;
    }

    public void setNumberOfChar(byte numberOfChar) {
        this.numberOfChar = numberOfChar;
    }

    public BigInteger getCode() {
        return code;
    }

    public void setCode(BigInteger code) {
        this.code = code;
    }

    public BigInteger getCrc() {
        return crc;
    }

    public void setCrc(BigInteger crc) {
        this.crc = crc;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Coupon)) {
            return false;
        }

        Coupon c = (Coupon) o;
        if (code.equals(c.code)) {
            return true;
        }
        return false;

    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 89 * hash + (code != null ? code.hashCode() : 0);
        hash = 89 * hash + (crc != null ? crc.hashCode() : 0);
        return hash;
    }

}




public class CouponGenerateError extends Exception {

    public CouponGenerateError(String message) {
        super(message);
    }

    private static final long serialVersionUID = -8232285755941178115L;

}




import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.LinkedHashSet;
import java.util.Set;

public class CouponGenerator {

    private SecureRandom random = new SecureRandom();

    /**
     * Represent of number of CRC chracter. If it is 1. One Character of CRC code will be generated and attached next to
     * coupon code
     */
    static int CRC_SIZE = 1;

    /**
     * CRC Generator should never be changed unless you like to invalidate all previously published coupons. It is a key
     * value to prove if the CRC value is right or not
     */
    static int CRC_GENERATOR = 31;

    /**
     * @param numberOfChar
     *            is define how long is the Voucher code. Voucher code consists of coupon code and CRC code
     * @param numberOfCoupon
     *            is define how many Vouchers must be generated
     * @return
     * @throws CouponGenerateError
     */
    public Set getCouponSet(byte numberOfChar, long numberOfCoupon) throws CouponGenerateError {

        isValidNumberOfChar(numberOfChar);

        Set couponSet = new LinkedHashSet();

        while (numberOfCoupon > couponSet.size()) {
            couponSet.add(generateCoupon(numberOfChar));
        }

        return couponSet;
    }

    /**
     * @param numberOfChar
     *            It must be bigger than 2 and smaller than 12. If the parameter is 2, it will have 32 cases of coupon
     *            code. It is meaning less to generate. 
* If the parameter is bigger than 12. Internal long type variable can not calculate it. It will make a * byte overflow and return garbage value. Because, Long is 8 byte which is 64 bits. Each coupon chracter * takes 5 bit. 13 Chracters will take 65 bits * @throws CouponGenerateError */ private void isValidNumberOfChar(byte numberOfChar) throws CouponGenerateError { if (numberOfChar < 3 || numberOfChar > 12) { throw new CouponGenerateError( "Invalid numberOfChar for Coupon chracters. It must be bigger than 2 and smaller than 12"); } } /** * @param numberOfChar * length of Alphanumeric code value is defined by numberOfChar * @param numberOfChar * must be bigger than 3 or equal and smaller than 12 or equal chracters * @return * @throws CouponGenerateError */ public Coupon getCoupon(byte numberOfChar) throws CouponGenerateError { isValidNumberOfChar(numberOfChar); return generateCoupon(numberOfChar); } private Coupon generateCoupon(byte numberOfChar) throws CouponGenerateError { Coupon coupon = new Coupon(); coupon.setNumberOfChar(numberOfChar); // Create 5 random bits per character. 5 bits represent Base32 Encoding coupon.setCode(new BigInteger((numberOfChar - CRC_SIZE) * 5, random)); coupon.setCrc(CouponUtil.calculateCrc(coupon)); return coupon; } }



import java.math.BigInteger;

import org.apache.commons.lang.StringUtils;

public class CouponUtil {

    /**
     * @param coupon
     *            
     * 7.  Base 32 Encoding with Extended Hex Alphabet
     * 
     *    The following description of base 32 is derived from [7].  This
     *    encoding may be referred to as "base32hex".  This encoding should not
     *    be regarded as the same as the "base32" encoding and should not be
     *    referred to as only "base32".  This encoding is used by, e.g.,
     *    NextSECure3 (NSEC3) [10].
     * 
     *    One property with this alphabet, which the base64 and base32
     *    alphabets lack, is that encoded data maintains its sort order when
     *    the encoded data is compared bit-wise.
     * 
     *    This encoding is identical to the previous one, except for the
     *    alphabet.  The new alphabet is found in Table 4.
     * 
     *                  Table 4: The "Extended Hex" Base 32 Alphabet
     * 
     *          Value Encoding  Value Encoding  Value Encoding  Value Encoding
     *              0 0             9 9            18 I            27 R
     *              1 1            10 A            19 J            28 S
     *              2 2            11 B            20 K            29 T
     *              3 3            12 C            21 L            30 U
     *              4 4            13 D            22 M            31 V
     *              5 5            14 E            23 N
     *              6 6            15 F            24 O         (pad) =
     *              7 7            16 G            25 P
     *              8 8            17 H            26 Q
     * 
     * @return
     */
    public static String getVoucherString(Coupon coupon) {

        byte numberOfChar = coupon.getNumberOfChar();

        String code = coupon.getCode().toString(32);

        if (!isCodeSizeRight(numberOfChar, code)) {
            code = leftPadding0(numberOfChar, code);
        }
        // Debug purpose
        // System.out.println("code : " + code);
        // System.out.println("bit code : " + coupon.getCode().toString(2));
        // System.out.println("CRC : " + coupon.getCrc().toString(32));
        return code + coupon.getCrc().toString(32);
    }

    private static String leftPadding0(byte numberOfChar, String code) {

        return StringUtils.repeat("0", numberOfChar - CouponGenerator.CRC_SIZE).substring(
                code.length() % numberOfChar)
                + code;

    }

    private static boolean isCodeSizeRight(byte numberOfChar, String code) {
        return code.length() % (numberOfChar - CouponGenerator.CRC_SIZE) == 0;
    }

    /**
     * It validate the CRC value from the coupon.
     * 
     * @param coupon
     * @return
     */
    static boolean isValidCrc(Coupon coupon) {

        if (coupon.getCrc().equals(calculateCrc(coupon))) {
            return true;
        } else {
            return false;
        }

    }

    /**
     * It cancluate CRC value and return it. coupon parameter must be set code value to calculate CRC value.
     * 
     * @param coupon
     * @return
     */
    static BigInteger calculateCrc(Coupon coupon) {

        BigInteger code = coupon.getCode();
        String crcValue = String.valueOf(code.longValue() % CouponGenerator.CRC_GENERATOR);
        // Debug purpose
        // System.out.println("code.toString(2) : " + code.toString(2));
        // System.out.println("code.longValue() : " + code.longValue());
        // System.out.println("crcValue : " + crcValue);
        return new BigInteger(crcValue, 10);
    }
}




import java.math.BigInteger;
import java.util.Set;

import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

public class CouponGeneratorTest {

    static CouponGenerator couponGenerator = new CouponGenerator();

    static Set couponSet;

    @BeforeClass
    public static void init() throws CouponGenerateError {

        couponSet = couponGenerator.getCouponSet((byte) 10, 1000);
    }

    /**
     * Test if the CouponGenerator generate requested number of coupons
     * 
     * @throws Exception
     */
    @Test
    public void testCouponCount() throws Exception {

        Assert.assertEquals("Coupon creation didn't match the requested number of coupon", 1000, couponSet.size());
    }

    /**
     * Test if the CodeGenerator generate exact size of vaucher code chracter size.
     * 
     * @throws Exception
     */
    @Test
    public void testCouponCharacterSize() throws Exception {

        for (Coupon coupon : couponSet) {

            // System.out.println(CouponUtil.getVoucherString(coupon));
            Assert.assertEquals("Coupon Characters didn't match the requested number of character", 10, CouponUtil
                    .getVoucherString(coupon).length());
        }

    }

    /**
     * Test if the generated CRC value passes validation check. 
* If it fails, one of generation or validation is wrong * * @throws Exception */ @Test public void testCrcCreation() throws Exception { for (Coupon coupon : couponSet) { Assert.assertTrue("Checksum value is not valid", CouponUtil.isValidCrc(coupon)); } } /** * Test if the given coupon passes CRC validation check. * * @throws Exception */ @Test public void testCrcValidation() throws Exception { Coupon coupon = new Coupon(); coupon.setCode(new BigInteger("54321", 32)); // CRC value is 1. ex ) 54321 (32 radix) % 31 = 5377089 (10 radix) % 31 = 15 (F) coupon.setCrc(new BigInteger("f", 32)); Assert.assertTrue("CRC value value is not valid", CouponUtil.isValidCrc(coupon)); } /** * Test if the given coupon fails CRC validation check. * * @throws Exception */ @Test public void testCrcValidation2() throws Exception { Coupon coupon = new Coupon(); coupon.setCode(new BigInteger("54321", 32)); // CRC value is 1. ex ) 54321 (32 radix) % 31 = 5377089 (10 radix) % 31 = 15 (F) coupon.setCrc(new BigInteger("2", 32)); Assert.assertFalse("CRC value must not be valid", CouponUtil.isValidCrc(coupon)); } /** * Test if the CouponGenerator accept requesting less than 3 chracters or bigger than 12 chracters. In this case, it * must throw Exception */ @Test public void testCouponMinusCharacterSize() { try { couponGenerator.getCoupon((byte) 2); Assert.fail("small charater size is not allowed"); } catch (CouponGenerateError e) { Assert.assertTrue(true); } try { couponGenerator.getCoupon((byte) -6); Assert.fail("Minus charater size is not allowed"); } catch (CouponGenerateError e) { Assert.assertTrue(true); } try { couponGenerator.getCoupon((byte) 13); Assert.fail("Bigger than 12 charater size is not allowed"); } catch (CouponGenerateError e) { Assert.assertTrue(true); } } }


3 comments: