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

com.addthis.codec.kv.CodecKV Maven / Gradle / Ivy

There is a newer version: 3.8.2
Show newest version
/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.addthis.codec.kv;

import java.lang.reflect.Array;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import com.addthis.basis.kv.KVPair;
import com.addthis.basis.kv.KVPairs;
import com.addthis.basis.util.Base64;
import com.addthis.basis.util.Bytes;

import com.addthis.codec.Codec;
import com.addthis.codec.reflection.Fields;
import com.addthis.codec.codables.SuperCodable;
import com.addthis.codec.reflection.CodableClassInfo;
import com.addthis.codec.reflection.CodableFieldInfo;
import com.addthis.codec.util.CodableStatistics;

public final class CodecKV extends Codec {
    private CodecKV() {}

    public static final CodecKV INSTANCE = new CodecKV();

    @Override
    public Object decode(Object shell, byte[] data) throws Exception {
        return decodeString(Fields.getClassFieldMap(shell.getClass()), shell,
                            new KVPairs(Bytes.toString(data)));
    }

    @Override
    public byte[] encode(Object obj) throws Exception {
        return Bytes.toBytes(encodeString(obj));
    }

    @Override
    public CodableStatistics statistics(Object obj) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean storesNull(byte data[]) {
        throw new UnsupportedOperationException();
    }

    //    @SuppressWarnings("unchecked")
    public static String encodeString(Object object) throws Exception {
        if (object instanceof SuperCodable) {
            ((SuperCodable) object).preEncode();
        }
        KVPairs kv = new KVPairs();
        CodableClassInfo fieldMap = Fields.getClassFieldMap(object.getClass());
        if (fieldMap.size() == 0) {
            return object.toString();
        }
        String stype = fieldMap.getClassName(object);
        if (stype != null) {
            kv.putValue(fieldMap.getClassField(), stype);
        }
        for (Iterator fields = fieldMap.values().iterator(); fields.hasNext(); ) {
            CodableFieldInfo field = fields.next();
            Object value = field.get(object);
            if (value == null) {
                continue;
            }
            String name = field.getName();
            if (field.isArray()) {
                int len = Array.getLength(value);
                if (field.getType() == byte.class) {
                    kv.putValue(name, new String(Base64.encode((byte[]) value)));
                } else {
                    for (int i = 0; i < len; i++) {
                        Object av = Array.get(value, i);
                        kv.putValue(name + i, encodeString(av));
                    }
                }
            } else if (field.isCollection()) {
                Collection coll = (Collection) value;
                int idx = 0;
                for (Iterator iter = coll.iterator(); iter.hasNext(); idx++) {
                    kv.putValue(name + idx, encodeString(iter.next()));
                }
            } else if (field.isMap()) {
                Map map = (Map) value;
                KVPairs nv = new KVPairs();
                for (Entry entry : map.entrySet()) {
                    nv.putValue(entry.getKey().toString(), encodeString(entry.getValue()));
                }
                kv.putValue(field.getName(), nv.toString());
            } else if (field.isNative()) {
                kv.putValue(name, value.toString());
            } else if (field.isEnum()) {
                kv.putValue(name, value.toString());
            } else if (field.isCodable()) {
                kv.putValue(name, encodeString(value));
            }
        }
        return kv.toString();
    }

    public static Object decodeArray(Class type, KVPairs data, String name) throws ArrayIndexOutOfBoundsException, IllegalArgumentException, Exception {
        if (type == byte.class) {
            String text = data.getValue(name);
            if (text != null) {
                return Base64.decode(text.toCharArray());
            }
        } else {
            List arr = getList(data, name);
            int size = arr.size();
            if (size > 0) {
                Object value = Array.newInstance(type, size);
                for (int i = 0; i < size; i++) {
                    Array.set(value, i, decodeString(type, arr.get(i)));
                }
                return value;
            }
        }
        return null;
    }

    public static Object decodeString(Class type, String kv) throws Exception {
        if (Fields.isNative(type)) {
            return decodeNative(type, kv);
        } else {
            return decodeString(Fields.getClassFieldMap(type), type, kv);
        }
    }

    public static Object decodeString(CodableClassInfo fieldMap, Class type, String kv) throws Exception {
        if (fieldMap.size() == 0) {
            return decodeNative(type, kv);
        }
        return decodeKV(fieldMap, type, new KVPairs(kv));
    }

    public static Object decodeKV(Class type, KVPairs data) throws Exception {
        return decodeString(Fields.getClassFieldMap(type), type, data);
    }

    public static Object decodeKV(CodableClassInfo fieldMap, Class type, KVPairs data) throws Exception {
        String stype = data.getValue(fieldMap.getClassField());
        Class atype = stype != null ? fieldMap.getClass(stype) : type;
        if (atype != null && atype != type) {
            fieldMap = Fields.getClassFieldMap(atype);
            type = atype;
        }
        Object object = type.newInstance();
        return decodeString(fieldMap, object, data);
    }

    public static Object decodeString(CodableClassInfo fieldMap, Object object, KVPairs data) throws Exception {
        for (Iterator fields = fieldMap.values().iterator(); fields.hasNext();) {
            CodableFieldInfo field = fields.next();
            String name = field.getName();
            if (field.isArray()) {
                field.set(object, decodeArray(field.getType(), data, name));
            } else if (field.isCollection()) {
                List arr = getList(data, name);
                int size = arr.size();
                if (size > 0) {
                    Collection value = (Collection) field.getType().newInstance();
                    Class vc = field.getCollectionClass();
                    boolean va = field.isCollectionArray();
                    for (int i = 0; i < size; i++) {
                        value.add(decodeString(vc, arr.get(i)));
                    }
                    field.set(object, value);
                }
            } else if (field.isMap()) {
                Map map = (Map) field.getType().newInstance();
                // value type, assume key is String
                Class kc = field.getMapKeyClass();
                Class vc = field.getMapValueClass();
                for (KVPair p : new KVPairs(data.getValue(name))) {
                    map.put(p.getKey(), decodeString(vc, p.getValue()));
                }
                field.set(object, map);
            } else if (field.isCodable()) {
                field.set(object, decodeString(field.getType(), data.getValue(name)));
            } else if (field.isEnum()) {
                field.set(object, decodeEnum((Class) field.getType(),
                                             data.getValue(name)));
            } else if (field.isNative()) {
                field.set(object, decodeNative(field.getType(), data.getValue(name)));
            }
        }
        if (object instanceof SuperCodable) {
            ((SuperCodable) object).postDecode();
        }
        return object;
    }

    private static List getList(KVPairs data, String name) {
        List arr = new ArrayList();
        for (int i = 0;; i++) {
            String val = data.getValue(name + i);
            if (val != null) {
                arr.add(val);
            } else {
                break;
            }
        }
        return arr;
    }

    // compresses nested kv strings by tick-level-encoding
    public static String tickCode(String s) {
        char c[] = s.toCharArray();
        int mod;
        int pos;
        int pass = 0;
        boolean done;
        do {
            done = true;
            mod = 1;
            pos = 0;
            char nc[] = new char[c.length + 1];
            for (int i = 0; i < c.length; i++) {
                if (i > 0 && c[i] == '\0') {
                    break;
                }
                if (i == 0 && pass == 0) {
                    nc[pos++] = '&';
                }
                if (c[i] == '%' && i < c.length - 2) {
                    int hd1 = Bytes.hex2dec(c[i + 1]);
                    int hd2 = Bytes.hex2dec(c[i + 2]);
                    if (hd1 >= 0 && hd2 >= 0) {
                        char ch = (char) ((hd1 << 4) | hd2);
                        switch (ch) {
                            case '&':
                                for (int k = 0; k < pass + 1; k++) {
                                    nc[pos++] = '`';
                                }
                                nc[pos++] = '1';
                                mod++;
                                break;
                            case '=':
                                for (int k = 0; k < pass + 1; k++) {
                                    nc[pos++] = '`';
                                }
                                nc[pos++] = '2';
                                mod++;
                                break;
                            case '`':
                                nc[pos++] = '`';
                                nc[pos++] = '`';
                                break;
                            case '%':
                                mod++;
                                done = false;
                            default:
                                nc[pos++] = ch;
                                break;
                        }
                        i += 2;
                    } else {
                        nc[pos++] = c[i];
                    }
                } else if (c[i] == '+') {
                    nc[pos++] = ' ';
                    mod++;
                } else {
                    nc[pos++] = c[i];
                }
            }
            if (mod > 0) {
                c = nc;
            }
            pass++;
//            System.out.println(pass+" pass [ "+new String(nc,0,pos)+" ] "+c.length+" "+pos+" "+mod+" "+done);
        }
        while (!done);
//        c[pos++] = ' ';
        return new String(c, 0, pos);
    }

    public static Object decodeNative(Class type, String text) {
        if (type == String.class) {
            return text;
        } else if (type == Integer.class || type == int.class) {
            return text != null ? Integer.parseInt(text) : 0;
        } else if (type == Long.class || type == long.class) {
            return text != null ? Long.parseLong(text) : 0L;
        } else if (type == Boolean.class || type == boolean.class) {
            return text != null && Boolean.parseBoolean(text);
        } else if (type == Short.class || type == short.class) {
            return text != null ? Short.parseShort(text) : 0;
        } else if (type == Double.class || type == double.class) {
            return text != null ? Double.parseDouble(text) : 0;
        } else if (type == Float.class || type == float.class) {
            return text != null ? Float.parseFloat(text) : 0;
        } else if (type == AtomicLong.class) {
            return text != null ? new AtomicLong(Long.parseLong(text)) : new AtomicLong(0);
        } else if (type == AtomicInteger.class) {
            return text != null ? new AtomicInteger(Integer.parseInt(text)) : new AtomicInteger(0);
        } else if (type == AtomicBoolean.class) {
            return text != null ?
                   new AtomicBoolean(Boolean.parseBoolean(text)) :
                   new AtomicBoolean(false);
        } else if (type.isEnum()) {
            return Enum.valueOf((Class) type, text);
        } else {
            return text;
        }
    }

    public static Object decodeEnum(Class type, String text) {
        return Enum.valueOf(type, text);
    }
}