ModelValidator.java

/* $Id$
 *****************************************************************************
 * Copyright (c) 2009 Contributors - see below
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    tfmorris
 *****************************************************************************
 *
 * Some portions of this file was previously release using the BSD License:
 */

// Copyright (c) 2007 The Regents of the University of California. All
// Rights Reserved. Permission to use, copy, modify, and distribute this
// software and its documentation without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph appear in all copies. This software program and
// documentation are copyrighted by The Regents of the University of
// California. The software program and documentation are supplied "AS
// IS", without any accompanying services from The Regents. The Regents
// does not warrant that the operation of the program will be
// uninterrupted or error-free. The end-user understands that the program
// was developed for research purposes and is advised not to rely
// exclusively on the program for any reason. IN NO EVENT SHALL THE
// UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
// SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT,
// UPDATES, ENHANCEMENTS, OR MODIFICATIONS.

package org.argouml.language.sql;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.argouml.model.Model;

// TODO Use Translator for error messages
/**
 * Class that validates an UML model to be a valid relational model.
 */
class ModelValidator {
    private Map<String, Object> associationForName = 
    	new HashMap<String, Object>();

    private Map<Object, Object> fkAttrForAssoc = new HashMap<Object, Object>();

    private List<String> problems;

    /**
     * Default constructor.
     */
    public ModelValidator() {
    }

    /**
     * Validate the specified elements.
     * 
     * @param elements
     *            The elements to validate.
     * @return A list of problems found by validation. If there are no problems
     *         the returned list is empty.
     */
    public List<String> validate(Collection elements) {
        problems = new ArrayList<String>();

        for (Iterator it = elements.iterator(); it.hasNext();) {
            Object relation = it.next();
            if (Model.getFacade().isAClass(relation)
                    && !Model.getFacade().isAAssociationClass(relation)) {
                validateRelation(relation);
            }
        }

        Set<Entry<String, Object>> entries = associationForName.entrySet();
        for (Entry<String, Object> entry : entries) {
            String assocName = (String) entry.getKey();
            Object association = entry.getValue();
            Object fkAttribute = fkAttrForAssoc.get(association);
            if (fkAttribute == null) {
                problems.add("Foreign key attribute missing for association "
                        + assocName);
            }
        }

        return problems;
    }

    private void validateRelation(Object relation) {
        validatePrimaryKey(relation);
        validateFkAttributes(relation);
        validateAssociations(relation);
    }

    private void validateFkAttributes(Object relation) {
        Collection attributes = Model.getFacade().getAttributes(relation);
        for (Iterator it = attributes.iterator(); it.hasNext();) {
            Object attribute = it.next();
            if (Utils.isFk(attribute)) {
                validateFkAttribute(relation, attribute);
            }
        }
    }

    /**
     * Checks if every relation has a primary key. (rule 1)
     * 
     * @param relation
     *            The relation to validate.
     */
    private void validatePrimaryKey(Object relation) {
        List attributes = Model.getFacade().getAttributes(relation);
        Iterator it = attributes.iterator();
        boolean valid = false;
        while (it.hasNext()) {
            Object attribute = it.next();
            if (Utils.isPk(attribute)) {
                valid = true;
                break;
            }
        }

        if (!valid) {
            problems.add("Primary key missing for "
                    + Model.getFacade().getName(relation));
        }
    }

    /**
     * Checks if a foreign key attribute is referencing an association. Further
     * checks if this foreign key attribute is referencing an attribute in
     * another relation. Checks rules 2 to 6.
     * 
     * @param relation
     * @param attribute
     */
    private void validateFkAttribute(Object relation, Object attribute) {
        String relName = Model.getFacade().getName(relation);
        String attrName = Model.getFacade().getName(attribute);
        String assocName = Model.getFacade().getTaggedValueValue(attribute,
                GeneratorSql.ASSOCIATION_NAME_TAGGED_VALUE);

        Object association = Utils.getAssociationForName(relation, assocName);
        if (association == null) {
            problems.add("association named '" + assocName + "' for relation "
                    + Model.getFacade().getName(relation) + " not found");
        } else {
            fkAttrForAssoc.put(association, attribute);

            Object relationAssocEnd = Model.getFacade().getAssociationEnd(
                    relation, association);
            Collection otherAssocEnds = Model.getFacade()
                    .getOtherAssociationEnds(relationAssocEnd);

            if (otherAssocEnds.size() == 1) {
                Object otherAssocEnd = otherAssocEnds.iterator().next();
                Object otherRelation = Model.getFacade().getClassifier(
                        otherAssocEnd);

                Object srcAttr = Utils.getSourceAttribute(attribute,
                        otherRelation);
                if (srcAttr == null) {
                    problems.add("fk attribute " + relName + "." + attrName
                            + " does not reference " + " an attribute in "
                            + Model.getFacade().getName(otherRelation));
                }

                int otherUpper = Model.getFacade().getUpper(otherAssocEnd);
                if (otherUpper != 1) {
                    problems.add("foreign key attribute " + relName + "."
                            + attrName
                            + " cannot be used to reference multiple "
                            + Model.getFacade().getName(otherRelation));
                }

                int otherLower = Model.getFacade().getLower(otherAssocEnd);
                validateFkConsistence(relation, attribute, otherLower);
            }
        }
    }

    /**
     * <p>
     * Checks if the <code>foreignKey</code> is of a stereotype NULL/NOT NULL
     * and if it conflicts with the multiplicity of the association end. A
     * conflict results from one of these constellations:
     * <ol>
     * <li>attribute is of stereotype NOT NULL, the corresponding association
     * end multiplicity is 0..1
     * <li>attribute is of stereotype NULL, the corresponding association end
     * multiplicity is 1
     * </ol>
     * <p>
     * If attribute is none of these two stereotypes there is no conflict.
     * <p>
     * Checks rules 5 and 6.
     * 
     * @param fkAttribute
     *            The foreign key attribute to check
     * @param relation
     *            The relatoin the foreign key should refer to
     * @param lowerBound
     *            The lower multiplicity of the corresponding association end
     */
    private void validateFkConsistence(Object relation, Object fkAttribute,
            int lowerBound) {
        String entName = Model.getFacade().getName(relation);
        String attrName = Model.getFacade().getName(fkAttribute);

        if (Utils.isNull(fkAttribute) && lowerBound == 1) {
            problems.add("conflict in " + entName + "." + attrName + ": "
                    + "attribute is nullable and association lower bound "
                    + "is one");
        } else if (Utils.isNotNull(fkAttribute) && lowerBound == 0) {
            problems.add("conflict in " + entName + "." + attrName + ": "
                    + "attribute is not nullable and association lower "
                    + "bound is zero");
        }
    }

    /**
     * Validate every association for the given relation.
     * 
     * @param relation
     */
    private void validateAssociations(Object relation) {
        Collection associationEnds = Model.getFacade().getAssociationEnds(
                relation);
        Iterator it = associationEnds.iterator();
        while (it.hasNext()) {
            Object relationAssocEnd = it.next();
            Object association = Model.getFacade().getAssociation(
                    relationAssocEnd);
            validateAssociation(association);
        }
    }

    private Set validatedAssociations = new HashSet();

    /**
     * Validate the specified association. The association needs to have a
     * unique name, must be binary and at most 1:n. And there must exist a
     * foreign key attribute for an association.
     * 
     * @param association
     */
    private void validateAssociation(Object association) {
        if (validatedAssociations.contains(association)) {
            return;
        }

        validatedAssociations.add(association);

        String assocName = Model.getFacade().getName(association);
        if (associationForName.containsKey(assocName)) {
            problems.add("Association name " + assocName
                    + " found more than once");
        } else {
            associationForName.put(assocName, association);

            Collection assocEnds = Model.getFacade()
                    .getConnections(association);
            if (assocEnds.size() != 2) {
                problems.add("Association " + assocName + " is not binary");
            } else {
                Iterator it = assocEnds.iterator();

                Object assocEnd1 = it.next();
                Object assocEnd2 = it.next();

                int end1Upper = Model.getFacade().getUpper(assocEnd1);
                int end2Upper = Model.getFacade().getUpper(assocEnd2);

                if (end1Upper != 1 && end2Upper != 1) {
                    problems.add("Association " + assocName + " is n:m (not "
                            + "allowed in a relational data model)");
                }
            }
        }
    }
}