Introduce hint to force specific code set for Code-128 encoding (#1411)

* Introduce hint to force specific code set for Code-128 encoding

* Address review comments

* Make character check more readable; address review comments

* Address review comments

Co-authored-by: Gero Schwäricke <gero@schwaericke.email>
diff --git a/core/src/main/java/com/google/zxing/EncodeHintType.java b/core/src/main/java/com/google/zxing/EncodeHintType.java
index 9a2fd57..98c66ee 100644
--- a/core/src/main/java/com/google/zxing/EncodeHintType.java
+++ b/core/src/main/java/com/google/zxing/EncodeHintType.java
@@ -115,4 +115,9 @@
    * {@link String } value).
    */
   GS1_FORMAT,
+
+  /**
+   * Forces which encoding will be used. Currently only used for Code-128 code sets (Type {@link String}). Valid values are "A", "B", "C".
+   */
+  FORCE_CODE_SET,
 }
diff --git a/core/src/main/java/com/google/zxing/oned/Code128Writer.java b/core/src/main/java/com/google/zxing/oned/Code128Writer.java
index 58499f2..87359e3 100644
--- a/core/src/main/java/com/google/zxing/oned/Code128Writer.java
+++ b/core/src/main/java/com/google/zxing/oned/Code128Writer.java
@@ -17,11 +17,13 @@
 package com.google.zxing.oned;
 
 import com.google.zxing.BarcodeFormat;
+import com.google.zxing.EncodeHintType;
 import com.google.zxing.common.BitMatrix;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Map;
 
 /**
  * This object renders a CODE128 code as a {@link BitMatrix}.
@@ -65,27 +67,77 @@
 
   @Override
   public boolean[] encode(String contents) {
+    return encode(contents, null);
+  }
+
+  @Override
+  protected boolean[] encode(String contents, Map<EncodeHintType,?> hints) {
     int length = contents.length();
     // Check length
     if (length < 1 || length > 80) {
       throw new IllegalArgumentException(
           "Contents length should be between 1 and 80 characters, but got " + length);
     }
+
+    // Check for forced code set hint.
+    int forcedCodeSet = -1;
+    if (hints != null && hints.containsKey(EncodeHintType.FORCE_CODE_SET)) {
+      String codeSetHint = hints.get(EncodeHintType.FORCE_CODE_SET).toString();
+      switch (codeSetHint) {
+        case "A":
+          forcedCodeSet = CODE_CODE_A;
+          break;
+        case "B":
+          forcedCodeSet = CODE_CODE_B;
+          break;
+        case "C":
+          forcedCodeSet = CODE_CODE_C;
+          break;
+        default:
+          throw new IllegalArgumentException("Unsupported code set hint: " + codeSetHint);
+      }
+    }
+
     // Check content
     for (int i = 0; i < length; i++) {
       char c = contents.charAt(i);
+      // check for non ascii characters that are not special GS1 characters
       switch (c) {
+        // special function characters
         case ESCAPE_FNC_1:
         case ESCAPE_FNC_2:
         case ESCAPE_FNC_3:
         case ESCAPE_FNC_4:
           break;
+        // non ascii characters
         default:
           if (c > 127) {
-            // support for FNC4 isn't implemented, no full Latin-1 character set available at the moment
-            throw new IllegalArgumentException("Bad character in input: " + c);
+            // no full Latin-1 character set available at the moment
+            // shift and manual code change are not supported
+            throw new IllegalArgumentException("Bad character in input: ASCII value=" + (int) c);
           }
       }
+      // check characters for compatibility with forced code set
+      switch (forcedCodeSet) {
+        case CODE_CODE_A:
+          // allows no ascii above 95 (no lower caps, no special symbols)
+          if (c > 95 && c <= 127) {
+            throw new IllegalArgumentException("Bad character in input for forced code set A: ASCII value=" + (int) c);
+          }
+          break;
+        case CODE_CODE_B:
+          // allows no ascii below 32 (terminal symbols)
+          if (c <= 32) {
+            throw new IllegalArgumentException("Bad character in input for forced code set B: ASCII value=" + (int) c);
+          }
+          break;
+        case CODE_CODE_C:
+          // allows only numbers and no FNC 2/3/4
+          if (c < 48 || (c > 57 && c <= 127) || c == ESCAPE_FNC_2 || c == ESCAPE_FNC_3 || c == ESCAPE_FNC_4) {
+            throw new IllegalArgumentException("Bad character in input for forced code set C: ASCII value=" + (int) c);
+          }
+          break;
+      }
     }
 
     Collection<int[]> patterns = new ArrayList<>(); // temporary storage for patterns
@@ -96,7 +148,12 @@
 
     while (position < length) {
       //Select code to use
-      int newCodeSet = chooseCode(contents, position, codeSet);
+      int newCodeSet;
+      if (forcedCodeSet == -1) {
+        newCodeSet = chooseCode(contents, position, codeSet);
+      } else {
+        newCodeSet = forcedCodeSet;
+      }
 
       //Get the pattern index
       int patternIndex;
@@ -135,6 +192,10 @@
                 break;
               default:
                 // CODE_CODE_C
+                if (position + 1 == length) {
+                  // this is the last character, but the encoding is C, which always encodes two characers
+                  throw new IllegalArgumentException("Bad number of characters for digit only encoding.");
+                }
                 patternIndex = Integer.parseInt(contents.substring(position, position + 2));
                 position++; // Also incremented below
                 break;
diff --git a/core/src/main/java/com/google/zxing/oned/OneDimensionalCodeWriter.java b/core/src/main/java/com/google/zxing/oned/OneDimensionalCodeWriter.java
index 3ad398e..d277d2e 100644
--- a/core/src/main/java/com/google/zxing/oned/OneDimensionalCodeWriter.java
+++ b/core/src/main/java/com/google/zxing/oned/OneDimensionalCodeWriter.java
@@ -33,6 +33,25 @@
 public abstract class OneDimensionalCodeWriter implements Writer {
   private static final Pattern NUMERIC = Pattern.compile("[0-9]+");
 
+  /**
+   * Encode the contents to boolean array expression of one-dimensional barcode.
+   * Start code and end code should be included in result, and side margins should not be included.
+   *
+   * @param contents barcode contents to encode
+   * @return a {@code boolean[]} of horizontal pixels (false = white, true = black)
+   */
+  public abstract boolean[] encode(String contents);
+
+  /**
+   * Can be overwritten if the encode requires to read the hints map. Otherwise it defaults to {@code encode}.
+   * @param contents barcode contents to encode
+   * @param hints encoding hints
+   * @return a {@code boolean[]} of horizontal pixels (false = white, true = black)
+   */
+  protected boolean[] encode(String contents, Map<EncodeHintType,?> hints) {
+    return encode(contents);
+  }
+
   @Override
   public final BitMatrix encode(String contents, BarcodeFormat format, int width, int height) {
     return encode(contents, format, width, height, null);
@@ -70,7 +89,7 @@
       sidesMargin = Integer.parseInt(hints.get(EncodeHintType.MARGIN).toString());
     }
 
-    boolean[] code = encode(contents);
+    boolean[] code = encode(contents, hints);
     return renderResult(code, width, height, sidesMargin);
   }
 
@@ -135,14 +154,5 @@
     // This seems like a decent idea for a default for all formats.
     return 10;
   }
-
-  /**
-   * Encode the contents to boolean array expression of one-dimensional barcode.
-   * Start code and end code should be included in result, and side margins should not be included.
-   *
-   * @param contents barcode contents to encode
-   * @return a {@code boolean[]} of horizontal pixels (false = white, true = black)
-   */
-  public abstract boolean[] encode(String contents);
 }
 
diff --git a/core/src/test/java/com/google/zxing/oned/Code128WriterTestCase.java b/core/src/test/java/com/google/zxing/oned/Code128WriterTestCase.java
index a12d44d..9e7ca8f 100644
--- a/core/src/test/java/com/google/zxing/oned/Code128WriterTestCase.java
+++ b/core/src/test/java/com/google/zxing/oned/Code128WriterTestCase.java
@@ -22,12 +22,16 @@
 import org.junit.Test;
 
 import com.google.zxing.BarcodeFormat;
+import com.google.zxing.EncodeHintType;
 import com.google.zxing.Result;
 import com.google.zxing.Writer;
 import com.google.zxing.WriterException;
 import com.google.zxing.common.BitArray;
 import com.google.zxing.common.BitMatrix;
 
+import java.util.Map;
+import java.util.EnumMap;
+
 /**
  * Tests {@link Code128Writer}.
  */
@@ -151,4 +155,82 @@
     String actualRoundtripResultText = rtResult.getText();
     assertEquals(toEncode, actualRoundtripResultText);
   }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testEncodeWithForcedCodeSetFailureCodeSetABadCharacter() throws Exception {
+    // Lower case characters should not be accepted when the code set is forced to A.
+    String toEncode = "ASDFx0123";
+
+    Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
+    hints.put(EncodeHintType.FORCE_CODE_SET, "A");
+    BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testEncodeWithForcedCodeSetFailureCodeSetBBadCharacter() throws Exception {
+    String toEncode = "ASdf\00123"; // \0 (ascii value 0)
+    // Characters with ASCII value below 32 should not be accepted when the code set is forced to B.
+
+    Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
+    hints.put(EncodeHintType.FORCE_CODE_SET, "B");
+    BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testEncodeWithForcedCodeSetFailureCodeSetCBadCharactersNonNum() throws Exception {
+    String toEncode = "123a5678";
+    // Non-digit characters should not be accepted when the code set is forced to C.
+
+    Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
+    hints.put(EncodeHintType.FORCE_CODE_SET, "C");
+    BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testEncodeWithForcedCodeSetFailureCodeSetCBadCharactersFncCode() throws Exception {
+    String toEncode = "123\u00f2a678";
+    // Function codes other than 1 should not be accepted when the code set is forced to C.
+
+    Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
+    hints.put(EncodeHintType.FORCE_CODE_SET, "C");
+    BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testEncodeWithForcedCodeSetFailureCodeSetCWrongAmountOfDigits() throws Exception {
+    String toEncode = "123456789";
+    // An uneven amount of digits should not be accepted when the code set is forced to C.
+
+    Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
+    hints.put(EncodeHintType.FORCE_CODE_SET, "C");
+    BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints);
+  }
+
+  @Test
+  public void testEncodeWithForcedCodeSetFailureCodeSetA() throws Exception {
+    String toEncode = "AB123";
+    //                          would default to B             "A"             "B"             "1"             "2"             "3"  check digit 10
+    String expected = QUIET_SPACE + START_CODE_A + "10100011000" + "10001011000" + "10011100110" + "11001110010" + "11001011100" + "11001000100" + STOP + QUIET_SPACE;
+
+    Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
+    hints.put(EncodeHintType.FORCE_CODE_SET, "A");
+    BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints);
+
+    String actual = BitMatrixTestCase.matrixToString(result);
+    assertEquals(expected, actual);
+  }
+
+  @Test
+  public void testEncodeWithForcedCodeSetFailureCodeSetB() throws Exception {
+    String toEncode = "1234";
+    //                          would default to C           "1"             "2"             "3"             "4"  check digit 88
+    String expected = QUIET_SPACE + START_CODE_B + "10011100110" + "11001110010" + "11001011100" + "11001001110" + "11110010010" + STOP + QUIET_SPACE;
+
+    Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
+    hints.put(EncodeHintType.FORCE_CODE_SET, "B");
+    BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints);
+
+    String actual = BitMatrixTestCase.matrixToString(result);
+    assertEquals(expected, actual);
+  }
 }