Skip to content

Instantly share code, notes, and snippets.

@rponte
Last active June 18, 2020 14:06
Show Gist options
  • Save rponte/c806d0c26cdb5b5b06d589afca5c9994 to your computer and use it in GitHub Desktop.
Save rponte/c806d0c26cdb5b5b06d589afca5c9994 to your computer and use it in GitHub Desktop.
Just a simple implementation of a TLV encoder
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]", "");
}
}
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, "");
}
}
@rponte
Copy link
Author

rponte commented Jun 18, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment