View Javadoc
1   package org.andromda.core.mapping;
2   
3   import java.io.File;
4   import java.io.FileReader;
5   import java.net.MalformedURLException;
6   import java.net.URL;
7   import java.util.Collection;
8   import java.util.HashMap;
9   import java.util.Iterator;
10  import java.util.LinkedHashMap;
11  import java.util.Map;
12  import org.andromda.core.common.ExceptionUtils;
13  import org.andromda.core.common.ResourceUtils;
14  import org.andromda.core.common.XmlObjectFactory;
15  import org.apache.commons.lang.StringUtils;
16  import org.apache.commons.lang.builder.ToStringBuilder;
17  
18  /**
19   * <p> An object responsible for mapping multiple <code>from</code> values to
20   * single <code>to</code>. The public constructor should NOT be used to
21   * construct this instance. An instance of this object should be retrieved
22   * through the method getInstance(java.net.URL).
23   * </p>
24   * <p> The mappings will change based upon the language, database, etc being
25   * used. </p>
26   *
27   * @author Chad Brandon
28   * @author Wouter Zoons
29   * @author Bob Fields
30   * @see org.andromda.core.common.XmlObjectFactory
31   */
32  public class Mappings
33  {
34      /**
35       * Contains the set of Mapping objects keyed by the 'type' element defined
36       * within the from type mapping XML file.
37       */
38      private final Map<String, Mapping> mappings = new LinkedHashMap<String, Mapping>();
39  
40      /**
41       * A static mapping containing all logical mappings currently available.
42       */
43      private static final Map<String, Mappings> logicalMappings = new LinkedHashMap<String, Mappings>();
44  
45      /**
46       * Holds the resource path from which this Mappings object was loaded.
47       */
48      private URL resource;
49  
50      /**
51       * Returns a new configured instance of this Mappings configured from the
52       * mappings configuration URI string.
53       *
54       * @param mappingsUri the URI to the XML type mappings configuration file.
55       * @return Mappings the configured Mappings instance.
56       */
57      public static Mappings getInstance(String mappingsUri)
58      {
59          mappingsUri = StringUtils.trimToEmpty(mappingsUri);
60          ExceptionUtils.checkEmpty(
61              "mappingsUri",
62              mappingsUri);
63          try
64          {
65              Mappings mappings = logicalMappings.get(mappingsUri);
66              if (mappings == null)
67              {
68                  try
69                  {
70                      mappings = getInstance(new URL(mappingsUri));
71                  }
72                  catch (final MalformedURLException exception)
73                  {
74                      throw new MappingsException("The given URI --> '" + mappingsUri + "' is invalid", exception);
75                  }
76              }
77              return getInheritedMappings(mappings);
78          }
79          catch (final Throwable throwable)
80          {
81              throw new MappingsException(throwable);
82          }
83      }
84  
85      /**
86       * Attempts to get any inherited mappings for the
87       * given <code>mappings</code>.
88       *
89       * @param mappings the mappings instance for which to
90       *        get the inherited mappings.
91       * @return the Mappings populated with any inherited mappings
92       *         or just the same mappings unchanged if the
93       *         <code>mappings</code> doesn't extend anything.
94       * @throws Exception if an exception occurs.
95       */
96      private static Mappings getInheritedMappings(final Mappings mappings)
97          throws Exception
98      {
99          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 }