001package org.andromda.metafacades.uml;
002
003import java.text.CharacterIterator;
004import java.text.StringCharacterIterator;
005import java.util.ArrayList;
006import java.util.Collection;
007import java.util.Collections;
008import java.util.LinkedHashSet;
009import java.util.List;
010import java.util.Random;
011import org.andromda.core.common.ExceptionUtils;
012import org.apache.commons.lang.StringEscapeUtils;
013import org.apache.commons.lang.StringUtils;
014import org.apache.log4j.Logger;
015
016/**
017 * Utilities for dealing with entity metafacades
018 *
019 * @author Chad Brandon
020 * @author Bob Fields
021 */
022public class EntityMetafacadeUtils
023{
024    /**
025     * The logger instance.
026     */
027    private static final Logger LOGGER = Logger.getLogger(EntityMetafacadeUtils.class);
028
029    /**
030     * <p/> Converts a string following the Java naming conventions to a
031     * database attribute name. For example convert customerName to
032     * CUSTOMER_NAME.
033     * </p>
034     *
035     * @param modelElementName the string to convert
036     * @param separator character used to separate words
037     * @return string converted to database attribute format
038     */
039    public static String toSqlName(
040        String modelElementName,
041        Object separator)
042    {
043        ExceptionUtils.checkEmpty(
044            "modelElementName",
045            modelElementName);
046
047        StringBuilder sqlName = new StringBuilder();
048        StringCharacterIterator iterator = new StringCharacterIterator(StringUtils.uncapitalize(modelElementName));
049
050        for (char character = iterator.first(); character != CharacterIterator.DONE; character = iterator.next())
051        {
052            if (Character.isUpperCase(character))
053            {
054                sqlName.append(separator);
055            }
056            character = Character.toUpperCase(character);
057            sqlName.append(character);
058        }
059        return StringEscapeUtils.escapeSql(sqlName.toString());
060    }
061
062    /**
063     * Gets the SQL name. (i.e. column name, table name, etc.). If it can't find
064     * the corresponding tagged value with the specified <code>name</code>,
065     * then it uses the element name by default and just returns that.
066     *
067     * @param prefix the optional prefix to add to the sql name (i.e. table name
068     *        prefix, etc.).
069     * @param element from which to retrieve the SQL name.
070     * @param name the name of the tagged value.
071     * @param nameMaxLength if this is not null, then the name returned will be
072     *        trimmed to this length (if it happens to be longer).
073     * @param separator character used to separate words
074     * @param shortenSqlNamesMethod Method of shortening SQL Names, i.e. removeVowels
075     * @return the SQL name as a String.
076     */
077    public static String getSqlNameFromTaggedValue(
078        String prefix,
079        ModelElementFacade element,
080        String name,
081        Short nameMaxLength,
082        Object separator,
083        Object shortenSqlNamesMethod)
084    {
085        return getSqlNameFromTaggedValue(
086            prefix,
087            element,
088            name,
089            nameMaxLength,
090            null,
091            separator,
092            shortenSqlNamesMethod);
093    }
094
095    /**
096     * Gets the SQL name. (i.e. column name, table name, etc.). If it can't find
097     * the corresponding tagged value with the specified <code>name</code>,
098     * then it uses the element name by default and just returns that.
099     *
100     * @param element from which to retrieve the SQL name.
101     * @param name the name of the tagged value.
102     * @param nameMaxLength if this is not null, then the name returned will be
103     *        trimmed to this length (if it happens to be longer).
104     * @param suffix the optional suffix to add to the sql name (i.e. foreign
105     *        key suffix, etc.)
106     * @param separator character used to separate words
107     * @param shortenSqlNamesMethod Method of shortening SQL Names, i.e. removeVowels
108     * @return the SQL name as a String.
109     */
110    public static String getSqlNameFromTaggedValue(
111        ModelElementFacade element,
112        String name,
113        Short nameMaxLength,
114        String suffix,
115        Object separator,
116        Object shortenSqlNamesMethod)
117    {
118        return getSqlNameFromTaggedValue(
119            null,
120            element,
121            name,
122            nameMaxLength,
123            suffix,
124            separator,
125            shortenSqlNamesMethod);
126    }
127
128    /**
129     * Gets the SQL name. (i.e. column name, table name, etc.). If it can't find
130     * the corresponding tagged value with the specified <code>name</code>,
131     * then it uses the element name by default and just returns that.
132     *
133     * @param element from which to retrieve the SQL name.
134     * @param name the name of the tagged value.
135     * @param nameMaxLength if this is not null, then the name returned will be
136     *        trimmed to this length (if it happens to be longer).
137     * @param separator character used to separate words
138     * @param shortenSqlNamesMethod Method of shortening SQL Names, i.e. removeVowels
139     * @return the SQL name as a String.
140     */
141    public static String getSqlNameFromTaggedValue(
142        ModelElementFacade element,
143        String name,
144        Short nameMaxLength,
145        Object separator,
146        Object shortenSqlNamesMethod)
147    {
148        return getSqlNameFromTaggedValue(
149            null,
150            element,
151            name,
152            nameMaxLength,
153            null,
154            separator,
155            shortenSqlNamesMethod);
156    }
157
158    /**
159     * Gets the SQL name. (i.e. column name, table name, etc.). If it can't find
160     * the corresponding tagged value with the specified <code>name</code>,
161     * then it uses the element name by default and just returns that.
162     *
163     * @param prefix the optional prefix to add to the sql name (i.e. table name
164     *        prefix, etc.).
165     * @param element from which to retrieve the SQL name.
166     * @param name the name of the tagged value.
167     * @param nameMaxLength if this is not null, then the name returned will be
168     *        trimmed to this length (if it happens to be longer).
169     * @param suffix the optional suffix to add to the sql name (i.e. foreign
170     *        key suffix, etc.)
171     * @param separator character used to separate words
172     * @param shortenSqlNamesMethod Method of shortening SQL Names, i.e. removeVowels
173     * @return the SQL name as a String.
174     */
175    public static String getSqlNameFromTaggedValue(
176        String prefix,
177        final ModelElementFacade element,
178        String name,
179        final Short nameMaxLength,
180        String suffix,
181        final Object separator,
182        final Object shortenSqlNamesMethod)
183    {
184        if (element != null)
185        {
186            Object value = element.findTaggedValue(name);
187            StringBuilder buffer = new StringBuilder(StringUtils.trimToEmpty((String)value));
188            if (StringUtils.isEmpty(buffer.toString()))
189            {
190                // if we can't find the tagValue then use the
191                // element name for the name
192                buffer = new StringBuilder(toSqlName(
193                            element.getName(),
194                            separator));
195                suffix = StringUtils.trimToEmpty(suffix);
196                prefix = StringUtils.trimToEmpty(prefix);
197                if (nameMaxLength != null)
198                {
199                    final short maxLength = (short)(nameMaxLength.shortValue() - suffix.length() - prefix.length());
200                    buffer =
201                        new StringBuilder(
202                            EntityMetafacadeUtils.ensureMaximumNameLength(
203                                buffer.toString(),
204                                Short.valueOf(maxLength),
205                                (String)shortenSqlNamesMethod));
206                }
207                if (StringUtils.isNotBlank(prefix))
208                {
209                    buffer.insert(
210                        0,
211                        prefix);
212                }
213                if (StringUtils.isNotBlank(suffix))
214                {
215                    buffer.append(suffix);
216                }
217            }
218            name = buffer.toString();
219        }
220        return name;
221    }
222
223    private static List<Entity> sortedEntities;
224    /**
225     * Puts non-abstract entities in order based on associations. To be used in constructors and
226     * tests so that entities may be created and deleted in the proper order, without
227     * violating key constraints in the database
228     *
229     * @param entities the entity list to be sorted.
230     * @param ascending true for entities with no associations first.
231     * @return the sorted entity list.
232     */
233    public static List<Entity> sortEntities(
234        final Collection<Entity> entities,
235        boolean ascending)
236    {
237        // Initially holds entities with no outgoing relations. Add related entities to the end
238        // Multiple andromda invocations - entity list is cached - check size
239        List<Entity> sorted = new ArrayList<Entity>();
240        if (sortedEntities==null)
241        {
242            sortedEntities = new ArrayList<Entity>();
243        }
244        if (entities == null || entities.isEmpty() ||
245            (sortedEntities!=null && !sortedEntities.isEmpty() && sortedEntities.size() == entities.size()
246            && sortedEntities.contains(entities.iterator().next())))
247        {
248            sorted = sortedEntities;
249        }
250        else
251        {
252            // Clear left-over entities from last time.
253            sortedEntities.clear();
254            // Move entities into the sorted list step by step
255            List<Entity> toBeMoved = new ArrayList<Entity>();
256            List<Entity> unsorted = new ArrayList<Entity>();
257            for (Entity entity : entities)
258            {
259             // Remove abstract entities from the list: We won't be testing or creating abstract entities
260                if (entity.isAbstract())
261                {
262                    toBeMoved.add(entity);
263                }
264                else
265                {
266                    //Collection<AssociationEndFacade> ends = entity.getNavigableConnectingEnds();
267                    //System.out.println(entity.getName() + " Nav ends=" + ends.size());
268                    // Put the entities with no associations first in the sorted list
269                    if (entity.getNavigableConnectingEnds().size()==0)
270                    {
271                        sorted.add(entity);
272                        //System.out.println(entity.getName() + " No associations");
273                    }
274                    else
275                    {
276                        unsorted.add(entity);
277                    }
278                }
279            }
280            // Prevent ConcurrentModificationException by using a temp removal list
281            for (Entity entity : toBeMoved)
282            {
283                entities.remove(entity);
284            }
285            /*// Holds entities with relations after first pass. Second pass sorts the entities
286            for (Entity entity : entities)
287            {
288                Collection<AssociationEndFacade> ends = entity.getNavigableConnectingEnds();
289                //System.out.println(entity.getName() + " Nav ends=" + ends.size());
290                // Put the entities with no associations first in the sorted list
291                if (ends.size()==0)
292                {
293                    sorted.add(entity);
294                    //System.out.println(entity.getName() + " No associations");
295                }
296                else
297                {
298                    unsorted.add(entity);
299                }
300            }*/
301            /*for (Entity entity : sorted)
302            {
303                System.out.println(entity.getName() + " Sorted First");
304            }
305            String moved = " ToBeMoved: ";
306            for (Entity entity : unsorted)
307            {
308                moved += entity.getName() + " ";
309            }
310            System.out.println(sorted.size() + " Sorted " + unsorted.size() + " Unsorted " + moved);*/
311            // Work backwards from unsorted list, moving entities to sorted list until none remain
312            // Since all associations must be owned by one side, all will eventually be moved to sorted list
313            int moveCount = 1; // Stop looping if no more to be moved, prevent infinite loop on circular relationships
314            while (unsorted.size() > 0 && moveCount > 0)
315            {
316                toBeMoved = new ArrayList<Entity>();
317                for (Entity entity : unsorted)
318                {
319                    Collection<AssociationEndFacade> ends = entity.getNavigableConnectingEnds();
320                    // See if any navigable connecting ends are not owned by this entity
321                    boolean createFirst = true;
322                    for (AssociationEndFacade end : ends)
323                    {
324                        ClassifierFacade entityEnd = end.getType();
325                        // Owning relations are sorted after entities on the opposite end
326                        int referencedPosition = unsorted.lastIndexOf(entityEnd);
327                        //System.out.println(entity.getName() + " -> " + entityEnd.getName() + " refPos=" + referencedPosition + " isOwningEnd " + UMLMetafacadeUtils.isOwningEnd(end));
328                        if (referencedPosition > -1 && UMLMetafacadeUtils.isOwningEnd(end) && !entityEnd.getFullyQualifiedName().equals(entity.getFullyQualifiedName()))
329                        {
330                            // Other Entity side of an owned relationship must be created first
331                            createFirst = false;
332                            break;
333                        }
334                    }
335                    if (createFirst)
336                    {
337                        toBeMoved.add(entity);
338                        //System.out.println(entity.getName() + " Added to toBeMoved");
339                    }
340                }
341                moveCount = toBeMoved.size();
342                /*moved = " ToBeMoved: ";
343                for (Entity entity : toBeMoved)
344                {
345                    moved += entity.getName() + " ";
346                }*/
347                //System.out.println(sorted.size() + " Sorted " + unsorted.size() + " Unsorted " + toBeMoved.size() + moved);
348                for (Entity entity : toBeMoved)
349                {
350                    unsorted.remove(entity);
351                    sorted.add(entity);
352                }
353            }
354            if (moveCount==0 && unsorted.size() > 0)
355            {
356                // There are unresolvable circular relationships
357                String circular = "Circular relationships between Entities:";
358                for (Entity entity : unsorted)
359                {
360                    circular += " " + entity.getName();
361                }
362                LOGGER.error(circular);
363                // Add them to the sorted list anyway and hope for the best...
364            }
365            /*int moves = -1;
366            for (Entity entity : unsorted)
367            {
368                if (!entity.isAbstract())
369                {
370                    Collection<AssociationEndFacade> ends = entity.getAssociationEnds();
371                    System.out.println(entity.getName() + " Sorting " + ends.size() + " Ends");
372                    // Test each association to see if incoming or outgoing. sort outgoing before incoming.
373                    for (AssociationEndFacade end : ends)
374                    {
375                        AssociationEndFacade otherEnd = end.getOtherEnd();
376                        if (otherEnd.getType() instanceof Entity)
377                        {
378                            // otherEnd is actually the association/type on this entity
379                            Entity entityEnd = (Entity)otherEnd.getType();
380                            // Incoming relations are sorted after other entities
381                            // Aggregation and Composition always owns the end
382                            // One to Many association many end comes last
383                            int thisPosition = unsorted.lastIndexOf(entity);
384                            int referencedPosition = unsorted.lastIndexOf(entityEnd);
385                            // Determine if this end is the relationship owner
386                            //boolean primary = BooleanUtils.toBoolean(
387                            //    ObjectUtils.toString(end.findTaggedValue("andromda_persistence_associationEnd_primary")));
388                            System.out.println(entity.getName() + "=" + thisPosition + " " + entityEnd.getName() + "=" + referencedPosition + " prop=" + otherEnd.getName() + " owned=" + isOwningEnd(otherEnd));
389                            //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);
390                            // This owning end should be created after the other side Entity
391                            if (thisPosition > -1 && referencedPosition > -1)
392                            {
393                                if (isOwningEnd(otherEnd) && thisPosition < referencedPosition)
394                                {
395                                    // Move the locations of the two List entries if referenced entity is higher in the list
396                                    // Avoid ConcurrentModificationEx by operating on temp List
397                                    unsorted.remove(entityEnd);
398                                    unsorted.add(unsorted.lastIndexOf(entity), entityEnd);
399                                    System.out.println(entityEnd.getName() + " moved in front of " + entity.getName());
400                                    moves += 1;
401                                }
402                                else if (!isOwningEnd(otherEnd) && thisPosition > referencedPosition)
403                                {
404                                    // Move the locations of the two List entries if referenced entity is higher in the list
405                                    unsorted.remove(end);
406                                    unsorted.add(unsorted.lastIndexOf(entityEnd), entity);
407                                    System.out.println(entity.getName() + " moved behind " + entityEnd.getName());
408                                    moves += 1;
409                                }
410                            }
411                        }
412                    }
413                }
414            }*/
415            sorted.addAll(unsorted);
416            sortedEntities = sorted;
417        }
418        if (!ascending)
419        {
420            Collections.reverse(sorted);
421        }
422        return sorted;
423    }
424
425    /**
426     * Gets the execution priority of a specific entity, based on dependency/owning relationships,
427     * so that TestNG unit tests can be put in the proper order across all entities and CRUD methods
428     *
429     * @param entity the entity to be prioritized.
430     * @return int the entity priority.
431     */
432    public static int getPriority(
433        final Entity entity)
434    {
435        int priority = 0;
436        if (sortedEntities!=null && !sortedEntities.isEmpty())
437        {
438            priority = sortedEntities.indexOf(entity);
439            if (priority < 1)
440            {
441                priority = 0;
442            }
443            else
444            {
445                // Change from zero based to one based, allow 10 additional test methods between the standard tests.
446                priority = priority*10 + 10;
447            }
448        }
449        return priority;
450    }
451
452    /**
453     * <p/> Trims the passed in value to the maximum name length.
454     * </p>
455     * If no maximum length has been set then this method does nothing.
456     *
457     * @param name the name length to check and trim if necessary
458     * @param nameMaxLength if this is not null, then the name returned will be
459     *        trimmed to this length (if it happens to be longer).
460     * @param method Method of shortening SQL Names, i.e. removeVowels
461     * @return String the string to be used as SQL type
462     */
463    public static String ensureMaximumNameLength(
464        String name,
465        Short nameMaxLength,
466        String method)
467    {
468        if (StringUtils.isNotBlank(name) && nameMaxLength != null)
469        {
470            short max = nameMaxLength.shortValue();
471            if(method != null && method.equalsIgnoreCase(UMLMetafacadeProperties.SHORTEN_SQL_NAMES_METHOD_REMOVE_VOWELS))
472            {
473                while(name.length() > max)
474                {
475                    final String[] vowels = new String[]{"A","E","I","O","U","a","e","i","o","u"};
476                    final int lastVowelPos = StringUtils.lastIndexOfAny(name, vowels);
477                    if(lastVowelPos == -1)
478                    {
479                        break; //no more vowels
480                    }
481                    else
482                    {
483                        name = name.substring(0,lastVowelPos)+name.substring(lastVowelPos+1);
484                    }
485                }
486            }
487
488            if (name.length() > max)
489            {
490                name = name.substring(
491                        0,
492                        max);
493            }
494        }
495        return name;
496    }
497
498    /**
499     * Gets all identifiers for an entity. If 'follow' is true, and if
500     * no identifiers can be found on the entity, a search up the
501     * inheritance chain will be performed, and the identifiers from
502     * the first super class having them will be used.   If no
503     * identifiers exist, a default identifier will be created if the
504     * allowDefaultIdentifiers property is set to true.
505     *
506     * @param entity the entity for which to retrieve the identifiers
507     * @param follow a flag indicating whether or not the inheritance hierarchy
508     *        should be followed
509     * @return the collection of entity identifier attributes.
510     */
511    public static Collection<ModelElementFacade> getIdentifiers(
512        final Entity entity,
513        final boolean follow)
514    {
515        //final Collection<ModelElementFacade> properties = entity.getAllProperties();
516        final Collection<ModelElementFacade> identifiers = new ArrayList<ModelElementFacade>();
517        // TODO Entity.getAttributes returns List<? extends AttributeFacade>, currently unchecked conversion
518        final Collection<AttributeFacade> attributes = new ArrayList<AttributeFacade>(entity.getAttributes());
519        MetafacadeUtils.filterByStereotype(
520            attributes,
521            UMLProfile.STEREOTYPE_IDENTIFIER);
522        identifiers.addAll(attributes);
523        final Collection<AssociationEndFacade> associations = new ArrayList<AssociationEndFacade>(entity.getNavigableConnectingEnds(follow));
524        MetafacadeUtils.filterByStereotype(
525                associations,
526                UMLProfile.STEREOTYPE_IDENTIFIER);
527        identifiers.addAll(associations);
528        /*MetafacadeUtils.filterByStereotype(
529                properties,
530                UMLProfile.STEREOTYPE_IDENTIFIER);*/
531        /*// Find identifiers of association identifiers otherEnd
532        for (AssociationEndFacade associationEnd : associations)
533        {
534            ClassifierFacade classifier = associationEnd.getType();
535            if (classifier instanceof Entity)
536            {
537                Collection<ModelElementFacade> entityIdentifiers = getIdentifiers((Entity)classifier, true);
538                identifiers.addAll(entityIdentifiers);
539            }
540        } */
541        // Reorder join columns if order is specified - must match FK column order
542        final Collection<ModelElementFacade> sortedIdentifiers = new ArrayList<ModelElementFacade>();
543        String joinOrder = (String)entity.findTaggedValue(UMLProfile.TAGGEDVALUE_PERSISTENCE_JOINCOLUMN_ORDER);
544        if (StringUtils.isNotBlank(joinOrder))
545        {
546            //System.out.println(entity.getName() + " getIdentifiers " + joinOrder + " identifiers=" + identifiers.size());
547            String[] joinList = StringUtils.split(joinOrder, " ,;|");
548            for (String column : joinList)
549            {
550                for (ModelElementFacade facade : identifiers)
551                {
552                    if (facade instanceof EntityAssociationEnd)
553                    {
554                        EntityAssociationEnd assoc = (EntityAssociationEnd)facade;
555                        if (assoc.getColumnName().equalsIgnoreCase(column))
556                        {
557                            sortedIdentifiers.add(assoc);
558                            //System.out.println(entity.getName() + " added " + assoc);
559                        }
560                    }
561                    else if (facade instanceof EntityAttribute)
562                    {
563                        EntityAttribute attr = (EntityAttribute)facade;
564                        if (attr.getColumnName().equalsIgnoreCase(column))
565                        {
566                            sortedIdentifiers.add(attr);
567                            //System.out.println(entity.getName() + " added " + attr);
568                        }
569                    }
570                }
571            }
572            //System.out.println(entity.getName() + " getIdentifiers " + joinOrder + " sorted=" + sortedIdentifiers.size() + " " + identifiers.size());
573            // Add remaining identifiers not found in joincolumn ordered list
574            // .contains() does not work correctly for EntityAttribute
575            for (ModelElementFacade facade : identifiers)
576            {
577                boolean contains = false;
578                for (ModelElementFacade sorted : sortedIdentifiers)
579                {
580                    if (sorted.getFullyQualifiedName().equals(facade.getFullyQualifiedName()))
581                    {
582                        contains = true;
583                        //System.out.println(entity.getName() + " contains " + facade.getName() + " facade=" + facade);
584                    }
585                }
586                if (!contains)
587                {
588                    sortedIdentifiers.add(facade);
589                    //System.out.println(entity.getName() + " added " + facade.getName() + " facade=" + facade);
590                }
591            }
592            //System.out.println(entity.getName() + " getIdentifiers " + joinOrder + " sorted=" + sortedIdentifiers.size() + " " +  + identifiers.size());
593        }
594        else
595        {
596            sortedIdentifiers.addAll(identifiers);
597        }
598        if (sortedIdentifiers.isEmpty() && follow && entity.getGeneralization() instanceof Entity)
599        {
600            sortedIdentifiers.addAll(getIdentifiers((Entity)entity.getGeneralization(), follow));
601        }
602        return sortedIdentifiers;
603    }
604
605    /**
606     * Gets all identifier attributes for an entity. If 'follow' is true, and if
607     * no identifiers can be found on the entity, a search up the
608     * inheritance chain will be performed, and the identifiers from
609     * the first super class having them will be used. All identifier association
610     * relationships are traversed to find the identifying attributes of the related
611     * associationEnd classifiers, in addition to the primitive/wrapped identifiers
612     * on the Entity itself.
613     *
614     * @param entity the entity for which to retrieve the identifiers
615     * @param follow a flag indicating whether or not the inheritance hierarchy
616     *        should be followed
617     * @return the collection of entity identifier attributes.
618     */
619    public static Collection<ModelElementFacade> getIdentifierAttributes(
620        final Entity entity,
621        final boolean follow)
622    {
623        Collection<ModelElementFacade> identifierAttributes = new ArrayList<ModelElementFacade>();
624        final Collection<ModelElementFacade> identifiers = EntityMetafacadeUtils.getIdentifiers(entity, follow);
625        // Find identifiers of association identifiers otherEnd
626        for (ModelElementFacade identifier : identifiers)
627        {
628            if (identifier instanceof AssociationEndFacade)
629            {
630                AssociationEndFacade associationEnd = (AssociationEndFacade)identifier;
631                ClassifierFacade classifier = associationEnd.getType();
632                if (classifier instanceof Entity)
633                {
634                    Collection<ModelElementFacade> entityIdentifiers = getIdentifierAttributes((Entity)classifier, true);
635                    identifierAttributes.addAll(entityIdentifiers);
636                }
637            }
638            else
639            {
640                identifierAttributes.add(identifier);
641            }
642        }
643        if (identifiers.isEmpty() && follow && entity.getGeneralization() instanceof Entity)
644        {
645            identifierAttributes.addAll(getIdentifiers((Entity)entity.getGeneralization(), follow));
646        }
647        // Reorder join columns if order is specified - must match FK column order
648        String joinOrder = (String)entity.findTaggedValue("andromda_persistence_joincolumn_order");
649        /*System.out.println(entity.getName() + " getIdentifierAttributes " + joinOrder + " tags=" + entity.getTaggedValues().size() + " identifierAttr=" + identifierAttributes.size());
650        if (entity.getTaggedValues().size() > 1)
651        {
652            for ( TaggedValueFacade value : entity.getTaggedValues())
653            {
654                System.out.println(entity.getName() + " tag name=" + value.getName() + " value=" + value);
655            }
656        }*/
657        if (StringUtils.isNotBlank(joinOrder))
658        {
659            final Collection<ModelElementFacade> sortedIdentifiers = new ArrayList<ModelElementFacade>();
660            String[] joinList = StringUtils.split(joinOrder, " ,;|");
661            for (String column : joinList)
662            {
663                for (ModelElementFacade facade : identifierAttributes)
664                {
665                    if (facade instanceof EntityAssociationEnd)
666                    {
667                        EntityAssociationEnd assoc = (EntityAssociationEnd)facade;
668                        if (assoc.getColumnName().equalsIgnoreCase(column))
669                        {
670                            sortedIdentifiers.add(assoc);
671                        }
672                    }
673                    else if (facade instanceof EntityAttribute)
674                    {
675                        EntityAttribute attr = (EntityAttribute)facade;
676                        if (attr.getColumnName().equalsIgnoreCase(column))
677                        {
678                            sortedIdentifiers.add(attr);
679                        }
680                    }
681                }
682            }
683            //System.out.println(entity.getName() + " getIdentifierAttributes " + joinOrder + identifiers.size());
684            // Add remaining identifiers not found in joincolumn ordered list
685            if (sortedIdentifiers.size() < identifierAttributes.size())
686            for (ModelElementFacade facade : identifierAttributes)
687            {
688                if (!sortedIdentifiers.contains(facade))
689                {
690                    sortedIdentifiers.add(facade);
691                }
692            }
693            identifierAttributes = sortedIdentifiers;
694        }
695        else
696        {
697            identifiers.addAll(identifierAttributes);
698        }
699        return identifierAttributes;
700    }
701
702    /**
703     * Constructs a sql type name from the given <code>mappedName</code> and
704     * <code>columnLength</code>.
705     *
706     * @param typeName the actual type name (usually retrieved from a mappings
707     *        file, i.e. NUMBER(19).
708     * @param columnLength the length of the column.
709     * @return the new name construct
710     */
711    public static String constructSqlTypeName(
712        final String typeName,
713        final String columnLength)
714    {
715        String value = typeName;
716        if (StringUtils.isNotBlank(typeName))
717        {
718            final char beginChar = '(';
719            final char endChar = ')';
720            final int beginIndex = value.indexOf(beginChar);
721            final int endIndex = value.indexOf(endChar);
722            if (beginIndex != -1 && endIndex != -1 && endIndex > beginIndex)
723            {
724                String replacement = value.substring(
725                        beginIndex,
726                        endIndex) + endChar;
727                value = StringUtils.replace(
728                        value,
729                        replacement,
730                        beginChar + columnLength + endChar);
731            }
732            else
733            {
734                value = value + beginChar + columnLength + endChar;
735            }
736        }
737        return value;
738    }
739
740    /**
741     * Constructs and returns the foreign key constraint name for the given <code>associationEnd</code>, <code>suffix</code>, <code>sqlNameSeparator</code>
742     * and <code>maxLengthProperty</code>.
743     *
744     * @param associationEnd the association end for which to construct the constraint name.
745     * @param suffix the suffix appended to the constraint name (if not limited by length).
746     * @param sqlNameSeparator the SQL name separator to use (i.e. '_').
747     * @param maxLengthProperty the numeric value stored as a string indicating the max length the constraint may be.
748     * @param shortenSqlNamesMethod Method of shortening SQL Names, i.e. removeVowels
749     * @return the constructed foreign key constraint name.
750     */
751    public static String getForeignKeyConstraintName(EntityAssociationEnd associationEnd, String suffix, String sqlNameSeparator, String maxLengthProperty, String shortenSqlNamesMethod)
752    {
753        String constraintName;
754
755        final Object taggedValueObject = associationEnd.findTaggedValue(
756                UMLProfile.TAGGEDVALUE_PERSISTENCE_FOREIGN_KEY_CONSTRAINT_NAME);
757        if (taggedValueObject == null)
758        {
759            // we construct our own foreign key constraint name here
760            StringBuilder buffer = new StringBuilder();
761
762            final ClassifierFacade type = associationEnd.getOtherEnd().getType();
763            if (type instanceof Entity)
764            {
765                Entity entity = (Entity)type;
766                buffer.append(entity.getTableName());
767            }
768            else
769            {
770                // should not happen
771                buffer.append(type.getName().toUpperCase());
772            }
773
774            buffer.append(sqlNameSeparator);
775            buffer.append(associationEnd.getColumnName());
776            constraintName = buffer.toString();
777
778            // we take into consideration the maximum length allowed
779            final short maxLength = (short)(Short.valueOf(maxLengthProperty).shortValue() - suffix.length());
780            buffer = new StringBuilder(EntityMetafacadeUtils.ensureMaximumNameLength(constraintName, Short.valueOf(maxLength), shortenSqlNamesMethod));
781            buffer.append(suffix);
782            constraintName = EntityMetafacadeUtils.getUniqueForeignKeyConstraintName(buffer.toString());
783        }
784        else
785        {
786            // use the tagged value
787            constraintName = taggedValueObject.toString();
788        }
789        return constraintName;
790    }
791
792    /**
793     * An internal static cache for foreign key names (allows us to keep track
794     * of which ones have been used).  Its not great that its static, but for now
795     * this is the easiest way to enforce this.
796     */
797    private static Collection<String> foreignKeyConstraintNameCache = new ArrayList<String>();
798
799    /**
800     * Retrieves a unique foreign key constraint name given the proposedName.  Compares the proposedName
801     * against any foreign key names already stored in an internal collection.
802     *
803     * @param proposedName the proposed foreign key name.
804     * @return the unique foreign key name.
805     */
806    private static String getUniqueForeignKeyConstraintName(String proposedName)
807    {
808        final char[] characters = proposedName.toCharArray();
809        int numericValue = 0;
810        for (int ctr = 0; ctr < characters.length; ctr++)
811        {
812            numericValue = numericValue + Character.getNumericValue(characters[0]);
813        }
814        return getUniqueForeignKeyConstraintName(proposedName, new Random(numericValue));
815    }
816
817    /**
818     * Retrieves a unique foreign key constraint name given the proposedName.  Compares the proposedName
819     * against any foreign key names already stored in an internal collection.
820     *
821     * @param proposedName the proposed foreign key name.
822     * @param random the Random number generator to use for enforcing uniqueness.
823     * @return the unique foreign key name.
824     */
825    private static String getUniqueForeignKeyConstraintName(String proposedName, final Random random)
826    {
827        String name;
828        if (foreignKeyConstraintNameCache.contains(proposedName))
829        {
830            final char[] characters = proposedName.toCharArray();
831            int randomInt = random.nextInt(characters.length);
832            char randomChar = Character.toUpperCase(characters[randomInt]);
833            proposedName = proposedName.substring(0, proposedName.length() - 1) + randomChar;
834            name = getUniqueForeignKeyConstraintName(proposedName, random);
835        }
836        else
837        {
838            name = proposedName;
839            foreignKeyConstraintNameCache.add(name);
840        }
841        return name;
842    }
843
844    /**
845     * Clears out the foreign key cache.
846     */
847    public static void clearForeignKeyConstraintNameCache()
848    {
849        foreignKeyConstraintNameCache.clear();
850    }
851
852    /**
853     * Finds the top level package in the model containing any classes or entity classes.
854     * Can be used to define a groupId for a model, or in code generation scripts where top level package
855     * is the starting point for further operations.
856     * @param classifiers Entities in the current model
857     * @param entityOnly true to find the top package containing entities
858     * @return Package at the top level
859     */
860    public static PackageFacade getTopLevelPackage(LinkedHashSet<ClassifierFacade> classifiers, boolean entityOnly)
861    {
862        PackageFacade pkgFacade = null;
863        if (classifiers == null || classifiers.isEmpty()) return pkgFacade;
864        //System.out.println("getTopLevelPackage classifiers=" + classifiers.size());
865        List<PackageFacade> packages = new ArrayList<PackageFacade>();
866        for (ClassifierFacade classifier : classifiers)
867        {
868            //System.out.println("getTopLevelPackage classifier=" + classifier);
869            if (!classifier.isDataType())
870            {
871                if (!entityOnly || classifier instanceof Entity)
872                {
873                    if (classifier.getStereotypeNames().size() > 0 && !packages.contains(classifier.getPackage()))
874                    {
875                        packages.add((PackageFacade)classifier.getPackage());
876                        //System.out.println("getTopLevelPackage add " + ((PackageFacade)classifier.getPackage()).getFullyQualifiedName() + " package=" + classifier.getPackage());
877                    }
878                }
879            }
880        }
881        if (packages.size()>0)
882        {
883            pkgFacade = packages.get(0);
884            // Find the shortest name in package list containing the other names
885            for (PackageFacade pkg : packages)
886            {
887                //System.out.println("getTopLevelPackage fqn=" + pkgFacade.getFullyQualifiedName() + " " + pkg.getFullyQualifiedName());
888                if (pkgFacade.getFullyQualifiedName().indexOf(pkg.getFullyQualifiedName()) > 0)
889                {
890                    pkgFacade = pkg;
891                }
892                else if (pkg.getFullyQualifiedName().indexOf(pkgFacade.getFullyQualifiedName()) > 0)
893                {
894                    // Shortest name is there already
895                }
896                else
897                {
898
899                }
900            }
901            //System.out.println("getTopLevelPackage pkgFacade=" + pkgFacade.getFullyQualifiedName());
902        }
903        return pkgFacade;
904    }
905}