The BFO JSON package is yet another Java JSON parser, that has grown to add a few more formats.
The JSON and CBOR package is in com.bfo.json
. It has the follow emphasis:
put
, retrieved with get
, read with read
and written with write
. Collections are used for maps and lists, and you can use the whole API with no more than about 5 or 6 methods. Which means although the API is fully documented, you can probably get away without reading any of it.git clone http://github.com/faceless2/json.git
. Type ant
. Jar is in dist
, docs are in docs
Mapping JavaScript objects to a Java object can be done by use of a JsonFactory, however this is done after the object is read. Most of the complexity of other Java Json APIs comes from the mapping process between Json and Java objects; if you want to go down this route you have a trivially simple interface to implement, but you’re on your own.
JavaScript is loosely typed, and this API acknowleges this: if you request an int value from a Json string, it will try to parse the String as an integer. If you don’t want this, see the JsonReadOptions to turn it off.
Json in the wild has many variations - comments are embedded, maps have unquoted keys, and so on. By default the API will adhere closely to RFC8259 when reading or writing, although this can be changed. Again see JsonReadOptions.
When reading Json numbers, they will be mapped to ints, longs, BigIntegers and double as necessary. If BigDecimal support is required, this can be turned on in JsonReadOptions
Json is read from Readers and written to Appendable. You can read from an InputStream too, in which case it will look for a BOM at the start of the stream. CBOR/Msgpack are read from an InputStream and written to an OutputStream. Errors encountered during reading are reported with context, line and column numbers (for JSON) or byte offset (for CBOR/Msgpack).
CBOR serialization offers two complexities that are not supported in this API: duplicate keys in maps and “special” types that are not defined. Duplicate keys encountered during reading throw an IOException, and special types (which should really only be encountered while testing) are converted to a tagged null object. Tags are limited to 63 bits, and tags applied to Map keys are ignored.
CBOR serialization will convert tag types 2 and 3 on a “buffer” to BigInteger, as described in RFC7049.
But other tags used to distinguish Dates, non-UTF8 strings, URLs etc. are not applied.
A JsonFactory
can easily be written to cover as many of these are needed.
Msgpack serialization is similar to CBOR, but simpler. “extension types” are stored as Buffers, with the extension type stored as a tag from 0..255. Like CBOR, duplicate keys encountered during read will throw an IOException.
It’s possible (since v4) to read and write indefinitely large strings and buffers - the JsonReadOptions.Filter class can be used to divert content away to a File, for example. The use of intermediate buffers has been kept to an absolute minimum.
// The basics
Json json = Json.read("{}"}; // Create a new map
json.put("a", "apples"); // Add a string
json.putPath("b.c", new Json("oranges")); // Add an intermediate map and another string
json.putPath("b.c[1]", 3}; // Replace previous string with an array, add a null then a number.
json.putPath("\"d.e\"", true); // Add a key containing a quote character
System.out.println(json); // {"a":"apples","b":{"c":[null,3]},"d.e":true}
json.write(System.out, null); // The same as above, but doesn't serialize to a String first.
System.out.println(json.getPath("b.c[1]").stringValue()); // "3"
System.out.println(json.getPath("b.c[1]").intValue()); // 3
System.out.println(json.get("b").get("c").get(1).intValue()); // 3
// Types
json.put("d", "2");
json.put("e", 0);
System.out.println(json.type()); // "map"
System.out.println(json.isMap()); // true
System.out.println(json.get("d")); // "2"
System.out.println(json.get("d").type()); // "string"
System.out.println(json.get("d").intValue()); // 2 (automatically converted from string)
System.out.println(json.get("d").numberValue().getClass()); // java.lang.Integer
json.put("d", "9999999999");
System.out.println(json.get("d").numberValue().getClass()); // java.lang.Long
json.put("d", "99999999999999999999999999");
System.out.println(json.get("d").numberValue().getClass()); // java.math.BigInteger
System.out.println(json.get("d").booleanValue()); // true
System.out.println(json.get("e").booleanValue()); // false
json.put("e", Json.read("[]")); // Add a new list
System.out.println(json.get("e").type()); // "list"
json.putPath("e[0]", false);
System.out.println(json.get("e")); // [false] - e is a list
json.putPath("e[\"a\"]", true); // this will convert e to a map
System.out.println(json.get("e")); // {"0":false,"a":true}
json.setValue(new Json("string")); // copy value from specified object
System.out.println(json.value()); // "string"
// Serialization
json = Json.read("{b:1, a: 2}"); // Fails, keys is not quoted
json = Json.read(new StringReader("{b: 1, a: 2}"), new JsonReadOptions().setAllowUnquotedKey(true)); // OK
json.write(System.out, new JsonWriteOptions().setPretty(true).setSorted(true)); // pretty print and sort keys
// {
// "a": 2,
// "b": 1,
// }
// CBOR / Msgpack
json.put("buffer", ByteBuffer.wrap(new byte[] { ... })); // add a ByteBuffer
System.out.println(json.get("buffer").type()); // "buffer"
System.out.println(json.get("buffer").stringValue()); // Base64 encoded value of buffer
json.setTag(20); // Set a CBOR tag on a value
json.writeCbor(new OutputStream(...), null); // serialize the same JSON object to CBOR
json = Json.readCbor(new InputStream(...), null); // read CBOR from an Inputream
System.out.println(json.get("buffer").getTag()); // "20"
System.out.println(json.get("b").getTag()); // "-1", which means no tag
json.put("nan", Double.NaN);
json.writeCbor(new OutputStream(...), null); // infinity is fine in CBOR
json.write(new StringWriter(), null); // throws IOException - infinity not allowed in Json
json.write(new StringWriter(), new JsonWriteOptions().setAllowNaN(true)); // infinity serializes as null
json.writeMsgpack(new OutputStream(...), null); // Msgpack instead of CBOR
// Events
json.addListener(new JsonListener() {
public void jsonEvent(Json root, JsonEvent event) {
if (event.after == null) {
System.out.println("Removed " + root.find(event.before));
} else if (event.before == null) {
System.out.println("Added " + root.find(event.after));
} else {
System.out.println("Changed " + root.find(event.after) + " from " + event.before+" to " + event.after);
}
}
});
json.putPath("a.b", true); // "Added a.b"
json.getPath("a.b").put("c", true); // "Added a.b.c"
json.getPath("a.b").put("c", false); // "Changed a.b.c" from true to false
json.removePath("a.b"); // "Removed a.b"
// JsonPath
json = Json.parse("{\"a\":{\"b\":{\"c\":[10,20,30,40]}}}");
json.eval("$..c[2]").intValue(); // 30
In addition the JWT class adds basic support for Java Web Tokens. With only a handful of methods it is trivial to use, but supports all JWT signing methods. Encryption with JWE is not supported
JWT jwt = new JWT(Json.parse("{\"iss\":\"foo\"}"));
byte[] secretkeydata = ...
SecretKey key = new JWK(secretkeydata, "HS256").getSecretKey();
jwt.sign(key); // Sign using a symmetric key
jwt = new JWT(jwt.toString()); // Encode then decode
assert jwt.verify(key); // Verify using the same symmetric key
PublicKey pubkey = ...
PrivateKey prikey = ...
jwt.getHeader().put("x5u", ...); // Add custom content to header
jwt.sign(prikey); // Sign using a assymmetric key
assert jwt.verify(pubkey); // Verify using corresponding key
jwt.getPayload().clear(); // Modify the payload
assert !jwt.verify(pubkey); // Signature is no longer valid
COSE was added in version 5. Signing with single or multiple signatures are supported, but counter-signing (for timestamps) is pending and encryption support is not currently planned.
// Signing
COSE cose = new COSE();
cose.setPayload(ByteBuffer.wrap("Hello, World".getBytes("UTF-8")));
cose.sign(privateKey); // Sign using a private key, eg ES256
cose.writeCBOR(..., null); // Write COSE to stream, or...
ByteBuffer buf = cose.toCbor(); // Write COSE to ByteBuffer
// Verifying
Json json = Json.readCBOR(..., null); // Reload COSE
cose = new COSE(json);
String s = new String(cose.getPayload().array(), "UTF-8"); // Hello, World
assert jwt.verify(publicKey) == 0; // Verify with the public key
For both JWT and COSE, the JWK utility class can convert
between the Java PublicKey
, PrivateKey
and SecretKey
implementations and their JWK or COSE-key representations.
Version 5 adds the com.bfo.box
package, which contains a general purpose parser for the
ISO Base Media Format, or BMFF. This is a standard box model used in a number of file formats,
including MP4, JP2 etc. The parser is very general, and will not load unrecognised boxes into memory so
can be used to scan large files for metadata (which is the primary reason we use it; most of the
currently recognised boxes have a metadata focus).
import com.bfo.box.*;
Box box;
BoxFactory factory = new BoxFactory();
InputStream in = new BufferedInputStream(new FileInputStream("file.mpf"));
while ((box=factory.load(in)) != null) {
traverse(box, "");
}
void traverse(Box box, String prefix) {
System.out.println(prefix + box);
for (Box b=box.first();b!=null;b=b.next()) {
traverse(box, prefix + " ");
}
}
A specific use of BMFF is C2PA, and most of this package are
classes to read and write C2PA objects (“stores”), including a
helper class
to deal with embdding them into JPEG.
While the C2PA format is built on BMFF boxes, those boxes typically contain JSON or CBOR
and the signature is COSE. So this package makes heavy use of com.bfo.json
.
The C2PAStore class is the top level entrypoint into the C2PA package. Here’s a quick example showing how to verify C2PA embedded in a JPEG
import com.bfo.box.*;
import com.bfo.json.*;
Json json = C2PAHelper.readJPEG(new FileInputStream(file));
if (json.has("c2pa")) {
C2PAStore c2pa = (C2PAStore)new BoxFactory().load(json.bufferValue("c2pa"));
C2PAManifest manifest = c2pa.getActiveManifest();
manifest.setInputStream(new FileInputStream(file));
boolean valid = true;
for (C2PAStatus status : manifest.getSignature().verify(keystore)) {
System.out.println(status);
if (status.isError()) {
valid = false;
}
}
}
and here’s how to sign a JPEG with the bare minimum single assertion.
import com.bfo.box.*;
import com.bfo.json.*;
PrivateKey key = ...
List<X509Certificate> certs = ...
C2PAStore c2pa = new C2PAStore();
C2PAManifest manifest = new C2PAManifest("urn:manifestid");
c2pa.getManifests().add(manifest);
C2PAClaim claim = manifest.getClaim();
C2PASignature sig = manifest.getSignature();
claim.setFormat("image/jpeg");
claim.setInstanceID("urn:instanceid");
manifest.getAssertions().add(new C2PA_AssertionHashData());
manifest.getSignature().setSigner(key, certs);
Json json = C2PAHelper.readJPEG(new FileInputStream("unsigned.jpg"));
C2PAHelper.writeJPEG(json, c2pa, new FileOutputStream("signed.jpg"));
There is a main()
method on the C2PAHelper
class which can be used for basic
operations on JPEG files. To run it, download the Jar then
java -cp bfojson-5.jar com.bfo.box.C2PAHelper
java com.bfo.box.C2PAHelper args...
--help this help
--verify switch to verify mode (the default)
--sign switch to signing mode
--debug turn on debug to dump the c2pa store as CBOR-diag
--boxdebug turn on debug to dump the c2pa store as a box tree
--nodebug turn off --debug
--noboxdebug turn off --boxdebug
--repackage if signing a file with an existing C2PA, reference it from a 'repackage' action
--keystore <path> if signing, the path to Keystore to load credentials from
--alias <name> if signing, the alias from the keystore (default is the first one)
--password <password> if signing, the password to open the keystore
--alg <algorithm> if signing, the hash algorithm
--creativework <path> if signing, filename containing a JSON schema to embed
--out <path> if signing, filename to write signed output to (default will derive from input)
--c2pa <path> if signing/verifying, filename to dump the C2PA object to (default is not dumped)
<path> the filename to sign or verify
The C2PA classes have been developed against C2PA 1.2; output from earlier versions may not verify.
NOTE: The C2PA classes are under development, so changes are likely
This code is written by the team at bfo.com. If you like it, come and see what else we do.