JavaScript Extension Development API for Oracle Visual Builder Cloud Service - Classic Applications

Source: entity.dt/js/api/EntityUtils.js

define([
    'core/js/api/utils/ArrayUtils',
    'core/js/api/utils/StringUtils',
    'entity/js/api/DataModel',
    'entity/js/api/Entity',
    'entity/js/api/Property',
    'entity/js/api/PropertyType',
    'entity/js/api/Relation',
    'entity/js/api/RelationCardinality'
], function (
        ArrayUtils,
        StringUtils,
        DataModel,
        Entity,
        Property,
        PropertyType,
        Relation,
        RelationCardinality
        ) {

    'use strict';

    /**
     * A utility object for entities.
     *
     * @exports entity.dt/js/api/EntityUtils
     */
    var EntityUtils = function () {
        AbcsLib.throwStaticClassError();
    };

    var INVALID_CHARS_REGEXP = /[^\w\d]+/g;

    EntityUtils.replaceInvalidChars = function (name) {
        //return name.replace(/[\s\|,'"<>=~^;!\[\]]/g, '_');//keep less restrictive case commented for now.
        //we may be more restrictive then necessary, may be less restrictive for user prided ids
        var replaced = name.replace(INVALID_CHARS_REGEXP, '_').replace(/^_+/g, '');//remove starting '_';

        return replaced;
    };

    var ENTITY_RESERVED_WORDS = ['DESCRIBE'];

    //taken from http://docs.oracle.com/cd/B10501_01/appdev.920/a42525/apb.htm#toc141
    var RESERVED_WORDS = ['ACCESS', 'ELSE', 'MODIFY', 'START', 'ADD', 'EXCLUSIVE', 'NOAUDIT', 'SELECT', 'ALL',
        'EXISTS', 'NOCOMPRESS', 'SESSION', 'ALTER', 'FILE', 'NOT', 'SET', 'AND', 'FLOAT', 'NOTFOUND',
        'SHARE', 'ANY', 'FOR', 'NOWAIT', 'SIZE', 'ARRAYLEN', 'FROM', 'NULL', 'SMALLINT', 'AS',
        'GRANT', 'NUMBER', 'SQLBUF', 'ASC', 'GROUP', 'OF', 'SUCCESSFUL', 'AUDIT', 'HAVING',
        'OFFLINE', 'SYNONYM', 'BETWEEN', 'IDENTIFIED', 'ON', 'SYSDATE', 'BY', 'IMMEDIATE', 'ONLINE',
        'TABLE', 'CHAR', 'IN', 'OPTION', 'THEN', 'CHECK', 'INCREMENT', 'OR', 'TO', 'CLUSTER',
        'INDEX', 'ORDER', 'TRIGGER', 'COLUMN', 'INITIAL', 'PCTFREE', 'UID', 'COMMENT', 'INSERT',
        'PRIOR', 'UNION', 'COMPRESS', 'INTEGER', 'PRIVILEGES', 'UNIQUE', 'CONNECT', 'INTERSECT',
        'PUBLIC', 'UPDATE', 'CREATE', 'INTO', 'RAW', 'USER', 'CURRENT', 'IS', 'RENAME', 'VALIDATE',
        'DATE', 'LEVEL', 'RESOURCE', 'VALUES', 'DECIMAL', 'LIKE', 'REVOKE', 'VARCHAR', 'DEFAULT',
        'LOCK', 'ROW', 'VARCHAR2', 'DELETE', 'LONG', 'ROWID', 'VIEW', 'DESC', 'MAXEXTENTS',
        'ROWLABEL', 'WHENEVER', 'DISTINCT', 'MINUS', 'ROWNUM', 'WHERE', 'DROP', 'MODE', 'ROWS',
        'WITH', 'ADMIN', 'CURSOR', 'FOUND', 'MOUNT', 'AFTER', 'CYCLE', 'FUNCTION', 'NEXT',
        'ALLOCATE', 'DATABASE', 'GO', 'NEW', 'ANALYZE', 'DATAFILE', 'GOTO', 'NOARCHIVELOG',
        'ARCHIVE', 'DBA', 'GROUPS', 'NOCACHE', 'ARCHIVELOG', 'DEC', 'INCLUDING', 'NOCYCLE',
        'AUTHORIZATION', 'DECLARE', 'INDICATOR', 'NOMAXVALUE', 'AVG', 'DISABLE', 'INITRANS',
        'NOMINVALUE', 'BACKUP', 'DISMOUNT', 'INSTANCE', 'NONE', 'BEGIN', 'DOUBLE', 'INT', 'NOORDER',
        'BECOME', 'DUMP', 'KEY', 'NORESETLOGS', 'BEFORE', 'EACH', 'LANGUAGE', 'NORMAL', 'BLOCK',
        'ENABLE', 'LAYER', 'NOSORT', 'BODY', 'END', 'LINK', 'NUMERIC', 'CACHE', 'ESCAPE', 'LISTS',
        'OFF', 'CANCEL', 'EVENTS', 'LOGFILE', 'OLD', 'CASCADE', 'EXCEPT', 'MANAGE', 'ONLY',
        'CHANGE', 'EXCEPTIONS', 'MANUAL', 'OPEN', 'CHARACTER', 'EXEC', 'MAX', 'OPTIMAL',
        'CHECKPOINT', 'EXPLAIN', 'MAXDATAFILES', 'OWN', 'CLOSE', 'EXECUTE', 'MAXINSTANCES',
        'PACKAGE', 'COBOL', 'EXTENT', 'MAXLOGFILES', 'PARALLEL', 'COMMIT', 'EXTERNALLY',
        'MAXLOGHISTORY', 'PCTINCREASE', 'COMPILE', 'FETCH', 'MAXLOGMEMBERS', 'PCTUSED', 'CONSTRAINT',
        'FLUSH', 'MAXTRANS', 'PLAN', 'CONSTRAINTS', 'FREELIST', 'MAXVALUE', 'PLI', 'CONTENTS',
        'FREELISTS', 'MIN', 'PRECISION', 'CONTINUE', 'FORCE', 'MINEXTENTS', 'PRIMARY', 'CONTROLFILE',
        'FOREIGN', 'MINVALUE', 'PRIVATE', 'COUNT', 'FORTRAN', 'MODULE', 'PROCEDURE', 'PROFILE',
        'SAVEPOINT', 'SQLSTATE', 'TRACING', 'QUOTA', 'SCHEMA', 'STATEMENT_ID', 'TRANSACTION', 'READ',
        'SCN', 'STATISTICS', 'TRIGGERS', 'REAL', 'SECTION', 'STOP', 'TRUNCATE', 'RECOVER', 'SEGMENT',
        'STORAGE', 'UNDER', 'REFERENCES', 'SEQUENCE', 'SUM', 'UNLIMITED', 'REFERENCING', 'SHARED',
        'SWITCH', 'UNTIL', 'RESETLOGS', 'SNAPSHOT', 'SYSTEM', 'USE', 'RESTRICTED', 'SOME', 'TABLES',
        'USING', 'REUSE', 'SORT', 'TABLESPACE', 'WHEN', 'ROLE', 'SQL', 'TEMPORARY', 'WRITE', 'ROLES',
        'SQLCODE', 'THREAD', 'WORK', 'ROLLBACK', 'SQLERROR', 'TIME', 'ABORT', 'BETWEEN', 'CRASH',
        'DIGITS', 'ACCEPT', 'BINARY_INTEGER', 'CREATE', 'DISPOSE', 'ACCESS', 'BODY', 'CURRENT',
        'DISTINCT', 'ADD', 'BOOLEAN', 'CURRVAL', 'DO', 'ALL', 'BY', 'CURSOR', 'DROP', 'ALTER', 'CASE',
        'DATABASE', 'ELSE', 'AND', 'CHAR', 'DATA_BASE', 'ELSIF', 'ANY', 'CHAR_BASE', 'DATE', 'END',
        'ARRAY', 'CHECK', 'DBA', 'ENTRY', 'ARRAYLEN', 'CLOSE', 'DEBUGOFF', 'EXCEPTION', 'AS',
        'CLUSTER', 'DEBUGON', 'EXCEPTION_INIT', 'ASC', 'CLUSTERS', 'DECLARE', 'EXISTS', 'ASSERT',
        'COLAUTH', 'DECIMAL', 'EXIT', 'ASSIGN', 'COLUMNS', 'DEFAULT', 'FALSE', 'AT', 'COMMIT',
        'DEFINITION', 'FETCH', 'AUTHORIZATION', 'COMPRESS', 'DELAY', 'FLOAT', 'AVG', 'CONNECT',
        'DELETE', 'FOR', 'BASE_TABLE', 'CONSTANT', 'DELTA', 'FORM', 'BEGIN', 'COUNT', 'DESC', 'FROM',
        'FUNCTION', 'NEW', 'RELEASE', 'SUM', 'GENERIC', 'NEXTVAL', 'REMR', 'TABAUTH', 'GOTO',
        'NOCOMPRESS', 'RENAME', 'TABLE', 'GRANT', 'NOT', 'RESOURCE', 'TABLES', 'GROUP', 'NULL',
        'RETURN', 'TASK', 'HAVING', 'NUMBER', 'REVERSE', 'TERMINATE', 'IDENTIFIED', 'NUMBER_BASE',
        'REVOKE', 'THEN', 'IF', 'OF', 'ROLLBACK', 'TO', 'IN', 'ON', 'ROWID', 'TRUE', 'INDEX', 'OPEN',
        'ROWLABEL', 'TYPE', 'INDEXES', 'OPTION', 'ROWNUM', 'UNION', 'INDICATOR', 'OR', 'ROWTYPE',
        'UNIQUE', 'INSERT', 'ORDER', 'RUN', 'UPDATE', 'INTEGER', 'OTHERS', 'SAVEPOINT', 'USE',
        'INTERSECT', 'OUT', 'SCHEMA', 'VALUES', 'INTO', 'PACKAGE', 'SELECT', 'VARCHAR', 'IS',
        'PARTITION', 'SEPARATE', 'VARCHAR2', 'LEVEL', 'PCTFREE', 'SET', 'VARIANCE', 'LIKE',
        'POSITIVE', 'SIZE', 'VIEW', 'LIMITED', 'PRAGMA', 'SMALLINT', 'VIEWS', 'LOOP', 'PRIOR',
        'SPACE', 'WHEN', 'MAX', 'PRIVATE', 'SQL', 'WHERE', 'MIN', 'PROCEDURE', 'SQLCODE', 'WHILE',
        'MINUS', 'PUBLIC', 'SQLERRM', 'WITH', 'MLSLABEL', 'RAISE', 'START', 'WORK', 'MOD', 'RANGE',
        'STATEMENT', 'XOR', 'MODE', 'REAL', 'STDDEV', 'NATURAL', 'RECORD', 'SUBTYPE', 'SHAREDRT', 'SAMPLE'];

    var normalizeTypeFor = function(typeFor) {
        if (!typeFor) {
            // for safety if typeFor unknown assume Entity as it is smallest
            typeFor = Entity;
        }
        if (!(typeFor === Entity || typeFor === Property || typeFor === Relation)) {
            throw new Error('typeFor must be Entity, Property or Relation');
        }
        return typeFor;
    };

    EntityUtils.replaceReservedWord = function (text) {
        if (EntityUtils.isReservedWord(text)) {
            return text + '_';
        }
        return text;
    };

    /**
     * Tests whether the given text is a reserved word.
     * @param {String} text to check
     * @param {Object} typeFor the type of identifier the text is for
     * (entity/js/api/Entity or entity/js/api/Property)
     * @return {Boolean} true if the text is a reserved word.
     */
    EntityUtils.isReservedWord = function (text, typeFor) {
        typeFor = normalizeTypeFor(typeFor);

        var upper = text.toUpperCase();
        if (ArrayUtils.contains(RESERVED_WORDS, upper)) {
            return true;
        }
        return typeFor === Entity
                && ArrayUtils.contains(ENTITY_RESERVED_WORDS, upper);
    };

    /**
     * Construct a valid id based on the text passed in, optionally
     * using camelCase.
     * @param {String} text The string to base the id on
     * @param {String} camelCase, optionally pass in StringUtils.CAMELCASE_FIRST_UPPER
     * if an id using CamelCase formatting is required or StringUtils.CAMELCASE_FIRST_LOWER
     * for camelCase.
     * @param {Object} typeFor the type the id is for (entity/js/api/Entity or
     * entity/js/api/Property)
     * @returns {String} A valid ID string.
     */
    EntityUtils._makeValidId = function (text, camelCase, typeFor) {
        var id = EntityUtils.replaceInvalidChars(text);
        if (camelCase) {
            id = StringUtils.toCamelCase(id.replace(/_/g,' '), camelCase);
        }
        return EntityUtils.replaceReservedWord(id, typeFor);
    };

    /**
     * Checks whether the given id is a reserved word or contains illegal
     * characters. It does not to a length or exists check!
     * @param {type} id the id to check
     * @param {type} typeFor the type the id is for (entity/js/api/Entity or
     * entity/js/api/Property)
     * @returns {Boolean} false if the id is not valid, true otherwise
     */
    EntityUtils.isValidId = function (id, typeFor) {
        //check invalid characters
        if (INVALID_CHARS_REGEXP.test(id)) {
            return false;
        }
        return !EntityUtils.isReservedWord(id, typeFor);
    };

    /**
     * Derive a reasonable and unique entity ID from the given entity displayName.
     *
     * @param {string} displayName - the entity display name or suggested id
     * @param {Boolean} useCamelCase, optionally pass in true if an id using
     * CamelCase formatting is required.
     * @returns {string}
     */
    EntityUtils.generateEntityId = function (displayName, useCamelCase) {
        AbcsLib.checkDefined(displayName, 'displayName');

        var camelCase = useCamelCase ? StringUtils.CAMELCASE_FIRST_UPPER : undefined;

        var clear = EntityUtils._makeValidId(displayName, camelCase, Entity);

        if (!clear || clear.length < 2 || !(/^[a-zA-Z]{2}/.test(clear))) {
            clear = EntityUtils._makeValidId('bo_' + displayName, camelCase, Entity);
        } else if (clear.indexOf('breeze2_') === 0) {
            clear = clear.replace(/^breeze2_/g, 'breeze_2_');//let 'breeze2_' be special prefix
        }

        //now check if such entity exist and if so add a number postfix and try again until lucky
        var count = 2;
        var gen = EntityUtils._shortenToMaxLength(clear);
        while (DataModel.getInstance().getEntities().entityExists(gen)) {
            var suffix = '_' + count;
            count++;
            gen = EntityUtils._shortenToMaxLength(clear, suffix);
        }
        return gen;
    };

    /**
     * Tests whether an identifier for the given type is a valid length
     * @param {String} id the id to test
     * @param {Object} typeFor the type of object the identifier is for
     * (entity/js/api/Entity or entity/js/api/Property)
     * @return {int} the maximum length of an identifier
     */
    EntityUtils.isValidIdLength = function(id, typeFor) {
        return id.length <= EntityUtils._getIdMaxLength(typeFor);
    };

    EntityUtils._getIdMaxLength = function(typeFor) {
        typeFor = normalizeTypeFor(typeFor);

        // database limitation of table/column names
        var max = 30;
        if (typeFor === Entity) {
            // See BUFP-2609 - need to take into account '_PK_SEQ'
            max -= 7;
        } else if (typeFor === Relation) { // for the accessors
            max = 1000; // no real limit
        }
        return max;
    };

    /**
     * Will truncate the given name+suffix to the maximum length for the given
     * type.  The suffix, if given, will always be
     * appended, so any truncation of the text will be from name.
     * @param {String} name The name to be truncated
     * @param {String} suffix Suffix to include at the end of the name if given.
     * @param {Object} typeFor the type the id is for (entity/js/api/Entity or
     *  entity/js/api/Property)
     * @returns {String} shortened string with suffix
     */
    EntityUtils._shortenToMaxLength = function (name, suffix, typeFor) {
        var suffLen = suffix ? suffix.length : 0;
        var max = EntityUtils._getIdMaxLength(typeFor) - suffLen; /** for the dash and the unique numeric id **/
        if (name.length > max) {
            name = name.substring(0, max);
        }
        if (suffix) {
            return name + suffix;
        } else {
            return name;
        }
    };

    /**
     * Derive a reasonable and unique entity property ID from the given property displayName.
     *
     * @param {string} displayName - the property display name
     * @param {Entity} entity - the entity the property belongs to (optional, if
     * present the id is ensured to be unique).
     * @param {Array} usedIdsExtra extra ids already reserved (optional)
     * @param {Boolean} camelCase, optionally pass in true if an id using
     * camelCase formatting is required.
     * @param {boolean} accessorsInExtras if true, foreign relations are not rebuilt
     * to get all accessors of the entity, and the ids of thoses accessors are assumed to be
     * containsed within the extras array
     * @returns {string}
     */
    EntityUtils.generatePropertyId = function (displayName, entity, usedIdsExtra, camelCase, accessorsInExtras) {
        AbcsLib.checkDefined(displayName, 'displayName');
        var id = this.formatPropertyId(displayName, entity, camelCase);
        return EntityUtils._generateUniquePropertyOrAccessorId(id, entity, usedIdsExtra, Property, accessorsInExtras);
    };

    /**
     * Generates and sets a default Accessor ID(s) on the given Relation.
     * Accessors are used by RAMP for traversal of relations so this is only applicable
     * to Relations of Custom BOs.
     * @param {Relation} relation The Relation on which the default Accessor ID
     * (and/or reverse Accessor ID) should be set.
     */
    EntityUtils.setDefaultAccessors = function (relation) {
        AbcsLib.checkDataType(relation, Relation);
        if (relation.setAccessorID && relation.setReverseAccessorID) {
            var sourceEntity = relation.getSourceEntity();
            var targetEntity = relation.getTargetEntity();
            if (sourceEntity && sourceEntity.isInternal() && targetEntity && targetEntity.isInternal()) {
                var accessorID = EntityUtils._getDefaultAccessorName(relation, true);
                relation.setAccessorID(EntityUtils._generateUniquePropertyOrAccessorId(accessorID, sourceEntity, undefined, Relation));
                if (relation.isChildRelationship()) {
                    var reverseAccessorID = EntityUtils._getDefaultAccessorName(relation, false);
                    // namespace of reverse accessor needs to be within properties and
                    // accessors of the target entity
                    relation.setReverseAccessorID(EntityUtils._generateUniquePropertyOrAccessorId(reverseAccessorID, targetEntity, undefined, Relation));
                }
            }
        }
    };

    EntityUtils._getDefaultAccessorName = function(relation, bForward) {
        // logic is copied from DefinitionBuilder default accessor generation
        var manySUFFIX = 'Collection';
        var oneSUFFIX = 'Object';
        var accName = null;
        var card = relation.getCardinality();
        if (!card) {
            card = RelationCardinality.MANY_TO_ONE;
        }
        var endIsMany = card === RelationCardinality.MANY_TO_MANY ||
                (bForward && card === RelationCardinality.ONE_TO_MANY) ||
                (!bForward && card === RelationCardinality.MANY_TO_ONE);
        if (endIsMany) {
            var entityID = (bForward ? relation.getTargetEntityID() : relation.getSourceEntityID());
            if (!relation.isChildRelationship()) {
                accName = entityID + '_' + relation.getMappingPropertyID() + manySUFFIX;
            } else {
                accName = entityID + manySUFFIX;
            }
        } else {
            // TODO - As part of BUFP-11854 (and after BUFP-11852 is done),
            // the following hard coded refId + Object should change to:
            // String accessorID = relation.getMappingPropertyID();
            // if (accessorID.startsWith("ref2")) {
            //     accessorID = accessorID.substring(4);
            // }
           accName = bForward ?
                   relation.getMappingPropertyID() + oneSUFFIX :
                   relation.getSourceEntityID() + oneSUFFIX;
        }
        return accName;
    };

    /**
     * Return IDs of all Accessors of the given Entity.
     * Accessors are used by RAMP for traversal of relations so this is only applicable
     * to Relations of Custom BOs.
     * @param {Entity} entity The entiy owning the Accessors
     * @returns {String[]} Returns an array (possibly of zero length) of IDs of
     * all Accessors of the given Entity.
     */
    EntityUtils.getAccessorIds = function(entity) {
        var ids = [];
        if (entity) {
            AbcsLib.checkDataType(entity, Entity);
            entity.getRelations().forEach(function(rel) { // intentionally gets owned and foreign rels
                var accId = rel.getAccessorID();
                if (accId) {
                    ids.push(accId);
                }
            });
        }
        return ids;
    };

    /**
     * The ids of Properties and Accessors (used by RAMP for traversal of relations)
     * need to be in the same namespace as both appear at the same level in the
     * REST response.  This function will find a unique id, based on the id passed in
     * with some possible numeric suffix and trunctaion of length if necessary, within
     * the existing Property IDs, Accessor IDs and any other strings passed in as "extras".
     * The check for uniqueness will be case INSENSITIVE.
     *
     * @param {String} id The base value of the ID which will be used in generating
     * a unique id if not already unique.
     * @param {Entity} entity The Entity from which existing Property ID and Accessor
     * IDs will be tested
     * @param {String[]} extras an array of additional string to test against
     * @param {Object} typeFor indicator of the type of object the id is for
     * @param {boolean} accessorsInExtras if true, foreign relations are not rebuilt
     * to get all accessors of the entity, and the ids of thoses accessors are assumed to be
     * containsed within the extras array
     * @returns {String} A unique ID suitable for use as a Property or Accessor
     * ID for the given Entity.
     */
    EntityUtils._generateUniquePropertyOrAccessorId = function (id, entity, extras, typeFor, accessorsInExtras) {

        var existingIDs = {};
        if (entity) {
            entity.getProperties().forEach(function(prop){
                existingIDs[prop.getId().toUpperCase()] = true;
            });
            if (!accessorsInExtras) {
                EntityUtils.getAccessorIds(entity).forEach(function(id){
                    existingIDs[id.toUpperCase()] = true;
                });
            }
        }
        if (extras && extras.length) {
            extras.forEach(function(id) {
                existingIDs[id.toUpperCase()] = true;
            });
        }

        //now check if such id exists and if so add a number postfix and try again until lucky
        var count = 2;//if you have an object "MyObject" it seems reasonable to have second one marked as "MyObject_2"
        var gen = EntityUtils._shortenToMaxLength(id, undefined, typeFor);
        while (existingIDs[gen.toUpperCase()]) {
            var suffix = '_' + count;
            count++;
            gen = EntityUtils._shortenToMaxLength(id, suffix, typeFor);
        }
        return gen;
    };

    /**
     * Formats given property display name into a format Breeze understands.
     *
     * @param {string} displayName - the property display name
     * @param {Entity} [entity] - the entity the property belongs to
     * @param {Boolean} useCamelCase, optionally pass in true if an id using
     * camelCase formatting is required.
     * @returns {string}
     */
    EntityUtils.formatPropertyId = function (displayName, entity, useCamelCase) {
        AbcsLib.checkDefined(displayName, 'displayName');

        var camelCase = useCamelCase ? StringUtils.CAMELCASE_FIRST_LOWER : undefined;

        var clear = EntityUtils._makeValidId(displayName, camelCase, Property);

        if (clear === '_' || !clear) {
            //sometimes there are only special characters or empty name, it's more frienly to have:
            clear = 'pid';
        }

        if (clear.length < 2 || !(/^[a-zA-Z]{2}/.test(clear))) {
            if (camelCase) {
                clear = EntityUtils._makeValidId('pid ' + displayName, camelCase, Property);
            } else {
               clear = 'pid' + clear;
            }
        }

        if (entity && entity._isExtensionEntity()) {
            // prefix extEntity properties for easier identification;
            // this is needed to be done for extEntity primary key property
            // but for all other properties this can be removed if desirable;
            // prefixing makes them easier to identify
            clear = 'ext_' + clear;
        }

        return clear;
    };

    /**
     * Seeks for the existing relation between the parent's and child entity if
     * there is any. Searches for a relation connecting this entity and the
     * view's parent entity that has cardinality many-to-one (i.e. may be
     * displayed as a table)
     *
     * @param {Entity} parentEntity parent's entity to seek the relation to
     * @param {Entity} childEntity child's entity to seek for its relation
     * @returns {Relation|false} relation if found, false otherwise
     */
    EntityUtils.findParentChildRelation = function (parentEntity, childEntity) {
        AbcsLib.checkDefined(parentEntity, 'parentEntity');
        AbcsLib.checkDefined(childEntity, 'childEntity');

        var parentRelation;
        var rels = childEntity.getOwnRelations();

        $.each(rels, function (index, rel) {
            if (rel.getTargetEntityID() === parentEntity.getId()
                    && rel.isChildRelationship() // find the correct parent/child relation
                    && rel.getCardinality() === RelationCardinality.MANY_TO_ONE) {
                parentRelation = rel;
                return false;
            }
        });
        return parentRelation;
    };

    /**
     * Gets the specific display type for the given property type. Some types
     * are displayed to the user slightly differently than the
     * property.getType() would suggest,
     * e.g. 'lookup' and 'reference' both share PropertyType.REFERENCE and must
     * be differentiated by the target entity of their relation.
     *
     * @param {Entity} entity the entity
     * @param {Entity} property the property to determine which type
     * @returns {String} the display type of the property
     */
    EntityUtils.getDisplayType = function (entity, property) {
        var type = property.getType();
        if (type === PropertyType.REFERENCE) {
           var relation = entity.getRelation(property);
           var target = relation && relation.getTargetEntity();
           if (target && target.isLookupEntity()) {
               return 'LOOKUP';
           }
        }
        return type;
    };

    /**
     * Gets the PropertyType to set as the taragetPropertyType for a Relation
     * targetted at the given Entity.
     * @param {type} entity The target Entity
     * @returns {PropertyType} The PropertyType to use.  undefined is expected
     * for any Relation where the generic PropertyType.REFERENCE is to be used.
     * This will cover numeric keys.  However if the id of the target entity
     * is not numeric (or not known), a different PropertyType (typically
     * PropertyType.TEXT) will be returned.
     */
    EntityUtils.getPropertyTypeForRelationTarget = function (entity) {
        var propType; // Default value of undefined maps to PropertyType.REFERENCE
        if (!entity.isInternal()) {
            // For external entities, ServicesLoader holds a cache of the
            // original parsed service.json which for an entity optionally
            // includes a primaryKey indicator. if this is set we can use it
            // to determine the type of the target property. Otherwise, we use
            // TEXT as this will cover all cases
            propType = PropertyType.TEXT;
            var entities = EntityUtils.getEntitiesInTheSameProvider(entity);
            for (var i = 0; i < entities.length; i++) {
                var extEnt = entities[i];
                if (extEnt.id === entity.getAttribute('internalId')) {
                    if (extEnt.primaryKey) {
                        // Primary key Proerty defined against the entity
                        // so we can use this to determine the type
                        var pk = entity.getProperty(extEnt.primaryKey);
                        if (pk.getType() === PropertyType.NUMBER) {
                            propType = undefined;
                        }
                    }
                    break;
                }
            }
        }
        return propType;
    };

    /**
     * Gets the default mapping property id for a reference to the given target.
     * @param {String} target target entity or entity id.
     * @returns {String} default mapping property id
     */
    EntityUtils.mappingPropertyIdFor = function(target) {
        if (target && target.getId) {
            target = target.getId();
        }
        return 'ref2' + target;
    };

    /**
     * Returns all the entities in the same provider as the given entity
     * @param {Entity} entity the context entity
     * @returns {Array} an array of entities in this current provider, or an
     * empty array if the provider could not be found
     */
    EntityUtils.getEntitiesInTheSameProvider = function (entity) {
        if (entity) {
            AbcsLib.checkDataType(entity, Entity);
            var entityProvider = entity.getEntityProvider();
            if (entityProvider) {
                return entityProvider.getEntities();
            }
        }

        return [];
    };

    return EntityUtils;

});