EntityMetafacadeUtils.java

package org.andromda.metafacades.uml;

import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Random;
import org.andromda.core.common.ExceptionUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

/**
 * Utilities for dealing with entity metafacades
 *
 * @author Chad Brandon
 * @author Bob Fields
 */
public class EntityMetafacadeUtils
{
    /**
     * The logger instance.
     */
    private static final Logger LOGGER = Logger.getLogger(EntityMetafacadeUtils.class);

    /**
     * <p/> Converts a string following the Java naming conventions to a
     * database attribute name. For example convert customerName to
     * CUSTOMER_NAME.
     * </p>
     *
     * @param modelElementName the string to convert
     * @param separator character used to separate words
     * @return string converted to database attribute format
     */
    public static String toSqlName(
        String modelElementName,
        Object separator)
    {
        ExceptionUtils.checkEmpty(
            "modelElementName",
            modelElementName);

        StringBuilder sqlName = new StringBuilder();
        StringCharacterIterator iterator = new StringCharacterIterator(StringUtils.uncapitalize(modelElementName));

        for (char character = iterator.first(); character != CharacterIterator.DONE; character = iterator.next())
        {
            if (Character.isUpperCase(character))
            {
                sqlName.append(separator);
            }
            character = Character.toUpperCase(character);
            sqlName.append(character);
        }
        return StringEscapeUtils.escapeSql(sqlName.toString());
    }

    /**
     * Gets the SQL name. (i.e. column name, table name, etc.). If it can't find
     * the corresponding tagged value with the specified <code>name</code>,
     * then it uses the element name by default and just returns that.
     *
     * @param prefix the optional prefix to add to the sql name (i.e. table name
     *        prefix, etc.).
     * @param element from which to retrieve the SQL name.
     * @param name the name of the tagged value.
     * @param nameMaxLength if this is not null, then the name returned will be
     *        trimmed to this length (if it happens to be longer).
     * @param separator character used to separate words
     * @param shortenSqlNamesMethod Method of shortening SQL Names, i.e. removeVowels
     * @return the SQL name as a String.
     */
    public static String getSqlNameFromTaggedValue(
        String prefix,
        ModelElementFacade element,
        String name,
        Short nameMaxLength,
        Object separator,
        Object shortenSqlNamesMethod)
    {
        return getSqlNameFromTaggedValue(
            prefix,
            element,
            name,
            nameMaxLength,
            null,
            separator,
            shortenSqlNamesMethod);
    }

    /**
     * Gets the SQL name. (i.e. column name, table name, etc.). If it can't find
     * the corresponding tagged value with the specified <code>name</code>,
     * then it uses the element name by default and just returns that.
     *
     * @param element from which to retrieve the SQL name.
     * @param name the name of the tagged value.
     * @param nameMaxLength if this is not null, then the name returned will be
     *        trimmed to this length (if it happens to be longer).
     * @param suffix the optional suffix to add to the sql name (i.e. foreign
     *        key suffix, etc.)
     * @param separator character used to separate words
     * @param shortenSqlNamesMethod Method of shortening SQL Names, i.e. removeVowels
     * @return the SQL name as a String.
     */
    public static String getSqlNameFromTaggedValue(
        ModelElementFacade element,
        String name,
        Short nameMaxLength,
        String suffix,
        Object separator,
        Object shortenSqlNamesMethod)
    {
        return getSqlNameFromTaggedValue(
            null,
            element,
            name,
            nameMaxLength,
            suffix,
            separator,
            shortenSqlNamesMethod);
    }

    /**
     * Gets the SQL name. (i.e. column name, table name, etc.). If it can't find
     * the corresponding tagged value with the specified <code>name</code>,
     * then it uses the element name by default and just returns that.
     *
     * @param element from which to retrieve the SQL name.
     * @param name the name of the tagged value.
     * @param nameMaxLength if this is not null, then the name returned will be
     *        trimmed to this length (if it happens to be longer).
     * @param separator character used to separate words
     * @param shortenSqlNamesMethod Method of shortening SQL Names, i.e. removeVowels
     * @return the SQL name as a String.
     */
    public static String getSqlNameFromTaggedValue(
        ModelElementFacade element,
        String name,
        Short nameMaxLength,
        Object separator,
        Object shortenSqlNamesMethod)
    {
        return getSqlNameFromTaggedValue(
            null,
            element,
            name,
            nameMaxLength,
            null,
            separator,
            shortenSqlNamesMethod);
    }

    /**
     * Gets the SQL name. (i.e. column name, table name, etc.). If it can't find
     * the corresponding tagged value with the specified <code>name</code>,
     * then it uses the element name by default and just returns that.
     *
     * @param prefix the optional prefix to add to the sql name (i.e. table name
     *        prefix, etc.).
     * @param element from which to retrieve the SQL name.
     * @param name the name of the tagged value.
     * @param nameMaxLength if this is not null, then the name returned will be
     *        trimmed to this length (if it happens to be longer).
     * @param suffix the optional suffix to add to the sql name (i.e. foreign
     *        key suffix, etc.)
     * @param separator character used to separate words
     * @param shortenSqlNamesMethod Method of shortening SQL Names, i.e. removeVowels
     * @return the SQL name as a String.
     */
    public static String getSqlNameFromTaggedValue(
        String prefix,
        final ModelElementFacade element,
        String name,
        final Short nameMaxLength,
        String suffix,
        final Object separator,
        final Object shortenSqlNamesMethod)
    {
        if (element != null)
        {
            Object value = element.findTaggedValue(name);
            StringBuilder buffer = new StringBuilder(StringUtils.trimToEmpty((String)value));
            if (StringUtils.isEmpty(buffer.toString()))
            {
                // if we can't find the tagValue then use the
                // element name for the name
                buffer = new StringBuilder(toSqlName(
                            element.getName(),
                            separator));
                suffix = StringUtils.trimToEmpty(suffix);
                prefix = StringUtils.trimToEmpty(prefix);
                if (nameMaxLength != null)
                {
                    final short maxLength = (short)(nameMaxLength.shortValue() - suffix.length() - prefix.length());
                    buffer =
                        new StringBuilder(
                            EntityMetafacadeUtils.ensureMaximumNameLength(
                                buffer.toString(),
                                Short.valueOf(maxLength),
                                (String)shortenSqlNamesMethod));
                }
                if (StringUtils.isNotBlank(prefix))
                {
                    buffer.insert(
                        0,
                        prefix);
                }
                if (StringUtils.isNotBlank(suffix))
                {
                    buffer.append(suffix);
                }
            }
            name = buffer.toString();
        }
        return name;
    }

    private static List<Entity> sortedEntities;
    /**
     * Puts non-abstract entities in order based on associations. To be used in constructors and
     * tests so that entities may be created and deleted in the proper order, without
     * violating key constraints in the database
     *
     * @param entities the entity list to be sorted.
     * @param ascending true for entities with no associations first.
     * @return the sorted entity list.
     */
    public static List<Entity> sortEntities(
        final Collection<Entity> entities,
        boolean ascending)
    {
        // Initially holds entities with no outgoing relations. Add related entities to the end
        // Multiple andromda invocations - entity list is cached - check size
        List<Entity> sorted = new ArrayList<Entity>();
        if (sortedEntities==null)
        {
            sortedEntities = new ArrayList<Entity>();
        }
        if (entities == null || entities.isEmpty() ||
            (sortedEntities!=null && !sortedEntities.isEmpty() && sortedEntities.size() == entities.size()
            && sortedEntities.contains(entities.iterator().next())))
        {
            sorted = sortedEntities;
        }
        else
        {
            // Clear left-over entities from last time.
            sortedEntities.clear();
            // Move entities into the sorted list step by step
            List<Entity> toBeMoved = new ArrayList<Entity>();
            List<Entity> unsorted = new ArrayList<Entity>();
            for (Entity entity : entities)
            {
             // Remove abstract entities from the list: We won't be testing or creating abstract entities
                if (entity.isAbstract())
                {
                    toBeMoved.add(entity);
                }
                else
                {
                    //Collection<AssociationEndFacade> ends = entity.getNavigableConnectingEnds();
                    //System.out.println(entity.getName() + " Nav ends=" + ends.size());
                    // Put the entities with no associations first in the sorted list
                    if (entity.getNavigableConnectingEnds().size()==0)
                    {
                        sorted.add(entity);
                        //System.out.println(entity.getName() + " No associations");
                    }
                    else
                    {
                        unsorted.add(entity);
                    }
                }
            }
            // Prevent ConcurrentModificationException by using a temp removal list
            for (Entity entity : toBeMoved)
            {
                entities.remove(entity);
            }
            /*// Holds entities with relations after first pass. Second pass sorts the entities
            for (Entity entity : entities)
            {
                Collection<AssociationEndFacade> ends = entity.getNavigableConnectingEnds();
                //System.out.println(entity.getName() + " Nav ends=" + ends.size());
                // Put the entities with no associations first in the sorted list
                if (ends.size()==0)
                {
                    sorted.add(entity);
                    //System.out.println(entity.getName() + " No associations");
                }
                else
                {
                    unsorted.add(entity);
                }
            }*/
            /*for (Entity entity : sorted)
            {
                System.out.println(entity.getName() + " Sorted First");
            }
            String moved = " ToBeMoved: ";
            for (Entity entity : unsorted)
            {
                moved += entity.getName() + " ";
            }
            System.out.println(sorted.size() + " Sorted " + unsorted.size() + " Unsorted " + moved);*/
            // Work backwards from unsorted list, moving entities to sorted list until none remain
            // Since all associations must be owned by one side, all will eventually be moved to sorted list
            int moveCount = 1; // Stop looping if no more to be moved, prevent infinite loop on circular relationships
            while (unsorted.size() > 0 && moveCount > 0)
            {
                toBeMoved = new ArrayList<Entity>();
                for (Entity entity : unsorted)
                {
                    Collection<AssociationEndFacade> ends = entity.getNavigableConnectingEnds();
                    // See if any navigable connecting ends are not owned by this entity
                    boolean createFirst = true;
                    for (AssociationEndFacade end : ends)
                    {
                        ClassifierFacade entityEnd = end.getType();
                        // Owning relations are sorted after entities on the opposite end
                        int referencedPosition = unsorted.lastIndexOf(entityEnd);
                        //System.out.println(entity.getName() + " -> " + entityEnd.getName() + " refPos=" + referencedPosition + " isOwningEnd " + UMLMetafacadeUtils.isOwningEnd(end));
                        if (referencedPosition > -1 && UMLMetafacadeUtils.isOwningEnd(end) && !entityEnd.getFullyQualifiedName().equals(entity.getFullyQualifiedName()))
                        {
                            // Other Entity side of an owned relationship must be created first
                            createFirst = false;
                            break;
                        }
                    }
                    if (createFirst)
                    {
                        toBeMoved.add(entity);
                        //System.out.println(entity.getName() + " Added to toBeMoved");
                    }
                }
                moveCount = toBeMoved.size();
                /*moved = " ToBeMoved: ";
                for (Entity entity : toBeMoved)
                {
                    moved += entity.getName() + " ";
                }*/
                //System.out.println(sorted.size() + " Sorted " + unsorted.size() + " Unsorted " + toBeMoved.size() + moved);
                for (Entity entity : toBeMoved)
                {
                    unsorted.remove(entity);
                    sorted.add(entity);
                }
            }
            if (moveCount==0 && unsorted.size() > 0)
            {
                // There are unresolvable circular relationships
                String circular = "Circular relationships between Entities:";
                for (Entity entity : unsorted)
                {
                    circular += " " + entity.getName();
                }
                LOGGER.error(circular);
                // Add them to the sorted list anyway and hope for the best...
            }
            /*int moves = -1;
            for (Entity entity : unsorted)
            {
                if (!entity.isAbstract())
                {
                    Collection<AssociationEndFacade> ends = entity.getAssociationEnds();
                    System.out.println(entity.getName() + " Sorting " + ends.size() + " Ends");
                    // Test each association to see if incoming or outgoing. sort outgoing before incoming.
                    for (AssociationEndFacade end : ends)
                    {
                        AssociationEndFacade otherEnd = end.getOtherEnd();
                        if (otherEnd.getType() instanceof Entity)
                        {
                            // otherEnd is actually the association/type on this entity
                            Entity entityEnd = (Entity)otherEnd.getType();
                            // Incoming relations are sorted after other entities
                            // Aggregation and Composition always owns the end
                            // One to Many association many end comes last
                            int thisPosition = unsorted.lastIndexOf(entity);
                            int referencedPosition = unsorted.lastIndexOf(entityEnd);
                            // Determine if this end is the relationship owner
                            //boolean primary = BooleanUtils.toBoolean(
                            //    ObjectUtils.toString(end.findTaggedValue("andromda_persistence_associationEnd_primary")));
                            System.out.println(entity.getName() + "=" + thisPosition + " " + entityEnd.getName() + "=" + referencedPosition + " prop=" + otherEnd.getName() + " owned=" + isOwningEnd(otherEnd));
                            //System.out.println(entity.getName() + "=" + thisPosition + " " + entityEnd.getName() + "=" + referencedPosition + " prop=" + end.getName() + " AggE=" + end.isAggregation() + " CompE=" + end.isComposition() + " OAgg=" + otherEnd.isAggregation() + " OComp=" + otherEnd.isComposition() + " Many=" + end.isMany() + " One2One=" + end.isOne2One() + " end=" + end + " other=" + otherEnd);
                            // This owning end should be created after the other side Entity
                            if (thisPosition > -1 && referencedPosition > -1)
                            {
                                if (isOwningEnd(otherEnd) && thisPosition < referencedPosition)
                                {
                                    // Move the locations of the two List entries if referenced entity is higher in the list
                                    // Avoid ConcurrentModificationEx by operating on temp List
                                    unsorted.remove(entityEnd);
                                    unsorted.add(unsorted.lastIndexOf(entity), entityEnd);
                                    System.out.println(entityEnd.getName() + " moved in front of " + entity.getName());
                                    moves += 1;
                                }
                                else if (!isOwningEnd(otherEnd) && thisPosition > referencedPosition)
                                {
                                    // Move the locations of the two List entries if referenced entity is higher in the list
                                    unsorted.remove(end);
                                    unsorted.add(unsorted.lastIndexOf(entityEnd), entity);
                                    System.out.println(entity.getName() + " moved behind " + entityEnd.getName());
                                    moves += 1;
                                }
                            }
                        }
                    }
                }
            }*/
            sorted.addAll(unsorted);
            sortedEntities = sorted;
        }
        if (!ascending)
        {
            Collections.reverse(sorted);
        }
        return sorted;
    }

    /**
     * Gets the execution priority of a specific entity, based on dependency/owning relationships,
     * so that TestNG unit tests can be put in the proper order across all entities and CRUD methods
     *
     * @param entity the entity to be prioritized.
     * @return int the entity priority.
     */
    public static int getPriority(
        final Entity entity)
    {
        int priority = 0;
        if (sortedEntities!=null && !sortedEntities.isEmpty())
        {
            priority = sortedEntities.indexOf(entity);
            if (priority < 1)
            {
                priority = 0;
            }
            else
            {
                // Change from zero based to one based, allow 10 additional test methods between the standard tests.
                priority = priority*10 + 10;
            }
        }
        return priority;
    }

    /**
     * <p/> Trims the passed in value to the maximum name length.
     * </p>
     * If no maximum length has been set then this method does nothing.
     *
     * @param name the name length to check and trim if necessary
     * @param nameMaxLength if this is not null, then the name returned will be
     *        trimmed to this length (if it happens to be longer).
     * @param method Method of shortening SQL Names, i.e. removeVowels
     * @return String the string to be used as SQL type
     */
    public static String ensureMaximumNameLength(
        String name,
        Short nameMaxLength,
        String method)
    {
        if (StringUtils.isNotBlank(name) && nameMaxLength != null)
        {
            short max = nameMaxLength.shortValue();
            if(method != null && method.equalsIgnoreCase(UMLMetafacadeProperties.SHORTEN_SQL_NAMES_METHOD_REMOVE_VOWELS))
            {
                while(name.length() > max)
                {
                    final String[] vowels = new String[]{"A","E","I","O","U","a","e","i","o","u"};
                    final int lastVowelPos = StringUtils.lastIndexOfAny(name, vowels);
                    if(lastVowelPos == -1)
                    {
                        break; //no more vowels
                    }
                    else
                    {
                        name = name.substring(0,lastVowelPos)+name.substring(lastVowelPos+1);
                    }
                }
            }

            if (name.length() > max)
            {
                name = name.substring(
                        0,
                        max);
            }
        }
        return name;
    }

    /**
     * Gets all identifiers for an entity. If 'follow' is true, and if
     * no identifiers can be found on the entity, a search up the
     * inheritance chain will be performed, and the identifiers from
     * the first super class having them will be used.   If no
     * identifiers exist, a default identifier will be created if the
     * allowDefaultIdentifiers property is set to true.
     *
     * @param entity the entity for which to retrieve the identifiers
     * @param follow a flag indicating whether or not the inheritance hierarchy
     *        should be followed
     * @return the collection of entity identifier attributes.
     */
    public static Collection<ModelElementFacade> getIdentifiers(
        final Entity entity,
        final boolean follow)
    {
        //final Collection<ModelElementFacade> properties = entity.getAllProperties();
        final Collection<ModelElementFacade> identifiers = new ArrayList<ModelElementFacade>();
        // TODO Entity.getAttributes returns List<? extends AttributeFacade>, currently unchecked conversion
        final Collection<AttributeFacade> attributes = new ArrayList<AttributeFacade>(entity.getAttributes());
        MetafacadeUtils.filterByStereotype(
            attributes,
            UMLProfile.STEREOTYPE_IDENTIFIER);
        identifiers.addAll(attributes);
        final Collection<AssociationEndFacade> associations = new ArrayList<AssociationEndFacade>(entity.getNavigableConnectingEnds(follow));
        MetafacadeUtils.filterByStereotype(
                associations,
                UMLProfile.STEREOTYPE_IDENTIFIER);
        identifiers.addAll(associations);
        /*MetafacadeUtils.filterByStereotype(
                properties,
                UMLProfile.STEREOTYPE_IDENTIFIER);*/
        /*// Find identifiers of association identifiers otherEnd
        for (AssociationEndFacade associationEnd : associations)
        {
            ClassifierFacade classifier = associationEnd.getType();
            if (classifier instanceof Entity)
            {
                Collection<ModelElementFacade> entityIdentifiers = getIdentifiers((Entity)classifier, true);
                identifiers.addAll(entityIdentifiers);
            }
        } */
        // Reorder join columns if order is specified - must match FK column order
        final Collection<ModelElementFacade> sortedIdentifiers = new ArrayList<ModelElementFacade>();
        String joinOrder = (String)entity.findTaggedValue(UMLProfile.TAGGEDVALUE_PERSISTENCE_JOINCOLUMN_ORDER);
        if (StringUtils.isNotBlank(joinOrder))
        {
            //System.out.println(entity.getName() + " getIdentifiers " + joinOrder + " identifiers=" + identifiers.size());
            String[] joinList = StringUtils.split(joinOrder, " ,;|");
            for (String column : joinList)
            {
                for (ModelElementFacade facade : identifiers)
                {
                    if (facade instanceof EntityAssociationEnd)
                    {
                        EntityAssociationEnd assoc = (EntityAssociationEnd)facade;
                        if (assoc.getColumnName().equalsIgnoreCase(column))
                        {
                            sortedIdentifiers.add(assoc);
                            //System.out.println(entity.getName() + " added " + assoc);
                        }
                    }
                    else if (facade instanceof EntityAttribute)
                    {
                        EntityAttribute attr = (EntityAttribute)facade;
                        if (attr.getColumnName().equalsIgnoreCase(column))
                        {
                            sortedIdentifiers.add(attr);
                            //System.out.println(entity.getName() + " added " + attr);
                        }
                    }
                }
            }
            //System.out.println(entity.getName() + " getIdentifiers " + joinOrder + " sorted=" + sortedIdentifiers.size() + " " + identifiers.size());
            // Add remaining identifiers not found in joincolumn ordered list
            // .contains() does not work correctly for EntityAttribute
            for (ModelElementFacade facade : identifiers)
            {
                boolean contains = false;
                for (ModelElementFacade sorted : sortedIdentifiers)
                {
                    if (sorted.getFullyQualifiedName().equals(facade.getFullyQualifiedName()))
                    {
                        contains = true;
                        //System.out.println(entity.getName() + " contains " + facade.getName() + " facade=" + facade);
                    }
                }
                if (!contains)
                {
                    sortedIdentifiers.add(facade);
                    //System.out.println(entity.getName() + " added " + facade.getName() + " facade=" + facade);
                }
            }
            //System.out.println(entity.getName() + " getIdentifiers " + joinOrder + " sorted=" + sortedIdentifiers.size() + " " +  + identifiers.size());
        }
        else
        {
            sortedIdentifiers.addAll(identifiers);
        }
        if (sortedIdentifiers.isEmpty() && follow && entity.getGeneralization() instanceof Entity)
        {
            sortedIdentifiers.addAll(getIdentifiers((Entity)entity.getGeneralization(), follow));
        }
        return sortedIdentifiers;
    }

    /**
     * Gets all identifier attributes for an entity. If 'follow' is true, and if
     * no identifiers can be found on the entity, a search up the
     * inheritance chain will be performed, and the identifiers from
     * the first super class having them will be used. All identifier association
     * relationships are traversed to find the identifying attributes of the related
     * associationEnd classifiers, in addition to the primitive/wrapped identifiers
     * on the Entity itself.
     *
     * @param entity the entity for which to retrieve the identifiers
     * @param follow a flag indicating whether or not the inheritance hierarchy
     *        should be followed
     * @return the collection of entity identifier attributes.
     */
    public static Collection<ModelElementFacade> getIdentifierAttributes(
        final Entity entity,
        final boolean follow)
    {
        Collection<ModelElementFacade> identifierAttributes = new ArrayList<ModelElementFacade>();
        final Collection<ModelElementFacade> identifiers = EntityMetafacadeUtils.getIdentifiers(entity, follow);
        // Find identifiers of association identifiers otherEnd
        for (ModelElementFacade identifier : identifiers)
        {
            if (identifier instanceof AssociationEndFacade)
            {
                AssociationEndFacade associationEnd = (AssociationEndFacade)identifier;
                ClassifierFacade classifier = associationEnd.getType();
                if (classifier instanceof Entity)
                {
                    Collection<ModelElementFacade> entityIdentifiers = getIdentifierAttributes((Entity)classifier, true);
                    identifierAttributes.addAll(entityIdentifiers);
                }
            }
            else
            {
                identifierAttributes.add(identifier);
            }
        }
        if (identifiers.isEmpty() && follow && entity.getGeneralization() instanceof Entity)
        {
            identifierAttributes.addAll(getIdentifiers((Entity)entity.getGeneralization(), follow));
        }
        // Reorder join columns if order is specified - must match FK column order
        String joinOrder = (String)entity.findTaggedValue("andromda_persistence_joincolumn_order");
        /*System.out.println(entity.getName() + " getIdentifierAttributes " + joinOrder + " tags=" + entity.getTaggedValues().size() + " identifierAttr=" + identifierAttributes.size());
        if (entity.getTaggedValues().size() > 1)
        {
            for ( TaggedValueFacade value : entity.getTaggedValues())
            {
                System.out.println(entity.getName() + " tag name=" + value.getName() + " value=" + value);
            }
        }*/
        if (StringUtils.isNotBlank(joinOrder))
        {
            final Collection<ModelElementFacade> sortedIdentifiers = new ArrayList<ModelElementFacade>();
            String[] joinList = StringUtils.split(joinOrder, " ,;|");
            for (String column : joinList)
            {
                for (ModelElementFacade facade : identifierAttributes)
                {
                    if (facade instanceof EntityAssociationEnd)
                    {
                        EntityAssociationEnd assoc = (EntityAssociationEnd)facade;
                        if (assoc.getColumnName().equalsIgnoreCase(column))
                        {
                            sortedIdentifiers.add(assoc);
                        }
                    }
                    else if (facade instanceof EntityAttribute)
                    {
                        EntityAttribute attr = (EntityAttribute)facade;
                        if (attr.getColumnName().equalsIgnoreCase(column))
                        {
                            sortedIdentifiers.add(attr);
                        }
                    }
                }
            }
            //System.out.println(entity.getName() + " getIdentifierAttributes " + joinOrder + identifiers.size());
            // Add remaining identifiers not found in joincolumn ordered list
            if (sortedIdentifiers.size() < identifierAttributes.size())
            for (ModelElementFacade facade : identifierAttributes)
            {
                if (!sortedIdentifiers.contains(facade))
                {
                    sortedIdentifiers.add(facade);
                }
            }
            identifierAttributes = sortedIdentifiers;
        }
        else
        {
            identifiers.addAll(identifierAttributes);
        }
        return identifierAttributes;
    }

    /**
     * Constructs a sql type name from the given <code>mappedName</code> and
     * <code>columnLength</code>.
     *
     * @param typeName the actual type name (usually retrieved from a mappings
     *        file, i.e. NUMBER(19).
     * @param columnLength the length of the column.
     * @return the new name construct
     */
    public static String constructSqlTypeName(
        final String typeName,
        final String columnLength)
    {
        String value = typeName;
        if (StringUtils.isNotBlank(typeName))
        {
            final char beginChar = '(';
            final char endChar = ')';
            final int beginIndex = value.indexOf(beginChar);
            final int endIndex = value.indexOf(endChar);
            if (beginIndex != -1 && endIndex != -1 && endIndex > beginIndex)
            {
                String replacement = value.substring(
                        beginIndex,
                        endIndex) + endChar;
                value = StringUtils.replace(
                        value,
                        replacement,
                        beginChar + columnLength + endChar);
            }
            else
            {
                value = value + beginChar + columnLength + endChar;
            }
        }
        return value;
    }

    /**
     * Constructs and returns the foreign key constraint name for the given <code>associationEnd</code>, <code>suffix</code>, <code>sqlNameSeparator</code>
     * and <code>maxLengthProperty</code>.
     *
     * @param associationEnd the association end for which to construct the constraint name.
     * @param suffix the suffix appended to the constraint name (if not limited by length).
     * @param sqlNameSeparator the SQL name separator to use (i.e. '_').
     * @param maxLengthProperty the numeric value stored as a string indicating the max length the constraint may be.
     * @param shortenSqlNamesMethod Method of shortening SQL Names, i.e. removeVowels
     * @return the constructed foreign key constraint name.
     */
    public static String getForeignKeyConstraintName(EntityAssociationEnd associationEnd, String suffix, String sqlNameSeparator, String maxLengthProperty, String shortenSqlNamesMethod)
    {
        String constraintName;

        final Object taggedValueObject = associationEnd.findTaggedValue(
                UMLProfile.TAGGEDVALUE_PERSISTENCE_FOREIGN_KEY_CONSTRAINT_NAME);
        if (taggedValueObject == null)
        {
            // we construct our own foreign key constraint name here
            StringBuilder buffer = new StringBuilder();

            final ClassifierFacade type = associationEnd.getOtherEnd().getType();
            if (type instanceof Entity)
            {
                Entity entity = (Entity)type;
                buffer.append(entity.getTableName());
            }
            else
            {
                // should not happen
                buffer.append(type.getName().toUpperCase());
            }

            buffer.append(sqlNameSeparator);
            buffer.append(associationEnd.getColumnName());
            constraintName = buffer.toString();

            // we take into consideration the maximum length allowed
            final short maxLength = (short)(Short.valueOf(maxLengthProperty).shortValue() - suffix.length());
            buffer = new StringBuilder(EntityMetafacadeUtils.ensureMaximumNameLength(constraintName, Short.valueOf(maxLength), shortenSqlNamesMethod));
            buffer.append(suffix);
            constraintName = EntityMetafacadeUtils.getUniqueForeignKeyConstraintName(buffer.toString());
        }
        else
        {
            // use the tagged value
            constraintName = taggedValueObject.toString();
        }
        return constraintName;
    }

    /**
     * An internal static cache for foreign key names (allows us to keep track
     * of which ones have been used).  Its not great that its static, but for now
     * this is the easiest way to enforce this.
     */
    private static Collection<String> foreignKeyConstraintNameCache = new ArrayList<String>();

    /**
     * Retrieves a unique foreign key constraint name given the proposedName.  Compares the proposedName
     * against any foreign key names already stored in an internal collection.
     *
     * @param proposedName the proposed foreign key name.
     * @return the unique foreign key name.
     */
    private static String getUniqueForeignKeyConstraintName(String proposedName)
    {
        final char[] characters = proposedName.toCharArray();
        int numericValue = 0;
        for (int ctr = 0; ctr < characters.length; ctr++)
        {
            numericValue = numericValue + Character.getNumericValue(characters[0]);
        }
        return getUniqueForeignKeyConstraintName(proposedName, new Random(numericValue));
    }

    /**
     * Retrieves a unique foreign key constraint name given the proposedName.  Compares the proposedName
     * against any foreign key names already stored in an internal collection.
     *
     * @param proposedName the proposed foreign key name.
     * @param random the Random number generator to use for enforcing uniqueness.
     * @return the unique foreign key name.
     */
    private static String getUniqueForeignKeyConstraintName(String proposedName, final Random random)
    {
        String name;
        if (foreignKeyConstraintNameCache.contains(proposedName))
        {
            final char[] characters = proposedName.toCharArray();
            int randomInt = random.nextInt(characters.length);
            char randomChar = Character.toUpperCase(characters[randomInt]);
            proposedName = proposedName.substring(0, proposedName.length() - 1) + randomChar;
            name = getUniqueForeignKeyConstraintName(proposedName, random);
        }
        else
        {
            name = proposedName;
            foreignKeyConstraintNameCache.add(name);
        }
        return name;
    }

    /**
     * Clears out the foreign key cache.
     */
    public static void clearForeignKeyConstraintNameCache()
    {
        foreignKeyConstraintNameCache.clear();
    }

    /**
     * Finds the top level package in the model containing any classes or entity classes.
     * Can be used to define a groupId for a model, or in code generation scripts where top level package
     * is the starting point for further operations.
     * @param classifiers Entities in the current model
     * @param entityOnly true to find the top package containing entities
     * @return Package at the top level
     */
    public static PackageFacade getTopLevelPackage(LinkedHashSet<ClassifierFacade> classifiers, boolean entityOnly)
    {
        PackageFacade pkgFacade = null;
        if (classifiers == null || classifiers.isEmpty()) return pkgFacade;
        //System.out.println("getTopLevelPackage classifiers=" + classifiers.size());
        List<PackageFacade> packages = new ArrayList<PackageFacade>();
        for (ClassifierFacade classifier : classifiers)
        {
            //System.out.println("getTopLevelPackage classifier=" + classifier);
            if (!classifier.isDataType())
            {
                if (!entityOnly || classifier instanceof Entity)
                {
                    if (classifier.getStereotypeNames().size() > 0 && !packages.contains(classifier.getPackage()))
                    {
                        packages.add((PackageFacade)classifier.getPackage());
                        //System.out.println("getTopLevelPackage add " + ((PackageFacade)classifier.getPackage()).getFullyQualifiedName() + " package=" + classifier.getPackage());
                    }
                }
            }
        }
        if (packages.size()>0)
        {
            pkgFacade = packages.get(0);
            // Find the shortest name in package list containing the other names
            for (PackageFacade pkg : packages)
            {
                //System.out.println("getTopLevelPackage fqn=" + pkgFacade.getFullyQualifiedName() + " " + pkg.getFullyQualifiedName());
                if (pkgFacade.getFullyQualifiedName().indexOf(pkg.getFullyQualifiedName()) > 0)
                {
                    pkgFacade = pkg;
                }
                else if (pkg.getFullyQualifiedName().indexOf(pkgFacade.getFullyQualifiedName()) > 0)
                {
                    // Shortest name is there already
                }
                else
                {

                }
            }
            //System.out.println("getTopLevelPackage pkgFacade=" + pkgFacade.getFullyQualifiedName());
        }
        return pkgFacade;
    }
}