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}