Merge branch '2.5' into 2.6
diff --git a/README.md b/README.md
index 44a6b42..b76d839 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
Project contains versions 2.0 and above: source code for earlier (1.x) versions is available from [Codehaus](http://jackson.codehaus.org) SVN repository.
-[![Build Status](https://travis-ci.org/FasterXML/jackson-core.png?branch=master)](https://travis-ci.org/FasterXML/jackson-core) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.fasterxml.jackson.core/jackson-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.fasterxml.jackson.core/jackson-core)
+[![Build Status](https://travis-ci.org/FasterXML/jackson-core.svg?branch=master)](https://travis-ci.org/FasterXML/jackson-core) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.fasterxml.jackson.core/jackson-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.fasterxml.jackson.core/jackson-core)
# Get it!
@@ -38,7 +38,7 @@
</dependency>
```
-or download jars from Maven repository or links on [Wiki]](../../wiki).
+or download jars from Maven repository or links on [Wiki](../../wiki).
Core jar is a functional OSGi bundle, with proper import/export declarations.
Package has no external dependencies, except for testing (which uses `JUnit`).
diff --git a/pom.xml b/pom.xml
index 6510db2..f58af18 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,18 +3,18 @@
<parent>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-parent</artifactId>
- <version>2.5.1</version>
+ <version>2.6.2</version>
</parent>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<name>Jackson-core</name>
- <version>2.5.6-SNAPSHOT</version>
+ <version>2.6.6-SNAPSHOT</version>
<packaging>bundle</packaging>
- <description>Core Jackson abstractions, basic JSON streaming API implementation
- </description>
+ <description>Core Jackson abstractions, basic JSON streaming API implementation</description>
+ <inceptionYear>2008</inceptionYear>
- <url>https://github.com/FasterXML/jackson</url>
+ <url>https://github.com/FasterXML/jackson-core</url>
<scm>
<connection>scm:git:git@github.com:FasterXML/jackson-core.git</connection>
<developerConnection>scm:git:git@github.com:FasterXML/jackson-core.git</developerConnection>
@@ -65,6 +65,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
+ <version>${version.plugin.surefire}</version>
<configuration>
<redirectTestOutputToFile>${surefire.redirectTestOutputToFile}</redirectTestOutputToFile>
<excludes>
diff --git a/release-notes/CREDITS b/release-notes/CREDITS
index ef23c1f..4e01f69 100644
--- a/release-notes/CREDITS
+++ b/release-notes/CREDITS
@@ -54,3 +54,19 @@
* Reported #184: WRITE_NUMBERS_AS_STRINGS disables WRITE_BIGDECIMAL_AS_PLAIN
(2.4.6 / 2.5.2)
+Masaru Hasegawa (masaruh@github):
+ * Reported, contributed fix for#182: Inconsistent TextBuffer#getTextBuffer behavior
+ (2.6.0)
+
+Ruediger Moeller (RuedigerMoeller@github)
+ * Requested #195: Add `JsonGenerator.getOutputBuffered()` to find out amount of content buffered,
+ not yet flushed.
+ (2.6.0)
+
+Florian Schoppmann (fschopp@github@github)
+ * Reported #207: `ArrayIndexOutOfBoundsException` in `ByteQuadsCanonicalizer`
+ (2.6.1)
+
+Iskren Ivov Chernev (ichernev@github)
+ * Reported #213: Parser is sometimes wrong when using CANONICALIZE_FIELD_NAMES
+ (2.6.2)
diff --git a/release-notes/VERSION b/release-notes/VERSION
index f381045..00053a7 100644
--- a/release-notes/VERSION
+++ b/release-notes/VERSION
@@ -14,6 +14,58 @@
=== Releases ===
------------------------------------------------------------------------
+2.6.6 (not yet released)
+
+#248: VersionUtil.versionFor() unexpectedly return null instead of Version.unknownVersion()
+ (reported by sammyhk@github)
+
+2.6.5 (19-Jan-2015)
+2.6.4 (07-Dec-2015)
+
+No changes since 2.6.3.
+
+2.6.3 (12-Oct-2015)
+
+#220: Problem with `JsonParser.nextFieldName(SerializableString)` for byte-backed parser
+
+2.6.2 (14-Sep-2015)
+
+#213: Parser is sometimes wrong when using CANONICALIZE_FIELD_NAMES
+ (reported by ichernev@github)
+#216: ArrayIndexOutOfBoundsException: 128 when repeatedly serializing to a byte array
+ (reported by geekbeast@github)
+
+2.6.1 (09-Aug-2015)
+
+#207: `ArrayIndexOutOfBoundsException` in `ByteQuadsCanonicalizer`
+ (reported by Florian S, fschopp@github)
+
+2.6.0 (17-Jul-2015)
+
+#137: Allow filtering content read via `JsonParser` by specifying `JsonPointer`;
+ uses new class `com.fasterxml.jackson.core.filter.FilteringParserDelegate`
+ (and related, `TokenFilter`)
+#177: Add a check so `JsonGenerator.writeString()` won't work if `writeFieldName()` expected.
+#182: Inconsistent TextBuffer#getTextBuffer behavior
+ (contributed by Masaru H)
+#185: Allow filtering content written via `JsonGenerator` by specifying `JsonPointer`;
+ uses new class `com.fasterxml.jackson.core.filter.FilteringGeneratorDelegate`
+ (and related, `TokenFilter`)
+#188: `JsonParser.getValueAsString()` should return field name for `JsonToken.FIELD_NAME`, not `null`
+#189: Add `JsonFactory.Feature.USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING` (default: true), which may
+ be disabled to prevent use of ThreadLocal-based buffer recyling.
+ (suggested by soldierkam@github)
+#195: Add `JsonGenerator.getOutputBuffered()` to find out amount of content buffered,
+ not yet flushed.
+ (requested by Ruediger M)
+#196: Add support for `FormatFeature` extension, for format-specifc Enum-backed
+ parser/generator options
+- Minor improvement to construction of "default PrettyPrinter": now overridable by data format
+ modules
+- Implement a new yet more optimized symbol table for byte-backed parsers
+- Add `JsonParser.Feature.IGNORE_UNDEFINED`, useful for data formats like protobuf
+- Optimize writing of String names (remove intermediate copy; with JDK7 no speed benefit)
+
2.5.5 (07-Dec-2015)
#220: Problem with `JsonParser.nextFieldName(SerializableString)` for byte-backed parser
@@ -63,7 +115,7 @@
- Added `JsonParser.hasTokenId(id)` convenience method
- Added `JsonParser.nextFieldName()` (no args)
-2.4.6 (not released yet)
+2.4.6 (23-Apr-2015)
#184: WRITE_NUMBERS_AS_STRINGS disables WRITE_BIGDECIMAL_AS_PLAIN
(reported by Derek C)
@@ -108,6 +160,13 @@
of `String` input as well.
- Refactor `BufferRecycler` to eliminate helper enums
+2.3.5 (13-Jan-2015)
+
+#152: Exception for property names longer than 256k
+#173: An exception is thrown for a valid JsonPointer expression
+#176: `JsonPointer` should not consider "00" to be valid index
+
+2.3.4 (17-Jul-2014)
2.3.3 (10-Apr-2014)
No changes since 2.3.2.
diff --git a/src/main/java/com/fasterxml/jackson/core/FormatFeature.java b/src/main/java/com/fasterxml/jackson/core/FormatFeature.java
new file mode 100644
index 0000000..e775e4c
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/core/FormatFeature.java
@@ -0,0 +1,33 @@
+package com.fasterxml.jackson.core;
+
+/**
+ * Marker interface that is to be implemented by data format - specific features.
+ * Interface used since Java Enums can not extend classes or other Enums, but
+ * they can implement interfaces; and as such we may be able to use limited
+ * amount of generic functionality.
+ *<p>
+ * Note that this type is only implemented by non-JSON formats:
+ * types {@link JsonParser.Feature} and {@link JsonGenerator.Feature} do NOT
+ * implement it. This is to make it easier to avoid ambiguity with method
+ * calls.
+ *
+ * @since 2.6 (to be fully used in 2.7 and beyond)
+ */
+public interface FormatFeature
+{
+ /**
+ * Accessor for checking whether this feature is enabled by default.
+ */
+ public boolean enabledByDefault();
+
+ /**
+ * Returns bit mask for this feature instance; must be a single bit,
+ * that is of form <code>(1 << N)</code>
+ */
+ public int getMask();
+
+ /**
+ * Convenience method for checking whether feature is enabled in given bitmask
+ */
+ public boolean enabledIn(int flags);
+}
diff --git a/src/main/java/com/fasterxml/jackson/core/JsonFactory.java b/src/main/java/com/fasterxml/jackson/core/JsonFactory.java
index 730413f..08037aa 100644
--- a/src/main/java/com/fasterxml/jackson/core/JsonFactory.java
+++ b/src/main/java/com/fasterxml/jackson/core/JsonFactory.java
@@ -12,7 +12,7 @@
import com.fasterxml.jackson.core.format.MatchStrength;
import com.fasterxml.jackson.core.io.*;
import com.fasterxml.jackson.core.json.*;
-import com.fasterxml.jackson.core.sym.BytesToNameCanonicalizer;
+import com.fasterxml.jackson.core.sym.ByteQuadsCanonicalizer;
import com.fasterxml.jackson.core.sym.CharsToNameCanonicalizer;
import com.fasterxml.jackson.core.util.BufferRecycler;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
@@ -42,10 +42,7 @@
implements Versioned,
java.io.Serializable // since 2.1 (for Android, mostly)
{
- /**
- * Computed for Jackson 2.4.0 release
- */
- private static final long serialVersionUID = 3306684576057132431L;
+ private static final long serialVersionUID = 1; // since 2.6.0
/*
/**********************************************************
@@ -104,6 +101,21 @@
*/
FAIL_ON_SYMBOL_HASH_OVERFLOW(true),
+ /**
+ * Feature that determines whether we will use {@link BufferRecycler} with
+ * {@link ThreadLocal} and {@link SoftReference}, for efficient reuse of
+ * underlying input/output buffers.
+ * This usually makes sense on normal J2SE/J2EE server-side processing;
+ * but may not make sense on platforms where {@link SoftReference} handling
+ * is broken (like Android), or if there are retention issues due to
+ * {@link ThreadLocal} (see
+ * <a href="https://github.com/FasterXML/jackson-core/issues/189">Issue #189</a>
+ * for a possible case)
+ *
+ * @since 2.6
+ */
+ USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING(true)
+
;
/**
@@ -188,8 +200,20 @@
*<p>
* TODO: should clean up this; looks messy having 2 alternatives
* with not very clear differences.
+ *
+ * @since 2.6.0
*/
- protected final transient BytesToNameCanonicalizer _rootByteSymbols = BytesToNameCanonicalizer.createRoot();
+ protected final transient ByteQuadsCanonicalizer _byteSymbolCanonicalizer = ByteQuadsCanonicalizer.createRoot();
+
+ /**
+ * Earlier byte-based symbol table; replaced with 2.6 with a new implementation.
+ * Left in for version 2.6.0: will be removed in 2.7 or later.
+ *
+ * @deprecated Since 2.6.0, only use {@link #_byteSymbolCanonicalizer}
+ */
+ @Deprecated
+ protected final transient com.fasterxml.jackson.core.sym.BytesToNameCanonicalizer _rootByteSymbols
+ = com.fasterxml.jackson.core.sym.BytesToNameCanonicalizer.createRoot();
/*
/**********************************************************
@@ -389,10 +413,31 @@
* @since 2.4
*/
public boolean canUseCharArrays() { return true; }
-
+
+ /**
+ * Method for accessing kind of {@link FormatFeature} that a parser
+ * {@link JsonParser} produced by this factory would accept, if any;
+ * <code>null</code> returned if none.
+ *
+ * @since 2.6
+ */
+ public Class<? extends FormatFeature> getFormatReadFeatureType() {
+ return null;
+ }
+
+ /**
+ * Method for accessing kind of {@link FormatFeature} that a parser
+ * {@link JsonGenerator} produced by this factory would accept, if any;
+ * <code>null</code> returned if none.
+ *
+ * @since 2.6
+ */
+ public Class<? extends FormatFeature> getFormatWriteFeatureType() {
+ return null;
+ }
/*
/**********************************************************
- /* Format detection functionality (since 1.8)
+ /* Format detection functionality
/**********************************************************
*/
@@ -430,6 +475,10 @@
return null;
}
+ /**
+ * Convenience method for trying to determine whether input via given accessor
+ * is of format type supported by this factory.
+ */
public MatchStrength hasFormat(InputAccessor acc) throws IOException
{
// since we can't keep this abstract, only implement for "vanilla" instance
@@ -1189,7 +1238,7 @@
protected JsonParser _createParser(InputStream in, IOContext ctxt) throws IOException {
// As per [JACKSON-259], may want to fully disable canonicalization:
return new ByteSourceJsonBootstrapper(ctxt, in).constructParser(_parserFeatures,
- _objectCodec, _rootByteSymbols, _rootCharSymbols, _factoryFeatures);
+ _objectCodec, _byteSymbolCanonicalizer, _rootCharSymbols, _factoryFeatures);
}
/**
@@ -1236,7 +1285,7 @@
protected JsonParser _createParser(byte[] data, int offset, int len, IOContext ctxt) throws IOException
{
return new ByteSourceJsonBootstrapper(ctxt, data, offset, len).constructParser(_parserFeatures,
- _objectCodec, _rootByteSymbols, _rootCharSymbols, _factoryFeatures);
+ _objectCodec, _byteSymbolCanonicalizer, _rootCharSymbols, _factoryFeatures);
}
/*
@@ -1375,12 +1424,22 @@
*/
public BufferRecycler _getBufferRecycler()
{
- SoftReference<BufferRecycler> ref = _recyclerRef.get();
- BufferRecycler br = (ref == null) ? null : ref.get();
+ BufferRecycler br;
- if (br == null) {
+ /* 23-Apr-2015, tatu: Let's allow disabling of buffer recycling
+ * scheme, for cases where it is considered harmful (possibly
+ * on Android, for example)
+ */
+ if (isEnabled(Feature.USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING)) {
+ SoftReference<BufferRecycler> ref = _recyclerRef.get();
+ br = (ref == null) ? null : ref.get();
+
+ if (br == null) {
+ br = new BufferRecycler();
+ _recyclerRef.set(new SoftReference<BufferRecycler>(br));
+ }
+ } else {
br = new BufferRecycler();
- _recyclerRef.set(new SoftReference<BufferRecycler>(br));
}
return br;
}
diff --git a/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java b/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java
index 9167f3c..7d57513 100644
--- a/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java
+++ b/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java
@@ -4,19 +4,7 @@
*/
package com.fasterxml.jackson.core;
-import static com.fasterxml.jackson.core.JsonTokenId.ID_EMBEDDED_OBJECT;
-import static com.fasterxml.jackson.core.JsonTokenId.ID_END_ARRAY;
-import static com.fasterxml.jackson.core.JsonTokenId.ID_END_OBJECT;
-import static com.fasterxml.jackson.core.JsonTokenId.ID_FALSE;
-import static com.fasterxml.jackson.core.JsonTokenId.ID_FIELD_NAME;
-import static com.fasterxml.jackson.core.JsonTokenId.ID_NOT_AVAILABLE;
-import static com.fasterxml.jackson.core.JsonTokenId.ID_NULL;
-import static com.fasterxml.jackson.core.JsonTokenId.ID_NUMBER_FLOAT;
-import static com.fasterxml.jackson.core.JsonTokenId.ID_NUMBER_INT;
-import static com.fasterxml.jackson.core.JsonTokenId.ID_START_ARRAY;
-import static com.fasterxml.jackson.core.JsonTokenId.ID_START_OBJECT;
-import static com.fasterxml.jackson.core.JsonTokenId.ID_STRING;
-import static com.fasterxml.jackson.core.JsonTokenId.ID_TRUE;
+import static com.fasterxml.jackson.core.JsonTokenId.*;
import java.io.*;
import java.math.BigDecimal;
@@ -43,6 +31,8 @@
* Enumeration that defines all togglable features for generators.
*/
public enum Feature {
+ // // Low-level I/O / content features
+
/**
* Feature that determines whether generator will automatically
* close underlying output target that is NOT owned by the
@@ -71,6 +61,21 @@
AUTO_CLOSE_JSON_CONTENT(true),
/**
+ * Feature that specifies that calls to {@link #flush} will cause
+ * matching <code>flush()</code> to underlying {@link OutputStream}
+ * or {@link Writer}; if disabled this will not be done.
+ * Main reason to disable this feature is to prevent flushing at
+ * generator level, if it is not possible to prevent method being
+ * called by other code (like <code>ObjectMapper</code> or third
+ * party libraries).
+ *<p>
+ * Feature is enabled by default.
+ */
+ FLUSH_PASSED_TO_STREAM(true),
+
+ // // Quoting-related features
+
+ /**
* Feature that determines whether JSON Object field names are
* quoted using double-quotes, as specified by JSON specification
* or not. Ability to disable quoting was added to support use
@@ -125,19 +130,6 @@
WRITE_BIGDECIMAL_AS_PLAIN(false),
/**
- * Feature that specifies that calls to {@link #flush} will cause
- * matching <code>flush()</code> to underlying {@link OutputStream}
- * or {@link Writer}; if disabled this will not be done.
- * Main reason to disable this feature is to prevent flushing at
- * generator level, if it is not possible to prevent method being
- * called by other code (like <code>ObjectMapper</code> or third
- * party libraries).
- *<p>
- * Feature is enabled by default.
- */
- FLUSH_PASSED_TO_STREAM(true),
-
- /**
* Feature that specifies that all characters beyond 7-bit ASCII
* range (i.e. code points of 128 and above) need to be output
* using format-specific escapes (for JSON, backslash escapes),
@@ -154,6 +146,8 @@
*/
ESCAPE_NON_ASCII(false),
+ // // Schema/Validity support features
+
/**
* Feature that determines whether {@link JsonGenerator} will explicitly
* check that no duplicate JSON Object field names are written.
@@ -170,7 +164,7 @@
* @since 2.3
*/
STRICT_DUPLICATE_DETECTION(false),
-
+
/**
* Feature that determines what to do if the underlying data format requires knowledge
* of all properties to output, and if no definition is found for a property that
@@ -270,53 +264,6 @@
@Override
public abstract Version version();
- /**
- * Method that can be used to get access to object that is used
- * as target for generated output; this is usually either
- * {@link OutputStream} or {@link Writer}, depending on what
- * generator was constructed with.
- * Note that returned value may be null in some cases; including
- * case where implementation does not want to exposed raw
- * source to caller.
- * In cases where output has been decorated, object returned here
- * is the decorated version; this allows some level of interaction
- * between users of generator and decorator object.
- *<p>
- * In general use of this accessor should be considered as
- * "last effort", i.e. only used if no other mechanism is applicable.
- */
- public Object getOutputTarget() {
- return null;
- }
-
- /**
- * Helper method, usually equivalent to:
- *<code>
- * getOutputContext().getCurrentValue();
- *</code>
- *
- * @since 2.5
- */
- public Object getCurrentValue() {
- JsonStreamContext ctxt = getOutputContext();
- return (ctxt == null) ? null : ctxt.getCurrentValue();
- }
-
- /**
- * Helper method, usually equivalent to:
- *<code>
- * getOutputContext().setCurrentValue(v);
- *</code>
- *
- * @since 2.5
- */
- public void setCurrentValue(Object v) {
- JsonStreamContext ctxt = getOutputContext();
- if (ctxt != null) {
- ctxt.setCurrentValue(v);
- }
- }
-
/*
/**********************************************************
/* Public API, Feature configuration
@@ -358,9 +305,10 @@
/**
- * Bulk access method for getting state of all standard {@link Feature}s.
+ * Bulk access method for getting state of all standard (non-dataformat-specific)
+ * {@link JsonGenerator.Feature}s.
*
- * @return Bit mask that defines current states of all standard {@link Feature}s.
+ * @return Bit mask that defines current states of all standard {@link JsonGenerator.Feature}s.
*
* @since 2.3
*/
@@ -378,6 +326,60 @@
*/
public abstract JsonGenerator setFeatureMask(int values);
+ /**
+ * Bulk set method for (re)setting states of features specified by <code>mask</code>.
+ * Functionally equivalent to
+ *<code>
+ * int oldState = getFeatureMask();
+ * int newState = (oldState & ~mask) | (values & mask);
+ * setFeatureMask(newState);
+ *</code>
+ *
+ * @param values Bit mask of set/clear state for features to change
+ * @param mask Bit mask of features to change
+ *
+ * @since 2.6
+ */
+ public JsonGenerator overrideStdFeatures(int values, int mask) {
+ int oldState = getFeatureMask();
+ int newState = (oldState & ~mask) | (values & mask);
+ return setFeatureMask(newState);
+ }
+
+ /**
+ * Bulk access method for getting state of all {@link FormatFeature}s, format-specific
+ * on/off configuration settings.
+ *
+ * @return Bit mask that defines current states of all standard {@link FormatFeature}s.
+ *
+ * @since 2.6
+ */
+ public int getFormatFeatures() {
+ return 0;
+ }
+
+ /**
+ * Bulk set method for (re)setting states of {@link FormatFeature}s,
+ * by specifying values (set / clear) along with a mask, to determine
+ * which features to change, if any.
+ *<p>
+ * Default implementation will simply throw an exception to indicate that
+ * the generator implementation does not support any {@link FormatFeature}s.
+ *
+ * @param values Bit mask of set/clear state for features to change
+ * @param mask Bit mask of features to change
+ *
+ * @since 2.6
+ */
+ public JsonGenerator overrideFormatFeatures(int values, int mask) {
+ throw new IllegalArgumentException("No FormatFeatures defined for generator of type "+getClass().getName());
+ /*
+ int oldState = getFeatureMask();
+ int newState = (oldState & ~mask) | (values & mask);
+ return setFeatureMask(newState);
+ */
+ }
+
/*
/**********************************************************
/* Public API, Schema configuration
@@ -521,6 +523,86 @@
/*
/**********************************************************
+ /* Public API, output state access
+ /**********************************************************
+ */
+
+ /**
+ * Method that can be used to get access to object that is used
+ * as target for generated output; this is usually either
+ * {@link OutputStream} or {@link Writer}, depending on what
+ * generator was constructed with.
+ * Note that returned value may be null in some cases; including
+ * case where implementation does not want to exposed raw
+ * source to caller.
+ * In cases where output has been decorated, object returned here
+ * is the decorated version; this allows some level of interaction
+ * between users of generator and decorator object.
+ *<p>
+ * In general use of this accessor should be considered as
+ * "last effort", i.e. only used if no other mechanism is applicable.
+ */
+ public Object getOutputTarget() {
+ return null;
+ }
+
+ /**
+ * Method for verifying amount of content that is buffered by generator
+ * but not yet flushed to the underlying target (stream, writer),
+ * in units (byte, char) that the generator implementation uses for buffering;
+ * or -1 if this information is not available.
+ * Unit used is often the same as the unit of underlying target (that is,
+ * `byte` for {@link java.io.OutputStream}, `char` for {@link java.io.Writer}),
+ * but may differ if buffering is done before encoding.
+ * Default JSON-backed implementations do use matching units.
+ *<p>
+ * Note: non-JSON implementations will be retrofitted for 2.6 and beyond;
+ * please report if you see -1 (missing override)
+ *
+ * @return Amount of content buffered in internal units, if amount known and
+ * accessible; -1 if not accessible.
+ *
+ * @since 2.6
+ */
+ public int getOutputBuffered() {
+ return -1;
+ }
+
+ /**
+ * Helper method, usually equivalent to:
+ *<code>
+ * getOutputContext().getCurrentValue();
+ *</code>
+ *<p>
+ * Note that "current value" is NOT populated (or used) by Streaming parser;
+ * it is only used by higher-level data-binding functionality.
+ * The reason it is included here is that it can be stored and accessed hierarchically,
+ * and gets passed through data-binding.
+ *
+ * @since 2.5
+ */
+ public Object getCurrentValue() {
+ JsonStreamContext ctxt = getOutputContext();
+ return (ctxt == null) ? null : ctxt.getCurrentValue();
+ }
+
+ /**
+ * Helper method, usually equivalent to:
+ *<code>
+ * getOutputContext().setCurrentValue(v);
+ *</code>
+ *
+ * @since 2.5
+ */
+ public void setCurrentValue(Object v) {
+ JsonStreamContext ctxt = getOutputContext();
+ if (ctxt != null) {
+ ctxt.setCurrentValue(v);
+ }
+ }
+
+ /*
+ /**********************************************************
/* Public API, capability introspection methods
/**********************************************************
*/
@@ -644,8 +726,7 @@
* are allowed: meaning everywhere except for when
* a field name is expected.
*/
- public abstract void writeStartObject()
- throws IOException;
+ public abstract void writeStartObject() throws IOException;
/**
* Method for writing closing marker of a JSON Object value
@@ -657,8 +738,7 @@
* complete value, or START-OBJECT marker (see JSON specification
* for more details).
*/
- public abstract void writeEndObject()
- throws IOException;
+ public abstract void writeEndObject() throws IOException;
/**
* Method for writing a field name (JSON String surrounded by
@@ -669,8 +749,7 @@
* JSON specification for details), when field name is expected
* (field names alternate with values).
*/
- public abstract void writeFieldName(String name)
- throws IOException;
+ public abstract void writeFieldName(String name) throws IOException;
/**
* Method similar to {@link #writeFieldName(String)}, main difference
@@ -683,8 +762,7 @@
* serialized String; implementations are strongly encouraged to make
* use of more efficient methods argument object has.
*/
- public abstract void writeFieldName(SerializableString name)
- throws IOException;
+ public abstract void writeFieldName(SerializableString name) throws IOException;
/*
/**********************************************************
@@ -699,8 +777,7 @@
* surrounded in double quotes, and contents will be properly
* escaped as required by JSON specification.
*/
- public abstract void writeString(String text)
- throws IOException;
+ public abstract void writeString(String text) throws IOException;
/**
* Method for outputting a String value. Depending on context
@@ -709,8 +786,7 @@
* surrounded in double quotes, and contents will be properly
* escaped as required by JSON specification.
*/
- public abstract void writeString(char[] text, int offset, int len)
- throws IOException;
+ public abstract void writeString(char[] text, int offset, int len) throws IOException;
/**
* Method similar to {@link #writeString(String)}, but that takes
@@ -722,8 +798,7 @@
* sub-classes should override it with more efficient implementation
* if possible.
*/
- public abstract void writeString(SerializableString text)
- throws IOException;
+ public abstract void writeString(SerializableString text) throws IOException;
/**
* Method similar to {@link #writeString(String)} but that takes as
diff --git a/src/main/java/com/fasterxml/jackson/core/JsonParser.java b/src/main/java/com/fasterxml/jackson/core/JsonParser.java
index 1241164..0af1d43 100644
--- a/src/main/java/com/fasterxml/jackson/core/JsonParser.java
+++ b/src/main/java/com/fasterxml/jackson/core/JsonParser.java
@@ -185,7 +185,31 @@
* @since 2.3
*/
STRICT_DUPLICATE_DETECTION(false),
- ;
+
+ /**
+ * Feature that determines what to do if the underlying data format requires knowledge
+ * of all properties to decode (usually via a Schema), and if no definition is
+ * found for a property that input content contains.
+ * Typically most textual data formats do NOT require schema information (although
+ * some do, such as CSV), whereas many binary data formats do require definitions
+ * (such as Avro, protobuf), although not all (Smile, CBOR, BSON and MessagePack do not).
+ * Further note that some formats that do require schema information will not be able
+ * to ignore undefined properties: for example, Avro is fully positional and there is
+ * no possibility of undefined data. This leaves formats like Protobuf that have identifiers
+ * that may or may not map; and as such Protobuf format does make use of this feature.
+ *<p>
+ * Note that support for this feature is implemented by individual data format
+ * module, if (and only if) it makes sense for the format in question. For JSON,
+ * for example, this feature has no effect as properties need not be pre-defined.
+ *<p>
+ * Feature is disabled by default, meaning that if the underlying data format
+ * requires knowledge of all properties to output, attempts to read an unknown
+ * property will result in a {@link JsonProcessingException}
+ *
+ * @since 2.6
+ */
+ IGNORE_UNDEFINED(false)
+ ;
/**
* Whether feature is enabled or disabled by default.
@@ -282,6 +306,11 @@
*<code>
* getParsingContext().getCurrentValue();
*</code>
+ *<p>
+ * Note that "current value" is NOT populated (or used) by Streaming parser;
+ * it is only used by higher-level data-binding functionality.
+ * The reason it is included here is that it can be stored and accessed hierarchically,
+ * and gets passed through data-binding.
*
* @since 2.5
*/
@@ -496,16 +525,67 @@
/**
* Bulk set method for (re)setting states of all standard {@link Feature}s
- *
- * @since 2.3
*
* @return This parser object, to allow chaining of calls
+ *
+ * @since 2.3
*/
public JsonParser setFeatureMask(int mask) {
_features = mask;
return this;
}
+
+ /**
+ * Bulk set method for (re)setting states of features specified by <code>mask</code>.
+ * Functionally equivalent to
+ *<code>
+ * int oldState = getFeatureMask();
+ * int newState = (oldState & ~mask) | (values & mask);
+ * setFeatureMask(newState);
+ *</code>
+ *
+ * @param values Bit mask of set/clear state for features to change
+ * @param mask Bit mask of features to change
+ *
+ * @since 2.6
+ */
+ public JsonParser overrideStdFeatures(int values, int mask) {
+ _features = (_features & ~mask) | (values & mask);
+ return this;
+ }
+
+ /**
+ * Bulk access method for getting state of all {@link FormatFeature}s, format-specific
+ * on/off configuration settings.
+ *
+ * @return Bit mask that defines current states of all standard {@link FormatFeature}s.
+ *
+ * @since 2.6
+ */
+ public int getFormatFeatures() {
+ return 0;
+ }
+ /**
+ * Bulk set method for (re)setting states of {@link FormatFeature}s,
+ * by specifying values (set / clear) along with a mask, to determine
+ * which features to change, if any.
+ *<p>
+ * Default implementation will simply throw an exception to indicate that
+ * the generator implementation does not support any {@link FormatFeature}s.
+ *
+ * @param values Bit mask of set/clear state for features to change
+ * @param mask Bit mask of features to change
+ *
+ * @since 2.6
+ */
+ public JsonParser overrideFormatFeatures(int values, int mask) {
+ throw new IllegalArgumentException("No FormatFeatures defined for parser of type "+getClass().getName());
+ /*
+ _formatFeatures = (_formatFeatures & ~mask) | (values & mask);
+ */
+ }
+
/*
/**********************************************************
/* Public API, traversal
@@ -568,8 +648,7 @@
* @since 2.5
*/
public String nextFieldName() throws IOException, JsonParseException {
- return (nextToken() == JsonToken.FIELD_NAME)
- ? getCurrentName() : null;
+ return (nextToken() == JsonToken.FIELD_NAME) ? getCurrentName() : null;
}
/**
@@ -727,6 +806,21 @@
public abstract boolean hasTokenId(int id);
/**
+ * Method that is functionally equivalent to:
+ *<code>
+ * return getCurrentTokenId() == id
+ *</code>
+ * but may be more efficiently implemented.
+ *<p>
+ * Note that no traversal or conversion is performed; so in some
+ * cases calling method like {@link #isExpectedStartArrayToken()}
+ * is necessary instead.
+ *
+ * @since 2.6
+ */
+ public abstract boolean hasToken(JsonToken t);
+
+ /**
* Method that can be called to get the name associated with
* the current token: for {@link JsonToken#FIELD_NAME}s it will
* be the same as what {@link #getText} returns;
diff --git a/src/main/java/com/fasterxml/jackson/core/JsonPointer.java b/src/main/java/com/fasterxml/jackson/core/JsonPointer.java
index 59ef9ae..ddbab6d 100644
--- a/src/main/java/com/fasterxml/jackson/core/JsonPointer.java
+++ b/src/main/java/com/fasterxml/jackson/core/JsonPointer.java
@@ -208,10 +208,10 @@
}
public JsonPointer matchProperty(String name) {
- if (_nextSegment == null || !_matchingPropertyName.equals(name)) {
- return null;
+ if ((_nextSegment != null) && _matchingPropertyName.equals(name)) {
+ return _nextSegment;
}
- return _nextSegment;
+ return null;
}
/**
@@ -225,6 +225,16 @@
}
/**
+ * @since 2.6
+ */
+ public JsonPointer matchElement(int index) {
+ if ((index != _matchingElementIndex) || (index < 0)) {
+ return null;
+ }
+ return _nextSegment;
+ }
+
+ /**
* Accessor for getting a "sub-pointer", instance where current segment
* has been removed and pointer includes rest of segments.
* For matching state, will return null.
diff --git a/src/main/java/com/fasterxml/jackson/core/JsonStreamContext.java b/src/main/java/com/fasterxml/jackson/core/JsonStreamContext.java
index 9e0e31f..5e8ff17 100644
--- a/src/main/java/com/fasterxml/jackson/core/JsonStreamContext.java
+++ b/src/main/java/com/fasterxml/jackson/core/JsonStreamContext.java
@@ -108,9 +108,11 @@
* Method for accessing currently active value being used by data-binding
* (as the source of streaming data to write, or destination of data being
* read), at this level in hierarchy.
- * The value may not exist or be available due to various limitations (at
- * least during reading of data, as target value object may not have yet
- * been constructed).
+ *<p>
+ * Note that "current value" is NOT populated (or used) by Streaming parser or generator;
+ * it is only used by higher-level data-binding functionality.
+ * The reason it is included here is that it can be stored and accessed hierarchically,
+ * and gets passed through data-binding.
*
* @return Currently active value, if one has been assigned.
*
diff --git a/src/main/java/com/fasterxml/jackson/core/PrettyPrinter.java b/src/main/java/com/fasterxml/jackson/core/PrettyPrinter.java
index 17716ae..8246453 100644
--- a/src/main/java/com/fasterxml/jackson/core/PrettyPrinter.java
+++ b/src/main/java/com/fasterxml/jackson/core/PrettyPrinter.java
@@ -57,7 +57,7 @@
* to output a curly bracket as well, but can surround that
* with other (white-space) decoration.
*/
- void writeStartObject(JsonGenerator jg)
+ void writeStartObject(JsonGenerator gen)
throws IOException, JsonGenerationException;
/**
@@ -73,7 +73,7 @@
* @param nrOfEntries Number of direct members of the array that
* have been output
*/
- void writeEndObject(JsonGenerator jg, int nrOfEntries)
+ void writeEndObject(JsonGenerator gen, int nrOfEntries)
throws IOException, JsonGenerationException;
/**
@@ -85,7 +85,7 @@
* to output a comma as well, but can surround that with other
* (white-space) decoration.
*/
- void writeObjectEntrySeparator(JsonGenerator jg)
+ void writeObjectEntrySeparator(JsonGenerator gen)
throws IOException, JsonGenerationException;
/**
@@ -97,7 +97,7 @@
* to output a colon as well, but can surround that with other
* (white-space) decoration.
*/
- void writeObjectFieldValueSeparator(JsonGenerator jg)
+ void writeObjectFieldValueSeparator(JsonGenerator gen)
throws IOException, JsonGenerationException;
// // // Array handling
@@ -112,7 +112,7 @@
* to output a bracket as well, but can surround that
* with other (white-space) decoration.
*/
- void writeStartArray(JsonGenerator jg)
+ void writeStartArray(JsonGenerator gen)
throws IOException, JsonGenerationException;
/**
@@ -128,7 +128,7 @@
* @param nrOfValues Number of direct members of the array that
* have been output
*/
- void writeEndArray(JsonGenerator jg, int nrOfValues)
+ void writeEndArray(JsonGenerator gen, int nrOfValues)
throws IOException, JsonGenerationException;
/**
@@ -140,7 +140,7 @@
* to output a comma as well, but can surround that with other
* (white-space) decoration.
*/
- void writeArrayValueSeparator(JsonGenerator jg)
+ void writeArrayValueSeparator(JsonGenerator gen)
throws IOException, JsonGenerationException;
/*
@@ -159,7 +159,7 @@
* Default handling does not output anything, but pretty-printer
* is free to add any white space decoration.
*/
- void beforeArrayValues(JsonGenerator jg)
+ void beforeArrayValues(JsonGenerator gen)
throws IOException, JsonGenerationException;
/**
@@ -171,7 +171,7 @@
* Default handling does not output anything, but pretty-printer
* is free to add any white space decoration.
*/
- void beforeObjectEntries(JsonGenerator jg)
+ void beforeObjectEntries(JsonGenerator gen)
throws IOException, JsonGenerationException;
}
diff --git a/src/main/java/com/fasterxml/jackson/core/Version.java b/src/main/java/com/fasterxml/jackson/core/Version.java
index e57a7c5..884d3d7 100644
--- a/src/main/java/com/fasterxml/jackson/core/Version.java
+++ b/src/main/java/com/fasterxml/jackson/core/Version.java
@@ -66,7 +66,7 @@
public boolean isUknownVersion() { return (this == UNKNOWN_VERSION); }
- // Added in patch 2.5.6 (added for good in 2.7)
+ // Added in patch 2.6.6 (added for good in 2.7)
public boolean isUnknownVersion() { return (this == UNKNOWN_VERSION); }
public boolean isSnapshot() { return (_snapshotInfo != null && _snapshotInfo.length() > 0); }
diff --git a/src/main/java/com/fasterxml/jackson/core/base/GeneratorBase.java b/src/main/java/com/fasterxml/jackson/core/base/GeneratorBase.java
index 68f699b..f806bdf 100644
--- a/src/main/java/com/fasterxml/jackson/core/base/GeneratorBase.java
+++ b/src/main/java/com/fasterxml/jackson/core/base/GeneratorBase.java
@@ -31,6 +31,15 @@
| Feature.STRICT_DUPLICATE_DETECTION.getMask()
;
+ // // // Constants for validation messages (since 2.6)
+
+ protected final String WRITE_BINARY = "write a binary value";
+ protected final String WRITE_BOOLEAN = "write a boolean value";
+ protected final String WRITE_NULL = "write a null";
+ protected final String WRITE_NUMBER = "write a number";
+ protected final String WRITE_RAW = "write a raw (unencoded) value";
+ protected final String WRITE_STRING = "write a string";
+
/*
/**********************************************************
/* Configuration
@@ -100,8 +109,9 @@
}
/**
- * Implemented with detection that tries to find "VERSION.txt" in same
- * package as the implementation class.
+ * Implemented with standard version number detection algorithm, typically using
+ * a simple generated class, with information extracted from Maven project file
+ * during build.
*/
@Override public Version version() { return VersionUtil.versionFor(getClass()); }
@@ -193,7 +203,7 @@
if (getPrettyPrinter() != null) {
return this;
}
- return setPrettyPrinter(new DefaultPrettyPrinter());
+ return setPrettyPrinter(_constructDefaultPrettyPrinter());
}
@Override public JsonGenerator setCodec(ObjectCodec oc) {
@@ -363,6 +373,16 @@
*/
protected abstract void _verifyValueWrite(String typeMsg) throws IOException;
+ /**
+ * Overridable factory method called to instantiate an appropriate {@link PrettyPrinter}
+ * for case of "just use the default one", when {@link #useDefaultPrettyPrinter()} is called.
+ *
+ * @since 2.6
+ */
+ protected PrettyPrinter _constructDefaultPrettyPrinter() {
+ return new DefaultPrettyPrinter();
+ }
+
/*
/**********************************************************
/* UTF-8 related helper method(s)
diff --git a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java
index a197c38..7c97e2a 100644
--- a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java
+++ b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java
@@ -281,8 +281,7 @@
*/
protected ParserBase(IOContext ctxt, int features) {
- super();
- _features = features;
+ super(features);
_ioContext = ctxt;
_textBuffer = ctxt.constructTextBuffer();
DupDetector dups = Feature.STRICT_DUPLICATE_DETECTION.enabledIn(features)
@@ -654,7 +653,7 @@
{
if ((_numTypesValid & NR_INT) == 0) {
if (_numTypesValid == NR_UNKNOWN) { // not parsed at all
- _parseNumericValue(NR_INT); // will also check event type
+ return _parseIntValue();
}
if ((_numTypesValid & NR_INT) == 0) { // wasn't an int natively?
convertNumberToInt(); // let's make it so, if possible
@@ -799,7 +798,38 @@
}
_reportError("Current token ("+_currToken+") not numeric, can not use numeric value accessors");
}
-
+
+ /**
+ * @since 2.6
+ */
+ protected int _parseIntValue() throws IOException
+ {
+ // Inlined variant of: _parseNumericValue(NR_INT)
+
+ if (_currToken == JsonToken.VALUE_NUMBER_INT) {
+ char[] buf = _textBuffer.getTextBuffer();
+ int offset = _textBuffer.getTextOffset();
+ int len = _intLength;
+ if (_numberNegative) {
+ ++offset;
+ }
+ if (len <= 9) {
+ int i = NumberInput.parseInt(buf, offset, len);
+ if (_numberNegative) {
+ i = -i;
+ }
+ _numberInt = i;
+ _numTypesValid = NR_INT;
+ return i;
+ }
+ }
+ _parseNumericValue(NR_INT);
+ if ((_numTypesValid & NR_INT) == 0) {
+ convertNumberToInt();
+ }
+ return _numberInt;
+ }
+
private void _parseSlowFloat(int expType) throws IOException
{
/* Nope: floating point. Here we need to be careful to get
diff --git a/src/main/java/com/fasterxml/jackson/core/base/ParserMinimalBase.java b/src/main/java/com/fasterxml/jackson/core/base/ParserMinimalBase.java
index f030a3f..310ee4e 100644
--- a/src/main/java/com/fasterxml/jackson/core/base/ParserMinimalBase.java
+++ b/src/main/java/com/fasterxml/jackson/core/base/ParserMinimalBase.java
@@ -49,7 +49,7 @@
/* Minimal generally useful state
/**********************************************************
*/
-
+
/**
* Last token retrieved via {@link #nextToken}, if any.
* Null before the first call to <code>nextToken()</code>,
@@ -62,7 +62,7 @@
* effect when {@link #clearCurrentToken} was called.
*/
protected JsonToken _lastClearedToken;
-
+
/*
/**********************************************************
/* Life-cycle
@@ -74,7 +74,7 @@
// NOTE: had base impl in 2.3 and before; but shouldn't
// public abstract Version version();
-
+
/*
/**********************************************************
/* Configuration overrides if any
@@ -97,7 +97,7 @@
@Override public abstract JsonToken nextToken() throws IOException;
@Override public JsonToken getCurrentToken() { return _currToken; }
- @Override public final int getCurrentTokenId() {
+ @Override public int getCurrentTokenId() {
final JsonToken t = _currToken;
return (t == null) ? JsonTokenId.ID_NO_TOKEN : t.id();
}
@@ -111,6 +111,10 @@
return t.id() == id;
}
+ @Override public boolean hasToken(JsonToken t) {
+ return (_currToken == t);
+ }
+
@Override public boolean isExpectedStartArrayToken() { return _currToken == JsonToken.START_ARRAY; }
@Override public boolean isExpectedStartObjectToken() { return _currToken == JsonToken.START_OBJECT; }
@@ -258,9 +262,28 @@
}
@Override
+ public int getValueAsInt() throws IOException
+ {
+ JsonToken t = _currToken;
+ if (t == JsonToken.VALUE_NUMBER_INT) {
+ return getIntValue();
+ }
+ if (t == JsonToken.VALUE_NUMBER_FLOAT) {
+ return getIntValue();
+ }
+ return getValueAsInt(0);
+ }
+
+ @Override
public int getValueAsInt(int defaultValue) throws IOException
{
JsonToken t = _currToken;
+ if (t == JsonToken.VALUE_NUMBER_INT) {
+ return getIntValue();
+ }
+ if (t == JsonToken.VALUE_NUMBER_FLOAT) {
+ return getIntValue();
+ }
if (t != null) {
switch (t.id()) {
case ID_STRING:
@@ -269,9 +292,6 @@
return 0;
}
return NumberInput.parseAsInt(str, defaultValue);
- case ID_NUMBER_INT:
- case ID_NUMBER_FLOAT:
- return getIntValue();
case ID_TRUE:
return 1;
case ID_FALSE:
@@ -287,11 +307,30 @@
}
return defaultValue;
}
+
+ @Override
+ public long getValueAsLong() throws IOException
+ {
+ JsonToken t = _currToken;
+ if (t == JsonToken.VALUE_NUMBER_INT) {
+ return getLongValue();
+ }
+ if (t == JsonToken.VALUE_NUMBER_FLOAT) {
+ return getLongValue();
+ }
+ return getValueAsLong(0L);
+ }
@Override
public long getValueAsLong(long defaultValue) throws IOException
{
JsonToken t = _currToken;
+ if (t == JsonToken.VALUE_NUMBER_INT) {
+ return getLongValue();
+ }
+ if (t == JsonToken.VALUE_NUMBER_FLOAT) {
+ return getLongValue();
+ }
if (t != null) {
switch (t.id()) {
case ID_STRING:
@@ -300,9 +339,6 @@
return 0L;
}
return NumberInput.parseAsLong(str, defaultValue);
- case ID_NUMBER_INT:
- case ID_NUMBER_FLOAT:
- return getLongValue();
case ID_TRUE:
return 1L;
case ID_FALSE:
@@ -349,11 +385,26 @@
}
@Override
+ public String getValueAsString() throws IOException {
+ if (_currToken == JsonToken.VALUE_STRING) {
+ return getText();
+ }
+ if (_currToken == JsonToken.FIELD_NAME) {
+ return getCurrentName();
+ }
+ return getValueAsString(null);
+ }
+
+ @Override
public String getValueAsString(String defaultValue) throws IOException {
- if (_currToken != JsonToken.VALUE_STRING) {
- if (_currToken == null || _currToken == JsonToken.VALUE_NULL || !_currToken.isScalarValue()) {
- return defaultValue;
- }
+ if (_currToken == JsonToken.VALUE_STRING) {
+ return getText();
+ }
+ if (_currToken == JsonToken.FIELD_NAME) {
+ return getCurrentName();
+ }
+ if (_currToken == null || _currToken == JsonToken.VALUE_NULL || !_currToken.isScalarValue()) {
+ return defaultValue;
}
return getText();
}
@@ -378,42 +429,6 @@
}
}
- /**
- * @param bindex Relative index within base64 character unit; between 0
- * and 3 (as unit has exactly 4 characters)
- *
- * @deprecated in 2.2.3; should migrate away
- */
- @Deprecated
- protected void _reportInvalidBase64(Base64Variant b64variant, char ch, int bindex, String msg)
- throws JsonParseException
- {
- String base;
- if (ch <= INT_SPACE) {
- base = "Illegal white space character (code 0x"+Integer.toHexString(ch)+") as character #"+(bindex+1)+" of 4-char base64 unit: can only used between units";
- } else if (b64variant.usesPaddingChar(ch)) {
- base = "Unexpected padding character ('"+b64variant.getPaddingChar()+"') as character #"+(bindex+1)+" of 4-char base64 unit: padding only legal as 3rd or 4th character";
- } else if (!Character.isDefined(ch) || Character.isISOControl(ch)) {
- // Not sure if we can really get here... ? (most illegal xml chars are caught at lower level)
- base = "Illegal character (code 0x"+Integer.toHexString(ch)+") in base64 content";
- } else {
- base = "Illegal character '"+ch+"' (code 0x"+Integer.toHexString(ch)+") in base64 content";
- }
- if (msg != null) {
- base = base + ": " + msg;
- }
- throw _constructError(base);
- }
-
- /**
- *
- * @deprecated in 2.2.3; should migrate away
- */
- @Deprecated
- protected void _reportBase64EOF() throws JsonParseException {
- throw _constructError("Unexpected end-of-String in base64 content");
- }
-
/*
/**********************************************************
/* Coercion helper methods (overridable)
diff --git a/src/main/java/com/fasterxml/jackson/core/filter/FilteringGeneratorDelegate.java b/src/main/java/com/fasterxml/jackson/core/filter/FilteringGeneratorDelegate.java
new file mode 100644
index 0000000..8468a37
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/core/filter/FilteringGeneratorDelegate.java
@@ -0,0 +1,868 @@
+package com.fasterxml.jackson.core.filter;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import com.fasterxml.jackson.core.*;
+import com.fasterxml.jackson.core.util.JsonGeneratorDelegate;
+
+/**
+ * Specialized {@link JsonGeneratorDelegate} that allows use of
+ * {@link TokenFilter} for outputting a subset of content that
+ * caller tries to generate.
+ *
+ * @since 2.6
+ */
+public class FilteringGeneratorDelegate extends JsonGeneratorDelegate
+{
+ /*
+ /**********************************************************
+ /* Configuration
+ /**********************************************************
+ */
+
+ /**
+ * Object consulted to determine whether to write parts of content generator
+ * is asked to write or not.
+ */
+ protected TokenFilter rootFilter;
+
+ /**
+ * Flag that determines whether filtering will continue after the first
+ * match is indicated or not: if `false`, output is based on just the first
+ * full match (returning {@link TokenFilter#INCLUDE_ALL}) and no more
+ * checks are made; if `true` then filtering will be applied as necessary
+ * until end of content.
+ */
+ protected boolean _allowMultipleMatches;
+
+ /**
+ * Flag that determines whether path leading up to included content should
+ * also be automatically included or not. If `false`, no path inclusion is
+ * done and only explicitly included entries are output; if `true` then
+ * path from main level down to match is also included as necessary.
+ */
+ protected boolean _includePath;
+
+ /* NOTE: this feature is included in the first version (2.6), but
+ * there is no public API to enable it, yet, since there isn't an
+ * actual use case. But it seemed possible need could arise, which
+ * is feature has not yet been removed. If no use is found within
+ * first version or two, just remove.
+ *
+ * Marked as deprecated since its status is uncertain.
+ */
+ @Deprecated
+ protected boolean _includeImmediateParent = false;
+
+ /*
+ /**********************************************************
+ /* Additional state
+ /**********************************************************
+ */
+
+ /**
+ * Although delegate has its own output context it is not sufficient since we actually
+ * have to keep track of excluded (filtered out) structures as well as ones delegate
+ * actually outputs.
+ */
+ protected TokenFilterContext _filterContext;
+
+ /**
+ * State that applies to the item within container, used where applicable.
+ * Specifically used to pass inclusion state between property name and
+ * property, and also used for array elements.
+ */
+ protected TokenFilter _itemFilter;
+
+ /**
+ * Number of tokens for which {@link TokenFilter#INCLUDE_ALL}
+ * has been returned
+ */
+ protected int _matchCount;
+
+ /*
+ /**********************************************************
+ /* Construction, initialization
+ /**********************************************************
+ */
+
+ public FilteringGeneratorDelegate(JsonGenerator d, TokenFilter f,
+ boolean includePath, boolean allowMultipleMatches)
+ {
+ // By default, do NOT delegate copy methods
+ super(d, false);
+ rootFilter = f;
+ // and this is the currently active filter for root values
+ _itemFilter = f;
+ _filterContext = TokenFilterContext.createRootContext(f);
+ _includePath = includePath;
+ _allowMultipleMatches = allowMultipleMatches;
+ }
+
+ /*
+ /**********************************************************
+ /* Extended API
+ /**********************************************************
+ */
+
+ public TokenFilter getFilter() { return rootFilter; }
+
+ public JsonStreamContext getFilterContext() {
+ return _filterContext;
+ }
+
+ /**
+ * Accessor for finding number of matches, where specific token and sub-tree
+ * starting (if structured type) are passed.
+ */
+ public int getMatchCount() {
+ return _matchCount;
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, accessors
+ /**********************************************************
+ */
+
+ @Override
+ public JsonStreamContext getOutputContext() {
+ /* 11-Apr-2015, tatu: Choice is between pre- and post-filter context;
+ * let's expose post-filter context that correlates with the view
+ * of caller.
+ */
+ return _filterContext;
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, write methods, structural
+ /**********************************************************
+ */
+
+ @Override
+ public void writeStartArray() throws IOException
+ {
+ // First things first: whole-sale skipping easy
+ if (_itemFilter == null) {
+ _filterContext = _filterContext.createChildArrayContext(null, false);
+ return;
+ }
+ if (_itemFilter == TokenFilter.INCLUDE_ALL) { // include the whole sub-tree?
+ _filterContext = _filterContext.createChildArrayContext(_itemFilter, true);
+ delegate.writeStartArray();
+ return;
+ }
+ // Ok; regular checking state then
+ _itemFilter = _filterContext.checkValue(_itemFilter);
+ if (_itemFilter == null) {
+ _filterContext = _filterContext.createChildArrayContext(null, false);
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ _itemFilter = _itemFilter.filterStartArray();
+ }
+ if (_itemFilter == TokenFilter.INCLUDE_ALL) {
+ _checkParentPath();
+ _filterContext = _filterContext.createChildArrayContext(_itemFilter, true);
+ delegate.writeStartArray();
+ } else {
+ _filterContext = _filterContext.createChildArrayContext(_itemFilter, false);
+ }
+ }
+
+ @Override
+ public void writeStartArray(int size) throws IOException
+ {
+ if (_itemFilter == null) {
+ _filterContext = _filterContext.createChildArrayContext(null, false);
+ return;
+ }
+ if (_itemFilter == TokenFilter.INCLUDE_ALL) {
+ _filterContext = _filterContext.createChildArrayContext(_itemFilter, true);
+ delegate.writeStartArray(size);
+ return;
+ }
+ _itemFilter = _filterContext.checkValue(_itemFilter);
+ if (_itemFilter == null) {
+ _filterContext = _filterContext.createChildArrayContext(null, false);
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ _itemFilter = _itemFilter.filterStartArray();
+ }
+ if (_itemFilter == TokenFilter.INCLUDE_ALL) {
+ _checkParentPath();
+ _filterContext = _filterContext.createChildArrayContext(_itemFilter, true);
+ delegate.writeStartArray(size);
+ } else {
+ _filterContext = _filterContext.createChildArrayContext(_itemFilter, false);
+ }
+ }
+
+ @Override
+ public void writeEndArray() throws IOException
+ {
+ _filterContext = _filterContext.closeArray(delegate);
+
+ if (_filterContext != null) {
+ _itemFilter = _filterContext.getFilter();
+ }
+ }
+
+ @Override
+ public void writeStartObject() throws IOException
+ {
+ if (_itemFilter == null) {
+ _filterContext = _filterContext.createChildObjectContext(_itemFilter, false);
+ return;
+ }
+ if (_itemFilter == TokenFilter.INCLUDE_ALL) {
+ _filterContext = _filterContext.createChildObjectContext(_itemFilter, true);
+ delegate.writeStartObject();
+ return;
+ }
+
+ TokenFilter f = _filterContext.checkValue(_itemFilter);
+ if (f == null) {
+ return;
+ }
+
+ if (f != TokenFilter.INCLUDE_ALL) {
+ f = f.filterStartObject();
+ }
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _checkParentPath();
+ _filterContext = _filterContext.createChildObjectContext(f, true);
+ delegate.writeStartObject();
+ } else { // filter out
+ _filterContext = _filterContext.createChildObjectContext(f, false);
+ }
+ }
+
+ @Override
+ public void writeEndObject() throws IOException
+ {
+ _filterContext = _filterContext.closeObject(delegate);
+ if (_filterContext != null) {
+ _itemFilter = _filterContext.getFilter();
+ }
+ }
+
+ @Override
+ public void writeFieldName(String name) throws IOException
+ {
+ TokenFilter state = _filterContext.setFieldName(name);
+ if (state == null) {
+ _itemFilter = null;
+ return;
+ }
+ if (state == TokenFilter.INCLUDE_ALL) {
+ _itemFilter = state;
+ delegate.writeFieldName(name);
+ return;
+ }
+ state = state.includeProperty(name);
+ _itemFilter = state;
+ if (state == TokenFilter.INCLUDE_ALL) {
+ _checkPropertyParentPath();
+ }
+ }
+
+ @Override
+ public void writeFieldName(SerializableString name) throws IOException
+ {
+ TokenFilter state = _filterContext.setFieldName(name.getValue());
+ if (state == null) {
+ _itemFilter = null;
+ return;
+ }
+ if (state == TokenFilter.INCLUDE_ALL) {
+ _itemFilter = state;
+ delegate.writeFieldName(name);
+ return;
+ }
+ state = state.includeProperty(name.getValue());
+ _itemFilter = state;
+ if (state == TokenFilter.INCLUDE_ALL) {
+ _checkPropertyParentPath();
+ }
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, write methods, text/String values
+ /**********************************************************
+ */
+
+ @Override
+ public void writeString(String value) throws IOException
+ {
+ if (_itemFilter == null) {
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ TokenFilter state = _filterContext.checkValue(_itemFilter);
+ if (state == null) {
+ return;
+ }
+ if (state != TokenFilter.INCLUDE_ALL) {
+ if (!state.includeString(value)) {
+ return;
+ }
+ }
+ _checkParentPath();
+ }
+ delegate.writeString(value);
+ }
+
+ @Override
+ public void writeString(char[] text, int offset, int len) throws IOException
+ {
+ if (_itemFilter == null) {
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ String value = new String(text, offset, len);
+ TokenFilter state = _filterContext.checkValue(_itemFilter);
+ if (state == null) {
+ return;
+ }
+ if (state != TokenFilter.INCLUDE_ALL) {
+ if (!state.includeString(value)) {
+ return;
+ }
+ }
+ _checkParentPath();
+ }
+ delegate.writeString(text, offset, len);
+ }
+
+ @Override
+ public void writeString(SerializableString value) throws IOException
+ {
+ if (_itemFilter == null) {
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ TokenFilter state = _filterContext.checkValue(_itemFilter);
+ if (state == null) {
+ return;
+ }
+ if (state != TokenFilter.INCLUDE_ALL) {
+ if (!state.includeString(value.getValue())) {
+ return;
+ }
+ }
+ _checkParentPath();
+ }
+ delegate.writeString(value);
+ }
+
+ @Override
+ public void writeRawUTF8String(byte[] text, int offset, int length) throws IOException
+ {
+ if (_checkRawValueWrite()) {
+ delegate.writeRawUTF8String(text, offset, length);
+ }
+ }
+
+ @Override
+ public void writeUTF8String(byte[] text, int offset, int length) throws IOException
+ {
+ // not exact match, but best we can do
+ if (_checkRawValueWrite()) {
+ delegate.writeRawUTF8String(text, offset, length);
+ }
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, write methods, binary/raw content
+ /**********************************************************
+ */
+
+ @Override
+ public void writeRaw(String text) throws IOException
+ {
+ if (_checkRawValueWrite()) {
+ delegate.writeRaw(text);
+ }
+ }
+
+ @Override
+ public void writeRaw(String text, int offset, int len) throws IOException
+ {
+ if (_checkRawValueWrite()) {
+ delegate.writeRaw(text);
+ }
+ }
+
+ @Override
+ public void writeRaw(SerializableString text) throws IOException
+ {
+ if (_checkRawValueWrite()) {
+ delegate.writeRaw(text);
+ }
+ }
+
+ @Override
+ public void writeRaw(char[] text, int offset, int len) throws IOException
+ {
+ if (_checkRawValueWrite()) {
+ delegate.writeRaw(text, offset, len);
+ }
+ }
+
+ @Override
+ public void writeRaw(char c) throws IOException
+ {
+ if (_checkRawValueWrite()) {
+ delegate.writeRaw(c);
+ }
+ }
+
+ @Override
+ public void writeRawValue(String text) throws IOException
+ {
+ if (_checkRawValueWrite()) {
+ delegate.writeRaw(text);
+ }
+ }
+
+ @Override
+ public void writeRawValue(String text, int offset, int len) throws IOException
+ {
+ if (_checkRawValueWrite()) {
+ delegate.writeRaw(text, offset, len);
+ }
+ }
+
+ @Override
+ public void writeRawValue(char[] text, int offset, int len) throws IOException
+ {
+ if (_checkRawValueWrite()) {
+ delegate.writeRaw(text, offset, len);
+ }
+ }
+
+ @Override
+ public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) throws IOException
+ {
+ if (_checkBinaryWrite()) {
+ delegate.writeBinary(b64variant, data, offset, len);
+ }
+ }
+
+ @Override
+ public int writeBinary(Base64Variant b64variant, InputStream data, int dataLength) throws IOException
+ {
+ if (_checkBinaryWrite()) {
+ return delegate.writeBinary(b64variant, data, dataLength);
+ }
+ return -1;
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, write methods, other value types
+ /**********************************************************
+ */
+
+ @Override
+ public void writeNumber(short v) throws IOException
+ {
+ if (_itemFilter == null) {
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ TokenFilter state = _filterContext.checkValue(_itemFilter);
+ if (state == null) {
+ return;
+ }
+ if (state != TokenFilter.INCLUDE_ALL) {
+ if (!state.includeNumber(v)) {
+ return;
+ }
+ }
+ _checkParentPath();
+ }
+ delegate.writeNumber(v);
+ }
+
+ @Override
+ public void writeNumber(int v) throws IOException
+ {
+ if (_itemFilter == null) {
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ TokenFilter state = _filterContext.checkValue(_itemFilter);
+ if (state == null) {
+ return;
+ }
+ if (state != TokenFilter.INCLUDE_ALL) {
+ if (!state.includeNumber(v)) {
+ return;
+ }
+ }
+ _checkParentPath();
+ }
+ delegate.writeNumber(v);
+ }
+
+ @Override
+ public void writeNumber(long v) throws IOException
+ {
+ if (_itemFilter == null) {
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ TokenFilter state = _filterContext.checkValue(_itemFilter);
+ if (state == null) {
+ return;
+ }
+ if (state != TokenFilter.INCLUDE_ALL) {
+ if (!state.includeNumber(v)) {
+ return;
+ }
+ }
+ _checkParentPath();
+ }
+ delegate.writeNumber(v);
+ }
+
+ @Override
+ public void writeNumber(BigInteger v) throws IOException
+ {
+ if (_itemFilter == null) {
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ TokenFilter state = _filterContext.checkValue(_itemFilter);
+ if (state == null) {
+ return;
+ }
+ if (state != TokenFilter.INCLUDE_ALL) {
+ if (!state.includeNumber(v)) {
+ return;
+ }
+ }
+ _checkParentPath();
+ }
+ delegate.writeNumber(v);
+ }
+
+ @Override
+ public void writeNumber(double v) throws IOException
+ {
+ if (_itemFilter == null) {
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ TokenFilter state = _filterContext.checkValue(_itemFilter);
+ if (state == null) {
+ return;
+ }
+ if (state != TokenFilter.INCLUDE_ALL) {
+ if (!state.includeNumber(v)) {
+ return;
+ }
+ }
+ _checkParentPath();
+ }
+ delegate.writeNumber(v);
+ }
+
+ @Override
+ public void writeNumber(float v) throws IOException
+ {
+ if (_itemFilter == null) {
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ TokenFilter state = _filterContext.checkValue(_itemFilter);
+ if (state == null) {
+ return;
+ }
+ if (state != TokenFilter.INCLUDE_ALL) {
+ if (!state.includeNumber(v)) {
+ return;
+ }
+ }
+ _checkParentPath();
+ }
+ delegate.writeNumber(v);
+ }
+
+ @Override
+ public void writeNumber(BigDecimal v) throws IOException
+ {
+ if (_itemFilter == null) {
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ TokenFilter state = _filterContext.checkValue(_itemFilter);
+ if (state == null) {
+ return;
+ }
+ if (state != TokenFilter.INCLUDE_ALL) {
+ if (!state.includeNumber(v)) {
+ return;
+ }
+ }
+ _checkParentPath();
+ }
+ delegate.writeNumber(v);
+ }
+
+ @Override
+ public void writeNumber(String encodedValue) throws IOException, UnsupportedOperationException
+ {
+ if (_itemFilter == null) {
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ TokenFilter state = _filterContext.checkValue(_itemFilter);
+ if (state == null) {
+ return;
+ }
+ if (state != TokenFilter.INCLUDE_ALL) {
+ if (!state.includeRawValue()) { // close enough?
+ return;
+ }
+ }
+ _checkParentPath();
+ }
+ delegate.writeNumber(encodedValue);
+ }
+
+ @Override
+ public void writeBoolean(boolean v) throws IOException
+ {
+ if (_itemFilter == null) {
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ TokenFilter state = _filterContext.checkValue(_itemFilter);
+ if (state == null) {
+ return;
+ }
+ if (state != TokenFilter.INCLUDE_ALL) {
+ if (!state.includeBoolean(v)) {
+ return;
+ }
+ }
+ _checkParentPath();
+ }
+ delegate.writeBoolean(v);
+ }
+
+ @Override
+ public void writeNull() throws IOException
+ {
+ if (_itemFilter == null) {
+ return;
+ }
+ if (_itemFilter != TokenFilter.INCLUDE_ALL) {
+ TokenFilter state = _filterContext.checkValue(_itemFilter);
+ if (state == null) {
+ return;
+ }
+ if (state != TokenFilter.INCLUDE_ALL) {
+ if (!state.includeNull()) {
+ return;
+ }
+ }
+ _checkParentPath();
+ }
+ delegate.writeNull();
+ }
+
+ /*
+ /**********************************************************
+ /* Overridden field methods
+ /**********************************************************
+ */
+
+ @Override
+ public void writeOmittedField(String fieldName) throws IOException {
+ // Hmmh. Not sure how this would work but...
+ if (_itemFilter != null) {
+ delegate.writeOmittedField(fieldName);
+ }
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, write methods, Native Ids
+ /**********************************************************
+ */
+
+ // 25-Mar-2015, tatu: These are tricky as they sort of predate actual filtering calls.
+ // Let's try to use current state as a clue at least...
+
+ @Override
+ public void writeObjectId(Object id) throws IOException {
+ if (_itemFilter != null) {
+ delegate.writeObjectId(id);
+ }
+ }
+
+ @Override
+ public void writeObjectRef(Object id) throws IOException {
+ if (_itemFilter != null) {
+ delegate.writeObjectRef(id);
+ }
+ }
+
+ @Override
+ public void writeTypeId(Object id) throws IOException {
+ if (_itemFilter != null) {
+ delegate.writeTypeId(id);
+ }
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, write methods, serializing Java objects
+ /**********************************************************
+ */
+
+ // Base class definitions for these seems correct to me, iff not directly delegating:
+
+ /*
+ @Override
+ public void writeObject(Object pojo) throws IOException,JsonProcessingException {
+ if (delegateCopyMethods) {
+ delegate.writeObject(pojo);
+ return;
+ }
+ // NOTE: copied from
+ if (pojo == null) {
+ writeNull();
+ } else {
+ if (getCodec() != null) {
+ getCodec().writeValue(this, pojo);
+ return;
+ }
+ _writeSimpleObject(pojo);
+ }
+ }
+
+ @Override
+ public void writeTree(TreeNode rootNode) throws IOException {
+ if (delegateCopyMethods) {
+ delegate.writeTree(rootNode);
+ return;
+ }
+ // As with 'writeObject()', we are not check if write would work
+ if (rootNode == null) {
+ writeNull();
+ } else {
+ if (getCodec() == null) {
+ throw new IllegalStateException("No ObjectCodec defined");
+ }
+ getCodec().writeValue(this, rootNode);
+ }
+ }
+ */
+
+ /*
+ /**********************************************************
+ /* Public API, copy-through methods
+ /**********************************************************
+ */
+
+ // Base class definitions for these seems correct to me, iff not directly delegating:
+
+ /*
+ @Override
+ public void copyCurrentEvent(JsonParser jp) throws IOException {
+ if (delegateCopyMethods) delegate.copyCurrentEvent(jp);
+ else super.copyCurrentEvent(jp);
+ }
+
+ @Override
+ public void copyCurrentStructure(JsonParser jp) throws IOException {
+ if (delegateCopyMethods) delegate.copyCurrentStructure(jp);
+ else super.copyCurrentStructure(jp);
+ }
+ */
+
+ /*
+ /**********************************************************
+ /* Helper methods
+ /**********************************************************
+ */
+
+ protected void _checkParentPath() throws IOException
+ {
+ ++_matchCount;
+ // only need to construct path if parent wasn't written
+ if (_includePath) {
+ _filterContext.writePath(delegate);
+ }
+ // also: if no multiple matches desired, short-cut checks
+ if (!_allowMultipleMatches) {
+ // Mark parents as "skip" so that further check calls are not made
+ _filterContext.skipParentChecks();
+ }
+ }
+
+ /**
+ * Specialized variant of {@link #_checkParentPath} used when checking
+ * parent for a property name to be included with value: rules are slightly
+ * different.
+ */
+ protected void _checkPropertyParentPath() throws IOException
+ {
+ ++_matchCount;
+ if (_includePath) {
+ _filterContext.writePath(delegate);
+ } else if (_includeImmediateParent) {
+ // 21-Apr-2015, tatu: Note that there is no API to enable this currently...
+ // retained for speculative future use
+ _filterContext.writeImmediatePath(delegate);
+ }
+
+ // also: if no multiple matches desired, short-cut checks
+ if (!_allowMultipleMatches) {
+ // Mark parents as "skip" so that further check calls are not made
+ _filterContext.skipParentChecks();
+ }
+ }
+
+ protected boolean _checkBinaryWrite() throws IOException
+ {
+ if (_itemFilter == null) {
+ return false;
+ }
+ if (_itemFilter == TokenFilter.INCLUDE_ALL) {
+ return true;
+ }
+ if (_itemFilter.includeBinary()) { // close enough?
+ _checkParentPath();
+ return true;
+ }
+ return false;
+ }
+
+ protected boolean _checkRawValueWrite() throws IOException
+ {
+ if (_itemFilter == null) {
+ return false;
+ }
+ if (_itemFilter == TokenFilter.INCLUDE_ALL) {
+ return true;
+ }
+ if (_itemFilter.includeRawValue()) { // close enough?
+ _checkParentPath();
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/fasterxml/jackson/core/filter/FilteringParserDelegate.java b/src/main/java/com/fasterxml/jackson/core/filter/FilteringParserDelegate.java
new file mode 100644
index 0000000..0d65bff
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/core/filter/FilteringParserDelegate.java
@@ -0,0 +1,880 @@
+package com.fasterxml.jackson.core.filter;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import com.fasterxml.jackson.core.*;
+import com.fasterxml.jackson.core.util.JsonParserDelegate;
+
+import static com.fasterxml.jackson.core.JsonTokenId.*;
+
+/**
+ * Specialized {@link JsonParserDelegate} that allows use of
+ * {@link TokenFilter} for outputting a subset of content that
+ * is visible to caller
+ *
+ * @since 2.6
+ */
+public class FilteringParserDelegate extends JsonParserDelegate
+{
+ /*
+ /**********************************************************
+ /* Configuration
+ /**********************************************************
+ */
+
+ /**
+ * Object consulted to determine whether to write parts of content generator
+ * is asked to write or not.
+ */
+ protected TokenFilter rootFilter;
+
+ /**
+ * Flag that determines whether filtering will continue after the first
+ * match is indicated or not: if `false`, output is based on just the first
+ * full match (returning {@link TokenFilter#INCLUDE_ALL}) and no more
+ * checks are made; if `true` then filtering will be applied as necessary
+ * until end of content.
+ */
+ protected boolean _allowMultipleMatches;
+
+ /**
+ * Flag that determines whether path leading up to included content should
+ * also be automatically included or not. If `false`, no path inclusion is
+ * done and only explicitly included entries are output; if `true` then
+ * path from main level down to match is also included as necessary.
+ */
+ protected boolean _includePath;
+
+ /* NOTE: this feature is included in the first version (2.6), but
+ * there is no public API to enable it, yet, since there isn't an
+ * actual use case. But it seemed possible need could arise, which
+ * is feature has not yet been removed. If no use is found within
+ * first version or two, just remove.
+ *
+ * Marked as deprecated since its status is uncertain.
+ */
+ @Deprecated
+ protected boolean _includeImmediateParent = false;
+
+ /*
+ /**********************************************************
+ /* State
+ /**********************************************************
+ */
+
+ /**
+ * Last token retrieved via {@link #nextToken}, if any.
+ * Null before the first call to <code>nextToken()</code>,
+ * as well as if token has been explicitly cleared
+ */
+ protected JsonToken _currToken;
+
+ /**
+ * Last cleared token, if any: that is, value that was in
+ * effect when {@link #clearCurrentToken} was called.
+ */
+ protected JsonToken _lastClearedToken;
+
+ /**
+ * During traversal this is the actual "open" parse tree, which sometimes
+ * is the same as {@link #_exposedContext}, and at other times is ahead
+ * of it. Note that this context is never null.
+ */
+ protected TokenFilterContext _headContext;
+
+ /**
+ * In cases where {@link #_headContext} is "ahead" of context exposed to
+ * caller, this context points to what is currently exposed to caller.
+ * When the two are in sync, this context reference will be <code>null</code>.
+ */
+ protected TokenFilterContext _exposedContext;
+
+ /**
+ * State that applies to the item within container, used where applicable.
+ * Specifically used to pass inclusion state between property name and
+ * property, and also used for array elements.
+ */
+ protected TokenFilter _itemFilter;
+
+ /**
+ * Number of tokens for which {@link TokenFilter#INCLUDE_ALL}
+ * has been returned.
+ */
+ protected int _matchCount;
+
+ /*
+ /**********************************************************
+ /* Construction, initialization
+ /**********************************************************
+ */
+
+ public FilteringParserDelegate(JsonParser p, TokenFilter f,
+ boolean includePath, boolean allowMultipleMatches)
+ {
+ super(p);
+ rootFilter = f;
+ // and this is the currently active filter for root values
+ _itemFilter = f;
+ _headContext = TokenFilterContext.createRootContext(f);
+ _includePath = includePath;
+ _allowMultipleMatches = allowMultipleMatches;
+ }
+
+ /*
+ /**********************************************************
+ /* Extended API
+ /**********************************************************
+ */
+
+ public TokenFilter getFilter() { return rootFilter; }
+
+ /**
+ * Accessor for finding number of matches, where specific token and sub-tree
+ * starting (if structured type) are passed.
+ */
+ public int getMatchCount() {
+ return _matchCount;
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, token accessors
+ /**********************************************************
+ */
+
+ @Override public JsonToken getCurrentToken() { return _currToken; }
+
+ @Override public final int getCurrentTokenId() {
+ final JsonToken t = _currToken;
+ return (t == null) ? JsonTokenId.ID_NO_TOKEN : t.id();
+ }
+
+ @Override public boolean hasCurrentToken() { return _currToken != null; }
+ @Override public boolean hasTokenId(int id) {
+ final JsonToken t = _currToken;
+ if (t == null) {
+ return (JsonTokenId.ID_NO_TOKEN == id);
+ }
+ return t.id() == id;
+ }
+
+ @Override public final boolean hasToken(JsonToken t) {
+ return (_currToken == t);
+ }
+
+ @Override public boolean isExpectedStartArrayToken() { return _currToken == JsonToken.START_ARRAY; }
+ @Override public boolean isExpectedStartObjectToken() { return _currToken == JsonToken.START_OBJECT; }
+
+ @Override public JsonLocation getCurrentLocation() { return delegate.getCurrentLocation(); }
+
+ @Override
+ public JsonStreamContext getParsingContext() {
+ return _filterContext();
+ }
+
+ // !!! TODO: Verify it works as expected: copied from standard JSON parser impl
+ @Override
+ public String getCurrentName() throws IOException {
+ JsonStreamContext ctxt = _filterContext();
+ if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) {
+ JsonStreamContext parent = ctxt.getParent();
+ return (parent == null) ? null : parent.getCurrentName();
+ }
+ return ctxt.getCurrentName();
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, token state overrides
+ /**********************************************************
+ */
+
+ @Override
+ public void clearCurrentToken() {
+ if (_currToken != null) {
+ _lastClearedToken = _currToken;
+ _currToken = null;
+ }
+ }
+
+ @Override
+ public JsonToken getLastClearedToken() { return _lastClearedToken; }
+
+ @Override
+ public void overrideCurrentName(String name) {
+ /* 14-Apr-2015, tatu: Not sure whether this can be supported, and if so,
+ * what to do with it... Delegation won't work for sure, so let's for
+ * now throw an exception
+ */
+ throw new UnsupportedOperationException("Can not currently override name during filtering read");
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, traversal
+ /**********************************************************
+ */
+
+ @Override
+ public JsonToken nextToken() throws IOException
+ {
+ // Anything buffered?
+ TokenFilterContext ctxt = _exposedContext;
+
+ if (ctxt != null) {
+ while (true) {
+ JsonToken t = ctxt.nextTokenToRead();
+ if (t != null) {
+ _currToken = t;
+ return t;
+ }
+ // all done with buffered stuff?
+ if (ctxt == _headContext) {
+ _exposedContext = null;
+ if (ctxt.inArray()) {
+ t = delegate.getCurrentToken();
+// Is this guaranteed to work without further checks?
+// if (t != JsonToken.START_ARRAY) {
+ _currToken = t;
+ return t;
+ }
+
+ // Almost! Most likely still have the current token;
+ // with the sole exception of
+ /*
+ t = delegate.getCurrentToken();
+ if (t != JsonToken.FIELD_NAME) {
+ _currToken = t;
+ return t;
+ }
+ */
+ break;
+ }
+ // If not, traverse down the context chain
+ ctxt = _headContext.findChildOf(ctxt);
+ _exposedContext = ctxt;
+ if (ctxt == null) { // should never occur
+ throw _constructError("Unexpected problem: chain of filtered context broken");
+ }
+ }
+ }
+
+ // If not, need to read more. If we got any:
+ JsonToken t = delegate.nextToken();
+ if (t == null) {
+ // no strict need to close, since we have no state here
+ return (_currToken = t);
+ }
+
+ // otherwise... to include or not?
+ TokenFilter f;
+
+ switch (t.id()) {
+ case ID_START_ARRAY:
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildArrayContext(f, true);
+ return (_currToken = t);
+ }
+ if (f == null) { // does this occur?
+ delegate.skipChildren();
+ break;
+ }
+ // Otherwise still iffy, need to check
+ f = _headContext.checkValue(f);
+ if (f == null) {
+ delegate.skipChildren();
+ break;
+ }
+ if (f != TokenFilter.INCLUDE_ALL) {
+ f = f.filterStartArray();
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildArrayContext(f, true);
+ return (_currToken = t);
+ }
+ _headContext = _headContext.createChildArrayContext(f, false);
+
+ // Also: only need buffering if parent path to be included
+ if (_includePath) {
+ t = _nextTokenWithBuffering(_headContext);
+ if (t != null) {
+ _currToken = t;
+ return t;
+ }
+ }
+ break;
+
+ case ID_START_OBJECT:
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildObjectContext(f, true);
+ return (_currToken = t);
+ }
+ if (f == null) { // does this occur?
+ delegate.skipChildren();
+ break;
+ }
+ // Otherwise still iffy, need to check
+ f = _headContext.checkValue(f);
+ if (f == null) {
+ delegate.skipChildren();
+ break;
+ }
+ if (f != TokenFilter.INCLUDE_ALL) {
+ f = f.filterStartObject();
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildObjectContext(f, true);
+ return (_currToken = t);
+ }
+ _headContext = _headContext.createChildObjectContext(f, false);
+ // Also: only need buffering if parent path to be included
+ if (_includePath) {
+ t = _nextTokenWithBuffering(_headContext);
+ if (t != null) {
+ _currToken = t;
+ return t;
+ }
+ }
+ // note: inclusion of surrounding Object handled separately via
+ // FIELD_NAME
+ break;
+
+ case ID_END_ARRAY:
+ case ID_END_OBJECT:
+ {
+ boolean returnEnd = _headContext.isStartHandled();
+ f = _headContext.getFilter();
+ if ((f != null) && (f != TokenFilter.INCLUDE_ALL)) {
+ f.filterFinishArray();
+ }
+ _headContext = _headContext.getParent();
+ _itemFilter = _headContext.getFilter();
+ if (returnEnd) {
+ return (_currToken = t);
+ }
+ }
+ break;
+
+ case ID_FIELD_NAME:
+ {
+ final String name = delegate.getCurrentName();
+ // note: this will also set 'needToHandleName'
+ f = _headContext.setFieldName(name);
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _itemFilter = f;
+ if (!_includePath) {
+ // Minor twist here: if parent NOT included, may need to induce output of
+ // surrounding START_OBJECT/END_OBJECT
+ if (_includeImmediateParent && !_headContext.isStartHandled()) {
+ t = _headContext.nextTokenToRead(); // returns START_OBJECT but also marks it handled
+ _exposedContext = _headContext;
+ }
+ }
+ return (_currToken = t);
+ }
+ if (f == null) {
+ delegate.nextToken();
+ delegate.skipChildren();
+ break;
+ }
+ f = f.includeProperty(name);
+ if (f == null) {
+ delegate.nextToken();
+ delegate.skipChildren();
+ break;
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ if (_includePath) {
+ return (_currToken = t);
+ }
+ }
+ if (_includePath) {
+ t = _nextTokenWithBuffering(_headContext);
+ if (t != null) {
+ _currToken = t;
+ return t;
+ }
+ }
+ break;
+ }
+
+ default: // scalar value
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ return (_currToken = t);
+ }
+ if (f != null) {
+ f = _headContext.checkValue(f);
+ if ((f == TokenFilter.INCLUDE_ALL)
+ || ((f != null) && f.includeValue(delegate))) {
+ return (_currToken = t);
+ }
+ }
+ // Otherwise not included (leaves must be explicitly included)
+ break;
+ }
+
+ // We get here if token was not yet found; offlined handling
+ return _nextToken2();
+ }
+
+ /**
+ * Offlined handling for cases where there was no buffered token to
+ * return, and the token read next could not be returned as-is,
+ * at least not yet, but where we have not yet established that
+ * buffering is needed.
+ */
+ protected final JsonToken _nextToken2() throws IOException
+ {
+ main_loop:
+ while (true) {
+ JsonToken t = delegate.nextToken();
+ if (t == null) { // is this even legal?
+ return (_currToken = t);
+ }
+ TokenFilter f;
+
+ switch (t.id()) {
+ case ID_START_ARRAY:
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildArrayContext(f, true);
+ return (_currToken = t);
+ }
+ if (f == null) { // does this occur?
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ // Otherwise still iffy, need to check
+ f = _headContext.checkValue(f);
+ if (f == null) {
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ if (f != TokenFilter.INCLUDE_ALL) {
+ f = f.filterStartArray();
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildArrayContext(f, true);
+ return (_currToken = t);
+ }
+ _headContext = _headContext.createChildArrayContext(f, false);
+ // but if we didn't figure it out yet, need to buffer possible events
+ if (_includePath) {
+ t = _nextTokenWithBuffering(_headContext);
+ if (t != null) {
+ _currToken = t;
+ return t;
+ }
+ }
+ continue main_loop;
+
+ case ID_START_OBJECT:
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildObjectContext(f, true);
+ return (_currToken = t);
+ }
+ if (f == null) { // does this occur?
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ // Otherwise still iffy, need to check
+ f = _headContext.checkValue(f);
+ if (f == null) {
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ if (f != TokenFilter.INCLUDE_ALL) {
+ f = f.filterStartObject();
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildObjectContext(f, true);
+ return (_currToken = t);
+ }
+ _headContext = _headContext.createChildObjectContext(f, false);
+ if (_includePath) {
+ t = _nextTokenWithBuffering(_headContext);
+ if (t != null) {
+ _currToken = t;
+ return t;
+ }
+ }
+ continue main_loop;
+
+ case ID_END_ARRAY:
+ case ID_END_OBJECT:
+ {
+ boolean returnEnd = _headContext.isStartHandled();
+ f = _headContext.getFilter();
+ if ((f != null) && (f != TokenFilter.INCLUDE_ALL)) {
+ f.filterFinishArray();
+ }
+ _headContext = _headContext.getParent();
+ _itemFilter = _headContext.getFilter();
+ if (returnEnd) {
+ return (_currToken = t);
+ }
+ }
+ continue main_loop;
+
+ case ID_FIELD_NAME:
+ {
+ final String name = delegate.getCurrentName();
+ f = _headContext.setFieldName(name);
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _itemFilter = f;
+ return (_currToken = t);
+ }
+ if (f == null) { // filter out the value
+ delegate.nextToken();
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ f = f.includeProperty(name);
+ if (f == null) { // filter out the value
+ delegate.nextToken();
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ if (_includePath) {
+ return (_currToken = t);
+ }
+// if (_includeImmediateParent) { ...
+ continue main_loop;
+ }
+ if (_includePath) {
+ t = _nextTokenWithBuffering(_headContext);
+ if (t != null) {
+ _currToken = t;
+ return t;
+ }
+ }
+ }
+ continue main_loop;
+
+ default: // scalar value
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ return (_currToken = t);
+ }
+ if (f != null) {
+ f = _headContext.checkValue(f);
+ if ((f == TokenFilter.INCLUDE_ALL)
+ || ((f != null) && f.includeValue(delegate))) {
+ return (_currToken = t);
+ }
+ }
+ // Otherwise not included (leaves must be explicitly included)
+ break;
+ }
+ }
+ }
+
+ /**
+ * Method called when a new potentially included context is found.
+ */
+ protected final JsonToken _nextTokenWithBuffering(final TokenFilterContext buffRoot)
+ throws IOException
+ {
+ main_loop:
+ while (true) {
+ JsonToken t = delegate.nextToken();
+ if (t == null) { // is this even legal?
+ return t;
+ }
+ TokenFilter f;
+
+ // One simplification here: we know for a fact that the item filter is
+ // neither null nor 'include all', for most cases; the only exception
+ // being FIELD_NAME handling
+
+ switch (t.id()) {
+ case ID_START_ARRAY:
+ f = _headContext.checkValue(_itemFilter);
+ if (f == null) {
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ if (f != TokenFilter.INCLUDE_ALL) {
+ f = f.filterStartArray();
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildArrayContext(f, true);
+ return _nextBuffered(buffRoot);
+ }
+ _headContext = _headContext.createChildArrayContext(f, false);
+ continue main_loop;
+
+ case ID_START_OBJECT:
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildObjectContext(f, true);
+ return t;
+ }
+ if (f == null) { // does this occur?
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ // Otherwise still iffy, need to check
+ f = _headContext.checkValue(f);
+ if (f == null) {
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ if (f != TokenFilter.INCLUDE_ALL) {
+ f = f.filterStartObject();
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildObjectContext(f, true);
+ return _nextBuffered(buffRoot);
+ }
+ _headContext = _headContext.createChildObjectContext(f, false);
+ continue main_loop;
+
+ case ID_END_ARRAY:
+ case ID_END_OBJECT:
+ {
+ // Unlike with other loops, here we know that content was NOT
+ // included (won't get this far otherwise)
+ f = _headContext.getFilter();
+ if ((f != null) && (f != TokenFilter.INCLUDE_ALL)) {
+ f.filterFinishArray();
+ }
+ boolean gotEnd = (_headContext == buffRoot);
+ boolean returnEnd = gotEnd && _headContext.isStartHandled();
+
+ _headContext = _headContext.getParent();
+ _itemFilter = _headContext.getFilter();
+
+ if (returnEnd) {
+ return t;
+ }
+ // Hmmh. Do we need both checks, or should above suffice?
+ if (gotEnd || (_headContext == buffRoot)) {
+ return null;
+ }
+ }
+ continue main_loop;
+
+ case ID_FIELD_NAME:
+ {
+ final String name = delegate.getCurrentName();
+ f = _headContext.setFieldName(name);
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _itemFilter = f;
+ return _nextBuffered(buffRoot);
+ }
+ if (f == null) { // filter out the value
+ delegate.nextToken();
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ f = f.includeProperty(name);
+ if (f == null) { // filter out the value
+ delegate.nextToken();
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ return _nextBuffered(buffRoot);
+ }
+ }
+ continue main_loop;
+
+ default: // scalar value
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ return _nextBuffered(buffRoot);
+ }
+ if (f != null) {
+ f = _headContext.checkValue(f);
+ if ((f == TokenFilter.INCLUDE_ALL)
+ || ((f != null) && f.includeValue(delegate))) {
+ return _nextBuffered(buffRoot);
+ }
+ }
+ // Otherwise not included (leaves must be explicitly included)
+ continue main_loop;
+ }
+ }
+ }
+
+ private JsonToken _nextBuffered(TokenFilterContext buffRoot) throws IOException
+ {
+ _exposedContext = buffRoot;
+ TokenFilterContext ctxt = buffRoot;
+ JsonToken t = ctxt.nextTokenToRead();
+ if (t != null) {
+ return t;
+ }
+ while (true) {
+ // all done with buffered stuff?
+ if (ctxt == _headContext) {
+ throw _constructError("Internal error: failed to locate expected buffered tokens");
+ /*
+ _exposedContext = null;
+ break;
+ */
+ }
+ // If not, traverse down the context chain
+ ctxt = _exposedContext.findChildOf(ctxt);
+ _exposedContext = ctxt;
+ if (ctxt == null) { // should never occur
+ throw _constructError("Unexpected problem: chain of filtered context broken");
+ }
+ t = _exposedContext.nextTokenToRead();
+ if (t != null) {
+ return t;
+ }
+ }
+ }
+
+ @Override
+ public JsonToken nextValue() throws IOException {
+ // Re-implemented same as ParserMinimalBase:
+ JsonToken t = nextToken();
+ if (t == JsonToken.FIELD_NAME) {
+ t = nextToken();
+ }
+ return t;
+ }
+
+ /**
+ * Need to override, re-implement similar to how method defined in
+ * {@link com.fasterxml.jackson.core.base.ParserMinimalBase}, to keep
+ * state correct here.
+ */
+ @Override
+ public JsonParser skipChildren() throws IOException
+ {
+ if ((_currToken != JsonToken.START_OBJECT)
+ && (_currToken != JsonToken.START_ARRAY)) {
+ return this;
+ }
+ int open = 1;
+
+ // Since proper matching of start/end markers is handled
+ // by nextToken(), we'll just count nesting levels here
+ while (true) {
+ JsonToken t = nextToken();
+ if (t == null) { // not ideal but for now, just return
+ return this;
+ }
+ if (t.isStructStart()) {
+ ++open;
+ } else if (t.isStructEnd()) {
+ if (--open == 0) {
+ return this;
+ }
+ }
+ }
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, access to token information, text
+ /**********************************************************
+ */
+
+ @Override public String getText() throws IOException { return delegate.getText(); }
+ @Override public boolean hasTextCharacters() { return delegate.hasTextCharacters(); }
+ @Override public char[] getTextCharacters() throws IOException { return delegate.getTextCharacters(); }
+ @Override public int getTextLength() throws IOException { return delegate.getTextLength(); }
+ @Override public int getTextOffset() throws IOException { return delegate.getTextOffset(); }
+
+ /*
+ /**********************************************************
+ /* Public API, access to token information, numeric
+ /**********************************************************
+ */
+
+ @Override
+ public BigInteger getBigIntegerValue() throws IOException { return delegate.getBigIntegerValue(); }
+
+ @Override
+ public boolean getBooleanValue() throws IOException { return delegate.getBooleanValue(); }
+
+ @Override
+ public byte getByteValue() throws IOException { return delegate.getByteValue(); }
+
+ @Override
+ public short getShortValue() throws IOException { return delegate.getShortValue(); }
+
+ @Override
+ public BigDecimal getDecimalValue() throws IOException { return delegate.getDecimalValue(); }
+
+ @Override
+ public double getDoubleValue() throws IOException { return delegate.getDoubleValue(); }
+
+ @Override
+ public float getFloatValue() throws IOException { return delegate.getFloatValue(); }
+
+ @Override
+ public int getIntValue() throws IOException { return delegate.getIntValue(); }
+
+ @Override
+ public long getLongValue() throws IOException { return delegate.getLongValue(); }
+
+ @Override
+ public NumberType getNumberType() throws IOException { return delegate.getNumberType(); }
+
+ @Override
+ public Number getNumberValue() throws IOException { return delegate.getNumberValue(); }
+
+ /*
+ /**********************************************************
+ /* Public API, access to token information, coercion/conversion
+ /**********************************************************
+ */
+
+ @Override public int getValueAsInt() throws IOException { return delegate.getValueAsInt(); }
+ @Override public int getValueAsInt(int defaultValue) throws IOException { return delegate.getValueAsInt(defaultValue); }
+ @Override public long getValueAsLong() throws IOException { return delegate.getValueAsLong(); }
+ @Override public long getValueAsLong(long defaultValue) throws IOException { return delegate.getValueAsLong(defaultValue); }
+ @Override public double getValueAsDouble() throws IOException { return delegate.getValueAsDouble(); }
+ @Override public double getValueAsDouble(double defaultValue) throws IOException { return delegate.getValueAsDouble(defaultValue); }
+ @Override public boolean getValueAsBoolean() throws IOException { return delegate.getValueAsBoolean(); }
+ @Override public boolean getValueAsBoolean(boolean defaultValue) throws IOException { return delegate.getValueAsBoolean(defaultValue); }
+ @Override public String getValueAsString() throws IOException { return delegate.getValueAsString(); }
+ @Override public String getValueAsString(String defaultValue) throws IOException { return delegate.getValueAsString(defaultValue); }
+
+ /*
+ /**********************************************************
+ /* Public API, access to token values, other
+ /**********************************************************
+ */
+
+ @Override public Object getEmbeddedObject() throws IOException { return delegate.getEmbeddedObject(); }
+ @Override public byte[] getBinaryValue(Base64Variant b64variant) throws IOException { return delegate.getBinaryValue(b64variant); }
+ @Override public int readBinaryValue(Base64Variant b64variant, OutputStream out) throws IOException { return delegate.readBinaryValue(b64variant, out); }
+ @Override public JsonLocation getTokenLocation() { return delegate.getTokenLocation(); }
+
+ /*
+ /**********************************************************
+ /* Internal helper methods
+ /**********************************************************
+ */
+
+ protected JsonStreamContext _filterContext() {
+ if (_exposedContext != null) {
+ return _exposedContext;
+ }
+ return _headContext;
+ }
+}
diff --git a/src/main/java/com/fasterxml/jackson/core/filter/JsonPointerBasedFilter.java b/src/main/java/com/fasterxml/jackson/core/filter/JsonPointerBasedFilter.java
new file mode 100644
index 0000000..d3dedee
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/core/filter/JsonPointerBasedFilter.java
@@ -0,0 +1,69 @@
+package com.fasterxml.jackson.core.filter;
+
+import com.fasterxml.jackson.core.JsonPointer;
+
+/**
+ * Simple {@link TokenFilter} implementation that takes a single
+ * {@link JsonPointer} and matches a single value accordingly.
+ * Instances are immutable and fully thread-safe, shareable,
+ * and efficient to use.
+ *
+ * @since 2.6
+ */
+public class JsonPointerBasedFilter extends TokenFilter
+{
+ protected final JsonPointer _pathToMatch;
+
+ public JsonPointerBasedFilter(String ptrExpr) {
+ this(JsonPointer.compile(ptrExpr));
+ }
+
+ public JsonPointerBasedFilter(JsonPointer match) {
+ _pathToMatch = match;
+ }
+
+ @Override
+ public TokenFilter includeElement(int index) {
+ JsonPointer next = _pathToMatch.matchElement(index);
+ if (next == null) {
+ return null;
+ }
+ if (next.matches()) {
+ return TokenFilter.INCLUDE_ALL;
+ }
+ return new JsonPointerBasedFilter(next);
+ }
+
+ @Override
+ public TokenFilter includeProperty(String name) {
+ JsonPointer next = _pathToMatch.matchProperty(name);
+ if (next == null) {
+ return null;
+ }
+ if (next.matches()) {
+ return TokenFilter.INCLUDE_ALL;
+ }
+ return new JsonPointerBasedFilter(next);
+ }
+
+ @Override
+ public TokenFilter filterStartArray() {
+ return this;
+ }
+
+ @Override
+ public TokenFilter filterStartObject() {
+ return this;
+ }
+
+ @Override
+ protected boolean _includeScalar() {
+ // should only occur for root-level scalars, path "/"
+ return _pathToMatch.matches();
+ }
+
+ @Override
+ public String toString() {
+ return "[JsonPointerFilter at: "+_pathToMatch+"]";
+ }
+}
diff --git a/src/main/java/com/fasterxml/jackson/core/filter/TokenFilter.java b/src/main/java/com/fasterxml/jackson/core/filter/TokenFilter.java
new file mode 100644
index 0000000..4546fbf
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/core/filter/TokenFilter.java
@@ -0,0 +1,363 @@
+package com.fasterxml.jackson.core.filter;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+
+/**
+ * Strategy class that can be implemented to specify actual inclusion/exclusion
+ * criteria for filtering, used by {@link FilteringGeneratorDelegate}.
+ *
+ * @since 2.6
+ */
+public class TokenFilter
+{
+
+ // // Marker values
+
+ /**
+ * Marker value that should be used to indicate inclusion of a structured
+ * value (sub-tree representing Object or Array), or value of a named
+ * property (regardless of type).
+ * Note that if this instance is returned, it will used as a marker, and
+ * no actual callbacks need to be made. For this reason, it is more efficient
+ * to return this instance if the whole sub-tree is to be included, instead
+ * of implementing similar filter functionality explicitly.
+ */
+ public final static TokenFilter INCLUDE_ALL = new TokenFilter();
+
+ // Life-cycle
+
+ protected TokenFilter() { }
+
+ /*
+ /**********************************************************
+ /* API, structured values
+ /**********************************************************
+ */
+
+ /**
+ * Method called to check whether Object value at current output
+ * location should be included in output.
+ * Three kinds of return values may be used as follows:
+ *<ul>
+ * <li><code>null</code> to indicate that the Object should be skipped
+ * </li>
+ * <li>{@link #INCLUDE_ALL} to indicate that the Object should be included
+ * completely in output
+ * </li>
+ * <li>Any other {@link TokenFilter} implementation (possibly this one) to mean
+ * that further inclusion calls on return filter object need to be made
+ * on contained properties, as necessary. {@link #filterFinishObject()} will
+ * also be called on returned filter object
+ * </li>
+ * </ul>
+ *<p>
+ * Default implementation returns <code>this</code>, which means that checks
+ * are made recursively for properties of the Object to determine possible inclusion.
+ *
+ * @return TokenFilter to use for further calls within Array, unless return value
+ * is <code>null</code> or {@link #INCLUDE_ALL} (which have simpler semantics)
+ */
+ public TokenFilter filterStartObject() {
+ return this;
+ }
+
+ /**
+ * Method called to check whether Array value at current output
+ * location should be included in output.
+ * Three kinds of return values may be used as follows:
+ *<ul>
+ * <li><code>null</code> to indicate that the Array should be skipped
+ * </li>
+ * <li>{@link #INCLUDE_ALL} to indicate that the Array should be included
+ * completely in output
+ * </li>
+ * <li>Any other {@link TokenFilter} implementation (possibly this one) to mean
+ * that further inclusion calls on return filter object need to be made
+ * on contained element values, as necessary. {@link #filterFinishArray()} will
+ * also be called on returned filter object
+ * </li>
+ * </ul>
+ *<p>
+ * Default implementation returns <code>this</code>, which means that checks
+ * are made recursively for elements of the array to determine possible inclusion.
+ *
+ * @return TokenFilter to use for further calls within Array, unless return value
+ * is <code>null</code> or {@link #INCLUDE_ALL} (which have simpler semantics)
+ */
+ public TokenFilter filterStartArray() {
+ return this;
+ }
+
+ /**
+ * Method called to indicate that output of non-filtered Object (one that may
+ * have been included either completely, or in part) is completed,
+ * in cases where filter other that {@link #INCLUDE_ALL} was returned.
+ * This occurs when {@link JsonGenerator#writeEndObject()} is called.
+ */
+ public void filterFinishObject() { }
+
+ /**
+ * Method called to indicate that output of non-filtered Array (one that may
+ * have been included either completely, or in part) is completed,
+ * in cases where filter other that {@link #INCLUDE_ALL} was returned.
+ * This occurs when {@link JsonGenerator#writeEndArray()} is called.
+ */
+ public void filterFinishArray() { }
+
+ /*
+ /**********************************************************
+ /* API, properties/elements
+ /**********************************************************
+ */
+
+ /**
+ * Method called to check whether property value with specified name,
+ * at current output location, should be included in output.
+ * Three kinds of return values may be used as follows:
+ *<ul>
+ * <li><code>null</code> to indicate that the property and its value should be skipped
+ * </li>
+ * <li>{@link #INCLUDE_ALL} to indicate that the property and its value should be included
+ * completely in output
+ * </li>
+ * <li>Any other {@link TokenFilter} implementation (possibly this one) to mean
+ * that further inclusion calls on returned filter object need to be made
+ * as necessary, to determine inclusion.
+ * </li>
+ * </ul>
+ *<p>
+ * The default implementation simply returns <code>this</code> to continue calling
+ * methods on this filter object, without full inclusion or exclusion.
+ *
+ * @return TokenFilter to use for further calls within property value, unless return value
+ * is <code>null</code> or {@link #INCLUDE_ALL} (which have simpler semantics)
+ */
+ public TokenFilter includeProperty(String name) {
+ return this;
+ }
+
+ /**
+ * Method called to check whether array element with specified index (zero-based),
+ * at current output location, should be included in output.
+ * Three kinds of return values may be used as follows:
+ *<ul>
+ * <li><code>null</code> to indicate that the Array element should be skipped
+ * </li>
+ * <li>{@link #INCLUDE_ALL} to indicate that the Array element should be included
+ * completely in output
+ * </li>
+ * <li>Any other {@link TokenFilter} implementation (possibly this one) to mean
+ * that further inclusion calls on returned filter object need to be made
+ * as necessary, to determine inclusion.
+ * </li>
+ * </ul>
+ *<p>
+ * The default implementation simply returns <code>this</code> to continue calling
+ * methods on this filter object, without full inclusion or exclusion.
+ *
+ * @return TokenFilter to use for further calls within element value, unless return value
+ * is <code>null</code> or {@link #INCLUDE_ALL} (which have simpler semantics)
+ */
+ public TokenFilter includeElement(int index) {
+ return this;
+ }
+
+ /**
+ * Method called to check whether root-level value,
+ * at current output location, should be included in output.
+ * Three kinds of return values may be used as follows:
+ *<ul>
+ * <li><code>null</code> to indicate that the root value should be skipped
+ * </li>
+ * <li>{@link #INCLUDE_ALL} to indicate that the root value should be included
+ * completely in output
+ * </li>
+ * <li>Any other {@link TokenFilter} implementation (possibly this one) to mean
+ * that further inclusion calls on returned filter object need to be made
+ * as necessary, to determine inclusion.
+ * </li>
+ * </ul>
+ *<p>
+ * The default implementation simply returns <code>this</code> to continue calling
+ * methods on this filter object, without full inclusion or exclusion.
+ *
+ * @return TokenFilter to use for further calls within root value, unless return value
+ * is <code>null</code> or {@link #INCLUDE_ALL} (which have simpler semantics)
+ */
+ public TokenFilter includeRootValue(int index) {
+ return this;
+ }
+
+ /*
+ /**********************************************************
+ /* API, scalar values (being read)
+ /**********************************************************
+ */
+
+ /**
+ * Call made when verifying whether a scaler value is being
+ * read from a parser.
+ *<p>
+ * Default action is to call <code>_includeScalar()</code> and return
+ * whatever it indicates.
+ */
+ public boolean includeValue(JsonParser p) throws IOException {
+ return _includeScalar();
+ }
+
+ /*
+ /**********************************************************
+ /* API, scalar values (being written)
+ /**********************************************************
+ */
+
+ /**
+ * Call made to verify whether leaf-level
+ * boolean value
+ * should be included in output or not.
+ */
+ public boolean includeBoolean(boolean value) {
+ return _includeScalar();
+ }
+
+ /**
+ * Call made to verify whether leaf-level
+ * null value
+ * should be included in output or not.
+ */
+ public boolean includeNull() {
+ return _includeScalar();
+ }
+
+ /**
+ * Call made to verify whether leaf-level
+ * String value
+ * should be included in output or not.
+ */
+ public boolean includeString(String value) {
+ return _includeScalar();
+ }
+
+ /**
+ * Call made to verify whether leaf-level
+ * <code>int</code> value
+ * should be included in output or not.
+ *
+ * NOTE: also called for `short`, `byte`
+ */
+ public boolean includeNumber(int v) {
+ return _includeScalar();
+ }
+
+ /**
+ * Call made to verify whether leaf-level
+ * <code>long</code> value
+ * should be included in output or not.
+ */
+ public boolean includeNumber(long v) {
+ return _includeScalar();
+ }
+
+ /**
+ * Call made to verify whether leaf-level
+ * <code>float</code> value
+ * should be included in output or not.
+ */
+ public boolean includeNumber(float v) {
+ return _includeScalar();
+ }
+
+ /**
+ * Call made to verify whether leaf-level
+ * <code>double</code> value
+ * should be included in output or not.
+ */
+ public boolean includeNumber(double v) {
+ return _includeScalar();
+ }
+
+ /**
+ * Call made to verify whether leaf-level
+ * {@link BigDecimal} value
+ * should be included in output or not.
+ */
+ public boolean includeNumber(BigDecimal v) {
+ return _includeScalar();
+ }
+
+ /**
+ * Call made to verify whether leaf-level
+ * {@link BigInteger} value
+ * should be included in output or not.
+ */
+ public boolean includeNumber(BigInteger v) {
+ return _includeScalar();
+ }
+
+ /**
+ * Call made to verify whether leaf-level
+ * Binary value
+ * should be included in output or not.
+ *<p>
+ * NOTE: no binary payload passed; assumption is this won't be of much use.
+ */
+ public boolean includeBinary() {
+ return _includeScalar();
+ }
+
+ /**
+ * Call made to verify whether leaf-level
+ * raw (pre-encoded, not quoted by generator) value
+ * should be included in output or not.
+ *<p>
+ * NOTE: value itself not passed since it may come on multiple forms
+ * and is unlikely to be of much use in determining inclusion
+ * criteria.
+ */
+ public boolean includeRawValue() {
+ return _includeScalar();
+ }
+
+ /**
+ * Call made to verify whether leaf-level
+ * embedded (Opaque) value
+ * should be included in output or not.
+ */
+ public boolean includeEmbeddedValue(Object ob) {
+ return _includeScalar();
+ }
+
+ /*
+ /**********************************************************
+ /* Overrides
+ /**********************************************************
+ */
+
+ @Override
+ public String toString() {
+ if (this == INCLUDE_ALL) {
+ return "TokenFilter.INCLUDE_ALL";
+ }
+ return super.toString();
+ }
+
+ /*
+ /**********************************************************
+ /* Other methods
+ /**********************************************************
+ */
+
+ /**
+ * Overridable default implementation delegated to all scalar value
+ * inclusion check methods.
+ * The default implementation simply includes all leaf values.
+ */
+ protected boolean _includeScalar() {
+ return true;
+ }
+}
diff --git a/src/main/java/com/fasterxml/jackson/core/filter/TokenFilterContext.java b/src/main/java/com/fasterxml/jackson/core/filter/TokenFilterContext.java
new file mode 100644
index 0000000..fcca317
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/core/filter/TokenFilterContext.java
@@ -0,0 +1,353 @@
+package com.fasterxml.jackson.core.filter;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.*;
+
+/**
+ * Alternative variant of {@link JsonStreamContext}, used when filtering
+ * content being read or written (based on {@link TokenFilter}).
+ *
+ * @since 2.6
+ */
+public class TokenFilterContext extends JsonStreamContext
+{
+ /**
+ * Parent context for this context; null for root context.
+ */
+ protected final TokenFilterContext _parent;
+
+ /*
+ /**********************************************************
+ /* Simple instance reuse slots; speed up things
+ /* a bit (10-15%) for docs with lots of small
+ /* arrays/objects
+ /**********************************************************
+ */
+
+ protected TokenFilterContext _child = null;
+
+ /*
+ /**********************************************************
+ /* Location/state information
+ /**********************************************************
+ */
+
+ /**
+ * Name of the field of which value is to be parsed; only
+ * used for OBJECT contexts
+ */
+ protected String _currentName;
+
+ /**
+ * Filter to use for items in this state (for properties of Objects,
+ * elements of Arrays, and root-level values of root context)
+ */
+ protected TokenFilter _filter;
+
+ /**
+ * Flag that indicates that start token has been read/written,
+ * so that matching close token needs to be read/written as well
+ * when context is getting closed.
+ */
+ protected boolean _startHandled;
+
+ /**
+ * Flag that indicates that the current name of this context
+ * still needs to be read/written, if path from root down to
+ * included leaf is to be exposed.
+ */
+ protected boolean _needToHandleName;
+
+ /*
+ /**********************************************************
+ /* Life-cycle
+ /**********************************************************
+ */
+
+ protected TokenFilterContext(int type, TokenFilterContext parent,
+ TokenFilter filter, boolean startHandled)
+ {
+ super();
+ _type = type;
+ _parent = parent;
+ _filter = filter;
+ _index = -1;
+ _startHandled = startHandled;
+ _needToHandleName = false;
+ }
+
+ protected TokenFilterContext reset(int type,
+ TokenFilter filter, boolean startWritten)
+ {
+ _type = type;
+ _filter = filter;
+ _index = -1;
+ _currentName = null;
+ _startHandled = startWritten;
+ _needToHandleName = false;
+ return this;
+ }
+
+ /*
+ /**********************************************************
+ /* Factory methods
+ /**********************************************************
+ */
+
+ public static TokenFilterContext createRootContext(TokenFilter filter) {
+ // true -> since we have no start/end marker, consider start handled
+ return new TokenFilterContext(TYPE_ROOT, null, filter, true);
+ }
+
+ public TokenFilterContext createChildArrayContext(TokenFilter filter, boolean writeStart) {
+ TokenFilterContext ctxt = _child;
+ if (ctxt == null) {
+ _child = ctxt = new TokenFilterContext(TYPE_ARRAY, this, filter, writeStart);
+ return ctxt;
+ }
+ return ctxt.reset(TYPE_ARRAY, filter, writeStart);
+ }
+
+ public TokenFilterContext createChildObjectContext(TokenFilter filter, boolean writeStart) {
+ TokenFilterContext ctxt = _child;
+ if (ctxt == null) {
+ _child = ctxt = new TokenFilterContext(TYPE_OBJECT, this, filter, writeStart);
+ return ctxt;
+ }
+ return ctxt.reset(TYPE_OBJECT, filter, writeStart);
+ }
+
+ /*
+ /**********************************************************
+ /* State changes
+ /**********************************************************
+ */
+
+ public TokenFilter setFieldName(String name) throws JsonProcessingException {
+ _currentName = name;
+ _needToHandleName = true;
+ return _filter;
+ }
+
+ /**
+ * Method called to check whether value is to be included at current output
+ * position, either as Object property, Array element, or root value.
+ */
+ public TokenFilter checkValue(TokenFilter filter) {
+ // First, checks for Object properties have been made earlier:
+ if (_type == TYPE_OBJECT) {
+ return filter;
+ }
+ // We increase it first because at the beginning of array, value is -1
+ int ix = ++_index;
+ if (_type == TYPE_ARRAY) {
+ return filter.includeElement(ix);
+ }
+ return filter.includeRootValue(ix);
+ }
+
+ /**
+ * Method called to ensure that parent path from root is written up to
+ * and including this node.
+ */
+ public void writePath(JsonGenerator gen) throws IOException
+ {
+ if ((_filter == null) || (_filter == TokenFilter.INCLUDE_ALL)) {
+ return;
+ }
+ if (_parent != null) {
+ _parent._writePath(gen);
+ }
+ if (_startHandled) {
+ // even if Object started, need to start leaf-level name
+ if (_needToHandleName) {
+ gen.writeFieldName(_currentName);
+ }
+ } else {
+ _startHandled = true;
+ if (_type == TYPE_OBJECT) {
+ gen.writeStartObject();
+ gen.writeFieldName(_currentName); // we know name must be written
+ } else if (_type == TYPE_ARRAY) {
+ gen.writeStartArray();
+ }
+ }
+ }
+
+ /**
+ * Variant of {@link #writePath(JsonGenerator)} called when all we
+ * need is immediately surrounding Object. Method typically called
+ * when including a single property but not including full path
+ * to root.
+ */
+ public void writeImmediatePath(JsonGenerator gen) throws IOException
+ {
+ if ((_filter == null) || (_filter == TokenFilter.INCLUDE_ALL)) {
+ return;
+ }
+ if (_startHandled) {
+ // even if Object started, need to start leaf-level name
+ if (_needToHandleName) {
+ gen.writeFieldName(_currentName);
+ }
+ } else {
+ _startHandled = true;
+ if (_type == TYPE_OBJECT) {
+ gen.writeStartObject();
+ if (_needToHandleName) {
+ gen.writeFieldName(_currentName);
+ }
+ } else if (_type == TYPE_ARRAY) {
+ gen.writeStartArray();
+ }
+ }
+ }
+
+ private void _writePath(JsonGenerator gen) throws IOException
+ {
+ if ((_filter == null) || (_filter == TokenFilter.INCLUDE_ALL)) {
+ return;
+ }
+ if (_parent != null) {
+ _parent._writePath(gen);
+ }
+ if (_startHandled) {
+ // even if Object started, need to start leaf-level name
+ if (_needToHandleName) {
+ _needToHandleName = false; // at parent must explicitly clear
+ gen.writeFieldName(_currentName);
+ }
+ } else {
+ _startHandled = true;
+ if (_type == TYPE_OBJECT) {
+ gen.writeStartObject();
+ if (_needToHandleName) {
+ _needToHandleName = false; // at parent must explicitly clear
+ gen.writeFieldName(_currentName);
+ }
+ } else if (_type == TYPE_ARRAY) {
+ gen.writeStartArray();
+ }
+ }
+ }
+
+ public TokenFilterContext closeArray(JsonGenerator gen) throws IOException
+ {
+ if (_startHandled) {
+ gen.writeEndArray();
+ }
+ if ((_filter != null) && (_filter != TokenFilter.INCLUDE_ALL)) {
+ _filter.filterFinishArray();
+ }
+ return _parent;
+ }
+
+ public TokenFilterContext closeObject(JsonGenerator gen) throws IOException
+ {
+ if (_startHandled) {
+ gen.writeEndObject();
+ }
+ if ((_filter != null) && (_filter != TokenFilter.INCLUDE_ALL)) {
+ _filter.filterFinishObject();
+ }
+ return _parent;
+ }
+
+ public void skipParentChecks() {
+ _filter = null;
+ for (TokenFilterContext ctxt = _parent; ctxt != null; ctxt = ctxt._parent) {
+ _parent._filter = null;
+ }
+ }
+
+ /*
+ /**********************************************************
+ /* Accessors, mutators
+ /**********************************************************
+ */
+
+ @Override
+ public Object getCurrentValue() { return null; }
+
+ @Override
+ public void setCurrentValue(Object v) { }
+
+ @Override public final TokenFilterContext getParent() { return _parent; }
+ @Override public final String getCurrentName() { return _currentName; }
+
+ public TokenFilter getFilter() { return _filter; }
+ public boolean isStartHandled() { return _startHandled; }
+
+ public JsonToken nextTokenToRead() {
+ if (!_startHandled) {
+ _startHandled = true;
+ if (_type == TYPE_OBJECT) {
+ return JsonToken.START_OBJECT;
+ }
+ // Note: root should never be unhandled
+ return JsonToken.START_ARRAY;
+ }
+ // But otherwise at most might have FIELD_NAME
+ if (_needToHandleName && (_type == TYPE_OBJECT)) {
+ _needToHandleName = false;
+ return JsonToken.FIELD_NAME;
+ }
+ return null;
+ }
+
+ public TokenFilterContext findChildOf(TokenFilterContext parent) {
+ if (_parent == parent) {
+ return this;
+ }
+ TokenFilterContext curr = _parent;
+ while (curr != null) {
+ TokenFilterContext p = curr._parent;
+ if (p == parent) {
+ return curr;
+ }
+ curr = p;
+ }
+ // should never occur but...
+ return null;
+ }
+
+ // // // Internally used abstract methods
+
+ protected void appendDesc(StringBuilder sb) {
+ if (_parent != null) {
+ _parent.appendDesc(sb);
+ }
+ if (_type == TYPE_OBJECT) {
+ sb.append('{');
+ if (_currentName != null) {
+ sb.append('"');
+ // !!! TODO: Name chars should be escaped?
+ sb.append(_currentName);
+ sb.append('"');
+ } else {
+ sb.append('?');
+ }
+ sb.append('}');
+ } else if (_type == TYPE_ARRAY) {
+ sb.append('[');
+ sb.append(getCurrentIndex());
+ sb.append(']');
+ } else {
+ // nah, ROOT:
+ sb.append("/");
+ }
+ }
+
+ // // // Overridden standard methods
+
+ /**
+ * Overridden to provide developer writeable "JsonPath" representation
+ * of the context.
+ */
+ @Override public String toString() {
+ StringBuilder sb = new StringBuilder(64);
+ appendDesc(sb);
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/fasterxml/jackson/core/io/IOContext.java b/src/main/java/com/fasterxml/jackson/core/io/IOContext.java
index 7229da2..84fefc1 100644
--- a/src/main/java/com/fasterxml/jackson/core/io/IOContext.java
+++ b/src/main/java/com/fasterxml/jackson/core/io/IOContext.java
@@ -109,6 +109,14 @@
_encoding = enc;
}
+ /**
+ * @since 1.6
+ */
+ public IOContext withEncoding(JsonEncoding enc) {
+ _encoding = enc;
+ return this;
+ }
+
/*
/**********************************************************
/* Public API, accessors
diff --git a/src/main/java/com/fasterxml/jackson/core/io/NumberOutput.java b/src/main/java/com/fasterxml/jackson/core/io/NumberOutput.java
index bbe0c08..523c401 100644
--- a/src/main/java/com/fasterxml/jackson/core/io/NumberOutput.java
+++ b/src/main/java/com/fasterxml/jackson/core/io/NumberOutput.java
@@ -316,6 +316,13 @@
return Double.toString(v);
}
+ /**
+ * @since 2.6.0
+ */
+ public static String toString(float v) {
+ return Float.toString(v);
+ }
+
/*
/**********************************************************
/* Internal methods
diff --git a/src/main/java/com/fasterxml/jackson/core/json/ByteSourceJsonBootstrapper.java b/src/main/java/com/fasterxml/jackson/core/json/ByteSourceJsonBootstrapper.java
index 3dfda9d..f59e032 100644
--- a/src/main/java/com/fasterxml/jackson/core/json/ByteSourceJsonBootstrapper.java
+++ b/src/main/java/com/fasterxml/jackson/core/json/ByteSourceJsonBootstrapper.java
@@ -6,7 +6,7 @@
import com.fasterxml.jackson.core.format.InputAccessor;
import com.fasterxml.jackson.core.format.MatchStrength;
import com.fasterxml.jackson.core.io.*;
-import com.fasterxml.jackson.core.sym.BytesToNameCanonicalizer;
+import com.fasterxml.jackson.core.sym.ByteQuadsCanonicalizer;
import com.fasterxml.jackson.core.sym.CharsToNameCanonicalizer;
/**
@@ -209,7 +209,7 @@
}
public JsonParser constructParser(int parserFeatures, ObjectCodec codec,
- BytesToNameCanonicalizer rootByteSymbols, CharsToNameCanonicalizer rootCharSymbols,
+ ByteQuadsCanonicalizer rootByteSymbols, CharsToNameCanonicalizer rootCharSymbols,
int factoryFeatures) throws IOException
{
JsonEncoding enc = detectEncoding();
@@ -219,7 +219,7 @@
* (which is ok for larger input; not so hot for smaller; but this is not a common case)
*/
if (JsonFactory.Feature.CANONICALIZE_FIELD_NAMES.enabledIn(factoryFeatures)) {
- BytesToNameCanonicalizer can = rootByteSymbols.makeChild(factoryFeatures);
+ ByteQuadsCanonicalizer can = rootByteSymbols.makeChild(factoryFeatures);
return new UTF8StreamJsonParser(_context, parserFeatures, _in, codec, can,
_inputBuffer, _inputPtr, _inputEnd, _bufferRecyclable);
}
diff --git a/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java b/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java
index dbe7c9d..9ce93e4 100644
--- a/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java
+++ b/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java
@@ -163,8 +163,7 @@
// // Overrides just to make things final, to possibly help with inlining
@Override
- public final void writeStringField(String fieldName, String value)
- throws IOException, JsonGenerationException
+ public final void writeStringField(String fieldName, String value) throws IOException
{
writeFieldName(fieldName);
writeString(value);
diff --git a/src/main/java/com/fasterxml/jackson/core/json/JsonWriteContext.java b/src/main/java/com/fasterxml/jackson/core/json/JsonWriteContext.java
index 4ff12d7..f15bd54 100644
--- a/src/main/java/com/fasterxml/jackson/core/json/JsonWriteContext.java
+++ b/src/main/java/com/fasterxml/jackson/core/json/JsonWriteContext.java
@@ -133,16 +133,12 @@
return ctxt.reset(TYPE_OBJECT);
}
- // // // Shared API
-
@Override public final JsonWriteContext getParent() { return _parent; }
@Override public final String getCurrentName() { return _currentName; }
public DupDetector getDupDetector() {
return _dups;
}
-
- // // // API sub-classes are to implement
/**
* Method that writer is to call before it writes a field name.
@@ -166,6 +162,9 @@
public int writeValue() {
// Most likely, object:
if (_type == TYPE_OBJECT) {
+ if (!_gotName) {
+ return STATUS_EXPECT_NAME;
+ }
_gotName = false;
++_index;
return STATUS_OK_AFTER_COLON;
diff --git a/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java
index cf8085a..1bc9d34 100644
--- a/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java
+++ b/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java
@@ -250,6 +250,9 @@
}
return _textBuffer.contentsAsString();
}
+ if (_currToken == JsonToken.FIELD_NAME) {
+ return getCurrentName();
+ }
return super.getValueAsString(null);
}
@@ -263,6 +266,9 @@
}
return _textBuffer.contentsAsString();
}
+ if (_currToken == JsonToken.FIELD_NAME) {
+ return getCurrentName();
+ }
return super.getValueAsString(defValue);
}
@@ -544,10 +550,10 @@
}
/*
- /**********************************************************
- /* Public API, traversal
- /**********************************************************
- */
+ /**********************************************************
+ /* Public API, traversal
+ /**********************************************************
+ */
/**
* @return Next token from the stream, if any found, or null
@@ -707,11 +713,168 @@
}
/*
- @Override
- public boolean nextFieldName(SerializableString str)
- throws IOException
+ /**********************************************************
+ /* Public API, nextXxx() overrides
+ /**********************************************************
*/
+ /*
+ @Override
+ public boolean nextFieldName(SerializableString str)
+ throws IOException
+ {
+
+ }
+ */
+
+ @Override
+ public String nextFieldName() throws IOException
+ {
+ // // // Note: this is almost a verbatim copy of nextToken() (minus comments)
+
+ _numTypesValid = NR_UNKNOWN;
+ if (_currToken == JsonToken.FIELD_NAME) {
+ _nextAfterName();
+ return null;
+ }
+ if (_tokenIncomplete) {
+ _skipString();
+ }
+ int i = _skipWSOrEnd();
+ if (i < 0) {
+ close();
+ _currToken = null;
+ return null;
+ }
+ _tokenInputTotal = _currInputProcessed + _inputPtr - 1;
+ _tokenInputRow = _currInputRow;
+ _tokenInputCol = _inputPtr - _currInputRowStart - 1;
+ _binaryValue = null;
+ if (i == INT_RBRACKET) {
+ if (!_parsingContext.inArray()) {
+ _reportMismatchedEndMarker(i, '}');
+ }
+ _parsingContext = _parsingContext.getParent();
+ _currToken = JsonToken.END_ARRAY;
+ return null;
+ }
+ if (i == INT_RCURLY) {
+ if (!_parsingContext.inObject()) {
+ _reportMismatchedEndMarker(i, ']');
+ }
+ _parsingContext = _parsingContext.getParent();
+ _currToken = JsonToken.END_OBJECT;
+ return null;
+ }
+ if (_parsingContext.expectComma()) {
+ i = _skipComma(i);
+ }
+
+ if (!_parsingContext.inObject()) {
+ _nextTokenNotInObject(i);
+ return null;
+ }
+
+ String name = (i == INT_QUOTE) ? _parseName() : _handleOddName(i);
+ _parsingContext.setCurrentName(name);
+ _currToken = JsonToken.FIELD_NAME;
+ i = _skipColon();
+
+ if (i == INT_QUOTE) {
+ _tokenIncomplete = true;
+ _nextToken = JsonToken.VALUE_STRING;
+ return name;
+ }
+
+ // Ok: we must have a value... what is it?
+
+ JsonToken t;
+
+ switch (i) {
+ case '-':
+ t = _parseNegNumber();
+ break;
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ t = _parsePosNumber(i);
+ break;
+ case 'f':
+ _matchFalse();
+ t = JsonToken.VALUE_FALSE;
+ break;
+ case 'n':
+ _matchNull();
+ t = JsonToken.VALUE_NULL;
+ break;
+ case 't':
+ _matchTrue();
+ t = JsonToken.VALUE_TRUE;
+ break;
+ case '[':
+ t = JsonToken.START_ARRAY;
+ break;
+ case '{':
+ t = JsonToken.START_OBJECT;
+ break;
+ default:
+ t = _handleOddValue(i);
+ break;
+ }
+ _nextToken = t;
+ return name;
+ }
+
+ private final JsonToken _nextTokenNotInObject(int i) throws IOException
+ {
+ if (i == INT_QUOTE) {
+ _tokenIncomplete = true;
+ return (_currToken = JsonToken.VALUE_STRING);
+ }
+ switch (i) {
+ case '[':
+ _parsingContext = _parsingContext.createChildArrayContext(_tokenInputRow, _tokenInputCol);
+ return (_currToken = JsonToken.START_ARRAY);
+ case '{':
+ _parsingContext = _parsingContext.createChildObjectContext(_tokenInputRow, _tokenInputCol);
+ return (_currToken = JsonToken.START_OBJECT);
+ case 't':
+ _matchToken("true", 1);
+ return (_currToken = JsonToken.VALUE_TRUE);
+ case 'f':
+ _matchToken("false", 1);
+ return (_currToken = JsonToken.VALUE_FALSE);
+ case 'n':
+ _matchToken("null", 1);
+ return (_currToken = JsonToken.VALUE_NULL);
+ case '-':
+ return (_currToken = _parseNegNumber());
+ /* Should we have separate handling for plus? Although
+ * it is not allowed per se, it may be erroneously used,
+ * and could be indicated by a more specific error message.
+ */
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ return (_currToken = _parsePosNumber(i));
+ }
+ return (_currToken = _handleOddValue(i));
+ }
+
// note: identical to one in UTF8StreamJsonParser
@Override
public final String nextTextValue() throws IOException
diff --git a/src/main/java/com/fasterxml/jackson/core/json/UTF8JsonGenerator.java b/src/main/java/com/fasterxml/jackson/core/json/UTF8JsonGenerator.java
index 6f34187..6d32010 100644
--- a/src/main/java/com/fasterxml/jackson/core/json/UTF8JsonGenerator.java
+++ b/src/main/java/com/fasterxml/jackson/core/json/UTF8JsonGenerator.java
@@ -13,12 +13,12 @@
private final static byte BYTE_u = (byte) 'u';
private final static byte BYTE_0 = (byte) '0';
-
+
private final static byte BYTE_LBRACKET = (byte) '[';
private final static byte BYTE_RBRACKET = (byte) ']';
private final static byte BYTE_LCURLY = (byte) '{';
private final static byte BYTE_RCURLY = (byte) '}';
-
+
private final static byte BYTE_BACKSLASH = (byte) '\\';
private final static byte BYTE_COMMA = (byte) ',';
private final static byte BYTE_COLON = (byte) ':';
@@ -26,7 +26,7 @@
// intermediate copies only made up to certain length...
private final static int MAX_BYTES_TO_BUFFER = 512;
-
+
final static byte[] HEX_CHARS = CharTypes.copyHexBytes();
private final static byte[] NULL_BYTES = { 'n', 'u', 'l', 'l' };
@@ -38,7 +38,7 @@
/* Output buffering
/**********************************************************
*/
-
+
/**
* Underlying output stream used for writing JSON content.
*/
@@ -67,18 +67,18 @@
* in the output buffer after escaping
*/
protected final int _outputMaxContiguous;
-
+
/**
* Intermediate buffer in which characters of a String are copied
* before being encoded.
*/
protected char[] _charBuffer;
-
+
/**
* Length of <code>_charBuffer</code>
*/
protected final int _charBufferLength;
-
+
/**
* 6 character temporary buffer allocated if needed, for constructing
* escape sequences
@@ -145,7 +145,7 @@
_outputBuffer = outputBuffer;
_outputEnd = _outputBuffer.length;
// up to 6 bytes per char (see above), rounded up to 1/8
- _outputMaxContiguous = _outputEnd >> 3;
+ _outputMaxContiguous = (_outputEnd >> 3);
_charBuffer = ctxt.allocConcatBuffer();
_charBufferLength = _charBuffer.length;
_cfgUnqNames = !Feature.QUOTE_FIELD_NAMES.enabledIn(features);
@@ -162,6 +162,12 @@
return _outputStream;
}
+ @Override
+ public int getOutputBuffered() {
+ // Assuming tail is always valid, set to 0 on close
+ return _outputTail;
+ }
+
/*
/**********************************************************
/* Overridden methods
@@ -202,15 +208,14 @@
_flushBuffer();
}
_outputBuffer[_outputTail++] = BYTE_QUOTE;
- name.getChars(0, len, _charBuffer, 0);
// But as one segment, or multiple?
if (len <= _outputMaxContiguous) {
if ((_outputTail + len) > _outputEnd) { // caller must ensure enough space
_flushBuffer();
}
- _writeStringSegment(_charBuffer, 0, len);
+ _writeStringSegment(name, 0, len);
} else {
- _writeStringSegments(_charBuffer, 0, len);
+ _writeStringSegments(name, 0, len);
}
// and closing quotes; need room for one more char:
if (_outputTail >= _outputEnd) {
@@ -416,45 +421,22 @@
@Override
public void writeString(String text) throws IOException
{
- _verifyValueWrite("write text value");
+ _verifyValueWrite(WRITE_STRING);
if (text == null) {
_writeNull();
return;
}
- // First: can we make a local copy of chars that make up text?
+ // First: if we can't guarantee it all fits, quoted, within output, offline
final int len = text.length();
- if (len > _charBufferLength) { // nope: off-line handling
+ if (len > _outputMaxContiguous) { // nope: off-line handling
_writeStringSegments(text, true);
return;
}
- // yes: good.
- text.getChars(0, len, _charBuffer, 0);
- // Output: if we can't guarantee it fits in output buffer, off-line as well:
- if (len > _outputMaxContiguous) {
- _writeLongString(_charBuffer, 0, len);
- return;
- }
if ((_outputTail + len) >= _outputEnd) {
_flushBuffer();
}
_outputBuffer[_outputTail++] = BYTE_QUOTE;
- _writeStringSegment(_charBuffer, 0, len); // we checked space already above
- /* [JACKSON-462] But that method may have had to expand multi-byte Unicode
- * chars, so we must check again
- */
- if (_outputTail >= _outputEnd) {
- _flushBuffer();
- }
- _outputBuffer[_outputTail++] = BYTE_QUOTE;
- }
-
- private void _writeLongString(char[] text, int offset, int len) throws IOException
- {
- if (_outputTail >= _outputEnd) {
- _flushBuffer();
- }
- _outputBuffer[_outputTail++] = BYTE_QUOTE;
- _writeStringSegments(text, 0, len);
+ _writeStringSegment(text, 0, len); // we checked space already above
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
@@ -464,7 +446,7 @@
@Override
public void writeString(char[] text, int offset, int len) throws IOException
{
- _verifyValueWrite("write text value");
+ _verifyValueWrite(WRITE_STRING);
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
@@ -488,7 +470,7 @@
@Override
public final void writeString(SerializableString text) throws IOException
{
- _verifyValueWrite("write text value");
+ _verifyValueWrite(WRITE_STRING);
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
@@ -504,11 +486,11 @@
}
_outputBuffer[_outputTail++] = BYTE_QUOTE;
}
-
+
@Override
public void writeRawUTF8String(byte[] text, int offset, int length) throws IOException
{
- _verifyValueWrite("write text value");
+ _verifyValueWrite(WRITE_STRING);
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
@@ -523,7 +505,7 @@
@Override
public void writeUTF8String(byte[] text, int offset, int len) throws IOException
{
- _verifyValueWrite("write text value");
+ _verifyValueWrite(WRITE_STRING);
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
@@ -590,7 +572,7 @@
// since 2.5
@Override
public void writeRawValue(SerializableString text) throws IOException {
- _verifyValueWrite("write raw value");
+ _verifyValueWrite(WRITE_RAW);
byte[] raw = text.asUnquotedUTF8();
if (raw.length > 0) {
_writeBytes(raw);
@@ -710,7 +692,7 @@
byte[] data, int offset, int len)
throws IOException, JsonGenerationException
{
- _verifyValueWrite("write binary value");
+ _verifyValueWrite(WRITE_BINARY);
// Starting quotes
if (_outputTail >= _outputEnd) {
_flushBuffer();
@@ -729,7 +711,7 @@
InputStream data, int dataLength)
throws IOException, JsonGenerationException
{
- _verifyValueWrite("write binary value");
+ _verifyValueWrite(WRITE_BINARY);
// Starting quotes
if (_outputTail >= _outputEnd) {
_flushBuffer();
@@ -768,7 +750,7 @@
public void writeNumber(short s)
throws IOException, JsonGenerationException
{
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
// up to 5 digits and possible minus sign
if ((_outputTail + 6) >= _outputEnd) {
_flushBuffer();
@@ -793,7 +775,7 @@
public void writeNumber(int i)
throws IOException, JsonGenerationException
{
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
// up to 10 digits and possible minus sign
if ((_outputTail + 11) >= _outputEnd) {
_flushBuffer();
@@ -819,7 +801,7 @@
public void writeNumber(long l)
throws IOException, JsonGenerationException
{
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
if (_cfgNumbersAsStrings) {
_writeQuotedLong(l);
return;
@@ -845,7 +827,7 @@
public void writeNumber(BigInteger value)
throws IOException, JsonGenerationException
{
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
if (value == null) {
_writeNull();
} else if (_cfgNumbersAsStrings) {
@@ -868,7 +850,7 @@
return;
}
// What is the max length for doubles? 40 chars?
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
writeRaw(String.valueOf(d));
}
@@ -884,7 +866,7 @@
return;
}
// What is the max length for floats?
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
writeRaw(String.valueOf(f));
}
@@ -893,7 +875,7 @@
throws IOException, JsonGenerationException
{
// Don't really know max length for big decimal, no point checking
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
if (value == null) {
_writeNull();
} else if (_cfgNumbersAsStrings) {
@@ -910,7 +892,7 @@
public void writeNumber(String encodedValue)
throws IOException, JsonGenerationException
{
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
if (_cfgNumbersAsStrings) {
_writeQuotedRaw(encodedValue);
} else {
@@ -935,7 +917,7 @@
public void writeBoolean(boolean state)
throws IOException, JsonGenerationException
{
- _verifyValueWrite("write boolean value");
+ _verifyValueWrite(WRITE_BOOLEAN);
if ((_outputTail + 5) >= _outputEnd) {
_flushBuffer();
}
@@ -949,7 +931,7 @@
public void writeNull()
throws IOException, JsonGenerationException
{
- _verifyValueWrite("write null value");
+ _verifyValueWrite(WRITE_NULL);
_writeNull();
}
@@ -960,8 +942,7 @@
*/
@Override
- protected final void _verifyValueWrite(String typeMsg)
- throws IOException, JsonGenerationException
+ protected final void _verifyValueWrite(String typeMsg) throws IOException
{
int status = _writeContext.writeValue();
if (status == JsonWriteContext.STATUS_EXPECT_NAME) {
@@ -999,8 +980,7 @@
_verifyPrettyValueWrite(typeMsg, status);
}
- protected final void _verifyPrettyValueWrite(String typeMsg, int status)
- throws IOException, JsonGenerationException
+ protected final void _verifyPrettyValueWrite(String typeMsg, int status) throws IOException
{
// If we have a pretty printer, it knows what to do:
switch (status) {
@@ -1034,8 +1014,7 @@
*/
@Override
- public void flush()
- throws IOException
+ public void flush() throws IOException
{
_flushBuffer();
if (_outputStream != null) {
@@ -1046,8 +1025,7 @@
}
@Override
- public void close()
- throws IOException
+ public void close() throws IOException
{
super.close();
@@ -1055,7 +1033,7 @@
* scopes.
*/
// First: let's see that we still have buffers...
- if (_outputBuffer != null
+ if ((_outputBuffer != null)
&& isEnabled(Feature.AUTO_CLOSE_JSON_CONTENT)) {
while (true) {
JsonStreamContext ctxt = getOutputContext();
@@ -1069,6 +1047,7 @@
}
}
_flushBuffer();
+ _outputTail = 0; // just to ensure we don't think there's anything buffered
/* 25-Nov-2008, tatus: As per [JACKSON-16] we are not to call close()
* on the underlying Reader, unless we "own" it, or auto-closing
@@ -1162,15 +1141,13 @@
int left = text.length();
int offset = 0;
- final char[] cbuf = _charBuffer;
while (left > 0) {
int len = Math.min(_outputMaxContiguous, left);
- text.getChars(offset, offset+len, cbuf, 0);
if ((_outputTail + len) > _outputEnd) { // caller must ensure enough space
_flushBuffer();
}
- _writeStringSegment(cbuf, 0, len);
+ _writeStringSegment(text, offset, len);
offset += len;
left -= len;
}
@@ -1189,8 +1166,7 @@
* the output buffer. If so, we will need to choose smaller output
* chunks to write at a time.
*/
- private final void _writeStringSegments(char[] cbuf, int offset, int totalLen)
- throws IOException, JsonGenerationException
+ private final void _writeStringSegments(char[] cbuf, int offset, int totalLen) throws IOException
{
do {
int len = Math.min(_outputMaxContiguous, totalLen);
@@ -1203,6 +1179,19 @@
} while (totalLen > 0);
}
+ private final void _writeStringSegments(String text, int offset, int totalLen) throws IOException
+ {
+ do {
+ int len = Math.min(_outputMaxContiguous, totalLen);
+ if ((_outputTail + len) > _outputEnd) { // caller must ensure enough space
+ _flushBuffer();
+ }
+ _writeStringSegment(text, offset, len);
+ offset += len;
+ totalLen -= len;
+ } while (totalLen > 0);
+ }
+
/*
/**********************************************************
/* Internal methods, low-level writing, text segments
@@ -1218,7 +1207,7 @@
* potentially enough space for other cases (but not necessarily flushed)
*/
private final void _writeStringSegment(char[] cbuf, int offset, int len)
- throws IOException, JsonGenerationException
+ throws IOException
{
// note: caller MUST ensure (via flushing) there's room for ASCII only
@@ -1253,12 +1242,42 @@
}
}
+ private final void _writeStringSegment(String text, int offset, int len) throws IOException
+ {
+ // note: caller MUST ensure (via flushing) there's room for ASCII only
+ // Fast+tight loop for ASCII-only, no-escaping-needed output
+ len += offset; // becomes end marker, then
+
+ int outputPtr = _outputTail;
+ final byte[] outputBuffer = _outputBuffer;
+ final int[] escCodes = _outputEscapes;
+
+ while (offset < len) {
+ int ch = text.charAt(offset);
+ // note: here we know that (ch > 0x7F) will cover case of escaping non-ASCII too:
+ if (ch > 0x7F || escCodes[ch] != 0) {
+ break;
+ }
+ outputBuffer[outputPtr++] = (byte) ch;
+ ++offset;
+ }
+ _outputTail = outputPtr;
+ if (offset < len) {
+ if (_characterEscapes != null) {
+ _writeCustomStringSegment2(text, offset, len);
+ } else if (_maximumNonEscapedChar == 0) {
+ _writeStringSegment2(text, offset, len);
+ } else {
+ _writeStringSegmentASCII2(text, offset, len);
+ }
+ }
+ }
+
/**
* Secondary method called when content contains characters to escape,
* and/or multi-byte UTF-8 characters.
*/
- private final void _writeStringSegment2(final char[] cbuf, int offset, final int end)
- throws IOException, JsonGenerationException
+ private final void _writeStringSegment2(final char[] cbuf, int offset, final int end) throws IOException
{
// Ok: caller guarantees buffer can have room; but that may require flushing:
if ((_outputTail + 6 * (end - offset)) > _outputEnd) {
@@ -1297,6 +1316,44 @@
_outputTail = outputPtr;
}
+ private final void _writeStringSegment2(final String text, int offset, final int end) throws IOException
+ {
+ if ((_outputTail + 6 * (end - offset)) > _outputEnd) {
+ _flushBuffer();
+ }
+
+ int outputPtr = _outputTail;
+
+ final byte[] outputBuffer = _outputBuffer;
+ final int[] escCodes = _outputEscapes;
+
+ while (offset < end) {
+ int ch = text.charAt(offset++);
+ if (ch <= 0x7F) {
+ if (escCodes[ch] == 0) {
+ outputBuffer[outputPtr++] = (byte) ch;
+ continue;
+ }
+ int escape = escCodes[ch];
+ if (escape > 0) { // 2-char escape, fine
+ outputBuffer[outputPtr++] = BYTE_BACKSLASH;
+ outputBuffer[outputPtr++] = (byte) escape;
+ } else {
+ // ctrl-char, 6-byte escape...
+ outputPtr = _writeGenericEscape(ch, outputPtr);
+ }
+ continue;
+ }
+ if (ch <= 0x7FF) { // fine, just needs 2 byte output
+ outputBuffer[outputPtr++] = (byte) (0xc0 | (ch >> 6));
+ outputBuffer[outputPtr++] = (byte) (0x80 | (ch & 0x3f));
+ } else {
+ outputPtr = _outputMultiByteChar(ch, outputPtr);
+ }
+ }
+ _outputTail = outputPtr;
+ }
+
/*
/**********************************************************
/* Internal methods, low-level writing, text segment
@@ -1308,8 +1365,7 @@
* Same as <code>_writeStringSegment2(char[], ...)</code., but with
* additional escaping for high-range code points
*/
- private final void _writeStringSegmentASCII2(final char[] cbuf, int offset, final int end)
- throws IOException, JsonGenerationException
+ private final void _writeStringSegmentASCII2(final char[] cbuf, int offset, final int end) throws IOException
{
// Ok: caller guarantees buffer can have room; but that may require flushing:
if ((_outputTail + 6 * (end - offset)) > _outputEnd) {
@@ -1353,6 +1409,50 @@
_outputTail = outputPtr;
}
+ private final void _writeStringSegmentASCII2(final String text, int offset, final int end) throws IOException
+ {
+ // Ok: caller guarantees buffer can have room; but that may require flushing:
+ if ((_outputTail + 6 * (end - offset)) > _outputEnd) {
+ _flushBuffer();
+ }
+
+ int outputPtr = _outputTail;
+
+ final byte[] outputBuffer = _outputBuffer;
+ final int[] escCodes = _outputEscapes;
+ final int maxUnescaped = _maximumNonEscapedChar;
+
+ while (offset < end) {
+ int ch = text.charAt(offset++);
+ if (ch <= 0x7F) {
+ if (escCodes[ch] == 0) {
+ outputBuffer[outputPtr++] = (byte) ch;
+ continue;
+ }
+ int escape = escCodes[ch];
+ if (escape > 0) { // 2-char escape, fine
+ outputBuffer[outputPtr++] = BYTE_BACKSLASH;
+ outputBuffer[outputPtr++] = (byte) escape;
+ } else {
+ // ctrl-char, 6-byte escape...
+ outputPtr = _writeGenericEscape(ch, outputPtr);
+ }
+ continue;
+ }
+ if (ch > maxUnescaped) { // [JACKSON-102] Allow forced escaping if non-ASCII (etc) chars:
+ outputPtr = _writeGenericEscape(ch, outputPtr);
+ continue;
+ }
+ if (ch <= 0x7FF) { // fine, just needs 2 byte output
+ outputBuffer[outputPtr++] = (byte) (0xc0 | (ch >> 6));
+ outputBuffer[outputPtr++] = (byte) (0x80 | (ch & 0x3f));
+ } else {
+ outputPtr = _outputMultiByteChar(ch, outputPtr);
+ }
+ }
+ _outputTail = outputPtr;
+ }
+
/*
/**********************************************************
/* Internal methods, low-level writing, text segment
@@ -1364,8 +1464,7 @@
* Same as <code>_writeStringSegmentASCII2(char[], ...)</code., but with
* additional checking for completely custom escapes
*/
- private final void _writeCustomStringSegment2(final char[] cbuf, int offset, final int end)
- throws IOException, JsonGenerationException
+ private final void _writeCustomStringSegment2(final char[] cbuf, int offset, final int end) throws IOException
{
// Ok: caller guarantees buffer can have room; but that may require flushing:
if ((_outputTail + 6 * (end - offset)) > _outputEnd) {
@@ -1422,6 +1521,63 @@
_outputTail = outputPtr;
}
+ private final void _writeCustomStringSegment2(final String text, int offset, final int end) throws IOException
+ {
+ // Ok: caller guarantees buffer can have room; but that may require flushing:
+ if ((_outputTail + 6 * (end - offset)) > _outputEnd) {
+ _flushBuffer();
+ }
+ int outputPtr = _outputTail;
+
+ final byte[] outputBuffer = _outputBuffer;
+ final int[] escCodes = _outputEscapes;
+ // may or may not have this limit
+ final int maxUnescaped = (_maximumNonEscapedChar <= 0) ? 0xFFFF : _maximumNonEscapedChar;
+ final CharacterEscapes customEscapes = _characterEscapes; // non-null
+
+ while (offset < end) {
+ int ch = text.charAt(offset++);
+ if (ch <= 0x7F) {
+ if (escCodes[ch] == 0) {
+ outputBuffer[outputPtr++] = (byte) ch;
+ continue;
+ }
+ int escape = escCodes[ch];
+ if (escape > 0) { // 2-char escape, fine
+ outputBuffer[outputPtr++] = BYTE_BACKSLASH;
+ outputBuffer[outputPtr++] = (byte) escape;
+ } else if (escape == CharacterEscapes.ESCAPE_CUSTOM) {
+ SerializableString esc = customEscapes.getEscapeSequence(ch);
+ if (esc == null) {
+ _reportError("Invalid custom escape definitions; custom escape not found for character code 0x"
+ +Integer.toHexString(ch)+", although was supposed to have one");
+ }
+ outputPtr = _writeCustomEscape(outputBuffer, outputPtr, esc, end-offset);
+ } else {
+ // ctrl-char, 6-byte escape...
+ outputPtr = _writeGenericEscape(ch, outputPtr);
+ }
+ continue;
+ }
+ if (ch > maxUnescaped) { // [JACKSON-102] Allow forced escaping if non-ASCII (etc) chars:
+ outputPtr = _writeGenericEscape(ch, outputPtr);
+ continue;
+ }
+ SerializableString esc = customEscapes.getEscapeSequence(ch);
+ if (esc != null) {
+ outputPtr = _writeCustomEscape(outputBuffer, outputPtr, esc, end-offset);
+ continue;
+ }
+ if (ch <= 0x7FF) { // fine, just needs 2 byte output
+ outputBuffer[outputPtr++] = (byte) (0xc0 | (ch >> 6));
+ outputBuffer[outputPtr++] = (byte) (0x80 | (ch & 0x3f));
+ } else {
+ outputPtr = _outputMultiByteChar(ch, outputPtr);
+ }
+ }
+ _outputTail = outputPtr;
+ }
+
private final int _writeCustomEscape(byte[] outputBuffer, int outputPtr, SerializableString esc, int remainingChars)
throws IOException, JsonGenerationException
{
diff --git a/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java
index 8df7c35..cc04868 100644
--- a/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java
+++ b/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java
@@ -7,7 +7,7 @@
import com.fasterxml.jackson.core.base.ParserBase;
import com.fasterxml.jackson.core.io.CharTypes;
import com.fasterxml.jackson.core.io.IOContext;
-import com.fasterxml.jackson.core.sym.*;
+import com.fasterxml.jackson.core.sym.ByteQuadsCanonicalizer;
import com.fasterxml.jackson.core.util.*;
import static com.fasterxml.jackson.core.JsonTokenId.*;
@@ -46,14 +46,14 @@
/**
* Symbol table that contains field names encountered so far
*/
- final protected BytesToNameCanonicalizer _symbols;
-
+ final protected ByteQuadsCanonicalizer _symbols;
+
/*
/**********************************************************
/* Parsing state
/**********************************************************
*/
-
+
/**
* Temporary buffer used for name parsing.
*/
@@ -70,13 +70,13 @@
* Temporary storage for partially parsed name bytes.
*/
private int _quad1;
-
+
/*
/**********************************************************
/* Input buffering (from former 'StreamBasedParserBase')
/**********************************************************
*/
-
+
protected InputStream _inputStream;
/*
@@ -108,7 +108,7 @@
*/
public UTF8StreamJsonParser(IOContext ctxt, int features, InputStream in,
- ObjectCodec codec, BytesToNameCanonicalizer sym,
+ ObjectCodec codec, ByteQuadsCanonicalizer sym,
byte[] inputBuffer, int start, int end,
boolean bufferRecyclable)
{
@@ -277,13 +277,12 @@
*/
@Override
- public String getText()
- throws IOException, JsonParseException
+ public String getText() throws IOException
{
if (_currToken == JsonToken.VALUE_STRING) {
if (_tokenIncomplete) {
_tokenIncomplete = false;
- _finishString(); // only strings can be incomplete
+ return _finishAndReturnString(); // only strings can be incomplete
}
return _textBuffer.contentsAsString();
}
@@ -294,31 +293,77 @@
// @since 2.1
@Override
- public String getValueAsString() throws IOException, JsonParseException
+ public String getValueAsString() throws IOException
{
if (_currToken == JsonToken.VALUE_STRING) {
if (_tokenIncomplete) {
_tokenIncomplete = false;
- _finishString(); // only strings can be incomplete
+ return _finishAndReturnString(); // only strings can be incomplete
}
return _textBuffer.contentsAsString();
}
+ if (_currToken == JsonToken.FIELD_NAME) {
+ return getCurrentName();
+ }
return super.getValueAsString(null);
}
// @since 2.1
@Override
- public String getValueAsString(String defValue) throws IOException, JsonParseException
+ public String getValueAsString(String defValue) throws IOException
{
if (_currToken == JsonToken.VALUE_STRING) {
if (_tokenIncomplete) {
_tokenIncomplete = false;
- _finishString(); // only strings can be incomplete
+ return _finishAndReturnString(); // only strings can be incomplete
}
return _textBuffer.contentsAsString();
}
+ if (_currToken == JsonToken.FIELD_NAME) {
+ return getCurrentName();
+ }
return super.getValueAsString(defValue);
}
+
+ // since 2.6
+ @Override
+ public int getValueAsInt() throws IOException
+ {
+ JsonToken t = _currToken;
+ if ((t == JsonToken.VALUE_NUMBER_INT) || (t == JsonToken.VALUE_NUMBER_FLOAT)) {
+ // inlined 'getIntValue()'
+ if ((_numTypesValid & NR_INT) == 0) {
+ if (_numTypesValid == NR_UNKNOWN) {
+ return _parseIntValue();
+ }
+ if ((_numTypesValid & NR_INT) == 0) {
+ convertNumberToInt();
+ }
+ }
+ return _numberInt;
+ }
+ return super.getValueAsInt(0);
+ }
+
+ // since 2.6
+ @Override
+ public int getValueAsInt(int defValue) throws IOException
+ {
+ JsonToken t = _currToken;
+ if ((t == JsonToken.VALUE_NUMBER_INT) || (t == JsonToken.VALUE_NUMBER_FLOAT)) {
+ // inlined 'getIntValue()'
+ if ((_numTypesValid & NR_INT) == 0) {
+ if (_numTypesValid == NR_UNKNOWN) {
+ return _parseIntValue();
+ }
+ if ((_numTypesValid & NR_INT) == 0) {
+ convertNumberToInt();
+ }
+ }
+ return _numberInt;
+ }
+ return super.getValueAsInt(defValue);
+ }
protected final String _getText2(JsonToken t)
{
@@ -340,8 +385,7 @@
}
@Override
- public char[] getTextCharacters()
- throws IOException, JsonParseException
+ public char[] getTextCharacters() throws IOException
{
if (_currToken != null) { // null only before/after document
switch (_currToken.id()) {
@@ -378,8 +422,7 @@
}
@Override
- public int getTextLength()
- throws IOException, JsonParseException
+ public int getTextLength() throws IOException
{
if (_currToken != null) { // null only before/after document
switch (_currToken.id()) {
@@ -404,7 +447,7 @@
}
@Override
- public int getTextOffset() throws IOException, JsonParseException
+ public int getTextOffset() throws IOException
{
// Most have offset of 0, only some may have other values:
if (_currToken != null) {
@@ -427,8 +470,7 @@
}
@Override
- public byte[] getBinaryValue(Base64Variant b64variant)
- throws IOException, JsonParseException
+ public byte[] getBinaryValue(Base64Variant b64variant) throws IOException
{
if (_currToken != JsonToken.VALUE_STRING &&
(_currToken != JsonToken.VALUE_EMBEDDED_OBJECT || _binaryValue == null)) {
@@ -459,8 +501,7 @@
}
@Override
- public int readBinaryValue(Base64Variant b64variant, OutputStream out)
- throws IOException, JsonParseException
+ public int readBinaryValue(Base64Variant b64variant, OutputStream out) throws IOException
{
// if we have already read the token, just use whatever we may have
if (!_tokenIncomplete || _currToken != JsonToken.VALUE_STRING) {
@@ -478,8 +519,7 @@
}
protected int _readBinary(Base64Variant b64variant, OutputStream out,
- byte[] buffer)
- throws IOException, JsonParseException
+ byte[] buffer) throws IOException
{
int outputPtr = 0;
final int outputEnd = buffer.length - 3;
@@ -697,8 +737,8 @@
return _nextTokenNotInObject(i);
}
// So first parse the field name itself:
- Name n = _parseName(i);
- _parsingContext.setCurrentName(n.getName());
+ String n = _parseName(i);
+ _parsingContext.setCurrentName(n);
_currToken = JsonToken.FIELD_NAME;
i = _skipColon();
@@ -894,7 +934,8 @@
while (true) {
if (ptr == end) { // yes, match!
_parsingContext.setCurrentName(str.getValue());
- _isNextTokenNameYes(_skipColonFast(ptr+1));
+ i = _skipColonFast(ptr+1);
+ _isNextTokenNameYes(i);
return true;
}
if (nameBytes[offset] != _inputBuffer[ptr]) {
@@ -964,8 +1005,7 @@
return null;
}
- Name n = _parseName(i);
- final String nameStr = n.getName();
+ final String nameStr = _parseName(i);
_parsingContext.setCurrentName(nameStr);
_currToken = JsonToken.FIELD_NAME;
@@ -980,11 +1020,6 @@
case '-':
t = _parseNegNumber();
break;
-
- /* Should we have separate handling for plus? Although
- * it is not allowed per se, it may be erroneously used,
- * and could be indicate by a more specific error message.
- */
case '0':
case '1':
case '2':
@@ -1122,14 +1157,10 @@
private final boolean _isNextTokenNameMaybe(int i, SerializableString str) throws IOException
{
// // // and this is back to standard nextToken()
-
- Name n = _parseName(i);
- final boolean match;
- {
- String nameStr = n.getName();
- _parsingContext.setCurrentName(nameStr);
- match = nameStr.equals(str.getValue());
- }
+
+ String n = _parseName(i);
+ _parsingContext.setCurrentName(n);
+ final boolean match = n.equals(str.getValue());
_currToken = JsonToken.FIELD_NAME;
i = _skipColon();
@@ -1194,7 +1225,7 @@
if (t == JsonToken.VALUE_STRING) {
if (_tokenIncomplete) {
_tokenIncomplete = false;
- _finishString();
+ return _finishAndReturnString();
}
return _textBuffer.contentsAsString();
}
@@ -1278,16 +1309,16 @@
return null;
}
- switch (nextToken().id()) {
- case ID_TRUE:
+ JsonToken t = nextToken();
+ if (t == JsonToken.VALUE_TRUE) {
return Boolean.TRUE;
- case ID_FALSE:
- return Boolean.FALSE;
- default:
- return null;
}
+ if (t == JsonToken.VALUE_FALSE) {
+ return Boolean.FALSE;
+ }
+ return null;
}
-
+
/*
/**********************************************************
/* Internal methods, number parsing
@@ -1613,13 +1644,13 @@
/**********************************************************
*/
- protected final Name _parseName(int i) throws IOException
+ protected final String _parseName(int i) throws IOException
{
if (i != INT_QUOTE) {
return _handleOddName(i);
}
// First: can we optimize out bounds checks?
- if ((_inputPtr + 9) > _inputEnd) { // Need 8 chars, plus one trailing (quote)
+ if ((_inputPtr + 13) > _inputEnd) { // Need up to 12 chars, plus one trailing (quote)
return slowParseName();
}
@@ -1670,12 +1701,12 @@
return parseName(q, i, 1);
}
if (q == INT_QUOTE) { // special case, ""
- return BytesToNameCanonicalizer.getEmptyName();
+ return "";
}
return parseName(0, q, 0); // quoting or invalid char
}
- protected final Name parseMediumName(int q2) throws IOException
+ protected final String parseMediumName(int q2) throws IOException
{
final byte[] input = _inputBuffer;
final int[] codes = _icLatin1;
@@ -1712,18 +1743,62 @@
}
return parseName(_quad1, q2, i, 4);
}
- return parseLongName(i, q2);
+ return parseMediumName2(i, q2);
}
- protected final Name parseLongName(int q, final int q2) throws IOException
+ /**
+ * @since 2.6
+ */
+ protected final String parseMediumName2(int q3, final int q2) throws IOException
+ {
+ final byte[] input = _inputBuffer;
+ final int[] codes = _icLatin1;
+
+ // Got 9 name bytes so far
+ int i = input[_inputPtr++] & 0xFF;
+ if (codes[i] != 0) {
+ if (i == INT_QUOTE) { // 9 bytes
+ return findName(_quad1, q2, q3, 1);
+ }
+ return parseName(_quad1, q2, q3, i, 1);
+ }
+ q3 = (q3 << 8) | i;
+ i = input[_inputPtr++] & 0xFF;
+ if (codes[i] != 0) {
+ if (i == INT_QUOTE) { // 10 bytes
+ return findName(_quad1, q2, q3, 2);
+ }
+ return parseName(_quad1, q2, q3, i, 2);
+ }
+ q3 = (q3 << 8) | i;
+ i = input[_inputPtr++] & 0xFF;
+ if (codes[i] != 0) {
+ if (i == INT_QUOTE) { // 11 bytes
+ return findName(_quad1, q2, q3, 3);
+ }
+ return parseName(_quad1, q2, q3, i, 3);
+ }
+ q3 = (q3 << 8) | i;
+ i = input[_inputPtr++] & 0xFF;
+ if (codes[i] != 0) {
+ if (i == INT_QUOTE) { // 12 bytes
+ return findName(_quad1, q2, q3, 4);
+ }
+ return parseName(_quad1, q2, q3, i, 4);
+ }
+ return parseLongName(i, q2, q3);
+ }
+
+ protected final String parseLongName(int q, final int q2, int q3) throws IOException
{
_quadBuffer[0] = _quad1;
_quadBuffer[1] = q2;
+ _quadBuffer[2] = q3;
// As explained above, will ignore UTF-8 encoding at this point
final byte[] input = _inputBuffer;
final int[] codes = _icLatin1;
- int qlen = 2;
+ int qlen = 3;
while ((_inputPtr + 4) <= _inputEnd) {
int i = input[_inputPtr++] & 0xFF;
@@ -1778,10 +1853,10 @@
/**
* Method called when not even first 8 bytes are guaranteed
- * to come consecutively. Happens rarely, so this is off-lined;
+ * to come consequtively. Happens rarely, so this is offlined;
* plus we'll also do full checks for escaping etc.
*/
- protected Name slowParseName() throws IOException
+ protected String slowParseName() throws IOException
{
if (_inputPtr >= _inputEnd) {
if (!loadMore()) {
@@ -1790,28 +1865,34 @@
}
int i = _inputBuffer[_inputPtr++] & 0xFF;
if (i == INT_QUOTE) { // special case, ""
- return BytesToNameCanonicalizer.getEmptyName();
+ return "";
}
return parseEscapedName(_quadBuffer, 0, 0, i, 0);
}
- private final Name parseName(int q1, int ch, int lastQuadBytes) throws IOException {
+ private final String parseName(int q1, int ch, int lastQuadBytes) throws IOException {
return parseEscapedName(_quadBuffer, 0, q1, ch, lastQuadBytes);
}
- private final Name parseName(int q1, int q2, int ch, int lastQuadBytes) throws IOException {
+ private final String parseName(int q1, int q2, int ch, int lastQuadBytes) throws IOException {
_quadBuffer[0] = q1;
return parseEscapedName(_quadBuffer, 1, q2, ch, lastQuadBytes);
}
+ private final String parseName(int q1, int q2, int q3, int ch, int lastQuadBytes) throws IOException {
+ _quadBuffer[0] = q1;
+ _quadBuffer[1] = q2;
+ return parseEscapedName(_quadBuffer, 2, q3, ch, lastQuadBytes);
+ }
+
/**
* Slower parsing method which is generally branched to when
* an escape sequence is detected (or alternatively for long
- * names, or ones crossing input buffer boundary). In any case,
- * needs to be able to handle more exceptional cases, gets
- * slower, and hance is offlined to a separate method.
+ * names, one crossing input buffer boundary).
+ * Needs to be able to handle more exceptional cases, gets slower,
+ * and hance is offlined to a separate method.
*/
- protected final Name parseEscapedName(int[] quads, int qlen, int currQuad, int ch,
+ protected final String parseEscapedName(int[] quads, int qlen, int currQuad, int ch,
int currQuadBytes) throws IOException
{
/* 25-Nov-2008, tatu: This may seem weird, but here we do not want to worry about
@@ -1890,13 +1971,14 @@
}
ch = _inputBuffer[_inputPtr++] & 0xFF;
}
+
if (currQuadBytes > 0) {
if (qlen >= quads.length) {
_quadBuffer = quads = growArrayBy(quads, quads.length);
}
quads[qlen++] = pad(currQuad, currQuadBytes);
}
- Name name = _symbols.findName(quads, qlen);
+ String name = _symbols.findName(quads, qlen);
if (name == null) {
name = addName(quads, qlen, currQuadBytes);
}
@@ -1909,7 +1991,7 @@
* In standard mode will just throw an expection; but
* in non-standard modes may be able to parse name.
*/
- protected Name _handleOddName(int ch) throws IOException
+ protected String _handleOddName(int ch) throws IOException
{
// [JACKSON-173]: allow single quotes
if (ch == '\'' && isEnabled(Feature.ALLOW_SINGLE_QUOTES)) {
@@ -1970,7 +2052,7 @@
}
quads[qlen++] = currQuad;
}
- Name name = _symbols.findName(quads, qlen);
+ String name = _symbols.findName(quads, qlen);
if (name == null) {
name = addName(quads, qlen, currQuadBytes);
}
@@ -1982,7 +2064,7 @@
* for valid JSON -- more alternatives, more code, generally
* bit slower execution.
*/
- protected Name _parseAposName() throws IOException
+ protected String _parseAposName() throws IOException
{
if (_inputPtr >= _inputEnd) {
if (!loadMore()) {
@@ -1991,7 +2073,7 @@
}
int ch = _inputBuffer[_inputPtr++] & 0xFF;
if (ch == '\'') { // special case, ''
- return BytesToNameCanonicalizer.getEmptyName();
+ return "";
}
int[] quads = _quadBuffer;
int qlen = 0;
@@ -2080,7 +2162,7 @@
}
quads[qlen++] = pad(currQuad, currQuadBytes);
}
- Name name = _symbols.findName(quads, qlen);
+ String name = _symbols.findName(quads, qlen);
if (name == null) {
name = addName(quads, qlen, currQuadBytes);
}
@@ -2093,12 +2175,11 @@
/**********************************************************
*/
- private final Name findName(int q1, int lastQuadBytes)
- throws JsonParseException
+ private final String findName(int q1, int lastQuadBytes) throws JsonParseException
{
q1 = pad(q1, lastQuadBytes);
// Usually we'll find it from the canonical symbol table already
- Name name = _symbols.findName(q1);
+ String name = _symbols.findName(q1);
if (name != null) {
return name;
}
@@ -2107,12 +2188,11 @@
return addName(_quadBuffer, 1, lastQuadBytes);
}
- private final Name findName(int q1, int q2, int lastQuadBytes)
- throws JsonParseException
+ private final String findName(int q1, int q2, int lastQuadBytes) throws JsonParseException
{
q2 = pad(q2, lastQuadBytes);
// Usually we'll find it from the canonical symbol table already
- Name name = _symbols.findName(q1, q2);
+ String name = _symbols.findName(q1, q2);
if (name != null) {
return name;
}
@@ -2122,14 +2202,27 @@
return addName(_quadBuffer, 2, lastQuadBytes);
}
- private final Name findName(int[] quads, int qlen, int lastQuad, int lastQuadBytes)
- throws JsonParseException
+ private final String findName(int q1, int q2, int q3, int lastQuadBytes) throws JsonParseException
+ {
+ q3 = pad(q3, lastQuadBytes);
+ String name = _symbols.findName(q1, q2, q3);
+ if (name != null) {
+ return name;
+ }
+ int[] quads = _quadBuffer;
+ quads[0] = q1;
+ quads[1] = q2;
+ quads[2] = pad(q3, lastQuadBytes);
+ return addName(quads, 3, lastQuadBytes);
+ }
+
+ private final String findName(int[] quads, int qlen, int lastQuad, int lastQuadBytes) throws JsonParseException
{
if (qlen >= quads.length) {
_quadBuffer = quads = growArrayBy(quads, quads.length);
}
quads[qlen++] = pad(lastQuad, lastQuadBytes);
- Name name = _symbols.findName(quads, qlen);
+ String name = _symbols.findName(quads, qlen);
if (name == null) {
return addName(quads, qlen, lastQuadBytes);
}
@@ -2142,8 +2235,7 @@
* multi-byte chars (if any), and then construct Name instance
* and add it to the symbol table.
*/
- private final Name addName(int[] quads, int qlen, int lastQuadBytes)
- throws JsonParseException
+ private final String addName(int[] quads, int qlen, int lastQuadBytes) throws JsonParseException
{
/* Ok: must decode UTF-8 chars. No other validation is
* needed, since unescaping has been done earlier as necessary
@@ -2289,6 +2381,40 @@
_finishString2(outBuf, outPtr);
}
+ /**
+ * @since 2.6
+ */
+ protected String _finishAndReturnString() throws IOException
+ {
+ // First, single tight loop for ASCII content, not split across input buffer boundary:
+ int ptr = _inputPtr;
+ if (ptr >= _inputEnd) {
+ loadMoreGuaranteed();
+ ptr = _inputPtr;
+ }
+ int outPtr = 0;
+ char[] outBuf = _textBuffer.emptyAndGetCurrentSegment();
+ final int[] codes = _icUTF8;
+
+ final int max = Math.min(_inputEnd, (ptr + outBuf.length));
+ final byte[] inputBuffer = _inputBuffer;
+ while (ptr < max) {
+ int c = (int) inputBuffer[ptr] & 0xFF;
+ if (codes[c] != 0) {
+ if (c == INT_QUOTE) {
+ _inputPtr = ptr+1;
+ return _textBuffer.setCurrentAndReturn(outPtr);
+ }
+ break;
+ }
+ ++ptr;
+ outBuf[outPtr++] = (char) c;
+ }
+ _inputPtr = ptr;
+ _finishString2(outBuf, outPtr);
+ return _textBuffer.contentsAsString();
+ }
+
private final void _finishString2(char[] outBuf, int outPtr)
throws IOException
{
@@ -2857,7 +2983,7 @@
}
return _skipColon2(false);
}
-
+
private final int _skipColon2(boolean gotColon) throws IOException
{
while (_inputPtr < _inputEnd || loadMore()) {
diff --git a/src/main/java/com/fasterxml/jackson/core/json/WriterBasedJsonGenerator.java b/src/main/java/com/fasterxml/jackson/core/json/WriterBasedJsonGenerator.java
index 0e8b6c5..4e91969 100644
--- a/src/main/java/com/fasterxml/jackson/core/json/WriterBasedJsonGenerator.java
+++ b/src/main/java/com/fasterxml/jackson/core/json/WriterBasedJsonGenerator.java
@@ -87,7 +87,14 @@
public Object getOutputTarget() {
return _writer;
}
-
+
+ @Override
+ public int getOutputBuffered() {
+ // Assuming tail and head are kept but... trust and verify:
+ int len = _outputTail - _outputHead;
+ return Math.max(0, len);
+ }
+
/*
/**********************************************************
/* Overridden methods
@@ -319,7 +326,7 @@
@Override
public void writeString(String text) throws IOException
{
- _verifyValueWrite("write text value");
+ _verifyValueWrite(WRITE_STRING);
if (text == null) {
_writeNull();
return;
@@ -339,7 +346,7 @@
@Override
public void writeString(char[] text, int offset, int len) throws IOException
{
- _verifyValueWrite("write text value");
+ _verifyValueWrite(WRITE_STRING);
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
@@ -355,7 +362,7 @@
@Override
public void writeString(SerializableString sstr) throws IOException
{
- _verifyValueWrite("write text value");
+ _verifyValueWrite(WRITE_STRING);
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
@@ -507,7 +514,7 @@
public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int len)
throws IOException, JsonGenerationException
{
- _verifyValueWrite("write binary value");
+ _verifyValueWrite(WRITE_BINARY);
// Starting quotes
if (_outputTail >= _outputEnd) {
_flushBuffer();
@@ -526,7 +533,7 @@
InputStream data, int dataLength)
throws IOException, JsonGenerationException
{
- _verifyValueWrite("write binary value");
+ _verifyValueWrite(WRITE_BINARY);
// Starting quotes
if (_outputTail >= _outputEnd) {
_flushBuffer();
@@ -564,7 +571,7 @@
@Override
public void writeNumber(short s) throws IOException
{
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
if (_cfgNumbersAsStrings) {
_writeQuotedShort(s);
return;
@@ -588,7 +595,7 @@
@Override
public void writeNumber(int i) throws IOException
{
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
if (_cfgNumbersAsStrings) {
_writeQuotedInt(i);
return;
@@ -612,7 +619,7 @@
@Override
public void writeNumber(long l) throws IOException
{
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
if (_cfgNumbersAsStrings) {
_writeQuotedLong(l);
return;
@@ -638,7 +645,7 @@
@Override
public void writeNumber(BigInteger value) throws IOException
{
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
if (value == null) {
_writeNull();
} else if (_cfgNumbersAsStrings) {
@@ -659,7 +666,7 @@
return;
}
// What is the max length for doubles? 40 chars?
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
writeRaw(String.valueOf(d));
}
@@ -673,7 +680,7 @@
return;
}
// What is the max length for floats?
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
writeRaw(String.valueOf(f));
}
@@ -681,7 +688,7 @@
public void writeNumber(BigDecimal value) throws IOException
{
// Don't really know max length for big decimal, no point checking
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
if (value == null) {
_writeNull();
} else if (_cfgNumbersAsStrings) {
@@ -697,7 +704,7 @@
@Override
public void writeNumber(String encodedValue) throws IOException
{
- _verifyValueWrite("write number");
+ _verifyValueWrite(WRITE_NUMBER);
if (_cfgNumbersAsStrings) {
_writeQuotedRaw(encodedValue);
} else {
@@ -721,7 +728,7 @@
@Override
public void writeBoolean(boolean state) throws IOException
{
- _verifyValueWrite("write boolean value");
+ _verifyValueWrite(WRITE_BOOLEAN);
if ((_outputTail + 5) >= _outputEnd) {
_flushBuffer();
}
@@ -744,7 +751,7 @@
@Override
public void writeNull() throws IOException {
- _verifyValueWrite("write null value");
+ _verifyValueWrite(WRITE_NULL);
_writeNull();
}
@@ -840,8 +847,7 @@
}
@Override
- public void close()
- throws IOException
+ public void close() throws IOException
{
super.close();
@@ -863,6 +869,8 @@
}
}
_flushBuffer();
+ _outputHead = 0;
+ _outputTail = 0;
/* 25-Nov-2008, tatus: As per [JACKSON-16] we are not to call close()
* on the underlying Reader, unless we "own" it, or auto-closing
diff --git a/src/main/java/com/fasterxml/jackson/core/sym/ByteQuadsCanonicalizer.java b/src/main/java/com/fasterxml/jackson/core/sym/ByteQuadsCanonicalizer.java
new file mode 100644
index 0000000..d9e6c4f
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/core/sym/ByteQuadsCanonicalizer.java
@@ -0,0 +1,1272 @@
+package com.fasterxml.jackson.core.sym;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.util.InternCache;
+
+/**
+ * Replacement for <code>BytesToNameCanonicalizer</code> which aims at more localized
+ * memory access due to flattening of name quad data.
+ * Performance improvement modest for simple JSON document data binding (maybe 3%),
+ * but should help more for larger symbol tables, or for binary formats like Smile.
+ *
+ * @since 2.6
+ */
+public final class ByteQuadsCanonicalizer
+{
+ /**
+ * Initial size of the primary hash area. Each entry consumes 4 ints (16 bytes),
+ * and secondary area is same as primary; so default size will use 2kB of memory_tertiaryStart
+ * (plus 64x4 or 64x8 (256/512 bytes) for references to Strings, and Strings
+ * themselves).
+ */
+ private static final int DEFAULT_T_SIZE = 64;
+// private static final int DEFAULT_T_SIZE = 256;
+
+ /**
+ * Let's not expand symbol tables past some maximum size;
+ * this should protected against OOMEs caused by large documents
+ * with unique (~= random) names.
+ * Size is in
+ */
+ private static final int MAX_T_SIZE = 0x10000; // 64k entries == 2M mem hash area
+
+ /**
+ * No point in trying to construct tiny tables, just need to resize soon.
+ */
+ final static int MIN_HASH_SIZE = 16;
+
+ /**
+ * Let's only share reasonably sized symbol tables. Max size set to 3/4 of 8k;
+ * this corresponds to 256k main hash index. This should allow for enough distinct
+ * names for almost any case, while preventing ballooning for cases where names
+ * are unique (or close thereof).
+ */
+ final static int MAX_ENTRIES_FOR_REUSE = 6000;
+
+ /*
+ /**********************************************************
+ /* Linkage, needed for merging symbol tables
+ /**********************************************************
+ */
+
+ /**
+ * Reference to the root symbol table, for child tables, so
+ * that they can merge table information back as necessary.
+ */
+ final protected ByteQuadsCanonicalizer _parent;
+
+ /**
+ * Member that is only used by the root table instance: root
+ * passes immutable state into child instances, and children
+ * may return new state if they add entries to the table.
+ * Child tables do NOT use the reference.
+ */
+ final protected AtomicReference<TableInfo> _tableInfo;
+
+ /**
+ * Seed value we use as the base to make hash codes non-static between
+ * different runs, but still stable for lifetime of a single symbol table
+ * instance.
+ * This is done for security reasons, to avoid potential DoS attack via
+ * hash collisions.
+ */
+ final private int _seed;
+
+ /*
+ /**********************************************************
+ /* Configuration
+ /**********************************************************
+ */
+
+ /**
+ * Whether canonical symbol Strings are to be intern()ed before added
+ * to the table or not.
+ *<p>
+ * NOTE: non-final to allow disabling intern()ing in case of excessive
+ * collisions.
+ */
+ protected boolean _intern;
+
+ /**
+ * Flag that indicates whether we should throw an exception if enough
+ * hash collisions are detected (true); or just worked around (false).
+ *
+ * @since 2.4
+ */
+ protected final boolean _failOnDoS;
+
+ /*
+ /**********************************************************
+ /* First, main hash area info
+ /**********************************************************
+ */
+
+ /**
+ * Primary hash information area: consists of <code>2 * _hashSize</code>
+ * entries of 16 bytes (4 ints), arranged in a cascading lookup
+ * structure (details of which may be tweaked depending on expected rates
+ * of collisions).
+ */
+ protected int[] _hashArea;
+
+ /**
+ * Number of slots for primary entries within {@link #_hashArea}; which is
+ * at most <code>1/8</code> of actual size of the underlying array (4-int slots,
+ * primary covers only half of the area; plus, additional area for longer
+ * symbols after hash area).
+ */
+ protected int _hashSize;
+
+ /**
+ * Offset within {@link #_hashArea} where secondary entries start
+ */
+ protected int _secondaryStart;
+
+ /**
+ * Offset within {@link #_hashArea} where tertiary entries start
+ */
+ protected int _tertiaryStart;
+
+ /**
+ * Constant that determines size of buckets for tertiary entries:
+ * <code>1 << _tertiaryShift</code> is the size, and shift value
+ * is also used for translating from primary offset into
+ * tertiary bucket (shift right by <code>4 + _tertiaryShift</code>).
+ *<p>
+ * Default value is 2, for buckets of 4 slots; grows bigger with
+ * bigger table sizes.
+ */
+ protected int _tertiaryShift;
+
+ /**
+ * Total number of Strings in the symbol table; only used for child tables.
+ */
+ protected int _count;
+
+ /**
+ * Array that contains <code>String</code> instances matching
+ * entries in {@link #_hashArea}.
+ * Contains nulls for unused entries. Note that this size is twice
+ * that of {@link #_hashArea}
+ */
+ protected String[] _names;
+
+ /*
+ /**********************************************************
+ /* Then information on collisions etc
+ /**********************************************************
+ */
+
+ /**
+ * Pointer to the offset within spill-over area where there is room
+ * for more spilled over entries (if any).
+ * Spill over area is within fixed-size portion of {@link #_hashArea}.
+ */
+ protected int _spilloverEnd;
+
+ /**
+ * Offset within {@link #_hashArea} that follows main slots and contains
+ * quads for longer names (13 bytes or longers), and points to the
+ * first available int that may be used for appending quads of the next
+ * long name.
+ * Note that long name area follows immediately after the fixed-size
+ * main hash area ({@link #_hashArea}).
+ */
+ protected int _longNameOffset;
+
+ /**
+ * This flag is set if, after adding a new entry, it is deemed
+ * that a rehash is warranted if any more entries are to be added.
+ */
+ private transient boolean _needRehash;
+
+ /*
+ /**********************************************************
+ /* Sharing, versioning
+ /**********************************************************
+ */
+
+ // // // Which of the buffers may be shared (and are copy-on-write)?
+
+ /**
+ * Flag that indicates whether underlying data structures for
+ * the main hash area are shared or not. If they are, then they
+ * need to be handled in copy-on-write way, i.e. if they need
+ * to be modified, a copy needs to be made first; at this point
+ * it will not be shared any more, and can be modified.
+ *<p>
+ * This flag needs to be checked both when adding new main entries,
+ * and when adding new collision list queues (i.e. creating a new
+ * collision list head entry)
+ */
+ private boolean _hashShared;
+
+ /*
+ /**********************************************************
+ /* Life-cycle: constructors
+ /**********************************************************
+ */
+
+ /**
+ * Constructor used for creating per-<code>JsonFactory</code> "root"
+ * symbol tables: ones used for merging and sharing common symbols
+ *
+ * @param sz Initial primary hash area size
+ * @param intern Whether Strings contained should be {@link String#intern}ed
+ * @param seed Random seed valued used to make it more difficult to cause
+ * collisions (used for collision-based DoS attacks).
+ */
+ private ByteQuadsCanonicalizer(int sz, boolean intern, int seed, boolean failOnDoS) {
+ _parent = null;
+ _seed = seed;
+ _intern = intern;
+ _failOnDoS = failOnDoS;
+ // Sanity check: let's now allow hash sizes below certain minimum value
+ if (sz < MIN_HASH_SIZE) {
+ sz = MIN_HASH_SIZE;
+ } else {
+ // Also; size must be 2^N; otherwise hash algorithm won't
+ // work... so let's just pad it up, if so
+ if ((sz & (sz - 1)) != 0) { // only true if it's 2^N
+ int curr = MIN_HASH_SIZE;
+ while (curr < sz) {
+ curr += curr;
+ }
+ sz = curr;
+ }
+ }
+ _tableInfo = new AtomicReference<TableInfo>(TableInfo.createInitial(sz));
+ }
+
+ /**
+ * Constructor used when creating a child instance
+ */
+ private ByteQuadsCanonicalizer(ByteQuadsCanonicalizer parent, boolean intern,
+ int seed, boolean failOnDoS, TableInfo state)
+ {
+ _parent = parent;
+ _seed = seed;
+ _intern = intern;
+ _failOnDoS = failOnDoS;
+ _tableInfo = null; // not used by child tables
+
+ // Then copy shared state
+ _count = state.count;
+ _hashSize = state.size;
+ _secondaryStart = _hashSize << 2; // right after primary area
+ _tertiaryStart = _secondaryStart + (_secondaryStart >> 1); // right after secondary
+ _tertiaryShift = state.tertiaryShift;
+
+ _hashArea = state.mainHash;
+ _names = state.names;
+
+ _spilloverEnd = state.spilloverEnd;
+ _longNameOffset = state.longNameOffset;
+
+ // and then set other state to reflect sharing status
+ _needRehash = false;
+ _hashShared = true;
+ }
+
+ /*
+ /**********************************************************
+ /* Life-cycle: factory methods, merging
+ /**********************************************************
+ */
+
+ /**
+ * Factory method to call to create a symbol table instance with a
+ * randomized seed value.
+ */
+ public static ByteQuadsCanonicalizer createRoot() {
+ /* [Issue-21]: Need to use a variable seed, to thwart hash-collision
+ * based attacks.
+ */
+ long now = System.currentTimeMillis();
+ // ensure it's not 0; and might as well require to be odd so:
+ int seed = (((int) now) + ((int) (now >>> 32))) | 1;
+ return createRoot(seed);
+ }
+
+ /**
+ * Factory method that should only be called from unit tests, where seed
+ * value should remain the same.
+ */
+ protected static ByteQuadsCanonicalizer createRoot(int seed) {
+ return new ByteQuadsCanonicalizer(DEFAULT_T_SIZE, true, seed, true);
+ }
+
+ /**
+ * Factory method used to create actual symbol table instance to
+ * use for parsing.
+ */
+ public ByteQuadsCanonicalizer makeChild(int flags) {
+ return new ByteQuadsCanonicalizer(this,
+ JsonFactory.Feature.INTERN_FIELD_NAMES.enabledIn(flags),
+ _seed,
+ JsonFactory.Feature.FAIL_ON_SYMBOL_HASH_OVERFLOW.enabledIn(flags),
+ _tableInfo.get());
+ }
+
+ /**
+ * Method called by the using code to indicate it is done
+ * with this instance. This lets instance merge accumulated
+ * changes into parent (if need be), safely and efficiently,
+ * and without calling code having to know about parent
+ * information
+ */
+ public void release()
+ {
+ // we will try to merge if child table has new entries
+ if (_parent != null && maybeDirty()) {
+ _parent.mergeChild(new TableInfo(this));
+ /* Let's also mark this instance as dirty, so that just in
+ * case release was too early, there's no corruption of possibly shared data.
+ */
+ _hashShared = true;
+ }
+ }
+
+ private void mergeChild(TableInfo childState)
+ {
+ final int childCount = childState.count;
+ TableInfo currState = _tableInfo.get();
+
+ // Should usually grow; but occasionally could also shrink if (but only if)
+ // collision list overflow ends up clearing some collision lists.
+ if (childCount == currState.count) {
+ return;
+ }
+
+ // One caveat: let's try to avoid problems with degenerate cases of documents with
+ // generated "random" names: for these, symbol tables would bloat indefinitely.
+ // One way to do this is to just purge tables if they grow
+ // too large, and that's what we'll do here.
+ if (childCount > MAX_ENTRIES_FOR_REUSE) {
+ // At any rate, need to clean up the tables
+ childState = TableInfo.createInitial(DEFAULT_T_SIZE);
+ }
+ _tableInfo.compareAndSet(currState, childState);
+ }
+
+ /*
+ /**********************************************************
+ /* API, accessors
+ /**********************************************************
+ */
+
+ public int size()
+ {
+ if (_tableInfo != null) { // root table
+ return _tableInfo.get().count;
+ }
+ // nope, child table
+ return _count;
+ }
+
+ /**
+ * Returns number of primary slots table has currently
+ */
+ public int bucketCount() { return _hashSize; }
+
+ /**
+ * Method called to check to quickly see if a child symbol table
+ * may have gotten additional entries. Used for checking to see
+ * if a child table should be merged into shared table.
+ */
+ public boolean maybeDirty() { return !_hashShared; }
+
+ public int hashSeed() { return _seed; }
+
+ /**
+ * Method mostly needed by unit tests; calculates number of
+ * entries that are in the primary slot set. These are
+ * "perfect" entries, accessible with a single lookup
+ */
+ public int primaryCount()
+ {
+ int count = 0;
+ for (int offset = 3, end = _secondaryStart; offset < end; offset += 4) {
+ if (_hashArea[offset] != 0) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Method mostly needed by unit tests; calculates number of entries
+ * in secondary buckets
+ */
+ public int secondaryCount() {
+ int count = 0;
+ int offset = _secondaryStart + 3;
+ for (int end = _tertiaryStart; offset < end; offset += 4) {
+ if (_hashArea[offset] != 0) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Method mostly needed by unit tests; calculates number of entries
+ * in tertiary buckets
+ */
+ public int tertiaryCount() {
+ int count = 0;
+ int offset = _tertiaryStart + 3; // to 1.5x, starting point of tertiary
+ for (int end = offset + _hashSize; offset < end; offset += 4) {
+ if (_hashArea[offset] != 0) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Method mostly needed by unit tests; calculates number of entries
+ * in shared spillover area
+ */
+ public int spilloverCount() {
+ // difference between spillover end, start, divided by 4 (four ints per slot)
+ return (_spilloverEnd - _spilloverStart()) >> 2;
+ }
+
+ public int totalCount()
+ {
+ int count = 0;
+ for (int offset = 3, end = (_hashSize << 3); offset < end; offset += 4) {
+ if (_hashArea[offset] != 0) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public String toString() {
+ int pri = primaryCount();
+ int sec = secondaryCount();
+ int tert = tertiaryCount();
+ int spill = spilloverCount();
+ int total = totalCount();
+ return String.format("[%s: size=%d, hashSize=%d, %d/%d/%d/%d pri/sec/ter/spill (=%s), total:%d]",
+ getClass().getName(), _count, _hashSize,
+ pri, sec, tert, spill, total, (pri+sec+tert+spill), total);
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, accessing symbols
+ /**********************************************************
+ */
+
+ public String findName(int q1)
+ {
+ int offset = _calcOffset(calcHash(q1));
+ // first: primary match?
+ final int[] hashArea = _hashArea;
+
+ int len = hashArea[offset+3];
+
+ if (len == 1) {
+ if (hashArea[offset] == q1) {
+ return _names[offset >> 2];
+ }
+ } else if (len == 0) { // empty slot; unlikely but avoid further lookups if so
+ return null;
+ }
+ // secondary? single slot shared by N/2 primaries
+ int offset2 = _secondaryStart + ((offset >> 3) << 2);
+
+ len = hashArea[offset2+3];
+
+ if (len == 1) {
+ if (hashArea[offset2] == q1) {
+ return _names[offset2 >> 2];
+ }
+ } else if (len == 0) { // empty slot; unlikely but avoid further lookups if so
+ return null;
+ }
+
+ // tertiary lookup & spillovers best to offline
+ return _findSecondary(offset, q1);
+ }
+
+ public String findName(int q1, int q2)
+ {
+ int offset = _calcOffset(calcHash(q1, q2));
+
+ final int[] hashArea = _hashArea;
+
+ int len = hashArea[offset+3];
+
+ if (len == 2) {
+ if ((q1 == hashArea[offset]) && (q2 == hashArea[offset+1])) {
+ return _names[offset >> 2];
+ }
+ } else if (len == 0) { // empty slot; unlikely but avoid further lookups if so
+ return null;
+ }
+ // secondary?
+ int offset2 = _secondaryStart + ((offset >> 3) << 2);
+
+ len = hashArea[offset2+3];
+
+ if (len == 2) {
+ if ((q1 == hashArea[offset2]) && (q2 == hashArea[offset2+1])) {
+ return _names[offset2 >> 2];
+ }
+ } else if (len == 0) { // empty slot? Short-circuit if no more spillovers
+ return null;
+ }
+ return _findSecondary(offset, q1, q2);
+ }
+
+ public String findName(int q1, int q2, int q3)
+ {
+ int offset = _calcOffset(calcHash(q1, q2, q3));
+ final int[] hashArea = _hashArea;
+ int len = hashArea[offset+3];
+
+ if (len == 3) {
+ if ((q1 == hashArea[offset]) && (hashArea[offset+1] == q2) && (hashArea[offset+2] == q3)) {
+ return _names[offset >> 2];
+ }
+ } else if (len == 0) { // empty slot; unlikely but avoid further lookups if so
+ return null;
+ }
+ // secondary?
+ int offset2 = _secondaryStart + ((offset >> 3) << 2);
+
+ len = hashArea[offset2+3];
+
+ if (len == 3) {
+ if ((q1 == hashArea[offset2]) && (hashArea[offset2+1] == q2) && (hashArea[offset2+2] == q3)) {
+ return _names[offset2 >> 2];
+ }
+ } else if (len == 0) { // empty slot? Short-circuit if no more spillovers
+ return null;
+ }
+ return _findSecondary(offset, q1, q2, q3);
+ }
+
+ public String findName(int[] q, int qlen)
+ {
+ /* This version differs significantly, because longer names do not fit within cell.
+ * Rather, they contain hash in main slot, and offset+length to extension area
+ * that contains actual quads.
+ */
+ if (qlen < 4) { // another sanity check
+ if (qlen == 3) {
+ return findName(q[0], q[1], q[2]);
+ }
+ if (qlen == 2) {
+ return findName(q[0], q[1]);
+ }
+ return findName(q[0]);
+ }
+ final int hash = calcHash(q, qlen);
+ int offset = _calcOffset(hash);
+
+ final int[] hashArea = _hashArea;
+
+ final int len = hashArea[offset+3];
+
+ if ((hash == hashArea[offset]) && (len == qlen)) {
+ // probable but not guaranteed: verify
+ if (_verifyLongName(q, qlen, hashArea[offset+1])) {
+ return _names[offset >> 2];
+ }
+ }
+ if (len == 0) { // empty slot; unlikely but avoid further lookups if so
+ return null;
+ }
+ // secondary?
+ int offset2 = _secondaryStart + ((offset >> 3) << 2);
+
+ final int len2 = hashArea[offset2+3];
+ if ((hash == hashArea[offset2]) && (len2 == qlen)) {
+ if (_verifyLongName(q, qlen, hashArea[offset2+1])) {
+ return _names[offset2 >> 2];
+ }
+ }
+ if (len == 0) { // empty slot? Short-circuit if no more spillovers
+ return null;
+ }
+ return _findSecondary(offset, hash, q, qlen);
+ }
+
+ private final int _calcOffset(int hash)
+ {
+ // NOTE: simple for initial impl, but we may want to interleave it a bit
+ // in near future
+ // So: first, hash into primary hash index
+ int ix = hash & (_hashSize-1);
+ // keeping in mind we have 4 ints per entry
+ return (ix << 2);
+ }
+
+ /*
+ /**********************************************************
+ /* Access from spill-over areas
+ /**********************************************************
+ */
+
+ private String _findSecondary(int origOffset, int q1)
+ {
+ // tertiary area division is dynamic. First; its size is N/4 compared to
+ // primary hash size; and offsets are for 4 int slots. So to get to logical
+ // index would shift by 4. But! Tertiary area is further split into buckets,
+ // determined by shift value. And finally, from bucket back into physical offsets
+ int offset = _tertiaryStart + ((origOffset >> (_tertiaryShift + 2)) << _tertiaryShift);
+ final int[] hashArea = _hashArea;
+ final int bucketSize = (1 << _tertiaryShift);
+ for (int end = offset + bucketSize; offset < end; offset += 4) {
+ int len = hashArea[offset+3];
+ if ((q1 == hashArea[offset]) && (1 == len)) {
+ return _names[offset >> 2];
+ }
+ if (len == 0) {
+ return null;
+ }
+ }
+ // but if tertiary full, check out spill-over area as last resort
+ // shared spillover starts at 7/8 of the main hash area
+ // (which is sized at 2 * _hashSize), so:
+ for (offset = _spilloverStart(); offset < _spilloverEnd; offset += 4) {
+ if ((q1 == hashArea[offset]) && (1 == hashArea[offset+3])) {
+ return _names[offset >> 2];
+ }
+ }
+ return null;
+ }
+
+ private String _findSecondary(int origOffset, int q1, int q2)
+ {
+ int offset = _tertiaryStart + ((origOffset >> (_tertiaryShift + 2)) << _tertiaryShift);
+ final int[] hashArea = _hashArea;
+
+ final int bucketSize = (1 << _tertiaryShift);
+ for (int end = offset + bucketSize; offset < end; offset += 4) {
+ int len = hashArea[offset+3];
+ if ((q1 == hashArea[offset]) && (q2 == hashArea[offset+1]) && (2 == len)) {
+ return _names[offset >> 2];
+ }
+ if (len == 0) {
+ return null;
+ }
+ }
+ for (offset = _spilloverStart(); offset < _spilloverEnd; offset += 4) {
+ if ((q1 == hashArea[offset]) && (q2 == hashArea[offset+1]) && (2 == hashArea[offset+3])) {
+ return _names[offset >> 2];
+ }
+ }
+ return null;
+ }
+
+ private String _findSecondary(int origOffset, int q1, int q2, int q3)
+ {
+ int offset = _tertiaryStart + ((origOffset >> (_tertiaryShift + 2)) << _tertiaryShift);
+ final int[] hashArea = _hashArea;
+
+ final int bucketSize = (1 << _tertiaryShift);
+ for (int end = offset + bucketSize; offset < end; offset += 4) {
+ int len = hashArea[offset+3];
+ if ((q1 == hashArea[offset]) && (q2 == hashArea[offset+1]) && (q3 == hashArea[offset+2]) && (3 == len)) {
+ return _names[offset >> 2];
+ }
+ if (len == 0) {
+ return null;
+ }
+ }
+ for (offset = _spilloverStart(); offset < _spilloverEnd; offset += 4) {
+ if ((q1 == hashArea[offset]) && (q2 == hashArea[offset+1]) && (q3 == hashArea[offset+2])
+ && (3 == hashArea[offset+3])) {
+ return _names[offset >> 2];
+ }
+ }
+ return null;
+ }
+
+ private String _findSecondary(int origOffset, int hash, int[] q, int qlen)
+ {
+ int offset = _tertiaryStart + ((origOffset >> (_tertiaryShift + 2)) << _tertiaryShift);
+ final int[] hashArea = _hashArea;
+
+ final int bucketSize = (1 << _tertiaryShift);
+ for (int end = offset + bucketSize; offset < end; offset += 4) {
+ int len = hashArea[offset+3];
+ if ((hash == hashArea[offset]) && (qlen == len)) {
+ if (_verifyLongName(q, qlen, hashArea[offset+1])) {
+ return _names[offset >> 2];
+ }
+ }
+ if (len == 0) {
+ return null;
+ }
+ }
+ for (offset = _spilloverStart(); offset < _spilloverEnd; offset += 4) {
+ if ((hash == hashArea[offset]) && (qlen == hashArea[offset+3])) {
+ if (_verifyLongName(q, qlen, hashArea[offset+1])) {
+ return _names[offset >> 2];
+ }
+ }
+ }
+ return null;
+ }
+
+ private boolean _verifyLongName(int[] q, int qlen, int spillOffset)
+ {
+ final int[] hashArea = _hashArea;
+ // spillOffset assumed to be physical index right into quad string
+ int ix = 0;
+
+ switch (qlen) {
+ default:
+ return _verifyLongName2(q, qlen, spillOffset);
+ case 8:
+ if (q[ix++] != hashArea[spillOffset++]) return false;
+ case 7:
+ if (q[ix++] != hashArea[spillOffset++]) return false;
+ case 6:
+ if (q[ix++] != hashArea[spillOffset++]) return false;
+ case 5:
+ if (q[ix++] != hashArea[spillOffset++]) return false;
+ case 4: // always at least 4
+ if (q[ix++] != hashArea[spillOffset++]) return false;
+ if (q[ix++] != hashArea[spillOffset++]) return false;
+ if (q[ix++] != hashArea[spillOffset++]) return false;
+ if (q[ix++] != hashArea[spillOffset++]) return false;
+ }
+ return true;
+ }
+
+ private boolean _verifyLongName2(int[] q, int qlen, int spillOffset)
+ {
+ int ix = 0;
+ do {
+ if (q[ix++] != _hashArea[spillOffset++]) {
+ return false;
+ }
+ } while (ix < qlen);
+ return true;
+ }
+
+ /*
+ /**********************************************************
+ /* API, mutators
+ /**********************************************************
+ */
+
+ public String addName(String name, int q1) {
+ _verifySharing();
+ if (_intern) {
+ name = InternCache.instance.intern(name);
+ }
+ int offset = _findOffsetForAdd(calcHash(q1));
+ _hashArea[offset] = q1;
+ _hashArea[offset+3] = 1;
+ _names[offset >> 2] = name;
+ ++_count;
+ _verifyNeedForRehash();
+ return name;
+ }
+
+ public String addName(String name, int q1, int q2) {
+ _verifySharing();
+ if (_intern) {
+ name = InternCache.instance.intern(name);
+ }
+ int hash = (q2 == 0) ? calcHash(q1) : calcHash(q1, q2);
+ int offset = _findOffsetForAdd(hash);
+ _hashArea[offset] = q1;
+ _hashArea[offset+1] = q2;
+ _hashArea[offset+3] = 2;
+ _names[offset >> 2] = name;
+ ++_count;
+ _verifyNeedForRehash();
+ return name;
+ }
+
+ public String addName(String name, int q1, int q2, int q3) {
+ _verifySharing();
+ if (_intern) {
+ name = InternCache.instance.intern(name);
+ }
+ int offset = _findOffsetForAdd(calcHash(q1, q2, q3));
+ _hashArea[offset] = q1;
+ _hashArea[offset+1] = q2;
+ _hashArea[offset+2] = q3;
+ _hashArea[offset+3] = 3;
+ _names[offset >> 2] = name;
+ ++_count;
+ _verifyNeedForRehash();
+ return name;
+ }
+
+ public String addName(String name, int[] q, int qlen)
+ {
+ _verifySharing();
+ if (_intern) {
+ name = InternCache.instance.intern(name);
+ }
+ int offset;
+
+ switch (qlen) {
+ case 1:
+ {
+ offset = _findOffsetForAdd(calcHash(q[0]));
+ _hashArea[offset] = q[0];
+ _hashArea[offset+3] = 1;
+ }
+ break;
+ case 2:
+ {
+ offset = _findOffsetForAdd(calcHash(q[0], q[1]));
+ _hashArea[offset] = q[0];
+ _hashArea[offset+1] = q[1];
+ _hashArea[offset+3] = 2;
+ }
+ break;
+ case 3:
+ {
+ offset = _findOffsetForAdd(calcHash(q[0], q[1], q[2]));
+ _hashArea[offset] = q[0];
+ _hashArea[offset+1] = q[1];
+ _hashArea[offset+2] = q[2];
+ _hashArea[offset+3] = 3;
+ }
+ break;
+ default:
+ final int hash = calcHash(q, qlen);
+ offset = _findOffsetForAdd(hash);
+
+ _hashArea[offset] = hash;
+ int longStart = _appendLongName(q, qlen);
+ _hashArea[offset+1] = longStart;
+ _hashArea[offset+3] = qlen;
+ }
+ // plus add the actual String
+ _names[offset >> 2] = name;
+
+ // and finally; see if we really should rehash.
+ ++_count;
+ _verifyNeedForRehash();
+ return name;
+ }
+
+ private void _verifyNeedForRehash() {
+ // Yes if above 80%, or above 50% AND have ~1% spill-overs
+ if (_count > (_hashSize >> 1)) { // over 50%
+ int spillCount = (_spilloverEnd - _spilloverStart()) >> 2;
+ if ((spillCount > (1 + _count >> 7))
+ || (_count > (_hashSize * 0.80))) {
+ _needRehash = true;
+ }
+ }
+ }
+
+ private void _verifySharing()
+ {
+ if (_hashShared) {
+ _hashArea = Arrays.copyOf(_hashArea, _hashArea.length);
+ _names = Arrays.copyOf(_names, _names.length);
+ _hashShared = false;
+ // 09-Sep-2015, tatu: As per [jackson-core#216], also need to ensure
+ // we rehash as needed, as need-rehash flag is not copied from parent
+ _verifyNeedForRehash();
+ }
+ if (_needRehash) {
+ rehash();
+ }
+ }
+
+ /**
+ * Method called to find the location within hash table to add a new symbol in.
+ */
+ private int _findOffsetForAdd(int hash)
+ {
+ // first, check the primary:
+ int offset = _calcOffset(hash);
+ final int[] hashArea = _hashArea;
+ if (hashArea[offset+3] == 0) {
+//System.err.printf(" PRImary slot #%d, hash %X\n", (offset>>2), hash & 0x7F);
+ return offset;
+ }
+ // then secondary
+ int offset2 = _secondaryStart + ((offset >> 3) << 2);
+ if (hashArea[offset2+3] == 0) {
+//System.err.printf(" SECondary slot #%d (start x%X), hash %X\n",(offset >> 3), _secondaryStart, (hash & 0x7F));
+ return offset2;
+ }
+ // if not, tertiary?
+
+ offset2 = _tertiaryStart + ((offset >> (_tertiaryShift + 2)) << _tertiaryShift);
+ final int bucketSize = (1 << _tertiaryShift);
+ for (int end = offset2 + bucketSize; offset2 < end; offset2 += 4) {
+ if (hashArea[offset2+3] == 0) {
+//System.err.printf(" TERtiary slot x%X (from x%X, start x%X), hash %X.\n", offset2, ((offset >> (_tertiaryShift + 2)) << _tertiaryShift), _tertiaryStart, (hash & 0x7F));
+ return offset2;
+ }
+ }
+
+ // and if even tertiary full, append at the end of spill area
+ offset = _spilloverEnd;
+ _spilloverEnd += 4;
+
+//System.err.printf(" SPIll-over at x%X; start x%X; end x%X, hash %X\n", offset, _spilloverStart(), _hashArea.length, (hash & 0x7F));
+
+ // one caveat: in the unlikely event if spill-over filling up,
+ // check if that could be considered a DoS attack; handle appropriately
+ // (NOTE: approximate for now; we could verify details if that becomes necessary)
+ /* 31-Jul-2015, tatu: Note that spillover area does NOT end at end of array,
+ * since "long names" area follows. Instead, need to calculate from hash size.
+ */
+ final int end = (_hashSize << 3);
+ if (_spilloverEnd >= end) {
+ if (_failOnDoS) {
+ _reportTooManyCollisions();
+ }
+ // and if we didn't fail, we'll simply force rehash for next add
+ // (which, in turn, may double up or nuke contents, depending on size etc)
+ _needRehash = true;
+ }
+ return offset;
+ }
+
+ private int _appendLongName(int[] quads, int qlen)
+ {
+ int start = _longNameOffset;
+
+ // note: at this point we must already be shared. But may not have enough space
+ if ((start + qlen) > _hashArea.length) {
+ // try to increment in reasonable chunks; at least space that we need
+ int toAdd = (start + qlen) - _hashArea.length;
+ // but at least 1/8 of regular hash area size or 16kB (whichever smaller)
+ int minAdd = Math.min(4096, _hashSize);
+
+ int newSize = _hashArea.length + Math.max(toAdd, minAdd);
+ _hashArea = Arrays.copyOf(_hashArea, newSize);
+ }
+ System.arraycopy(quads, 0, _hashArea, start, qlen);
+ _longNameOffset += qlen;
+ return start;
+ }
+
+ /*
+ /**********************************************************
+ /* Hash calculation
+ /**********************************************************
+ */
+
+ /* Note on hash calculation: we try to make it more difficult to
+ * generate collisions automatically; part of this is to avoid
+ * simple "multiply-add" algorithm (like JDK String.hashCode()),
+ * and add bit of shifting. And other part is to make this
+ * non-linear, at least for shorter symbols.
+ */
+
+ // JDK uses 31; other fine choices are 33 and 65599, let's use 33
+ // as it seems to give fewest collisions for us
+ // (see [http://www.cse.yorku.ca/~oz/hash.html] for details)
+ private final static int MULT = 33;
+ private final static int MULT2 = 65599;
+ private final static int MULT3 = 31;
+
+ public int calcHash(int q1)
+ {
+ int hash = q1 ^ _seed;
+ /* 29-Mar-2015, tatu: Earlier used 15 + 9 right shifts, which worked ok
+ * except for one specific problem case: numbers. So needed to make sure
+ * that all 4 least-significant bits participate in hash. Couple of ways
+ * to work it out, but this is the simplest, fast and seems to do ok.
+ */
+ hash += (hash >>> 16); // to xor hi- and low- 16-bits
+ hash ^= (hash << 3); // shuffle back a bit
+ hash += (hash >>> 12); // and bit more
+ return hash;
+ }
+
+ public int calcHash(int q1, int q2)
+ {
+ // For two quads, let's change algorithm a bit, to spice
+ // things up (can do bit more processing anyway)
+ int hash = q1;
+
+ hash += (hash >>> 15); // try mixing first and second byte pairs first
+ hash ^= (hash >>> 9); // as well as lowest 2 bytes
+ hash += (q2 * MULT); // then add second quad
+ hash ^= _seed;
+ hash += (hash >>> 16); // and shuffle some more
+ hash ^= (hash >>> 4);
+ hash += (hash << 3);
+
+ return hash;
+ }
+
+ public int calcHash(int q1, int q2, int q3)
+ { // use same algorithm as multi-byte, tested to work well
+ int hash = q1 ^ _seed;
+ hash += (hash >>> 9);
+ hash *= MULT3;
+ hash += q2;
+ hash *= MULT;
+ hash += (hash >>> 15);
+ hash ^= q3;
+ // 26-Mar-2015, tatu: As per two-quad case, a short shift seems to help more here
+ hash += (hash >>> 4);
+
+ hash += (hash >>> 15);
+ hash ^= (hash << 9);
+
+ return hash;
+ }
+
+ public int calcHash(int[] q, int qlen)
+ {
+ if (qlen < 4) {
+ throw new IllegalArgumentException();
+ }
+ /* And then change handling again for "multi-quad" case; mostly
+ * to make calculation of collisions less fun. For example,
+ * add seed bit later in the game, and switch plus/xor around,
+ * use different shift lengths.
+ */
+ int hash = q[0] ^ _seed;
+ hash += (hash >>> 9);
+ hash += q[1];
+ hash += (hash >>> 15);
+ hash *= MULT;
+ hash ^= q[2];
+ hash += (hash >>> 4);
+
+ for (int i = 3; i < qlen; ++i) {
+ int next = q[i];
+ next = next ^ (next >> 21);
+ hash += next;
+ }
+ hash *= MULT2;
+
+ // and finally shuffle some more once done
+ hash += (hash >>> 19);
+ hash ^= (hash << 5);
+ return hash;
+ }
+
+ /*
+ /**********************************************************
+ /* Rehashing
+ /**********************************************************
+ */
+
+ private void rehash()
+ {
+ _needRehash = false;
+ // Note: since we'll make copies, no need to unshare, can just mark as such:
+ _hashShared = false;
+
+ // And then we can first deal with the main hash area. Since we are expanding
+ // linearly (double up), we know there'll be no collisions during this phase.
+ final int[] oldHashArea = _hashArea;
+ final String[] oldNames = _names;
+ final int oldSize = _hashSize;
+ final int oldCount = _count;
+ final int newSize = oldSize + oldSize;
+ final int oldEnd = _spilloverEnd;
+
+ /* 13-Mar-2010, tatu: Let's guard against OOME that could be caused by
+ * large documents with unique (or mostly so) names
+ */
+ if (newSize > MAX_T_SIZE) {
+ nukeSymbols(true);
+ return;
+ }
+ // double up main hash area, but do not expand long-name area:
+ _hashArea = new int[oldHashArea.length + (oldSize<<3)];
+ _hashSize = newSize;
+ _secondaryStart = (newSize << 2); // 4 ints per entry
+ _tertiaryStart = _secondaryStart + (_secondaryStart >> 1); // right after secondary
+ _tertiaryShift = _calcTertiaryShift(newSize);
+
+ // and simply double up name array
+ _names = new String[oldNames.length << 1];
+ nukeSymbols(false);
+
+ // Plus we can scan only through the primary hash area, looking for non-empty
+ // slots, without worrying about ordering. This should never reduce priority
+ // of existing entries: primaries remain primaries; however, due to increased
+ // space, secondaries may become primaries etc
+
+ int copyCount = 0;
+ int[] q = new int[16];
+ for (int offset = 0, end = oldEnd; offset < end; offset += 4) {
+ int len = oldHashArea[offset+3];
+ if (len == 0) { // empty slot, skip
+ continue;
+ }
+ ++copyCount;
+ String name = oldNames[offset>>2];
+ switch (len) {
+ case 1:
+ q[0] = oldHashArea[offset];
+ addName(name, q, 1);
+ break;
+ case 2:
+ q[0] = oldHashArea[offset];
+ q[1] = oldHashArea[offset+1];
+ addName(name, q, 2);
+ break;
+ case 3:
+ q[0] = oldHashArea[offset];
+ q[1] = oldHashArea[offset+1];
+ q[2] = oldHashArea[offset+2];
+ addName(name, q, 3);
+ break;
+ default:
+ if (len > q.length) {
+ q = new int[len];
+ }
+ // #0 is hash, #1 offset
+ int qoff = oldHashArea[offset+1];
+ System.arraycopy(oldHashArea, qoff, q, 0, len);
+ addName(name, q, len);
+ break;
+ }
+ }
+
+ // Sanity checks: since corruption difficult to detect, assert explicitly
+ // with production code
+ if (copyCount != oldCount) {
+ throw new IllegalStateException("Failed rehash(): old count="+oldCount+", copyCount="+copyCount);
+ }
+ }
+
+ /**
+ * Helper method called to empty all shared symbols, but to leave
+ * arrays allocated
+ */
+ private void nukeSymbols(boolean fill) {
+ _count = 0;
+ // reset spill-over to empty (starting at 7/8 of hash area)
+ _spilloverEnd = _spilloverStart();
+ // and long name area to empty, starting immediately after hash area
+ _longNameOffset = _hashSize << 3;
+ if (fill) {
+ Arrays.fill(_hashArea, 0);
+ Arrays.fill(_names, null);
+ }
+ }
+
+ /*
+ /**********************************************************
+ /* Helper methods
+ /**********************************************************
+ */
+
+ /**
+ * Helper method that calculates start of the spillover area
+ */
+ private final int _spilloverStart() {
+ // we'll need slot at 1.75x of hashSize, but with 4-ints per slot.
+ // So basically multiply by 7
+ int offset = _hashSize;
+ return (offset << 3) - offset;
+ }
+
+ protected void _reportTooManyCollisions()
+ {
+ // First: do not fuzz about small symbol tables; may get balanced by doubling up
+ if (_hashSize <= 1024) { // would have spill-over area of 128 entries
+ return;
+ }
+ throw new IllegalStateException("Spill-over slots in symbol table with "+_count
+ +" entries, hash area of "+_hashSize+" slots is now full (all "
+ +(_hashSize >> 3)+" slots -- suspect a DoS attack based on hash collisions."
+ +" You can disable the check via `JsonFactory.Feature.FAIL_ON_SYMBOL_HASH_OVERFLOW`");
+ }
+
+ static int _calcTertiaryShift(int primarySlots)
+ {
+ // first: we only get 1/4 of slots of primary, to divide
+ int tertSlots = (primarySlots) >> 2;
+
+ // default is for buckets of 4 slots (each 4 ints, i.e. 1 << 4)
+ if (tertSlots < 64) {
+ return 4;
+ }
+ if (tertSlots <= 256) { // buckets of 8 slots (up to 256 == 32 x 8)
+ return 5;
+ }
+ if (tertSlots <= 1024) { // buckets of 16 slots (up to 1024 == 64 x 16)
+ return 6;
+ }
+ // and biggest buckets have 32 slots
+ return 7;
+ }
+
+ /*
+ /**********************************************************
+ /* Helper classes
+ /**********************************************************
+ */
+
+ /**
+ * Immutable value class used for sharing information as efficiently
+ * as possible, by only require synchronization of reference manipulation
+ * but not access to contents.
+ *
+ * @since 2.1
+ */
+ private final static class TableInfo
+ {
+ public final int size;
+ public final int count;
+ public final int tertiaryShift;
+ public final int[] mainHash;
+ public final String[] names;
+ public final int spilloverEnd;
+ public final int longNameOffset;
+
+ public TableInfo(int size, int count, int tertiaryShift,
+ int[] mainHash, String[] names, int spilloverEnd, int longNameOffset)
+ {
+ this.size = size;
+ this.count = count;
+ this.tertiaryShift = tertiaryShift;
+ this.mainHash = mainHash;
+ this.names = names;
+ this.spilloverEnd = spilloverEnd;
+ this.longNameOffset = longNameOffset;
+ }
+
+ public TableInfo(ByteQuadsCanonicalizer src)
+ {
+ size = src._hashSize;
+ count = src._count;
+ tertiaryShift = src._tertiaryShift;
+ mainHash = src._hashArea;
+ names = src._names;
+ spilloverEnd = src._spilloverEnd;
+ longNameOffset = src._longNameOffset;
+ }
+
+ public static TableInfo createInitial(int sz) {
+ int hashAreaSize = sz << 3;
+ int tertShift = _calcTertiaryShift(sz);
+
+ return new TableInfo(sz, // hashSize
+ 0, // count
+ tertShift,
+ new int[hashAreaSize], // mainHash, 2x slots, 4 ints per slot
+ new String[sz << 1], // names == 2x slots
+ hashAreaSize - sz, // at 7/8 of the total area
+ hashAreaSize // longNameOffset, immediately after main hashes
+ );
+ }
+ }
+}
diff --git a/src/main/java/com/fasterxml/jackson/core/sym/BytesToNameCanonicalizer.java b/src/main/java/com/fasterxml/jackson/core/sym/BytesToNameCanonicalizer.java
index 0de6edf..7d610c1 100644
--- a/src/main/java/com/fasterxml/jackson/core/sym/BytesToNameCanonicalizer.java
+++ b/src/main/java/com/fasterxml/jackson/core/sym/BytesToNameCanonicalizer.java
@@ -15,8 +15,9 @@
* symbol tables, to be able to make use of usually shared vocabulary
* of subsequent parsing runs.
*
- * @author Tatu Saloranta
+ * @deprecated Since 2.6, replaced by {@link ByteQuadsCanonicalizer}
*/
+@Deprecated
public final class BytesToNameCanonicalizer
{
private static final int DEFAULT_T_SIZE = 64;
@@ -33,7 +34,7 @@
* this corresponds to 64k main hash index. This should allow for enough distinct
* names for almost any case.
*/
- private final static int MAX_ENTRIES_FOR_REUSE = 6000;
+ final static int MAX_ENTRIES_FOR_REUSE = 6000;
/**
* Also: to thwart attacks based on hash collisions (which may or may not
@@ -333,13 +334,13 @@
0 // longestCollisionList
);
}
-
+
/*
/**********************************************************
/* Life-cycle: factory methods, merging
/**********************************************************
*/
-
+
/**
* Factory method to call to create a symbol table instance with a
* randomized seed value.
@@ -361,7 +362,7 @@
protected static BytesToNameCanonicalizer createRoot(int seed) {
return new BytesToNameCanonicalizer(DEFAULT_T_SIZE, true, seed, true);
}
-
+
/**
* Factory method used to create actual symbol table instance to
* use for parsing.
@@ -592,6 +593,37 @@
return null;
}
+ public Name findName(int q1, int q2, int q3)
+ {
+ int hash = calcHash(q1, q2, q3);
+ int ix = (hash & _hashMask);
+ int val = _hash[ix];
+
+ if ((((val >> 8) ^ hash) << 8) == 0) { // match
+ // Ok, but do we have an actual match?
+ Name name = _mainNames[ix];
+ if (name == null) { // main slot empty; can't find
+ return null;
+ }
+ if (name.equals(q1, q2, q3)) {
+ return name;
+ }
+ } else if (val == 0) { // empty slot? no match
+ return null;
+ }
+ // Maybe a spill-over?
+ val &= 0xFF;
+ if (val > 0) { // 0 means 'empty'
+ val -= 1; // to convert from 1-based to 0...
+ Bucket bucket = _collList[val];
+ if (bucket != null) {
+ return bucket.find(hash, q1, q2, q3);
+ }
+ }
+ // Nope, no match whatsoever
+ return null;
+ }
+
/**
* Finds and returns name matching the specified symbol, if such
* name already exists in the table; or if not, creates name object,
@@ -611,7 +643,10 @@
*/
public Name findName(int[] q, int qlen)
{
- if (qlen < 3) { // another sanity check
+ if (qlen < 4) { // another sanity check
+ if (qlen == 3) {
+ return findName(q[0], q[1], q[2]);
+ }
return findName(q[0], (qlen < 2) ? 0 : q[1]);
}
int hash = calcHash(q, qlen);
@@ -661,8 +696,14 @@
name = InternCache.instance.intern(name);
}
int hash;
- if (qlen < 3) {
- hash = (qlen == 1) ? calcHash(q[0]) : calcHash(q[0], q[1]);
+ if (qlen < 4) {
+ if (qlen == 1) {
+ hash = calcHash(q[0]);
+ } else if (qlen == 2) {
+ hash = calcHash(q[0], q[1]);
+ } else {
+ hash = calcHash(q[0], q[1], q[2]);
+ }
} else {
hash = calcHash(q, qlen);
}
@@ -715,10 +756,28 @@
return hash;
}
+ public int calcHash(int q1, int q2, int q3)
+ {
+ // use same algorithm as multi-byte, tested to work well
+ int hash = q1 ^ _seed;
+ hash += (hash >>> 9);
+ hash *= MULT;
+ hash += q2;
+ hash *= MULT2;
+ hash += (hash >>> 15);
+ hash ^= q3;
+ hash += (hash >>> 17);
+
+ // and finally shuffle some more once done
+ hash += (hash >>> 15); // to get high-order bits to mix more
+ hash ^= (hash << 9); // as well as lowest 2 bytes
+
+ return hash;
+ }
+
public int calcHash(int[] q, int qlen)
{
- // Note: may be called for qlen < 3; but has at least one int
- if (qlen < 3) {
+ if (qlen < 4) {
throw new IllegalArgumentException();
}
@@ -1123,8 +1182,8 @@
case 2:
return new Name2(name, hash, quads[0], quads[1]);
case 3:
- return new Name3(name, hash, quads[0], quads[1], quads[2]);
default:
+ return new Name3(name, hash, quads[0], quads[1], quads[2]);
}
}
return NameN.construct(name, hash, quads, qlen);
@@ -1135,7 +1194,7 @@
/* Other helper methods
/**********************************************************
*/
-
+
/**
* @since 2.1
*/
@@ -1144,7 +1203,7 @@
throw new IllegalStateException("Longest collision chain in symbol table (of size "+_count
+") now exceeds maximum, "+maxLen+" -- suspect a DoS attack based on hash collisions");
}
-
+
/*
/**********************************************************
/* Helper classes
@@ -1226,6 +1285,23 @@
return null;
}
+ public Name find(int h, int q1, int q2, int q3) {
+ if (hash == h) {
+ if (name.equals(q1, q2, q3)) {
+ return name;
+ }
+ }
+ for (Bucket curr = next; curr != null; curr = curr.next) {
+ if (curr.hash == h) {
+ Name currName = curr.name;
+ if (currName.equals(q1, q2, q3)) {
+ return currName;
+ }
+ }
+ }
+ return null;
+ }
+
public Name find(int h, int[] quads, int qlen) {
if (hash == h) {
if (name.equals(quads, qlen)) {
diff --git a/src/main/java/com/fasterxml/jackson/core/sym/CharsToNameCanonicalizer.java b/src/main/java/com/fasterxml/jackson/core/sym/CharsToNameCanonicalizer.java
index 43fa7c7..a668f2e 100644
--- a/src/main/java/com/fasterxml/jackson/core/sym/CharsToNameCanonicalizer.java
+++ b/src/main/java/com/fasterxml/jackson/core/sym/CharsToNameCanonicalizer.java
@@ -554,7 +554,10 @@
* and truncates to be used as the index.
*/
public int _hashToIndex(int rawHash) {
- rawHash += (rawHash >>> 15); // this seems to help quite a bit, at least for our tests
+ // doing these seems to help a bit
+ rawHash += (rawHash >>> 15);
+ rawHash ^= (rawHash << 7);
+ rawHash += (rawHash >>> 3);
return (rawHash & _indexMask);
}
diff --git a/src/main/java/com/fasterxml/jackson/core/sym/Name.java b/src/main/java/com/fasterxml/jackson/core/sym/Name.java
index d26f55d..3546008 100644
--- a/src/main/java/com/fasterxml/jackson/core/sym/Name.java
+++ b/src/main/java/com/fasterxml/jackson/core/sym/Name.java
@@ -26,9 +26,14 @@
/**********************************************************
*/
- public abstract boolean equals(int quad1);
+ public abstract boolean equals(int q1);
- public abstract boolean equals(int quad1, int quad2);
+ public abstract boolean equals(int q1, int q2);
+
+ /**
+ * @since 2.6
+ */
+ public abstract boolean equals(int q1, int q2, int q3);
public abstract boolean equals(int[] quads, int qlen);
diff --git a/src/main/java/com/fasterxml/jackson/core/sym/Name1.java b/src/main/java/com/fasterxml/jackson/core/sym/Name1.java
index 7a9830f..fee4255 100644
--- a/src/main/java/com/fasterxml/jackson/core/sym/Name1.java
+++ b/src/main/java/com/fasterxml/jackson/core/sym/Name1.java
@@ -23,5 +23,7 @@
@Override public boolean equals(int quad) { return (quad == q); }
@Override public boolean equals(int quad1, int quad2) { return (quad1 == q) && (quad2 == 0); }
+ @Override public boolean equals(int q1, int q2, int q3) { return false; }
+
@Override public boolean equals(int[] quads, int qlen) { return (qlen == 1 && quads[0] == q); }
}
diff --git a/src/main/java/com/fasterxml/jackson/core/sym/Name2.java b/src/main/java/com/fasterxml/jackson/core/sym/Name2.java
index 42ef1b5..e60fbd4 100644
--- a/src/main/java/com/fasterxml/jackson/core/sym/Name2.java
+++ b/src/main/java/com/fasterxml/jackson/core/sym/Name2.java
@@ -25,6 +25,8 @@
@Override
public boolean equals(int quad1, int quad2) { return (quad1 == q1) && (quad2 == q2); }
+ @Override public boolean equals(int quad1, int quad2, int q3) { return false; }
+
@Override
public boolean equals(int[] quads, int qlen) { return (qlen == 2 && quads[0] == q1 && quads[1] == q2); }
}
diff --git a/src/main/java/com/fasterxml/jackson/core/sym/Name3.java b/src/main/java/com/fasterxml/jackson/core/sym/Name3.java
index ed2381c..2da81e9 100644
--- a/src/main/java/com/fasterxml/jackson/core/sym/Name3.java
+++ b/src/main/java/com/fasterxml/jackson/core/sym/Name3.java
@@ -25,6 +25,11 @@
public boolean equals(int quad1, int quad2) { return false; }
@Override
+ public boolean equals(int quad1, int quad2, int quad3) {
+ return (q1 == quad1) && (q2 == quad2) && (q3 == quad3);
+ }
+
+ @Override
public boolean equals(int[] quads, int qlen) {
return (qlen == 3) && (quads[0] == q1) && (quads[1] == q2) && (quads[2] == q3);
}
diff --git a/src/main/java/com/fasterxml/jackson/core/sym/NameN.java b/src/main/java/com/fasterxml/jackson/core/sym/NameN.java
index 0695b36..2d51df7 100644
--- a/src/main/java/com/fasterxml/jackson/core/sym/NameN.java
+++ b/src/main/java/com/fasterxml/jackson/core/sym/NameN.java
@@ -57,6 +57,10 @@
@Override
public boolean equals(int quad1, int quad2) { return false; }
+ // Implies quad length == 3, never matches
+ @Override
+ public boolean equals(int quad1, int quad2, int quad3) { return false; }
+
@Override
public boolean equals(int[] quads, int len) {
if (len != qlen) { return false; }
diff --git a/src/main/java/com/fasterxml/jackson/core/type/ResolvedType.java b/src/main/java/com/fasterxml/jackson/core/type/ResolvedType.java
index d5f6b5c..2c57157 100644
--- a/src/main/java/com/fasterxml/jackson/core/type/ResolvedType.java
+++ b/src/main/java/com/fasterxml/jackson/core/type/ResolvedType.java
@@ -46,6 +46,19 @@
public abstract boolean isCollectionLikeType();
+ /**
+ * Whether this type is a referential type, meaning that values are
+ * basically pointers to "real" values (or null) and not regular
+ * values themselves. Typical examples include things like
+ * {@link java.util.concurrent.atomic.AtomicReference}, and various
+ * <code>Optional</code> types (in JDK8, Guava).
+ *
+ * @since 2.6
+ */
+ public boolean isReferenceType() {
+ return getReferencedType() != null;
+ }
+
public abstract boolean isMapLikeType();
/*
@@ -94,6 +107,16 @@
public abstract ResolvedType getContentType();
/**
+ * Method for accessing type of value that instances of this
+ * type references, if any.
+ *
+ * @return Referenced type, if any; null if not.
+ *
+ * @since 2.6
+ */
+ public abstract ResolvedType getReferencedType();
+
+ /**
* Method for checking how many contained types this type
* has. Contained types are usually generic types, so that
* generic Maps have 2 contained types.
diff --git a/src/main/java/com/fasterxml/jackson/core/util/BufferRecycler.java b/src/main/java/com/fasterxml/jackson/core/util/BufferRecycler.java
index fdc7f32..bd2ad9c 100644
--- a/src/main/java/com/fasterxml/jackson/core/util/BufferRecycler.java
+++ b/src/main/java/com/fasterxml/jackson/core/util/BufferRecycler.java
@@ -113,7 +113,7 @@
public final char[] allocCharBuffer(int ix) {
return allocCharBuffer(ix, 0);
}
-
+
public char[] allocCharBuffer(int ix, int minSize) {
final int DEF_SIZE = charBufferLength(ix);
if (minSize < DEF_SIZE) {
diff --git a/src/main/java/com/fasterxml/jackson/core/util/DefaultIndenter.java b/src/main/java/com/fasterxml/jackson/core/util/DefaultIndenter.java
index e655595..143a51f 100644
--- a/src/main/java/com/fasterxml/jackson/core/util/DefaultIndenter.java
+++ b/src/main/java/com/fasterxml/jackson/core/util/DefaultIndenter.java
@@ -37,12 +37,15 @@
private final int charsPerLevel;
private final String eol;
- /** Indent with two spaces and the system's default line feed */
+ /**
+ * Indent with two spaces and the system's default line feed
+ */
public DefaultIndenter() {
this(" ", SYS_LF);
}
- /** Create an indenter which uses the <code>indent</code> string to indent one level
+ /**
+ * Create an indenter which uses the <code>indent</code> string to indent one level
* and the <code>eol</code> string to separate lines.
*/
public DefaultIndenter(String indent, String eol)
diff --git a/src/main/java/com/fasterxml/jackson/core/util/DefaultPrettyPrinter.java b/src/main/java/com/fasterxml/jackson/core/util/DefaultPrettyPrinter.java
index e19bd57..0a004c5 100644
--- a/src/main/java/com/fasterxml/jackson/core/util/DefaultPrettyPrinter.java
+++ b/src/main/java/com/fasterxml/jackson/core/util/DefaultPrettyPrinter.java
@@ -146,7 +146,7 @@
* @since 2.6.0
*/
public DefaultPrettyPrinter withRootSeparator(String rootSeparator) {
- return withRootSeparator(new SerializedString(rootSeparator));
+ return withRootSeparator((rootSeparator == null) ? null : new SerializedString(rootSeparator));
}
public void indentArraysWith(Indenter i) {
@@ -349,26 +349,24 @@
* (white-space) decoration.
*/
@Override
- public void writeArrayValueSeparator(JsonGenerator jg)
- throws IOException, JsonGenerationException
+ public void writeArrayValueSeparator(JsonGenerator gen) throws IOException
{
- jg.writeRaw(',');
- _arrayIndenter.writeIndentation(jg, _nesting);
+ gen.writeRaw(',');
+ _arrayIndenter.writeIndentation(gen, _nesting);
}
@Override
- public void writeEndArray(JsonGenerator jg, int nrOfValues)
- throws IOException, JsonGenerationException
+ public void writeEndArray(JsonGenerator gen, int nrOfValues) throws IOException
{
if (!_arrayIndenter.isInline()) {
--_nesting;
}
if (nrOfValues > 0) {
- _arrayIndenter.writeIndentation(jg, _nesting);
+ _arrayIndenter.writeIndentation(gen, _nesting);
} else {
- jg.writeRaw(' ');
+ gen.writeRaw(' ');
}
- jg.writeRaw(']');
+ gen.writeRaw(']');
}
/*
@@ -393,7 +391,7 @@
}
/**
- * This is a very simple indenter that only every adds a
+ * This is a very simple indenter that only adds a
* single space for indentation. It is used as the default
* indenter for array values.
*/
@@ -403,8 +401,7 @@
public static final FixedSpaceIndenter instance = new FixedSpaceIndenter();
@Override
- public void writeIndentation(JsonGenerator jg, int level)
- throws IOException, JsonGenerationException
+ public void writeIndentation(JsonGenerator jg, int level) throws IOException
{
jg.writeRaw(' ');
}
diff --git a/src/main/java/com/fasterxml/jackson/core/util/JsonGeneratorDelegate.java b/src/main/java/com/fasterxml/jackson/core/util/JsonGeneratorDelegate.java
index 4f3577b..9d53afa 100644
--- a/src/main/java/com/fasterxml/jackson/core/util/JsonGeneratorDelegate.java
+++ b/src/main/java/com/fasterxml/jackson/core/util/JsonGeneratorDelegate.java
@@ -27,7 +27,7 @@
/* Construction, initialization
/**********************************************************
*/
-
+
public JsonGeneratorDelegate(JsonGenerator d) {
this(d, true);
}
@@ -77,6 +77,7 @@
@Override public FormatSchema getSchema() { return delegate.getSchema(); }
@Override public Version version() { return delegate.version(); }
@Override public Object getOutputTarget() { return delegate.getOutputTarget(); }
+ @Override public int getOutputBuffered() { return delegate.getOutputBuffered(); }
/*
/**********************************************************
diff --git a/src/main/java/com/fasterxml/jackson/core/util/JsonParserDelegate.java b/src/main/java/com/fasterxml/jackson/core/util/JsonParserDelegate.java
index 13e76eb..7f4241d 100644
--- a/src/main/java/com/fasterxml/jackson/core/util/JsonParserDelegate.java
+++ b/src/main/java/com/fasterxml/jackson/core/util/JsonParserDelegate.java
@@ -98,8 +98,9 @@
@Override public int getCurrentTokenId() { return delegate.getCurrentTokenId(); }
@Override public boolean hasCurrentToken() { return delegate.hasCurrentToken(); }
@Override public boolean hasTokenId(int id) { return delegate.hasTokenId(id); }
-
- @Override public String getCurrentName() throws IOException, JsonParseException { return delegate.getCurrentName(); }
+ @Override public boolean hasToken(JsonToken t) { return delegate.hasToken(t); }
+
+ @Override public String getCurrentName() throws IOException { return delegate.getCurrentName(); }
@Override public JsonLocation getCurrentLocation() { return delegate.getCurrentLocation(); }
@Override public JsonStreamContext getParsingContext() { return delegate.getParsingContext(); }
@Override public boolean isExpectedStartArrayToken() { return delegate.isExpectedStartArrayToken(); }
@@ -121,11 +122,11 @@
/**********************************************************
*/
- @Override public String getText() throws IOException, JsonParseException { return delegate.getText(); }
+ @Override public String getText() throws IOException { return delegate.getText(); }
@Override public boolean hasTextCharacters() { return delegate.hasTextCharacters(); }
- @Override public char[] getTextCharacters() throws IOException, JsonParseException { return delegate.getTextCharacters(); }
- @Override public int getTextLength() throws IOException, JsonParseException { return delegate.getTextLength(); }
- @Override public int getTextOffset() throws IOException, JsonParseException { return delegate.getTextOffset(); }
+ @Override public char[] getTextCharacters() throws IOException { return delegate.getTextCharacters(); }
+ @Override public int getTextLength() throws IOException { return delegate.getTextLength(); }
+ @Override public int getTextOffset() throws IOException { return delegate.getTextOffset(); }
/*
/**********************************************************
@@ -134,37 +135,37 @@
*/
@Override
- public BigInteger getBigIntegerValue() throws IOException,JsonParseException { return delegate.getBigIntegerValue(); }
+ public BigInteger getBigIntegerValue() throws IOException { return delegate.getBigIntegerValue(); }
@Override
- public boolean getBooleanValue() throws IOException, JsonParseException { return delegate.getBooleanValue(); }
+ public boolean getBooleanValue() throws IOException { return delegate.getBooleanValue(); }
@Override
- public byte getByteValue() throws IOException, JsonParseException { return delegate.getByteValue(); }
+ public byte getByteValue() throws IOException { return delegate.getByteValue(); }
@Override
- public short getShortValue() throws IOException, JsonParseException { return delegate.getShortValue(); }
+ public short getShortValue() throws IOException { return delegate.getShortValue(); }
@Override
- public BigDecimal getDecimalValue() throws IOException, JsonParseException { return delegate.getDecimalValue(); }
+ public BigDecimal getDecimalValue() throws IOException { return delegate.getDecimalValue(); }
@Override
- public double getDoubleValue() throws IOException, JsonParseException { return delegate.getDoubleValue(); }
+ public double getDoubleValue() throws IOException { return delegate.getDoubleValue(); }
@Override
- public float getFloatValue() throws IOException, JsonParseException { return delegate.getFloatValue(); }
+ public float getFloatValue() throws IOException { return delegate.getFloatValue(); }
@Override
- public int getIntValue() throws IOException, JsonParseException { return delegate.getIntValue(); }
+ public int getIntValue() throws IOException { return delegate.getIntValue(); }
@Override
- public long getLongValue() throws IOException, JsonParseException { return delegate.getLongValue(); }
+ public long getLongValue() throws IOException { return delegate.getLongValue(); }
@Override
- public NumberType getNumberType() throws IOException, JsonParseException { return delegate.getNumberType(); }
+ public NumberType getNumberType() throws IOException { return delegate.getNumberType(); }
@Override
- public Number getNumberValue() throws IOException, JsonParseException { return delegate.getNumberValue(); }
+ public Number getNumberValue() throws IOException { return delegate.getNumberValue(); }
/*
/**********************************************************
@@ -172,16 +173,16 @@
/**********************************************************
*/
- @Override public int getValueAsInt() throws IOException, JsonParseException { return delegate.getValueAsInt(); }
- @Override public int getValueAsInt(int defaultValue) throws IOException, JsonParseException { return delegate.getValueAsInt(defaultValue); }
- @Override public long getValueAsLong() throws IOException, JsonParseException { return delegate.getValueAsLong(); }
- @Override public long getValueAsLong(long defaultValue) throws IOException, JsonParseException { return delegate.getValueAsLong(defaultValue); }
- @Override public double getValueAsDouble() throws IOException, JsonParseException { return delegate.getValueAsDouble(); }
- @Override public double getValueAsDouble(double defaultValue) throws IOException, JsonParseException { return delegate.getValueAsDouble(defaultValue); }
- @Override public boolean getValueAsBoolean() throws IOException, JsonParseException { return delegate.getValueAsBoolean(); }
- @Override public boolean getValueAsBoolean(boolean defaultValue) throws IOException, JsonParseException { return delegate.getValueAsBoolean(defaultValue); }
- @Override public String getValueAsString() throws IOException, JsonParseException { return delegate.getValueAsString(); }
- @Override public String getValueAsString(String defaultValue) throws IOException, JsonParseException { return delegate.getValueAsString(defaultValue); }
+ @Override public int getValueAsInt() throws IOException { return delegate.getValueAsInt(); }
+ @Override public int getValueAsInt(int defaultValue) throws IOException { return delegate.getValueAsInt(defaultValue); }
+ @Override public long getValueAsLong() throws IOException { return delegate.getValueAsLong(); }
+ @Override public long getValueAsLong(long defaultValue) throws IOException { return delegate.getValueAsLong(defaultValue); }
+ @Override public double getValueAsDouble() throws IOException { return delegate.getValueAsDouble(); }
+ @Override public double getValueAsDouble(double defaultValue) throws IOException { return delegate.getValueAsDouble(defaultValue); }
+ @Override public boolean getValueAsBoolean() throws IOException { return delegate.getValueAsBoolean(); }
+ @Override public boolean getValueAsBoolean(boolean defaultValue) throws IOException { return delegate.getValueAsBoolean(defaultValue); }
+ @Override public String getValueAsString() throws IOException { return delegate.getValueAsString(); }
+ @Override public String getValueAsString(String defaultValue) throws IOException { return delegate.getValueAsString(defaultValue); }
/*
/**********************************************************
@@ -189,15 +190,15 @@
/**********************************************************
*/
- @Override public Object getEmbeddedObject() throws IOException, JsonParseException { return delegate.getEmbeddedObject(); }
- @Override public byte[] getBinaryValue(Base64Variant b64variant) throws IOException, JsonParseException { return delegate.getBinaryValue(b64variant); }
- @Override public int readBinaryValue(Base64Variant b64variant, OutputStream out) throws IOException, JsonParseException { return delegate.readBinaryValue(b64variant, out); }
+ @Override public Object getEmbeddedObject() throws IOException { return delegate.getEmbeddedObject(); }
+ @Override public byte[] getBinaryValue(Base64Variant b64variant) throws IOException { return delegate.getBinaryValue(b64variant); }
+ @Override public int readBinaryValue(Base64Variant b64variant, OutputStream out) throws IOException { return delegate.readBinaryValue(b64variant, out); }
@Override public JsonLocation getTokenLocation() { return delegate.getTokenLocation(); }
- @Override public JsonToken nextToken() throws IOException, JsonParseException { return delegate.nextToken(); }
- @Override public JsonToken nextValue() throws IOException, JsonParseException { return delegate.nextValue(); }
+ @Override public JsonToken nextToken() throws IOException { return delegate.nextToken(); }
+ @Override public JsonToken nextValue() throws IOException { return delegate.nextValue(); }
@Override
- public JsonParser skipChildren() throws IOException, JsonParseException {
+ public JsonParser skipChildren() throws IOException {
delegate.skipChildren();
// NOTE: must NOT delegate this method to delegate, needs to be self-reference for chaining
return this;
@@ -211,6 +212,6 @@
@Override public boolean canReadObjectId() { return delegate.canReadObjectId(); }
@Override public boolean canReadTypeId() { return delegate.canReadTypeId(); }
- @Override public Object getObjectId() throws IOException, JsonGenerationException { return delegate.getObjectId(); }
- @Override public Object getTypeId() throws IOException, JsonGenerationException { return delegate.getTypeId(); }
+ @Override public Object getObjectId() throws IOException { return delegate.getObjectId(); }
+ @Override public Object getTypeId() throws IOException { return delegate.getTypeId(); }
}
diff --git a/src/main/java/com/fasterxml/jackson/core/util/MinimalPrettyPrinter.java b/src/main/java/com/fasterxml/jackson/core/util/MinimalPrettyPrinter.java
index ab65ffe..889d853 100644
--- a/src/main/java/com/fasterxml/jackson/core/util/MinimalPrettyPrinter.java
+++ b/src/main/java/com/fasterxml/jackson/core/util/MinimalPrettyPrinter.java
@@ -26,7 +26,7 @@
public class MinimalPrettyPrinter
implements PrettyPrinter, java.io.Serializable
{
- private static final long serialVersionUID = -562765100295218442L;
+ private static final long serialVersionUID = 1L;
/**
* Default String used for separating root values is single space.
diff --git a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java
index e6f1cbc..68ba240 100644
--- a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java
+++ b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java
@@ -294,7 +294,12 @@
if (_resultString != null) return false;
return true;
}
-
+
+ /**
+ * Accessor that may be used to get the contents of this buffer in a single
+ * <code>char</code> array regardless of whether they were collected in a segmented
+ * fashion or not.
+ */
public char[] getTextBuffer()
{
// Are we just using shared input buffer?
@@ -304,7 +309,9 @@
return (_resultArray = _resultString.toCharArray());
}
// Nope; but does it fit in just one segment?
- if (!_hasSegments) return _currentSegment;
+ if (!_hasSegments) {
+ return (_currentSegment == null) ? NO_CHARS : _currentSegment;
+ }
// Nope, need to have/create a non-segmented array and return it
return contentsAsArray();
}
diff --git a/src/main/java/com/fasterxml/jackson/core/util/VersionUtil.java b/src/main/java/com/fasterxml/jackson/core/util/VersionUtil.java
index 8b01016..4c121ad 100644
--- a/src/main/java/com/fasterxml/jackson/core/util/VersionUtil.java
+++ b/src/main/java/com/fasterxml/jackson/core/util/VersionUtil.java
@@ -19,6 +19,9 @@
* 2.1 and 2.2; earlier code used file named "VERSION.txt"; but this has serious
* performance issues on some platforms (Android), so a replacement system
* was implemented to use class generation and dynamic class loading.
+ *<p>
+ * Note that functionality for reading "VERSION.txt" was removed completely
+ * from Jackson 2.6.
*/
public class VersionUtil
{
@@ -64,81 +67,36 @@
* First, tries to load version info from a class named
* "PackageVersion" in the same package as the class.
*
- * Next, if that fails, class loader that loaded specified class is
- * asked to load resource with name "VERSION" from same location
- * (package) as class itself had.
- *
* If no version information is found, {@link Version#unknownVersion()} is returned.
*/
- @SuppressWarnings("resource")
public static Version versionFor(Class<?> cls)
{
- Version packageVersion = packageVersionFor(cls);
- if (packageVersion != null) {
- return packageVersion;
- }
- final InputStream in = cls.getResourceAsStream("VERSION.txt");
- if (in == null) {
- return Version.unknownVersion();
- }
- try {
- InputStreamReader reader = new InputStreamReader(in, "UTF-8");
- return doReadVersion(reader);
- } catch (UnsupportedEncodingException e) {
- return Version.unknownVersion();
- } finally {
- _close(in);
- }
+ return packageVersionFor(cls);
}
/**
* Loads version information by introspecting a class named
* "PackageVersion" in the same package as the given class.
- *
+ *<p>
* If the class could not be found or does not have a public
* static Version field named "VERSION", returns null.
*/
public static Version packageVersionFor(Class<?> cls)
{
+ Version v = null;
try {
String versionInfoClassName = cls.getPackage().getName() + ".PackageVersion";
Class<?> vClass = Class.forName(versionInfoClassName, true, cls.getClassLoader());
// However, if class exists, it better work correctly, no swallowing exceptions
try {
- return ((Versioned) vClass.newInstance()).version();
+ v = ((Versioned) vClass.newInstance()).version();
} catch (Exception e) {
throw new IllegalArgumentException("Failed to get Versioned out of "+vClass);
}
- } catch (Exception e) { // ok to be missing (not good, acceptable)
- return null;
+ } catch (Exception e) { // ok to be missing (not good but acceptable)
+ ;
}
- }
-
- private static Version doReadVersion(final Reader r)
- {
- String version = null, group = null, artifact = null;
-
- final BufferedReader br = new BufferedReader(r);
- try {
- version = br.readLine();
- if (version != null) {
- group = br.readLine();
- if (group != null) {
- artifact = br.readLine();
- }
- }
- } catch (IOException ignored) {
- } finally {
- _close(br);
- }
- // We don't trim() version: parseVersion() takes care ot that
- if (group != null) {
- group = group.trim();
- }
- if (artifact != null) {
- artifact = artifact.trim();
- }
- return parseVersion(version, group, artifact);
+ return (v == null) ? Version.unknownVersion() : v;
}
/**
@@ -151,8 +109,12 @@
* @param groupId the groupId of the library
* @param artifactId the artifactId of the library
* @return The version
+ *
+ * @deprecated Since 2.6: functionality not used by any official Jackson component, should be
+ * moved out if anyone needs it
*/
@SuppressWarnings("resource")
+ @Deprecated // since 2.6
public static Version mavenVersionFor(ClassLoader cl, String groupId, String artifactId)
{
InputStream pomProperties = cl.getResourceAsStream("META-INF/maven/"
@@ -174,6 +136,9 @@
return Version.unknownVersion();
}
+ /**
+ * Method used by <code>PackageVersion</code> classes to decode version injected by Maven build.
+ */
public static Version parseVersion(String s, String groupId, String artifactId)
{
if (s != null && (s = s.trim()).length() > 0) {
@@ -184,7 +149,7 @@
(parts.length > 3) ? parts[3] : null,
groupId, artifactId);
}
- return null;
+ return Version.unknownVersion();
}
protected static int parseVersionPart(String s) {
diff --git a/src/test/java/com/fasterxml/jackson/core/BaseTest.java b/src/test/java/com/fasterxml/jackson/core/BaseTest.java
index 3beed51..5184162 100644
--- a/src/test/java/com/fasterxml/jackson/core/BaseTest.java
+++ b/src/test/java/com/fasterxml/jackson/core/BaseTest.java
@@ -311,6 +311,45 @@
/*
/**********************************************************
+ /* Helper read/write methods
+ /**********************************************************
+ */
+
+ protected void writeJsonDoc(JsonFactory f, String doc, Writer w) throws IOException
+ {
+ writeJsonDoc(f, doc, f.createGenerator(w));
+ }
+
+ protected void writeJsonDoc(JsonFactory f, String doc, JsonGenerator g) throws IOException
+ {
+ JsonParser p = f.createParser(aposToQuotes(doc));
+
+ while (p.nextToken() != null) {
+ g.copyCurrentStructure(p);
+ }
+ p.close();
+ g.close();
+ }
+
+ protected String readAndWrite(JsonFactory f, JsonParser p) throws IOException
+ {
+ StringWriter sw = new StringWriter(100);
+ JsonGenerator g = f.createGenerator(sw);
+ try {
+ while (p.nextToken() != null) {
+ g.copyCurrentEvent(p);
+ }
+ } catch (IOException e) {
+ g.flush();
+ fail("Unexpected problem during `readAndWrite`. Output so far: '"+sw+"'; problem: "+e);
+ }
+ p.close();
+ g.close();
+ return sw.toString();
+ }
+
+ /*
+ /**********************************************************
/* Additional assertion methods
/**********************************************************
*/
@@ -356,8 +395,7 @@
* available methods, and ensures results are consistent, before
* returning them
*/
- protected String getAndVerifyText(JsonParser jp)
- throws IOException, JsonParseException
+ protected String getAndVerifyText(JsonParser jp) throws IOException
{
// Ok, let's verify other accessors
int actLen = jp.getTextLength();
@@ -393,10 +431,14 @@
return result;
}
- public String quote(String str) {
+ protected String quote(String str) {
return '"'+str+'"';
}
+ protected String aposToQuotes(String json) {
+ return json.replace("'", "\"");
+ }
+
protected void fieldNameFor(StringBuilder sb, int index)
{
/* let's do something like "f1.1" to exercise different
@@ -425,4 +467,23 @@
return sb.toString();
}
+ protected int[] calcQuads(byte[] wordBytes) {
+ int blen = wordBytes.length;
+ int[] result = new int[(blen + 3) / 4];
+ for (int i = 0; i < blen; ++i) {
+ int x = wordBytes[i] & 0xFF;
+
+ if (++i < blen) {
+ x = (x << 8) | (wordBytes[i] & 0xFF);
+ if (++i < blen) {
+ x = (x << 8) | (wordBytes[i] & 0xFF);
+ if (++i < blen) {
+ x = (x << 8) | (wordBytes[i] & 0xFF);
+ }
+ }
+ }
+ result[i >> 2] = x;
+ }
+ return result;
+ }
}
diff --git a/src/test/java/com/fasterxml/jackson/core/filter/BasicGeneratorFilteringTest.java b/src/test/java/com/fasterxml/jackson/core/filter/BasicGeneratorFilteringTest.java
new file mode 100644
index 0000000..8c6369c
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/core/filter/BasicGeneratorFilteringTest.java
@@ -0,0 +1,308 @@
+package com.fasterxml.jackson.core.filter;
+
+import java.io.*;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.*;
+
+import com.fasterxml.jackson.core.*;
+import com.fasterxml.jackson.core.io.SerializedString;
+
+/**
+ * Low-level tests for explicit, hand-written tests for generator-side
+ * filtering.
+ */
+@SuppressWarnings("resource")
+public class BasicGeneratorFilteringTest extends BaseTest
+{
+ static class NameMatchFilter extends TokenFilter
+ {
+ private final Set<String> _names;
+
+ public NameMatchFilter(String... names) {
+ _names = new HashSet<String>(Arrays.asList(names));
+ }
+
+ @Override
+ public TokenFilter includeElement(int index) {
+ return this;
+ }
+
+ @Override
+ public TokenFilter includeProperty(String name) {
+ if (_names.contains(name)) {
+ return TokenFilter.INCLUDE_ALL;
+ }
+ return this;
+ }
+
+ @Override
+ protected boolean _includeScalar() { return false; }
+ }
+
+ static class IndexMatchFilter extends TokenFilter
+ {
+ private final BitSet _indices;
+
+ public IndexMatchFilter(int... ixs) {
+ _indices = new BitSet();
+ for (int ix : ixs) {
+ _indices.set(ix);
+ }
+ }
+
+ @Override
+ public TokenFilter includeProperty(String name) {
+ return this;
+ }
+
+ @Override
+ public TokenFilter includeElement(int index) {
+ if (_indices.get(index)) {
+ return TokenFilter.INCLUDE_ALL;
+ }
+ return null;
+ }
+
+ @Override
+ protected boolean _includeScalar() { return false; }
+ }
+
+ /*
+ /**********************************************************
+ /* Test methods
+ /**********************************************************
+ */
+
+ private final JsonFactory JSON_F = new JsonFactory();
+
+ public void testNonFiltering() throws Exception
+ {
+ // First, verify non-filtering
+ StringWriter w = new StringWriter();
+ JsonGenerator gen = JSON_F.createGenerator(w);
+ final String JSON = "{'a':123,'array':[1,2],'ob':{'value0':2,'value':3,'value2':4},'b':true}";
+ writeJsonDoc(JSON_F, JSON, gen);
+ assertEquals(aposToQuotes(
+ "{'a':123,'array':[1,2],'ob':{'value0':2,'value':3,'value2':4},'b':true}"),
+ w.toString());
+ }
+
+ public void testSingleMatchFilteringWithoutPath() throws Exception
+ {
+ StringWriter w = new StringWriter();
+ JsonGenerator gen = new FilteringGeneratorDelegate(JSON_F.createGenerator(w),
+ new NameMatchFilter("value"),
+ false, // includePath
+ false // multipleMatches
+ );
+ final String JSON = "{'a':123,'array':[1,2],'ob':{'value0':2,'value':3,'value2':4},'b':true}";
+ writeJsonDoc(JSON_F, JSON, gen);
+
+ // 21-Apr-2015, tatu: note that there were plans to actually
+ // allow "immediate parent inclusion" for matches on property
+ // names. This behavior was NOT included in release however, so:
+// assertEquals(aposToQuotes("{'value':3}"), w.toString());
+
+ assertEquals(aposToQuotes("3"), w.toString());
+ }
+
+ public void testSingleMatchFilteringWithPath() throws Exception
+ {
+ StringWriter w = new StringWriter();
+ JsonGenerator origGen = JSON_F.createGenerator(w);
+ NameMatchFilter filter = new NameMatchFilter("value");
+ FilteringGeneratorDelegate gen = new FilteringGeneratorDelegate(origGen,
+ filter,
+ true, // includePath
+ false // multipleMatches
+ );
+
+ // Hmmh. Should we get access to eventual target?
+ assertSame(w, gen.getOutputTarget());
+ assertNotNull(gen.getFilterContext());
+ assertSame(filter, gen.getFilter());
+
+ final String JSON = "{'a':123,'array':[1,2],'ob':{'value0':2,'value':3,'value2':4},'b':true}";
+ writeJsonDoc(JSON_F, JSON, gen);
+ assertEquals(aposToQuotes("{'ob':{'value':3}}"), w.toString());
+
+ assertEquals(1, gen.getMatchCount());
+ }
+
+ public void testSingleMatchFilteringWithPathSkippedArray() throws Exception
+ {
+ StringWriter w = new StringWriter();
+ JsonGenerator origGen = JSON_F.createGenerator(w);
+ NameMatchFilter filter = new NameMatchFilter("value");
+ FilteringGeneratorDelegate gen = new FilteringGeneratorDelegate(origGen,
+ filter,
+ true, // includePath
+ false // multipleMatches
+ );
+
+ // Hmmh. Should we get access to eventual target?
+ assertSame(w, gen.getOutputTarget());
+ assertNotNull(gen.getFilterContext());
+ assertSame(filter, gen.getFilter());
+
+ final String JSON = "{'array':[1,[2,3]],'ob':[{'value':'bar'}],'b':{'foo':[1,'foo']}}";
+ writeJsonDoc(JSON_F, JSON, gen);
+ assertEquals(aposToQuotes("{'ob':[{'value':'bar'}]}"), w.toString());
+ assertEquals(1, gen.getMatchCount());
+ }
+
+ // Alternative take, using slightly different calls for FIELD_NAME, START_ARRAY
+ public void testSingleMatchFilteringWithPathAlternate1() throws Exception
+ {
+ StringWriter w = new StringWriter();
+ FilteringGeneratorDelegate gen = new FilteringGeneratorDelegate(JSON_F.createGenerator(w),
+ new NameMatchFilter("value"),
+ true, // includePath
+ false // multipleMatches
+ );
+ //final String JSON = "{'a':123,'array':[1,2],'ob':{'value0':2,'value':[3],'value2':'foo'},'b':true}";
+
+ gen.writeStartObject();
+ gen.writeFieldName(new SerializedString("a"));
+ gen.writeNumber(123);
+
+ gen.writeFieldName("array");
+ gen.writeStartArray(2);
+ gen.writeNumber("1");
+ gen.writeNumber((short) 2);
+ gen.writeEndArray();
+
+ gen.writeFieldName(new SerializedString("ob"));
+ gen.writeStartObject();
+ gen.writeNumberField("value0", 2);
+ gen.writeFieldName(new SerializedString("value"));
+ gen.writeStartArray(1);
+ gen.writeString(new SerializedString("x")); // just to vary generation method
+ gen.writeEndArray();
+ gen.writeStringField("value2", "foo");
+
+ gen.writeEndObject();
+
+ gen.writeBooleanField("b", true);
+
+ gen.writeEndObject();
+ gen.close();
+
+ assertEquals(aposToQuotes("{'ob':{'value':['x']}}"), w.toString());
+ }
+
+ public void testSingleMatchFilteringWithPathRawBinary() throws Exception
+ {
+ StringWriter w = new StringWriter();
+ FilteringGeneratorDelegate gen = new FilteringGeneratorDelegate(JSON_F.createGenerator(w),
+ new NameMatchFilter("array"),
+ true, // includePath
+ false // multipleMatches
+ );
+ //final String JSON = "{'header':['ENCODED',raw],'array':['base64stuff',1,2,3,4,5,6.25,7.5],'extra':[1,2,3,4,5,6.25,7.5]}";
+
+ gen.writeStartObject();
+
+ gen.writeFieldName("header");
+ gen.writeStartArray();
+ gen.writeBinary(new byte[] { 1 });
+ gen.writeRawValue(new SerializedString("1"));
+ gen.writeRawValue("2");
+ gen.writeEndArray();
+
+ gen.writeFieldName("array");
+
+ gen.writeStartArray();
+ gen.writeBinary(new byte[] { 1 });
+ gen.writeNumber((short) 1);
+ gen.writeNumber((int) 2);
+ gen.writeNumber((long) 3);
+ gen.writeNumber(BigInteger.valueOf(4));
+ gen.writeRaw(" ");
+ gen.writeNumber(new BigDecimal("5.0"));
+ gen.writeRaw(new SerializedString(" /*x*/"));
+ gen.writeNumber(6.25f);
+ gen.writeNumber(7.5);
+ gen.writeEndArray();
+
+ gen.writeArrayFieldStart("extra");
+ gen.writeNumber((short) 1);
+ gen.writeNumber((int) 2);
+ gen.writeNumber((long) 3);
+ gen.writeNumber(BigInteger.valueOf(4));
+ gen.writeNumber(new BigDecimal("5.0"));
+ gen.writeNumber(6.25f);
+ gen.writeNumber(7.5);
+ gen.writeEndArray();
+
+ gen.writeEndObject();
+ gen.close();
+
+ assertEquals(aposToQuotes("{'array':['AQ==',1,2,3,4 ,5.0 /*x*/,6.25,7.5]}"), w.toString());
+ }
+
+ public void testMultipleMatchFilteringWithPath1() throws Exception
+ {
+ StringWriter w = new StringWriter();
+ JsonGenerator gen = new FilteringGeneratorDelegate(JSON_F.createGenerator(w),
+ new NameMatchFilter("value0", "value2"),
+ true, /* includePath */ true /* multipleMatches */ );
+ final String JSON = "{'a':123,'array':[1,2],'ob':{'value0':2,'value':3,'value2':4},'b':true}";
+ writeJsonDoc(JSON_F, JSON, gen);
+ assertEquals(aposToQuotes("{'ob':{'value0':2,'value2':4}}"), w.toString());
+ }
+
+ public void testMultipleMatchFilteringWithPath2() throws Exception
+ {
+ StringWriter w = new StringWriter();
+
+ JsonGenerator gen = new FilteringGeneratorDelegate(JSON_F.createGenerator(w),
+ new NameMatchFilter("array", "b", "value"),
+ true, true);
+ final String JSON = "{'a':123,'array':[1,2],'ob':{'value0':2,'value':3,'value2':4},'b':true}";
+ writeJsonDoc(JSON_F, JSON, gen);
+ assertEquals(aposToQuotes("{'array':[1,2],'ob':{'value':3},'b':true}"), w.toString());
+ }
+
+ public void testMultipleMatchFilteringWithPath3() throws Exception
+ {
+ StringWriter w = new StringWriter();
+
+ JsonGenerator gen = new FilteringGeneratorDelegate(JSON_F.createGenerator(w),
+ new NameMatchFilter("value"),
+ true, true);
+ final String JSON = "{'root':{'a0':true,'a':{'value':3},'b':{'value':4}},'b0':false}";
+ writeJsonDoc(JSON_F, JSON, gen);
+ assertEquals(aposToQuotes("{'root':{'a':{'value':3},'b':{'value':4}}}"), w.toString());
+ }
+
+ public void testIndexMatchWithPath1() throws Exception
+ {
+ StringWriter w = new StringWriter();
+ JsonGenerator gen = new FilteringGeneratorDelegate(JSON_F.createGenerator(w),
+ new IndexMatchFilter(1),
+ true, true);
+ final String JSON = "{'a':123,'array':[1,2],'ob':{'value0':2,'value':3,'value2':4},'b':true}";
+ writeJsonDoc(JSON_F, JSON, gen);
+ assertEquals(aposToQuotes("{'array':[2]}"), w.toString());
+
+ w = new StringWriter();
+ gen = new FilteringGeneratorDelegate(JSON_F.createGenerator(w),
+ new IndexMatchFilter(0),
+ true, true);
+ writeJsonDoc(JSON_F, JSON, gen);
+ assertEquals(aposToQuotes("{'array':[1]}"), w.toString());
+ }
+
+ public void testIndexMatchWithPath2() throws Exception
+ {
+ StringWriter w = new StringWriter();
+ JsonGenerator gen = new FilteringGeneratorDelegate(JSON_F.createGenerator(w),
+ new IndexMatchFilter(0,1),
+ true, true);
+ final String JSON = "{'a':123,'array':[1,2],'ob':{'value0':2,'value':3,'value2':4},'b':true}";
+ writeJsonDoc(JSON_F, JSON, gen);
+ assertEquals(aposToQuotes("{'array':[1,2]}"), w.toString());
+ }
+}
diff --git a/src/test/java/com/fasterxml/jackson/core/filter/BasicParserFilteringTest.java b/src/test/java/com/fasterxml/jackson/core/filter/BasicParserFilteringTest.java
new file mode 100644
index 0000000..f5abba9
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/core/filter/BasicParserFilteringTest.java
@@ -0,0 +1,168 @@
+package com.fasterxml.jackson.core.filter;
+
+import java.util.*;
+
+import com.fasterxml.jackson.core.*;
+
+public class BasicParserFilteringTest extends BaseTest
+{
+ static class NameMatchFilter extends TokenFilter
+ {
+ private final Set<String> _names;
+
+ public NameMatchFilter(String... names) {
+ _names = new HashSet<String>(Arrays.asList(names));
+ }
+
+ @Override
+ public TokenFilter includeElement(int index) {
+ return this;
+ }
+
+ @Override
+ public TokenFilter includeProperty(String name) {
+ if (_names.contains(name)) {
+ return TokenFilter.INCLUDE_ALL;
+ }
+ return this;
+ }
+
+ @Override
+ protected boolean _includeScalar() { return false; }
+ }
+
+ static class IndexMatchFilter extends TokenFilter
+ {
+ private final BitSet _indices;
+
+ public IndexMatchFilter(int... ixs) {
+ _indices = new BitSet();
+ for (int ix : ixs) {
+ _indices.set(ix);
+ }
+ }
+
+ @Override
+ public TokenFilter includeProperty(String name) {
+ return this;
+ }
+
+ @Override
+ public TokenFilter includeElement(int index) {
+ if (_indices.get(index)) {
+ return TokenFilter.INCLUDE_ALL;
+ }
+ return null;
+ }
+
+ @Override
+ protected boolean _includeScalar() { return false; }
+ }
+
+ /*
+ /**********************************************************
+ /* Test methods
+ /**********************************************************
+ */
+
+ private final JsonFactory JSON_F = new JsonFactory();
+
+ private final String SIMPLE = aposToQuotes("{'a':123,'array':[1,2],'ob':{'value0':2,'value':3,'value2':4},'b':true}");
+
+ @SuppressWarnings("resource")
+ public void testNonFiltering() throws Exception
+ {
+ JsonParser p = JSON_F.createParser(SIMPLE);
+ String result = readAndWrite(JSON_F, p);
+ assertEquals(SIMPLE, result);
+ }
+
+ @SuppressWarnings("resource")
+ public void testSingleMatchFilteringWithoutPath() throws Exception
+ {
+ JsonParser p0 = JSON_F.createParser(SIMPLE);
+ JsonParser p = new FilteringParserDelegate(p0,
+ new NameMatchFilter("value"),
+ false, // includePath
+ false // multipleMatches
+ );
+ String result = readAndWrite(JSON_F, p);
+ assertEquals(aposToQuotes("3"), result);
+ }
+
+ @SuppressWarnings("resource")
+ public void testSingleMatchFilteringWithPath() throws Exception
+ {
+ JsonParser p0 = JSON_F.createParser(SIMPLE);
+ JsonParser p = new FilteringParserDelegate(p0,
+ new NameMatchFilter("value"),
+ true, // includePath
+ false // multipleMatches
+ );
+ String result = readAndWrite(JSON_F, p);
+ assertEquals(aposToQuotes("{'ob':{'value':3}}"), result);
+ }
+
+ @SuppressWarnings("resource")
+ public void testMultipleMatchFilteringWithPath1() throws Exception
+ {
+ JsonParser p0 = JSON_F.createParser(SIMPLE);
+ JsonParser p = new FilteringParserDelegate(p0,
+ new NameMatchFilter("value0", "value2"),
+ true, /* includePath */ true /* multipleMatches */ );
+ String result = readAndWrite(JSON_F, p);
+ assertEquals(aposToQuotes("{'ob':{'value0':2,'value2':4}}"), result);
+ }
+
+ @SuppressWarnings("resource")
+ public void testMultipleMatchFilteringWithPath2() throws Exception
+ {
+ String INPUT = aposToQuotes("{'a':123,'ob':{'value0':2,'value':3,'value2':4},'b':true}");
+ JsonParser p0 = JSON_F.createParser(INPUT);
+ JsonParser p = new FilteringParserDelegate(p0,
+ new NameMatchFilter("b", "value"),
+ true, true);
+
+ String result = readAndWrite(JSON_F, p);
+ assertEquals(aposToQuotes("{'ob':{'value':3},'b':true}"), result);
+ }
+
+ @SuppressWarnings("resource")
+ public void testMultipleMatchFilteringWithPath3() throws Exception
+ {
+ final String JSON = aposToQuotes("{'root':{'a0':true,'a':{'value':3},'b':{'value':4}},'b0':false}");
+ JsonParser p0 = JSON_F.createParser(JSON);
+ JsonParser p = new FilteringParserDelegate(p0,
+ new NameMatchFilter("value"),
+ true, true);
+ String result = readAndWrite(JSON_F, p);
+ assertEquals(aposToQuotes("{'root':{'a':{'value':3},'b':{'value':4}}}"), result);
+ }
+
+ @SuppressWarnings("resource")
+ public void testIndexMatchWithPath1() throws Exception
+ {
+ JsonParser p = new FilteringParserDelegate(JSON_F.createParser(SIMPLE),
+ new IndexMatchFilter(1), true, true);
+ String result = readAndWrite(JSON_F, p);
+ assertEquals(aposToQuotes("{'array':[2]}"), result);
+
+ p = new FilteringParserDelegate(JSON_F.createParser(SIMPLE),
+ new IndexMatchFilter(0), true, true);
+ result = readAndWrite(JSON_F, p);
+ assertEquals(aposToQuotes("{'array':[1]}"), result);
+ }
+
+ @SuppressWarnings("resource")
+ public void testIndexMatchWithPath2() throws Exception
+ {
+ JsonParser p = new FilteringParserDelegate(JSON_F.createParser(SIMPLE),
+ new IndexMatchFilter(0, 1), true, true);
+ assertEquals(aposToQuotes("{'array':[1,2]}"), readAndWrite(JSON_F, p));
+
+ String JSON = aposToQuotes("{'a':123,'array':[1,2,3,4,5],'b':[1,2,3]}");
+ p = new FilteringParserDelegate(JSON_F.createParser(JSON),
+ new IndexMatchFilter(1, 3), true, true);
+ assertEquals(aposToQuotes("{'array':[2,4],'b':[2]}"), readAndWrite(JSON_F, p));
+ }
+}
diff --git a/src/test/java/com/fasterxml/jackson/core/filter/JsonPointerGeneratorFilteringTest.java b/src/test/java/com/fasterxml/jackson/core/filter/JsonPointerGeneratorFilteringTest.java
new file mode 100644
index 0000000..0113a30
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/core/filter/JsonPointerGeneratorFilteringTest.java
@@ -0,0 +1,111 @@
+package com.fasterxml.jackson.core.filter;
+
+import java.io.*;
+
+import com.fasterxml.jackson.core.*;
+
+@SuppressWarnings("resource")
+public class JsonPointerGeneratorFilteringTest extends com.fasterxml.jackson.core.BaseTest
+{
+ private final JsonFactory JSON_F = new JsonFactory();
+
+ final String SIMPLE_INPUT = aposToQuotes("{'a':1,'b':[1,2,3],'c':{'d':{'a':true}},'d':null}");
+
+ public void testSimplePropertyWithPath() throws Exception
+ {
+ _assert(SIMPLE_INPUT, "/c", true, "{'c':{'d':{'a':true}}}");
+ _assert(SIMPLE_INPUT, "/c/d", true, "{'c':{'d':{'a':true}}}");
+ _assert(SIMPLE_INPUT, "/c/d/a", true, "{'c':{'d':{'a':true}}}");
+
+ _assert(SIMPLE_INPUT, "/c/d/a", true, "{'c':{'d':{'a':true}}}");
+
+ _assert(SIMPLE_INPUT, "/a", true, "{'a':1}");
+ _assert(SIMPLE_INPUT, "/d", true, "{'d':null}");
+
+ // and then non-match
+ _assert(SIMPLE_INPUT, "/x", true, "");
+ }
+
+ public void testSimplePropertyWithoutPath() throws Exception
+ {
+ _assert(SIMPLE_INPUT, "/c", false, "{'d':{'a':true}}");
+ _assert(SIMPLE_INPUT, "/c/d", false, "{'a':true}");
+ _assert(SIMPLE_INPUT, "/c/d/a", false, "true");
+
+ _assert(SIMPLE_INPUT, "/a", false, "1");
+ _assert(SIMPLE_INPUT, "/d", false, "null");
+
+ // and then non-match
+ _assert(SIMPLE_INPUT, "/x", false, "");
+ }
+
+ public void testArrayElementWithPath() throws Exception
+ {
+ _assert(SIMPLE_INPUT, "/b", true, "{'b':[1,2,3]}");
+ _assert(SIMPLE_INPUT, "/b/1", true, "{'b':[2]}");
+ _assert(SIMPLE_INPUT, "/b/2", true, "{'b':[3]}");
+
+ // and then non-match
+ _assert(SIMPLE_INPUT, "/b/8", true, "");
+ }
+
+ public void testArrayNestedWithPath() throws Exception
+ {
+ _assert("{'a':[true,{'b':3,'d':2},false]}", "/a/1/b", true, "{'a':[{'b':3}]}");
+ _assert("[true,[1]]", "/0", true, "[true]");
+ _assert("[true,[1]]", "/1", true, "[[1]]");
+ _assert("[true,[1,2,[true],3],0]", "/0", true, "[true]");
+ _assert("[true,[1,2,[true],3],0]", "/1", true, "[[1,2,[true],3]]");
+
+ _assert("[true,[1,2,[true],3],0]", "/1/2", true, "[[[true]]]");
+ _assert("[true,[1,2,[true],3],0]", "/1/2/0", true, "[[[true]]]");
+ _assert("[true,[1,2,[true],3],0]", "/1/3/0", true, "");
+ }
+
+ public void testArrayNestedWithoutPath() throws Exception
+ {
+ _assert("{'a':[true,{'b':3,'d':2},false]}", "/a/1/b", false, "3");
+ _assert("[true,[1,2,[true],3],0]", "/0", false, "true");
+ _assert("[true,[1,2,[true],3],0]", "/1", false,
+ "[1,2,[true],3]");
+
+ _assert("[true,[1,2,[true],3],0]", "/1/2", false, "[true]");
+ _assert("[true,[1,2,[true],3],0]", "/1/2/0", false, "true");
+ _assert("[true,[1,2,[true],3],0]", "/1/3/0", false, "");
+ }
+
+// final String SIMPLE_INPUT = aposToQuotes("{'a':1,'b':[1,2,3],'c':{'d':{'a':true}},'d':null}");
+
+ public void testArrayElementWithoutPath() throws Exception
+ {
+ _assert(SIMPLE_INPUT, "/b", false, "[1,2,3]");
+ _assert(SIMPLE_INPUT, "/b/1", false, "2");
+ _assert(SIMPLE_INPUT, "/b/2", false, "3");
+
+ _assert(SIMPLE_INPUT, "/b/8", false, "");
+
+ // and then non-match
+ _assert(SIMPLE_INPUT, "/x", false, "");
+ }
+
+ private void _assert(String input, String pathExpr, boolean includeParent, String exp)
+ throws Exception
+ {
+ StringWriter w = new StringWriter();
+
+ JsonGenerator g0 = JSON_F.createGenerator(w);
+ FilteringGeneratorDelegate g = new FilteringGeneratorDelegate(g0,
+ new JsonPointerBasedFilter(pathExpr),
+ includeParent, false);
+
+ try {
+ writeJsonDoc(JSON_F, input, g);
+ } catch (Exception e) {
+ g0.flush();
+ System.err.println("With input '"+input+"', output at point of failure: <"+w+">");
+ throw e;
+ }
+
+ assertEquals(aposToQuotes(exp), w.toString());
+ }
+}
diff --git a/src/test/java/com/fasterxml/jackson/core/filter/JsonPointerParserFilteringTest.java b/src/test/java/com/fasterxml/jackson/core/filter/JsonPointerParserFilteringTest.java
new file mode 100644
index 0000000..58ecca7
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/core/filter/JsonPointerParserFilteringTest.java
@@ -0,0 +1,83 @@
+package com.fasterxml.jackson.core.filter;
+
+import java.io.StringWriter;
+
+import com.fasterxml.jackson.core.*;
+
+public class JsonPointerParserFilteringTest extends com.fasterxml.jackson.core.BaseTest
+{
+ private final JsonFactory JSON_F = new JsonFactory();
+
+ final String SIMPLEST_INPUT = aposToQuotes("{'a':1,'b':2,'c':3}");
+
+ final String SIMPLE_INPUT = aposToQuotes("{'a':1,'b':[1,2,3],'c':{'d':{'a':true}},'d':null}");
+
+ public void testSimplestWithPath() throws Exception
+ {
+ _assert(SIMPLEST_INPUT, "/a", true, "{'a':1}");
+ _assert(SIMPLEST_INPUT, "/b", true, "{'b':2}");
+ _assert(SIMPLEST_INPUT, "/c", true, "{'c':3}");
+ _assert(SIMPLEST_INPUT, "/c/0", true, "");
+ _assert(SIMPLEST_INPUT, "/d", true, "");
+ }
+
+ public void testSimplestNoPath() throws Exception
+ {
+ _assert(SIMPLEST_INPUT, "/a", false, "1");
+ _assert(SIMPLEST_INPUT, "/b", false, "2");
+ _assert(SIMPLEST_INPUT, "/b/2", false, "");
+ _assert(SIMPLEST_INPUT, "/c", false, "3");
+ _assert(SIMPLEST_INPUT, "/d", false, "");
+ }
+
+ public void testSimpleWithPath() throws Exception
+ {
+ _assert(SIMPLE_INPUT, "/c", true, "{'c':{'d':{'a':true}}}");
+ _assert(SIMPLE_INPUT, "/c/d", true, "{'c':{'d':{'a':true}}}");
+ _assert(SIMPLE_INPUT, "/a", true, "{'a':1}");
+ _assert(SIMPLE_INPUT, "/b", true, "{'b':[1,2,3]}");
+ _assert(SIMPLE_INPUT, "/b/0", true, "{'b':[1]}");
+ _assert(SIMPLE_INPUT, "/b/1", true, "{'b':[2]}");
+ _assert(SIMPLE_INPUT, "/b/2", true, "{'b':[3]}");
+ _assert(SIMPLE_INPUT, "/b/3", true, "");
+ }
+
+ public void testSimpleNoPath() throws Exception
+ {
+ _assert(SIMPLE_INPUT, "/c", false, "{'d':{'a':true}}");
+
+ _assert(SIMPLE_INPUT, "/c/d", false, "{'a':true}");
+ _assert(SIMPLE_INPUT, "/a", false, "1");
+ _assert(SIMPLE_INPUT, "/b", false, "[1,2,3]");
+ _assert(SIMPLE_INPUT, "/b/0", false, "1");
+ _assert(SIMPLE_INPUT, "/b/1", false, "2");
+ _assert(SIMPLE_INPUT, "/b/2", false, "3");
+ _assert(SIMPLE_INPUT, "/b/3", false, "");
+ }
+
+ @SuppressWarnings("resource")
+ void _assert(String input, String pathExpr, boolean includeParent, String exp)
+ throws Exception
+ {
+ JsonParser p0 = JSON_F.createParser(input);
+ FilteringParserDelegate p = new FilteringParserDelegate(p0,
+ new JsonPointerBasedFilter(pathExpr),
+ includeParent, false);
+ StringWriter w = new StringWriter();
+ JsonGenerator g = JSON_F.createGenerator(w);
+
+ try {
+ while (p.nextToken() != null) {
+ g.copyCurrentEvent(p);
+ }
+ p.close();
+ g.close();
+ } catch (Exception e) {
+ g.flush();
+ System.err.println("With input '"+input+"', output at point of failure: <"+w+">");
+ throw e;
+ }
+
+ assertEquals(aposToQuotes(exp), w.toString());
+ }
+}
diff --git a/src/test/java/com/fasterxml/jackson/core/io/SegmentedStringWriterTest.java b/src/test/java/com/fasterxml/jackson/core/io/SegmentedStringWriterTest.java
new file mode 100644
index 0000000..08dfddd
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/core/io/SegmentedStringWriterTest.java
@@ -0,0 +1,48 @@
+package com.fasterxml.jackson.core.io;
+
+import com.fasterxml.jackson.core.io.SegmentedStringWriter;
+import com.fasterxml.jackson.core.util.BufferRecycler;
+
+public class SegmentedStringWriterTest
+ extends com.fasterxml.jackson.core.BaseTest
+{
+ public void testSimple() throws Exception
+ {
+ BufferRecycler br = new BufferRecycler();
+ SegmentedStringWriter w = new SegmentedStringWriter(br);
+
+ StringBuilder exp = new StringBuilder();
+
+ for (int i = 0; exp.length() < 100; ++i) {
+ String nr = String.valueOf(i);
+ exp.append(' ').append(nr);
+ w.append(' ');
+ switch (i % 4) {
+ case 0:
+ w.append(nr);
+ break;
+ case 1:
+ {
+ String str = " "+nr;
+ w.append(str, 2, str.length());
+ }
+ break;
+ case 2:
+ w.write(nr.toCharArray());
+ break;
+ default:
+ {
+ char[] ch = (" "+nr+" ").toCharArray();
+ w.write(ch, 1, nr.length());
+ }
+ break;
+ }
+ }
+ // flush, close are nops but trigger just for fun
+ w.flush();
+ w.close();
+
+ String act = w.getAndClear();
+ assertEquals(exp.toString(), act);
+ }
+}
diff --git a/src/test/java/com/fasterxml/jackson/core/io/TestUTF8Writer.java b/src/test/java/com/fasterxml/jackson/core/io/UTF8WriterTest.java
similarity index 70%
rename from src/test/java/com/fasterxml/jackson/core/io/TestUTF8Writer.java
rename to src/test/java/com/fasterxml/jackson/core/io/UTF8WriterTest.java
index a08a1ea..32d1c39 100644
--- a/src/test/java/com/fasterxml/jackson/core/io/TestUTF8Writer.java
+++ b/src/test/java/com/fasterxml/jackson/core/io/UTF8WriterTest.java
@@ -6,7 +6,7 @@
import com.fasterxml.jackson.core.io.UTF8Writer;
import com.fasterxml.jackson.core.util.BufferRecycler;
-public class TestUTF8Writer
+public class UTF8WriterTest
extends com.fasterxml.jackson.core.BaseTest
{
public void testSimple() throws Exception
@@ -25,6 +25,7 @@
w.append(ch[0]);
w.write(ch[1]);
w.write(ch, 2, 3);
+ w.flush();
w.write(str, 0, str.length());
w.close();
@@ -39,6 +40,26 @@
assertEquals(str+str+str, act);
}
+ public void testSimpleAscii() throws Exception
+ {
+ BufferRecycler rec = new BufferRecycler();
+ IOContext ctxt = new IOContext(rec, null, false);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ UTF8Writer w = new UTF8Writer(ctxt, out);
+
+ String str = "abcdefghijklmnopqrst\u00A0";
+ char[] ch = str.toCharArray();
+
+ w.write(ch, 0, ch.length);
+ w.close();
+
+ byte[] data = out.toByteArray();
+ // one 2-byte encoded char
+ assertEquals(ch.length+1, data.length);
+ String act = out.toString("UTF-8");
+ assertEquals(str, act);
+ }
+
public void testFlushAfterClose() throws Exception
{
BufferRecycler rec = new BufferRecycler();
diff --git a/src/test/java/com/fasterxml/jackson/core/json/TestJsonGenerator.java b/src/test/java/com/fasterxml/jackson/core/json/GeneratorBasicTest.java
similarity index 79%
rename from src/test/java/com/fasterxml/jackson/core/json/TestJsonGenerator.java
rename to src/test/java/com/fasterxml/jackson/core/json/GeneratorBasicTest.java
index 27312e2..7b159a9 100644
--- a/src/test/java/com/fasterxml/jackson/core/json/TestJsonGenerator.java
+++ b/src/test/java/com/fasterxml/jackson/core/json/GeneratorBasicTest.java
@@ -8,14 +8,15 @@
* Set of basic unit tests for verifying that the basic generator
* functionality works as expected.
*/
-public class TestJsonGenerator
+public class GeneratorBasicTest
extends com.fasterxml.jackson.core.BaseTest
{
+ private final JsonFactory JSON_F = new JsonFactory();
+
// // // First, tests for primitive (non-structured) values
public void testStringWrite() throws Exception
{
- JsonFactory jf = new JsonFactory();
String[] inputStrings = new String[] { "", "X", "1234567890" };
for (int useReader = 0; useReader < 2; ++useReader) {
for (int writeString = 0; writeString < 2; ++writeString) {
@@ -24,9 +25,9 @@
JsonGenerator gen;
ByteArrayOutputStream bout = new ByteArrayOutputStream();
if (useReader != 0) {
- gen = jf.createGenerator(new OutputStreamWriter(bout, "UTF-8"));
+ gen = JSON_F.createGenerator(new OutputStreamWriter(bout, "UTF-8"));
} else {
- gen = jf.createGenerator(bout, JsonEncoding.UTF8);
+ gen = JSON_F.createGenerator(bout, JsonEncoding.UTF8);
}
if (writeString > 0) {
gen.writeString(input);
@@ -39,7 +40,7 @@
}
gen.flush();
gen.close();
- JsonParser jp = jf.createParser(new ByteArrayInputStream(bout.toByteArray()));
+ JsonParser jp = JSON_F.createParser(new ByteArrayInputStream(bout.toByteArray()));
JsonToken t = jp.nextToken();
assertNotNull("Document \""+bout.toString("UTF-8")+"\" yielded no tokens", t);
@@ -52,16 +53,16 @@
}
}
- public void testIntWrite() throws Exception
+ public void testIntValueWrite() throws Exception
{
- doTestIntWrite(false);
- doTestIntWrite(true);
+ doTestIntValueWrite(false);
+ doTestIntValueWrite(true);
}
- public void testLongWrite() throws Exception
+ public void testLongValueWrite() throws Exception
{
- doTestLongWrite(false);
- doTestLongWrite(true);
+ doTestLongValueWrite(false);
+ doTestLongValueWrite(true);
}
public void testBooleanWrite() throws Exception
@@ -70,7 +71,7 @@
boolean state = (i & 1) == 0;
boolean pad = (i & 2) == 0;
StringWriter sw = new StringWriter();
- JsonGenerator gen = new JsonFactory().createGenerator(sw);
+ JsonGenerator gen = JSON_F.createGenerator(sw);
gen.writeBoolean(state);
if (pad) {
gen.writeRaw(" ");
@@ -95,7 +96,7 @@
for (int i = 0; i < 2; ++i) {
boolean pad = (i & 1) == 0;
StringWriter sw = new StringWriter();
- JsonGenerator gen = new JsonFactory().createGenerator(sw);
+ JsonGenerator gen = JSON_F.createGenerator(sw);
gen.writeNull();
if (pad) {
gen.writeRaw(" ");
@@ -120,7 +121,7 @@
throws Exception
{
StringWriter sw = new StringWriter();
- JsonGenerator gen = new JsonFactory().createGenerator(sw);
+ JsonGenerator gen = JSON_F.createGenerator(sw);
gen.writeNumber(1);
gen.writeNumber(2);
gen.writeNumber(-13);
@@ -144,7 +145,7 @@
throws Exception
{
StringWriter sw = new StringWriter();
- JsonGenerator gen = new JsonFactory().createGenerator(sw);
+ JsonGenerator gen = JSON_F.createGenerator(sw);
gen.writeStartObject();
gen.writeNumberField("long", 3L);
gen.writeNumberField("double", 0.25);
@@ -161,7 +162,7 @@
public void testOutputContext() throws Exception
{
StringWriter sw = new StringWriter();
- JsonGenerator gen = new JsonFactory().createGenerator(sw);
+ JsonGenerator gen = JSON_F.createGenerator(sw);
JsonStreamContext ctxt = gen.getOutputContext();
assertTrue(ctxt.inRoot());
@@ -226,35 +227,44 @@
gen.close();
}
- // [core#167]: no error for writing field name twice
- public void testDupFieldNameWrites() throws Exception
+ public void testGetOutputTarget() throws Exception
{
- JsonFactory f = new JsonFactory();
- _testDupFieldNameWrites(f, false);
- _testDupFieldNameWrites(f, true);
+ OutputStream out = new ByteArrayOutputStream();
+ JsonGenerator gen = JSON_F.createGenerator(out);
+ assertSame(out, gen.getOutputTarget());
+ gen.close();
+
+ StringWriter sw = new StringWriter();
+ gen = JSON_F.createGenerator(sw);
+ assertSame(sw, gen.getOutputTarget());
+ gen.close();
}
- private void _testDupFieldNameWrites(JsonFactory f, boolean useReader) throws Exception
+ // for [core#195]
+ public void testGetOutputBufferd() throws Exception
{
- JsonGenerator gen;
- ByteArrayOutputStream bout = new ByteArrayOutputStream();
- if (useReader) {
- gen = f.createGenerator(new OutputStreamWriter(bout, "UTF-8"));
- } else {
- gen = f.createGenerator(bout, JsonEncoding.UTF8);
- }
- gen.writeStartObject();
- gen.writeFieldName("a");
-
- try {
- gen.writeFieldName("b");
- gen.flush();
- String json = bout.toString("UTF-8");
- fail("Should not have let two consequtive field name writes succeed: output = "+json);
- } catch (JsonProcessingException e) {
- verifyException(e, "can not write a field name, expecting a value");
- }
+ OutputStream out = new ByteArrayOutputStream();
+ JsonGenerator gen = JSON_F.createGenerator(out);
+ _testOutputBuffered(gen);
gen.close();
+
+ StringWriter sw = new StringWriter();
+ gen = JSON_F.createGenerator(sw);
+ _testOutputBuffered(gen);
+ gen.close();
+ }
+
+ private void _testOutputBuffered(JsonGenerator gen) throws IOException
+ {
+ gen.writeStartArray(); // 1 byte
+ gen.writeNumber(1234); // 4 bytes
+ assertEquals(5, gen.getOutputBuffered());
+ gen.flush();
+ assertEquals(0, gen.getOutputBuffered());
+ gen.writeEndArray();
+ assertEquals(1, gen.getOutputBuffered());
+ gen.close();
+ assertEquals(0, gen.getOutputBuffered());
}
/*
@@ -262,9 +272,8 @@
/* Internal methods
/**********************************************************
*/
-
- private void doTestIntWrite(boolean pad)
- throws Exception
+
+ private void doTestIntValueWrite(boolean pad) throws Exception
{
int[] VALUES = new int[] {
0, 1, -9, 32, -32, 57, 189, 2017, -9999, 13240, 123456,
@@ -273,7 +282,7 @@
for (int i = 0; i < VALUES.length; ++i) {
int VALUE = VALUES[i];
StringWriter sw = new StringWriter();
- JsonGenerator gen = new JsonFactory().createGenerator(sw);
+ JsonGenerator gen = JSON_F.createGenerator(sw);
gen.writeNumber(VALUE);
if (pad) {
gen.writeRaw(" ");
@@ -295,8 +304,7 @@
}
}
- private void doTestLongWrite(boolean pad)
- throws Exception
+ private void doTestLongValueWrite(boolean pad) throws Exception
{
long[] VALUES = new long[] {
0L, 1L, -1L, -12005002294L, Long.MIN_VALUE, Long.MAX_VALUE
@@ -304,7 +312,7 @@
for (int i = 0; i < VALUES.length; ++i) {
long VALUE = VALUES[i];
StringWriter sw = new StringWriter();
- JsonGenerator gen = new JsonFactory().createGenerator(sw);
+ JsonGenerator gen = JSON_F.createGenerator(sw);
gen.writeNumber(VALUE);
if (pad) {
gen.writeRaw(" ");
@@ -325,3 +333,4 @@
}
}
}
+
diff --git a/src/test/java/com/fasterxml/jackson/core/json/GeneratorFailTest.java b/src/test/java/com/fasterxml/jackson/core/json/GeneratorFailTest.java
new file mode 100644
index 0000000..7b4368b
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/core/json/GeneratorFailTest.java
@@ -0,0 +1,87 @@
+package com.fasterxml.jackson.core.json;
+
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStreamWriter;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+public class GeneratorFailTest
+ extends com.fasterxml.jackson.core.BaseTest
+{
+ private final JsonFactory F = new JsonFactory();
+
+ // [core#167]: no error for writing field name twice
+ public void testDupFieldNameWrites() throws Exception
+ {
+ _testDupFieldNameWrites(F, false);
+ _testDupFieldNameWrites(F, true);
+ }
+
+ // [core#177]
+ // Also: should not try writing JSON String if field name expected
+ // (in future maybe take one as alias... but not yet)
+ public void testFailOnWritingStringNotFieldNameBytes() throws Exception {
+ _testFailOnWritingStringNotFieldName(F, false);
+ }
+
+ // [core#177]
+ public void testFailOnWritingStringNotFieldNameChars() throws Exception {
+ _testFailOnWritingStringNotFieldName(F, true);
+ }
+
+ /*
+ /**********************************************************
+ /* Internal methods
+ /**********************************************************
+ */
+
+ private void _testDupFieldNameWrites(JsonFactory f, boolean useReader) throws Exception
+ {
+ JsonGenerator gen;
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ if (useReader) {
+ gen = f.createGenerator(new OutputStreamWriter(bout, "UTF-8"));
+ } else {
+ gen = f.createGenerator(bout, JsonEncoding.UTF8);
+ }
+ gen.writeStartObject();
+ gen.writeFieldName("a");
+
+ try {
+ gen.writeFieldName("b");
+ gen.flush();
+ String json = bout.toString("UTF-8");
+ fail("Should not have let two consecutive field name writes succeed: output = "+json);
+ } catch (JsonProcessingException e) {
+ verifyException(e, "can not write a field name, expecting a value");
+ }
+ gen.close();
+ }
+
+ private void _testFailOnWritingStringNotFieldName(JsonFactory f, boolean useReader) throws Exception
+ {
+ JsonGenerator gen;
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ if (useReader) {
+ gen = f.createGenerator(new OutputStreamWriter(bout, "UTF-8"));
+ } else {
+ gen = f.createGenerator(bout, JsonEncoding.UTF8);
+ }
+ gen.writeStartObject();
+
+ try {
+ gen.writeString("a");
+ gen.flush();
+ String json = bout.toString("UTF-8");
+ fail("Should not have let "+gen.getClass().getName()+".writeString() be used in place of 'writeFieldName()': output = "+json);
+ } catch (JsonProcessingException e) {
+ verifyException(e, "can not write a String");
+ }
+ gen.close();
+ }
+
+
+}
diff --git a/src/test/java/com/fasterxml/jackson/core/json/JsonFactoryTest.java b/src/test/java/com/fasterxml/jackson/core/json/JsonFactoryTest.java
new file mode 100644
index 0000000..ede5ec1
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/core/json/JsonFactoryTest.java
@@ -0,0 +1,135 @@
+package com.fasterxml.jackson.core.json;
+
+import java.io.*;
+
+import com.fasterxml.jackson.core.*;
+
+public class JsonFactoryTest
+ extends com.fasterxml.jackson.core.BaseTest
+{
+ public void testGeneratorFeatures() throws Exception
+ {
+ JsonFactory f = new JsonFactory();
+ assertNull(f.getCodec());
+
+ f.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, true);
+ assertTrue(f.isEnabled(JsonGenerator.Feature.QUOTE_FIELD_NAMES));
+ f.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false);
+ assertFalse(f.isEnabled(JsonGenerator.Feature.QUOTE_FIELD_NAMES));
+ }
+
+ public void testFactoryFeatures() throws Exception
+ {
+ JsonFactory f = new JsonFactory();
+
+ f.configure(JsonFactory.Feature.INTERN_FIELD_NAMES, true);
+ assertTrue(f.isEnabled(JsonFactory.Feature.INTERN_FIELD_NAMES));
+ f.configure(JsonFactory.Feature.INTERN_FIELD_NAMES, false);
+ assertFalse(f.isEnabled(JsonFactory.Feature.INTERN_FIELD_NAMES));
+
+ // by default, should be enabled
+ assertTrue(f.isEnabled(JsonFactory.Feature.USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING));
+ f.configure(JsonFactory.Feature.USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING, false);
+ assertFalse(f.isEnabled(JsonFactory.Feature.USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING));
+ }
+
+ // for [core#189]: verify that it's ok to disable recycling
+ // Basically simply exercises basic functionality, to ensure
+ // there are no obvious problems; needed since testing never
+ // disables this handling otherwise
+ public void testDisablingBufferRecycling() throws Exception
+ {
+ JsonFactory f = new JsonFactory();
+
+ f.disable(JsonFactory.Feature.USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING);
+
+ // First, generation
+ for (int i = 0; i < 3; ++i) {
+ StringWriter w = new StringWriter();
+ JsonGenerator gen = f.createGenerator(w);
+ gen.writeStartObject();
+ gen.writeEndObject();
+ gen.close();
+ assertEquals("{}", w.toString());
+ }
+
+ for (int i = 0; i < 3; ++i) {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ JsonGenerator gen = f.createGenerator(bytes);
+ gen.writeStartArray();
+ gen.writeEndArray();
+ gen.close();
+ assertEquals("[]", bytes.toString("UTF-8"));
+ }
+
+ // Then parsing:
+ for (int i = 0; i < 3; ++i) {
+ JsonParser p = f.createParser("{}");
+ assertToken(JsonToken.START_OBJECT, p.nextToken());
+ assertToken(JsonToken.END_OBJECT, p.nextToken());
+ assertNull(p.nextToken());
+ p.close();
+
+ p = f.createParser("{}".getBytes("UTF-8"));
+ assertToken(JsonToken.START_OBJECT, p.nextToken());
+ assertToken(JsonToken.END_OBJECT, p.nextToken());
+ assertNull(p.nextToken());
+ p.close();
+ }
+ }
+
+ public void testJsonWithFiles() throws Exception
+ {
+ File file = File.createTempFile("jackson-test", null);
+ file.deleteOnExit();
+
+ JsonFactory f = new JsonFactory();
+
+ // First: create file via generator.. and use an odd encoding
+ JsonGenerator jg = f.createGenerator(file, JsonEncoding.UTF16_LE);
+ jg.writeStartObject();
+ jg.writeRaw(" ");
+ jg.writeEndObject();
+ jg.close();
+
+ // Ok: first read file directly
+ JsonParser jp = f.createParser(file);
+ assertToken(JsonToken.START_OBJECT, jp.nextToken());
+ assertToken(JsonToken.END_OBJECT, jp.nextToken());
+ assertNull(jp.nextToken());
+ jp.close();
+
+ // Then via URL:
+ jp = f.createParser(file.toURI().toURL());
+ assertToken(JsonToken.START_OBJECT, jp.nextToken());
+ assertToken(JsonToken.END_OBJECT, jp.nextToken());
+ assertNull(jp.nextToken());
+ jp.close();
+
+ // ok, delete once we are done
+ file.delete();
+ }
+
+ // #72
+ public void testCopy() throws Exception
+ {
+ JsonFactory jf = new JsonFactory();
+ // first, verify defaults
+ assertTrue(jf.isEnabled(JsonFactory.Feature.INTERN_FIELD_NAMES));
+ assertFalse(jf.isEnabled(JsonParser.Feature.ALLOW_COMMENTS));
+ assertFalse(jf.isEnabled(JsonGenerator.Feature.ESCAPE_NON_ASCII));
+ jf.disable(JsonFactory.Feature.INTERN_FIELD_NAMES);
+ jf.enable(JsonParser.Feature.ALLOW_COMMENTS);
+ jf.enable(JsonGenerator.Feature.ESCAPE_NON_ASCII);
+ // then change, verify that changes "stick"
+ assertFalse(jf.isEnabled(JsonFactory.Feature.INTERN_FIELD_NAMES));
+ assertTrue(jf.isEnabled(JsonParser.Feature.ALLOW_COMMENTS));
+ assertTrue(jf.isEnabled(JsonGenerator.Feature.ESCAPE_NON_ASCII));
+
+ JsonFactory jf2 = jf.copy();
+ assertFalse(jf2.isEnabled(JsonFactory.Feature.INTERN_FIELD_NAMES));
+ assertTrue(jf.isEnabled(JsonParser.Feature.ALLOW_COMMENTS));
+ assertTrue(jf.isEnabled(JsonGenerator.Feature.ESCAPE_NON_ASCII));
+ }
+}
+
diff --git a/src/test/java/com/fasterxml/jackson/core/json/ParserSequenceTest.java b/src/test/java/com/fasterxml/jackson/core/json/ParserSequenceTest.java
new file mode 100644
index 0000000..6b3f4c9
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/core/json/ParserSequenceTest.java
@@ -0,0 +1,52 @@
+package com.fasterxml.jackson.core.json;
+
+import com.fasterxml.jackson.core.*;
+import com.fasterxml.jackson.core.util.JsonParserSequence;
+
+public class ParserSequenceTest
+ extends com.fasterxml.jackson.core.BaseTest
+{
+ private final JsonFactory JSON_FACTORY = new JsonFactory();
+
+ public void testSimple() throws Exception
+ {
+ JsonParser p1 = JSON_FACTORY.createParser("[ 1 ]");
+ JsonParser p2 = JSON_FACTORY.createParser("[ 2 ]");
+ JsonParserSequence seq = JsonParserSequence.createFlattened(p1, p2);
+ assertEquals(2, seq.containedParsersCount());
+
+ assertFalse(p1.isClosed());
+ assertFalse(p2.isClosed());
+ assertFalse(seq.isClosed());
+ assertToken(JsonToken.START_ARRAY, seq.nextToken());
+ assertToken(JsonToken.VALUE_NUMBER_INT, seq.nextToken());
+ assertEquals(1, seq.getIntValue());
+ assertToken(JsonToken.END_ARRAY, seq.nextToken());
+ assertFalse(p1.isClosed());
+ assertFalse(p2.isClosed());
+ assertFalse(seq.isClosed());
+ assertToken(JsonToken.START_ARRAY, seq.nextToken());
+
+ // first parser ought to be closed now
+ assertTrue(p1.isClosed());
+ assertFalse(p2.isClosed());
+ assertFalse(seq.isClosed());
+
+ assertToken(JsonToken.VALUE_NUMBER_INT, seq.nextToken());
+ assertEquals(2, seq.getIntValue());
+ assertToken(JsonToken.END_ARRAY, seq.nextToken());
+ assertTrue(p1.isClosed());
+ assertFalse(p2.isClosed());
+ assertFalse(seq.isClosed());
+
+ assertNull(seq.nextToken());
+ assertTrue(p1.isClosed());
+ assertTrue(p2.isClosed());
+ assertTrue(seq.isClosed());
+
+ seq.close();
+ // redundant, but call to remove IDE warnings
+ p1.close();
+ p2.close();
+ }
+}
diff --git a/src/test/java/com/fasterxml/jackson/core/main/TestStringGeneration.java b/src/test/java/com/fasterxml/jackson/core/json/StringGenerationTest.java
similarity index 76%
rename from src/test/java/com/fasterxml/jackson/core/main/TestStringGeneration.java
rename to src/test/java/com/fasterxml/jackson/core/json/StringGenerationTest.java
index ed167e6..b36cbe6 100644
--- a/src/test/java/com/fasterxml/jackson/core/main/TestStringGeneration.java
+++ b/src/test/java/com/fasterxml/jackson/core/json/StringGenerationTest.java
@@ -1,4 +1,4 @@
-package com.fasterxml.jackson.core.main;
+package com.fasterxml.jackson.core.json;
import java.io.*;
@@ -10,7 +10,7 @@
* Set of basic unit tests for verifying that the string
* generation, including character escaping, works as expected.
*/
-public class TestStringGeneration
+public class StringGenerationTest
extends BaseTest
{
final static String[] SAMPLES = new String[] {
@@ -23,15 +23,33 @@
private final JsonFactory FACTORY = new JsonFactory();
- public void testBasicEscaping()
- throws Exception
+ public void testBasicEscaping() throws Exception
{
doTestBasicEscaping(false);
doTestBasicEscaping(true);
}
- public void testLongerRandomSingleChunk()
- throws Exception
+ // for [core#194]
+ public void testMediumStringsBytes() throws Exception
+ {
+ _testMediumStrings(true, 1100);
+ _testMediumStrings(true, 2300);
+ _testMediumStrings(true, 3800);
+ _testMediumStrings(true, 7500);
+ _testMediumStrings(true, 19000);
+ }
+
+ // for [core#194]
+ public void testMediumStringsChars() throws Exception
+ {
+ _testMediumStrings(false, 1100);
+ _testMediumStrings(false, 2300);
+ _testMediumStrings(false, 3800);
+ _testMediumStrings(false, 7500);
+ _testMediumStrings(false, 19000);
+ }
+
+ public void testLongerRandomSingleChunk() throws Exception
{
/* Let's first generate 100k of pseudo-random characters, favoring
* 7-bit ascii range
@@ -43,8 +61,7 @@
}
}
- public void testLongerRandomMultiChunk()
- throws Exception
+ public void testLongerRandomMultiChunk() throws Exception
{
/* Let's first generate 100k of pseudo-random characters, favoring
* 7-bit ascii range
@@ -62,6 +79,29 @@
/**********************************************************
*/
+ private String _generareMediumText(int minLen)
+ {
+ StringBuilder sb = new StringBuilder(minLen + 1000);
+ Random rnd = new Random(minLen);
+ do {
+ switch (rnd.nextInt() % 4) {
+ case 0:
+ sb.append(" foo");
+ break;
+ case 1:
+ sb.append(" bar");
+ break;
+ case 2:
+ sb.append(String.valueOf(sb.length()));
+ break;
+ default:
+ sb.append(" \"stuff\"");
+ break;
+ }
+ } while (sb.length() < minLen);
+ return sb.toString();
+ }
+
private String generateRandom(int len)
{
StringBuilder sb = new StringBuilder(len+1000); // pad for surrogates
@@ -84,8 +124,36 @@
}
}
return sb.toString();
- }
+ }
+ private void _testMediumStrings(boolean useBinary, int length) throws Exception
+ {
+ String text = _generareMediumText(length);
+ StringWriter sw = new StringWriter();
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ JsonGenerator gen = useBinary ? FACTORY.createGenerator(bytes)
+ : FACTORY.createGenerator(sw);
+ gen.writeStartArray();
+ gen.writeString(text);
+ gen.writeEndArray();
+ gen.close();
+
+ String json;
+ if (useBinary) {
+ json = bytes.toString("UTF-8");
+ } else {
+ json = sw.toString();
+ }
+
+ JsonParser p = FACTORY.createParser(json);
+ assertToken(JsonToken.START_ARRAY, p.nextToken());
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals(text, p.getText());
+ assertToken(JsonToken.END_ARRAY, p.nextToken());
+ p.close();
+ }
+
private void doTestBasicEscaping(boolean charArray)
throws Exception
{
diff --git a/src/test/java/com/fasterxml/jackson/core/json/TestJsonFactory.java b/src/test/java/com/fasterxml/jackson/core/json/TestJsonFactory.java
deleted file mode 100644
index d29651f..0000000
--- a/src/test/java/com/fasterxml/jackson/core/json/TestJsonFactory.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package com.fasterxml.jackson.core.json;
-
-import java.io.*;
-
-import com.fasterxml.jackson.core.*;
-
-public class TestJsonFactory
- extends com.fasterxml.jackson.core.BaseTest
-{
- public void testGeneratorFeatures() throws Exception
- {
- JsonFactory f = new JsonFactory();
- assertNull(f.getCodec());
-
- f.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, true);
- assertTrue(f.isEnabled(JsonGenerator.Feature.QUOTE_FIELD_NAMES));
- f.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false);
- assertFalse(f.isEnabled(JsonGenerator.Feature.QUOTE_FIELD_NAMES));
- }
-
- public void testParserFeatures() throws Exception
- {
- JsonFactory f = new JsonFactory();
- assertNull(f.getCodec());
-
- f.configure(JsonFactory.Feature.INTERN_FIELD_NAMES, true);
- assertTrue(f.isEnabled(JsonFactory.Feature.INTERN_FIELD_NAMES));
- f.configure(JsonFactory.Feature.INTERN_FIELD_NAMES, false);
- assertFalse(f.isEnabled(JsonFactory.Feature.INTERN_FIELD_NAMES));
- }
-
- public void testJsonWithFiles() throws Exception
- {
- File file = File.createTempFile("jackson-test", null);
- file.deleteOnExit();
-
- JsonFactory f = new JsonFactory();
-
- // First: create file via generator.. and use an odd encoding
- JsonGenerator jg = f.createGenerator(file, JsonEncoding.UTF16_LE);
- jg.writeStartObject();
- jg.writeRaw(" ");
- jg.writeEndObject();
- jg.close();
-
- // Ok: first read file directly
- JsonParser jp = f.createParser(file);
- assertToken(JsonToken.START_OBJECT, jp.nextToken());
- assertToken(JsonToken.END_OBJECT, jp.nextToken());
- assertNull(jp.nextToken());
- jp.close();
-
- // Then via URL:
- jp = f.createParser(file.toURI().toURL());
- assertToken(JsonToken.START_OBJECT, jp.nextToken());
- assertToken(JsonToken.END_OBJECT, jp.nextToken());
- assertNull(jp.nextToken());
- jp.close();
-
- // ok, delete once we are done
- file.delete();
- }
-
- // #72
- public void testCopy() throws Exception
- {
- JsonFactory jf = new JsonFactory();
- // first, verify defaults
- assertTrue(jf.isEnabled(JsonFactory.Feature.INTERN_FIELD_NAMES));
- assertFalse(jf.isEnabled(JsonParser.Feature.ALLOW_COMMENTS));
- assertFalse(jf.isEnabled(JsonGenerator.Feature.ESCAPE_NON_ASCII));
- jf.disable(JsonFactory.Feature.INTERN_FIELD_NAMES);
- jf.enable(JsonParser.Feature.ALLOW_COMMENTS);
- jf.enable(JsonGenerator.Feature.ESCAPE_NON_ASCII);
- // then change, verify that changes "stick"
- assertFalse(jf.isEnabled(JsonFactory.Feature.INTERN_FIELD_NAMES));
- assertTrue(jf.isEnabled(JsonParser.Feature.ALLOW_COMMENTS));
- assertTrue(jf.isEnabled(JsonGenerator.Feature.ESCAPE_NON_ASCII));
-
- JsonFactory jf2 = jf.copy();
- assertFalse(jf2.isEnabled(JsonFactory.Feature.INTERN_FIELD_NAMES));
- assertTrue(jf.isEnabled(JsonParser.Feature.ALLOW_COMMENTS));
- assertTrue(jf.isEnabled(JsonGenerator.Feature.ESCAPE_NON_ASCII));
- }
-}
-
diff --git a/src/test/java/com/fasterxml/jackson/core/json/TestJsonParser.java b/src/test/java/com/fasterxml/jackson/core/json/TestJsonParser.java
index ea9410d..d4dead5 100644
--- a/src/test/java/com/fasterxml/jackson/core/json/TestJsonParser.java
+++ b/src/test/java/com/fasterxml/jackson/core/json/TestJsonParser.java
@@ -1,6 +1,7 @@
package com.fasterxml.jackson.core.json;
import com.fasterxml.jackson.core.*;
+import com.fasterxml.jackson.core.util.JsonParserDelegate;
import java.io.*;
import java.net.URL;
@@ -510,7 +511,65 @@
}
jp.close();
}
+
+ public void testGetValueAsTextBytes() throws Exception
+ {
+ JsonFactory f = new JsonFactory();
+ _testGetValueAsText(f, true, false);
+ _testGetValueAsText(f, true, true);
+ }
+
+ public void testGetValueAsTextChars() throws Exception
+ {
+ JsonFactory f = new JsonFactory();
+ _testGetValueAsText(f, false, false);
+ _testGetValueAsText(f, false, true);
+ }
+ @SuppressWarnings("resource")
+ private void _testGetValueAsText(JsonFactory f,
+ boolean useBytes, boolean delegate) throws Exception
+ {
+ String JSON = "{\"a\":1,\"b\":true,\"c\":null,\"d\":\"foo\"}";
+ JsonParser p = useBytes ? f.createParser(JSON.getBytes("UTF-8"))
+ : f.createParser(JSON);
+
+ if (delegate) {
+ p = new JsonParserDelegate(p);
+ }
+
+ assertToken(JsonToken.START_OBJECT, p.nextToken());
+ assertNull(p.getValueAsString());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("a", p.getText());
+ assertEquals("a", p.getValueAsString());
+ assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
+ assertEquals("1", p.getValueAsString());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("b", p.getValueAsString());
+ assertToken(JsonToken.VALUE_TRUE, p.nextToken());
+ assertEquals("true", p.getValueAsString());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("c", p.getValueAsString());
+ assertToken(JsonToken.VALUE_NULL, p.nextToken());
+ // null token returned as Java null, as per javadoc
+ assertNull(p.getValueAsString());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("d", p.getValueAsString());
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("foo", p.getValueAsString());
+
+ assertToken(JsonToken.END_OBJECT, p.nextToken());
+ assertNull(p.getValueAsString());
+
+ assertNull(p.nextToken());
+ p.close();
+ }
+
/*
/**********************************************************
/* Helper methods
diff --git a/src/test/java/com/fasterxml/jackson/core/json/TestNextXxx.java b/src/test/java/com/fasterxml/jackson/core/json/TestNextXxx.java
index 8a95c42..feb8560 100644
--- a/src/test/java/com/fasterxml/jackson/core/json/TestNextXxx.java
+++ b/src/test/java/com/fasterxml/jackson/core/json/TestNextXxx.java
@@ -19,6 +19,8 @@
/********************************************************
*/
+ private final JsonFactory JSON_F = new JsonFactory();
+
public void testIsNextTokenName() throws Exception
{
_testIsNextTokenName1(false);
@@ -29,21 +31,14 @@
_testIsNextTokenName3(true);
}
- // for [core#220]: problem with `nextFieldName(str)`, indented content
- public void testNextNameWithIndentation() throws Exception
- {
- _testNextFieldNameIndent(false);
- _testNextFieldNameIndent(true);
- }
-
- // [core#34]
+ // [jackson-core#34]
public void testIssue34() throws Exception
{
_testIssue34(false);
_testIssue34(true);
}
- // [core#38] with nextFieldName
+ // [jackson-core#38] with nextFieldName
public void testIssue38() throws Exception
{
_testIssue38(false);
@@ -52,12 +47,41 @@
public void testNextNameWithLongContent() throws Exception
{
- final JsonFactory jf = new JsonFactory();
-
- _testLong(jf, false);
- _testLong(jf, true);
+ _testNextNameWithLong(false);
+ _testNextNameWithLong(true);
}
+ // for [core#220]: problem with `nextFieldName(str)`, indented content
+ public void testNextNameWithIndentation() throws Exception
+ {
+ _testNextFieldNameIndent(false);
+ _testNextFieldNameIndent(true);
+ }
+
+ public void testNextTextValue() throws Exception
+ {
+ _textNextText(false);
+ _textNextText(true);
+ }
+
+ public void testNextIntValue() throws Exception
+ {
+ _textNextInt(false);
+ _textNextInt(true);
+ }
+
+ public void testNextLongValue() throws Exception
+ {
+ _textNextLong(false);
+ _textNextLong(true);
+ }
+
+ public void testNextBooleanValue() throws Exception
+ {
+ _textNextBoolean(false);
+ _textNextBoolean(true);
+ }
+
/*
/********************************************************
/* Actual test code
@@ -138,10 +162,9 @@
private void _testIsNextTokenName2(boolean useStream) throws Exception
{
final String DOC = "{\"name\":123,\"name2\":14,\"x\":\"name\"}";
- JsonFactory jf = new JsonFactory();
JsonParser jp = useStream ?
- jf.createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8")))
- : jf.createParser(new StringReader(DOC));
+ JSON_F.createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8")))
+ : JSON_F.createParser(new StringReader(DOC));
SerializableString NAME = new SerializedString("name");
assertFalse(jp.nextFieldName(NAME));
assertToken(JsonToken.START_OBJECT, jp.getCurrentToken());
@@ -177,48 +200,46 @@
private void _testIsNextTokenName3(boolean useStream) throws Exception
{
final String DOC = "{\"name\":123,\"name2\":14,\"x\":\"name\"}";
- JsonFactory jf = new JsonFactory();
- JsonParser jp = useStream ?
- jf.createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8")))
- : jf.createParser(new StringReader(DOC));
- assertNull(jp.nextFieldName());
- assertToken(JsonToken.START_OBJECT, jp.getCurrentToken());
- assertEquals("name", jp.nextFieldName());
- assertToken(JsonToken.FIELD_NAME, jp.getCurrentToken());
- assertEquals("name", jp.getCurrentName());
- assertEquals("name", jp.getText());
- assertNull(jp.nextFieldName());
- assertToken(JsonToken.VALUE_NUMBER_INT, jp.getCurrentToken());
- assertEquals(123, jp.getIntValue());
+ JsonParser p = useStream ?
+ JSON_F.createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8")))
+ : JSON_F.createParser(new StringReader(DOC));
+ assertNull(p.nextFieldName());
+ assertToken(JsonToken.START_OBJECT, p.getCurrentToken());
+ assertEquals("name", p.nextFieldName());
+ assertToken(JsonToken.FIELD_NAME, p.getCurrentToken());
+ assertEquals("name", p.getCurrentName());
+ assertEquals("name", p.getText());
+ assertNull(p.nextFieldName());
+ assertToken(JsonToken.VALUE_NUMBER_INT, p.getCurrentToken());
+ assertEquals(123, p.getIntValue());
- assertEquals("name2", jp.nextFieldName());
- assertToken(JsonToken.FIELD_NAME, jp.getCurrentToken());
- assertEquals("name2", jp.getCurrentName());
- assertToken(JsonToken.VALUE_NUMBER_INT, jp.nextToken());
+ assertEquals("name2", p.nextFieldName());
+ assertToken(JsonToken.FIELD_NAME, p.getCurrentToken());
+ assertEquals("name2", p.getCurrentName());
+ assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
- assertEquals("x", jp.nextFieldName());
- assertToken(JsonToken.FIELD_NAME, jp.getCurrentToken());
- assertEquals("x", jp.getCurrentName());
+ assertEquals("x", p.nextFieldName());
+ assertToken(JsonToken.FIELD_NAME, p.getCurrentToken());
+ assertEquals("x", p.getCurrentName());
- assertNull(jp.nextFieldName());
- assertToken(JsonToken.VALUE_STRING, jp.getCurrentToken());
+ assertNull(p.nextFieldName());
+ assertToken(JsonToken.VALUE_STRING, p.getCurrentToken());
- assertNull(jp.nextFieldName());
- assertToken(JsonToken.END_OBJECT, jp.getCurrentToken());
+ assertNull(p.nextFieldName());
+ assertToken(JsonToken.END_OBJECT, p.getCurrentToken());
- assertNull(jp.nextFieldName());
- assertNull(jp.getCurrentToken());
+ assertNull(p.nextFieldName());
+ assertNull(p.getCurrentToken());
- jp.close();
+ p.close();
}
private void _testNextFieldNameIndent(boolean useStream) throws Exception
{
final String DOC = "{\n \"name\" : \n [\n ]\n }";
- JsonFactory f = new JsonFactory();
JsonParser p = useStream ?
- f.createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8")))
- : f.createParser(new StringReader(DOC));
+ JSON_F.createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8")))
+ : JSON_F.createParser(new StringReader(DOC));
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertTrue(p.nextFieldName(new SerializedString("name")));
@@ -230,7 +251,155 @@
p.close();
}
+
+ private void _textNextText(boolean useStream) throws Exception
+ {
+ final String DOC = aposToQuotes("{'a':'123','b':5,'c':[false,'foo']}");
+ JsonParser p = useStream ?
+ JSON_F.createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8")))
+ : JSON_F.createParser(new StringReader(DOC));
+ assertNull(p.nextTextValue());
+ assertToken(JsonToken.START_OBJECT, p.getCurrentToken());
+ assertNull(p.nextTextValue());
+ assertToken(JsonToken.FIELD_NAME, p.getCurrentToken());
+ assertEquals("a", p.getCurrentName());
+ assertEquals("123", p.nextTextValue());
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("b", p.getCurrentName());
+ assertNull(p.nextFieldName());
+ assertToken(JsonToken.VALUE_NUMBER_INT, p.getCurrentToken());
+
+ assertEquals("c", p.nextFieldName());
+
+ assertNull(p.nextTextValue());
+ assertToken(JsonToken.START_ARRAY, p.getCurrentToken());
+ assertNull(p.nextTextValue());
+ assertToken(JsonToken.VALUE_FALSE, p.getCurrentToken());
+ assertEquals("foo", p.nextTextValue());
+
+ assertNull(p.nextTextValue());
+ assertToken(JsonToken.END_ARRAY, p.getCurrentToken());
+ assertNull(p.nextTextValue());
+ assertToken(JsonToken.END_OBJECT, p.getCurrentToken());
+ assertNull(p.nextTextValue());
+ assertNull(p.getCurrentToken());
+
+ p.close();
+ }
+
+ private void _textNextInt(boolean useStream) throws Exception
+ {
+ final String DOC = aposToQuotes("{'a':'123','b':5,'c':[false,456]}");
+ JsonParser p = useStream ?
+ JSON_F.createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8")))
+ : JSON_F.createParser(new StringReader(DOC));
+ assertEquals(0, p.nextIntValue(0));
+ assertToken(JsonToken.START_OBJECT, p.getCurrentToken());
+ assertEquals(0, p.nextIntValue(0));
+ assertToken(JsonToken.FIELD_NAME, p.getCurrentToken());
+ assertEquals("a", p.getCurrentName());
+
+ assertEquals(0, p.nextIntValue(0));
+ assertToken(JsonToken.VALUE_STRING, p.getCurrentToken());
+ assertEquals("123", p.getText());
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("b", p.getCurrentName());
+ assertEquals(5, p.nextIntValue(0));
+
+ assertEquals("c", p.nextFieldName());
+
+ assertEquals(0, p.nextIntValue(0));
+ assertToken(JsonToken.START_ARRAY, p.getCurrentToken());
+ assertEquals(0, p.nextIntValue(0));
+ assertToken(JsonToken.VALUE_FALSE, p.getCurrentToken());
+ assertEquals(456, p.nextIntValue(0));
+
+ assertEquals(0, p.nextIntValue(0));
+ assertToken(JsonToken.END_ARRAY, p.getCurrentToken());
+ assertEquals(0, p.nextIntValue(0));
+ assertToken(JsonToken.END_OBJECT, p.getCurrentToken());
+ assertEquals(0, p.nextIntValue(0));
+ assertNull(p.getCurrentToken());
+
+ p.close();
+ }
+
+ private void _textNextLong(boolean useStream) throws Exception
+ {
+ final String DOC = aposToQuotes("{'a':'xyz','b':-59,'c':[false,-1]}");
+ JsonParser p = useStream ?
+ JSON_F.createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8")))
+ : JSON_F.createParser(new StringReader(DOC));
+ assertEquals(0L, p.nextLongValue(0L));
+ assertToken(JsonToken.START_OBJECT, p.getCurrentToken());
+ assertEquals(0L, p.nextLongValue(0L));
+ assertToken(JsonToken.FIELD_NAME, p.getCurrentToken());
+ assertEquals("a", p.getCurrentName());
+
+ assertEquals(0L, p.nextLongValue(0L));
+ assertToken(JsonToken.VALUE_STRING, p.getCurrentToken());
+ assertEquals("xyz", p.getText());
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("b", p.getCurrentName());
+ assertEquals(-59L, p.nextLongValue(0L));
+
+ assertEquals("c", p.nextFieldName());
+
+ assertEquals(0L, p.nextLongValue(0L));
+ assertToken(JsonToken.START_ARRAY, p.getCurrentToken());
+ assertEquals(0L, p.nextLongValue(0L));
+ assertToken(JsonToken.VALUE_FALSE, p.getCurrentToken());
+ assertEquals(-1L, p.nextLongValue(0L));
+
+ assertEquals(0L, p.nextLongValue(0L));
+ assertToken(JsonToken.END_ARRAY, p.getCurrentToken());
+ assertEquals(0L, p.nextLongValue(0L));
+ assertToken(JsonToken.END_OBJECT, p.getCurrentToken());
+ assertEquals(0L, p.nextLongValue(0L));
+ assertNull(p.getCurrentToken());
+
+ p.close();
+ }
+
+ private void _textNextBoolean(boolean useStream) throws Exception
+ {
+ final String DOC = aposToQuotes("{'a':'xyz','b':true,'c':[false,0]}");
+ JsonParser p = useStream ?
+ JSON_F.createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8")))
+ : JSON_F.createParser(new StringReader(DOC));
+ assertNull(p.nextBooleanValue());
+ assertToken(JsonToken.START_OBJECT, p.getCurrentToken());
+ assertNull(p.nextBooleanValue());
+ assertToken(JsonToken.FIELD_NAME, p.getCurrentToken());
+ assertEquals("a", p.getCurrentName());
+
+ assertNull(p.nextBooleanValue());
+ assertToken(JsonToken.VALUE_STRING, p.getCurrentToken());
+ assertEquals("xyz", p.getText());
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("b", p.getCurrentName());
+ assertEquals(Boolean.TRUE, p.nextBooleanValue());
+
+ assertEquals("c", p.nextFieldName());
+
+ assertNull(p.nextBooleanValue());
+ assertToken(JsonToken.START_ARRAY, p.getCurrentToken());
+ assertEquals(Boolean.FALSE, p.nextBooleanValue());
+ assertNull(p.nextBooleanValue());
+ assertToken(JsonToken.VALUE_NUMBER_INT, p.getCurrentToken());
+ assertEquals(0, p.getIntValue());
+
+ assertNull(p.nextBooleanValue());
+ assertToken(JsonToken.END_ARRAY, p.getCurrentToken());
+ assertNull(p.nextBooleanValue());
+ assertToken(JsonToken.END_OBJECT, p.getCurrentToken());
+ assertNull(p.nextBooleanValue());
+ assertNull(p.getCurrentToken());
+
+ p.close();
+ }
+
private void _testIssue34(boolean useStream) throws Exception
{
final int TESTROUNDS = 223;
@@ -283,7 +452,7 @@
parser.close();
}
- private void _testLong(JsonFactory f, boolean useStream) throws Exception
+ private void _testNextNameWithLong(boolean useStream) throws Exception
{
// do 5 meg thingy
final int SIZE = 5 * 1024 * 1024;
@@ -307,8 +476,8 @@
final String DOC = sb.toString();
JsonParser parser = useStream ?
- f.createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8")))
- : f.createParser(new StringReader(DOC));
+ JSON_F.createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8")))
+ : JSON_F.createParser(new StringReader(DOC));
assertToken(JsonToken.START_OBJECT, parser.nextToken());
rnd = new Random(1);
for (int i = 0; i < count; ++i) {
diff --git a/src/test/java/com/fasterxml/jackson/core/json/TestParserSymbols.java b/src/test/java/com/fasterxml/jackson/core/json/TestParserSymbols.java
index 7aaf43e..8d87beb 100644
--- a/src/test/java/com/fasterxml/jackson/core/json/TestParserSymbols.java
+++ b/src/test/java/com/fasterxml/jackson/core/json/TestParserSymbols.java
@@ -7,17 +7,21 @@
{
// For [Issue#148]
public void testSymbolsWithNullBytes() throws Exception {
- _testSymbolsWithNull(true);
+ JsonFactory f = new JsonFactory();
+ _testSymbolsWithNull(f, true);
+ // and repeat with same factory, just for fun, and to ensure symbol table is fine
+ _testSymbolsWithNull(f, true);
}
// For [Issue#148]
public void testSymbolsWithNullChars() throws Exception {
- _testSymbolsWithNull(false);
+ JsonFactory f = new JsonFactory();
+ _testSymbolsWithNull(f, false);
+ _testSymbolsWithNull(f, false);
}
-
- private void _testSymbolsWithNull(boolean useBytes) throws Exception
+
+ private void _testSymbolsWithNull(JsonFactory f, boolean useBytes) throws Exception
{
- final JsonFactory f = new JsonFactory();
final String INPUT = "{\"\\u0000abc\" : 1, \"abc\":2}";
JsonParser parser = useBytes ? f.createParser(INPUT.getBytes("UTF-8"))
: f.createParser(INPUT);
@@ -25,12 +29,23 @@
assertToken(JsonToken.START_OBJECT, parser.nextToken());
assertToken(JsonToken.FIELD_NAME, parser.nextToken());
- assertEquals("\u0000abc", parser.getCurrentName());
+ String currName = parser.getCurrentName();
+ if (!"\u0000abc".equals(currName)) {
+ fail("Expected \\0abc (4 bytes), '"+currName+"' ("+currName.length()+")");
+ }
assertToken(JsonToken.VALUE_NUMBER_INT, parser.nextToken());
assertEquals(1, parser.getIntValue());
assertToken(JsonToken.FIELD_NAME, parser.nextToken());
- assertEquals("abc", parser.getCurrentName());
+ currName = parser.getCurrentName();
+ if (!"abc".equals(currName)) {
+ /*
+ for (int i = 0; i < currName.length(); ++i) {
+ System.out.println("#"+i+" -> 0x"+Integer.toHexString(currName.charAt(i)));
+ }
+ */
+ fail("Expected 'abc' (3 bytes), '"+currName+"' ("+currName.length()+")");
+ }
assertToken(JsonToken.VALUE_NUMBER_INT, parser.nextToken());
assertEquals(2, parser.getIntValue());
diff --git a/src/test/java/com/fasterxml/jackson/core/json/TestRootValues.java b/src/test/java/com/fasterxml/jackson/core/json/TestRootValues.java
index 3e51d8c..585dbbe 100644
--- a/src/test/java/com/fasterxml/jackson/core/json/TestRootValues.java
+++ b/src/test/java/com/fasterxml/jackson/core/json/TestRootValues.java
@@ -29,6 +29,29 @@
jp.close();
}
+ public void testBrokeanNumber() throws Exception
+ {
+ _testBrokeanNumber(false);
+ _testBrokeanNumber(true);
+ }
+
+ private void _testBrokeanNumber(boolean useStream) throws Exception
+ {
+ JsonFactory f = new JsonFactory();
+ final String DOC = "14:89:FD:D3:E7:8C";
+ JsonParser p = useStream ?
+ createParserUsingStream(f, DOC, "UTF-8")
+ : createParserUsingReader(f, DOC);
+ // Should fail, right away
+ try {
+ p.nextToken();
+ fail("Ought to fail! Instead, got token: "+p.getCurrentToken());
+ } catch (JsonParseException e) {
+ verifyException(e, "unexpected character");
+ }
+ p.close();
+ }
+
public void testSimpleBooleans() throws Exception
{
_testSimpleBooleans(false);
diff --git a/src/test/java/com/fasterxml/jackson/core/json/TestUtf8Generator.java b/src/test/java/com/fasterxml/jackson/core/json/TestUtf8Generator.java
index 553693b..3fed2d5 100644
--- a/src/test/java/com/fasterxml/jackson/core/json/TestUtf8Generator.java
+++ b/src/test/java/com/fasterxml/jackson/core/json/TestUtf8Generator.java
@@ -1,14 +1,15 @@
package com.fasterxml.jackson.core.json;
-import java.io.ByteArrayOutputStream;
+import java.io.*;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.io.IOContext;
import com.fasterxml.jackson.core.util.BufferRecycler;
-public class TestUtf8Generator
- extends BaseTest
+public class TestUtf8Generator extends BaseTest
{
+ private final JsonFactory JSON_F = new JsonFactory();
+
public void testUtf8Issue462() throws Exception
{
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
@@ -25,8 +26,7 @@
gen.close();
// Also verify it's parsable?
- JsonFactory f = new JsonFactory();
- JsonParser p = f.createParser(bytes.toByteArray());
+ JsonParser p = JSON_F.createParser(bytes.toByteArray());
for (int i = 1; i <= length; ++i) {
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertEquals(1, p.getIntValue());
@@ -41,9 +41,8 @@
public void testSurrogatesWithRaw() throws Exception
{
final String VALUE = quote("\ud83d\ude0c");
- JsonFactory f = new JsonFactory();
ByteArrayOutputStream out = new ByteArrayOutputStream();
- JsonGenerator jgen = f.createGenerator(out);
+ JsonGenerator jgen = JSON_F.createGenerator(out);
jgen.writeStartArray();
jgen.writeRaw(VALUE);
jgen.writeEndArray();
@@ -51,7 +50,7 @@
final byte[] JSON = out.toByteArray();
- JsonParser jp = f.createParser(JSON);
+ JsonParser jp = JSON_F.createParser(JSON);
assertToken(JsonToken.START_ARRAY, jp.nextToken());
assertToken(JsonToken.VALUE_STRING, jp.nextToken());
String str = jp.getText();
diff --git a/src/test/java/com/fasterxml/jackson/core/json/TestUtf8Parser.java b/src/test/java/com/fasterxml/jackson/core/json/TestUtf8Parser.java
index d542559..d7f2f7c 100644
--- a/src/test/java/com/fasterxml/jackson/core/json/TestUtf8Parser.java
+++ b/src/test/java/com/fasterxml/jackson/core/json/TestUtf8Parser.java
@@ -46,8 +46,7 @@
jp.close();
}
- public void testUtf8Name2Bytes()
- throws Exception
+ public void testUtf8Name2Bytes() throws Exception
{
final String[] NAMES = UTF8_2BYTE_STRINGS;
@@ -56,10 +55,17 @@
String DOC = "{ \""+NAME+"\" : 0 }";
JsonParser jp = createParserUsingStream(DOC, "UTF-8");
assertToken(JsonToken.START_OBJECT, jp.nextToken());
-
+
assertToken(JsonToken.FIELD_NAME, jp.nextToken());
+
+ assertTrue(jp.hasToken(JsonToken.FIELD_NAME));
+ assertTrue(jp.hasTokenId(JsonTokenId.ID_FIELD_NAME));
+
assertEquals(NAME, jp.getCurrentName());
assertToken(JsonToken.VALUE_NUMBER_INT, jp.nextToken());
+ assertTrue(jp.hasToken(JsonToken.VALUE_NUMBER_INT));
+ assertTrue(jp.hasTokenId(JsonTokenId.ID_NUMBER_INT));
+
// should retain name during value entry, too
assertEquals(NAME, jp.getCurrentName());
diff --git a/src/test/java/com/fasterxml/jackson/core/sym/TestJsonParserSymbols.java b/src/test/java/com/fasterxml/jackson/core/sym/SymbolTableMergingTest.java
similarity index 96%
rename from src/test/java/com/fasterxml/jackson/core/sym/TestJsonParserSymbols.java
rename to src/test/java/com/fasterxml/jackson/core/sym/SymbolTableMergingTest.java
index 20c9e7f..766fada 100644
--- a/src/test/java/com/fasterxml/jackson/core/sym/TestJsonParserSymbols.java
+++ b/src/test/java/com/fasterxml/jackson/core/sym/SymbolTableMergingTest.java
@@ -11,7 +11,7 @@
* merge back symbols to the root symbol table
*/
@SuppressWarnings("serial")
-public class TestJsonParserSymbols
+public class SymbolTableMergingTest
extends com.fasterxml.jackson.core.BaseTest
{
/**
@@ -21,7 +21,7 @@
*/
final static class MyJsonFactory extends JsonFactory
{
- public int byteSymbolCount() { return _rootByteSymbols.size(); }
+ public int byteSymbolCount() { return _byteSymbolCanonicalizer.size(); }
public int charSymbolCount() { return _rootCharSymbols.size(); }
}
diff --git a/src/test/java/com/fasterxml/jackson/core/sym/SymbolsViaParserTest.java b/src/test/java/com/fasterxml/jackson/core/sym/SymbolsViaParserTest.java
new file mode 100644
index 0000000..8692735
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/core/sym/SymbolsViaParserTest.java
@@ -0,0 +1,94 @@
+package com.fasterxml.jackson.core.sym;
+
+import java.io.IOException;
+import java.util.HashSet;
+
+import com.fasterxml.jackson.core.*;
+
+/**
+ * Tests that use symbol table functionality through parser.
+ */
+public class SymbolsViaParserTest
+ extends com.fasterxml.jackson.core.BaseTest
+{
+ // for [jackson-core#213]
+ public void test17CharSymbols() throws Exception {
+ _test17Chars(false);
+ }
+
+ // for [jackson-core#213]
+ public void test17ByteSymbols() throws Exception {
+ _test17Chars(true);
+ }
+
+ // for [jackson-core#216]
+ public void testSymbolTableExpansionChars() throws Exception {
+ _testSymbolTableExpansion(false);
+ }
+
+ // for [jackson-core#216]
+ public void testSymbolTableExpansionBytes() throws Exception {
+ _testSymbolTableExpansion(true);
+ }
+
+ /*
+ /**********************************************************
+ /* Secondary test methods
+ /**********************************************************
+ */
+
+ private void _test17Chars(boolean useBytes) throws IOException
+ {
+ String doc = _createDoc17();
+ JsonFactory f = new JsonFactory();
+
+ JsonParser p = useBytes
+ ? f.createParser(doc.getBytes("UTF-8"))
+ : f.createParser(doc);
+ HashSet<String> syms = new HashSet<String>();
+ assertToken(JsonToken.START_OBJECT, p.nextToken());
+ for (int i = 0; i < 50; ++i) {
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ syms.add(p.getCurrentName());
+ assertToken(JsonToken.VALUE_TRUE, p.nextToken());
+ }
+ assertToken(JsonToken.END_OBJECT, p.nextToken());
+ assertEquals(50, syms.size());
+ p.close();
+ }
+
+ private String _createDoc17() {
+ StringBuilder sb = new StringBuilder(1000);
+ sb.append("{\n");
+ for (int i = 1; i <= 50; ++i) {
+ if (i > 1) {
+ sb.append(",\n");
+ }
+ sb.append("\"lengthmatters")
+ .append(1000 + i)
+ .append("\": true");
+ }
+ sb.append("\n}");
+ return sb.toString();
+ }
+
+ public void _testSymbolTableExpansion(boolean useBytes) throws Exception
+ {
+ JsonFactory jsonFactory = new JsonFactory();
+ // Important: must create separate documents to gradually build up symbol table
+ for (int i = 0; i < 200; i++) {
+ String field = Integer.toString(i);
+ final String doc = "{ \"" + field + "\" : \"test\" }";
+ JsonParser parser = useBytes
+ ? jsonFactory.createParser(doc.getBytes("UTF-8"))
+ : jsonFactory.createParser(doc);
+ assertToken(JsonToken.START_OBJECT, parser.nextToken());
+ assertToken(JsonToken.FIELD_NAME, parser.nextToken());
+ assertEquals(field, parser.getCurrentName());
+ assertToken(JsonToken.VALUE_STRING, parser.nextToken());
+ assertToken(JsonToken.END_OBJECT, parser.nextToken());
+ assertNull(parser.nextToken());
+ parser.close();
+ }
+ }
+}
diff --git a/src/test/java/com/fasterxml/jackson/core/sym/TestByteBasedSymbols.java b/src/test/java/com/fasterxml/jackson/core/sym/TestByteBasedSymbols.java
index 3360d48..a0ff70d 100644
--- a/src/test/java/com/fasterxml/jackson/core/sym/TestByteBasedSymbols.java
+++ b/src/test/java/com/fasterxml/jackson/core/sym/TestByteBasedSymbols.java
@@ -1,15 +1,13 @@
package com.fasterxml.jackson.core.sym;
import java.io.*;
+import java.lang.reflect.Field;
import com.fasterxml.jackson.core.*;
-import com.fasterxml.jackson.core.sym.BytesToNameCanonicalizer;
-import com.fasterxml.jackson.core.sym.Name;
/**
* Unit test(s) to verify that handling of (byte-based) symbol tables
- * is working. Created to verify fix to [JACKSON-5] (although not very
- * good at catching it...).
+ * is working.
*/
public class TestByteBasedSymbols
extends com.fasterxml.jackson.core.BaseTest
@@ -76,24 +74,22 @@
jp0.close();
}
- public void testAuxMethods()
- throws Exception
+ public void testAuxMethodsWithNewSymboTable() throws Exception
{
final int A_BYTES = 0x41414141; // "AAAA"
final int B_BYTES = 0x42424242; // "BBBB"
- BytesToNameCanonicalizer nc = BytesToNameCanonicalizer.createRoot()
+ ByteQuadsCanonicalizer nc = ByteQuadsCanonicalizer.createRoot()
.makeChild(JsonFactory.Feature.collectDefaults());
assertNull(nc.findName(A_BYTES));
assertNull(nc.findName(A_BYTES, B_BYTES));
nc.addName("AAAA", new int[] { A_BYTES }, 1);
- Name n1 = nc.findName(A_BYTES);
- assertNotNull(n1);
- assertEquals("AAAA", n1.getName());
+ String n1 = nc.findName(A_BYTES);
+ assertEquals("AAAA", n1);
nc.addName("AAAABBBB", new int[] { A_BYTES, B_BYTES }, 2);
- Name n2 = nc.findName(A_BYTES, B_BYTES);
- assertEquals("AAAABBBB", n2.getName());
+ String n2 = nc.findName(A_BYTES, B_BYTES);
+ assertEquals("AAAABBBB", n2);
assertNotNull(n2);
/* and let's then just exercise this method so it gets covered;
@@ -102,14 +98,35 @@
assertNotNull(nc.toString());
}
+ // as per name, for [core#207]
+ public void testIssue207() throws Exception
+ {
+ ByteQuadsCanonicalizer nc = ByteQuadsCanonicalizer.createRoot(-523743345);
+ Field byteSymbolCanonicalizerField = JsonFactory.class.getDeclaredField("_byteSymbolCanonicalizer");
+ byteSymbolCanonicalizerField.setAccessible(true);
+ JsonFactory jsonF = new JsonFactory();
+ byteSymbolCanonicalizerField.set(jsonF, nc);
+
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("{\n");
+ stringBuilder.append(" \"expectedGCperPosition\": null");
+ for (int i = 0; i < 60; ++i) {
+ stringBuilder.append(",\n \"").append(i + 1).append("\": null");
+ }
+ stringBuilder.append("\n}");
+
+ JsonParser p = jsonF.createParser(stringBuilder.toString().getBytes("UTF-8"));
+ while (p.nextToken() != null) { }
+ p.close();
+ }
+
/*
/**********************************************************
/* Helper methods
/**********************************************************
*/
- protected JsonParser createParser(JsonFactory jf, String input)
- throws IOException, JsonParseException
+ protected JsonParser createParser(JsonFactory jf, String input) throws IOException
{
byte[] data = input.getBytes("UTF-8");
InputStream is = new ByteArrayInputStream(data);
diff --git a/src/test/java/com/fasterxml/jackson/core/sym/TestHashCollisionChars.java b/src/test/java/com/fasterxml/jackson/core/sym/TestHashCollisionChars.java
index 40dac62..fd3bb1e 100644
--- a/src/test/java/com/fasterxml/jackson/core/sym/TestHashCollisionChars.java
+++ b/src/test/java/com/fasterxml/jackson/core/sym/TestHashCollisionChars.java
@@ -8,6 +8,11 @@
/**
* Some unit tests to try to exercise part of parser code that
* deals with symbol (table) management.
+ *<p>
+ * Note that the problem does not necessarily affect code at or
+ * after Jackson 2.6, since hash calculation algorithm has been
+ * completely changed. It may still be relevant for character-backed
+ * sources, however.
*/
public class TestHashCollisionChars
extends BaseTest
diff --git a/src/test/java/com/fasterxml/jackson/core/sym/TestSymbolTables.java b/src/test/java/com/fasterxml/jackson/core/sym/TestSymbolTables.java
index 8f8156c..045a943 100644
--- a/src/test/java/com/fasterxml/jackson/core/sym/TestSymbolTables.java
+++ b/src/test/java/com/fasterxml/jackson/core/sym/TestSymbolTables.java
@@ -1,10 +1,15 @@
package com.fasterxml.jackson.core.sym;
import java.io.IOException;
+import java.lang.reflect.Field;
import java.nio.charset.Charset;
import com.fasterxml.jackson.core.*;
+/**
+ * Tests that directly modify/access underlying low-level symbol tables
+ * (instead of indirectly using them via JsonParser).
+ */
public class TestSymbolTables extends com.fasterxml.jackson.core.BaseTest
{
// Test for verifying stability of hashCode, wrt collisions, using
@@ -13,67 +18,117 @@
{
// pass seed, to keep results consistent:
CharsToNameCanonicalizer symbols = CharsToNameCanonicalizer.createRoot(1);
- final int COUNT = 6000;
+ final int COUNT = 12000;
for (int i = 0; i < COUNT; ++i) {
String id = fieldNameFor(i);
char[] ch = id.toCharArray();
symbols.findSymbol(ch, 0, ch.length, symbols.calcHash(id));
}
- assertEquals(8192, symbols.bucketCount());
+ assertEquals(16384, symbols.bucketCount());
assertEquals(COUNT, symbols.size());
//System.out.printf("Char stuff: collisions %d, max-coll %d\n", symbols.collisionCount(), symbols.maxCollisionLength());
// holy guacamoley... there are way too many. 31 gives 3567 (!), 33 gives 2747
// ... at least before shuffling. Shuffling helps quite a lot, so:
+
+ assertEquals(3431, symbols.collisionCount());
- assertEquals(1401, symbols.collisionCount()); // with 33
-// assertEquals(1858, symbols.collisionCount()); // with 31
-
- // esp. with collisions; first got about 30;
- // with fixes 4 (for 33), 5 (for 31)
-
- assertEquals(4, symbols.maxCollisionLength()); // 33
-// assertEquals(5, symbols.maxCollisionLength()); // 31
+ assertEquals(6, symbols.maxCollisionLength());
}
// Test for verifying stability of hashCode, wrt collisions, using
// synthetic field name generation and byte-based input (UTF-8)
- public void testSyntheticWithBytes() throws IOException
+ @SuppressWarnings("deprecation")
+ public void testSyntheticWithBytesOld() throws IOException
{
// pass seed, to keep results consistent:
final int SEED = 33333;
BytesToNameCanonicalizer symbols =
BytesToNameCanonicalizer.createRoot(SEED).makeChild(JsonFactory.Feature.collectDefaults());
- final int COUNT = 6000;
+
+ final int COUNT = 12000;
for (int i = 0; i < COUNT; ++i) {
String id = fieldNameFor(i);
- int[] quads = BytesToNameCanonicalizer.calcQuads(id.getBytes("UTF-8"));
+ int[] quads = calcQuads(id.getBytes("UTF-8"));
symbols.addName(id, quads, quads.length);
}
assertEquals(COUNT, symbols.size());
- assertEquals(8192, symbols.bucketCount());
+ assertEquals(16384, symbols.bucketCount());
//System.out.printf("Byte stuff: collisions %d, max-coll %d\n", symbols.collisionCount(), symbols.maxCollisionLength());
-
- assertEquals(1733, symbols.collisionCount());
- // but not super long collision chains:
- assertEquals(9, symbols.maxCollisionLength());
+ assertEquals(3476, symbols.collisionCount());
+ // longest collision chain not optimal but ok:
+ assertEquals(15, symbols.maxCollisionLength());
+
+ // But also verify entries are actually found?
+ }
+
+ public void testSyntheticWithBytesNew() throws IOException
+ {
+ // pass seed, to keep results consistent:
+ final int SEED = 33333;
+ ByteQuadsCanonicalizer symbols =
+ ByteQuadsCanonicalizer.createRoot(SEED).makeChild(JsonFactory.Feature.collectDefaults());
+
+ final int COUNT = 12000;
+ for (int i = 0; i < COUNT; ++i) {
+ String id = fieldNameFor(i);
+ int[] quads = calcQuads(id.getBytes("UTF-8"));
+ symbols.addName(id, quads, quads.length);
+ }
+ assertEquals(COUNT, symbols.size());
+ assertEquals(16384, symbols.bucketCount());
+
+ // fragile, but essential to verify low collision counts;
+ // anywhere between 70-80% primary matches
+ assertEquals(8534, symbols.primaryCount());
+ // secondary between 10-20%
+ assertEquals(2534, symbols.secondaryCount());
+ // and most of remaining in tertiary
+ assertEquals(932, symbols.tertiaryCount());
+ // so that spill-over is empty or close to
+ assertEquals(0, symbols.spilloverCount());
}
// [Issue#145]
- public void testThousandsOfSymbols() throws IOException
+ public void testThousandsOfSymbolsWithChars() throws IOException
{
final int SEED = 33333;
- BytesToNameCanonicalizer symbolsBRoot = BytesToNameCanonicalizer.createRoot(SEED);
CharsToNameCanonicalizer symbolsCRoot = CharsToNameCanonicalizer.createRoot(SEED);
- final Charset utf8 = Charset.forName("UTF-8");
+ int exp = 0;
for (int doc = 0; doc < 100; ++doc) {
CharsToNameCanonicalizer symbolsC =
symbolsCRoot.makeChild(JsonFactory.Feature.collectDefaults());
+ for (int i = 0; i < 250; ++i) {
+ String name = "f_"+doc+"_"+i;
+ char[] ch = name.toCharArray();
+ String str = symbolsC.findSymbol(ch, 0, ch.length,
+ symbolsC.calcHash(name));
+ assertNotNull(str);
+ }
+ symbolsC.release();
+ exp += 250;
+ if (exp > CharsToNameCanonicalizer.MAX_ENTRIES_FOR_REUSE) {
+ exp = 0;
+ }
+ assertEquals(exp, symbolsCRoot.size());
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ public void testThousandsOfSymbolsWithOldBytes() throws IOException
+ {
+ final int SEED = 33333;
+
+ BytesToNameCanonicalizer symbolsBRoot = BytesToNameCanonicalizer.createRoot(SEED);
+ final Charset utf8 = Charset.forName("UTF-8");
+ int exp = 0;
+
+ for (int doc = 0; doc < 100; ++doc) {
BytesToNameCanonicalizer symbolsB =
symbolsBRoot.makeChild(JsonFactory.Feature.collectDefaults());
for (int i = 0; i < 250; ++i) {
@@ -81,18 +136,105 @@
int[] quads = BytesToNameCanonicalizer.calcQuads(name.getBytes(utf8));
symbolsB.addName(name, quads, quads.length);
-
- char[] ch = name.toCharArray();
- String str = symbolsC.findSymbol(ch, 0, ch.length,
- symbolsC.calcHash(name));
- assertNotNull(str);
+ Name n = symbolsB.findName(quads, quads.length);
+ assertEquals(name, n.getName());
}
symbolsB.release();
- symbolsC.release();
+ exp += 250;
+ if (exp > BytesToNameCanonicalizer.MAX_ENTRIES_FOR_REUSE) {
+ exp = 0;
+ }
+ assertEquals(exp, symbolsBRoot.size());
}
}
+ // Since 2.6
+ public void testThousandsOfSymbolsWithNew() throws IOException
+ {
+ final int SEED = 33333;
+
+ ByteQuadsCanonicalizer symbolsBRoot = ByteQuadsCanonicalizer.createRoot(SEED);
+ final Charset utf8 = Charset.forName("UTF-8");
+ int exp = 0;
+ ByteQuadsCanonicalizer symbolsB = null;
+
+ // loop to get
+ for (int doc = 0; doc < 100; ++doc) {
+ symbolsB = symbolsBRoot.makeChild(JsonFactory.Feature.collectDefaults());
+ for (int i = 0; i < 250; ++i) {
+ String name = "f_"+doc+"_"+i;
+
+ int[] quads = calcQuads(name.getBytes(utf8));
+
+ symbolsB.addName(name, quads, quads.length);
+ String n = symbolsB.findName(quads, quads.length);
+ assertEquals(name, n);
+ }
+ symbolsB.release();
+
+ exp += 250;
+ if (exp > ByteQuadsCanonicalizer.MAX_ENTRIES_FOR_REUSE) {
+ exp = 0;
+ }
+ assertEquals(exp, symbolsBRoot.size());
+ }
+ /* 05-Feb-2015, tatu: Fragile, but it is important to ensure that collision
+ * rates are not accidentally increased...
+ */
+ assertEquals(6250, symbolsB.size());
+ assertEquals(4761, symbolsB.primaryCount()); // 80% primary hit rate
+ assertEquals(1190, symbolsB.secondaryCount()); // 13% secondary
+ assertEquals(299, symbolsB.tertiaryCount()); // 7% tertiary
+ assertEquals(0, symbolsB.spilloverCount()); // and couple of leftovers
+ }
+
+ // And then one more test just for Bytes-based symbol table
+ public void testByteBasedSymbolTable() throws Exception
+ {
+ // combination of short, medium1/2, long names...
+ final String JSON = aposToQuotes("{'abc':1, 'abc\\u0000':2, '\\u0000abc':3, "
+ // then some medium
+ +"'abc123':4,'abcd1234':5,"
+ +"'abcd1234a':6,'abcd1234abcd':7,"
+ +"'abcd1234abcd1':8"
+ +"}");
+
+ JsonFactory f = new JsonFactory();
+ JsonParser p = f.createParser(JSON.getBytes("UTF-8"));
+ ByteQuadsCanonicalizer symbols = _findSymbols(p);
+ assertEquals(0, symbols.size());
+ _streamThrough(p);
+ assertEquals(8, symbols.size());
+ p.close();
+
+ // and, for fun, try again
+ p = f.createParser(JSON.getBytes("UTF-8"));
+ _streamThrough(p);
+ symbols = _findSymbols(p);
+ assertEquals(8, symbols.size());
+ p.close();
+
+ p = f.createParser(JSON.getBytes("UTF-8"));
+ _streamThrough(p);
+ symbols = _findSymbols(p);
+ assertEquals(8, symbols.size());
+ p.close();
+ }
+
+ private void _streamThrough(JsonParser p) throws IOException
+ {
+ while (p.nextToken() != null) { }
+ }
+
+ private ByteQuadsCanonicalizer _findSymbols(JsonParser p) throws Exception
+ {
+ Field syms = p.getClass().getDeclaredField("_symbols");
+ syms.setAccessible(true);
+ return ((ByteQuadsCanonicalizer) syms.get(p));
+ }
+
// [core#187]: unexpectedly high number of collisions for straight numbers
+ @SuppressWarnings("deprecation")
public void testCollisionsWithBytes187() throws IOException
{
BytesToNameCanonicalizer symbols =
@@ -129,9 +271,63 @@
assertEquals(65536, symbols.bucketCount());
// collision count rather high, but has to do
- assertEquals(14408, symbols.collisionCount());
+ assertEquals(7127, symbols.collisionCount());
// as well as collision counts
- assertEquals(10, symbols.maxCollisionLength());
+ assertEquals(4, symbols.maxCollisionLength());
+ }
+
+ // [core#187]: unexpectedly high number of collisions for straight numbers
+ public void testCollisionsWithBytesNew187a() throws IOException
+ {
+ ByteQuadsCanonicalizer symbols =
+ ByteQuadsCanonicalizer.createRoot(1).makeChild(JsonFactory.Feature.collectDefaults());
+
+ final int COUNT = 43000;
+ for (int i = 0; i < COUNT; ++i) {
+ String id = String.valueOf(10000 + i);
+ int[] quads = calcQuads(id.getBytes("UTF-8"));
+ symbols.addName(id, quads, quads.length);
+ }
+
+ assertEquals(COUNT, symbols.size());
+ assertEquals(65536, symbols.bucketCount());
+
+ /* 29-Mar-2015, tatu: To get collision counts down for this
+ * test took quite a bit of tweaking...
+ */
+ assertEquals(32342, symbols.primaryCount());
+ assertEquals(8863, symbols.secondaryCount());
+ assertEquals(1795, symbols.tertiaryCount());
+
+ // finally managed to get this to 0; other variants produced thousands
+ assertEquals(0, symbols.spilloverCount());
+ }
+
+ // Another variant, but with 1-quad names
+ public void testCollisionsWithBytesNew187b() throws IOException
+ {
+ ByteQuadsCanonicalizer symbols =
+ ByteQuadsCanonicalizer.createRoot(1).makeChild(JsonFactory.Feature.collectDefaults());
+
+ final int COUNT = 10000;
+ for (int i = 0; i < COUNT; ++i) {
+ String id = String.valueOf(i);
+ int[] quads = calcQuads(id.getBytes("UTF-8"));
+ symbols.addName(id, quads, quads.length);
+ }
+ assertEquals(COUNT, symbols.size());
+
+ assertEquals(16384, symbols.bucketCount());
+
+ // fragile, but essential to verify low collision counts;
+ // here bit low primary, 55%
+ assertEquals(5402, symbols.primaryCount());
+ // secondary higher than usual, above 25%
+ assertEquals(2744, symbols.secondaryCount());
+ // and most of remaining in tertiary
+ assertEquals(1834, symbols.tertiaryCount());
+ // with a bit of spillover
+ assertEquals(20, symbols.spilloverCount());
}
// [core#191]: similarly, but for "short" symbols:
@@ -152,6 +348,26 @@
p.close();
}
+ private String _shortDoc191() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{\n");
+ for (int i = 0; i < 400; ++i) {
+ if (i > 0) {
+ sb.append(",\n");
+ }
+ sb.append('"');
+ char c = (char) i;
+ if (Character.isLetterOrDigit(c)) {
+ sb.append((char) i);
+ } else {
+ sb.append(String.format("\\u%04x", i));
+ }
+ sb.append("\" : "+i);
+ }
+ sb.append("}\n");
+ return sb.toString();
+ }
+
// [core#191]
public void testShortQuotedDirectChars() throws IOException
{
@@ -166,11 +382,12 @@
assertEquals(COUNT, symbols.size());
assertEquals(1024, symbols.bucketCount());
- assertEquals(112, symbols.collisionCount());
+ assertEquals(50, symbols.collisionCount());
assertEquals(2, symbols.maxCollisionLength());
}
- public void testShortQuotedDirectBytes() throws IOException
+ @SuppressWarnings("deprecation")
+ public void testShortQuotedDirectBytesOld() throws IOException
{
final int COUNT = 400;
BytesToNameCanonicalizer symbols =
@@ -186,11 +403,31 @@
assertEquals(44, symbols.collisionCount());
assertEquals(2, symbols.maxCollisionLength());
}
-
- // [core#191]
- public void testShortNameCollisionsDirect() throws IOException
+
+ public void testShortQuotedDirectBytes() throws IOException
{
final int COUNT = 400;
+ ByteQuadsCanonicalizer symbols =
+ ByteQuadsCanonicalizer.createRoot(123).makeChild(JsonFactory.Feature.collectDefaults());
+ for (int i = 0; i < COUNT; ++i) {
+ String id = String.format("\\u%04x", i);
+ int[] quads = calcQuads(id.getBytes("UTF-8"));
+ symbols.addName(id, quads, quads.length);
+ }
+ assertEquals(COUNT, symbols.size());
+ assertEquals(512, symbols.bucketCount());
+
+ assertEquals(285, symbols.primaryCount());
+ assertEquals(90, symbols.secondaryCount());
+ assertEquals(25, symbols.tertiaryCount());
+ assertEquals(0, symbols.spilloverCount());
+ }
+
+ // [core#191]
+ @SuppressWarnings("deprecation")
+ public void testShortNameCollisionsDirect() throws IOException
+ {
+ final int COUNT = 600;
// First, char-based
{
@@ -203,37 +440,69 @@
assertEquals(COUNT, symbols.size());
assertEquals(1024, symbols.bucketCount());
- assertEquals(0, symbols.collisionCount());
- assertEquals(0, symbols.maxCollisionLength());
+ assertEquals(16, symbols.collisionCount());
+ assertEquals(1, symbols.maxCollisionLength());
}
-
+
// then byte-based
{
BytesToNameCanonicalizer symbols =
BytesToNameCanonicalizer.createRoot(1).makeChild(JsonFactory.Feature.collectDefaults());
for (int i = 0; i < COUNT; ++i) {
String id = String.valueOf((char) i);
- int[] quads = BytesToNameCanonicalizer.calcQuads(id.getBytes("UTF-8"));
+ int[] quads = calcQuads(id.getBytes("UTF-8"));
symbols.addName(id, quads, quads.length);
}
assertEquals(COUNT, symbols.size());
assertEquals(1024, symbols.bucketCount());
- assertEquals(15, symbols.collisionCount());
+ assertEquals(209, symbols.collisionCount());
assertEquals(1, symbols.maxCollisionLength());
}
}
-
- private String _shortDoc191() {
- StringBuilder sb = new StringBuilder();
- sb.append("{\n");
- for (int i = 0; i < 400; ++i) {
- if (i > 0) {
- sb.append(",\n");
+
+ public void testShortNameCollisionsDirectNew() throws IOException
+ {
+ final int COUNT = 700;
+ {
+ ByteQuadsCanonicalizer symbols =
+ ByteQuadsCanonicalizer.createRoot(333).makeChild(JsonFactory.Feature.collectDefaults());
+ for (int i = 0; i < COUNT; ++i) {
+ String id = String.valueOf((char) i);
+ int[] quads = calcQuads(id.getBytes("UTF-8"));
+ symbols.addName(id, quads, quads.length);
}
- sb.append(String.format("\"\\u%04x\" : %d", i, i));
+ assertEquals(COUNT, symbols.size());
+
+ assertEquals(1024, symbols.bucketCount());
+
+ // Primary is good, but secondary spills cluster in nasty way...
+ assertEquals(564, symbols.primaryCount());
+ assertEquals(122, symbols.secondaryCount());
+ assertEquals(14, symbols.tertiaryCount());
+ assertEquals(0, symbols.spilloverCount());
+
+ assertEquals(COUNT,
+ symbols.primaryCount() + symbols.secondaryCount() + symbols.tertiaryCount() + symbols.spilloverCount());
}
- sb.append("}\n");
- return sb.toString();
+ }
+
+ // to verify [jackson-core#213] -- did not fail, but ruled out low-level bug
+
+ public void testLongSymbols17Bytes() throws Exception
+ {
+ ByteQuadsCanonicalizer symbolsB =
+ ByteQuadsCanonicalizer.createRoot(3).makeChild(JsonFactory.Feature.collectDefaults());
+ CharsToNameCanonicalizer symbolsC = CharsToNameCanonicalizer.createRoot(3);
+
+ for (int i = 1001; i <= 1050; ++i) {
+ String id = "lengthmatters"+i;
+ int[] quads = calcQuads(id.getBytes("UTF-8"));
+ symbolsB.addName(id, quads, quads.length);
+ char[] idChars = id.toCharArray();
+ symbolsC.findSymbol(idChars, 0, idChars.length, symbolsC.calcHash(id));
+ }
+ assertEquals(50, symbolsB.size());
+ assertEquals(50, symbolsC.size());
}
}
diff --git a/src/test/java/com/fasterxml/jackson/core/sym/TestSymbolsWithMediaItem.java b/src/test/java/com/fasterxml/jackson/core/sym/TestSymbolsWithMediaItem.java
new file mode 100644
index 0000000..f0b5239
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/core/sym/TestSymbolsWithMediaItem.java
@@ -0,0 +1,94 @@
+package com.fasterxml.jackson.core.sym;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.*;
+
+public class TestSymbolsWithMediaItem extends com.fasterxml.jackson.core.BaseTest
+{
+ private final String JSON = aposToQuotes(
+ "{'media' : {\n"
+ +" 'uri' : 'http://foo.com',"
+ +" 'title' : 'Test title 1',"
+ +" 'width' : 640, 'height' : 480,"
+ +" 'format' : 'video/mpeg4',"
+ +" 'duration' : 18000000,"
+ +" 'size' : 58982400,"
+ +" 'bitrate' : 262144,"
+ +" 'persons' : [ ],"
+ +" 'player' : 'native',"
+ +" 'copyright' : 'None'"
+ +" },\n"
+ +" 'images' : [ {\n"
+ +" 'uri' : 'http://bar.com',\n"
+ +" 'title' : 'Test title 1',\n"
+ +" 'width' : 1024,'height' : 768,\n"
+ +" 'size' : 'LARGE'\n"
+ +" }, {\n"
+ +" 'uri' : 'http://foobar.org',\n"
+ +" 'title' : 'Javaone Keynote',\n"
+ +" 'width' : 320, 'height' : 240,\n"
+ +" 'size' : 'SMALL'\n"
+ +" } ]\n"
+ +"}\n");
+
+ public void testSmallSymbolSetWithBytes() throws IOException
+ {
+ final int SEED = 33333;
+
+ ByteQuadsCanonicalizer symbolsRoot = ByteQuadsCanonicalizer.createRoot(SEED);
+ ByteQuadsCanonicalizer symbols = symbolsRoot.makeChild(JsonFactory.Feature.collectDefaults());
+ JsonFactory f = new JsonFactory();
+ JsonParser p = f.createParser(JSON.getBytes("UTF-8"));
+
+ JsonToken t;
+ while ((t = p.nextToken()) != null) {
+ if (t != JsonToken.FIELD_NAME) {
+ continue;
+ }
+ String name = p.getCurrentName();
+ int[] quads = calcQuads(name.getBytes("UTF-8"));
+
+ if (symbols.findName(quads, quads.length) != null) {
+ continue;
+ }
+ symbols.addName(name, quads, quads.length);
+ }
+ p.close();
+
+ assertEquals(13, symbols.size());
+ assertEquals(12, symbols.primaryCount()); // 80% primary hit rate
+ assertEquals(1, symbols.secondaryCount()); // 13% secondary
+ assertEquals(0, symbols.tertiaryCount()); // 7% tertiary
+ assertEquals(0, symbols.spilloverCount()); // and couple of leftovers
+ }
+
+ public void testSmallSymbolSetWithChars() throws IOException
+ {
+ final int SEED = 33333;
+
+ CharsToNameCanonicalizer symbols = CharsToNameCanonicalizer.createRoot(SEED);
+ JsonFactory f = new JsonFactory();
+ JsonParser p = f.createParser(JSON);
+
+ JsonToken t;
+ while ((t = p.nextToken()) != null) {
+ if (t != JsonToken.FIELD_NAME) {
+ continue;
+ }
+ String name = p.getCurrentName();
+ char[] ch = name.toCharArray();
+ symbols.findSymbol(ch, 0, ch.length, symbols.calcHash(name));
+ }
+ p.close();
+
+ assertEquals(13, symbols.size());
+ assertEquals(13, symbols.size());
+ assertEquals(64, symbols.bucketCount());
+
+ // usually get 1 collision, but sometimes get lucky with 0; other times less so with 2
+ // (with differing shifting for hash etc)
+ assertEquals(0, symbols.collisionCount());
+ assertEquals(0, symbols.maxCollisionLength());
+ }
+}
diff --git a/src/test/java/com/fasterxml/jackson/core/type/TypeReferenceTest.java b/src/test/java/com/fasterxml/jackson/core/type/TypeReferenceTest.java
new file mode 100644
index 0000000..1bf326b
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/core/type/TypeReferenceTest.java
@@ -0,0 +1,27 @@
+package com.fasterxml.jackson.core.type;
+
+import java.util.List;
+
+import com.fasterxml.jackson.core.BaseTest;
+
+// Not much to test, but exercise to prevent code coverage tool from showing all red for package
+public class TypeReferenceTest extends BaseTest
+{
+ public void testSimple()
+ {
+ TypeReference<?> ref = new TypeReference<List<String>>() { };
+ assertNotNull(ref);
+ ref.equals(null);
+ }
+
+ @SuppressWarnings("rawtypes")
+ public void testInvalid()
+ {
+ try {
+ Object ob = new TypeReference() { };
+ fail("Should not pass, got: "+ob);
+ } catch (IllegalArgumentException e) {
+ verifyException(e, "without actual type information");
+ }
+ }
+}
diff --git a/src/test/java/com/fasterxml/jackson/core/util/ByteArrayBuilderTest.java b/src/test/java/com/fasterxml/jackson/core/util/ByteArrayBuilderTest.java
new file mode 100644
index 0000000..59ec2ae
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/core/util/ByteArrayBuilderTest.java
@@ -0,0 +1,30 @@
+package com.fasterxml.jackson.core.util;
+
+import org.junit.Assert;
+
+public class ByteArrayBuilderTest extends com.fasterxml.jackson.core.BaseTest
+{
+ public void testSimple() throws Exception
+ {
+ ByteArrayBuilder b = new ByteArrayBuilder(null, 20);
+ Assert.assertArrayEquals(new byte[0], b.toByteArray());
+
+ b.write((byte) 0);
+ b.append(1);
+
+ byte[] foo = new byte[98];
+ for (int i = 0; i < foo.length; ++i) {
+ foo[i] = (byte) (2 + i);
+ }
+ b.write(foo);
+
+ byte[] result = b.toByteArray();
+ assertEquals(100, result.length);
+ for (int i = 0; i < 100; ++i) {
+ assertEquals(i, (int) result[i]);
+ }
+
+ b.release();
+ b.close();
+ }
+}
diff --git a/src/test/java/com/fasterxml/jackson/core/util/TestDefaultPrettyPrinter.java b/src/test/java/com/fasterxml/jackson/core/util/TestDefaultPrettyPrinter.java
index 0d581a8..ad1a7e7 100644
--- a/src/test/java/com/fasterxml/jackson/core/util/TestDefaultPrettyPrinter.java
+++ b/src/test/java/com/fasterxml/jackson/core/util/TestDefaultPrettyPrinter.java
@@ -37,7 +37,7 @@
public void testWithIndent() throws IOException
{
PrettyPrinter pp = new DefaultPrettyPrinter()
- .withObjectIndenter(new DefaultIndenter().withIndent(" "));
+ .withObjectIndenter(new DefaultIndenter().withLinefeed("\n").withIndent(" "));
String EXP = "{\n" +
" \"name\" : \"John Doe\",\n" +
" \"age\" : 3.14\n" +
@@ -84,7 +84,7 @@
public void testRootSeparator() throws IOException
{
- PrettyPrinter pp = new DefaultPrettyPrinter()
+ DefaultPrettyPrinter pp = new DefaultPrettyPrinter()
.withRootSeparator("|");
final String EXP = "1|2|3";
@@ -107,6 +107,27 @@
gen.writeNumber(3);
gen.close();
assertEquals(EXP, bytes.toString("UTF-8"));
+
+ // Also: let's try removing separator altogether
+ pp = pp.withRootSeparator((String) null)
+ .withArrayIndenter(null)
+ .withObjectIndenter(null)
+ .withoutSpacesInObjectEntries();
+ sw = new StringWriter();
+ gen = JSON_F.createGenerator(sw);
+ gen.setPrettyPrinter(pp);
+
+ gen.writeNumber(1);
+ gen.writeStartArray();
+ gen.writeNumber(2);
+ gen.writeEndArray();
+ gen.writeStartObject();
+ gen.writeFieldName("a");
+ gen.writeNumber(3);
+ gen.writeEndObject();
+ gen.close();
+ // no root separator, nor array, object
+ assertEquals("1[2]{\"a\":3}", sw.toString());
}
private String _printTestData(PrettyPrinter pp, boolean useBytes) throws IOException
diff --git a/src/test/java/com/fasterxml/jackson/core/util/TestDelegates.java b/src/test/java/com/fasterxml/jackson/core/util/TestDelegates.java
index 468d3b4..9cce04a 100644
--- a/src/test/java/com/fasterxml/jackson/core/util/TestDelegates.java
+++ b/src/test/java/com/fasterxml/jackson/core/util/TestDelegates.java
@@ -6,22 +6,44 @@
public class TestDelegates extends com.fasterxml.jackson.core.BaseTest
{
+ private final JsonFactory JSON_F = new JsonFactory();
+
/**
* Test default, non-overridden parser delegate.
*/
public void testParserDelegate() throws IOException
{
- JsonParser jp = new JsonFactory().createParser("[ 1, true ]");
- assertNull(jp.getCurrentToken());
- assertToken(JsonToken.START_ARRAY, jp.nextToken());
- assertEquals("[", jp.getText());
- assertToken(JsonToken.VALUE_NUMBER_INT, jp.nextToken());
- assertEquals(1, jp.getIntValue());
- assertToken(JsonToken.VALUE_TRUE, jp.nextToken());
- assertTrue(jp.getBooleanValue());
- assertToken(JsonToken.END_ARRAY, jp.nextToken());
- jp.close();
- assertTrue(jp.isClosed());
+ final String TOKEN ="foo";
+
+ JsonParser parser = JSON_F.createParser("[ 1, true, null, { } ]");
+ JsonParserDelegate del = new JsonParserDelegate(parser);
+
+ assertNull(del.getCurrentToken());
+ assertToken(JsonToken.START_ARRAY, del.nextToken());
+ assertEquals("[", del.getText());
+ assertToken(JsonToken.VALUE_NUMBER_INT, del.nextToken());
+ assertEquals(1, del.getIntValue());
+
+ assertToken(JsonToken.VALUE_TRUE, del.nextToken());
+ assertTrue(del.getBooleanValue());
+
+ assertToken(JsonToken.VALUE_NULL, del.nextToken());
+ assertNull(del.getCurrentValue());
+ del.setCurrentValue(TOKEN);
+
+ assertToken(JsonToken.START_OBJECT, del.nextToken());
+ assertNull(del.getCurrentValue());
+
+ assertToken(JsonToken.END_OBJECT, del.nextToken());
+ assertEquals(TOKEN, del.getCurrentValue());
+
+ assertToken(JsonToken.END_ARRAY, del.nextToken());
+
+ del.close();
+ assertTrue(del.isClosed());
+ assertTrue(parser.isClosed());
+
+ parser.close();
}
/**
@@ -29,23 +51,48 @@
*/
public void testGeneratorDelegate() throws IOException
{
+ final String TOKEN ="foo";
+
StringWriter sw = new StringWriter();
- JsonGenerator jg = new JsonFactory().createGenerator(sw);
- jg.writeStartArray();
- jg.writeNumber(13);
- jg.writeNull();
- jg.writeBoolean(false);
- jg.writeEndArray();
- jg.close();
- assertTrue(jg.isClosed());
- assertEquals("[13,null,false]", sw.toString());
+ JsonGenerator g0 = JSON_F.createGenerator(sw);
+ JsonGeneratorDelegate del = new JsonGeneratorDelegate(g0);
+ del.writeStartArray();
+
+ assertEquals(1, del.getOutputBuffered());
+
+ del.writeNumber(13);
+ del.writeNull();
+ del.writeBoolean(false);
+ del.writeString("foo");
+
+ // verify that we can actually set/get "current value" as expected, even with delegates
+ assertNull(del.getCurrentValue());
+ del.setCurrentValue(TOKEN);
+
+ del.writeStartObject();
+ assertNull(del.getCurrentValue());
+ del.writeEndObject();
+ assertEquals(TOKEN, del.getCurrentValue());
+
+ del.writeStartArray(0);
+ del.writeEndArray();
+
+ del.writeEndArray();
+
+ del.flush();
+ del.close();
+ assertTrue(del.isClosed());
+ assertTrue(g0.isClosed());
+ assertEquals("[13,null,false,\"foo\",{},[]]", sw.toString());
+
+ g0.close();
}
public void testNotDelegateCopyMethods() throws IOException
{
- JsonParser jp = new JsonFactory().createParser("[{\"a\":[1,2,{\"b\":3}],\"c\":\"d\"},{\"e\":false},null]");
+ JsonParser jp = JSON_F.createParser("[{\"a\":[1,2,{\"b\":3}],\"c\":\"d\"},{\"e\":false},null]");
StringWriter sw = new StringWriter();
- JsonGenerator jg = new JsonGeneratorDelegate(new JsonFactory().createGenerator(sw), false) {
+ JsonGenerator jg = new JsonGeneratorDelegate(JSON_F.createGenerator(sw), false) {
@Override
public void writeFieldName(String name) throws IOException, JsonGenerationException {
super.writeFieldName(name+"-test");
diff --git a/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java b/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java
index 878224e..ab1d23a 100644
--- a/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java
+++ b/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java
@@ -1,8 +1,5 @@
package com.fasterxml.jackson.core.util;
-import com.fasterxml.jackson.core.util.BufferRecycler;
-import com.fasterxml.jackson.core.util.TextBuffer;
-
public class TestTextBuffer
extends com.fasterxml.jackson.core.BaseTest
{
@@ -77,4 +74,14 @@
}
}
}
+
+ // [Core#182]
+ public void testEmpty() {
+ TextBuffer tb = new TextBuffer(new BufferRecycler());
+ tb.resetWithEmpty();
+
+ assertTrue(tb.getTextBuffer().length == 0);
+ tb.contentsAsString();
+ assertTrue(tb.getTextBuffer().length == 0);
+ }
}
diff --git a/src/test/java/com/fasterxml/jackson/core/util/TestVersionUtil.java b/src/test/java/com/fasterxml/jackson/core/util/TestVersionUtil.java
index ecd0f43..eb8cebd 100644
--- a/src/test/java/com/fasterxml/jackson/core/util/TestVersionUtil.java
+++ b/src/test/java/com/fasterxml/jackson/core/util/TestVersionUtil.java
@@ -19,6 +19,7 @@
VersionUtil.parseVersion("1.2.15-foo", "group", "artifact"));
}
+ @SuppressWarnings("deprecation")
public void testMavenVersionParsing() {
assertEquals(new Version(1, 2, 3, "SNAPSHOT", "foo.bar", "foo-bar"),
VersionUtil.mavenVersionFor(TestVersionUtil.class.getClassLoader(), "foo.bar", "foo-bar"));
diff --git a/src/test/java/perf/EnumByBytesLookup.java b/src/test/java/perf/EnumByBytesLookup.java
new file mode 100644
index 0000000..1f757ac
--- /dev/null
+++ b/src/test/java/perf/EnumByBytesLookup.java
@@ -0,0 +1,173 @@
+package perf;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+/**
+ * Trie container/wrapper, in this case implements Enum-value lookup.
+ * Sample code to possibly use for streamlined-lookup by dictionary, using
+ * UTF-8 bytes of {@link Enum#name()} as the key.
+ */
+public class EnumByBytesLookup<E extends Enum<E>>
+{
+ private final static Charset UTF8 = Charset.forName("UTF-8");
+
+ private final Trie<E> _root;
+ private final int _size;
+
+ private EnumByBytesLookup(Trie<E> root, int size) {
+ _root = root;
+ _size = size;
+ }
+
+ public static <EIN extends Enum<EIN>> EnumByBytesLookup<EIN> buildFor(Class<EIN> enumClass)
+ {
+ Trie<EIN> root = new Trie<EIN>(null);
+ int size = 0;
+ for (EIN en : enumClass.getEnumConstants()) {
+ byte[] key = en.name().getBytes(UTF8);
+ root = root.with(en, key);
+ ++size;
+ }
+ return new EnumByBytesLookup<EIN>(root, size);
+ }
+
+ public E find(byte[] rawId) {
+ return _root.find(rawId);
+ }
+
+ public int size() { return _size; }
+}
+
+/**
+ * Trie nodes
+ */
+class Trie<T> {
+ private final static byte[] NO_BYTES = new byte[0];
+
+ private final static Trie<?>[] NO_NODES = new Trie<?>[0];
+
+ /**
+ * For leaves, value matched by sequence
+ */
+ private final T _match;
+
+ private final byte[] _nextBytes;
+ private final Trie<T>[] nextNodes;
+
+ private final int nextCount;
+
+ @SuppressWarnings("unchecked")
+ Trie(T match) {
+ this(match, NO_BYTES, (Trie<T>[]) NO_NODES);
+ }
+
+ private Trie(T match, byte[] nextBytes, Trie<T>[] nextNodes) {
+ this._match = match;
+ this._nextBytes = nextBytes;
+ this.nextNodes = nextNodes;
+ nextCount = nextBytes.length;
+ }
+
+ private Trie(Trie<T> base, T match) {
+ // should we allow duplicate calls with same match? For now, let's not
+ if (base._match != null) {
+ throw new IllegalArgumentException("Trying to add same match multiple times");
+ }
+ this._match = match;
+ _nextBytes = base._nextBytes;
+ nextNodes = base.nextNodes;
+ nextCount = base.nextCount;
+ }
+
+ private Trie(Trie<T> base, byte nextByte, Trie<T> nextNode) {
+ // should we allow duplicate calls with same match? For now, let's not
+ if (base._match != null) {
+ throw new IllegalArgumentException("Trying to add same match multiple times");
+ }
+ _match = base._match;
+ int size = base._nextBytes.length + 1;
+ _nextBytes = Arrays.copyOf(base._nextBytes, size);
+ _nextBytes[size-1] = nextByte;
+ nextNodes = Arrays.copyOf(base.nextNodes, size);
+ nextNodes[size-1] = nextNode;
+ nextCount = size;
+ }
+
+ /**
+ * Constructor used when an existing branch needs to be replaced due to addition
+ */
+ private Trie(Trie<T> base, int offset, Trie<T> newNode) {
+ _match = base._match;
+ // can keep nextBytes, as they don't change
+ _nextBytes = base._nextBytes;
+ // but must create a copy of next nodes, to modify one entry
+ nextNodes = Arrays.copyOf(base.nextNodes, base.nextNodes.length);
+ nextNodes[offset] = newNode;
+ nextCount = base.nextCount;
+ }
+
+ /**
+ * "Mutant factory" method: constructs a modified Trie, with specified raw id
+ * added.
+ */
+ public Trie<T> with(T match, byte[] rawId) {
+ return with(match, rawId, 0, rawId.length);
+ }
+
+ private Trie<T> with(T match, byte[] rawId, int start, int end) {
+ if (start == end) {
+ return new Trie<T>(this, match);
+ }
+ // Ok: two choices; either we follow existing branch; or need to create new one
+ final byte b = rawId[start++];
+ for (int i = 0; i < nextCount; ++i) {
+ if (_nextBytes[i] == b) {
+ // existing branch: good day for delegation...
+ Trie<T> old = nextNodes[i];
+ // to keep things truly immutable, copy underlying arrays, then
+ return new Trie<T>(this, i, old.with(match, rawId, start, end));
+ }
+ }
+ // simplest recursively, but for fun let's convert to iteration. Start with tail
+ Trie<T> curr = new Trie<T>(match);
+
+ for (int i = end-1; i >= start; --i) {
+ curr = new Trie<T>(this, rawId[i], curr);
+ }
+ return new Trie<T>(this, b, curr);
+ }
+
+ public T find(byte[] id) {
+ return find(id, 0, id.length);
+ }
+
+ public T find(byte[] id, int offset, int length) {
+ Trie<T> t = this;
+ final int end = offset+length;
+
+ for (; offset < end; ++offset) {
+ byte b = id[offset];
+ t = t.next(b);
+ if (t == null) {
+ // NOTE: if using null-padding, would trim here
+ /*
+ if (b == (byte) 0) {
+ break;
+ }
+ */
+ return null;
+ }
+ }
+ return t._match;
+ }
+
+ private Trie<T> next(int b) {
+ for (int i = 0; i < nextCount; ++i) {
+ if (_nextBytes[i] == b) {
+ return nextNodes[i];
+ }
+ }
+ return null;
+ }
+}