iristk.util.Record.java Source code

Java tutorial

Introduction

Here is the source code for iristk.util.Record.java

Source

/*******************************************************************************
 * Copyright (c) 2014 Gabriel Skantze.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 * 
 * Contributors:
 *     Gabriel Skantze - initial API and implementation
 ******************************************************************************/
package iristk.util;

import static iristk.util.Converters.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.io.IOUtils;

import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;
import com.eclipsesource.json.ParseException;

import javafx.util.converter.LocalDateStringConverter;
import javafx.util.converter.LocalDateTimeStringConverter;

/**
 * A Record is essentially a map of key-value pairs (much like any java {@link java.util.Map}). However, it supports a convenient way of adding and accessing values deep in the hierarchy by using colon (:) notation.
 * <p> For example you can add a value like this: {@code myRecord.put("foo:bar", 5)}. If the key "foo" does not already contain a nested Record, it will create one. It will then put the value 5 under the key "bar". 
 * <p> You can also access values like this  {@code myRecord.get("foo:bar")}. If it is not possible to traverse "foo" and then "bar" in the nested hierarchy, the method will return null.
 * <p> There are also convenience functions for getting a value of the right type. For example, {@code myRecord.getInt("foo:bar")} will try to convert the value to an Integer. If this fails the method will return null.
 * <p> Using kleen stars (*), it is also possible to search the hierarchy using the {@code has()} method. Thus, {@code myRecord.has("*:bar")} will return true.   
 *  */

public class Record {

    private static HashMap<Class<?>, RecordInfo> recordInfo = new HashMap<>();

    private final HashMap<String, Object> dynamicFields = new HashMap<String, Object>();

    public Record() {
        synchronized (Record.class) {
            if (!recordInfo.containsKey(getClass())) {
                recordInfo.put(getClass(), new RecordInfo(getClass()));
            }
        }
    }

    private RecordInfo getRecordInfo() {
        return recordInfo.get(getClass());
    }

    private static class RecordInfo {
        private final HashMap<String, Field> classFields = new HashMap<>();
        private final HashMap<String, Method> getMethodFields = new HashMap<>();
        private final HashMap<String, Method> setMethodFields = new HashMap<>();
        private final List<String> orderedFields;

        public RecordInfo(Class<? extends Record> clazz) {
            final HashMap<String, Integer> order = new HashMap<>();
            setupFields(clazz, order);
            orderedFields = new ArrayList<String>(getMethodFields.keySet());
            orderedFields.addAll(classFields.keySet());
            Collections.sort(orderedFields, new Comparator<String>() {
                @Override
                public int compare(String o1, String o2) {
                    return order.get(o1) - order.get(o2);
                }
            });
        }

        private void setupFields(Class<? extends Record> clazz, HashMap<String, Integer> order) {
            for (Field field : clazz.getDeclaredFields()) {
                if (field.isAnnotationPresent(RecordField.class)) {
                    field.setAccessible(true);
                    RecordField fieldAnnot = field.getAnnotation(RecordField.class);
                    String name = fieldAnnot.name().equals("DEFAULT") ? field.getName() : fieldAnnot.name();
                    classFields.put(name, field);
                    order.put(name, fieldAnnot.order());
                }
            }
            for (Method method : clazz.getDeclaredMethods()) {
                if (method.isAnnotationPresent(RecordField.class)) {
                    method.setAccessible(true);
                    RecordField fieldAnnot = method.getAnnotation(RecordField.class);
                    String name = fieldAnnot.name().equals("DEFAULT") ? method.getName() : fieldAnnot.name();
                    if (method.getParameterTypes().length == 1) {
                        setMethodFields.put(name, method);
                    } else if (method.getParameterTypes().length == 0) {
                        order.put(name, fieldAnnot.order());
                        getMethodFields.put(name, method);
                    }
                }
            }
            Class s = clazz.getSuperclass();
            if (s != null)
                setupFields(s, order);
        }

    }

    public Record(Map map) {
        this();
        putAll(map);
    }

    public Record(Object... init) {
        this();
        if (init.length % 2 == 1)
            throw new IllegalArgumentException("Can only initialize Record with even number of arguments");
        for (int i = 0; i < init.length; i += 2) {
            put(init[i].toString(), init[i + 1]);
        }
    }

    public Record(Record initRecord, Object... init) {
        this();
        if (init.length % 2 == 1)
            throw new IllegalArgumentException("Can only initialize Record with even number of arguments");
        putAllExceptNull(initRecord);
        for (int i = 0; i < init.length; i += 2) {
            put(init[i].toString(), init[i + 1]);
        }
    }

    public void putAllExceptNull(Record record) {
        for (String field : record.getFields()) {
            if (record.get(field) != null) {
                put(field, record.get(field));
            }
        }
    }

    public void putAll(Record record) {
        for (String field : record.getFields()) {
            put(field, record.get(field));
        }
    }

    public void putAll(Map map) {
        for (Object key : map.keySet()) {
            put(key.toString(), map.get(key));
        }
    }

    public synchronized Class<?> getFieldClass(String fieldName) {
        RecordInfo info = getRecordInfo();
        if (info.classFields.containsKey(fieldName)) {
            return info.classFields.get(fieldName).getType();
        }
        if (info.setMethodFields.containsKey(fieldName)) {
            return info.setMethodFields.get(fieldName).getParameterTypes()[0];
        }
        if (info.getMethodFields.containsKey(fieldName)) {
            return info.getMethodFields.get(fieldName).getReturnType();
        }
        Object val = dynamicFields.get(fieldName);
        if (val != null)
            return val.getClass();
        return null;
    }

    public static Object get(Object obj, String field) {
        if (obj == null) {
            return null;
        } else if (obj instanceof Record) {
            return ((Record) obj).get(field);
        } else if (obj instanceof List) {
            List list = (List) obj;
            try {
                if (field.contains(":")) {
                    int i = field.indexOf(":");
                    String subf = field.substring(0, i);
                    String rest = field.substring(i + 1);
                    int li = Integer.parseInt(subf);
                    if (li < list.size()) {
                        Object sub = list.get(li);
                        return get(sub, rest);
                    }
                } else {
                    int li = Integer.parseInt(field);
                    if (li < list.size()) {
                        return list.get(li);
                    }
                }
            } catch (NumberFormatException e) {
            }
        }
        return null;
    }

    public synchronized Object get(String field) {
        if (field == null)
            return null;
        //if (field.contains(".")) {
        //   System.err.println("Warning: use of dots when accessing record fields is deprecated: " + field);
        //   field = field.replace(".", ":");
        //}
        if (field.contains(":")) {
            int i = field.indexOf(":");
            String subf = field.substring(0, i);
            String rest = field.substring(i + 1);
            Object sub = get(subf);
            return get(sub, rest);
        } else {
            RecordInfo info = getRecordInfo();
            Method getMethod = info.getMethodFields.get(field);
            if (getMethod != null) {
                try {
                    return getMethod.invoke(this);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (IllegalArgumentException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
            Field getField = info.classFields.get(field);
            if (getField != null) {
                try {
                    return getField.get(this);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (IllegalArgumentException e) {
                    e.printStackTrace();
                }
            }
            if (dynamicFields.containsKey(field))
                return dynamicFields.get(field);
            try {
                int i = Integer.parseInt(field);
                if (i < dynamicFields.size()) {
                    return dynamicFields.values().toArray()[i];
                }
            } catch (NumberFormatException e) {
            }
            return null;
        }
    }

    public synchronized void putIfNotNull(String field, Object value) {
        if (value != null)
            put(field, value);
    }

    public synchronized void put(String field, Object value) {
        if (field != null) {
            //if (field.contains(".")) {
            //   System.err.println("Warning: use of dots when accessing record fields is deprecated: " + field);
            //   field = field.replace(".", ":");
            //}
            if (field.contains(":")) {
                int i = field.indexOf(":");
                String fn = field.substring(0, i);
                String rest = field.substring(i + 1);
                String[] subFields;
                if (fn.equals("*")) {
                    subFields = dynamicFields.keySet().toArray(new String[0]);
                } else {
                    subFields = new String[] { fn };
                }
                for (String f : subFields) {
                    Record subRec = getRecord(f);
                    if (subRec != null) {
                        subRec.put(rest, value);
                    } else {
                        Record record = new Record();
                        dynamicFields.put(f, record);
                        record.put(rest, value);
                    }
                }
            } else if (field.equals("*")) {
                for (String f : dynamicFields.keySet()) {
                    dynamicFields.put(f, value);
                }
            } else {
                RecordInfo info = getRecordInfo();
                Method setMethod = info.setMethodFields.get(field);
                if (setMethod != null) {
                    try {
                        setMethod.invoke(this, asType(value, setMethod.getParameterTypes()[0]));
                        return;
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (IllegalArgumentException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
                Field setField = info.classFields.get(field);
                if (setField != null) {
                    try {
                        if (setField.getType().equals(java.time.LocalDate.class)) {
                            setField.set(this, new LocalDateStringConverter().fromString((String) value));
                        } else if (setField.getType().equals(java.time.LocalDateTime.class)) {
                            setField.set(this, new LocalDateTimeStringConverter().fromString((String) value));
                        } else {
                            setField.set(this, asType(value, setField.getType()));
                        }
                        return;
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (IllegalArgumentException e) {
                        e.printStackTrace();
                    }
                }
                dynamicFields.put(field, value);
            }
        }
    }

    public synchronized boolean has(String field) {
        //if (field.contains(".")) {
        //   System.err.println("Warning: use of dots when accessing record fields is deprecated: " + field);
        //   field = field.replace(".", ":");
        //}
        if (field.contains(":")) {
            int i = field.indexOf(":");
            String subField = field.substring(0, i);
            String rest = field.substring(i + 1);
            if (subField.equals("*")) {
                for (String key : getFields()) {
                    Object sub = get(key);
                    if (sub instanceof Record) {
                        if (((Record) sub).has(rest))
                            return true;
                    }
                }
                return false;
            } else {
                Object sub = get(subField);
                if (sub instanceof Record) {
                    return ((Record) sub).has(rest);
                } else {
                    return false;
                }
            }
        } else {
            RecordInfo info = getRecordInfo();
            return info.getMethodFields.containsKey(field) || info.classFields.containsKey(field)
                    || field.equals("*") || dynamicFields.containsKey(field);
        }
    }

    public Object get(String field, Object def) {
        Object value = get(field);
        if (value != null)
            return value;
        else
            return def;
    }

    public String getString(String field, String def) {
        return asString(get(field), def);
    }

    public String getString(String field) {
        return asString(get(field));
    }

    public Float getFloat(String field, Float def) {
        return asFloat(get(field), def);
    }

    public Float getFloat(String field) {
        return asFloat(get(field));
    }

    public Double getDouble(String field) {
        return asDouble(get(field));
    }

    public Double getDouble(String field, Double def) {
        return asDouble(get(field), def);
    }

    public Boolean getBoolean(String field, Boolean def) {
        return asBoolean(get(field), def);
    }

    public Boolean getBoolean(String field) {
        return asBoolean(get(field));
    }

    public Integer getInteger(String field, Integer def) {
        return asInteger(get(field), def);
    }

    public Integer getInteger(String field) {
        return asInteger(get(field));
    }

    /**
     * Increments the Integer under @field by @incr and returns the incremented value. If there is not such @field, it is set to @incr.
     */
    public Integer incrInteger(String field, int incr) {
        int i = getInteger(field, 0) + incr;
        put(field, i);
        return i;
    }

    /**
     * Increments the Integer under field by 1 and returns the incremented value. If there is not such @field, it is set to 1.
     */
    public Integer incrInteger(String field) {
        return incrInteger(field, 1);
    }

    public Record getRecord(String field) {
        return asRecord(get(field));
    }

    public List getList(String field) {
        return asList(get(field));
    }

    public synchronized List<String> getFieldsOrdered() {
        List<String> fields = new ArrayList<>();
        RecordInfo info = getRecordInfo();
        fields.addAll(info.orderedFields);
        fields.addAll(dynamicFields.keySet());
        return fields;
    }

    public synchronized Set<String> getFields() {
        HashSet<String> fields = new HashSet<>();
        fields.addAll(dynamicFields.keySet());
        RecordInfo info = getRecordInfo();
        fields.addAll(info.classFields.keySet());
        fields.addAll(info.getMethodFields.keySet());
        return fields;
    }

    public synchronized Set<String> getPersistentFields() {
        HashSet<String> fields = new HashSet<>();
        fields.addAll(dynamicFields.keySet());
        RecordInfo info = getRecordInfo();
        fields.addAll(info.classFields.keySet());
        for (String f : info.getMethodFields.keySet()) {
            if (info.setMethodFields.containsKey(f))
                fields.add(f);
        }
        return fields;
    }

    public int size() {
        return getFields().size();
    }

    public boolean empty() {
        return size() == 0;
    }

    //TODO: should support nested keys
    public synchronized void remove(String key) {
        dynamicFields.remove(key);
    }

    @Override
    public String toString() {
        Map map = toMap();
        for (Object key : new ArrayList<String>(map.keySet())) {
            Object val = map.get(key);
            if (val instanceof Double || val instanceof Float) {
                map.put(key, String.format(Locale.US, "%.2f", val));
            }
            if (map.get(key) == null)
                map.remove(key);
        }
        return getClass().getSimpleName() + map.toString();
    }

    public Map toMap() {
        HashMap<String, Object> map = new HashMap<String, Object>(size());
        for (String field : getFields()) {
            map.put(field, get(field));
        }
        return map;
    }

    /*
    public Map toMapDeep() {
       HashMap<String,Object> map = new HashMap<String,Object>(size());
       for (String field : getFields()) {
     Object value = get(field);
     if (value != null && value instanceof Record)
        value = ((Record)value).toMapDeep();
     map.put(field, value);
       }
       return map;
    }
     */

    /**
     * Converts the Record to a JSONObject
     * @return JSONObject
     */
    public JsonObject toJSON() {
        JsonObject json = new JsonObject();
        if (this.getClass() != Record.class) {
            json.add("class", this.getClass().getName());
        }
        for (String key : getPersistentFields()) {
            Object val = get(key);
            if (val != null) {
                if (key.equals("class")) {
                    System.err.println("Warning: fields with the name 'class' are not allowed");
                } else if (val instanceof Float) {
                    json.add(key, (Float) val);
                } else if (val instanceof Double) {
                    json.add(key, (Double) val);
                } else if (val instanceof Integer) {
                    json.add(key, (Integer) val);
                } else if (val instanceof Long) {
                    json.add(key, (Long) val);
                } else if (val instanceof Boolean) {
                    json.add(key, (Boolean) val);
                } else if (val instanceof String) {
                    json.add(key, (String) val);
                } else if (val instanceof Record) {
                    json.add(key, ((Record) val).toJSON());
                } else if (val instanceof List) {
                    json.add(key, toJsonArray((List) val));
                } else if (val instanceof LocalDateTime) {
                    json.add(key, new LocalDateTimeStringConverter().toString((LocalDateTime) val));
                } else if (val instanceof LocalDate) {
                    json.add(key, new LocalDateStringConverter().toString((LocalDate) val));
                } else {
                    System.err.println("Warning: could not convert " + val.getClass() + " to JSON");
                }
            }
        }
        return json;
    }

    /**
     * Saves the record in JSON format to a file. If the file already exists, the content in the file is overwritten with the new record data.
     * 
     * @param inputJSONfile The file the record data are stored in
     * @throws IOException
     */
    public void toJSON(File inputJSONfile) throws IOException {
        //Without the if the method will throw a NullPointer when no parent is directly specified when creating the new file
        if (inputJSONfile.getParent() != null) {
            if (!inputJSONfile.getParentFile().exists()) {
                inputJSONfile.getParentFile().mkdirs();
            }
        }
        OutputStream out = new FileOutputStream(inputJSONfile);
        final PrintStream printStream = new PrintStream(out);
        printStream.print(toJSON().toString());
        printStream.close();
    }

    private static JsonArray toJsonArray(List list) {
        JsonArray arr = new JsonArray();
        for (Object val : list) {
            if (val instanceof Float) {
                arr.add((Float) val);
            } else if (val instanceof Double) {
                arr.add((Double) val);
            } else if (val instanceof Integer) {
                arr.add((Integer) val);
            } else if (val instanceof Long) {
                arr.add((Long) val);
            } else if (val instanceof Boolean) {
                arr.add((Boolean) val);
            } else if (val instanceof String) {
                arr.add((String) val);
            } else if (val instanceof Record) {
                arr.add(((Record) val).toJSON());
            } else if (val instanceof List) {
                arr.add(toJsonArray((List) val));
            } else if (val instanceof LocalDateTime) {
                arr.add(new LocalDateTimeStringConverter().toString((LocalDateTime) val));
            } else if (val instanceof LocalDate) {
                arr.add(new LocalDateStringConverter().toString((LocalDate) val));
            } else {
                System.err.println("Warning: could not convert " + val.getClass() + " to JSON");
            }
        }
        return arr;
    }

    public static Record fromJSON(URL resource) throws IOException, JsonToRecordException {
        return fromJSON(IOUtils.toString(resource.openStream(), "UTF-8"));
    }

    /**
     * Reads a Record from a Properties file.
     * 
     * @param file JSON file with Record data
     * @return Record
     * @throws IOException
     * @throws JsonToRecordException
     */
    public static Record fromJSON(File file) throws IOException, JsonToRecordException {
        return fromJSON(Utils.readTextFile(file));
    }

    /**
     * Converts the JSON compatible String to a Record.
     * @throws JsonToRecordException 
     */
    public static Record fromJSON(String string) throws JsonToRecordException {
        try {
            JsonObject jsonObject = JsonObject.readFrom(string);
            return parseJsonObject(jsonObject);
        } catch (ParseException e) {
            throw new JsonToRecordException(e.getMessage());
        }
    }

    public static Object fromJSONValue(String string) throws JsonToRecordException {
        try {
            JsonValue json = JsonValue.readFrom(string);
            return parseJsonValue(json);
        } catch (ParseException e) {
            throw new JsonToRecordException(e.getMessage());
        }
    }

    private static Record parseJsonObject(JsonObject json) throws JsonToRecordException {
        try {
            Record record;
            if (json.get("class") != null) {
                Constructor<?> constructor = Class.forName(json.get("class").asString()).getDeclaredConstructor();
                constructor.setAccessible(true);
                record = (Record) constructor.newInstance(null);
            } else {
                record = new Record();
            }
            for (String name : json.names()) {
                if (!name.equals("class")) {
                    JsonValue jvalue = json.get(name);
                    record.put(name, parseJsonValue(jvalue));
                }
            }
            //System.out.println(json + " " + record);
            return record;
        } catch (ClassNotFoundException e) {
            throw new JsonToRecordException("Class not found: " + e.getMessage());
        } catch (InstantiationException e) {
            throw new JsonToRecordException("Could not create: " + e.getMessage());
        } catch (IllegalAccessException e) {
            throw new JsonToRecordException("Could not access: " + e.getMessage());
        } catch (IllegalArgumentException e) {
            throw new JsonToRecordException("Illegal argument: " + e.getMessage());
        } catch (InvocationTargetException e) {
            throw new JsonToRecordException("Invocation problem: " + e.getMessage());
        } catch (NoSuchMethodException e) {
            throw new JsonToRecordException("No such method: " + e.getMessage());
        } catch (SecurityException e) {
            throw new JsonToRecordException("Securiry problem: " + e.getMessage());
        }
    }

    public static class JsonToRecordException extends IOException {

        public JsonToRecordException(String message) {
            super(message);
        }

    }

    private static Object parseJsonValue(JsonValue value) throws JsonToRecordException {
        if (value.isObject()) {
            return parseJsonObject(value.asObject());
        } else if (value.isNumber()) {
            try {
                return value.asInt();
            } catch (NumberFormatException e) {
                return value.asFloat();
            }
        } else if (value.isBoolean()) {
            return value.asBoolean();
        } else if (value.isArray()) {
            ArrayList<Object> array = new ArrayList<Object>();
            JsonArray ja = value.asArray();
            for (int i = 0; i < ja.size(); i++) {
                array.add(parseJsonValue(ja.get(i)));
            }
            return array;
        } else {
            return value.asString();
        }
    }

    public String toStringIndent() {
        return toStringIndent(0);
    }

    protected String toStringIndent(int level) {
        String result = "{";
        int n = 0;
        for (String key : getFields()) {
            if (n > 0) {
                result += "\n" + indent(level + 1);
            }
            result += key + ": ";
            String value = value(get(key), level + 2);
            if (value.contains("\n")) {
                result += "\n" + indent(level + 2) + value;
            } else {
                result += value;
            }
            n++;
        }
        result += "}";
        return result;
    }

    private static String value(Object o, int level) {
        if (o instanceof List) {
            return toStringIndent(((List) o), level);
        } else if (o instanceof Record) {
            return ((Record) o).toStringIndent(level);
        } else if (o instanceof Double || o instanceof Float) {
            float value = asFloat(o);
            if (Math.abs(value) < 1)
                return String.format(Locale.US, "%.3f", value);
            else
                return String.format(Locale.US, "%.2f", value);
        } else {
            return ("" + o).trim();
        }
    }

    private static String indent(int level) {
        String result = "";
        for (int i = 0; i < level; i++)
            result += " ";
        return result;
    }

    private static String toStringIndent(List list, int level) {
        String result = "[";
        boolean multiline = false;
        List<String> items = new ArrayList<>();
        for (Object item : list) {
            String value = value(item, level + 1);
            items.add(value);
            multiline = multiline || value.contains("\n");
        }
        int n = 0;
        for (String item : items) {
            if (n > 0 && multiline) {
                result += ",\n" + indent(level + 1);
            } else if (n > 0) {
                result += ", ";
            }
            result += item;
            n++;
        }
        result += "]";
        return result;
    }

    /*
    public static void main(String[] args) {
       Record r = new Record();
       r.put("test", 1);
       r.put("asdaafaf", 2);
       Record r2 = new Record();
       r.put("kfjkf", r2);
       r2.put("jsjs", 23);
       r2.put("js", 2324);
       List l1 = new ArrayList();
       r2.put("list", l1);
       l1.add(new Record("a", "b", "casd", "d"));
       l1.add(3);
       System.out.println(r.toStringIndent());
    }
     */

    /**
     * Converts the Record data to properties format.
     * 
     * @return Properties object with the record data
     */
    public Properties toProperties() {
        Properties prop = new Properties();
        writeProperties(prop, "", this);
        return prop;
    }

    /**
     * Saves the record in a properties file. If the file already exists, the content in the file is overwritten with the new record data.
     * 
     * @param file The file the record data are stored in
     * @throws IOException
     */
    public void toProperties(File file) throws IOException {
        Properties prop = toProperties();
        //Without the if the method will throw a NullPointer when no parent is directly specified when creating the new file
        if (file.getParent() != null) {
            if (!file.getParentFile().exists()) {
                file.getParentFile().mkdirs();
            }
        }
        OutputStream out = new FileOutputStream(file);
        prop.store(out, "");
        out.close();

    }

    private static void writeProperties(Properties prop, String key, Record rec) {
        for (String field : rec.getFields()) {
            Object o = rec.get(field);
            String nkey;
            if (key.length() > 0)
                nkey = key + "." + field;
            else
                nkey = field;
            if (o instanceof Record) {
                writeProperties(prop, nkey, (Record) o);
            } else {
                prop.put(nkey, "" + o);
            }
        }
    }

    /**
     * Converts an InputStream of a Properties file into a Record object.
     * 
     * @param inputStream InputStream from properties file with Record data
     * @return Record
     */
    public static Record fromProperties(InputStream inputStream) throws IOException {
        Properties prop = new Properties();
        prop.load(inputStream);
        return fromProperties(prop);
    }

    /**
     * Converts a Properties object into a Record object.
     * @param prop Properties file with Record data
     * @return Record
     */
    public static Record fromProperties(Properties prop) {
        Record rec = new Record();
        for (Object key : prop.keySet()) {
            rec.put(key.toString().replace(".", ":"), prop.get(key));
        }
        return rec;
    }

    /**
     * Reads a Record from a Properties file.
     * 
     * @param file A file with record data in properties format
     * @return Record
     * @throws IOException
     */
    public static Record fromProperties(File file) throws IOException {
        return fromProperties(new FileInputStream(file));
    }

    @Override
    public Record clone() {
        try {
            Constructor<?> constructor = getClass().getDeclaredConstructor();
            constructor.setAccessible(true);
            Record clone = (Record) constructor.newInstance(null);
            clone.putAll(this);
            return clone;
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }

    public Record deepClone() {
        try {
            Constructor<?> constructor = getClass().getDeclaredConstructor();
            constructor.setAccessible(true);
            Record clone = (Record) constructor.newInstance(null);
            for (String field : this.getFields()) {
                Object value = get(field);
                if (value != null) {
                    if (value instanceof Record)
                        clone.put(field, ((Record) value).deepClone());
                    else
                        clone.put(field, value);
                }
            }
            return clone;
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        } else if (this == obj) {
            return true;
        } else if (obj.getClass() == this.getClass()) {
            for (String field : getFields()) {
                Object o1 = get(field);
                Object o2 = ((Record) obj).get(field);
                if (o1 == null && o2 == null) {
                } else if (o1 == null || o2 == null) {
                    return false;
                } else if (!o1.equals(o2)) {
                    return false;
                }
            }
            return true;
        } else {
            return false;
        }
    }

    public void adjoin(Record record) {
        for (String field : record.getFields()) {
            Object value = record.get(field);
            if (value != null) {
                Object existing = this.get(field);
                if (existing instanceof List) {
                    ((List) existing).addAll((List) value);
                } else if (existing instanceof Record && value instanceof Record) {
                    ((Record) existing).adjoin((Record) value);
                } else {
                    this.put(field, value);
                }
            }
        }
    }

    public List getValues() {
        ArrayList values = new ArrayList();
        for (String field : getFields()) {
            Object value = get(field);
            if (value != null)
                values.add(value);
        }
        return values;
    }

    @Target({ ElementType.METHOD, ElementType.FIELD })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RecordField {
        String name() default "DEFAULT";

        int order() default Integer.MAX_VALUE;
    }

}