001package org.andromda.core.mapping;
002
003import java.io.File;
004import java.io.FileReader;
005import java.net.MalformedURLException;
006import java.net.URL;
007import java.util.Collection;
008import java.util.HashMap;
009import java.util.Iterator;
010import java.util.LinkedHashMap;
011import java.util.Map;
012import org.andromda.core.common.ExceptionUtils;
013import org.andromda.core.common.ResourceUtils;
014import org.andromda.core.common.XmlObjectFactory;
015import org.apache.commons.lang.StringUtils;
016import org.apache.commons.lang.builder.ToStringBuilder;
017
018/**
019 * <p> An object responsible for mapping multiple <code>from</code> values to
020 * single <code>to</code>. The public constructor should NOT be used to
021 * construct this instance. An instance of this object should be retrieved
022 * through the method getInstance(java.net.URL).
023 * </p>
024 * <p> The mappings will change based upon the language, database, etc being
025 * used. </p>
026 *
027 * @author Chad Brandon
028 * @author Wouter Zoons
029 * @author Bob Fields
030 * @see org.andromda.core.common.XmlObjectFactory
031 */
032public class Mappings
033{
034    /**
035     * Contains the set of Mapping objects keyed by the 'type' element defined
036     * within the from type mapping XML file.
037     */
038    private final Map<String, Mapping> mappings = new LinkedHashMap<String, Mapping>();
039
040    /**
041     * A static mapping containing all logical mappings currently available.
042     */
043    private static final Map<String, Mappings> logicalMappings = new LinkedHashMap<String, Mappings>();
044
045    /**
046     * Holds the resource path from which this Mappings object was loaded.
047     */
048    private URL resource;
049
050    /**
051     * Returns a new configured instance of this Mappings configured from the
052     * mappings configuration URI string.
053     *
054     * @param mappingsUri the URI to the XML type mappings configuration file.
055     * @return Mappings the configured Mappings instance.
056     */
057    public static Mappings getInstance(String mappingsUri)
058    {
059        mappingsUri = StringUtils.trimToEmpty(mappingsUri);
060        ExceptionUtils.checkEmpty(
061            "mappingsUri",
062            mappingsUri);
063        try
064        {
065            Mappings mappings = logicalMappings.get(mappingsUri);
066            if (mappings == null)
067            {
068                try
069                {
070                    mappings = getInstance(new URL(mappingsUri));
071                }
072                catch (final MalformedURLException exception)
073                {
074                    throw new MappingsException("The given URI --> '" + mappingsUri + "' is invalid", exception);
075                }
076            }
077            return getInheritedMappings(mappings);
078        }
079        catch (final Throwable throwable)
080        {
081            throw new MappingsException(throwable);
082        }
083    }
084
085    /**
086     * Attempts to get any inherited mappings for the
087     * given <code>mappings</code>.
088     *
089     * @param mappings the mappings instance for which to
090     *        get the inherited mappings.
091     * @return the Mappings populated with any inherited mappings
092     *         or just the same mappings unchanged if the
093     *         <code>mappings</code> doesn't extend anything.
094     * @throws Exception if an exception occurs.
095     */
096    private static Mappings getInheritedMappings(final Mappings mappings)
097        throws Exception
098    {
099        return getInheritedMappings(
100            mappings,
101            false);
102    }
103
104    /**
105     * Attempts to get any inherited mappings for the
106     * given <code>mappings</code>.
107     * This method may only be called when the logical mappings have been initialized.
108     *
109     * @param mappings the mappings instance for which to
110     *        get the inherited mappings.
111     * @param ignoreInheritanceFailure whether or not a failure retrieving the parent
112     *        should be ignored (an exception will be thrown otherwise).
113     * @return the Mappings populated with any inherited mappings
114     *         or just the same mappings unchanged if the
115     *         <code>mappings</code> doesn't extend anything.
116     * @throws Exception if an exception occurs.
117     * @see #initializeLogicalMappings()
118     */
119    private static Mappings getInheritedMappings(
120        final Mappings mappings,
121        final boolean ignoreInheritanceFailure)
122        throws Exception
123    {
124        // if we have a parent then we add the child mappings to
125        // the parent's (so we can override any duplicates in the
126        // parent) and set the child mappings to the parent's
127        if (mappings != null && StringUtils.isNotBlank(mappings.extendsUri))
128        {
129            Mappings parentMappings = logicalMappings.get(mappings.extendsUri);
130            if (parentMappings == null)
131            {
132                try
133                {
134                    // since we didn't find the parent in the logical
135                    // mappings, try a relative path
136                    parentMappings = getInstance(new File(mappings.getCompletePath(mappings.extendsUri)));
137                }
138                catch (final Exception exception)
139                {
140                    if (!ignoreInheritanceFailure)
141                    {
142                        throw exception;
143                    }
144                }
145            }
146            if (parentMappings != null)
147            {
148                mergeWithoutOverriding(parentMappings, mappings);
149            }
150        }
151        return mappings;
152    }
153
154    /**
155     * Returns a new configured instance of this Mappings configured from the
156     * mappings configuration URI.
157     *
158     * @param mappingsUri the URI to the XML type mappings configuration file.
159     * @return Mappings the configured Mappings instance.
160     */
161    public static Mappings getInstance(final URL mappingsUri)
162    {
163        return getInstance(
164            mappingsUri,
165            false);
166    }
167
168    /**
169     * Returns a new configured instance of this Mappings configured from the
170     * mappings configuration URI.
171     *
172     * @param mappingsUri the URI to the XML type mappings configuration file.
173     * @param ignoreInheritanceFailure a flag indicating whether or not failures while attempting
174     *        to retrieve the mapping's inheritance should be ignored.
175     * @return Mappings the configured Mappings instance.
176     */
177    private static Mappings getInstance(
178        final URL mappingsUri,
179        final boolean ignoreInheritanceFailure)
180    {
181        ExceptionUtils.checkNull(
182            "mappingsUri",
183            mappingsUri);
184        try
185        {
186            final Mappings mappings = (Mappings)XmlObjectFactory.getInstance(Mappings.class).getObject(mappingsUri);
187            mappings.resource = mappingsUri;
188            return getInheritedMappings(
189                mappings,
190                ignoreInheritanceFailure);
191        }
192        catch (final Throwable throwable)
193        {
194            throw new MappingsException(throwable);
195        }
196    }
197
198    /**
199     * Returns a new configured instance of this Mappings configured from the
200     * mappingsFile.
201     *
202     * @param mappingsFile the XML type mappings configuration file.
203     * @return Mappings the configured Mappings instance.
204     */
205    private static Mappings getInstance(final File mappingsFile)
206        throws Exception
207    {
208        final FileReader reader = new FileReader(mappingsFile);
209        final Mappings mappings =
210            (Mappings)XmlObjectFactory.getInstance(Mappings.class).getObject(reader);
211        mappings.resource = mappingsFile.toURI().toURL();
212        reader.close();
213        return mappings;
214    }
215
216    /**
217     * This initializes all logical mappings that
218     * are contained with global Mapping set.  This
219     * <strong>MUST</strong> be called after all logical
220     * mappings have been added through {@link #addLogicalMappings(java.net.URL)}
221     * otherwise inheritance between logical mappings will not work correctly.
222     */
223    public static void initializeLogicalMappings()
224    {
225        // !!! no calls to getInstance(..) must be made in this method !!!
226
227        // reorder the logical mappings so that they can safely be loaded
228        // (top-level mappings first)
229
230        final Map<String, Mappings> unprocessedMappings = new HashMap<String, Mappings>(logicalMappings);
231        final Map<String, Mappings> processedMappings = new LinkedHashMap<String, Mappings>(); // these will be in the good order
232
233        // keep looping until there are no more unprocessed mappings
234        // if nothing more can be processed but there are unprocessed mappings left
235        // then we have an error (cyclic dependency or unknown parent mappings) which cannot be solved
236        boolean processed = true;
237        while (processed)
238        {
239            // we need to have at least one entry processed before the routine qualifies for the next iteration
240            processed = false;
241
242            // we only process mappings if they have parents that have already been processed
243            for (final Iterator<Map.Entry<String, Mappings>> iterator = unprocessedMappings.entrySet().iterator(); iterator.hasNext();)
244            {
245                final Map.Entry<String, Mappings> logicalMapping = iterator.next();
246                final String name = logicalMapping.getKey();
247                final Mappings mappings = logicalMapping.getValue();
248
249                if (mappings.extendsUri == null)
250                {
251                    // no parent mappings are always safe to add
252
253                    // move to the map of processed mappings
254                    processedMappings.put(name, mappings);
255                    // remove from the map of unprocessed mappings
256                    iterator.remove();
257                    // set the flag
258                    processed = true;
259                }
260                else if (processedMappings.containsKey(mappings.extendsUri))
261                {
262                    final Mappings parentMappings = processedMappings.get(mappings.extendsUri);
263                    if (parentMappings != null)
264                    {
265                        mergeWithoutOverriding(parentMappings, mappings);
266                    }
267
268                    // move to the map of processed mappings
269                    processedMappings.put(name, mappings);
270                    // remove from the map of unprocessed mappings
271                    iterator.remove();
272                    // set the flag
273                    processed = true;
274                }
275            }
276
277        }
278
279        if (!unprocessedMappings.isEmpty())
280        {
281            throw new MappingsException(
282                "Logical mappings cannot be initialized due to invalid inheritance: " +
283                    unprocessedMappings.keySet());
284        }
285
286        logicalMappings.putAll(processedMappings);
287    }
288
289    /**
290     * Clears the entries from the logical mappings cache.
291     */
292    public static void clearLogicalMappings()
293    {
294        logicalMappings.clear();
295    }
296
297    /**
298     * Holds the name of this mapping. This corresponds usually to some language
299     * (i.e. Java, or a database such as Oracle, Sql Server, etc).
300     */
301    private String name = null;
302
303    /**
304     * Returns the name name (this is the name for which the type mappings are
305     * for).
306     *
307     * @return String the name name
308     */
309    public String getName()
310    {
311        final String methodName = "Mappings.getName";
312        if (StringUtils.isEmpty(this.name))
313        {
314            throw new MappingsException(methodName + " - name can not be null or empty");
315        }
316        return name;
317    }
318
319    /**
320     * Sets the name name.
321     *
322     * @param name a new name
323     */
324    public void setName(final String name)
325    {
326        this.name = name;
327    }
328
329    /**
330     * Stores the URI that this mappings extends.
331     */
332    private String extendsUri;
333
334    /**
335     * Sets the name of the mappings which this
336     * instance extends.
337     *
338     * @param extendsUri the URI of the mapping which
339     *        this one extends.
340     */
341    public void setExtendsUri(final String extendsUri)
342    {
343        this.extendsUri = extendsUri;
344    }
345
346    /**
347     * Adds a Mapping object to the set of current mappings.
348     *
349     * @param mapping the Mapping instance.
350     */
351    public void addMapping(final Mapping mapping)
352    {
353        ExceptionUtils.checkNull(
354            "mapping",
355            mapping);
356        final Collection<String> fromTypes = mapping.getFroms();
357        ExceptionUtils.checkNull(
358            "mapping.fromTypes",
359            fromTypes);
360        for (final String fromType : fromTypes)
361        {
362            mapping.setMappings(this);
363            this.mappings.put(
364                fromType,
365                mapping);
366        }
367    }
368
369    /**
370     * Adds the <code>mappings</code> instance to this Mappings instance
371     * overriding any mappings with duplicate names.
372     *
373     * @param mappings the Mappings instance to add this instance.
374     */
375    public void addMappings(final Mappings mappings)
376    {
377        if (mappings != null && mappings.mappings != null)
378        {
379            this.mappings.putAll(mappings.mappings);
380        }
381    }
382
383    /**
384     * Reads the argument parent mappings and copies any mapping entries that do not already exist in this instance.
385     * This method preserves ordering and add new entries to the end.
386     *
387     * @param sourceMappings the mappings from which to read possible new entries
388     * @param targetMappings the mappings to which to store possible new entries from the sourceMappings
389     */
390    private static void mergeWithoutOverriding(Mappings sourceMappings, Mappings targetMappings)
391    {
392        final Map<String, Mapping> allMappings = new LinkedHashMap<String, Mapping>(targetMappings.mappings.size() + sourceMappings.mappings.size());
393        allMappings.putAll(sourceMappings.mappings);
394        allMappings.putAll(targetMappings.mappings);
395        targetMappings.mappings.clear();
396        targetMappings.mappings.putAll(allMappings);
397    }
398
399    /**
400     * Returns the <code>to</code> mapping from a given <code>from</code>
401     * mapping.
402     *
403     * @param from the <code>from</code> mapping, this is the type/identifier
404     *        that is in the model.
405     * @return String to the <code>to</code> mapping (this is the mapping that
406     *         can be retrieved if a corresponding 'from' is found).
407     */
408    public String getTo(String from)
409    {
410        from = StringUtils.trimToEmpty(from);
411        final String initialFrom = from;
412        String to = null;
413
414        // first we check to see if there's an array
415        // type mapping directly defined in the mappings
416        final Mapping mapping = this.getMapping(from);
417        if (mapping != null)
418        {
419            to = mapping.getTo();
420        }
421        if (to == null)
422        {
423            to = initialFrom;
424        }
425        return StringUtils.trimToEmpty(to);
426    }
427
428    /**
429     * Adds a mapping to the globally available mappings, these are used by this
430     * class to instantiate mappings from logical names as opposed to physical
431     * names.
432     *
433     * @param mappingsUri the Mappings URI to add to the globally available Mapping
434     *        instances.
435     */
436    public static void addLogicalMappings(final URL mappingsUri)
437    {
438        final Mappings mappings = Mappings.getInstance(
439                mappingsUri,
440                true);
441        logicalMappings.put(
442            mappings.getName(),
443            mappings);
444    }
445
446    /**
447     * Returns true if the mapping contains the <code>from</code> value
448     *
449     * @param from the value of the from mapping.
450     * @return true if it contains <code>from</code>, false otherwise.
451     */
452    public boolean containsFrom(final String from)
453    {
454        return this.getMapping(from) != null;
455    }
456
457    /**
458     * Returns true if the mapping contains the <code>to</code> value
459     *
460     * @param to the value of the to mapping.
461     * @return true if it contains <code>to</code>, false otherwise.
462     */
463    public boolean containsTo(final String to)
464    {
465        for (Mapping mapping : this.getMappings())
466        {
467            if (mapping.getTo().trim().equals(to))
468            {
469                return true;
470            }
471        }
472        return false;
473    }
474
475    /**
476     * Returns the resource URI from which this Mappings object was loaded.
477     *
478     * @return URL of the resource.
479     */
480    public URL getResource()
481    {
482        return this.resource;
483    }
484
485    /**
486     * Gets all Mapping instances for for this Mappings instance.
487     *
488     * @return a collection containing <strong>all </strong> Mapping instances.
489     */
490    public Collection<Mapping> getMappings()
491    {
492        return this.mappings.values();
493    }
494
495    /**
496     * Gets the mapping having the given <code>from</code>.
497     *
498     * @param from the <code>from</code> mapping.
499     * @return the Mapping instance (or null if it doesn't exist).
500     */
501    public Mapping getMapping(final String from)
502    {
503        return this.mappings.get(StringUtils.trimToEmpty(from));
504    }
505
506    /**
507     * Caches the complete path.
508     */
509    private final Map<String, String> completePaths = new LinkedHashMap<String, String>();
510
511    /**
512     * Constructs the complete path from the given <code>relativePath</code>
513     * and the resource of the parent {@link Mappings#getResource()} as the root
514     * of the path.
515     * @param relativePath
516     * @return the complete path.
517     */
518    final String getCompletePath(final String relativePath)
519    {
520        String completePath = this.completePaths.get(relativePath);
521        if (completePath == null)
522        {
523            final StringBuilder path = new StringBuilder();
524            if (this.mappings != null)
525            {
526                final URL resource = this.getResource();
527                if (resource != null)
528                {
529                    String rootPath = resource.getFile().replace(
530                            '\\',
531                            '/');
532                    rootPath = rootPath.substring(
533                            0,
534                            rootPath.lastIndexOf('/') + 1);
535                    path.append(rootPath);
536                }
537            }
538            if (relativePath != null)
539            {
540                path.append(StringUtils.trimToEmpty(relativePath));
541            }
542            completePath = path.toString();
543            this.completePaths.put(
544                relativePath,
545                completePath);
546        }
547        return ResourceUtils.unescapeFilePath(completePath);
548    }
549
550    /**
551     * @see Object#toString()
552     */
553    public String toString()
554    {
555        return ToStringBuilder.reflectionToString(this);
556    }
557}