All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.apicatalog.cborld.decoder.Decoder Maven / Gradle / Ivy

The newest version!
package com.apicatalog.cborld.decoder;

import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.net.URI;
import java.util.Collection;
import java.util.List;

import com.apicatalog.cborld.CborLd;
import com.apicatalog.cborld.config.DefaultConfig;
import com.apicatalog.cborld.config.DictionaryAlgorithm;
import com.apicatalog.cborld.context.ContextError;
import com.apicatalog.cborld.decoder.DecoderError.Code;
import com.apicatalog.cborld.decoder.value.ValueDecoder;
import com.apicatalog.cborld.dictionary.Dictionary;
import com.apicatalog.cborld.encoder.Encoder;
import com.apicatalog.cborld.hex.Hex;
import com.apicatalog.cborld.loader.StaticContextLoader;
import com.apicatalog.cborld.mapper.Mapping;
import com.apicatalog.cborld.mapper.MappingProvider;
import com.apicatalog.cborld.mapper.TypeMap;
import com.apicatalog.jsonld.JsonLdOptions;
import com.apicatalog.jsonld.http.DefaultHttpClient;
import com.apicatalog.jsonld.http.media.MediaType;
import com.apicatalog.jsonld.json.JsonUtils;
import com.apicatalog.jsonld.loader.DocumentLoader;
import com.apicatalog.jsonld.loader.HttpLoader;

import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.Array;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.DoublePrecisionFloat;
import co.nstant.in.cbor.model.HalfPrecisionFloat;
import co.nstant.in.cbor.model.MajorType;
import co.nstant.in.cbor.model.Map;
import co.nstant.in.cbor.model.NegativeInteger;
import co.nstant.in.cbor.model.SimpleValue;
import co.nstant.in.cbor.model.SinglePrecisionFloat;
import co.nstant.in.cbor.model.Special;
import co.nstant.in.cbor.model.UnicodeString;
import co.nstant.in.cbor.model.UnsignedInteger;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonObject;
import jakarta.json.JsonObjectBuilder;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;

public class Decoder implements DecoderConfig {

    protected final byte[] encoded;
    protected final boolean compressed;
        
    protected MappingProvider provider;
    protected Dictionary index;
    
    // options
    protected Collection valueDecoders;
    protected boolean compactArrays;
    protected DocumentLoader loader;
    protected boolean bundledContexts;
    protected URI base;
    
    protected Decoder(byte[] encoded, boolean compressed) {
        this.encoded = encoded;
        this.compressed = compressed;
        
        // default options
        config(DefaultConfig.INSTANCE);
        
        this.bundledContexts = DefaultConfig.STATIC_CONTEXTS;
        this.base = null;
        this.loader = null;
    }

    /**
     * If set to true, the encoder replaces arrays with
     * just one element with that element during encoding saving one byte.
     * Enabled by default.
     *
     * @param enable true to enable arrays compaction
     * @return {@link Decoder} instance
     *
     */
    public Decoder compactArray(boolean enable) {
        compactArrays = enable;
        return this;
    }

    /**
     * Override any existing configuration by the given configuration set.
     * 
     * @param config a configuration set 
     * @return {@link Encoder} instance
     */
    public Decoder config(DecoderConfig config) {
        this.compactArrays = config.isCompactArrays();
        this.valueDecoders = config.valueDecoders();
        this.provider = config.provider();
        return this;
    }
    
    /**
     * Set {@link DocumentLoader} used to fetch referenced JSON-LD contexts. 
     * If not set then default document loader provided by {@link JsonLdOptions} is used. 
     * 
     * @param loader a document loader to set
     * @return {@link Decoder} instance
     */
    public Decoder loader(DocumentLoader loader) {
        this.loader = loader;
        return this;
    }
    
    /**
     * Use well-known contexts that are bundled with the library instead of fetching it online.
     * true by default. Disabling might cause slower processing.
     *
     * @param enable true to use static bundled contexts
     * @return {@link Decoder} instance
     */
    public Decoder useBundledContexts(boolean enable) {
        this.bundledContexts = enable;
        return this;
    }
    
    /**
     * If set, then is used as the input document's base IRI.
     *
     * @param base a document base
     * @return {@link Decoder} instance
     */
    public Decoder base(URI base) {
       this.base = base;
       return this;
    }
    
    public static final Decoder create(byte[] encodedDocument) throws DecoderError {
    
        if (encodedDocument == null) {
            throw new IllegalArgumentException("The encoded document paramenter must not be null but byte arrayy.");
        }
    
        if (encodedDocument.length < 4) {
            throw new DecoderError(Code.InvalidDocument,
                "The encoded document must be at least 4 bytes but is [" + encodedDocument.length + "].");
        }
    
        if (encodedDocument[0] != CborLd.CBOR_LD_BYTE_PREFIX[0]
            || encodedDocument[1] != CborLd.CBOR_LD_BYTE_PREFIX[1]) {
            throw new DecoderError(Code.InvalidDocument, "The document is not CBOR-LD document.");
        }
    
        if (encodedDocument[2] == CborLd.COMPRESSED_V1) {
            return new Decoder(encodedDocument, true);
        }
    
        if (encodedDocument[2] == CborLd.UNCOMPRESSED) {
            return new Decoder(encodedDocument, false);
        }
    
        throw new DecoderError(Code.UnknownCompression,
            "Unkknown CBOR-LD document compression, expected 0x00 - uncompressed or 0x01 - compressed, but found ["
                + Hex.toString(encodedDocument[2]) + "].");
    }
    
    /**
     * Decode  CBOR-LD document as JSON-LD document.
     * 
     * @return a decoded CBOR-LD document
     *
     * @throws ContextError
     * @throws DecoderError 
     */
    public JsonValue decode() throws ContextError, DecoderError {

        if (loader == null) {
            loader = new HttpLoader(DefaultHttpClient.defaultInstance());
            ((HttpLoader)loader).setFallbackContentType(MediaType.JSON);
        }
        
        if (bundledContexts) {
            loader = new StaticContextLoader(loader);
        }
        
        if (compressed) {
            return decodeCompressed();
        }
        return decodeUncompressed();
    }

    final JsonValue decodeCompressed() throws DecoderError, ContextError {
            
        try {
            final ByteArrayInputStream bais = new ByteArrayInputStream(encoded);
            final List dataItems = new CborDecoder(bais).decode();
    
            // nothing do de-compress
            if (dataItems.isEmpty()) {
                return null;
            }
            
            // decode as an array of objects
            if (dataItems.size() > 1) {
    
                final JsonArrayBuilder builder = Json.createArrayBuilder();
        
                for (final DataItem item : dataItems) {

                    builder.add(decodeCompressed(item));
                }
        
                return builder.build();
            }
    
            return decodeCompressed(dataItems.iterator().next());
    
        } catch (final CborException e) {
            throw new DecoderError(Code.InvalidDocument, e);            
        }
    }

    final JsonValue decodeCompressed(final DataItem data) throws DecoderError, ContextError {
  
        final Mapping mapping = provider.getDecoderMapping(data, base, loader, this);
            
        index = mapping.dictionary();

        return decodeData(data, null, mapping.typeMap());
    }

    final JsonValue decodeData(final DataItem data, final String term, final TypeMap def) throws DecoderError, ContextError {
    
        if (data == null) {
            throw new IllegalArgumentException("The data parameter must not be null.");
        }
    
        switch (data.getMajorType()) {
        case MAP:
            return decodeMap((Map) data, term != null ? def.getMapping(term) : def);
    
        case ARRAY:
            return decodeArray(((Array) data).getDataItems(), term, def);
    
        case UNICODE_STRING:
            return decodeString((UnicodeString) data, term);
    
        case UNSIGNED_INTEGER:
            return decodeInteger(data, term, def);
            
        case SPECIAL:
            return decode((Special) data);
            
        case NEGATIVE_INTEGER:
            return Json.createValue(((NegativeInteger)data).getValue());
            
        case BYTE_STRING:
            JsonValue decoded = decodeValue(data, term, def);
            if (decoded == null) {
                throw new DecoderError(Code.InvalidDocument, "Unknown encoded value [" + data.getMajorType() + "] at key [" + term + "].");
            }
            
            return decoded;
    
        default:
            throw new IllegalStateException("An unexpected data item type [" + data.getMajorType() + "].");
        }
    }

    final JsonObject decodeMap(final Map map, final TypeMap def) throws DecoderError, ContextError {
    
        if (map == null) {
            throw new IllegalArgumentException("The map parameter must not be null.");
        }
    
        if (map.getKeys().isEmpty()) {
            return JsonValue.EMPTY_JSON_OBJECT;
        }
    
        final JsonObjectBuilder builder = Json.createObjectBuilder();
    
        for (final DataItem key : map.getKeys()) {
    
            final DataItem value = map.get(key);
            
            boolean isArray = MajorType.UNSIGNED_INTEGER.equals(key.getMajorType())
                                && !((UnsignedInteger)key).getValue().mod(BigInteger.ONE.add(BigInteger.ONE)).equals(BigInteger.ZERO)
                                ;
            
            JsonValue json = null;
            String term = decodeKey(key);
            
            if (!isArray && MajorType.ARRAY.equals(value.getMajorType())) {
                json = decodeValue(value, term, def);
            }
            
            if (json == null) {
                json = decodeData(value, term, def);
                
                if (isArray 
                        && compactArrays
                        && (JsonUtils.isNotArray(json)
                                || json.asJsonArray().size() == 1
                                )
                        ) {
                    
                    json = Json.createArrayBuilder().add(json).build();
                }
            }
            
            
            builder.add(decodeKey(key), json);
        }
    
        return builder.build();
    }
    
    final String decodeKey(final DataItem data) {

        if (data == null) {
            throw new IllegalArgumentException("The data parameter must not be null.");
        }
    
        switch (data.getMajorType()) {
        case UNICODE_STRING:
            return decodeKey(((UnicodeString)data).getString());
    
        case UNSIGNED_INTEGER:
            return decodeKey(((UnsignedInteger)data).getValue());

        default:
            return data.toString();
        }
    }

    final String decodeKey(final String key) {
        return key;
    }
    
    final String decodeKey(final BigInteger key) {
        
        if (key.mod(BigInteger.ONE.add(BigInteger.ONE)).equals(BigInteger.ZERO)) {
            String result = index.getValue(key);
            return result != null ? result : key.toString();
        }        
    
        String result = index.getValue(key.subtract(BigInteger.ONE));

        return result != null ? result : key.toString();
    }

    final JsonArray decodeArray(final Collection items, final String key, final TypeMap def) throws DecoderError, ContextError {
    
        if (items == null) {
            throw new IllegalArgumentException("The items parameter must not be null.");
        }
    
        if (items.isEmpty()) {
            return JsonValue.EMPTY_JSON_ARRAY;
        }
    
        final JsonArrayBuilder builder = Json.createArrayBuilder();
    
        for (final DataItem item : items) {
            builder.add(decodeData(item, key, def));
        }
    
        return builder.build();
    }

    final JsonString decodeString(final UnicodeString string, final String key) {
    
        if (string == null) {
            throw new IllegalArgumentException("The string parameter must not be null.");
        }
        return Json.createValue(string.getString());
    }

    final JsonValue decodeInteger(final DataItem number, final String key, final TypeMap def) throws DecoderError {
    
        if (number == null) {
            throw new IllegalArgumentException("The number parameter must not be null.");
        }
    
        JsonValue decoded = decodeValue(number, key, def);

        if (decoded != null) {
            return decoded;
        }
        
        // fallback
        return Json.createValue(((UnsignedInteger)number).getValue());
    }

    final JsonValue decodeValue(final DataItem value, final String term, final TypeMap def) throws DecoderError {
        if (def != null) { 
            final Collection types = def.getType(term);

            for (final ValueDecoder decoder : valueDecoders) {
                final JsonValue decoded = decoder.decode(index, value, term, types);
                
                if (decoded != null) {
                    return decoded;
                }            
            }
        }
        return null;
    }
    
    final JsonValue decode(final Special value) {
        switch (value.getSpecialType()) {
        case IEEE_754_DOUBLE_PRECISION_FLOAT:
            return Json.createValue( ((DoublePrecisionFloat)value).getValue());
            
        case IEEE_754_HALF_PRECISION_FLOAT:
            return Json.createValue( ((HalfPrecisionFloat)value).getValue());
            
        case IEEE_754_SINGLE_PRECISION_FLOAT:
            return Json.createValue( ((SinglePrecisionFloat)value).getValue());
            
        case SIMPLE_VALUE:
            return decode((SimpleValue) value);
            
        default:
            break;
        }
        
        throw new IllegalStateException("Unsupported CBOR special type [" + value.getSpecialType() + "].");
    }
    
    final JsonValue decode(final SimpleValue value) {
        switch (value.getSimpleValueType()) {
        case FALSE:
            return JsonValue.FALSE;
            
        case TRUE:
            return JsonValue.TRUE;
            
        case NULL:
            return JsonValue.NULL;
            
        default:
            break;
        }

        throw new IllegalStateException("Unsupported CBOR simple value type [" + value.getSimpleValueType() + "].");        
    }
 
    final JsonValue decodeUncompressed() throws DecoderError {
        throw new DecoderError(Code.InvalidDocument, "Unsupported document compression algorithm");
    }
    
    @Override
    public boolean isCompactArrays() {
        return compactArrays;
    }

    @Override
    public DictionaryAlgorithm dictonaryAlgorithm() {
        return DictionaryAlgorithm.ProcessingOrderAppliedContexts;
    }

    @Override
    public Collection valueDecoders() {
        return valueDecoders;
    }

    @Override
    public MappingProvider provider() {
        return provider;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy