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 }