GeneratorSql.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) 2006-2008 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.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.argouml.application.api.Argo;
import org.argouml.configuration.Configuration;
import org.argouml.model.Model;
import org.argouml.ui.ExceptionDialog;
import org.argouml.ui.ProjectBrowser;
import org.argouml.ui.SelectCodeCreatorDialog;
import org.argouml.uml.generator.CodeGenerator;
import org.argouml.uml.generator.SourceUnit;
import org.argouml.uml.generator.TempFileUtils;

/**
 * SQL generator.
 */
public final class GeneratorSql implements CodeGenerator {
    /**
     * Constant representing the system dependent line separator.
     */
    static final String LINE_SEPARATOR = System.getProperty("line.separator");

    /**
     * Constant representing the attribute stereotype (as <code>String</code>)
     * that will be used to recognize a primary key attribute.
     */
    static final String PRIMARY_KEY_STEREOTYPE = "PK";

    /**
     * Constant representing the attribute stereotype (as <code>String</code>)
     * that will be used to recognize a foreign key attribute.
     */
    static final String FOREIGN_KEY_STEREOTYPE = "FK";

    /**
     * Constant representing the attribute stereotype (as <code>String</code>)
     * that will be used to recognize a foreign key attribute.
     */
    static final String AUTOINC_KEY_STEREOTYPE = "AUTOINC";

    /**
     * Constant representing the attribute stereotype (as <code>String</code>)
     * that will be used to recognize an attribute to be not nullable.
     */
    static final String NOT_NULL_STEREOTYPE = "NOT_NULL";

    /**
     * Constant representing the attribute stereotype (as <code>String</code>)
     * that will be used to recognize an attribute to be nullable.
     */
    static final String NULL_STEREOTYPE = "NULL";

    /**
     * Constant representing the attribute tagged value (as <code>String</code>)
     * that will be used to recognize what column a foreign key attribute is
     * referencing to.
     */
    static final String SOURCE_COLUMN_TAGGED_VALUE = "source_column";

    /**
     * Constant representing the attribute tagged value (as <code>String</code>)
     * that will be used to recognize what association a foreign key attribute
     * is referencing to.
     */
    static final String ASSOCIATION_NAME_TAGGED_VALUE = "association_name";

    private static final Logger LOG = Logger.getLogger(GeneratorSql.class);

    /**
     * The instances.
     */
    private static final GeneratorSql INSTANCE = new GeneratorSql();

    private DomainMapper domainMapper;

    private SqlCodeCreator sqlCodeCreator;

    private List<SqlCodeCreator> sqlCodeCreators;

    /**
     * Constructor.
     */
    private GeneratorSql() {
        domainMapper = new DomainMapper();
    }

	private List<SqlCodeCreator> loadSqlCodeCreators() {
		List<SqlCodeCreator> result = new ArrayList<SqlCodeCreator>();

        // URL url = getClass().getResource("GeneratorSql.class");
        // String extForm = url.toExternalForm();
        //
        // if (extForm.startsWith("file:")) {
        // String className = getClass().getName();
        // ... minus 7 because of length of ".class" and the trailing "/"
        // extForm = extForm.substring(0, extForm.length()
        // - className.length() - 7);
        // }

        SqlCreatorLoader el = new SqlCreatorLoader();

		// URI uri = new URI(extForm);
		// Collection classes = el.getLoadableClassesFromUri(uri,
		// SqlCodeCreator.class);
		Collection<Class<SqlCodeCreator>> classes = el.getCodeCreators();
		for (Class<SqlCodeCreator> c : classes) {
			try {
				SqlCodeCreator scc = c.newInstance();
				result.add(scc);
				// } catch (URISyntaxException e) {
				// LOG.error("Exception", e);
			} catch (InstantiationException e) {
				LOG.error("Exception while instantiating a SqlCodeCreator "
						+ c.getName(), e);
			} catch (IllegalAccessException e) {
				LOG.error("Exception while accessing the constructor of a "
						+ "SqlCodeCreator " + c.getName(), e);
			}
		}
        return result;
	}

    /**
     * @return the singleton instance.
     */
    public static GeneratorSql getInstance() {
        return INSTANCE;
    }

    /**
     * Generate code for the specified classifiers. If generation of
     * dependencies is requested, then every file the specified elements depends
     * on is generated too (e.g. if the class MyClass has an attribute of type
     * OtherClass, then files for OtherClass are generated too).
     * 
     * @param elements
     *            the UML model elements to generate code for.
     * @param deps
     *            Recursively generate dependency files too.
     * @return A collection of {@link org.argouml.uml.generator.SourceUnit}
     *         objects. The collection may be empty if no file is generated.
     * @see org.argouml.uml.generator.CodeGenerator#generate( Collection,
     *      boolean)
     */
    public Collection<SourceUnit> generate(Collection elements, boolean deps) {
        LOG.debug("generate() called");
        File tmpdir = null;
        try {
            tmpdir = TempFileUtils.createTempDir();
            if (tmpdir != null) {
                generateFiles(elements, tmpdir.getPath(), deps);
                return TempFileUtils.readAllFiles(tmpdir);
            }
            return Collections.emptyList();
        } finally {
            if (tmpdir != null) {
                TempFileUtils.deleteDir(tmpdir);
            }
            LOG.debug("generate() terminated");
        }
    }

    private TableDefinition getTableDefinition(Object element) {
        TableDefinition tableDefinition = new TableDefinition();
        tableDefinition.setName(Model.getFacade().getName(element));

        for (Object attribute : Model.getFacade().getAttributes(element)) {
            String name = Model.getFacade().getName(attribute);
            ColumnDefinition cd = new ColumnDefinition();
            cd.setName(name);

            Object domain = Model.getFacade().getType(attribute);
            String domainName = Model.getFacade().getName(domain);
            String datatype = domainMapper.getDatatype(sqlCodeCreator
                    .getClass(), domainName);
            cd.setDatatype(datatype);

            if (Utils.isNull(attribute)) {
                cd.setNullable(Boolean.TRUE);
            } else if (Utils.isNotNull(attribute)) {
                cd.setNullable(Boolean.FALSE);
            } else {
                cd.setNullable(null);
            }
            if (Utils.isAutoIncrement(attribute)) {
                cd.setAutoInc(Boolean.TRUE);
            } else {
                cd.setAutoInc(Boolean.FALSE);
            }

            tableDefinition.addColumnDefinition(cd);

            if (Utils.isPk(attribute)) {
                cd.setNullable(Boolean.FALSE);
                tableDefinition.addPrimaryKeyField(name);
            }
        }

        return tableDefinition;
    }

    private void setNullable(TableDefinition tableDef, List<String> columnNames,
            boolean nullable) {
        for (String name : columnNames) {
            tableDef.getColumnDefinition(name).setNullable(
                    Boolean.valueOf(nullable));
        }
    }

    private String generateCode(Collection elements) {
        tableDefinitions = new HashMap<Object, TableDefinition>();
        foreignKeyDefinitions = new ArrayList<ForeignKeyDefinition>();

        for (Object element : elements) {
            if (Model.getFacade().isAClass(element)) {
                TableDefinition tableDef = getTableDefinition(element);
                tableDefinitions.put(element, tableDef);
            }
        }

        if (elements.size() > 1) {
            for (Object element : elements) {
                Collection<ForeignKeyDefinition> fkDefs = 
                	getForeignKeyDefinitions(element);
                TableDefinition tableDef = tableDefinitions.get(element);
                for (ForeignKeyDefinition fkDef : fkDefs) {
                    if (fkDef.getReferencesLower() == 0) {
                        setNullable(tableDef, fkDef.getColumnNames(), true);
                    } else {
                        setNullable(tableDef, fkDef.getColumnNames(), false);
                    }
                }
                foreignKeyDefinitions.addAll(fkDefs);
            }
        }

        StringBuffer sb = new StringBuffer();
        sb.append("-- Table definitions").append(LINE_SEPARATOR);
        for (TableDefinition tableDef : tableDefinitions.values()) {
            sb.append(sqlCodeCreator.createTable(tableDef));
        }

        if (elements.size() > 1) {
            sb.append("-- Foreign key definitions").append(LINE_SEPARATOR);
            for (ForeignKeyDefinition fkDef : foreignKeyDefinitions) {
                sb.append(sqlCodeCreator.createForeignKey(fkDef));
            }
        }

        return sb.toString();
    }

    private static final String SCRIPT_FILENAME = "script.sql";

    private Map<Object, TableDefinition> tableDefinitions;

    private List<ForeignKeyDefinition> foreignKeyDefinitions;

    /**
     * Generate files for the specified classifiers.
     * 
     * @see #generate(Collection, boolean)
     * @param elements
     *            the UML model elements to generate code for.
     * @param path
     *            The source base path.
     * @param deps
     *            Recursively generate dependency files too.
     * @return The filenames (with relative path) as a collection of Strings.
     *         The collection may be empty if no file will be generated.
     * @see org.argouml.uml.generator.CodeGenerator#generateFiles( Collection,
     *      String, boolean)
     */
    public Collection<String> generateFiles(Collection elements, String path,
            boolean deps) {
        String filename = SCRIPT_FILENAME;
        if (!path.endsWith(FILE_SEPARATOR)) {
            path += FILE_SEPARATOR;
        }

        Collection<String> result = new ArrayList<String>();
        String fullFilename = path + filename;

        if (!elements.isEmpty()) {
            LOG.debug("validating model");
            ModelValidator validator = new ModelValidator();
            List<String> problems = validator.validate(elements);
            if (problems.size() > 0) {
                LOG.debug("model not valid, exiting code generation");
                String error = Utils.stringsToString(problems, LINE_SEPARATOR);

                ExceptionDialog ed = new ExceptionDialog(ProjectBrowser
                        .getInstance(), "Error in model", "Model not valid", error);
                ed.setModal(true);
                ed.setVisible(true);
            } else if (SelectCodeCreatorDialog.execute()) {
                String code = generateCode(elements);
                writeFile(fullFilename, code);
                result.add(fullFilename);
            }
        }
        return result;
    }

    private void writeFile(String filename, String content) {
        BufferedWriter fos = null;
        try {
            String inputSrcEnc = Configuration
                    .getString(Argo.KEY_INPUT_SOURCE_ENCODING);
            if (inputSrcEnc == null || inputSrcEnc.trim().equals("")) {
                inputSrcEnc = System.getProperty("file.encoding");
            }
            fos = new BufferedWriter(new OutputStreamWriter(
                    new FileOutputStream(filename), inputSrcEnc));
            fos.write(content);
        } catch (IOException e) {
            LOG.error("IO Exception: " + e);
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                LOG.error("FAILED: " + filename);
            }
        }
    }

    private Collection<ForeignKeyDefinition> getForeignKeyDefinitions(Object relation) {
        Collection<ForeignKeyDefinition> fkDefs = 
        	new HashSet<ForeignKeyDefinition>();

        for (Object assocEnd : Model.getFacade().getAssociationEnds(relation)) {

            Collection otherAssocEnds = Model.getFacade()
                    .getOtherAssociationEnds(assocEnd);
            Object otherAssocEnd = otherAssocEnds.iterator().next();

            ForeignKeyDefinition fkDef = getFkDef(relation, assocEnd,
                    otherAssocEnd);
            if (fkDef != null) {
                fkDefs.add(fkDef);
            }
        }

        return fkDefs;
    }

    private ForeignKeyDefinition getFkDef(Object relation, Object assocEnd,
            Object otherAssocEnd) {
        Object assoc = Model.getFacade().getAssociation(assocEnd);
        List fkAttributes = Utils.getFkAttributes(relation, assoc);
        int otherUpper = Model.getFacade().getUpper(otherAssocEnd);
        if (otherUpper != 1 || fkAttributes.size() == 0) {
            return null;
        }
        ForeignKeyDefinition fkDef = new ForeignKeyDefinition();

        List<Object> srcAttributes = new ArrayList<Object>();

        Object srcRelation = Model.getFacade().getClassifier(otherAssocEnd);
        for (Object fkAttr : fkAttributes) {
            Object srcAttr = Utils.getSourceAttribute(fkAttr, srcRelation);
            srcAttributes.add(srcAttr);
        }

        // List colNames = new ArrayList();
        TableDefinition tableDef = tableDefinitions
                .get(relation);
        fkDef.setTable(tableDef);
        for (Object fkAttr : fkAttributes) {
            // colNames.add(Model.getFacade().getName(fkAttr));

            ColumnDefinition colDef = tableDef.getColumnDefinition(Model
                    .getFacade().getName(fkAttr));
            fkDef.addColumnDefinition(colDef);
        }

        // List refColNames = new ArrayList();
        tableDef = tableDefinitions.get(srcRelation);
        fkDef.setReferencesTable(tableDef);
        for (Object srcAttr : srcAttributes) {
            // refColNames.add(Model.getFacade().getName(srcAttr));

            ColumnDefinition colDef = tableDef.getColumnDefinition(Model
                    .getFacade().getName(srcAttr));
            fkDef.addReferencesColumn(colDef);
        }

        int lower = Model.getFacade().getLower(assocEnd);
        int upper = Model.getFacade().getUpper(assocEnd);
        int refLower = Model.getFacade().getLower(otherAssocEnd);
        int refUpper = Model.getFacade().getUpper(otherAssocEnd);

        fkDef.setForeignKeyName(Model.getFacade().getName(assoc));

        // fkDef.setTableName(Model.getFacade().getName(relation));
        // fkDef.setColumnNames(colNames);

        // fkDef.setReferencesTableName(Model.getFacade().getName(srcRelation));
        // fkDef.setReferencesColumnNames(refColNames);

        fkDef.setLower(lower);
        fkDef.setUpper(upper);
        fkDef.setReferencesLower(refLower);
        fkDef.setReferencesUpper(refUpper);

        return fkDef;
    }

    /**
     * Returns a list of files that will be generated from the specified
     * modelelements.
     * 
     * @see #generate(Collection, boolean)
     * @param elements
     *            the UML model elements to generate code for.
     * @param deps
     *            Recursively generate dependency files too.
     * @return The filenames (with relative path) as a collection of Strings.
     *         The collection may be empty if no file will be generated.
     * @see org.argouml.uml.generator.CodeGenerator#generateFileList(
     *      Collection, boolean)
     */
    public Collection<String> generateFileList(Collection elements, 
    		boolean deps) {
		Collection<String> c = new HashSet<String>();
		c.add(SCRIPT_FILENAME);
		return c;
	}

    /**
     * @return A <code>List</code> of all code creators known to this class.
     */
    public synchronized List<SqlCodeCreator> getSqlCodeCreators() {
    	if (sqlCodeCreators == null) {
    		sqlCodeCreators = loadSqlCodeCreators();
    	}
        return sqlCodeCreators;
    }

    /**
     * @return The {@link DomainMapper} class responsible for mappings of
     *         domains to datatypes.
     */
    public DomainMapper getDomainMapper() {
        return domainMapper;
    }

    /**
     * Set a {@link SqlCodeCreator} to be the one to generate code.
     * 
     * @param sqlCodeCreator
     *            The {@link SqlCodeCreator} that should be used to generate DDL
     *            statements.
     */
    public void setSqlCodeCreator(SqlCodeCreator sqlCodeCreator) {
        this.sqlCodeCreator = sqlCodeCreator;
    }
} /* end class GeneratorSql */