Last active
June 18, 2020 14:06
-
-
Save rponte/c806d0c26cdb5b5b06d589afca5c9994 to your computer and use it in GitHub Desktop.
Just a simple implementation of a TLV encoder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package br.com.rponte.gist.util.tlv; | |
import static org.apache.commons.lang3.StringUtils.isBlank; | |
import static org.apache.commons.lang3.StringUtils.join; | |
import static org.apache.commons.lang3.StringUtils.leftPad; | |
import java.math.BigDecimal; | |
import java.util.LinkedHashMap; | |
import java.util.Map; | |
import hirondelle.date4j.DateTime; | |
/** | |
* Class responsible for encoding many data fields into TLV format | |
*/ | |
public class TLVEncoder { | |
/** | |
* Holds all fields keeping the their insertion order | |
*/ | |
private final Map<String, String> fields = new LinkedHashMap<>(); | |
/** | |
* Adds a <code>tagName</code> with a <code>value</code> of type | |
* <code>String</code> | |
* | |
* @param tagName the tag name | |
* @param value the string value | |
* @return returns this same instance | |
*/ | |
public TLVEncoder withField(String tagName, String value) { | |
addField(tagName, value); | |
return this; | |
} | |
/** | |
* Adds a <code>tagName</code> with a <code>value</code> of type | |
* <code>Integer</code> | |
* | |
* @param tagName the tag name | |
* @param value the integer value | |
* @return returns this same instance | |
*/ | |
public TLVEncoder withField(String tagName, Integer value) { | |
addField(tagName, value); | |
return this; | |
} | |
/** | |
* Adds a <code>tagName</code> with a <code>value</code> of type | |
* <code>Integer</code> and left pad the value with zeros | |
* | |
* @param tagName the tag name | |
* @param value the integer value | |
* @param leftPadding the size to pad to | |
* @return returns this same instance | |
*/ | |
public TLVEncoder withField(String tagName, Integer value, int leftPadding) { | |
addField(tagName, leftPaddingWithZeros(value, leftPadding)); | |
return this; | |
} | |
/** | |
* Adds a <code>tagName</code> with a <code>value</code> of type | |
* <code>Long</code> | |
* | |
* @param tagName the tag name | |
* @param value the long value | |
* @return returns this same instance | |
*/ | |
public TLVEncoder withField(String tagName, Long value) { | |
addField(tagName, value); | |
return this; | |
} | |
/** | |
* Adds a <code>tagName</code> with a <code>value</code> of type | |
* <code>Long</code> and left pad the value with zeros | |
* | |
* @param tagName the tag name | |
* @param value the long value | |
* @param leftPadding the size to pad to | |
* @return returns this same instance | |
*/ | |
public TLVEncoder withField(String tagName, Long value, int leftPadding) { | |
addField(tagName, leftPaddingWithZeros(value, leftPadding)); | |
return this; | |
} | |
/** | |
* Adds a <code>tagName</code> with a <code>value</code> of type | |
* <code>BigDecimal</code>. All non-numeric characters are removed, like '.' or | |
* ',' for example | |
* | |
* @param tagName the tag name | |
* @param value the bigdecimal value | |
* @return returns this same instance | |
*/ | |
public TLVEncoder withField(String tagName, BigDecimal value) { | |
addField(tagName, onlyNumbers(value)); | |
return this; | |
} | |
/** | |
* Adds a <code>tagName</code> with a <code>value</code> of type | |
* <code>BigDecimal</code> and left pad the value with zeros. All non numeric | |
* characters are removed, like '.' or ',' for example | |
* | |
* @param tagName the tag name | |
* @param value the bigdecimal value | |
* @param leftPadding the size to pad to | |
* @return returns this same instance | |
*/ | |
public TLVEncoder withField(String tagName, BigDecimal value, int leftPadding) { | |
String formatted = onlyNumbers(value); | |
formatted = leftPaddingWithZeros(formatted, leftPadding); | |
addField(tagName, formatted); | |
return this; | |
} | |
/** | |
* Adds a <code>tagName</code> with a <code>value</code> of type | |
* <code>DateTime</code> and formats the value with the pattern 'MMDDhhmmss' | |
* | |
* @param tagName the tag name | |
* @param value the datatime value | |
* @return returns this same instance | |
*/ | |
public TLVEncoder withField(String tagName, DateTime value) { | |
withField(tagName, value, "MMDDhhmmss"); | |
return this; | |
} | |
/** | |
* Adds a <code>tagName</code> with a <code>value</code> of type | |
* <code>DateTime</code> and formats the value with the specified | |
* <code>pattern</code> | |
* | |
* @param tagName the tag name | |
* @param value the datatime value | |
* @return returns this same instance | |
*/ | |
public TLVEncoder withField(String tagName, DateTime value, String pattern) { | |
String formatted = ""; | |
if (value != null) { | |
formatted = value.format(pattern); | |
} | |
addField(tagName, formatted); | |
return this; | |
} | |
/** | |
* Encodes all fields into a TLV text | |
*/ | |
public String encode() { | |
return join(fields.values(), ""); | |
} | |
@Override | |
public String toString() { | |
return "TLVEncoder [fields=" + fields + "]"; | |
} | |
/** | |
* Encodes <code>tagName</code> and its <code>value</code> into a TLV text | |
*/ | |
private void addField(String tagName, Object value) { | |
if (shouldIgnore(value)) return; | |
validate(tagName, value); | |
String strValue = value.toString(); | |
Integer length = strValue.length(); | |
String strLength = leftPad(length.toString(), 3, "0"); | |
String tlv = "{tag}{length}{value}" | |
.replace("{tag}" , tagName) | |
.replace("{length}", strLength) | |
.replace("{value}" , strValue); | |
fields.put(tagName, tlv); | |
} | |
/** | |
* Verifies whether <code>tagValue</code> must be ignored or not | |
*/ | |
private boolean shouldIgnore(Object tagValue) { | |
if (tagValue == null) return true; | |
if (isBlank(tagValue.toString())) return true; | |
return false; | |
} | |
/** | |
* Validates <code>tagName</code> and <code>value</code> content | |
*/ | |
private void validate(String tagName, Object value) { | |
if (isBlank(tagName)) | |
throw new IllegalArgumentException("Tag name must not be null or empty"); | |
if (tagName.trim().length() != 3) | |
throw new IllegalArgumentException("Tag name must be 3 characters long: " + tagName); | |
if (nvl(value).length() > 999) | |
throw new IllegalArgumentException("Tag value must be at most 999 characters long"); | |
} | |
private String nvl(Object value) { | |
if (value == null) { | |
return ""; | |
} | |
return value.toString(); | |
} | |
private String leftPaddingWithZeros(Object value, int size) { | |
String v = nvl(value); | |
if (isBlank(v)) { | |
return null; | |
} | |
return leftPad(v, size, "0"); | |
} | |
private String onlyNumbers(BigDecimal value) { | |
String v = nvl(value); | |
return v.replaceAll("[^0-9]", ""); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package br.com.rponte.gist.util.tlv; | |
import static org.junit.Assert.*; | |
import java.math.BigDecimal; | |
import org.apache.commons.lang3.StringUtils; | |
import org.junit.Rule; | |
import org.junit.Test; | |
import org.junit.rules.ExpectedException; | |
import hirondelle.date4j.DateTime; | |
public class TLVEncoderTest { | |
@Rule | |
public ExpectedException thrown = ExpectedException.none(); | |
@Test | |
public void shouldEncodeValuesIntoTLV() { | |
DateTime birthday = new DateTime("1984-03-07 20:20:01"); | |
String tlv = new TLVEncoder() | |
.withField("txt", "rponte") | |
.withField("IN1", 2020) | |
.withField("in2", 666, 9) | |
.withField("lo1", 20019393L) | |
.withField("lo2", 1984L, 6) | |
.withField("bi1", new BigDecimal("25.90")) | |
.withField("bi2", new BigDecimal("142.99"), 11) | |
.withField("dt1", birthday) | |
.withField("dt2", birthday, "DD/MM/YYYY") | |
.withField("dt3", birthday, "hh:mm:ss") | |
.encode(); | |
String expectedTlv = join("txt006rponte", | |
"IN10042020", | |
"in2009000000666", | |
"lo100820019393", | |
"lo2006001984", | |
"bi10042590", | |
"bi201100000014299", | |
"dt10100307202001", | |
"dt201007/03/1984", | |
"dt300820:20:01" | |
); | |
assertEquals("tlv", expectedTlv, tlv); | |
} | |
@Test | |
public void shouldNotEncodeValuesIntoTLV_whenValueIsNullOrEmpty() { | |
String tlv = new TLVEncoder() | |
.withField("tx1", (String) null) | |
.withField("tx2", (String) "") | |
.withField("tx3", (String) " ") | |
.withField("in1", (Integer) null) | |
.withField("in2", (Integer) null, 9) | |
.withField("lo1", (Long) null) | |
.withField("lo2", (Long) null, 13) | |
.withField("bi1", (BigDecimal) null) | |
.withField("bi2", (BigDecimal) null, 11) | |
.withField("dt1", (DateTime) null) | |
.withField("dt2", (DateTime) null, "DD/MM/YYYY") | |
.withField("dt3", (DateTime) null, "hh:mm:ss") | |
.encode(); | |
assertEquals("tlv", "", tlv); | |
} | |
@Test | |
public void shouldNotEncodeValuesIntoTLV_whenTagValueHasMoreThan999Characters() { | |
String invalidValue = StringUtils.repeat("a", 1000); | |
thrown.expect(IllegalArgumentException.class); | |
thrown.expectMessage("Tag value must be at most 999 characters long"); | |
new TLVEncoder().withField("abc", invalidValue); | |
} | |
@Test | |
public void shouldNotEncodeValuesIntoTLV_whenTagNameIsNull() { | |
thrown.expect(IllegalArgumentException.class); | |
thrown.expectMessage("Tag name must not be null or empty"); | |
new TLVEncoder().withField(null, "null tag"); | |
} | |
@Test | |
public void shouldNotEncodeValuesIntoTLV_whenTagNameIsEmpty() { | |
thrown.expect(IllegalArgumentException.class); | |
thrown.expectMessage("Tag name must not be null or empty"); | |
new TLVEncoder().withField("", "null tag"); | |
} | |
@Test | |
public void shouldNotEncodeValuesIntoTLV_whenTagNameIsBlank() { | |
thrown.expect(IllegalArgumentException.class); | |
thrown.expectMessage("Tag name must not be null or empty"); | |
new TLVEncoder().withField(" ", "null tag"); | |
} | |
@Test | |
public void shouldNotEncodeValuesIntoTLV_whenTagNameHasMoreThanThreeCharacters() { | |
thrown.expect(IllegalArgumentException.class); | |
thrown.expectMessage("Tag name must be 3 characters long: abcd"); | |
new TLVEncoder().withField("abcd", "invalid tagName"); | |
} | |
@Test | |
public void shouldNotEncodeValuesIntoTLV_whenTagNameHasLessThanThreeCharacters() { | |
thrown.expect(IllegalArgumentException.class); | |
thrown.expectMessage("Tag name must be 3 characters long: ab"); | |
new TLVEncoder().withField("ab", "invalid tagName"); | |
} | |
@Test | |
public void shouldEncodeValuesIntoTLV_andOverrideDuplicatedTags() { | |
DateTime birthday = new DateTime("1984-03-07 20:20:01"); | |
String tlv = new TLVEncoder() | |
.withField("txt", "a") | |
.withField("txt", "b") | |
.withField("int", 1) | |
.withField("int", 2) | |
.withField("lon", 3L) | |
.withField("lon", 4L) | |
.withField("big", BigDecimal.ONE) | |
.withField("big", BigDecimal.TEN) | |
.withField("dat", birthday) | |
.withField("dat", birthday, "DD/MM/YYYY") | |
.withField("dat", birthday, "hh:mm:ss") | |
.encode(); | |
String expectedTlv = join("txt001b", | |
"int0012", | |
"lon0014", | |
"big00210", | |
"dat00820:20:01" | |
); | |
assertEquals("tlv", expectedTlv, tlv); | |
} | |
@Test | |
public void shouldEncodeValuesIntoTLV_whenThereIsNoFields() { | |
String tlv = new TLVEncoder().encode(); | |
assertEquals("empty tlv", "", tlv); | |
} | |
private String join(String...tlv) { | |
return StringUtils.join(tlv, ""); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example of TLV Parser (decoder)