Implemented [JACKSON-208]

diff --git a/release-notes/CREDITS b/release-notes/CREDITS
index 9975666..a0d6dd3 100644
--- a/release-notes/CREDITS
+++ b/release-notes/CREDITS
@@ -240,3 +240,8 @@
   * Reported [JACKSON-196], suggested fix: Schema generation does not
     respect the annotation configured serializer on a bean property
    [1.4.0]
+
+Mark Stevens:
+  * Requested [JACKSON-208] Aallow unquoted control characters (esp. tabs)
+    in JSON Strings and field names
+   [1.4.0]
diff --git a/release-notes/VERSION b/release-notes/VERSION
index 6b77c6f..071813e 100644
--- a/release-notes/VERSION
+++ b/release-notes/VERSION
@@ -20,6 +20,10 @@
   * [JACKSON-196] Schema generation does not respect the annotation
     configured serializer on a bean property
    (reported by Gil M)
+  * [JACKSON-208] Add feature (JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS)
+    to allow unquoted control characters (esp. tabs) in Strings and
+    field names
+   (requested by Mark S)
 
   New features:
 
diff --git a/src/java/org/codehaus/jackson/JsonParser.java b/src/java/org/codehaus/jackson/JsonParser.java
index 65bf389..c5a03f5 100644
--- a/src/java/org/codehaus/jackson/JsonParser.java
+++ b/src/java/org/codehaus/jackson/JsonParser.java
@@ -114,6 +114,25 @@
         ,ALLOW_SINGLE_QUOTES(false)
 
         /**
+         * Feature that determines whether parser will allow
+         * JSON Strings to contain unquoted control characters
+         * (ascii characters with value less than 32, including
+         * tab and line feed characters) or not.
+         * If feature is set false, an exception is thrown if such a
+         * character is encountered.
+         *<p>
+         * Since JSON specification requires quoting for all
+         * control characters,
+         * this is a non-standard feature, and as such disabled by
+         * default.
+         *<p>
+         * This feature can be changed for parser instances.
+         *
+         * @since 1.4
+         */
+        ,ALLOW_UNQUOTED_CONTROL_CHARS(false)
+
+        /**
          * Feature that determines whether JSON object field names are
          * to be canonicalized using {@link String#intern} or not:
          * if enabled, all field names will be intern()ed (and caller
diff --git a/src/java/org/codehaus/jackson/impl/JsonParserBase.java b/src/java/org/codehaus/jackson/impl/JsonParserBase.java
index 7727378..362b398 100644
--- a/src/java/org/codehaus/jackson/impl/JsonParserBase.java
+++ b/src/java/org/codehaus/jackson/impl/JsonParserBase.java
@@ -585,12 +585,22 @@
         _reportError(msg);
     }
 
+    /**
+     * Method called to report a problem with unquoted control character.
+     * Note: starting with version 1.4, it is possible to suppress
+     * exception by enabling {@link JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS}.
+     *
+     * @return If space is accepted, space itself (otherwise exception thrown)
+     */
     protected void _throwUnquotedSpace(int i, String ctxtDesc)
         throws JsonParseException
     {
-        char c = (char) i;
-        String msg = "Illegal unquoted character ("+_getCharDesc(c)+"): has to be escaped using backslash to be included in "+ctxtDesc;
-        _reportError(msg);
+        // JACKSON-208; possible to allow unquoted control chars:
+        if (!isEnabled(Feature.ALLOW_UNQUOTED_CONTROL_CHARS) || i >= INT_SPACE) {
+            char c = (char) i;
+            String msg = "Illegal unquoted character ("+_getCharDesc(c)+"): has to be escaped using backslash to be included in "+ctxtDesc;
+            _reportError(msg);
+        }
     }
 
     protected void _reportMismatchedEndMarker(int actCh, char expCh)
diff --git a/src/java/org/codehaus/jackson/impl/Utf8StreamParser.java b/src/java/org/codehaus/jackson/impl/Utf8StreamParser.java
index 9179843..7998284 100644
--- a/src/java/org/codehaus/jackson/impl/Utf8StreamParser.java
+++ b/src/java/org/codehaus/jackson/impl/Utf8StreamParser.java
@@ -466,12 +466,12 @@
                 }
                 // Unquoted white space?
                 if (ch != INT_BACKSLASH) {
+                    // As per [JACKSON-208], call can now return:
                     _throwUnquotedSpace(ch, "name");
+                } else {
+                    // Nope, escape sequence
+                    ch = _decodeEscaped();
                 }
-
-                // Nope, escape sequence
-
-                ch = _decodeEscaped();
                 /* Oh crap. May need to UTF-8 (re-)encode it, if it's
                  * beyond 7-bit ascii. Gets pretty messy.
                  * If this happens often, may want to use different name
@@ -651,12 +651,12 @@
             if (ch != INT_QUOTE && codes[ch] != 0) {
                 if (ch != INT_BACKSLASH) {
                     // Unquoted white space?
+                    // As per [JACKSON-208], call can now return:
                     _throwUnquotedSpace(ch, "name");
+                } else {
+                    // Nope, escape sequence
+                    ch = _decodeEscaped();
                 }
-
-                // Nope, escape sequence
-
-                ch = _decodeEscaped();
                 /* Oh crap. May need to UTF-8 (re-)encode it, if it's
                  * beyond 7-bit ascii. Gets pretty messy.
                  * If this happens often, may want to use different name
@@ -974,10 +974,12 @@
                 break;
             default:
                 if (c < INT_SPACE) {
+                    // As per [JACKSON-208], call can now return:
                     _throwUnquotedSpace(c, "string value");
+                } else {
+                    // Is this good enough error message?
+                    _reportInvalidChar(c);
                 }
-                // Is this good enough error message?
-                _reportInvalidChar(c);
             }
             // Need more room?
             if (outPtr >= outBuf.length) {
@@ -1046,10 +1048,12 @@
                 break;
             default:
                 if (c < INT_SPACE) {
+                    // As per [JACKSON-208], call can now return:
                     _throwUnquotedSpace(c, "string value");
+                } else {
+                    // Is this good enough error message?
+                    _reportInvalidChar(c);
                 }
-                // Is this good enough error message?
-                _reportInvalidChar(c);
             }
         }
     }
diff --git a/src/mapper/java/org/codehaus/jackson/map/DeserializationContext.java b/src/mapper/java/org/codehaus/jackson/map/DeserializationContext.java
index 5e14a27..86760ba 100644
--- a/src/mapper/java/org/codehaus/jackson/map/DeserializationContext.java
+++ b/src/mapper/java/org/codehaus/jackson/map/DeserializationContext.java
@@ -27,6 +27,10 @@
 
     public DeserializationConfig getConfig() { return _config; }
 
+    /**
+     * Convenience method for checking whether specified on/off
+     * feature is enabled
+     */
     public boolean isEnabled(DeserializationConfig.Feature feat) {
     	return _config.isEnabled(feat);
     }
@@ -35,6 +39,10 @@
         return _config.getBase64Variant();
     }
 
+    /**
+     * Accessor for getting access to the underlying JSON parser used
+     * for deserialization.
+     */
     public abstract JsonParser getParser();
 
     /*
@@ -45,10 +53,18 @@
 
     /**
      * Method that can be used to get access to a reusable ObjectBuffer,
-     * useful for constructing Object arrays and Lists.
+     * useful for efficiently constructing Object arrays and Lists.
+     * Note that leased buffers should be returned once deserializer
+     * is done, to allow for reuse during same round of deserialization.
      */
     public abstract ObjectBuffer leaseObjectBuffer();
 
+    /**
+     * Method to call to return object buffer previously leased with
+     * {@link #leaseObjectBuffer}.
+     * 
+     * @param buf Returned object buffer
+     */
     public abstract void returnObjectBuffer(ObjectBuffer buf);
 
     /**
@@ -74,15 +90,42 @@
     //////////////////////////////////////////////////////////////
     */
 
+    /**
+     * Helper method for constructing generic mapping exception for specified type
+     */
     public abstract JsonMappingException mappingException(Class<?> targetClass);
+
+    /**
+     * Helper method for constructing instantiation exception for specified type,
+     * to indicate problem with physically constructing instance of
+     * specified class (missing constructor, exception from constructor)
+     */
     public abstract JsonMappingException instantiationException(Class<?> instClass, Exception e);
     
+    /**
+     * Helper method for constructing exception to indicate that input JSON
+     * String was not in recognized format for deserializing into given type.
+     */
     public abstract JsonMappingException weirdStringException(Class<?> instClass, String msg);
+
+    /**
+     * Helper method for constructing exception to indicate that input JSON
+     * Number was not suitable for deserializing into given type.
+     */
     public abstract JsonMappingException weirdNumberException(Class<?> instClass, String msg);
 
+    /**
+     * Helper method for constructing exception to indicate that given JSON
+     * Object field name was not in format to be able to deserialize specified
+     * key type.
+     */
     public abstract JsonMappingException weirdKeyException(Class<?> keyClass, String keyValue, String msg);
 
     /**
+     * Helper method for constructing exception to indicate that JSON Object
+     * field name did not map to a known property of type being
+     * deserialized.
+     * 
      * @param instanceOrClass Either value being populated (if one has been
      *   instantiated), or Class that indicates type that would be (or
      *   have been) instantiated
diff --git a/src/mapper/java/org/codehaus/jackson/map/ext/CoreXMLDeserializers.java b/src/mapper/java/org/codehaus/jackson/map/ext/CoreXMLDeserializers.java
index fe9f170..33ea44f 100644
--- a/src/mapper/java/org/codehaus/jackson/map/ext/CoreXMLDeserializers.java
+++ b/src/mapper/java/org/codehaus/jackson/map/ext/CoreXMLDeserializers.java
@@ -116,7 +116,7 @@
         final static DocumentBuilderFactory _parserFactory;
         static {
             _parserFactory = DocumentBuilderFactory.newInstance();
-            // yup, only cavemen do XML without recognizing namespaces...
+            // yup, only cave men do XML without recognizing namespaces...
             _parserFactory.setNamespaceAware(true);
         }
 
diff --git a/src/test/main/BaseTest.java b/src/test/main/BaseTest.java
index 5dafab2..5d6769d 100644
--- a/src/test/main/BaseTest.java
+++ b/src/test/main/BaseTest.java
@@ -254,6 +254,12 @@
         return str;
     }
 
+    /*
+    ////////////////////////////////////////////////////////
+    // And other helpers
+    ////////////////////////////////////////////////////////
+     */
+
     protected byte[] encodeInUTF32BE(String input)
     {
         int len = input.length();
@@ -267,4 +273,8 @@
         }
         return result;
     }
+
+    public String quote(String str) {
+        return '"'+str+'"';
+    }
 }
diff --git a/src/test/org/codehaus/jackson/main/TestJsonParser.java b/src/test/org/codehaus/jackson/main/TestJsonParser.java
index 5983ff3..d55b12c 100644
--- a/src/test/org/codehaus/jackson/main/TestJsonParser.java
+++ b/src/test/org/codehaus/jackson/main/TestJsonParser.java
@@ -25,7 +25,6 @@
         jp.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false);
         assertFalse(jp.isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE));
 
-        // default should be true here:
         assertTrue(jp.isEnabled(JsonParser.Feature.INTERN_FIELD_NAMES));
         jp.configure(JsonParser.Feature.INTERN_FIELD_NAMES, false);
         assertFalse(jp.isEnabled(JsonParser.Feature.INTERN_FIELD_NAMES));
diff --git a/src/test/org/codehaus/jackson/main/TestParserFeatures.java b/src/test/org/codehaus/jackson/main/TestParserFeatures.java
index ac2e94d..0c75e1d 100644
--- a/src/test/org/codehaus/jackson/main/TestParserFeatures.java
+++ b/src/test/org/codehaus/jackson/main/TestParserFeatures.java
@@ -15,6 +15,8 @@
         assertTrue(f.isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE));
         assertFalse(f.isEnabled(JsonParser.Feature.ALLOW_COMMENTS));
         assertFalse(f.isEnabled(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES));
+        assertFalse(f.isEnabled(JsonParser.Feature.ALLOW_SINGLE_QUOTES));
+        assertFalse(f.isEnabled(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS));
     }
 
     public void testQuotesRequired() throws Exception
@@ -49,6 +51,20 @@
         _testSingleQuotesEnabled(true);
     }
 
+    // // Tests for [JACKSON-208], unquoted tabs:
+
+    public void testTabsDefault() throws Exception
+    {
+        _testTabsDefault(false);
+        _testTabsDefault(true);
+    }
+
+    public void testTabsEnabled() throws Exception
+    {
+        _testTabsEnabled(false);
+        _testTabsEnabled(true);
+    }
+
     /*
     /////////////////////////////////////////////////////////////////
     // Secondary test methods
@@ -156,6 +172,7 @@
         assertToken(JsonToken.START_ARRAY, jp.nextToken());
         try {
             jp.nextToken();
+            fail("Expected exception");
         } catch (JsonParseException e) {
             verifyException(e, "Unexpected character ('''");
         }
@@ -166,6 +183,7 @@
         assertToken(JsonToken.START_OBJECT, jp.nextToken());
         try {
             jp.nextToken();
+            fail("Expected exception");
         } catch (JsonParseException e) {
             verifyException(e, "Unexpected character ('''");
         }
@@ -211,4 +229,40 @@
 
         assertToken(JsonToken.END_OBJECT, jp.nextToken());
     }
+
+    // // // Tests for [JACKSON-208]
+
+    private void _testTabsDefault(boolean useStream) throws Exception
+    {
+        JsonFactory f = new JsonFactory();
+        // First, let's see that by default unquoted tabs are illegal
+        String JSON = "[\"tab:\t\"]";
+        JsonParser jp = useStream ? createParserUsingStream(f, JSON, "UTF-8") : createParserUsingReader(f, JSON);
+        assertToken(JsonToken.START_ARRAY, jp.nextToken());
+        try {
+            jp.nextToken();
+            jp.getText();
+            fail("Expected exception");
+        } catch (JsonParseException e) {
+            verifyException(e, "Illegal unquoted character");
+        }
+    }
+
+    private void _testTabsEnabled(boolean useStream) throws Exception
+    {
+        JsonFactory f = new JsonFactory();
+        f.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
+
+        String FIELD = "a\tb";
+        String VALUE = "\t";
+        String JSON = "{ "+quote(FIELD)+" : "+quote(VALUE)+"}";
+        JsonParser jp = useStream ? createParserUsingStream(f, JSON, "UTF-8") : createParserUsingReader(f, JSON);
+
+        assertToken(JsonToken.START_OBJECT, jp.nextToken());
+        assertToken(JsonToken.FIELD_NAME, jp.nextToken());
+        assertEquals(FIELD, jp.getText());
+        assertToken(JsonToken.VALUE_STRING, jp.nextToken());
+        assertEquals(VALUE, jp.getText());
+        assertToken(JsonToken.END_OBJECT, jp.nextToken());
+    }
 }
diff --git a/src/test/org/codehaus/jackson/map/BaseMapTest.java b/src/test/org/codehaus/jackson/map/BaseMapTest.java
index 964f2af..6df79af 100644
--- a/src/test/org/codehaus/jackson/map/BaseMapTest.java
+++ b/src/test/org/codehaus/jackson/map/BaseMapTest.java
@@ -117,11 +117,4 @@
     {
         return serializeAsString(new ObjectMapper(), value);
     }
-
-    public String quote(String str) {
-        return '"'+str+'"';
-    }
 }
-
-
-