diff --git a/.gitignore b/.gitignore index be4b2ab..647bcbf 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ dist/ # Package files *.jar +# additional Libraries +libs/ + # Maven target/ dist/ @@ -47,7 +50,9 @@ Thumbs.db *.flv *.mov *.wmv +*.script test0001.camel.yaml testing/ .continue/agents/new-config.yaml +java/bin/ \ No newline at end of file diff --git a/java/src/Gs1EpcEncoder.java b/java/src/Gs1EpcEncoder.java new file mode 100644 index 0000000..311188f --- /dev/null +++ b/java/src/Gs1EpcEncoder.java @@ -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 1–N: GS1 company prefix (N = companyPrefixDigits)"); + System.out.println(" digits N+1–12: 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, 0–7)"); + 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 (6–12)"); + System.out.println(" determines the EPC partition value (0–6):"); + 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 6–12)"); + } + } + + 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(); + } +} diff --git a/java/src/Sgtin96Function.java b/java/src/Sgtin96Function.java new file mode 100644 index 0000000..d3f3afa --- /dev/null +++ b/java/src/Sgtin96Function.java @@ -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 (6–12) + }; + } + + @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()); + } + } + }; + } +} diff --git a/java/src/TransformAndEncode.java b/java/src/TransformAndEncode.java new file mode 100644 index 0000000..f9bc41d --- /dev/null +++ b/java/src/TransformAndEncode.java @@ -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 + * 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 "); + 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)"); + } +}