Mappings.java

package org.andromda.core.mapping;

import java.io.File;
import java.io.FileReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import org.andromda.core.common.ExceptionUtils;
import org.andromda.core.common.ResourceUtils;
import org.andromda.core.common.XmlObjectFactory;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.ToStringBuilder;

/**
 * <p> An object responsible for mapping multiple <code>from</code> values to
 * single <code>to</code>. The public constructor should NOT be used to
 * construct this instance. An instance of this object should be retrieved
 * through the method getInstance(java.net.URL).
 * </p>
 * <p> The mappings will change based upon the language, database, etc being
 * used. </p>
 *
 * @author Chad Brandon
 * @author Wouter Zoons
 * @author Bob Fields
 * @see org.andromda.core.common.XmlObjectFactory
 */
public class Mappings
{
    /**
     * Contains the set of Mapping objects keyed by the 'type' element defined
     * within the from type mapping XML file.
     */
    private final Map<String, Mapping> mappings = new LinkedHashMap<String, Mapping>();

    /**
     * A static mapping containing all logical mappings currently available.
     */
    private static final Map<String, Mappings> logicalMappings = new LinkedHashMap<String, Mappings>();

    /**
     * Holds the resource path from which this Mappings object was loaded.
     */
    private URL resource;

    /**
     * Returns a new configured instance of this Mappings configured from the
     * mappings configuration URI string.
     *
     * @param mappingsUri the URI to the XML type mappings configuration file.
     * @return Mappings the configured Mappings instance.
     */
    public static Mappings getInstance(String mappingsUri)
    {
        mappingsUri = StringUtils.trimToEmpty(mappingsUri);
        ExceptionUtils.checkEmpty(
            "mappingsUri",
            mappingsUri);
        try
        {
            Mappings mappings = logicalMappings.get(mappingsUri);
            if (mappings == null)
            {
                try
                {
                    mappings = getInstance(new URL(mappingsUri));
                }
                catch (final MalformedURLException exception)
                {
                    throw new MappingsException("The given URI --> '" + mappingsUri + "' is invalid", exception);
                }
            }
            return getInheritedMappings(mappings);
        }
        catch (final Throwable throwable)
        {
            throw new MappingsException(throwable);
        }
    }

    /**
     * Attempts to get any inherited mappings for the
     * given <code>mappings</code>.
     *
     * @param mappings the mappings instance for which to
     *        get the inherited mappings.
     * @return the Mappings populated with any inherited mappings
     *         or just the same mappings unchanged if the
     *         <code>mappings</code> doesn't extend anything.
     * @throws Exception if an exception occurs.
     */
    private static Mappings getInheritedMappings(final Mappings mappings)
        throws Exception
    {
        return getInheritedMappings(
            mappings,
            false);
    }

    /**
     * Attempts to get any inherited mappings for the
     * given <code>mappings</code>.
     * This method may only be called when the logical mappings have been initialized.
     *
     * @param mappings the mappings instance for which to
     *        get the inherited mappings.
     * @param ignoreInheritanceFailure whether or not a failure retrieving the parent
     *        should be ignored (an exception will be thrown otherwise).
     * @return the Mappings populated with any inherited mappings
     *         or just the same mappings unchanged if the
     *         <code>mappings</code> doesn't extend anything.
     * @throws Exception if an exception occurs.
     * @see #initializeLogicalMappings()
     */
    private static Mappings getInheritedMappings(
        final Mappings mappings,
        final boolean ignoreInheritanceFailure)
        throws Exception
    {
        // if we have a parent then we add the child mappings to
        // the parent's (so we can override any duplicates in the
        // parent) and set the child mappings to the parent's
        if (mappings != null && StringUtils.isNotBlank(mappings.extendsUri))
        {
            Mappings parentMappings = logicalMappings.get(mappings.extendsUri);
            if (parentMappings == null)
            {
                try
                {
                    // since we didn't find the parent in the logical
                    // mappings, try a relative path
                    parentMappings = getInstance(new File(mappings.getCompletePath(mappings.extendsUri)));
                }
                catch (final Exception exception)
                {
                    if (!ignoreInheritanceFailure)
                    {
                        throw exception;
                    }
                }
            }
            if (parentMappings != null)
            {
                mergeWithoutOverriding(parentMappings, mappings);
            }
        }
        return mappings;
    }

    /**
     * Returns a new configured instance of this Mappings configured from the
     * mappings configuration URI.
     *
     * @param mappingsUri the URI to the XML type mappings configuration file.
     * @return Mappings the configured Mappings instance.
     */
    public static Mappings getInstance(final URL mappingsUri)
    {
        return getInstance(
            mappingsUri,
            false);
    }

    /**
     * Returns a new configured instance of this Mappings configured from the
     * mappings configuration URI.
     *
     * @param mappingsUri the URI to the XML type mappings configuration file.
     * @param ignoreInheritanceFailure a flag indicating whether or not failures while attempting
     *        to retrieve the mapping's inheritance should be ignored.
     * @return Mappings the configured Mappings instance.
     */
    private static Mappings getInstance(
        final URL mappingsUri,
        final boolean ignoreInheritanceFailure)
    {
        ExceptionUtils.checkNull(
            "mappingsUri",
            mappingsUri);
        try
        {
            final Mappings mappings = (Mappings)XmlObjectFactory.getInstance(Mappings.class).getObject(mappingsUri);
            mappings.resource = mappingsUri;
            return getInheritedMappings(
                mappings,
                ignoreInheritanceFailure);
        }
        catch (final Throwable throwable)
        {
            throw new MappingsException(throwable);
        }
    }

    /**
     * Returns a new configured instance of this Mappings configured from the
     * mappingsFile.
     *
     * @param mappingsFile the XML type mappings configuration file.
     * @return Mappings the configured Mappings instance.
     */
    private static Mappings getInstance(final File mappingsFile)
        throws Exception
    {
        final FileReader reader = new FileReader(mappingsFile);
        final Mappings mappings =
            (Mappings)XmlObjectFactory.getInstance(Mappings.class).getObject(reader);
        mappings.resource = mappingsFile.toURI().toURL();
        reader.close();
        return mappings;
    }

    /**
     * This initializes all logical mappings that
     * are contained with global Mapping set.  This
     * <strong>MUST</strong> be called after all logical
     * mappings have been added through {@link #addLogicalMappings(java.net.URL)}
     * otherwise inheritance between logical mappings will not work correctly.
     */
    public static void initializeLogicalMappings()
    {
        // !!! no calls to getInstance(..) must be made in this method !!!

        // reorder the logical mappings so that they can safely be loaded
        // (top-level mappings first)

        final Map<String, Mappings> unprocessedMappings = new HashMap<String, Mappings>(logicalMappings);
        final Map<String, Mappings> processedMappings = new LinkedHashMap<String, Mappings>(); // these will be in the good order

        // keep looping until there are no more unprocessed mappings
        // if nothing more can be processed but there are unprocessed mappings left
        // then we have an error (cyclic dependency or unknown parent mappings) which cannot be solved
        boolean processed = true;
        while (processed)
        {
            // we need to have at least one entry processed before the routine qualifies for the next iteration
            processed = false;

            // we only process mappings if they have parents that have already been processed
            for (final Iterator<Map.Entry<String, Mappings>> iterator = unprocessedMappings.entrySet().iterator(); iterator.hasNext();)
            {
                final Map.Entry<String, Mappings> logicalMapping = iterator.next();
                final String name = logicalMapping.getKey();
                final Mappings mappings = logicalMapping.getValue();

                if (mappings.extendsUri == null)
                {
                    // no parent mappings are always safe to add

                    // move to the map of processed mappings
                    processedMappings.put(name, mappings);
                    // remove from the map of unprocessed mappings
                    iterator.remove();
                    // set the flag
                    processed = true;
                }
                else if (processedMappings.containsKey(mappings.extendsUri))
                {
                    final Mappings parentMappings = processedMappings.get(mappings.extendsUri);
                    if (parentMappings != null)
                    {
                        mergeWithoutOverriding(parentMappings, mappings);
                    }

                    // move to the map of processed mappings
                    processedMappings.put(name, mappings);
                    // remove from the map of unprocessed mappings
                    iterator.remove();
                    // set the flag
                    processed = true;
                }
            }

        }

        if (!unprocessedMappings.isEmpty())
        {
            throw new MappingsException(
                "Logical mappings cannot be initialized due to invalid inheritance: " +
                    unprocessedMappings.keySet());
        }

        logicalMappings.putAll(processedMappings);
    }

    /**
     * Clears the entries from the logical mappings cache.
     */
    public static void clearLogicalMappings()
    {
        logicalMappings.clear();
    }

    /**
     * Holds the name of this mapping. This corresponds usually to some language
     * (i.e. Java, or a database such as Oracle, Sql Server, etc).
     */
    private String name = null;

    /**
     * Returns the name name (this is the name for which the type mappings are
     * for).
     *
     * @return String the name name
     */
    public String getName()
    {
        final String methodName = "Mappings.getName";
        if (StringUtils.isEmpty(this.name))
        {
            throw new MappingsException(methodName + " - name can not be null or empty");
        }
        return name;
    }

    /**
     * Sets the name name.
     *
     * @param name a new name
     */
    public void setName(final String name)
    {
        this.name = name;
    }

    /**
     * Stores the URI that this mappings extends.
     */
    private String extendsUri;

    /**
     * Sets the name of the mappings which this
     * instance extends.
     *
     * @param extendsUri the URI of the mapping which
     *        this one extends.
     */
    public void setExtendsUri(final String extendsUri)
    {
        this.extendsUri = extendsUri;
    }

    /**
     * Adds a Mapping object to the set of current mappings.
     *
     * @param mapping the Mapping instance.
     */
    public void addMapping(final Mapping mapping)
    {
        ExceptionUtils.checkNull(
            "mapping",
            mapping);
        final Collection<String> fromTypes = mapping.getFroms();
        ExceptionUtils.checkNull(
            "mapping.fromTypes",
            fromTypes);
        for (final String fromType : fromTypes)
        {
            mapping.setMappings(this);
            this.mappings.put(
                fromType,
                mapping);
        }
    }

    /**
     * Adds the <code>mappings</code> instance to this Mappings instance
     * overriding any mappings with duplicate names.
     *
     * @param mappings the Mappings instance to add this instance.
     */
    public void addMappings(final Mappings mappings)
    {
        if (mappings != null && mappings.mappings != null)
        {
            this.mappings.putAll(mappings.mappings);
        }
    }

    /**
     * Reads the argument parent mappings and copies any mapping entries that do not already exist in this instance.
     * This method preserves ordering and add new entries to the end.
     *
     * @param sourceMappings the mappings from which to read possible new entries
     * @param targetMappings the mappings to which to store possible new entries from the sourceMappings
     */
    private static void mergeWithoutOverriding(Mappings sourceMappings, Mappings targetMappings)
    {
        final Map<String, Mapping> allMappings = new LinkedHashMap<String, Mapping>(targetMappings.mappings.size() + sourceMappings.mappings.size());
        allMappings.putAll(sourceMappings.mappings);
        allMappings.putAll(targetMappings.mappings);
        targetMappings.mappings.clear();
        targetMappings.mappings.putAll(allMappings);
    }

    /**
     * Returns the <code>to</code> mapping from a given <code>from</code>
     * mapping.
     *
     * @param from the <code>from</code> mapping, this is the type/identifier
     *        that is in the model.
     * @return String to the <code>to</code> mapping (this is the mapping that
     *         can be retrieved if a corresponding 'from' is found).
     */
    public String getTo(String from)
    {
        from = StringUtils.trimToEmpty(from);
        final String initialFrom = from;
        String to = null;

        // first we check to see if there's an array
        // type mapping directly defined in the mappings
        final Mapping mapping = this.getMapping(from);
        if (mapping != null)
        {
            to = mapping.getTo();
        }
        if (to == null)
        {
            to = initialFrom;
        }
        return StringUtils.trimToEmpty(to);
    }

    /**
     * Adds a mapping to the globally available mappings, these are used by this
     * class to instantiate mappings from logical names as opposed to physical
     * names.
     *
     * @param mappingsUri the Mappings URI to add to the globally available Mapping
     *        instances.
     */
    public static void addLogicalMappings(final URL mappingsUri)
    {
        final Mappings mappings = Mappings.getInstance(
                mappingsUri,
                true);
        logicalMappings.put(
            mappings.getName(),
            mappings);
    }

    /**
     * Returns true if the mapping contains the <code>from</code> value
     *
     * @param from the value of the from mapping.
     * @return true if it contains <code>from</code>, false otherwise.
     */
    public boolean containsFrom(final String from)
    {
        return this.getMapping(from) != null;
    }

    /**
     * Returns true if the mapping contains the <code>to</code> value
     *
     * @param to the value of the to mapping.
     * @return true if it contains <code>to</code>, false otherwise.
     */
    public boolean containsTo(final String to)
    {
        for (Mapping mapping : this.getMappings())
        {
            if (mapping.getTo().trim().equals(to))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the resource URI from which this Mappings object was loaded.
     *
     * @return URL of the resource.
     */
    public URL getResource()
    {
        return this.resource;
    }

    /**
     * Gets all Mapping instances for for this Mappings instance.
     *
     * @return a collection containing <strong>all </strong> Mapping instances.
     */
    public Collection<Mapping> getMappings()
    {
        return this.mappings.values();
    }

    /**
     * Gets the mapping having the given <code>from</code>.
     *
     * @param from the <code>from</code> mapping.
     * @return the Mapping instance (or null if it doesn't exist).
     */
    public Mapping getMapping(final String from)
    {
        return this.mappings.get(StringUtils.trimToEmpty(from));
    }

    /**
     * Caches the complete path.
     */
    private final Map<String, String> completePaths = new LinkedHashMap<String, String>();

    /**
     * Constructs the complete path from the given <code>relativePath</code>
     * and the resource of the parent {@link Mappings#getResource()} as the root
     * of the path.
     * @param relativePath
     * @return the complete path.
     */
    final String getCompletePath(final String relativePath)
    {
        String completePath = this.completePaths.get(relativePath);
        if (completePath == null)
        {
            final StringBuilder path = new StringBuilder();
            if (this.mappings != null)
            {
                final URL resource = this.getResource();
                if (resource != null)
                {
                    String rootPath = resource.getFile().replace(
                            '\\',
                            '/');
                    rootPath = rootPath.substring(
                            0,
                            rootPath.lastIndexOf('/') + 1);
                    path.append(rootPath);
                }
            }
            if (relativePath != null)
            {
                path.append(StringUtils.trimToEmpty(relativePath));
            }
            completePath = path.toString();
            this.completePaths.put(
                relativePath,
                completePath);
        }
        return ResourceUtils.unescapeFilePath(completePath);
    }

    /**
     * @see Object#toString()
     */
    public String toString()
    {
        return ToStringBuilder.reflectionToString(this);
    }
}