org.openbravo.service.json.JsonToDataConverter.java Source code

Java tutorial

Introduction

Here is the source code for org.openbravo.service.json.JsonToDataConverter.java

Source

/*
 *************************************************************************
 * The contents of this file are subject to the Openbravo  Public  License
 * Version  1.1  (the  "License"),  being   the  Mozilla   Public  License
 * Version 1.1  with a permitted attribution clause; you may not  use this
 * file except in compliance with the License. You  may  obtain  a copy of
 * the License at http://www.openbravo.com/legal/license.html 
 * Software distributed under the License  is  distributed  on  an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific  language  governing  rights  and  limitations
 * under the License. 
 * The Original Code is Openbravo ERP. 
 * The Initial Developer of the Original Code is Openbravo SLU 
 * All portions are Copyright (C) 2009-2014 Openbravo SLU 
 * All Rights Reserved. 
 * Contributor(s):  ______________________________________.
 ************************************************************************
 */

package org.openbravo.service.json;

import java.math.BigDecimal;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;

import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.Logger;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.openbravo.base.exception.OBException;
import org.openbravo.base.model.Entity;
import org.openbravo.base.model.Property;
import org.openbravo.base.model.domaintype.AbsoluteDateTimeDomainType;
import org.openbravo.base.model.domaintype.AbsoluteTimeDomainType;
import org.openbravo.base.model.domaintype.BigDecimalDomainType;
import org.openbravo.base.model.domaintype.BinaryDomainType;
import org.openbravo.base.model.domaintype.EncryptedStringDomainType;
import org.openbravo.base.model.domaintype.HashedStringDomainType;
import org.openbravo.base.model.domaintype.ProductCharacteristicsDomainType;
import org.openbravo.base.model.domaintype.TimestampDomainType;
import org.openbravo.base.provider.OBProvider;
import org.openbravo.base.structure.BaseOBObject;
import org.openbravo.base.structure.Traceable;
import org.openbravo.base.util.Check;
import org.openbravo.dal.service.OBDal;
import org.openbravo.dal.service.OBQuery;
import org.openbravo.model.common.enterprise.Organization;
import org.openbravo.utils.CryptoUtility;
import org.openbravo.utils.FormatUtilities;

/**
 * Converts json data to Openbravo business object(s).
 * 
 * This class can translate single a {@link BaseOBObject} as well as lists of {@link BaseOBObject}
 * instances.
 * 
 * A single JSONObject is assumed to represent one {@link BaseOBObject}. The conversion process goes
 * through the following steps for each JSONObject.
 * 
 * First it is checked if the JSONObject has an id and entityName value. If so the logic tries to
 * find the object in the internal key-to-object map (maintained during the conversion). If not
 * found then the database is search. if not found there then a new BaseOBObject is created.If the
 * object has an id then it is put in the key-to-object map so that it can be found if later objects
 * refer to it.
 * <p/>
 * Then the next step is to convert/set the values. the logic walks through the properties of the
 * object (the properties are defined by the entity of the object (see
 * {@link BaseOBObject#getEntity()} and {@link Entity#getProperties()}. For primitive values, the
 * property name ({@link Property#getName()}) is used to search a value in the JSONObject. The value
 * is retrieved converted and then set in the business object.
 * <p/>
 * Reference/foreign key properties are handled differently. In this case the property name is
 * appended with the ".id" postfix and this name is used to search for a value in the JSONObject. If
 * there is no such value present then the property name is used. The id value is used to search in
 * the key-to-object map and in the database (in this order). If not found then a new object is
 * created as it may be present later in the overall json string. The system later tracks that there
 * are no such maverick objects present (call {@link #getErrors()} to check this).
 * <p/>
 * This class also handles a special type of property/keys. It can handle setting values in related
 * objects also. For example say that the json object represents a sales order and has these keys
 * (and their values):
 * <ul>
 * <li>businessPartner.paymentTerm.name</li>
 * <li>businessPartner.paymentTerm.id</li>
 * <li>businessPartner.id</li>
 * </ul>
 * 
 * Then these keys are used to:
 * <ul>
 * <li>set the businesspartner object in the sales order using the businesspartner.id</li>
 * <li>set the paymentTerm in the business partner using the businessPartner.paymentTerm.id</li>
 * <li>set the name of the paymentTerm using the businessPartner.paymentTerm.name</li>
 * </ul>
 * 
 * So this class can handle flat JSON structures which represent a hierarchy/path in their name.
 * 
 * NOTE:
 * <ul>
 * <li>This class holds state during the conversion process. It can not be shared by multiple
 * threads.</li>
 * <li>After calling one of the toBaseOBObject* methods the conversion errors can be retrieved using
 * the {@link #getErrors()} method.</li>
 * <li>When calling toBaseOBObject* multiple times you can clear the internal state by calling
 * {@link #clearState()}.
 * </ul>
 * 
 * @author mtaal
 */
public class JsonToDataConverter {
    private static final Logger log = Logger.getLogger(JsonToDataConverter.class);

    private static final String DOT = ".";

    private final Map<String, BaseOBObject> keyToObject = new HashMap<String, BaseOBObject>();
    // these can be created as references but need to be also really imported.
    private final Map<String, BaseOBObject> newObjects = new HashMap<String, BaseOBObject>();

    private final static SimpleDateFormat xmlDateFormat = JsonUtils.createDateFormat();
    private final static SimpleDateFormat xmlDateTimeFormat = JsonUtils.createDateTimeFormat();
    private final static SimpleDateFormat xmlTimeFormat = JsonUtils.createTimeFormat();
    private final static SimpleDateFormat jsTimeFormat = JsonUtils.createJSTimeFormat();

    private final List<JsonConversionError> errors = new ArrayList<JsonConversionError>();

    /**
     * Gets a value from json and converts it to a valid value for the DAL.
     */
    public static synchronized Object convertJsonToPropertyValue(Property property, Object value) {
        try {
            if (isEmptyOrNull(value)) {
                return null;
            }
            if (!property.isPrimitive()) {
                return value;
            }
            // do some common conversions
            final Class<?> clz = property.getPrimitiveObjectType();
            if (clz != null && Date.class.isAssignableFrom(clz)) {
                try {
                    Date UTCTime = null;
                    if (property.getDomainType() instanceof TimestampDomainType
                            || property.getDomainType() instanceof AbsoluteTimeDomainType) {
                        String strValue = (String) value;
                        if (strValue.equals("null")) {
                            return null;
                        }
                        // there are cases that also the date part is sent in, get rid of it
                        if (strValue.indexOf("T") != -1) {
                            final int index = strValue.indexOf("T");
                            strValue = strValue.substring(index + 1);
                        }

                        if (strValue.indexOf("+") == -1 && strValue.indexOf("-") == -1) {
                            strValue = strValue + "+0000";
                        } else {
                            strValue = JsonUtils.convertFromXSDToJavaFormat(strValue);
                        }

                        Calendar now = Calendar.getInstance();
                        strValue = xmlDateFormat.format(now.getTime()) + "T" + strValue;
                        UTCTime = new Timestamp(jsTimeFormat.parse(strValue).getTime());
                    }
                    if (property.getDomainType() instanceof TimestampDomainType) {
                        Date localTime = convertToLocalTime(UTCTime);
                        return new Timestamp(localTime.getTime());
                    } else if (property.getDomainType() instanceof AbsoluteTimeDomainType) {
                        return UTCTime;
                    } else if (property.getDomainType() instanceof AbsoluteDateTimeDomainType) {
                        final String repairedString = JsonUtils.convertFromXSDToJavaFormat((String) value);
                        return new Timestamp(jsTimeFormat.parse(repairedString).getTime());
                    } else if (property.isDatetime() || Timestamp.class.isAssignableFrom(clz)) {
                        final String repairedString = JsonUtils.convertFromXSDToJavaFormat((String) value);
                        return new Timestamp(xmlDateTimeFormat.parse(repairedString).getTime());
                    } else {
                        return xmlDateFormat.parse((String) value);
                    }
                } catch (ParseException e) {
                    throw new Error(e);
                }
            } else if (property.isBoolean() && value instanceof String) {
                if (value.equals("Y")) {
                    return true;
                }
                return Boolean.parseBoolean((String) value);
            } else if (value instanceof Double) {
                return new BigDecimal((Double) value);
            } else if (value instanceof Integer && property.getPrimitiveObjectType() == Long.class) {
                return new Long((Integer) value);
            } else if (value instanceof Integer && property.getPrimitiveObjectType() == Float.class) {
                return new Float((Integer) value);
            } else if (value instanceof Long && property.getPrimitiveObjectType() == Float.class) {
                return new Float((Long) value);
            } else if (value instanceof BigDecimal && property.getPrimitiveObjectType() == BigDecimal.class) {
                return value;
            } else if (value instanceof Number && property.getPrimitiveObjectType() == BigDecimal.class) {
                return new BigDecimal(((Number) value).doubleValue());
            } else if (value instanceof String && property.getDomainType() instanceof HashedStringDomainType) {
                String str = (String) value;
                try {
                    return FormatUtilities.sha1Base64(str);
                } catch (ServletException e) {
                    log.error("Error hashing password", e);
                    // TODO: translate error message
                    throw new Error("Could not encrypt password", e);
                }
            } else if (value instanceof String && property.getDomainType() instanceof EncryptedStringDomainType) {
                String str = (String) value;
                try {
                    return CryptoUtility.encrypt(str);
                } catch (ServletException e) {
                    log.error("Error encrypting password", e);
                    // TODO: translate error message
                    throw new Error("Could not encrypt password", e);
                }
            } else if (value instanceof String && property.getDomainType() instanceof BinaryDomainType) {
                return Base64.decodeBase64((String) value);
            } else if (property.getDomainType() instanceof ProductCharacteristicsDomainType) {
                // under certain conditions the Product Characteristics value needs to be fixed
                // see issue https://issues.openbravo.com/view.php?id=28187
                return ProductCharacteristicsDomainType.fixValue(value);
            }
            return value;
        } catch (Exception e) {
            throw new OBException("Error when converting value " + value + " for prop " + property, e);
        }
    }

    private static Date convertToLocalTime(Date UTCTime) {
        Calendar localTime = Calendar.getInstance();
        localTime.setTime(UTCTime);

        int gmtMillisecondOffset = (localTime.get(Calendar.ZONE_OFFSET) + localTime.get(Calendar.DST_OFFSET));
        localTime.add(Calendar.MILLISECOND, gmtMillisecondOffset);

        return localTime.getTime();
    }

    private static boolean isEmptyOrNull(Object value) {
        if (JSONObject.NULL.equals(value)) { // note JSONObject.NULL.equals(null) == true
            return true;
        }
        if (value == null) {
            return true;
        }
        if (value instanceof String && ((String) value).trim().length() == 0) {
            return true;
        }
        return false;
    }

    public void clearState() {
        errors.clear();
        keyToObject.clear();
    }

    /**
     * Converts a JSONArray to a list of BaseOBObject instances. The JSONArray is assumed to contain
     * only JSONObjects which represent each a BaseOBObject.
     * 
     * See the conversion logic description in the class header for more information.
     * 
     * @param jsonArray
     *          the array with JSONObjects
     * @return the list of BaseOBObjects.
     */
    public List<BaseOBObject> toBaseOBObjects(JSONArray jsonArray) {
        try {
            Check.isNotNull(jsonArray, "Object may not be null");
            final List<BaseOBObject> result = new ArrayList<BaseOBObject>();
            for (int i = 0; i < jsonArray.length(); i++) {
                result.add(toBaseOBObject((JSONObject) jsonArray.get(i)));
            }
            return result;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Converts a list with JSONObjects to a list of BaseOBObject instances.
     * 
     * See the conversion logic description in the class header for more information.
     * 
     * @param jsonObjects
     *          the list with JSONObjects
     * @return the list of BaseOBObjects.
     */
    public List<BaseOBObject> toBaseOBObjects(List<JSONObject> jsonObjects) {
        try {
            Check.isNotNull(jsonObjects, "Object may not be null");
            final List<BaseOBObject> result = new ArrayList<BaseOBObject>();
            for (JSONObject jsonObject : jsonObjects) {
                result.add(toBaseOBObject(jsonObject));
            }
            return result;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Converts a single JSONObject to a BaseOBObject.
     * 
     * See the conversion logic description in the class header for more information.
     * 
     * @param jsonObject
     *          the jsonObject to convert
     * @return the BaseOBObject (new or retrieved from the database)
     */
    public BaseOBObject toBaseOBObject(JSONObject jsonObject) throws Exception {
        String id = null;
        if (jsonObject.has(JsonConstants.ID)) {
            id = jsonObject.getString(JsonConstants.ID);
        }
        // if there is a new indicator then nullify the id again to treat the object has new
        final boolean isNew = jsonObject.has(JsonConstants.NEW_INDICATOR)
                && jsonObject.getBoolean(JsonConstants.NEW_INDICATOR);
        if (isNew) {
            id = null;
        }

        String entityName = null;
        if (jsonObject.has(JsonConstants.ENTITYNAME)) {
            entityName = jsonObject.getString(JsonConstants.ENTITYNAME);
        }
        if (entityName == null) {
            throw new IllegalArgumentException("Entity name not defined in jsonobject " + jsonObject);
        }

        BaseOBObject obObject;
        if (id != null) {
            final String key = getObjectKey(id, entityName);
            if (keyToObject.get(key) != null) {
                obObject = keyToObject.get(key);
            } else {
                obObject = OBDal.getInstance().get(entityName, id);
                if (obObject == null) {
                    obObject = (BaseOBObject) OBProvider.getInstance().get(entityName);
                }
            }
            keyToObject.put(key, obObject);
            // remove it from the newObjects as we found it in the json set.
            newObjects.remove(key);
        } else {
            obObject = (BaseOBObject) OBProvider.getInstance().get(entityName);
        }

        if (isNew) {
            obObject.setNewOBObject(true);
        }

        setData(jsonObject, obObject);

        // see the DefaultJSonDataService for a description on specific handling of
        // new computed id's
        if (isNew) {
            // remove the id before it gets saved
            obObject.setValue(JsonConstants.ID, null);
        }

        return obObject;
    }

    /**
     * Sets the data in the BaseOBObject by converting data from the json object and finding the
     * correct properties to set.
     * 
     * @param jsonObject
     *          the source of the data
     * @param obObject
     *          the target of the data
     */
    protected void setData(JSONObject jsonObject, BaseOBObject obObject) throws JSONException {

        // just use a random entity to get the name of the updated property
        if (jsonObject.has(Organization.PROPERTY_UPDATED) && obObject instanceof Traceable) {
            final String jsonDateStr = jsonObject.getString(Organization.PROPERTY_UPDATED);
            if (jsonDateStr != null && !jsonDateStr.equals("null")) {
                try {
                    final String repairedString = JsonUtils.convertFromXSDToJavaFormat(jsonDateStr);
                    final Date jsonDate = new Timestamp(xmlDateTimeFormat.parse(repairedString).getTime());
                    final Date objectDate = ((Traceable) obObject).getUpdated();
                    if (!areDatesEqual(jsonDate, objectDate, true, false)) {
                        // return this message code to let the client show a translated label
                        throw new OBStaleObjectException("@OBJSON_StaleDate@");
                    }
                } catch (OBStaleObjectException x) {
                    throw x;
                } catch (Exception e) {
                    throw new OBException("Exception when updating " + obObject, e);
                }
            }
        }

        final Entity entity = obObject.getEntity();

        // collect the keys
        final List<String> keyNames = new ArrayList<String>();
        final Iterator<?> iterator = jsonObject.keys();
        while (iterator.hasNext()) {
            keyNames.add((String) iterator.next());
        }

        for (String keyName : keyNames) {
            // a foreign key
            final boolean multiPathProperty = keyName.contains(DOT);
            if (multiPathProperty) {
                // TODO: handle passwords here also
                handleMultiPathProperty("", keyName, obObject, jsonObject, jsonObject.get(keyName));
            } else {
                if (!entity.hasProperty(keyName)) {
                    continue;
                }
                final Property property = entity.getProperty(keyName);
                if (property.getDomainType() instanceof EncryptedStringDomainType
                        || property.getDomainType() instanceof HashedStringDomainType) {
                    if (jsonObject.has(keyName + "_cleartext")) {
                        Object valCleartext = jsonObject.get(keyName + "_cleartext");
                        setValue(obObject, property, valCleartext);
                    } else {
                        // no _cleartext value found -> skipping field
                    }
                } else if (property.getDomainType() instanceof BigDecimalDomainType
                        && jsonObject.has(keyName + "_textualValue")) {
                    final String strValue = (String) jsonObject.get(keyName + "_textualValue");
                    try {
                        if (strValue == null || strValue.trim().length() == 0) {
                            setValue(obObject, property, null);
                        } else {
                            setValue(obObject, property, new BigDecimal(strValue));
                        }
                    } catch (Exception e) {
                        throw new OBException(
                                "Exception while trying to convert value:-->" + strValue + "<-- " + e.getMessage(),
                                e);
                    }
                } else {
                    setValue(obObject, property, jsonObject.get(keyName));
                }
            }
        }
    }

    /**
     * Handles the complex case of keys in the JSONObject which consists of multiple steps. For
     * example businessPartner.paymentTerm.name. To handle these keys the system has to walk the path
     * and find the object at the end of the path to set the value.
     * 
     * The system should take into account that not all objects along the path already exists, for
     * example the paymentTerm in the businessPartner maybe null. In that case a new PaymentTerm needs
     * to be set in the businessPartner. The system should also check if there is an id passed in for
     * the paymentTerm: businessPartner.paymentTerm.id. If so then that one should be used to
     * create/read the PaymentTerm.
     * 
     * The previousPath and remainingPath parameters are used to keep track of the path walked. For
     * example if the system is processing the part of the paymentTerm then the previousPath will be:
     * 'businessPartner' and the remaining path will be: 'paymentTerm.name'.
     * 
     * @param previousPath
     *          see part of the path which has been 'walked', is initially empty.
     * @param remainingPath
     *          the part which needs to be handled.
     * @param obObject
     *          the current BaseOBObject to set
     * @param jsonObject
     *          the jsonObject containing the values
     * @param value
     *          the value to set when the end of the path has been reached.
     */
    protected void handleMultiPathProperty(String previousPath, String remainingPath, BaseOBObject obObject,
            JSONObject jsonObject, Object value) throws JSONException {
        final int dotIndex = remainingPath.indexOf(DOT);
        if (dotIndex == -1) {
            if (remainingPath.equals(JsonConstants.IDENTIFIER)) {
                return;
            }
            if (!obObject.getEntity().hasProperty(remainingPath)) {
                // invalid property, ignore those...
                logError(obObject, "Property not found " + remainingPath + " error when parsing path "
                        + (previousPath + DOT + remainingPath));
                return;
            }
            final Property property = obObject.getEntity().getProperty(remainingPath);
            setValue(obObject, property, value);
        } else {
            final String firstPart = remainingPath.substring(0, dotIndex);
            if (!obObject.getEntity().hasProperty(firstPart)) {
                logError(obObject, "Property not found " + firstPart + " error when parsing path "
                        + (previousPath + DOT + remainingPath));
                return;
            }
            final Property property = obObject.getEntity().getProperty(firstPart);
            final String secondPart = remainingPath.substring(dotIndex + 1);
            final boolean isId = secondPart.equals(JsonConstants.ID);
            if (property.isPrimitive() || property.isOneToMany()) {
                // TODO: log this error condition
                return;
            }
            if (isId) {
                // the final part of a multiPart
                setValue(obObject, property, value);
            } else {
                BaseOBObject currentOBObject = (BaseOBObject) obObject.get(property.getName());

                // really multi-part, check if there is an id property
                String id = null;
                final String idPath = previousPath + DOT + firstPart + DOT + JsonConstants.ID;
                // check if we are still looking at the correct object, if not replace it
                if (jsonObject.has(idPath) && !jsonObject.isNull(idPath)) {
                    id = jsonObject.getString(idPath);
                    if (currentOBObject == null || !id.equals(currentOBObject.getId())) {
                        setValue(obObject, property, id);
                        currentOBObject = (BaseOBObject) obObject.get(property.getName());
                        Check.isNotNull(currentOBObject, "The baseObObject was not set " + obObject + " "
                                + previousPath + " " + remainingPath);
                    }
                } else if (currentOBObject == null) {
                    currentOBObject = (BaseOBObject) OBProvider.getInstance()
                            .get(property.getTargetEntity().getName());
                }
                handleMultiPathProperty(previousPath + (previousPath.length() > 0 ? DOT : "") + firstPart,
                        secondPart, currentOBObject, jsonObject, value);
            }
        }

    }

    /**
     * @see #getBaseOBObjectFromId(Entity, Property, String)
     */
    protected BaseOBObject getBaseOBObjectFromId(Entity entity, String referencedId) {
        BaseOBObject value = null;
        if (referencedId != null) {
            final String key = getObjectKey(referencedId, entity.getName());
            if (keyToObject.get(key) != null) {
                value = keyToObject.get(key);
            } else {
                value = OBDal.getInstance().get(entity.getName(), referencedId);
            }
            if (value == null) {
                value = (BaseOBObject) OBProvider.getInstance().get(entity.getName());
                // put it here, it must be imported later as a real object
                newObjects.put(key, value);
            }
            keyToObject.put(key, value);
        } else {
            value = (BaseOBObject) OBProvider.getInstance().get(entity.getName());
        }

        // note: when inheritance is supported then this Check should be changed/removed
        Check.isTrue(value.getEntity() == entity,
                "The object " + value + " has a different entity then the request entity " + entity);
        return value;
    }

    /**
     * Tries to find an object first in the internal map which is maintained to first resolve id's
     * internally within the complete json string. If not found there then the database is queried
     * using the entity and referenceId. If not found there (or the referenceId == null) then create a
     * new instance of the entity.
     * 
     * @param entity
     *          the entity to retrieve/create
     * @param property
     *          the property referencing to the entity
     * @param referencedId
     *          the id of the entity
     * @return an existing of new BaseOBObject
     */
    protected BaseOBObject getBaseOBObjectFromId(Entity entity, Property property, String referencedId) {
        BaseOBObject value = null;
        if (referencedId != null) {
            final String key = getObjectKey(referencedId, entity.getName());
            if (keyToObject.get(key) != null) {
                value = keyToObject.get(key);
            } else {
                // if an id we should use the get method as it loads from the first level
                // cache
                if (property.getReferencedProperty() != null && !property.getReferencedProperty().isId()) {
                    final OBQuery<BaseOBObject> qry = OBDal.getInstance().createQuery(entity.getName(),
                            property.getReferencedProperty().getName() + "=:reference");
                    qry.setNamedParameter("reference", referencedId);
                    qry.setFilterOnActive(false);
                    qry.setFilterOnReadableClients(false);
                    qry.setFilterOnReadableOrganization(false);
                    final List<BaseOBObject> result = qry.list();
                    if (result.size() > 1) {
                        log.warn("More than one result when querying " + entity + " using property "
                                + property.getReferencedProperty() + " with value " + referencedId
                                + ", choosing the first result");
                        value = result.get(0);
                    } else if (result.size() == 1) {
                        value = result.get(0);
                    } else {
                        value = null;
                    }
                } else {
                    value = OBDal.getInstance().get(entity.getName(), referencedId);
                }
            }
            if (value == null) {
                value = (BaseOBObject) OBProvider.getInstance().get(entity.getName());
                // put it here, it must be imported later as a real object
                newObjects.put(key, value);
            }
            keyToObject.put(key, value);
        } else {
            value = (BaseOBObject) OBProvider.getInstance().get(entity.getName());
        }

        // note: when inheritance is supported then this Check should be changed/removed
        Check.isTrue(value.getEntity() == entity,
                "The object " + value + " has a different entity then the request entity " + entity);
        return value;
    }

    /**
     * Checks if the property is settable ({@link #isNotConvertable(BaseOBObject, Property)}. If so
     * the jsonValue is converted ({@link #convertJsonToPropertyValue(Property, Object)}, in case of a
     * primitive), or in case of a reference property the referenced object is read/created.
     * 
     * @param obObject
     *          the object to set
     * @param property
     *          the property of the object to set
     * @param jsonValue
     *          the value as it is present in the original JSONObject
     */
    protected void setValue(BaseOBObject obObject, Property property, Object jsonValue) {
        Check.isTrue(obObject.getEntity().hasProperty(property.getName()),
                "The object " + obObject + " does not have the property " + property);

        if (isNotConvertable(obObject, property)) {
            // valid case, do not log
            return;
        }

        try {
            // convert/read the value
            final Object value;
            if (isEmptyOrNull(jsonValue)) {
                value = null;
            } else if (property.isPrimitive()) {
                // convert the value
                value = convertJsonToPropertyValue(property, jsonValue);
            } else if (jsonValue instanceof String) {
                // an id
                final String referenceId = (String) jsonValue;
                if (property.getReferencedProperty() != null) {
                    value = getBaseOBObjectFromId(property.getTargetEntity(), property, referenceId);
                } else {
                    value = getBaseOBObjectFromId(property.getTargetEntity(), referenceId);
                }
            } else {
                // a json object
                // try another approach, maybe the value is a jsonobject itself
                final JSONObject fkValue = (JSONObject) jsonValue;
                // add the entityname to help retrieving it from the db
                if (!fkValue.has(JsonConstants.ENTITYNAME)) {
                    fkValue.put(JsonConstants.ENTITYNAME, property.getTargetEntity().getName());
                }
                value = toBaseOBObject(fkValue);
            }

            // if nothing changed then don't set anything
            // this is usefull if the current and new value are the same but not 'valid' anymore
            if (!obObject.isNewOBObject() && value != null) {
                final Object currentValue = obObject.get(property.getName());
                if (property.isPrimitive() && value.equals(currentValue)) {
                    return;
                } else if (value == currentValue) {
                    // non-primitive, if the exact same value then don't set it
                    return;
                } else if (Date.class.isAssignableFrom(value.getClass()) && currentValue != null
                        && Date.class.isAssignableFrom(currentValue.getClass())) {
                    // there are mismatches between json and the database in
                    // precision of times/dates, these are repaired here by
                    // not updating if the relevant part is the same
                    if (areDatesEqual((Date) value, (Date) currentValue,
                            property.isDatetime() || property.getDomainType() instanceof AbsoluteDateTimeDomainType,
                            property.getDomainType() instanceof TimestampDomainType
                                    || property.getDomainType() instanceof AbsoluteTimeDomainType)) {
                        return;
                    }
                }
            }

            // and set the value
            obObject.set(property.getName(), value);

        } catch (Throwable t) {
            // store/log all errors
            final JsonConversionError conversionError = new JsonConversionError();
            conversionError.setBaseOBObject(obObject);
            conversionError.setProperty(property);
            conversionError.setThrowable(t);
            errors.add(conversionError);
        }
    }

    /**
     * The json conversion looses precision in milliseconds and seconds. This comparison method only
     * compares the other parts of the date object. Depending if a date or a date time is set.
     * 
     * @param d1
     *          the first date to compare
     * @param d2
     *          the second date to compare
     * @param isDatetime
     *          is it a datetime
     * @return true if d1 and d2 have equal values for year, month and day and for date time also same
     *         values for hour, minutes and seconds.
     */
    protected boolean areDatesEqual(Date d1, Date d2, boolean isDatetime, boolean isTime) {
        final Calendar c1 = Calendar.getInstance();
        c1.setTime(d1);
        final Calendar c2 = Calendar.getInstance();
        c2.setTime(d2);
        if (isTime) {
            c2.set(Calendar.MILLISECOND, 0);
            c1.set(Calendar.MILLISECOND, 0);
        } else if (isDatetime) {
            c2.set(Calendar.MILLISECOND, 0);
            c1.set(Calendar.MILLISECOND, 0);
        } else {
            c2.set(Calendar.MILLISECOND, 0);
            c1.set(Calendar.MILLISECOND, 0);
            c2.set(Calendar.SECOND, 0);
            c1.set(Calendar.SECOND, 0);
            c2.set(Calendar.MINUTE, 0);
            c1.set(Calendar.MINUTE, 0);
            c2.set(Calendar.HOUR, 0);
            c1.set(Calendar.HOUR, 0);
            c1.set(Calendar.AM_PM, Calendar.AM);
            c2.set(Calendar.AM_PM, Calendar.AM);
        }
        return c2.getTimeInMillis() == c1.getTimeInMillis();
    }

    protected boolean areDatesEqual(Date d1, Date d2, boolean isDatetime) {
        return this.areDatesEqual(d1, d2, isDatetime, false);
    }

    /**
     * Determines if a property can be converted from json to a value stored in the database.
     * Properties which are not updatable (and the object is not new) are not converted, the same for
     * properties which are expired etc.
     * 
     * @param obObject
     *          the object for which the property is set
     * @param property
     *          the property to set
     * @see Property#isInactive()
     * @see Property#isUpdatable()
     * @see Property#isAuditInfo()
     * @see Property#isClientOrOrganization()
     * @see Property#isOneToMany()
     */
    protected boolean isNotConvertable(BaseOBObject obObject, Property property) {
        // one-to-many are never json-ized
        boolean doNotHandleThisProperty = property.isOneToMany();
        // do not change auditinfo or client for an existing object
        doNotHandleThisProperty |= !obObject.isNewOBObject()
                && (property.isAuditInfo() || property.getName().equals(Organization.PROPERTY_CLIENT));
        // do not change not changeable properties
        doNotHandleThisProperty |= property.isInactive();
        // do not change not updatable properties
        // Updatable is a UI concept
        // doNotHandleThisProperty |= !obObject.isNewOBObject() && !property.isUpdatable();
        doNotHandleThisProperty |= property.isProxy();
        return doNotHandleThisProperty;
    }

    private String getObjectKey(String id, String entityName) {
        return id + "_" + entityName;
    }

    public boolean hasErrors() {
        // create an exception for each new object
        if (!newObjects.isEmpty()) {
            for (String key : newObjects.keySet()) {
                final BaseOBObject bob = newObjects.get(key);
                final JsonConversionError conversionError = new JsonConversionError();
                conversionError.setBaseOBObject(bob);
                conversionError.setProperty(bob.getEntity().getIdProperties().get(0));
                conversionError.setExceptionWithMessage(
                        "New object " + bob + " (key: " + key + ") refered to but not present in the import set");
                errors.add(conversionError);
            }
        }
        return errors.size() > 0;
    }

    /**
     * Creates a {@link JsonConversionError} and adds it to the internal error list (
     * {@link #getErrors()}). As no property instance is passed the first id property of the obObject
     * is set as the property in the JsonConversionError (
     * {@link JsonConversionError#setProperty(Property)}).
     * 
     * @param obObject
     *          the object to which the error condition applies
     * @param msg
     *          the message to log
     */
    protected void logError(BaseOBObject obObject, String msg) {
        final JsonConversionError conversionError = new JsonConversionError();
        conversionError.setBaseOBObject(obObject);
        // just select any property
        conversionError.setProperty(obObject.getEntity().getIdProperties().get(0));
        conversionError.setExceptionWithMessage(msg);
        errors.add(conversionError);
    }

    public List<JsonConversionError> getErrors() {
        return errors;
    }

    /**
     * Is created when setting a value for a specific property fails.
     * 
     * @author mtaal
     */
    public class JsonConversionError {
        private BaseOBObject baseOBObject;
        private Throwable throwable;
        private Property property;

        public Throwable getThrowable() {
            return throwable;
        }

        public void setThrowable(Throwable throwable) {
            this.throwable = throwable;
        }

        public Property getProperty() {
            return property;
        }

        public void setProperty(Property property) {
            this.property = property;
        }

        public BaseOBObject getBaseOBObject() {
            return baseOBObject;
        }

        public void setBaseOBObject(BaseOBObject baseOBObject) {
            this.baseOBObject = baseOBObject;
        }

        public void setExceptionWithMessage(String msg) {
            throwable = new IllegalStateException(msg);
        }

        public String toString() {
            return "Error " + baseOBObject + " " + property + " " + throwable.getMessage();
        }
    }
}