Oject-10438 Add Java EPC encoder and Saxon-HE extension function

Add three Java source files that enable SGTIN-96 EPC generation
directly from within the DELIVERY.xsl transformation:

- Gs1EpcEncoder.java: encodes SGTIN-96 (38-bit serial via MD5) and
  SGTIN-198 (140-bit ASCII serial); serial derived from MD5(DOCNUM+POSNR+counter)
- Sgtin96Function.java: Saxon-HE ExtensionFunctionDefinition that
  registers gs1:encodeSgtin96FromIdoc() for use in XSLT
- TransformAndEncode.java: Saxon s9api runner that registers the
  extension and executes DELIVERY.xsl in a single pass

Also update .gitignore to track java/src/ while continuing to exclude
compiled output in java/bin/.
This commit is contained in:
Christian Schwarz
2026-04-13 13:49:47 +02:00
parent 26afdf3e9a
commit f07271d392
4 changed files with 480 additions and 0 deletions
+5
View File
@@ -19,6 +19,9 @@ dist/
# Package files # Package files
*.jar *.jar
# additional Libraries
libs/
# Maven # Maven
target/ target/
dist/ dist/
@@ -47,7 +50,9 @@ Thumbs.db
*.flv *.flv
*.mov *.mov
*.wmv *.wmv
*.script
test0001.camel.yaml test0001.camel.yaml
testing/ testing/
.continue/agents/new-config.yaml .continue/agents/new-config.yaml
java/bin/
+336
View File
@@ -0,0 +1,336 @@
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;
public class Gs1EpcEncoder {
public static void main(String[] args) {
if (args.length > 0 && (args[0].equals("-h") || args[0].equals("--help") || args[0].equals("-?"))) {
printHelp();
return;
}
// Example values — edit these to test different inputs
String gtin = "07333544290754"; // GTIN-14
String serial = "134539124202"; // Numeric for SGTIN-96, ASCII for SGTIN-198
int filter = 1; // EPC filter value (1 = POS item)
int companyPrefixDigits = 7; // GS1 company prefix length → partition 5
// Used only for the 96-idoc scheme
String idocNumber = "0000000096668203"; // 16-char SAP IDOC number
String segmentPos = "000042"; // 6-char IDOC segment position
// Pass a scheme as first argument; defaults to 198
String scheme = (args.length > 0) ? args[0] : "198";
String epcHex;
String fullMemory;
switch (scheme) {
case "96":
epcHex = encodeSgtin96(gtin, serial, filter, companyPrefixDigits);
fullMemory = buildFullMemory(epcHex, 96);
System.out.println("Scheme: SGTIN-96");
System.out.println("GTIN: " + gtin);
System.out.println("Serial: " + serial);
System.out.println("EPC (96-bit): " + epcHex.toUpperCase());
System.out.println("Full Tag Memory: " + fullMemory.toUpperCase());
break;
case "96-idoc":
epcHex = encodeSgtin96FromIdoc(gtin, idocNumber, segmentPos, filter, companyPrefixDigits);
fullMemory = buildFullMemory(epcHex, 96);
System.out.println("Scheme: SGTIN-96 (MD5 serial from IDOC)");
System.out.println("GTIN: " + gtin);
System.out.println("IDOC + Segment: " + idocNumber + segmentPos);
System.out.println("EPC (96-bit): " + epcHex.toUpperCase());
System.out.println("Full Tag Memory: " + fullMemory.toUpperCase());
break;
case "198":
epcHex = encodeSgtin198(gtin, serial, filter, companyPrefixDigits);
fullMemory = buildFullMemory(epcHex, 198);
System.out.println("Scheme: SGTIN-198");
System.out.println("GTIN: " + gtin);
System.out.println("Serial: " + serial);
System.out.println("EPC (198-bit): " + epcHex.toUpperCase());
System.out.println("Full Tag Memory: " + fullMemory.toUpperCase());
break;
default:
System.err.println("Unknown scheme: " + scheme);
System.err.println("Run with -h for usage.");
System.exit(1);
}
}
private static void printHelp() {
System.out.println("Gs1EpcEncoder — GS1 EPC tag encoder (SGTIN-96 / SGTIN-198)");
System.out.println();
System.out.println("USAGE");
System.out.println(" java Gs1EpcEncoder [scheme]");
System.out.println(" java Gs1EpcEncoder -h | --help | -?");
System.out.println();
System.out.println("SCHEMES");
System.out.println(" 198 SGTIN-198 (default)");
System.out.println(" 96-bit header/partition/prefix/item + 140-bit ASCII serial");
System.out.println(" Serial: up to 20 printable ASCII characters");
System.out.println(" Output: 50 hex chars (EPC) + 8 hex chars (CRC+PC) = 58 total");
System.out.println();
System.out.println(" 96 SGTIN-96");
System.out.println(" 58-bit header/partition/prefix/item + 38-bit numeric serial");
System.out.println(" Serial: integer 0 274,877,906,943");
System.out.println(" Output: 24 hex chars (EPC) + 8 hex chars (CRC+PC) = 32 total");
System.out.println();
System.out.println(" 96-idoc SGTIN-96 with MD5-derived serial");
System.out.println(" Concatenates IDOC number (16 chars) + segment position (6 chars),");
System.out.println(" MD5-hashes the result, and uses the top 38 bits as the serial.");
System.out.println(" Collision probability: 1 in 2^38 (~274 billion)");
System.out.println(" Output: same format as 96");
System.out.println();
System.out.println("PARAMETERS (set at top of main())");
System.out.println(" gtin GTIN-14 (14 digits)");
System.out.println(" digit 0 : indicator / packaging level");
System.out.println(" digits 1N: GS1 company prefix (N = companyPrefixDigits)");
System.out.println(" digits N+112: item reference");
System.out.println(" digit 13 : check digit (excluded from EPC)");
System.out.println(" serial Serial number");
System.out.println(" scheme 198 : ASCII string, max 20 chars");
System.out.println(" scheme 96 : numeric string, max 274,877,906,943");
System.out.println(" filter EPC filter value (3 bits, 07)");
System.out.println(" 0 = all others 1 = POS item 2 = full case");
System.out.println(" 3 = reserved 4 = inner pack 5 = reserved");
System.out.println(" 6 = unit load 7 = component");
System.out.println(" companyPrefixDigits GS1 company prefix length (612)");
System.out.println(" determines the EPC partition value (06):");
System.out.println(" 12→0 11→1 10→2 9→3 8→4 7→5 6→6");
System.out.println(" idocNumber [96-idoc only] SAP IDOC number, 16 chars");
System.out.println(" e.g. 0000000096668203");
System.out.println(" segmentPos [96-idoc only] IDOC segment position, 6 chars");
System.out.println(" e.g. 000042");
System.out.println();
System.out.println("OUTPUT");
System.out.println(" EPC hex encoded EPC without CRC/PC prefix");
System.out.println(" Full Tag Memory CRC (4 hex) + PC word (4 hex) + EPC hex");
System.out.println(" CRC is a dummy 0000 (real CRC computed by reader)");
System.out.println(" PC word encodes EPC length in 16-bit words");
System.out.println();
System.out.println("EXAMPLES");
System.out.println(" java Gs1EpcEncoder 198");
System.out.println(" java Gs1EpcEncoder 96");
System.out.println(" java Gs1EpcEncoder 96-idoc");
System.out.println(" java Gs1EpcEncoder -h");
}
// ------------------------------------------------------------------ //
// Encoders //
// ------------------------------------------------------------------ //
// Encode SGTIN-96 (96-bit EPC, numeric serial, max value 274,877,906,943)
public static String encodeSgtin96(String gtin, String serial, int filter, int prefixDigits) {
int partition = getPartition(prefixDigits);
int prefixBits = getPrefixBits(partition);
int itemBits = getItemBits(partition);
StringBuilder bits = new StringBuilder();
// 1. Header: 8 bits (SGTIN-96 = 0x30)
bits.append("00110000");
// 2. Filter + Partition: 3 bits each
bits.append(toBinary(filter, 3));
bits.append(toBinary(partition, 3));
// 3. Company prefix
String companyPrefix = gtin.substring(1, 1 + prefixDigits);
bits.append(toBinaryStringNumeric(companyPrefix, prefixBits));
// 4. Item reference (indicator digit + item digits, excluding check digit)
String itemRef = gtin.substring(0, 1) + gtin.substring(1 + prefixDigits, 13);
bits.append(toBinaryStringNumeric(itemRef, itemBits));
// 5. Serial number: 38 bits, numeric (not ASCII)
bits.append(toBinaryStringNumeric(serial, 38));
if (bits.length() != 96) {
throw new IllegalStateException("Bit length = " + bits.length() + ", expected 96");
}
return binaryToHexFixed(bits.toString(), 24); // 24 hex chars = 12 bytes = 96 bits
}
// Encode SGTIN-96 using a MD5-derived serial from an IDOC number + segment position.
// The 22-char combined input is hashed with MD5 (128 bits); the top 38 bits are
// used as the numeric serial, which fits the SGTIN-96 serial field (max 274,877,906,943).
public static String encodeSgtin96FromIdoc(String gtin, String idocNumber, String segmentPos, int filter, int prefixDigits) {
String serial = md5ToSerial38(idocNumber + segmentPos);
return encodeSgtin96(gtin, serial, filter, prefixDigits);
}
// MD5-hash the input string and return the top 38 bits as a decimal string.
// 128-bit MD5 → shift right by 90 → 38 bits → fits SGTIN-96 serial field.
private static String md5ToSerial38(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hash = md.digest(input.getBytes(StandardCharsets.UTF_8));
// Interpret all 16 bytes as an unsigned 128-bit integer, then take the top 38 bits
BigInteger full128 = new BigInteger(1, hash); // 1 = positive/unsigned
long serial38 = full128.shiftRight(128 - 38).longValue();
return Long.toString(serial38);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("MD5 not available", e);
}
}
// Encode SGTIN-198 (198-bit EPC, 7-bit ASCII serial, max 20 characters)
public static String encodeSgtin198(String gtin, String serial, int filter, int prefixDigits) {
int partition = getPartition(prefixDigits);
int prefixBits = getPrefixBits(partition);
int itemBits = getItemBits(partition);
StringBuilder bits = new StringBuilder();
// 1. Header: 8 bits (SGTIN-198 = 0x36)
bits.append("00110110");
// 2. Filter + Partition: 3 bits each
bits.append(toBinary(filter, 3));
bits.append(toBinary(partition, 3));
// 3. Company prefix
String companyPrefix = gtin.substring(1, 1 + prefixDigits);
bits.append(toBinaryStringNumeric(companyPrefix, prefixBits));
// 4. Item reference (indicator digit + item digits, excluding check digit)
String itemRef = gtin.substring(0, 1) + gtin.substring(1 + prefixDigits, 13);
bits.append(toBinaryStringNumeric(itemRef, itemBits));
// 5. Serial number: 140 bits, 7-bit ASCII, zero-padded
StringBuilder serialBits = new StringBuilder();
for (char c : serial.toCharArray()) {
serialBits.append(toBinary((int) c, 7));
}
while (serialBits.length() < 140) {
serialBits.append('0');
}
bits.append(serialBits.substring(0, 140));
if (bits.length() != 198) {
throw new IllegalStateException("Bit length = " + bits.length() + ", expected 198");
}
return binaryToHexFixed(bits.toString(), 50); // 50 hex chars = 25 bytes = 200 bits (2-bit pad)
}
// Encode SGTIN-198 — legacy version kept for reference (includes check digit in item ref, bug)
public static String encodeSgtin198_old(String gtin, String serial, int filter, int prefixDigits) {
int partition = getPartition(prefixDigits);
int prefixBits = getPrefixBits(partition);
int itemBits = getItemBits(partition);
StringBuilder bits = new StringBuilder();
bits.append("00110110");
bits.append(toBinary(filter, 3));
bits.append(toBinary(partition, 3));
String companyPrefix = gtin.substring(1, 1 + prefixDigits);
bits.append(toBinaryStringNumeric(companyPrefix, prefixBits));
// Bug: uses index 14, which includes the check digit at position 13
String itemRef = gtin.substring(0, 1) + gtin.substring(1 + prefixDigits, 14);
bits.append(toBinaryStringNumeric(itemRef, itemBits));
StringBuilder serialBits = new StringBuilder();
for (char c : serial.toCharArray()) {
serialBits.append(toBinary((int) c, 7));
}
while (serialBits.length() < 140) {
serialBits.append('0');
}
bits.append(serialBits.substring(0, 140));
if (bits.length() != 198) {
throw new IllegalStateException("Bit length = " + bits.length() + ", expected 198");
}
return binaryToHexFixed(bits.toString(), 50);
}
// ------------------------------------------------------------------ //
// Tag memory builder //
// ------------------------------------------------------------------ //
// Build full EPC memory (CRC + PC word + EPC).
// epcBits: total bit length of the EPC (e.g. 96 or 198).
public static String buildFullMemory(String epcHex, int epcBits) {
int epcWords = (epcBits + 15) / 16; // ceiling division: 96→6, 198→13
int pc = epcWords << 11; // PC word: upper 5 bits = EPC length in words
String pcHex = String.format("%04X", pc);
String crcHex = "0000"; // dummy CRC — real value computed by RFID reader
return crcHex + pcHex + epcHex;
}
// ------------------------------------------------------------------ //
// Partition helpers //
// ------------------------------------------------------------------ //
private static int getPartition(int prefixDigits) {
switch (prefixDigits) {
case 12: return 0;
case 11: return 1;
case 10: return 2;
case 9: return 3;
case 8: return 4;
case 7: return 5;
case 6: return 6;
default: throw new IllegalArgumentException("Invalid prefix length: " + prefixDigits + " (must be 612)");
}
}
private static int getPrefixBits(int partition) {
return new int[]{40, 37, 34, 30, 27, 24, 20}[partition];
}
private static int getItemBits(int partition) {
return new int[]{4, 7, 10, 14, 17, 20, 24}[partition];
}
// ------------------------------------------------------------------ //
// Bit / hex utilities //
// ------------------------------------------------------------------ //
private static String toBinary(long value, int length) {
String b = Long.toBinaryString(value);
return "0".repeat(Math.max(0, length - b.length())) + b;
}
// Convert a numeric string to an exact-length binary string
private static String toBinaryStringNumeric(String number, int length) {
BigInteger value = new BigInteger(number);
String b = value.toString(2);
if (b.length() > length) {
return b.substring(b.length() - length); // truncate high bits (should not happen)
}
return "0".repeat(length - b.length()) + b;
}
// Convert a binary string to a fixed-length uppercase hex string
private static String binaryToHexFixed(String binary, int hexLength) {
int pad = (8 - (binary.length() % 8)) % 8;
binary = binary + "0".repeat(pad);
StringBuilder hex = new StringBuilder();
for (int i = 0; i < binary.length(); i += 8) {
int val = Integer.parseInt(binary.substring(i, i + 8), 2);
hex.append(String.format("%02X", val));
}
while (hex.length() < hexLength) {
hex.insert(0, '0');
}
return hex.toString();
}
}
+68
View File
@@ -0,0 +1,68 @@
import net.sf.saxon.lib.ExtensionFunctionCall;
import net.sf.saxon.lib.ExtensionFunctionDefinition;
import net.sf.saxon.om.Sequence;
import net.sf.saxon.om.StructuredQName;
import net.sf.saxon.expr.XPathContext;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.value.NumericValue;
import net.sf.saxon.value.SequenceType;
import net.sf.saxon.value.StringValue;
/**
* Saxon-HE integrated extension function — no Saxon-PE/EE required.
* Registered programmatically via Processor.registerExtensionFunction().
*
* XSLT namespace: xmlns:gs1="urn:gs1:epc"
* XSLT call: gs1:encodeSgtin96FromIdoc($gtin14, $docnum, $posnr, 1, 7)
*/
public class Sgtin96Function extends ExtensionFunctionDefinition {
/** Must match the xmlns:gs1 URI declared in the stylesheet. */
public static final String NAMESPACE = "urn:gs1:epc";
@Override
public StructuredQName getFunctionQName() {
return new StructuredQName("gs1", NAMESPACE, "encodeSgtin96FromIdoc");
}
@Override
public SequenceType[] getArgumentTypes() {
return new SequenceType[]{
SequenceType.SINGLE_STRING, // gtin14 — GTIN-14 (14 digits)
SequenceType.SINGLE_STRING, // idocNumber — EDI_DC40/DOCNUM
SequenceType.SINGLE_STRING, // segmentPos — POSNR of the line item
SequenceType.SINGLE_INTEGER, // filter — EPC filter (1 = POS item)
SequenceType.SINGLE_INTEGER // prefixDigits — GS1 company prefix length (612)
};
}
@Override
public SequenceType getResultType(SequenceType[] suppliedArgTypes) {
return SequenceType.SINGLE_STRING;
}
@Override
public ExtensionFunctionCall makeCallExpression() {
return new ExtensionFunctionCall() {
@Override
public Sequence call(XPathContext ctx, Sequence[] args) throws XPathException {
try {
String gtin = args[0].head().getStringValue();
String idoc = args[1].head().getStringValue();
String posnr = args[2].head().getStringValue();
int filter = (int) ((NumericValue) args[3].head()).longValue();
int prefix = (int) ((NumericValue) args[4].head()).longValue();
String epc = Gs1EpcEncoder
.encodeSgtin96FromIdoc(gtin, idoc, posnr, filter, prefix)
.toUpperCase();
return new StringValue(epc);
} catch (Exception e) {
throw new XPathException(
"Gs1EpcEncoder.encodeSgtin96FromIdoc failed: " + e.getMessage());
}
}
};
}
}
+71
View File
@@ -0,0 +1,71 @@
import net.sf.saxon.s9api.Processor;
import net.sf.saxon.s9api.Serializer;
import net.sf.saxon.s9api.XsltCompiler;
import net.sf.saxon.s9api.XsltExecutable;
import net.sf.saxon.s9api.XsltTransformer;
import javax.xml.transform.stream.StreamSource;
import java.io.File;
/**
* Runs DELIVERY.xsl via Saxon-HE with the Sgtin96Function extension registered.
* The XSLT calls gs1:encodeSgtin96FromIdoc() directly — no post-processing needed.
*
* Usage:
* java TransformAndEncode <idoc-xml> <xsl> <output-xml>
* java TransformAndEncode -h | --help
*/
public class TransformAndEncode {
public static void main(String[] args) throws Exception {
if (args.length == 0 || args[0].equals("-h") || args[0].equals("--help")) {
printHelp();
return;
}
if (args.length < 3) {
System.err.println("Error: expected 3 arguments. Run with -h for usage.");
System.exit(1);
}
File idocFile = new File(args[0]);
File xslFile = new File(args[1]);
File outFile = new File(args[2]);
// Register the SGTIN-96 extension function with a Saxon-HE processor
Processor processor = new Processor(false);
processor.registerExtensionFunction(new Sgtin96Function());
// Compile and run the stylesheet
XsltCompiler compiler = processor.newXsltCompiler();
XsltExecutable executable = compiler.compile(new StreamSource(xslFile));
XsltTransformer transformer = executable.load();
transformer.setSource(new StreamSource(idocFile));
Serializer ser = processor.newSerializer(outFile);
ser.setOutputProperty(Serializer.Property.INDENT, "yes");
transformer.setDestination(ser);
transformer.transform();
System.out.println("Output: " + outFile.getAbsolutePath());
}
private static void printHelp() {
System.out.println("TransformAndEncode — DELIVERY.xsl runner with SGTIN-96 EPC support");
System.out.println();
System.out.println("USAGE");
System.out.println(" java TransformAndEncode <idoc-xml> <xsl> <output-xml>");
System.out.println(" java TransformAndEncode -h | --help");
System.out.println();
System.out.println("ARGUMENTS");
System.out.println(" idoc-xml SAP ZFSHDLV IDOC input file");
System.out.println(" xsl DELIVERY.xsl stylesheet path");
System.out.println(" output-xml XMLDESADV output file");
System.out.println();
System.out.println("EPC GENERATION CONDITIONS (evaluated in the XSLT)");
System.out.println(" EDI_DC40/TEST = 'X' IDOC in test mode");
System.out.println(" E1EDL37 absent No handling unit segments");
System.out.println();
System.out.println("EXTENSION FUNCTION");
System.out.println(" Namespace: " + Sgtin96Function.NAMESPACE);
System.out.println(" Function: encodeSgtin96FromIdoc(gtin14, idoc, posnr, filter, prefixDigits)");
}
}