public interface ArchitectSession extends UserPrompterFactory, SQLDatabaseMapping, WorkspaceContainer, RunnableDispatcher {

    public static final String PREFS_PL_INI_PATH = "PL.INI.PATH";
    
    public ProfileManager getProfileManager();
    
    /**
     * Returns the context that created this session.
     */
    public ArchitectSessionContext getContext();
    
    /**
     * Returns the database in use for this session. In a 
     * gui session, this would be the playpen database. 
     */
    public SQLDatabase getTargetDatabase();
    
    /**
     * Sets the value of name
     *
     * @param argName Value to assign to this.name
     */
    public void setName(String argName);
    
    /**
     * Gets the value of name
     *
     * @return the value of name
     */
    public String getName();
    
    /**
     * Returns the project associated with this session.  The project
     * holds the playpen objects, and can save and load itself in an
     * XML format.
     */
    public ProjectLoader getProjectLoader();
    
    /**
     *  This method is only used to create the correct type of project
     *  for an ArchitectSwingSessionImpl and should not be called anywhere
     *  else.
     */
    public void setProjectLoader(ProjectLoader project);
    
    
    /**
     *  Replaces the entire list of source databases for this session.
     *  This method is used reflectively by the code that does loading and saving,
     *  so DON'T DELETE THIS METHOD even if it looks like it's unused.
     * 
     * @param databases
     * @throws SQLObjectException
     */
    public void setSourceDatabaseList(List<SQLDatabase> databases) throws SQLObjectException;
    
    /**
     * The DDL Generator currently in use for this session.
     */
    public DDLGenerator getDDLGenerator();
    
    /**
     * Sets the new DDL Generator currently in use for this session.
     */
    public void setDDLGenerator(DDLGenerator generator);    

    /**
     * Returns the workspace object which is the root object of the ArchitectSession, the ArchitectProject.  
     */
    public ArchitectProject getWorkspace();    
    
    /**
     * Returns the root SQL object of the session, which is the tree that contains Columns, Databases, etc.
     */
    public SQLObjectRoot getRootObject();
    
    public DataSourceCollection<JDBCDataSource> getDataSources();
    
    public boolean isEnterpriseSession();

    public void addSessionLifecycleListener(SessionLifecycleListener<ArchitectSession> l);

    public void removeSessionLifecycleListener(SessionLifecycleListener<ArchitectSession> l);        
    
    /**
     * Ends this session, disposing its frame and releasing any system
     * resources that were obtained explicitly by this session. Also
     * fires a sessionClosing lifecycle event, so any resources used up
     * by subsystems dependent on this session can be freed by the appropriate
     * parties.
     * 
     * @return True if the session was successfully closed. False if the
     * session did not close due to an error or user intervention.
     */
    public boolean close();
    
    /**
     * Returns a list of {@link UserDefinedSQLType} defined in this session.
     */
    public List<UserDefinedSQLType> getSQLTypes();

    /**
     * Returns a list of {@link UserDefinedSQLType}s defined in this session. Only
     * enterprise sessions should contain domains.
     * @return List of UserDefinedSQLType
     */
    public List<UserDefinedSQLType> getDomains();
    
    /**
     * Searches the session for a {@link UserDefinedSQLType} with the given
     * UUID.
     * 
     * @param uuid
     *            The UUID of the desired {@link UserDefinedSQLType}.
     * @return Returns a {@link UserDefinedSQLType} with the given UUID if it
     *         exists in this session. If not, then it will return null.
     */
    public UserDefinedSQLType findSQLTypeByUUID(String uuid);
    
    /**
     * Searches the session for a {@link UserDefinedSQLType} with the given int
     * that represents a value from {@link java.sql.Types}.
     * 
     * @param type
     *            The {@link Types} value of the {@link UserDefinedSQLType}
     *            desired.
     * @return Returns a {@link UserDefinedSQLType} with the given type if one
     *         exists in this session. If not, then it will return null.
     */
    public UserDefinedSQLType findSQLTypeByJDBCType(int type);

	/**
	 * Return the LiquibaseSettings to be used for forward engineering a model.
	 * <br>
	 * The settings for the model compared are stored in CompareDMSettings
	 * @return liquibase settings for forward engineering
	 * @see ca.sqlpower.architect.swingui.CompareDMSettings#getLiquibaseSettings()
	 */
	public LiquibaseSettings getLiquibaseSettings();

	/**
	 * Store the LiquibaseSettings for forward engineering a model.
	 * Settings for model compare are stored in CompareDMSettings.
	 * @param settings Liquibase settings
	 */
	public void setLiquibaseSettings(LiquibaseSettings settings);

    /**
     * Returns the status information object to update the user on different
     * kinds of progress. May return null if no status information exists (for
     * headless mode).
     * 
     * @return architectStatusInformation
     */
	public ArchitectStatusInformation getStatusInformation();
	
	/**
     * Returns a runnable that will update the snapshot object in the given
     * snapshot to be the same as the type the snapshot maps to in the system
     * workspace.
     * 
     * @param snapshot
     *            The snapshot to update. This currently only works for
     *            snapshots of UserDefinedSQLTypes but will be extended to
     *            others in the future.
     */
    public Runnable createUpdateSnapshotRunnable(final SPObjectSnapshot<?> snapshot);

} public class CoreUserSettings {
    private static final Logger logger = Logger.getLogger(CoreUserSettings.class);

    /**
     * The prefs node we read and write all the settings in.
     */
    private final Preferences prefs;
    
	/**
	 * For now, this just holds the preferred printer.  
	 */
    private PrintUserSettings printUserSettings;

	/**
	 * GUI-related settings.  This technically shouldn't be here
	 * (model is referencing view stuff) but it didn't seem right to
	 * make the settings file primarily swing-specific with a
	 * reference to the general architect prefs.
	 */
    private UserSettings swingSettings;

	/**
	 * ETL-related settings.  This is not a design problem like
	 * swingSettings is, since ETL is part of the app's core
	 * functionality.
	 */
    private ETLUserSettings etlUserSettings;

	/**
	 * DDL-related settings.  
	 */
    private DDLUserSettings ddlUserSettings;

    private QFAUserSettings qfaUserSettings;
    
    private UpdateCheckSettings updateCheckSettings;

    private Locale defaultLocale = Locale.getDefault();
	
    public CoreUserSettings(Preferences prefs) {
		super();
		printUserSettings = new PrintUserSettings();
		swingSettings = new ArchitectSwingUserSettings();
		etlUserSettings = new ETLUserSettings();
		ddlUserSettings = new DDLUserSettings();
        qfaUserSettings = new QFAUserSettings();
        setUpdateCheckSettings(new UpdateCheckSettings());
        this.prefs = prefs;
        loadFromPrefs();
	}
	
    /**
     * Sets up the various prefs objects based on values stored in the java.util.prefs
     * preferences node.
     */
    private final void loadFromPrefs() {
        logger.debug("loading UserSettings from java.util.prefs.");
        logger.debug("Preferences class = " + prefs.getClass());

        swingSettings.setBoolean(ArchitectSwingUserSettings.PLAYPEN_RENDER_ANTIALIASED,
                prefs.getBoolean(ArchitectSwingUserSettings.PLAYPEN_RENDER_ANTIALIASED, false));
        if (getDefaultLocale() != null) {
            String localeVal = prefs.get(ArchitectSwingUserSettings.DEFAULT_LOCALE, getDefaultLocale().getDisplayLanguage());
            swingSettings.setString(ArchitectSwingUserSettings.DEFAULT_LOCALE,localeVal);
            setDefaultLocale(new Locale(localeVal));
        }  else {
            swingSettings.setString(ArchitectSwingUserSettings.DEFAULT_LOCALE,Locale.getDefault().getDisplayName());
        }
        etlUserSettings.setString(ETLUserSettings.PROP_PL_ENGINE_PATH,
            prefs.get(ETLUserSettings.PROP_PL_ENGINE_PATH, ""));
        etlUserSettings.setString(ETLUserSettings.PROP_ETL_LOG_PATH,
            prefs.get(ETLUserSettings.PROP_ETL_LOG_PATH, defaultHomeFile("etl.log")));

        ddlUserSettings.setString(DDLUserSettings.PROP_DDL_LOG_PATH,prefs.get(DDLUserSettings.PROP_DDL_LOG_PATH, defaultHomeFile("ddl.log")));

        qfaUserSettings.setBoolean(QFAUserSettings.EXCEPTION_REPORTING,prefs.getBoolean(QFAUserSettings.EXCEPTION_REPORTING,true));
        
        getUpdateCheckSettings().setBoolean(UpdateCheckSettings.AUTO_UPDATE_CHECK, prefs.getBoolean(UpdateCheckSettings.AUTO_UPDATE_CHECK, true));

        printUserSettings.setDefaultPrinterName(
                prefs.get(PrintUserSettings.DEFAULT_PRINTER_NAME, ""));
    }
    
    /**
     * Saves all the preferences that this class knows about back into the
     * prefs node.
     */
    public void write() throws SQLObjectException {
        logger.debug("Saving user settings to java.util.prefs");

        prefs.putBoolean(ArchitectSwingUserSettings.PLAYPEN_RENDER_ANTIALIASED,
                swingSettings.getBoolean(ArchitectSwingUserSettings.PLAYPEN_RENDER_ANTIALIASED, false));

        prefs.put(ETLUserSettings.PROP_PL_ENGINE_PATH, etlUserSettings.getString(ETLUserSettings.PROP_PL_ENGINE_PATH,""));
        prefs.put(ETLUserSettings.PROP_ETL_LOG_PATH, etlUserSettings.getString(ETLUserSettings.PROP_ETL_LOG_PATH,""));

        prefs.put(DDLUserSettings.PROP_DDL_LOG_PATH, ddlUserSettings.getString(DDLUserSettings.PROP_DDL_LOG_PATH,""));

        prefs.putBoolean(QFAUserSettings.EXCEPTION_REPORTING,qfaUserSettings.getBoolean(QFAUserSettings.EXCEPTION_REPORTING,true));
        
        prefs.putBoolean(UpdateCheckSettings.AUTO_UPDATE_CHECK, getUpdateCheckSettings().getBoolean(UpdateCheckSettings.AUTO_UPDATE_CHECK, true));

        prefs.put(PrintUserSettings.DEFAULT_PRINTER_NAME, printUserSettings.getDefaultPrinterName());
        
        prefs.put(ArchitectSwingUserSettings.DEFAULT_LOCALE, swingSettings.getString(ArchitectSwingUserSettings.DEFAULT_LOCALE, getDefaultLocale().toString()));

        try {
            prefs.flush();
        } catch (BackingStoreException e) {
            throw new SQLObjectException("Unable to flush Java preferences", e);
        }
    }
    
    /**
     * Creates a full pathname for a file of the given name inside the user's
     * home directory.
     *  
     * @param name The file name.
     */
    private String defaultHomeFile(String name) {
        return System.getProperty("user.home") + System.getProperty("file.separator") + name;
    }
    
	public void setPrintUserSettings (PrintUserSettings printUserSettings) {
		this.printUserSettings = printUserSettings;
	}

	public PrintUserSettings getPrintUserSettings () {
		return this.printUserSettings;
	}

	public UserSettings getSwingSettings() {
		return swingSettings;
	}

	public void setSwingSettings(UserSettings sprefs) {
		swingSettings = sprefs;
	}

	public ETLUserSettings getETLUserSettings() {
		return etlUserSettings;
	}

	public void setETLUserSettings(ETLUserSettings v) {
		etlUserSettings = v;
	}

	public QFAUserSettings getQfaUserSettings() {
        return qfaUserSettings;
    }

    public void setQfaUserSettings(QFAUserSettings qfaUserSettings) {
        this.qfaUserSettings = qfaUserSettings;
    }

    public DDLUserSettings getDDLUserSettings() {
		return ddlUserSettings;
	}

	public void setDDLUserSettings(DDLUserSettings v) {
		ddlUserSettings = v;
	}

    public void setUpdateCheckSettings(UpdateCheckSettings updateCheckSettings) {
        this.updateCheckSettings = updateCheckSettings;
    }

    public UpdateCheckSettings getUpdateCheckSettings() {
        return updateCheckSettings;
    }

    /**
     * 
     * @return current default Locale
     */
    public Locale getDefaultLocale() {
        return defaultLocale;
    }


    /**
     * Method used to set default Language
     * @param defaultLocale the defaultLocale to set
     */
    public void setDefaultLocale(Locale defaultLocale) {
        this.defaultLocale = defaultLocale;
        Locale.setDefault(defaultLocale);
    }

  
  
} public class ProjectSettings extends AbstractSPObject {
    
    /**
     * Defines an absolute ordering of the child types of this class.
     * 
     * IMPORTANT!: When changing this, ensure you maintain the order specified by {@link #getChildren()}
     */
    public static final List<Class<? extends SPObject>> allowedChildTypes = Collections.emptyList();
    
    private boolean savingEntireSource = false;
    
    private boolean displayRelationshipLabel = true;

    private boolean relationshipLinesDirect = false;
    
    private boolean usingLogicalNames = true;

    private boolean showPkTag = true;
    private boolean showFkTag = true;
    private boolean showAkTag = true;
    
    private ColumnVisibility columnVisibility = ColumnVisibility.ALL;
    
    public static enum ColumnVisibility {
        ALL, 
        PK, 
        PK_FK, 
        PK_FK_UNIQUE, 
        PK_FK_UNIQUE_INDEXED;
    }
    
    @Constructor
    public ProjectSettings() {
        super();
        setName("Project Settings");
    }

    @Accessor(isInteresting=true)
    public boolean isSavingEntireSource() {
        return savingEntireSource;        
    }

    @Mutator
    public void setSavingEntireSource(boolean savingEntireSource) {
        boolean oldValue = this.savingEntireSource;
        this.savingEntireSource = savingEntireSource;
        firePropertyChange("savingEntireSource", oldValue, savingEntireSource);
    }

    @Accessor(isInteresting=true)
    public boolean isDisplayRelationshipLabel() {
        return displayRelationshipLabel;
    }

    @Mutator
    public void setDisplayRelationshipLabel(boolean displayRelationshipLabel) {
        boolean oldValue = this.displayRelationshipLabel;
        this.displayRelationshipLabel = displayRelationshipLabel;
        firePropertyChange("displayRelationshipLabel", oldValue, displayRelationshipLabel);
    }

    @Accessor(isInteresting=true)
    public boolean isRelationshipLinesDirect() {
        return relationshipLinesDirect;
    }

    @Mutator
    public void setRelationshipLinesDirect(boolean relationshipLinesDirect) {
        boolean oldValue = this.relationshipLinesDirect;
        this.relationshipLinesDirect = relationshipLinesDirect;
        firePropertyChange("relationshipLinesDirect", oldValue, relationshipLinesDirect);
    }

    @Accessor(isInteresting=true)
    public boolean isUsingLogicalNames() {
        return usingLogicalNames;
    }

    @Mutator
    public void setUsingLogicalNames(boolean usingLogicalNames) {
        boolean oldValue = this.usingLogicalNames;
        this.usingLogicalNames = usingLogicalNames;
        firePropertyChange("usingLogicalNames", oldValue, usingLogicalNames);
    }

    @Accessor(isInteresting=true)
    public boolean isShowPkTag() {
        return showPkTag;
    }

    @Mutator
    public void setShowPkTag(boolean showPkTag) {
        boolean oldValue = this.showPkTag;
        this.showPkTag = showPkTag;
        firePropertyChange("showPkTag", oldValue, showPkTag);
    }

    @Accessor(isInteresting=true)
    public boolean isShowFkTag() {
        return showFkTag;
    }
    
    @Mutator
    public void setShowFkTag(boolean showFkTag) {
        boolean oldValue = this.showFkTag;
        this.showFkTag = showFkTag;
        firePropertyChange("showFkTag", oldValue, showFkTag);
    }

    @Accessor(isInteresting=true)
    public boolean isShowAkTag() {
        return showAkTag;
    }

    @Mutator
    public void setShowAkTag(boolean showAkTag) {
        boolean oldValue = this.showAkTag;
        this.showAkTag = showAkTag;
        firePropertyChange("showAkTag", oldValue, showAkTag);
    }

    @Accessor(isInteresting=true)
    public ColumnVisibility getColumnVisibility() {
        return columnVisibility;
    }

    @Mutator
    public void setColumnVisibility(ColumnVisibility columnVisibility) {
        ColumnVisibility oldValue = this.columnVisibility;
        this.columnVisibility = columnVisibility;
        firePropertyChange("columnVisibility", oldValue, columnVisibility);
    }

    @Override
    protected boolean removeChildImpl(SPObject child) {
        return false;
    }

    public List<Class<? extends SPObject>> getAllowedChildTypes() {
        return allowedChildTypes;
    }

    public List<? extends SPObject> getChildren() {
        return Collections.emptyList();
    }

    public List<? extends SPObject> getDependencies() {
        return Collections.emptyList();
    }

    public void removeDependency(SPObject dependency) {
        throw new IllegalArgumentException("This object does not have dependencies");
    }
    
} public static enum ColumnVisibility {
    ALL, 
    PK, 
    PK_FK, 
    PK_FK_UNIQUE, 
    PK_FK_UNIQUE_INDEXED;
} public interface DDLGenerator {

    /**
     * Returns the name of this DDL Generator, which should be a
     * human-readable string with the vendor and/or product name
     * (and version if the generator doesn't work with all versions)
     * of the database platform this generator targets.
     */
    public String getName();
    
    /**
     * Creates a list of DDLStatement objects which create all of the tables,
     * their columns and primary keys, and the foreign key relationships of the
     * given database.
     *
     * @param tables Collection of tables to generate a DDL representation of.
     * @return The list of DDL statements that can create the given database, in the
     * order they should be executed.
     * @throws SQLException If there is a problem getting type info from the target DB.
     * @throws SQLObjectException If there are problems with the Architect objects.
     */
    public List<DDLStatement> generateDDLStatements(Collection<SQLTable> tables)
    throws SQLException, SQLObjectException;

    /**
     * Generates the series of DDL Statements as in {@link #generateDDLStatements(Collection)},
     * then compiles them into a formatted script complete with statement terminators
     * and transaction start and end statements (if supported by the target platform).
     * This script is appropriate to feed into a target database using a vendor-supplied
     * tool for executing SQL scripts.
     * @param architectSwingSession 
     * 
     * @param tables The collection of tables the generated script should create.
     * @return The String representation of the generated DDL script.
     * @throws SQLException If there is a problem getting type info from the target DB.
     * @throws SQLObjectException If there are problems with the Architect objects.
     */
    public String generateDDLScript(ArchitectSwingSession architectSwingSession, Collection<SQLTable> tables) throws SQLException, SQLObjectException;


    /**
     * Adds a comment (remark) to the passed object (table, column, view, ...)
     *
     * @param o the object to add the comment for.
     */
    public void addComment(SQLObject o);

    /**
     * Updates the comment (remark) of the passed object (table, column, view, ...)
     *
     * @param o the object to add the comment for.
     */
    public void modifyComment(SQLObject o);

    /**
     * Appends the DDL statement to rename "oldCol" to "newCol.
     *
     * If the DBMS does not support renaming columns the generator
     * should create the approriate DROP/ADD sequence
     *
	 * @param oldColumn the old definition of the column
     * @param newColumn the new definition of the column
     */
    public void renameColumn(SQLColumn oldColumn, SQLColumn newColumn);

    /**
     * Appends the DDL statement for dropping the given column from its parent
     * table in this DDL Generator's target schema/catalog.
     *
     * @param c The column to create a DROP statement for.
     */
    public void dropColumn(SQLColumn c);

    /**
     * Appends the DDL statement for adding the given column to its parent
     * table in this DDL Generator's target schema/catalog.
     *
     * @param c The column to create a ADD statement for.
     */
    public void addColumn(SQLColumn c);

    /**
     * Appends the DDL statement for modifying the given column's datatype and
     * nullability in its parent table in this DDL Generator's target
     * schema/catalog.
     * 
     * @param c
     *            The column to create a MODIFY or ALTER COLUMN statement for.
     * @param diffChunk
     *            A collection of changes that need to be done to the column.
     *            Helpful to decide what to include in the modify column script.
     */
    public void modifyColumn(SQLColumn c, DiffChunk<SQLObject> diffChunk);

    /**
     * Appends the DDL statement for creating the given FK relationship
     * in this DDL Generator's target schema/catalog.
     *
     * @param r The relationship to create a FOREIGN KEY statement for.
     */
    public void addRelationship(SQLRelationship r);

    /**
     * Appends the DDL statement for renaming the given FK relationship
     * in this DDL Generator's target schema/catalog.
     *
     * @param oldFK The old relationship name
     * @param newFK The new relationship name
     */
    public void renameRelationship(SQLRelationship oldFK, SQLRelationship newFK);

    /**
     * Appends the DDL statement for dropping the given FK relationship
     * in this DDL Generator's target schema/catalog.
     *
     * @param r The relationship to create a DROP FOREIGN KEY statement for.
     */
    public void dropRelationship(SQLRelationship r);

    /**
     * Appends the DDL statement for renaming the table.
     * If the DBMS does not support renaming tables, the generator should
     * create the approriate DROP/CREATE sequence.
     *
     * @param oldTable the old definition of the table
	 * @param newTable the new definition of the table
     */
    public void renameTable(SQLTable oldTable, SQLTable newTable);
    
    /**
     * Appends the DDL statements for dropping the table in this DDL Generator's
     * current catalog and schema using SQLTable t's physical name.
     */
    public void dropTable(SQLTable t);

    /**
     * Appends the DDL statements for creating a table in this DDL Generator's
     * current catalog and schema using SQLTable t as a template.
     */
    public void addTable(SQLTable t) throws SQLException, SQLObjectException;

    /**
     * Appends the DDL statements for creating the given index this DDL Generator's
     * current catalog and schema.
     */
    public void addIndex(SQLIndex idx) throws SQLObjectException;

	/**
	 * Drops the specified index. Currently the CompareSQL does not detect index drops
	 * but I have added this for completeness (as it is needed e.g. for renaming an
	 * index with Liquibase)
	 *
	 * @param index
	 * @throws SQLObjectException
	 */
	public void dropIndex(SQLIndex index) throws SQLObjectException;

    /**
     * Appends the DDL statements for renaming the given index
	 * 
	 * @param oldIndex the old index definition
	 * @param newIndex the new index definition
     */
    public void renameIndex(SQLIndex oldIndex, SQLIndex newIndex) throws SQLObjectException;

    /**
     * Returns the list of DDL statements that have been created so far.  Call
     * {@link #generateDDLStatements(Collection)} to populate this list.
     */
    public List<DDLStatement> getDdlStatements();

    /**
     * Converts an arbitrary string (which may contain spaces, mixed case,
     * punctuation, and so on) into a valid identifier in the target
     * database system.  The implementation should mangle the input string
     * to the minimum degree necessary to make the given string into a valid
     * identifier on the target system.
     */
    public String toIdentifier(String name);

    /**
     * Creates and returns a DDL statement which will drop the given table in this
     * DDL Generator's current catalog and schema.
     *
     * @param table The name of the table to be dropped.
     * @return A SQL statement which will drop the table.
     */
    public String makeDropTableSQL(String table);

    /**
     * Creates and returns a DDL statement which will drop a foreign key relationship in this
     * DDL Generator's current catalog and schema.
     *
     * @param fkTable The name of the FK table whose relationship should be dropped.
     * @param fkName The name of the key to drop.
     * @return a SQL statement which will drop the key.
     */
    public String makeDropForeignKeySQL(String fkTable, String fkName);


    // ---------------------- accessors and mutators ----------------------

    /**
     * Tells the generator whether or not it can connect to the target
     * database and ask for additional information during the generation
     * process. For instance, to populate the type map.
     */
    public boolean getAllowConnection();

    /**
     * See {@link #getAllowConnection()}.
     */
    public void setAllowConnection(boolean argAllowConnection);

    /**
     * Gets the value of typeMap
     *
     * @return the value of typeMap
     */
    public Map<Integer, GenericTypeDescriptor> getTypeMap();

    /**
     * Sets the value of typeMap
     *
     * @param argTypeMap Value to assign to this.typeMap
     */
    public void setTypeMap(Map<Integer, GenericTypeDescriptor> argTypeMap);

    /**
     * See {@link ca.sqlpower.architect.ddl.GenericDDLGenerator#targetCatalog }
     * @return the value of targetCatalog
     */
    public String getTargetCatalog();

    /**
     * See {@link ca.sqlpower.architect.ddl.GenericDDLGenerator#targetCatalog }
     *
     * @param argTargetCatalog Value to assign to this.targetCatalog
     */
    public void setTargetCatalog(String argTargetCatalog);

    /**
     * see {@link ca.sqlpower.architect.ddl.GenericDDLGenerator#targetSchema }
     * @return the value of targetSchema
     */
    public String getTargetSchema();

    /**
     * See {@link ca.sqlpower.architect.ddl.GenericDDLGenerator#targetSchema}
     *
     * @param argTargetSchema Value to assign to this.targetSchema
     */
    public void setTargetSchema(String argTargetSchema);

    /**
     * The name that the target database gives to the JDBC idea of
     * "catalog."  For Oracle, this would be null (no catalogs) and
     * for SQL Server it would be "Database".
     */
    public String getCatalogTerm();

    /**
     * The name that the target database gives to the JDBC idea of
     * "schema."  For Oracle, this would be "Schema" and for SQL
     * Server it would be "Owner".
     */
    public String getSchemaTerm();

    public void dropPrimaryKey(SQLTable t) throws SQLObjectException;

    public void addPrimaryKey(SQLTable t) throws SQLObjectException;

    /**
     * Returns the string that should be used at the end of each statement.
     * For many platforms, this is a semicolon.
     */
    public String getStatementTerminator();
    /**
     * Check to see if the word word is on the list of reserved words for this database
     * @return
     */
    public boolean isReservedWord(String word);

    /**
     * get the datatype with scale and precision of the column, example: "decimal(10,5)"
     * @param col
     * @return
     */
    public String columnType(SQLColumn col);
    
    /**
     * Returns true if this DDL generator supports the rollback operation to
     * return the database to some previous state.
     */
    public boolean supportsRollback();

    /**
     * Returns true if this {@link DDLGenerator} supports having check
     * constraints on columns to restrict the value to satisfy specified
     * expression.
     */
    public boolean supportsCheckConstraint();

    /**
     * Returns true if this {@link DDLGenerator} supports having enumeration on
     * columns to restrict the value to satisfy one of the specified list of
     * enum values.
     */
    public boolean supportsEnumeration();
    
    
    /** 
     * @param object; is SQLObject
     * @return the physical name used for object in a physical database system.
     * return physical name with quotes only if databaseType supports quoting a name.
	 * 
     */
    public String getPhysicalName(SQLObject object);
    
    /**
     * 
     * set to 'true' if comparing Data Model for Postgres database. 
     * @param isComparingDMForPostgres
     */
    public void setComparingDMForPostgres(boolean isComparingDMForPostgres);
 
} public class ArchitectClientSideSession extends ArchitectSessionImpl implements RevisionController {	
	
	private static Logger logger = Logger.getLogger(ArchitectClientSideSession.class);
	
	private static CookieStore cookieStore = new BasicCookieStore();
	
	public static final String MONDRIAN_SCHEMA_REL_PATH = "/mondrian";
	
	/**
	 * The prefs node that will store information about the current settings of
	 * the DDL generator and compare DM panels. Currently this is stored in prefs
	 * because we want to store it per user for each project they are using. In the
	 * future we may want to store this in the server, once per user per project.
	 */
	private final Preferences prefs = Preferences.userNodeForPackage(ArchitectClientSideSession.class);
	
	/**
	 * Describes the location of the project that this session represents.
	 */
	private final ProjectLocation projectLocation;

    /**
     * An {@link HttpClient} used to send updates to the server for changes to
     * the project and to receive updates from other users from the server.
     */
	private final HttpClient outboundHttpClient;
	
	/**
	 * The persister that will update the project in this session with changes from
	 * the server.
	 */
	private final ArchitectSessionPersister sessionPersister;
	
	/**
	 * Used to convert JSON sent from the server into persist calls to forward the
	 * server changes to the {@link #sessionPersister}.
	 */
	private final SPJSONPersister jsonPersister;
	private final ArchitectNetworkConflictResolver updater;
	private final SPJSONMessageDecoder jsonMessageDecoder;
	private final DataSourceCollectionUpdater dataSourceCollectionUpdater;
	
	private DataSourceCollection <JDBCDataSource> dataSourceCollection;

	/**
	 * Used to store sessions which hold nothing but security info.
	 */
	public static Map<String, ArchitectClientSideSession> securitySessions;
	
    private AbstractPoolingSPListener deletionListener;

    /**
     * The executor to use as the foreground thread manager. If this is not null
     * and there is no EDT available this executor will be used to ensure the
     * system is single threaded.
     */
    private final ThreadPoolExecutor foregroundThreadExecutor;

    /**
     * This is false except for testing purposes when the single thread executor
     * may want to be used if the test is running headless.
     */
    private final boolean useThreadPool;
    
    /**
     * The threads used by {@link #foregroundThreadExecutor} to keep Architect
     * single threaded if we are using the {@link #foregroundThreadExecutor}.
     * The executor will only allow one of these threads to be used at a time.
     */
    private final Set<Thread> foregroundExecutorThread = Collections.synchronizedSet(new HashSet<Thread>());
    
    static {
        securitySessions = new HashMap<String, ArchitectClientSideSession>();
    }
	
	public ArchitectClientSideSession(ArchitectSessionContext context, 
			String name, ProjectLocation projectLocation) throws SQLObjectException {
	    this(context, name, projectLocation, false);
	}

    /**
     * This constructor is only used for testing. This constructor allows users
     * to specify an executor to use as the foreground thread instead of using
     * the normal EDT. This is handy for ensuring all of the events occur on the
     * correct thread and updates do not conflict with persists. If the executor
     * is null then the foreground thread will just execute the runnables on the
     * current thread.
     */
	public ArchitectClientSideSession(ArchitectSessionContext context,
	        String name, ProjectLocation projectLocation, boolean useThreadPool) throws SQLObjectException {
		super(context, name, new ArchitectSwingProject());
		
		this.projectLocation = projectLocation;
        this.useThreadPool = useThreadPool;
        this.foregroundThreadExecutor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, 
                new LinkedBlockingQueue<Runnable>(), new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread newThread = new Thread(r);
                        foregroundExecutorThread.add(newThread);
                        return newThread;
                    }
                });
        foregroundThreadExecutor.allowCoreThreadTimeOut(false);
		dataSourceCollectionUpdater = new ArchitectDataSourceCollectionUpdater(projectLocation);
		this.isEnterpriseSession = true;
		
		setupSnapshots();
		
		String ddlgClass = prefs.get(this.projectLocation.getUUID() + ".ddlg", null);
		if (ddlgClass != null) {
		    try {
                DDLGenerator ddlg = (DDLGenerator) Class.forName(ddlgClass, true, ArchitectClientSideSession.class.getClassLoader()).newInstance();
                setDDLGenerator(ddlg);
                ddlg.setTargetCatalog(prefs.get(this.projectLocation.getUUID() + ".targetCatalog", null));
                ddlg.setTargetSchema(prefs.get(this.projectLocation.getUUID() + ".targetSchema", null));
            } catch (Exception e) {
                createUserPrompter("Cannot load DDL settings due to missing class " + ddlgClass, 
                        UserPromptType.MESSAGE, UserPromptOptions.OK, UserPromptResponse.OK, null, "OK");
                logger.error("Cannot find DDL Generator for class " + ddlgClass + 
                        ", ddl generator properties are not loaded.");
            }
		}
		
		outboundHttpClient = ClientSideSessionUtils.createHttpClient(projectLocation.getServiceInfo(), cookieStore);
		dataSourceCollection = getDataSources();
		
		sessionPersister = new ArchitectSessionPersister("inbound-" + projectLocation.getUUID(), getWorkspace(), 
		        new ArchitectPersisterSuperConverter(dataSourceCollection, getWorkspace()));
		sessionPersister.setWorkspaceContainer(this);
		
		jsonMessageDecoder = new SPJSONMessageDecoder(sessionPersister);
		
		updater = new ArchitectNetworkConflictResolver(
		        projectLocation, 
		        jsonMessageDecoder, 
		        ClientSideSessionUtils.createHttpClient(projectLocation.getServiceInfo(), cookieStore), 
		        outboundHttpClient, this);
		
		jsonPersister = new SPJSONPersister(updater);
		
		verifyServerLicense(projectLocation);
	}

    protected void verifyServerLicense(ProjectLocation projectLocation) throws AssertionError {
        try {
            ServerInfoProvider.getServerVersion(
                    projectLocation.getServiceInfo().getServerAddress(), 
                    String.valueOf(projectLocation.getServiceInfo().getPort()), 
                    projectLocation.getServiceInfo().getPath(), 
                    projectLocation.getServiceInfo().getUsername(), 
                    projectLocation.getServiceInfo().getPassword(),
                    cookieStore);
        } catch (Exception e) {
            throw new AssertionError("Exception encountered while verifying the server license:" + e.getMessage());
        }
    }

    /**
     * Helper method for the constructor of a client side session.
     * <p>
     * This method will attach listeners and update snapshots as necessary on
     * the project's load.
     */
    private void setupSnapshots() {
        getTargetDatabase().addSPListener(new SPObjectSnapshotHierarchyListener(this));
        
        //This listener will update the obsolete flag on snapshots being added
        //to the workspace to keep old snapshot's obsolete flag correct when
        //they haven't been opened in some time but updates have occurred to the types.
        //While this will also check new snapshots being added to the system doing
        //the check doesn't hurt anything.
        final AbstractPoolingSPListener obsolescenceListener = new AbstractPoolingSPListener(false) {
            @Override
            public void childAddedImpl(SPChildEvent e) {
                if (e.getChild() instanceof UserDefinedSQLTypeSnapshot) {
                    UserDefinedSQLTypeSnapshot snapshot = (UserDefinedSQLTypeSnapshot) e.getChild();
                    UserDefinedSQLType systemType = findSystemTypeFromSnapshot(snapshot);
                    if (systemType == null) {
                        snapshot.setDeleted(true);
                    } else { 
                        if (!UserDefinedSQLType.areEqual(snapshot.getSPObject(), systemType)) {
                            snapshot.setObsolete(true);
                        }
                        snapshot.setDeleted(false);
                    }
                } else if (e.getChild() instanceof DomainCategorySnapshot) {
                    DomainCategorySnapshot snapshot = (DomainCategorySnapshot) e.getChild();
                    boolean deleted = true;
                    for (DomainCategory category : getSystemWorkspace().getDomainCategories()) {
                        if (category.getUUID().equals(snapshot.getOriginalUUID())) {
                            deleted = false;
                            if (!DomainCategory.areEqual(snapshot.getSPObject(), category)) {
                                snapshot.setObsolete(true);
                                deleted = true;
                            }
                        }
                    }
                    snapshot.setDeleted(deleted);
                }
            }
        };
        getWorkspace().getSnapshotCollection().addSPListener(obsolescenceListener);
        getWorkspace().addSPListener(new AbstractSPListener() {
            @Override
            public void transactionStarted(TransactionEvent e) {
                obsolescenceListener.transactionStarted(TransactionEvent.createStartTransactionEvent(getWorkspace().getSnapshotCollection(), "Simulated begin: " + e.getMessage()));
            }
            @Override
            public void transactionEnded(TransactionEvent e) {
                obsolescenceListener.transactionEnded(TransactionEvent.createEndTransactionEvent(getWorkspace().getSnapshotCollection(), "Simulated commit: " + e.getMessage()));
            }
            @Override
            public void transactionRollback(TransactionEvent e) {
                obsolescenceListener.transactionRollback(TransactionEvent.createRollbackTransactionEvent(getWorkspace().getSnapshotCollection(), "Simulated rollback: " + e.getMessage()));
            }
        });
        
        // If this returns null, it means the system session hasn't been
        // initialized yet. This means we are the system session, and attaching
        // the listener is unnecessary.
        if (getSystemSession() != null) {
            deletionListener = new AbstractPoolingSPListener() {
                @Override
                protected void childAddedImpl(SPChildEvent e) {
                    if (e.getChild() instanceof DomainCategory) {
                        e.getChild().addSPListener(deletionListener);
                    } else {
                        for (SPObjectSnapshot<SPObject> snapshot : getWorkspace().getSnapshotCollection().getChildren(SPObjectSnapshot.class)) {
                            if (snapshot.getOriginalUUID().equals(e.getChild().getUUID())) {
                                snapshot.setDeleted(false);
                            }
                        }
                    }
                }

                @Override
                protected void childRemovedImpl(SPChildEvent e) {
                    if (e.getChild() instanceof DomainCategory) {
                        e.getChild().removeSPListener(deletionListener);
                    } else {
                        for (SPObjectSnapshot<SPObject> snapshot : getWorkspace().getSnapshotCollection().getChildren(SPObjectSnapshot.class)) {
                            if (snapshot.getOriginalUUID().equals(e.getChild().getUUID())) {
                                snapshot.setDeleted(true);
                            }
                        }
                    }
                }
            };

            getSystemWorkspace().addSPListener(deletionListener);
            for (DomainCategory cat : getSystemWorkspace().getChildren(DomainCategory.class)) {
                cat.addSPListener(deletionListener);
            }
        }
    }

    /**
     * Map of server addresses to system workspaces. Use
     * {@link SPServerInfo#getServerAddress()} as the key.
     */
	public static Map<String, ArchitectClientSideSession> getSecuritySessions() {
        return securitySessions;
    }
	
	@Override
    public boolean close() {
    	if (getDDLGenerator() != null) {
    	    if (getDDLGenerator().getTargetCatalog() != null) {
    	        prefs.put(projectLocation.getUUID() + ".targetCatalog", getDDLGenerator().getTargetCatalog());
    	    }
    	    if (getDDLGenerator().getTargetSchema() != null) {
    	        prefs.put(projectLocation.getUUID() + ".targetSchema", getDDLGenerator().getTargetSchema());
    	    }
    	    prefs.put(projectLocation.getUUID() + ".ddlg", getDDLGenerator().getClass().getName());
    	}
    	
    	try {
    	    //TODO: Figure out how to de-register the session &c.
		} catch (Exception e) {
			try {
				logger.error(e);
				
				createUserPrompter("Cannot access the server to close the server session", 
						UserPromptType.MESSAGE,
						UserPromptOptions.OK, 
						UserPromptResponse.OK, 
						UserPromptResponse.OK, "OK");
				
			} catch (Throwable t) {
				//do nothing here because we failed on logging the error.
			}
		}
		
		updater.interrupt();
        outboundHttpClient.getConnectionManager().shutdown();
        
        if (dataSourceCollection != null) {
            dataSourceCollectionUpdater.detach(dataSourceCollection);
        }
        
        getSystemWorkspace().removeSPListener(deletionListener);
        for (DomainCategory cat : getSystemWorkspace().getChildren(DomainCategory.class)) {
            cat.removeSPListener(deletionListener);
        }
        
        return super.close();
    }

	@Override
	public DataSourceCollection<JDBCDataSource> getDataSources() {
	    if (dataSourceCollection == null) {
            dataSourceCollection = getDataSourcesFromServer();
            dataSourceCollectionUpdater.attach(dataSourceCollection);
        }
	    return dataSourceCollection;
	}
	
	private DataSourceCollection<JDBCDataSource> getDataSourcesFromServer() {
		ResponseHandler<DataSourceCollection<JDBCDataSource>> plIniHandler = 
            new ResponseHandler<DataSourceCollection<JDBCDataSource>>() {
            public DataSourceCollection<JDBCDataSource> handleResponse(HttpResponse response)
                    throws ClientProtocolException, IOException {
                
                if (response.getStatusLine().getStatusCode() == 401) {
                    throw new AccessDeniedException("Access Denied");
                }

                if (response.getStatusLine().getStatusCode() != 200) {
                    throw new IOException(
                            "Server error while reading data sources: " + response.getStatusLine());
                }
                
                
                PlDotIni plIni;
                try {
					plIni = new PlDotIni(ClientSideSessionUtils.getServerURI(projectLocation.getServiceInfo(), "/" + ClientSideSessionUtils.REST_TAG +"/jdbc/"),
					        ClientSideSessionUtils.getServerURI(projectLocation.getServiceInfo(), MONDRIAN_SCHEMA_REL_PATH)) {
					    
					    @Override
					    public List<UserDefinedSQLType> getSQLTypes() {
					        List<UserDefinedSQLType> types = new ArrayList<UserDefinedSQLType>();
					        types.addAll(ArchitectClientSideSession.this.getSQLTypes());
					        types.addAll(ArchitectClientSideSession.this.getDomains());
					        return types;
					    }
					    
					    @Override
					    public UserDefinedSQLType getNewSQLType(String name, int jdbcCode) {
					        UserDefinedSQLType newType = new UserDefinedSQLType();
					        newType.setName(name);
					        newType.setType(jdbcCode);
					        ArchitectSwingProject systemWorkspace = ArchitectClientSideSession.this.getSystemWorkspace();
                            systemWorkspace.addChild(newType, systemWorkspace.getChildren(UserDefinedSQLType.class).size());
                            return newType;
					    }
					    
					    public SPDataSource getDataSource(String name) {
					        SPDataSource ds = super.getDataSource(name);
					        if (ds == null) {
					            mergeNewDataSources();
					            return super.getDataSource(name);
					        } else {
					            return ds;
					        }
					    }
					    
					    public <C extends SPDataSource> C getDataSource(String name, java.lang.Class<C> classType) {
					        C ds = super.getDataSource(name, classType);
                            if (ds == null) {
                                mergeNewDataSources();
                                return super.getDataSource(name, classType);
                            } else {
                                return ds;
                            }
					    }
					    
					    private void mergeNewDataSources() {
					        DataSourceCollection<JDBCDataSource> dsc = getDataSourcesFromServer();
                            for (SPDataSource merge : dsc.getConnections()) {
                                mergeDataSource(merge);
                            }
					    }
					};
                    plIni.read(response.getEntity().getContent());
                    logger.debug("Data source collection has URI " + plIni.getServerBaseURI());
                } catch (URISyntaxException e) {
                    throw new RuntimeException(e);
                }
                
                return new SpecificDataSourceCollection<JDBCDataSource>(plIni, JDBCDataSource.class);
            }
        };
        
        DataSourceCollection<JDBCDataSource> dsc;
        try {
            dsc = ClientSideSessionUtils.executeServerRequest(outboundHttpClient, projectLocation.getServiceInfo(), 
                    "/" + ClientSideSessionUtils.REST_TAG + "/data-sources/", plIniHandler);
        } catch (AccessDeniedException e) {
            throw e;
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        
        return dsc;
	}
	
	public void startUpdaterThread() {
		
		final SPPersisterListener listener = new SPPersisterListener(jsonPersister, sessionPersister,
						new ArchitectPersisterSuperConverter(dataSourceCollection, getWorkspace()));
		SQLPowerUtils.listenToHierarchy(getWorkspace(), listener);
		
		updater.setListener(listener);
		updater.setConverter(new SessionPersisterSuperConverter(dataSourceCollection, getWorkspace()));
		updater.start();
		
		addSessionLifecycleListener(new SessionLifecycleListener<ArchitectSession>() {
			public void sessionClosing(SessionLifecycleEvent<ArchitectSession> e) {
				SQLPowerUtils.unlistenToHierarchy(getWorkspace(), listener);
			}

            public void sessionOpening(SessionLifecycleEvent<ArchitectSession> e) {
            }
		});
	}
	
	public User getUser() {
	    String username = getProjectLocation().getServiceInfo().getUsername();
	    User currentUser = null;
        for (User user : getSystemWorkspace().getChildren(User.class)) {
            if (user.getUsername().equals(username)) {
                currentUser = user;
            }
        }
	    return currentUser;
	}
	
	public void persistProjectToServer() throws SPPersistenceException {
		final SPPersisterListener tempListener = new SPPersisterListener(jsonPersister,
						new ArchitectPersisterSuperConverter(dataSourceCollection, getWorkspace()));
		tempListener.persistObject(getWorkspace(), 0);
	}
	
	public ArchitectSwingProject getSystemWorkspace() {
		return getSecuritySessions().get(getProjectLocation().getServiceInfo().getServerAddress()).getWorkspace();
	}
	
	public ArchitectClientSideSession getSystemSession() {
	    return getSecuritySessions().get(getProjectLocation().getServiceInfo().getServerAddress());
	}

	@Override
	public void runInForeground(Runnable runner) {
		// If we're in a SwingContext, run on the Swing Event Dispatch thread.
		// XXX: This is a bit of a quickfix and I think a better way to possibly fix
		// this could be to have WabitServerSession implement WabitSession, and
		// use a delegate session to delegate most of the server calls (instead
		// of extending WabitSessionImpl). Then if it's in a swing context, it would
		// have a WabitSwingSession instead.
		if (getContext() instanceof ArchitectSwingSessionContext) {
			SwingUtilities.invokeLater(runner);
		} else if (useThreadPool) {
		    foregroundThreadExecutor.execute(runner);
		} else {
			super.runInForeground(runner);
		}
	}
	
	@Override
	public boolean isForegroundThread() {
	    if (useThreadPool) {
	        return foregroundExecutorThread.contains(Thread.currentThread());
	    } else {
	        return super.isForegroundThread();
	    }
	}
	
	/**
	 * Exposes the shared cookie store so we don't spawn useless sessions
	 * through the client.
	 */
	public static CookieStore getCookieStore() {
		return cookieStore;
	}
	
	public ProjectLocation getProjectLocation() {
		return projectLocation;
	}
	
	public List<TransactionInformation> getTransactionList(long fromVersion, long toVersion)
    throws IOException, URISyntaxException, JSONException, ParseException {
        
        SPServerInfo serviceInfo = projectLocation.getServiceInfo();
        HttpClient httpClient = ClientSideSessionUtils.createHttpClient(serviceInfo, cookieStore);
        
        try {
            
            logger.info("Getting transactions between " + fromVersion + " and " + toVersion);
            JSONMessage message = ClientSideSessionUtils.executeServerRequest(httpClient, projectLocation.getServiceInfo(),
                    "/" + ClientSideSessionUtils.REST_TAG + "/project/" + projectLocation.getUUID() + "/revision_list",
                    "versions=" + fromVersion + ":" + toVersion,
                    new JSONResponseHandler());
            
            return ClientSideSessionUtils.decodeJSONRevisionList(message.getBody());
            
        } finally {
            httpClient.getConnectionManager().shutdown();
        }               
        
    }

	public static ProjectLocation uploadProject(SPServerInfo serviceInfo, String name, File project, UserPrompterFactory session) 
    throws URISyntaxException, ClientProtocolException, IOException, JSONException {
	    return ClientSideSessionUtils.uploadProject(serviceInfo, name, project, session, cookieStore);
	}
	
	public int revertServerWorkspace(int revisionNo) throws IOException, URISyntaxException, JSONException {
	    return revertServerWorkspace(projectLocation, revisionNo);
	}
    
	/**
     * This method reverts the server workspace specified by the given project location
     * to the specified revision number.
     * 
	 * @param projectLocation
	 * @param revisionNo
	 * @return The new global revision number, right after the reversion, or -1 if the server did not revert.
	 * @throws IOException
	 * @throws URISyntaxException
	 * @throws JSONException
	 */
    public static int revertServerWorkspace(ProjectLocation projectLocation, int revisionNo)
    throws IOException, URISyntaxException, JSONException {
        return ClientSideSessionUtils.revertServerWorkspace(projectLocation, revisionNo, cookieStore);
    }

    /**
     * This method can update any users password on the server given the correct
     * old password and done by a user with the privileges to change the user's
     * password.
     * 
     * @param user
     *            The user whose password to update.
     * @param oldPassword
     *            The old password of the user to validate that the password can
     *            be updated correctly.
     * @param newPassword
     *            The new password to update to.
     * @param upf
     *            A user prompter to display message and error information to
     *            the user as necessary.
     */
	public void updateUserPassword(User user, 
	        String oldPassword, String newPassword, UserPrompterFactory upf) {
	    SPServerInfo serviceInfo = getProjectLocation().getServiceInfo();
        
        HttpClient client = ClientSideSessionUtils.createHttpClient(serviceInfo, cookieStore);
        
        MessageDigest digester;
        try {
            digester = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException ex) {
            throw new RuntimeException(ex);
        }
        
        try {
            JSONObject begin = new JSONObject();
            begin.put("uuid", JSONObject.NULL);
            begin.put("method", "begin");
            
            JSONObject persist = new JSONObject();
            persist.put("uuid", user.getUUID());
            persist.put("propertyName", "password");
            persist.put("type", Datatype.STRING.toString());
            if (oldPassword == null) {
                persist.put("method", "persistProperty");
            } else {
                persist.put("method", "changeProperty");
                persist.put("oldValue", new String(Hex.encodeHex(digester.digest(oldPassword.getBytes()))));
            }
            persist.put("newValue", new String(Hex.encodeHex(digester.digest(newPassword.getBytes()))));
            
            JSONObject commit = new JSONObject();
            commit.put("uuid", JSONObject.NULL);
            commit.put("method", "commit");
            
            JSONArray transaction = new JSONArray();
            transaction.put(begin);
            transaction.put(persist);
            transaction.put(commit);

            URI serverURI = new URI("http", null, 
                    serviceInfo.getServerAddress(), 
                    serviceInfo.getPort(),
                    serviceInfo.getPath() + 
                    "/" + ClientSideSessionUtils.REST_TAG + "/project/system", 
                    "currentRevision=" + getCurrentRevisionNumber(), null);
            HttpPost postRequest = new HttpPost(serverURI);
            postRequest.setEntity(new StringEntity(transaction.toString())); 
            postRequest.setHeader("Content-Type", "application/json");
            HttpUriRequest request = postRequest;
            JSONMessage result = client.execute(request, new JSONResponseHandler());
            if (result.getStatusCode() != 200) {
                logger.warn("Failed password change");
                if (result.getStatusCode() == 412) {
                    upf.createUserPrompter("The password you have entered is incorrect.", 
                            UserPromptType.MESSAGE, 
                            UserPromptOptions.OK, 
                            UserPromptResponse.OK, 
                            "OK", "OK").promptUser("");
                } else {
                    upf.createUserPrompter(
                            "Could not change the password due to the following: " + 
                            result.getBody() + " See logs for more details.", 
                            UserPromptType.MESSAGE, 
                            UserPromptOptions.OK, 
                            UserPromptResponse.OK, 
                            "OK", "OK").promptUser("");
                }
            } else {
                upf.createUserPrompter(
                        "Password successfully changed. Please log into open projects" +
                        " with your new password.", 
                        UserPromptType.MESSAGE, 
                        UserPromptOptions.OK, 
                        UserPromptResponse.OK, 
                        "OK", "OK").promptUser("");
            }
        } catch (AccessDeniedException ex) {
            logger.warn("Failed password change", ex);
            upf.createUserPrompter("The password you have entered is incorrect.", 
                    UserPromptType.MESSAGE, 
                    UserPromptOptions.OK, 
                    UserPromptResponse.OK, 
                    "OK", "OK").promptUser("");
        } catch (Exception ex) {
            logger.warn("Failed password change", ex);
            upf.createUserPrompter(
                    "Could not change the password due to the following: " + 
                    ex.getMessage() + " See logs for more details.", 
                    UserPromptType.MESSAGE, 
                    UserPromptOptions.OK, 
                    UserPromptResponse.OK, 
                    "OK", "OK").promptUser("");
        }
	}
	
	public void persistRevisionFromServer(int revisionNo, SPJSONMessageDecoder targetDecoder)
	throws IOException, URISyntaxException, SPPersistenceException, IllegalArgumentException {
	    persistRevisionFromServer(projectLocation, revisionNo, targetDecoder);
	}

	/**
     * Gets a list of DiffChunks representing the differences between the two revisions from the server.
     */
	public List<DiffChunk<DiffInfo>> getComparisonDiffChunks(int oldRevisionNo, int newRevisionNo) 
	throws IOException, URISyntaxException, JSONException, SPPersistenceException {
	    
        SPServerInfo serviceInfo = projectLocation.getServiceInfo();
        HttpClient httpClient = ClientSideSessionUtils.createHttpClient(serviceInfo, cookieStore);
        
        try {
            JSONMessage response = ClientSideSessionUtils.executeServerRequest(httpClient, projectLocation.getServiceInfo(),
                    "/" + ClientSideSessionUtils.REST_TAG + "/project/" + projectLocation.getUUID() + "/compare",
                    "versions=" + oldRevisionNo + ":" + newRevisionNo, 
                    new JSONResponseHandler());    
                                  
            return SimpleDiffChunkJSONConverter.decode(response.getBody());
            
        } finally {
            httpClient.getConnectionManager().shutdown();
        }
        
	}
    
    public static HttpClient createHttpClient(SPServerInfo serviceInfo) {
        return ClientSideSessionUtils.createHttpClient(serviceInfo, cookieStore);
    }
    
    public static void persistRevisionFromServer(ProjectLocation projectLocation, 
            int revisionNo, 
            SPJSONMessageDecoder decoder)
    throws IOException, URISyntaxException, SPPersistenceException, IllegalArgumentException {
        
        ClientSideSessionUtils.persistRevisionFromServer(projectLocation, revisionNo, decoder, cookieStore);
    }
	
	public static ProjectLocation createNewServerSession(SPServerInfo serviceInfo, String name, 
	        ArchitectSession session)
    throws URISyntaxException, ClientProtocolException, IOException, JSONException {
        return ClientSideSessionUtils.createNewServerSession(serviceInfo,
                name,
                cookieStore,
                new DefaultUserPrompterFactory());
    }
    
    public static List<ProjectLocation> getWorkspaceNames(SPServerInfo serviceInfo,
            UserPrompterFactory upf) 
    throws IOException, URISyntaxException, JSONException {
        return ClientSideSessionUtils.getWorkspaceNames(serviceInfo, cookieStore, upf);
    }
	
	public static void deleteServerWorkspace(ProjectLocation projectLocation, ArchitectSession session) throws URISyntaxException, ClientProtocolException, IOException {
    	
	    ClientSideSessionUtils.deleteServerWorkspace(projectLocation,
	            cookieStore, new DefaultUserPrompterFactory());
    }
    
    public ArchitectNetworkConflictResolver getUpdater() {
        return updater;
    }
	
	// ----------- Preferences accessors and mutators -----------
	// XXX Add different types, such as int or boolean, as needed

	/**
	 * Enters a double value as a preference for this server session.
	 * It will be able to be loaded by this local user in the future.
	 */
    public void putPref(String prefName, double pref) {
        prefs.putDouble(projectLocation.getUUID() + "." + prefName, pref);
    }    
    
    /**
     * Enters a String as a preference for this server session.
     * It will be able to be loaded by this local user in the future.
     */
    public void putPref(String prefName, String pref) {
        prefs.put(projectLocation.getUUID() + "." + prefName, pref);
    }
    
    /**
     * Retrieves a locally saved preference of type double.
     * @param prefName The name of the previously saved preference
     * @return The previously saved preference, or 0 if none exists yet
     */
    public double getPrefDouble(String prefName) {
        return getPrefDouble(prefName, 0);
    }
    
    /**
     * Retrieves a locally saved preference of type double.
     * @param prefName The name of the previously saved preference
     * @param def The value this function should return 
     * if no preference was previously saved
     */
    public double getPrefDouble(String prefName, double def) {
        return prefs.getDouble(projectLocation.getUUID() + "." + prefName, def);
    }
    
    /**
     * Retrieves a locally saved preference of type String.
     * @param prefName The name of the previously saved preference
     * @return The previously saved preference, or null if none exists yet
     */
    public String getPref(String prefName) {
        return getPref(prefName, null);
    }
    
    /**
     * Retrieves a locally saved preference of type String.
     * @param prefName The name of the previously saved preference
     * @param def The value this function should return 
     * if no preference was previously saved
     */
    public String getPref(String prefName, String def) {
        return prefs.get(projectLocation.getUUID() + "." + prefName, def);
    }

    public int getCurrentRevisionNumber() {
        return updater.getRevision();
    }
    
    /*
     * Do not remove this method without first changing the custom PlDotini in getDataSources().
     * Doing so will cause infinite recursion when accessing the SQLTypes.
     */
    @Override
    public List<UserDefinedSQLType> getSQLTypes() {
        // The following was my attempt to merge the snapshot and system types lists together
        // without making it O(mn), but the code is a bit lengthier than I'd like, so perhaps
        // the added complexity may not be worth it?
        SnapshotCollection collection = getWorkspace().getSnapshotCollection();
        
        List<UserDefinedSQLTypeSnapshot> typeSnapshots = 
            new ArrayList<UserDefinedSQLTypeSnapshot>(
                   collection.getChildren(UserDefinedSQLTypeSnapshot.class));
        List<UserDefinedSQLType> systemTypes = 
            new ArrayList<UserDefinedSQLType>(
                    getSystemWorkspace().getChildren(UserDefinedSQLType.class));
        
        // Remove domain snapshots from the list
        for (int i = typeSnapshots.size() - 1; i >= 0; i--) {
            UserDefinedSQLTypeSnapshot snapshot = typeSnapshots.get(i);
            if (snapshot.isDomainSnapshot()) {
                typeSnapshots.remove(i);
            }
        }
        
        // If there are no snapshots, just return the system types.
        if (typeSnapshots.size() == 0) return Collections.unmodifiableList(systemTypes);

        // Sort both lists by the UUIDs of the system types
        Collections.sort(typeSnapshots, new Comparator<UserDefinedSQLTypeSnapshot>() {
            public int compare(UserDefinedSQLTypeSnapshot o1, UserDefinedSQLTypeSnapshot o2) {
                return o1.getOriginalUUID().compareTo(o2.getOriginalUUID());
            }
        });
        Collections.sort(systemTypes, new SPObjectUUIDComparator<UserDefinedSQLType>());

        // Now go through the list of system types. If a snapshot type's
        // original UUID matches, then replace the system type with the snapshot.
        Iterator<UserDefinedSQLTypeSnapshot> snapshotIterator = typeSnapshots.iterator();
        UserDefinedSQLTypeSnapshot currentSnapshot = snapshotIterator.next();
        
        for (int i = 0; i < systemTypes.size() ; i++) {
            UserDefinedSQLType type = systemTypes.get(i);
            int compareTo = currentSnapshot.getOriginalUUID().compareTo(type.getUUID());
            if (compareTo <= 0) {
                if (compareTo == 0) {
                    systemTypes.set(i, currentSnapshot.getSPObject());
                } else {
                    systemTypes.add(i, currentSnapshot.getSPObject());
                }
                if (snapshotIterator.hasNext() && i != systemTypes.size() - 1) {
                    currentSnapshot = snapshotIterator.next();
                } else {
                    break;
                }
            }
        }
        
        // If we've gone through all the system types, then append the remaining snapshot types
        while (snapshotIterator.hasNext()) {
            currentSnapshot = snapshotIterator.next();
            systemTypes.add(currentSnapshot.getSPObject());
        }
        
        return Collections.unmodifiableList(systemTypes);
    }

    /**
     * Returns the {@link List} of {@link UserDefinedSQLType}s in this session
     * that are children of a {@link DomainCategory}. Each domain in the system
     * will be in the list once as either the domain that is in the system
     * workspace or a snapshot of the domain that is in the current project. UDTs
     * that are children of a category are defined as domains instead of types.
     */
    @Override
    public List<UserDefinedSQLType> getDomains() {
        // The following was my attempt to merge the snapshot and system category lists together
        // without making it O(nm), but the code is a bit lengthier than I'd like, so perhaps
        // the added complexity may not be worth it?
        SnapshotCollection collection = getWorkspace().getSnapshotCollection();
        
        List<UserDefinedSQLTypeSnapshot> typeSnapshots = 
            new ArrayList<UserDefinedSQLTypeSnapshot>(collection.getChildren(UserDefinedSQLTypeSnapshot.class));
        List<DomainCategory> systemCategories = 
            new ArrayList<DomainCategory>(getSystemWorkspace().getChildren(DomainCategory.class));
        List<UserDefinedSQLTypeSnapshot> domainSnapshots = new ArrayList<UserDefinedSQLTypeSnapshot>();
        for (UserDefinedSQLTypeSnapshot udtSnapshot : typeSnapshots) {
            if (udtSnapshot.isDomainSnapshot()) {
                domainSnapshots.add(udtSnapshot);
            }
        }
        List<UserDefinedSQLType> systemDomains = new ArrayList<UserDefinedSQLType>();
        for (DomainCategory category : systemCategories) {
            systemDomains.addAll(category.getChildren(UserDefinedSQLType.class));
        }
        
        // If there are no snapshots, just return the system categories.
        if (domainSnapshots.size() == 0) return Collections.unmodifiableList(systemDomains);
        
        // Sort both lists by the UUIDs of the system categories
        Collections.sort(domainSnapshots, new Comparator<UserDefinedSQLTypeSnapshot>() {
            public int compare(UserDefinedSQLTypeSnapshot o1, UserDefinedSQLTypeSnapshot o2) {
                return o1.getOriginalUUID().compareTo(o2.getOriginalUUID());
            }
        });
        Collections.sort(systemDomains, new SPObjectUUIDComparator<UserDefinedSQLType>());

        // Now go through the list of system categories. If a snapshot category's
        // original UUID matches, then replace the system category with the snapshot.
        Iterator<UserDefinedSQLTypeSnapshot> snapshotIterator = domainSnapshots.iterator();
        UserDefinedSQLTypeSnapshot currentSnapshot = snapshotIterator.next();
        
        for (int i = 0; i < systemDomains.size() ; i++) {
            UserDefinedSQLType type = systemDomains.get(i);
            int compareTo = currentSnapshot.getOriginalUUID().compareTo(type.getUUID());
            if (compareTo <= 0) {
                if (compareTo == 0) {
                    systemDomains.set(i, currentSnapshot.getSPObject());
                } else {
                    systemDomains.add(i, currentSnapshot.getSPObject());
                }
                if (snapshotIterator.hasNext() && i != systemDomains.size() - 1) {
                    currentSnapshot = snapshotIterator.next();
                } else {
                    break;
                }
            }
        }
        
        // If we've gone through all the system types, then append the remaining snapshot categories
        while (snapshotIterator.hasNext()) {
            currentSnapshot = snapshotIterator.next();
            systemDomains.add(currentSnapshot.getSPObject());
        }
        
        return Collections.unmodifiableList(systemDomains);
    }

    @Override
    public ArchitectSwingProject getWorkspace() {
        return (ArchitectSwingProject) super.getWorkspace();
    }
    
    /**
     * Finds the UDT that has the same uuid as the given snapshot's
     * {@link SPObjectSnapshot#getOriginalUUID()} method. The snapshot given
     * cannot be null. Returns null if no type is found.
     */
    public UserDefinedSQLType findSystemTypeFromSnapshot(@Nonnull SPObjectSnapshot<?> snapshot) {
        for (UserDefinedSQLType systemType : getSystemWorkspace().getSqlTypes()) {
            if (systemType.getUUID().equals(snapshot.getOriginalUUID())) {
                return systemType;
            }
        }
        for (DomainCategory category : getSystemWorkspace().getDomainCategories()) {
            for (UserDefinedSQLType systemType : category.getChildren(UserDefinedSQLType.class)) {
                if (systemType.getUUID().equals(snapshot.getOriginalUUID())) {
                    return systemType;
                }
            }
        }
        return null;
    }

    /**
     * Returns an action that will update the snapshot object in the given
     * snapshot to be the same as the type the snapshot maps to in the system
     * workspace.
     * 
     * @param snapshot
     *            The snapshot to update. This currently only works for
     *            snapshots of UserDefinedSQLTypes but will be extended to
     *            others in the future.
     */
    public Runnable createUpdateSnapshotRunnable(final SPObjectSnapshot<?> snapshot) {
        return new Runnable() {

            @Override
            public void run() {
                if (snapshot.getSPObject() instanceof DomainCategory) {
                    for (DomainCategory category : getSystemWorkspace().getDomainCategories()) {
                        if (category.getUUID().equals(snapshot.getOriginalUUID())) {
                            ((DomainCategory) snapshot.getSPObject()).updateToMatch(category);
                            snapshot.setObsolete(false);
                            return;
                        }
                    }
                }
                
                if (!(snapshot.getSPObject() instanceof UserDefinedSQLType)) return;
                UserDefinedSQLType snapshotType = (UserDefinedSQLType) snapshot.getSPObject();
                UserDefinedSQLType systemType = findSystemTypeFromSnapshot(snapshot);
                if (systemType == null) return;
                
                snapshotType.updateToMatch(systemType);
                snapshot.setObsolete(false);
            }
        };
    }
} public class KettleJob implements Monitorable {
    
    private static final Logger logger = Logger.getLogger(KettleJob.class);
    
    /**
     * The spacing between nodes in Kettle jobs and transformations
     */
    private static int spacing = 150;
    
    /**
     * A list of tasks that an administrator or tech will have to do to the Kettle
     * job to make it run correctly.
     */
    private List<String> tasksToDo;
    
    /**
     * This is the monitor implementation for this class. This is used to handle the progress bar
     * on the action window.
     */
    private MonitorableImpl monitor;
    
    
    /**
     * The repository directory chooser that will select, or allow the user to select, the directory to save
     * to.
     */
    private KettleRepositoryDirectoryChooser dirChooser;

    /**
     * The session this job belongs to.
     */
    private final ArchitectSession session;
    
    /**
     * Holds the settings for persistence to server.
     */
    private final KettleSettings settings;
    
    /**
     * used when big job spilitted into small
     * list of job name
     */
    private List<JobMeta> jobMetaList = new ArrayList<JobMeta>();
    /**
     * job number used create inner job
     */
    private int job_no = 0;
    public KettleJob(ArchitectSession session, KettleRepositoryDirectoryChooser chooser) {
        this(session);
        dirChooser = chooser;
    }
    
    public KettleJob(ArchitectSession session) {
        super();
        this.session = session;
        if (session.getWorkspace() != null 
                && session.getWorkspace().allowsChildType(KettleSettings.class)
                && !session.getWorkspace().getChildren(KettleSettings.class).isEmpty()) {
            KettleSettings newSettings = session.getWorkspace().getChildren(KettleSettings.class).get(0);
            if (newSettings == null) throw new IllegalStateException(
                    "The workspace should not have null settings if it reports to have them.");
            settings = newSettings;
        } else {
            settings = new KettleSettings();
        }
        tasksToDo = new ArrayList<String>();
        monitor = new MonitorableImpl();
        dirChooser = new RootRepositoryDirectoryChooser();
    }
    
    /**
     * This method translates the list of SQLTables to a Kettle Job and Transformations and saves
     * them to KJB and KTR files OR a repository.
     * @param tableList
     *      List of tables used to generate kettle job. 
     *      It can be splitted table list if job is splitted into multiple job 
     *      otherwise it will be table list from PlayPen.
     * @param targetDB
     * @param playPenTableList
     *          List of tables in a Playpen. which can be used to find the mapping table
     * @throws KettleException 
     */
    public void doExport(List<SQLTable> tableList, SQLDatabase targetDB, List<SQLTable> playPenTableList) throws SQLObjectException, RuntimeException, IOException, KettleException, SQLException {

        monitor = new MonitorableImpl();
        monitor.setMessage("");
        monitor.setJobSize(new Integer(tableList.size()+1)); 
        monitor.setStarted(true);

        try {

            EnvUtil.environmentInit();
            LogWriter lw = LogWriter.getInstance();
            JobMeta jm = new JobMeta(lw);

            Map<SQLTable, StringBuffer> tableMapping;
            List<TransMeta> transformations = new ArrayList<TransMeta>();
            List<String> noTransTables = new ArrayList<String>();
            tasksToDo = new ArrayList<String>();


            // The depth-first search will do a topological sort of
            // the target table graph, so parent tables will come before
            // their children.
            tableList = new DepthFirstSearch(tableList).getFinishOrder();
            playPenTableList = new DepthFirstSearch(playPenTableList).getFinishOrder();
            Map<String, DatabaseMeta> databaseNames = new LinkedHashMap<String, DatabaseMeta>();

            for (SQLTable table: tableList) {

                TransMeta transMeta = new TransMeta();
                transMeta.setName(table.getName());
                tableMapping = new LinkedHashMap<SQLTable, StringBuffer>();

                JDBCDataSource target = targetDB.getDataSource();
                DatabaseMeta targetDatabaseMeta = addDatabaseConnection(databaseNames, target);
                transMeta.addDatabase(targetDatabaseMeta);

                List<SQLColumn> columnList = table.getColumns();
                List<String> noMappingForColumn = new ArrayList<String>();
                List<StepMeta> inputSteps = new ArrayList<StepMeta>();
                List<String[]> keyFields1 = new ArrayList<String[]>();
                List<String[]> keyFields2 = new ArrayList<String[]>();
                JDBCDataSourceType dsType = targetDB.getDataSource().getParentType();
                boolean isQuoting = dsType.getSupportsQuotingName();
                String ddlGeneratorClass = dsType.getDDLGeneratorClass();
                
                logger.debug("isQuoting:  " +isQuoting );
                for (SQLColumn column: columnList) {
                    SQLTable sourceTable;
                    String sourceColumn;
                    
                    String dataType = column.getSourceDataTypeName();
                   
                    if (settings.isTimeStampExcluded() && dataType.equalsIgnoreCase("timestamp")) {
                        continue;
                    }
                    // if quoting and not a PostgresDDLGenerator then adding square brackets for SQLServer
                    String columnName = column.getName();
                    columnName = getUpdatedName(isQuoting, columnName,ddlGeneratorClass);
                    logger.debug(" Quoted columnName: "+ columnName);
                    if (column.getSourceColumn() == null) {
                        /** if we have no source table then we will get nulls as
                         * placeholder from the target table.
                         */
                        sourceTable = table;
                        sourceColumn = "null";
                        noMappingForColumn.add(columnName);
                    } else {
                        sourceTable = column.getSourceColumn().getParent();
                        sourceColumn = column.getSourceColumn().getName();
                    }
                    /** if quoting and not a PostgresDDLGenerator then adding square brackets for SQLServer */
                    sourceColumn = getUpdatedName(isQuoting,sourceColumn,ddlGeneratorClass);
                    if(column.getSourceColumn() != null ) {
                        if (!tableMapping.containsKey(sourceTable)) {
                            List<String> pkCols = new ArrayList<String>();
                            List<String> fkCols = new ArrayList<String>();
                            StringBuffer buffer = new StringBuffer();
                            buffer.append("SELECT ");
                            buffer.append(sourceColumn);
                            buffer.append(" AS ").append(columnName);
                            pkCols.add(sourceColumn);
                            fkCols.add(sourceColumn);
                            List<SQLRelationship> exportedKeys = getPlaypenExportedKeys(playPenTableList, sourceTable);
                            for(SQLRelationship exportedKey : exportedKeys) {
                                for (ColumnMapping mapping: exportedKey.getMappings()) {
                                    SQLColumn pkCol = mapping.getPkColumn();
                                    if(pkCol != null && !pkCol.getName().isEmpty()) {
                                        String updatedPkColName = getUpdatedName(isQuoting, pkCol.getName(),ddlGeneratorClass);
                                        if(!pkCols.contains(updatedPkColName) ){
                                            buffer.append(", ").append
                                            (updatedPkColName).append(" AS ").append(updatedPkColName);
                                            pkCols.add(updatedPkColName);
                                        }
                                    }
                                }
                            }
                            List<SQLImportedKey> importedKeys = getPlaypenImportedKeys(playPenTableList, sourceTable);
                            for(SQLImportedKey importedKey : importedKeys) {
                                SQLRelationship relationShip = importedKey.getRelationship();
                                for (ColumnMapping mapping: relationShip.getMappings()) {
                                    SQLColumn fkCol = mapping.getFkColumn();
                                    if(fkCol != null && !fkCol.getName().isEmpty()) {
                                        String updatedFkColName = getUpdatedName(isQuoting, fkCol.getName(),ddlGeneratorClass);
                                        if(!fkCols.contains(updatedFkColName) ){
                                            buffer.append(", ").append
                                            (updatedFkColName).append(" AS ").append(updatedFkColName);
                                            fkCols.add(updatedFkColName);
                                        }
                                    }
                                }
                            }
                            tableMapping.put(sourceTable, buffer);
                        } else {
                            tableMapping.get(sourceTable).append(", ").append
                            (sourceColumn).append(" AS ").append(columnName);
                        }
                    }
                }

                if (tableMapping.containsKey(table)) {
                    if (tableMapping.size() == 1) {
                        noTransTables.add(table.getName());
                        tasksToDo.add("Update table " + table.getName() + " as no source data was found");
                        continue;
                    } else {
                        StringBuffer buffer = new StringBuffer();
                        if(noMappingForColumn.size() > 0) {
                            buffer.append("There is no source for the column(s): ");
                            for (String noMapForCol: noMappingForColumn) {
                                buffer.append(noMapForCol).append(" ");
                            }

                            tasksToDo.add(buffer.toString() + " for the table " + table.getName());
                            transMeta.addNote(new NotePadMeta(buffer.toString(), 0, 150, 125, 125));
                        }
                    }
                }

                for (SQLTable sourceTable: tableMapping.keySet()) {
                    StringBuffer buffer = tableMapping.get(sourceTable);
                    buffer.append(" FROM " + DDLUtils.toQualifiedName(sourceTable));
                }

                for (SQLTable sourceTable: tableMapping.keySet()) {
                    List<String> keys1 = new LinkedList<String>();
                    List<String> keys2 = new LinkedList<String>();
                    List<SQLRelationship> exportedKeys = getPlaypenExportedKeys(playPenTableList, sourceTable);
                    for (SQLRelationship exportedKey : exportedKeys) {
                        for (ColumnMapping mapping: exportedKey.getMappings()) {
                            SQLColumn pkCol = mapping.getPkColumn();
                            SQLColumn fkCol = mapping.getFkColumn();
                            if(pkCol != null && fkCol!= null) {
                                keys1.add(pkCol.getName());
                                keys2.add(fkCol.getName());
                            }
                        }
                    }

                    keyFields1.add(keys1.toArray(new String[keys1.size()]));
                    keyFields2.add(keys2.toArray(new String[keys2.size()]));
                    JDBCDataSource source = sourceTable.getParentDatabase().getDataSource();
                    DatabaseMeta databaseMeta = addDatabaseConnection(databaseNames, source);
                    transMeta.addDatabase(databaseMeta);

                    TableInputMeta tableInputMeta = new TableInputMeta();
                    String stepName = databaseMeta.getName() + ":" + DDLUtils.toQualifiedName(sourceTable);
                    StepMeta stepMeta = new StepMeta("TableInput", stepName, tableInputMeta);
                    stepMeta.setDraw(true);
                    stepMeta.setLocation(inputSteps.size()==0?spacing:(inputSteps.size())*spacing, (inputSteps.size()+1)*spacing);
                    tableInputMeta.setDatabaseMeta(databaseMeta);
                    tableInputMeta.setSQL(tableMapping.get(sourceTable).toString());
                    transMeta.addStep(stepMeta);
                    //sort the Rows
                    StepMeta sortedStepMeta = createSortRowsStep(transMeta,stepMeta,keyFields1);
                    transMeta.addStep(sortedStepMeta);
                    inputSteps.add(sortedStepMeta);
                }

                List<StepMeta> mergeSteps = createMergeJoins(settings.getJoinType(), transMeta, inputSteps,keyFields1, keyFields2);
                StepMeta outputStepMeta = null; 
                if (settings.isInsertUpdate()) {
                    InsertUpdateMeta insertUpdateMeta = new InsertUpdateMeta();
                    insertUpdateMeta.setDefault();
                    insertUpdateMeta.setDatabaseMeta(targetDatabaseMeta);
                    insertUpdateMeta.setTableName(table.getName());
                    insertUpdateMeta.setSchemaName(settings.getSchemaName());
                    outputStepMeta = new StepMeta("InsertUpdate", "Insert/Update " + table.getName(), insertUpdateMeta);

                } else {
                    TableOutputMeta tableOutputMeta = new TableOutputMeta();
                    tableOutputMeta.setDatabaseMeta(targetDatabaseMeta);
                    tableOutputMeta.setTablename(table.getName());
                    tableOutputMeta.setSchemaName(settings.getSchemaName());
                    outputStepMeta = new StepMeta("TableOutput", "Output to " + table.getName(), tableOutputMeta);
                    
                }
                if(outputStepMeta != null) {
                    outputStepMeta.setDraw(true);
                    outputStepMeta.setLocation((inputSteps.size()+2)*spacing, (inputSteps.size())*spacing);
                    transMeta.addStep(outputStepMeta);
                }
                if (inputSteps.size() > 1 ) {
                    TransHopMeta transHopMeta = 
                            new TransHopMeta(mergeSteps.isEmpty()?inputSteps.get(0):mergeSteps.get(mergeSteps.size()-1), outputStepMeta);
                    transMeta.addTransHop(transHopMeta);
                    transformations.add(transMeta);
                    logger.debug("Added a Trnasformation job for table "+table.getName());
                }
            }
            if (monitor.isCancelled()) {
                cancel();
                return;
            }
            if (!noTransTables.isEmpty()) {
                StringBuffer buffer = new StringBuffer();
                buffer.append("Transformations were not created for ");
                for (String tableName : noTransTables) {
                    buffer.append(tableName).append(" ");
                }
                buffer.append(" as the tables had no source information.");
                jm.addNote(new NotePadMeta(buffer.toString(), 0, 0, 125, 125));
            }

            //Start entry
            JobEntryCopy startEntry = new JobEntryCopy();
            JobEntrySpecial start = new JobEntrySpecial("Start", true, false);
            startEntry.setEntry(start);
            startEntry.setLocation(10, spacing);
            startEntry.setDrawn();
            jm.addJobEntry(startEntry);

            JobEntryCopy oldJobEntry = null;
            int i = 1;
            for (TransMeta transformation : transformations) {
                JobEntryCopy entry = new JobEntryCopy();
                JobEntryTrans trans = new JobEntryTrans(transformation.getName());
                //trans.setJobEntryType(JobEntryType.TRANSFORMATION);
                trans.setTransname(transformation.getName());
                entry.setEntry(trans);
                entry.setLocation(i*spacing, spacing);
                entry.setDrawn();
                jm.addJobEntry(entry);
                if (oldJobEntry != null) {
                    JobHopMeta hop = new JobHopMeta(oldJobEntry, entry);
                    jm.addJobHop(hop);
                } else {
                    JobHopMeta hop = new JobHopMeta(startEntry, entry);
                    jm.addJobHop(hop);
                }
                oldJobEntry = entry;
                i++;
            }

            // Success entry

            JobEntrySuccess success = new JobEntrySuccess();
            success.setName("Success");
            JobEntryCopy successEntry = new JobEntryCopy();
            successEntry.setEntry(success);
            successEntry.setLocation(i*spacing, spacing);
            successEntry.setDrawn();
            jm.addJobEntry(successEntry);
            if(oldJobEntry != null) {
                JobHopMeta hop = new JobHopMeta(oldJobEntry, successEntry);
                jm.addJobHop(hop);
            }
            if (monitor.isCancelled()) {
                cancel();
                return;
            }
            String jobname = settings.getJobName();
            if (settings.isSplittingJob() && getJob_no() > 0) {
                 jobname += "_"+getJob_no();
            }
            jm.setName(jobname);
            logger.debug("setting job name: "+jobname);
            if (settings.isSavingToFile()) {
                outputToXML(transformations, jm);
            } else {
                jm.setDirectory(new RepositoryDirectory());
                outputToRepository(jm, transformations, createRepository());
            }
        } finally {
            monitor.setFinished(true);
        }
    }
    
    /**
     * Method used to split the big job into multiple sub jobs based on sublist.
     * @param tableList 
     *              list of tables to split in a sublist 
     * @param targetDB
     *              target Database
     * @throws Exception
     */
    public void doSplitedJobExport(List<SQLTable> tableList, SQLDatabase targetDB ) throws Exception {
        if (settings.isSplittingJob() && tableList.size() > settings.getSplitJobNo()) {
            jobMetaList.clear();
            // split a big job into small a jobs of splitno of (table)transformation/job
            Map<String, List<String>> splijobMap = settings.getSplitMap();
            for(List<String> job: splijobMap.values()) {
                List<SQLTable> newtableList = getSQLTable(tableList,job);
                job_no++;
                doExport(newtableList,targetDB,tableList);
            }
            try {
                EnvUtil.environmentInit();
                LogWriter lw = LogWriter.getInstance();
                JobMeta jm = new JobMeta(lw);
                //Start Entry
                JobEntryCopy startEntry = new JobEntryCopy();
                JobEntrySpecial start = new JobEntrySpecial("Start", true, false);
                startEntry.setEntry(start);
                startEntry.setLocation(10, spacing);
                startEntry.setDrawn();
                jm.addJobEntry(startEntry);


                JobEntryCopy oldJobEntry = null;
                int i = 1;
                for (JobMeta job : jobMetaList) {
                    JobEntryCopy entry = new JobEntryCopy();
                    JobEntryJob jn = new JobEntryJob(job.getName());
                    jn.setJobName(job.getName());
                    entry.setEntry(jn);
                    entry.setLocation(i*spacing, spacing);
                    entry.setDrawn();
                    jm.addJobEntry(entry);
                    if (oldJobEntry != null) {
                        JobHopMeta hop = new JobHopMeta(oldJobEntry, entry);
                        jm.addJobHop(hop);
                    } else {
                        JobHopMeta hop = new JobHopMeta(startEntry, entry);
                        jm.addJobHop(hop);
                    }
                    oldJobEntry = entry;
                    i++;
                }

//                // Success entry
                JobEntrySuccess success = new JobEntrySuccess();
                success.setName("Success");
                JobEntryCopy successEntry = new JobEntryCopy();
                successEntry.setEntry(success);
                successEntry.setLocation(i*spacing, spacing);
                successEntry.setDrawn();
                jm.addJobEntry(successEntry);
              
                JobHopMeta hop = new JobHopMeta(oldJobEntry, successEntry);
                jm.addJobHop(hop);
                
                if (monitor.isCancelled()) {
                    cancel();
                    return;
                }

                jm.setName(settings.getJobName());
                job_no=0;
                if (settings.isSavingToFile()) {
                    jobOutputToXML(jobMetaList, jm);
                } else {
                    jm.setDirectory(new RepositoryDirectory());
                    jobOutputToRepository(jm, jobMetaList, createRepository());
                }
            } finally {
                job_no=0;
            }
            
        } else {
            doExport(tableList,targetDB,tableList);
        }
    }
    
 private List<SQLTable> getSQLTable(List<SQLTable> tableList, List<String> job) {
     List<SQLTable> jobList =  new ArrayList<SQLTable>();
     for(SQLTable table: tableList) {
         if(job.contains(table.getName()) /* && !jobList.contains(table)*/) {
             jobList.add(table);
         }
     }
        return jobList;
    }

private void jobOutputToRepository(JobMeta jm, List<JobMeta> jmList, Repository createRepository) {
        // TODO Auto-generated method stub
        
    }

private void jobOutputToXML(List<JobMeta> jmList, JobMeta jm) throws IOException {
    Map<File, String> outputs = new LinkedHashMap<File, String>();
    
   
    //This sets the location of the transformations in the job
    //The first entry is not a transformation so skip it
    //This is done here so we know where the files are being saved and that they are saved
    for (int i = 1; i < jm.nrJobEntries() -1; i++) {
        JobEntryJob jobs = (JobEntryJob)(jm.getJobEntry(i).getEntry());
        jobs.setFileName(getJobFilePath(jobs.getName()));
        logger.debug("jobOutputToXML::jobs fileName: "+jobs.getFileName());
    }

    String fileName = settings.getFilePath() ;
    if (!fileName.toUpperCase().endsWith(".KJB")) {
        fileName += ".kjb";
    }
    jm.setFilename(fileName);
    outputs.put(new File(fileName), jm.getXML());
    if (getJob_no() > 0) {
        jobMetaList.add(jm);
    }
    UserPrompter up = session.createUserPrompter(
            "The file {0} already exists. Overwrite?", UserPromptType.BOOLEAN, UserPromptOptions.OK_NOTOK_CANCEL, UserPromptResponse.NOT_OK, 
            false, "Overwrite", "Don't Overwrite", "Cancel");
    for (File f : outputs.keySet()) {
        try {
            logger.debug("jobOutputToXML :: The file to output is " + f.getPath());
            if (f.exists()) {
                UserPromptResponse overwriteOption = up.promptUser(f.getAbsolutePath());
                if (overwriteOption == UserPromptResponse.OK) {
                    f.delete();
                } else if (overwriteOption == UserPromptResponse.NOT_OK) {
                    continue;
                } else if (overwriteOption == UserPromptResponse.CANCEL) {
                    cancel();
                    return;
                } else {
                    throw new IllegalStateException(
                            "Unknown response value from user prompt: " + overwriteOption);
                }
            }
            f.createNewFile();
            BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new
                    FileOutputStream(f), "utf-8"));
            out.write(outputs.get(f));
            out.flush();
            out.close();
            monitor.setProgress(monitor.getProgress() + 1);
            
            if (monitor.isCancelled()) {
                cancel();
                return;
            }
        } catch (IOException er) {
            tasksToDo.clear();
            tasksToDo.add("File " + f.getName() + " was not created");
            throw er;
        }
    }
        
    }

private String getJobFilePath(String jobName) {
    String parentPath = new File(settings.getFilePath()).getParentFile().getPath();
    logger.debug("Parent file path is " + parentPath +" for job");
    return new File(parentPath,  jobName + ".kjb").getPath();
}

// chops a list into sublists of length L
    static <T> List<List<SQLTable>> chopped(List<SQLTable> list, final int L) {
        List<List<SQLTable>> parts = new ArrayList<List<SQLTable>>();
        final int N = list.size();
        for (int i = 0; i < N; i += L) {
            parts.add(new ArrayList<SQLTable>(
                list.subList(i, Math.min(N, i + L)))
            );
        }
        return parts;
    }

    /**
     * This is a helper method that sets the taskToDo list with
     * the correct message to display.
     */
    private void cancel() {
        tasksToDo.clear();
        tasksToDo.add("The Kettle job was cancelled so some files may be missing.");
    }
    
    /**
     * This method adds the given data source to the returned database metadata and
     * the databaseNames mapping if it does not already exist in the databaseNames.
     * This method is package private for testing
     */
    DatabaseMeta addDatabaseConnection(Map<String, DatabaseMeta> databaseNames, JDBCDataSource dataSource) throws RuntimeException {
        DatabaseMeta databaseMeta;
        if (!databaseNames.containsKey(dataSource.getName())) {
            try {
                databaseMeta = KettleUtils.createDatabaseMeta(dataSource);
                try {
                    Connection conn = dataSource.createConnection();
                    conn.close();
                } catch (SQLException e) {
                    logger.info("Could not connect to the database " + dataSource.getName() + ".");
                    tasksToDo.add("Check that the database " + dataSource.getName() + " can be connected to.");
                }
            } catch (RuntimeException re) {
                String databaseName = dataSource.getName();
                logger.error("Could not create the database connection for " + databaseName + ".");
                re.printStackTrace();
                tasksToDo.clear();
                tasksToDo.add("The Kettle job was not created as the database connection for " + databaseName + " could not be created.");
                throw re;
            }
            databaseNames.put(dataSource.getName(), databaseMeta);
        } else {
            databaseMeta = databaseNames.get(dataSource.getName());
        }
        return databaseMeta;
    }
    
    /**
     * This method outputs the xml for the given transformations and job to
     * files. The path class variable and parent file must be set before using
     * this method. The file is overwritten or not depending on the user
     * prompter provided by the ArchitectSession. This method is exposed as
     * package private for testing purposes.
     */
    void outputToXML(List<TransMeta> transformations, JobMeta job) throws IOException {
        Map<File, String> outputs = new LinkedHashMap<File, String>();
        
        for (TransMeta transMeta : transformations) {
            File file = new File(getTransFilePath(transMeta.getName()));
            logger.debug("transformation file: "+file.getAbsolutePath());
            transMeta.setFilename(file.getName());
            try {
                outputs.put(file, transMeta.getXML());
            } catch (KettleException e) {
                throw new RuntimeException(e);
            }
            if (monitor.isCancelled()) {
                cancel();
                return;
            }
        }
        
        //This sets the location of the transformations in the job
        //The first entry is not a transformation so skip it
        //This is done here so we know where the files are being saved and that they are saved
        for (int i = 1; i < job.nrJobEntries()-1; i++) {
            JobEntryTrans trans = (JobEntryTrans)(job.getJobEntry(i).getEntry());
            trans.setFileName(getTransFilePath(trans.getName()));
        }

        String fileName = settings.getFilePath() ;
        if (settings.isSplittingJob() && getJob_no() > 0) {
            String parentPath = new File(settings.getFilePath()).getParentFile().getPath();
            fileName = parentPath+"/"+job.getName()+".kjb";
        }
        if (!fileName.toUpperCase().endsWith(".KJB")) {
            
            fileName += ".kjb";
        }
        job.setFilename(fileName);
        outputs.put(new File(fileName), job.getXML());
        if (settings.isSplittingJob() && getJob_no() > 0) {
            jobMetaList.add(job);
        }
        UserPrompter up = session.createUserPrompter(
                "The file {0} already exists. Overwrite?", UserPromptType.BOOLEAN, UserPromptOptions.OK_NOTOK_CANCEL, UserPromptResponse.NOT_OK, 
                false, "Overwrite", "Don't Overwrite", "Cancel");
        for (File f : outputs.keySet()) {
            try {
                logger.debug("outputToXML :: The file to output is " + f.getPath());
                if (f.exists()) {
                    UserPromptResponse overwriteOption = up.promptUser(f.getAbsolutePath());
                    if (overwriteOption == UserPromptResponse.OK) {
                        f.delete();
                    } else if (overwriteOption == UserPromptResponse.NOT_OK) {
                        continue;
                    } else if (overwriteOption == UserPromptResponse.CANCEL) {
                        cancel();
                        return;
                    } else {
                        throw new IllegalStateException(
                                "Unknown response value from user prompt: " + overwriteOption);
                    }
                }
                f.createNewFile();
                BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new
                        FileOutputStream(f), "utf-8"));
                out.write(outputs.get(f));
                out.flush();
                out.close();
                monitor.setProgress(monitor.getProgress() + 1);
                
                if (monitor.isCancelled()) {
                    cancel();
                    return;
                }
            } catch (IOException er) {
                tasksToDo.clear();
                tasksToDo.add("File " + f.getName() + " was not created");
                throw er;
            }
        }
    }
    
    /**
     * This returns the full path and file name for the given transformation name.
     * The file location of the transformations is based on the location of the job.
     */
    String getTransFilePath(String transName) {
        String parentPath = new File(settings.getFilePath()).getParentFile().getPath();
        logger.debug("Parent file path is " + parentPath);
        return new File(parentPath, "transformation_for_table_" + transName + ".ktr").getPath();
    }
    
    /** 
     * Pass the repository a connection straight as the Repository
     * connect method loads its own drivers and we don't want to
     * include them.
     */
    public void createStraightConnection(Repository repo)  throws KettleException, SQLException {
        repo.getDatabase().setConnection(settings.getRepository().createConnection());
    }

    /**
     * This method translates the list of SQLTables to a Kettle Job and Transformations and saves 
     * them into a Kettle repository.
     * @throws KettleException 
     */
    void outputToRepository(JobMeta jm, List<TransMeta> transformations, Repository repo) throws KettleException, SQLException {
        
        try {
            createStraightConnection(repo);
            
            RepositoryDirectory directory;
            
            try {
                // We refresh the repository tree as we are passing a connection
                // straight. The Repository connect method does this automatically
                // but we now need to do it manually.
                repo.refreshRepositoryDirectoryTree();

                directory = dirChooser.selectDirectory(repo);
                if (directory == null) {
                    throw new KettleException("The directory of the repository was not available.");
                }
            } catch (KettleException e) {
                tasksToDo.clear();
                tasksToDo.add("The directory of the repository was not available.");
                throw e;
            } 

            try {
                UserPrompter up = session.createUserPrompter(
                        "{0} {1} already exists in the repository. Replace?", UserPromptType.BOOLEAN, UserPromptOptions.OK_NOTOK_CANCEL,
                        UserPromptResponse.NOT_OK, false, "Replace", "Don't Replace", "Cancel");
                for (TransMeta tm: transformations) {
                    if (monitor.isCancelled()) {
                        cancel();
                        return;
                    }
                    tm.setDirectory(directory);
                    
                    // check for existing transaction having same name
                    long id = repo.getTransformationID(tm.getName(), directory.getID());
                    if (id >= 0) {
                        logger.debug("We found a transformation with the same name, the id is " + id);
                        UserPromptResponse overwriteOption = up.promptUser("Transformation", tm.getName());
                        
                        if (overwriteOption == UserPromptResponse.OK) {
                            // will fall through and overwrite
                        } else if (overwriteOption == UserPromptResponse.NOT_OK) {
                            monitor.setProgress(monitor.getProgress() + 1);
                            continue;
                        } else if (overwriteOption == UserPromptResponse.CANCEL) {
                            cancel();
                            break;
                        } else {
                            throw new IllegalStateException(
                                    "Unknown user prompt response: " + overwriteOption);
                        }
                    }
                    tm.saveRep(repo);
                    monitor.setProgress(monitor.getProgress() + 1);
                    logger.debug("Progress is " + monitor.getProgress() + " out of " + monitor.getJobSize());
                }
                if (monitor.isCancelled()) {
                    cancel();
                    return;
                }

                //This sets the location of the transformations in the job
                //The first entry is not a transformation so skip it
                //This is done here so we know where the files are being saved and that they are saved
                for (int i = 1; i < jm.nrJobEntries(); i++) {
                    JobEntryTrans trans = (JobEntryTrans) (jm.getJobEntry(i).getEntry());
                    trans.setDirectory(directory.getDirectoryName());
                    trans.setDirectoryPath(directory.getPath());
                }

                jm.setDirectory(directory);
                if (repo.getTransformationID(jm.getName(), directory.getID()) >= 0) {
                    UserPromptResponse overwriteOption = up.promptUser("Job", jm.getName());
                    if (overwriteOption == UserPromptResponse.OK) {
                        // will fall through and overwrite
                    } else if (overwriteOption == UserPromptResponse.NOT_OK) {
                        return;
                    } else if (overwriteOption == UserPromptResponse.CANCEL) {
                        cancel();
                        return;
                    } else {
                        throw new IllegalStateException(
                                "Unknown user prompt response: " + overwriteOption);
                    }
                }
                jm.saveRep(repo);
                monitor.setProgress(monitor.getProgress() + 1);
                logger.debug("Progress is " + monitor.getProgress() + " out of " + monitor.getJobSize());
            } catch (KettleException e) {
                tasksToDo.clear();
                tasksToDo.add("Kettle job " + jm.getName() + " failed to save to respitory due to a Kettle error.");
                throw e;
            }
        } catch (SQLException e) {
            tasksToDo.clear();
            tasksToDo.add("Kettle job " + jm.getName() + " failed to save to respitory due to a SQL error.");
            throw e;
        } finally {
            repo.disconnect();
        }
    }
    
    /**
     * This method creates a Kettle Repository instance that refers to the
     * existing Kettle repository schema referred to by the {@link ca.sqlpower.architect.etl.kettle.KettleSettings#repository}
     * database connection.  This method does not attempt to actually make
     * a database connection itself; the returned Repository will attempt
     * the connection by itself later on.
     */
    Repository createRepository() {

        DatabaseMeta kettleDBMeta = KettleUtils.createDatabaseMeta(settings.getRepository());
        RepositoryMeta repoMeta = new RepositoryMeta("", "", kettleDBMeta);

        UserInfo userInfo = new UserInfo(settings.getRepository().get(KettleOptions.KETTLE_REPOS_LOGIN_KEY),
                settings.getRepository().get(KettleOptions.KETTLE_REPOS_PASSWORD_KEY),
                settings.getJobName(), "", true, null);
        LogWriter lw = LogWriter.getInstance(); // Repository constructor needs this for some reason

        Repository repo = new Repository(lw, repoMeta, userInfo);
        return repo;

    }
    
    /**
     * This method returns the necessary data to create a repository, but doesn't actually
     * create it so that we create a testing repository.
     */
    public Object[] createTestRepository() {

        DatabaseMeta kettleDBMeta = KettleUtils.createDatabaseMeta(settings.getRepository());
        RepositoryMeta repoMeta = new RepositoryMeta("", "", kettleDBMeta);

        UserInfo userInfo = new UserInfo(settings.getRepository().get(KettleOptions.KETTLE_REPOS_LOGIN_KEY),
                settings.getRepository().get(KettleOptions.KETTLE_REPOS_PASSWORD_KEY),
                settings.getJobName(), "", true, null);
        LogWriter lw = LogWriter.getInstance(); // Repository constructor needs this for some reason

        Object[] ret = new Object[3];
        ret[0] = (Object)lw;
        ret[1] = repoMeta;
        ret[2] = userInfo;
        return ret;

    }

    /**
     * Create a steps to Sort the Rows and add it to transMeta
     * @param transMeta
     * @param inputStep
     * @return stepMeta
     */
    private StepMeta createSortRowsStep(TransMeta transMeta, StepMeta inputStep, List<String[]> sortKeys) {
        StepMeta stepMeta = null ;

        SortRowsMeta sortRowsMeta = new SortRowsMeta();
        sortRowsMeta.setDefault();
        //adding sort Key
        if (!sortKeys.isEmpty()) {
            String[] sortFields = sortKeys.get(0);
            //get the 1st key only
            if(sortFields.length > 1) {
                String str =  sortFields[0];
                sortFields = new String[] {str};
            }
            boolean[] ascendingFields = new boolean[sortFields.length];
            boolean[] caseSensitive = new boolean[sortFields.length];
            for (int i =0 ;i<sortFields.length ; i++) {
                ascendingFields[i] = true;
                caseSensitive[i] = false;
            }
            sortRowsMeta.setFieldName(sortFields);
            sortRowsMeta.setAscending(new boolean[] {true});
            sortRowsMeta.setCaseSensitive(new boolean[] {false});
        }

        Point location = inputStep.getLocation();
        stepMeta = new StepMeta("SortRows", "SortRows for " + inputStep.getName(), sortRowsMeta );
        stepMeta.setDraw(true);
        stepMeta.setLocation(location.x*2, location.y);
        transMeta.addStep(stepMeta);
        // add the hop
        TransHopMeta transHopMeta = new TransHopMeta(inputStep, stepMeta);
        transMeta.addTransHop(transHopMeta);
        return stepMeta;
    }
    
    /**
     * This creates all of the MergeJoin kettle steps as well as their hops from
     * the steps in the inputSteps list. The MergeJoin steps are also put into the 
     * TransMeta. This method is package private for testing purposes.
     */
    List<StepMeta> createMergeJoins(int defaultJoinType, TransMeta transMeta, List<StepMeta> inputSteps, List<String[]> keyField1, List<String[]> keyField2) {
        List<StepMeta> mergeSteps = new ArrayList<StepMeta>();
        if (inputSteps.size() > 1) {
            MergeJoinMeta mergeJoinMeta = new MergeJoinMeta();
            mergeJoinMeta.setJoinType(MergeJoinMeta.join_types[defaultJoinType]);
            mergeJoinMeta.setStepName1(inputSteps.get(0).getName());
            mergeJoinMeta.setStepMeta1(inputSteps.get(0));
            mergeJoinMeta.setStepName2(inputSteps.get(1).getName());
            mergeJoinMeta.setStepMeta2(inputSteps.get(1));
            //add merge join key
            if(!keyField1.isEmpty()) {
                String[] keyField_1 = keyField1.get(0);
                logger.debug("Key_Field1 :"+Arrays.toString(keyField_1));
                //get the first key only
                if(keyField_1.length >1) {
                    String str =  keyField_1[0];
                    keyField_1 = new String[] {str};
                }
                mergeJoinMeta.setKeyFields1(keyField_1);
            }
            if(!keyField2.isEmpty()) {
                String[] keyField_2 = keyField2.get(0);
                logger.debug("Key_Field2 :"+Arrays.toString(keyField_2));
               	//get the first key only
                if(keyField_2.length >1) {
                    String str =  keyField_2[0];
                    keyField_2 = new String[] {str};
                }
                mergeJoinMeta.setKeyFields2(keyField_2);
            }
            logger.debug("MergeJoin Join tables " +
                    inputSteps.get(0).getName() + " and " + 
                    inputSteps.get(1).getName());
            //removing the text 'SortRows for' form the previous inputsets(sortRows steps) to make the name more meaning for merge join.
            String table1_name = inputSteps.get(0).getName();
            table1_name = table1_name.replaceFirst("SortRows for ","");
            String table2_name = inputSteps.get(0).getName();
            table2_name = table2_name.replaceFirst("SortRows for ","");
            StepMeta stepMeta = new StepMeta("MergeJoin", "Join tables " +
                    table1_name + " and " + table2_name, mergeJoinMeta);
            stepMeta.setDraw(true);
            stepMeta.setLocation(3*spacing, new Double(1.5*spacing).intValue());
            transMeta.addStep(stepMeta);
            mergeSteps.add(stepMeta);
            TransHopMeta transHopMeta = new TransHopMeta(inputSteps.get(0), stepMeta);
            transMeta.addTransHop(transHopMeta);
            transHopMeta = new TransHopMeta(inputSteps.get(1), stepMeta);
            transMeta.addTransHop(transHopMeta);
            //commenting it disable the final hop. So when user open transformation in Pentaho they received warning about hop
            tasksToDo.add("Verify the merge join " + stepMeta.getName() + " does the correct merge.");
        }

        for (int i = 0; i < inputSteps.size()-2; i++) {
            MergeJoinMeta mergeJoinMeta = new MergeJoinMeta();
            mergeJoinMeta.setJoinType(MergeJoinMeta.join_types[defaultJoinType]);
            mergeJoinMeta.setStepName1(mergeSteps.get(i).getName());
            mergeJoinMeta.setStepMeta1(mergeSteps.get(i));
            mergeJoinMeta.setStepName2(inputSteps.get(i+2).getName());
            mergeJoinMeta.setStepMeta2(inputSteps.get(i+2));
            if(!keyField1.isEmpty()) {
                String[] keyField_1 = keyField1.get(i+2);
                //get the first key only
                if(keyField_1.length >1) {
                    String str =  keyField_1[0];
                    keyField_1 = new String[] {str};
                }
                mergeJoinMeta.setKeyFields1(keyField_1);
            }
            if(!keyField2.isEmpty()) {
                String[] keyField_2 = keyField2.get(i+2);
                //get the first key only
                if(keyField_2.length >1) {
                    String str =  keyField_2[0];
                    keyField_2 = new String[] {str};
                }
                mergeJoinMeta.setKeyFields2(keyField_2);
            }
            String table1_name = inputSteps.get(i+2).getName();
            table1_name = table1_name.replaceFirst("SortRows for ","");
            StepMeta stepMeta = new StepMeta("MergeJoin", "Join table " + table1_name, mergeJoinMeta);
            stepMeta.setDraw(true);
            stepMeta.setLocation((i + 4) * spacing, new Double((i + 2.25) * spacing).intValue());
            transMeta.addStep(stepMeta);
            mergeSteps.add(stepMeta);
            TransHopMeta transHopMeta = new TransHopMeta(mergeSteps.get(i), stepMeta);
            transMeta.addTransHop(transHopMeta);
            transHopMeta = new TransHopMeta(inputSteps.get(i+2), stepMeta);
            transMeta.addTransHop(transHopMeta);
            //commenting it disable the final hop. So when user open transformation in Pentaho they received warning about hop
            tasksToDo.add("Verify the merge join " + stepMeta.getName() + " does the correct merge.");
        }
        return mergeSteps;
    }
    
    public String getFilePath() {
        return settings.getFilePath();
    }
    public void setFilePath(String filePath) {
        settings.setFilePath(filePath);
    }
    public String getJobName() {
        return settings.getJobName();
    }
    public void setJobName(String jobName) {
       settings.setJobName(jobName);
    }
    public int getKettleJoinType() {
        return settings.getJoinType();
    }
    public void setKettleJoinType(int kettleJoinType) {
        settings.setJoinType(kettleJoinType);
    }
    public String getSchemaName() {
        return settings.getSchemaName();
    }
    public void setSchemaName(String schemaName) {
        settings.setSchemaName(schemaName);
    }
 
    public boolean isTimeStampExcluded() {
        return settings.isTimeStampExcluded();
    }
 
    public void setTimeStampExcluded(boolean isTimeStampExcluded) {
        settings.setTimeStampExcluded(isTimeStampExcluded);
    }

    public List<String> getTasksToDo() {
        return tasksToDo;
    }

    public Integer getJobSize() {
        return monitor.getJobSize();
    }

    public String getMessage() {
        return monitor.getMessage();
    }

    public int getProgress() {
        return monitor.getProgress();
    }

    public boolean hasStarted() {
        return monitor.hasStarted();
    }

    public boolean isFinished() {
        return monitor.isFinished();
    }

    public void setCancelled(boolean cancelled) {
        monitor.setCancelled(cancelled);
    }

    public boolean isCancelled() {
        return monitor.isCancelled();
    }
    
    public boolean isSavingToFile() {
        return settings.isSavingToFile();
    }
    public void setSavingToFile(boolean savingToFile) {
        settings.setSavingToFile(savingToFile);
    }

    public void setRepository(JDBCDataSource source) {
        settings.setRepository(source);
    }

    public void setRepositoryDirectoryChooser(KettleRepositoryDirectoryChooser chooser) {
        dirChooser = chooser;
    }

    public SPDataSource getRepository() {
        return settings.getRepository();
    }
    
    public int getSplitJobNo() {
        return settings.getSplitJobNo();
    }
  

    public void setSplitJobNo(int newValue) {
        settings.setSplitJobNo(newValue);
    }

    /**
     * return the current job no
     */
    private int getJob_no() {
        return job_no;
    }
    
    public boolean isSplittingJob() {
        return settings.isSplittingJob();
    }
 
    public void setSplittingJob(boolean newValue) {
        settings.setSplittingJob(newValue);
    }
    
    public boolean isInsertUpdate() {
        return settings.isInsertUpdate();
    }
    
    public void setIsInsertUpdate(boolean newValue) {
        settings.setIsInsertUpdate(newValue);
    }
    
    public Map<String, List<String>> getSplitMap() {
        return settings.getSplitMap();
    }

 
    public void setSplitMap(Map<String, List<String>> newSplitMap) {
        settings.setSplitMap(newSplitMap);
    }
    
    /**
     * Method to get the latest exported keys in a playpen.
     * Exported keys are different for table in Database then table in PlayPen 
     * when user create new Relationship manually which doesn't exists in database table.
     * When user create a kettle job with tables in a Playpen, user might create 
     * new relationship which doesn't exists in database and limited to playpen only.
     * So getting the correct/latest exported keys from the relationship from the table whose parent is PlayPen.
     * Note: Even though the table is dragged from database tree it can have different exported keys 
     * (after dragging it in the playpen)specially when user create new relationship manually.
     * @param tableList
     *      List of Tables in a playpen
     * @param sourceTable
     *          Table whose parent is Database(dbo)
     * @return
     */
    private List<SQLRelationship> getPlaypenExportedKeys(List<SQLTable> tableList, SQLTable  sourceTable) {
        List<SQLRelationship> exportedKeys = new ArrayList<>();
        SQLTable playpenTable = null;
        for(SQLTable pTable :tableList) {
            if (sourceTable.getName().equalsIgnoreCase(pTable.getName())) {
                playpenTable = pTable; 
                break;
            }
        }
        if(playpenTable != null) {
            try {
                exportedKeys = playpenTable.getExportedKeys();
            } catch (SQLObjectException e) {
                e.printStackTrace();
            }
        }
        return exportedKeys;
    }

    /**
     * Method to get the latest imported keys in a playpen.
     * @param tableList
     *      List of Tables in a playpen
     * @param sourceTable
     *      Table whose parent is Database(dbo)
     * @return
     */
    private List<SQLImportedKey> getPlaypenImportedKeys(List<SQLTable> tableList, SQLTable sourceTable) {
        List<SQLImportedKey> importedKeys = new ArrayList<>();
        SQLTable playpenTable = null;
        for(SQLTable pTable :tableList) {
            if (sourceTable.getName().equalsIgnoreCase(pTable.getName())) {
                playpenTable = pTable; 
                break;
            }
        }
        if(playpenTable != null) {
            try {
                importedKeys = playpenTable.getImportedKeys();
            } catch (SQLObjectException e) {
                e.printStackTrace();
            }
        }
        return importedKeys;
    }

    /**
     * Get the updated Colum,n name based on 
     * @param isQuoting
     * @param columnName
     * @param ddlGeneratorClass
     * @return
     */
    private String getUpdatedName(boolean isQuoting, String columnName, String ddlGeneratorClass) {
        if ((isQuoting) && ddlGeneratorClass!= null && !ddlGeneratorClass.equals(PostgresDDLGenerator.class.getName())
                && !(columnName.startsWith("[") && columnName.endsWith("]"))) {
            columnName = "["+columnName+"]";
        }  else if ((isQuoting) && !(columnName.startsWith("\"") && columnName.endsWith("\""))) { 
            //else if quoting for PostgresDDLGenerator
            columnName = "\""+columnName+"\"";
        }
        return columnName;
    }
} public interface ProfileManager extends SPObject {

    /**
     * A client property defined on an object to count how many profiles
     * of the SQLObject this profile manager currently holds.
     */
    public String PROFILE_COUNT_PROPERTY = "profileCount";

    /**
     * Returns an unmodifiable snapshot of the list of all TableProfileResults that this
     * ProfileManager keeps track of. You may get multiple ProfileResults
     * for one table since you are allowed to have multiple profiles of a
     * single table that come from different times the profiling was done.
     */
    List<TableProfileResult> getResults();

    /**
     * Returns an unmodifiable list of all TableProfileResults for the given
     * table. You may get multiple ProfileResults for one table since you are
     * allowed to have multiple profiles of a single table that come from
     * different times the profiling was done.  Returns an empty list if there
     * are no profiles for the given table.
     */
    List<TableProfileResult> getResults(SQLTable t);

    /**
     * Creates a new profile result for the given table, and adds it to
     * this ProfileManager.  The act of adding a profile causes this
     * ProfileManager to fire a profileAdded event.
     * <p>
     * This method operates synchronously, so it will not return until the
     * profile has been created (which could be several seconds to several
     * minutes).  For use from a Swing GUI, consider
     * {@link #asynchCreateProfiles(Collection)}.
     *
     * @param table The database table(s) you want to profile.
     */
    public TableProfileResult createProfile(SQLTable table) throws SQLException, SQLObjectException;

    /**
     * Creates TableProfileResult objects for each of the tables in the
     * given list, then adds them to this ProfileManager in an unpopulated
     * state.  Then starts a new worker thread which will populate the results
     * one after the other.  It is likely that none of the profiles will be
     * populated yet by the time this method returns.
     */
    public Collection<Future<TableProfileResult>> asynchCreateProfiles(Collection<SQLTable> tables);

    /**
     * Schedules a profile to be populated on a separate worker thread. You will
     * not normally need to call this method, since it is done for you by
     * {@link #asynchCreateProfiles(Collection)}. The use case is for re-trying
     * profile results that have been canceled by the user before they managed
     * to fully populate.
     * 
     * @param result
     *            The result object to reschedule for profiling. It must already
     *            be owned by this profile manager, and must not be in the
     *            process of profiling, and also not already finished profiling.
     */
    public Future<TableProfileResult> scheduleProfile(TableProfileResult result);
    
    /**
     * Removes a single TableProfileResult from the List of TableProfileResults
     * that this ProfileManager keeps track of. If the remove was successful then
     * this method will fire a ProfileRemoved event and return true.
     */
    public boolean removeProfile(TableProfileResult victim);

    /**
     * Removes all contents of the list of TableProfileResults that this
     * ProfileManager keeps track of and fires a ProfileChanged event.
     */
    public void clear();
    
    /**
     * Returns the current default settings for new profile runs in this Profile Manager.
     */
    public ProfileSettings getDefaultProfileSettings();
    
    /**
     * Sets the defaults for new profiles created by this profile manager.
     */
    public void setDefaultProfileSettings(ProfileSettings settings);
    
    /**
     * Adds a ProfileChangeListener to this ProfileManager.
     */
    public void addProfileChangeListener(ProfileChangeListener listener);

    /**
     * Removes a ProfileChangeListener to this ProfileManager.
     */
    public void removeProfileChangeListener(ProfileChangeListener listener);

    /**
     * Modifies the order in which profile results will be calculated.
     * 
     * @param tpr A list of pending profile results that were already
     * part of this profile manager, but have not been calculated yet.
     */
    public void setProcessingOrder(List<TableProfileResult> tpr);
    
    /**
     * Closes the Executor service. This stops it from running any further jobs.
     */
    public void close();

    /**
     * Returns a list of the different types of profile creators.
     * @return a List of the possible profile creators
     */
    public List<TableProfileCreator> getProfileCreators();
    
    /**
     * Returns the current profile creator in use.
     * @return the currently used profile creator object
     */
    public TableProfileCreator getCreator();
    
    /**
     * Sets the profile manager to use the given profile creator.
     * @param tpc the profile creator to use, must not be null.
     */
    public void setCreator(TableProfileCreator tpc);
} public class ProfileManagerImpl extends AbstractSPObject implements ProfileManager {
    
    /**
     * Defines an absolute ordering of the child types of this class.
     * 
     * IMPORTANT!: When changing this, ensure you maintain the order specified by {@link #getChildren()}
     */
    @SuppressWarnings("unchecked")
    public static final List<Class<? extends SPObject>> allowedChildTypes = 
        Collections.unmodifiableList(new ArrayList<Class<? extends SPObject>>(
                Arrays.asList(ProfileSettings.class, TableProfileResult.class)));
    
    /**
     * Watches the session's root object, and reacts when SQLDatabase items
     * are removed. In that case, it ensures there are no dangling references
     * from the profiled tables back to the removed database or its children.
     * If there are, the user is asked to decide to either cancel the operation
     * or allow the ETL lineage (SQLColumn.sourceColumn) references to be broken.
     */
    private class DatabaseRemovalWatcher implements SQLObjectPreEventListener {

        public void dbChildrenPreRemove(SQLObjectPreEvent e) {
            logger.debug("Pre-remove on profile manager");
            UserPrompter up = getParent().getSession().createUserPrompter(
                    "{0} tables have been profiled from the database {1}.\n" +
                    "\n" +
                    "If you proceed, the profiling information from the database" +
                    " will be removed.", UserPromptType.BOOLEAN, UserPromptOptions.OK_NOTOK_CANCEL, UserPromptResponse.OK, 
                    Boolean.TRUE, "Remove Profiles", "Keep Profiles", "Cancel");
            for (SQLObject so : e.getChildren()) {
                SQLDatabase db = (SQLDatabase) so;
                List<TableProfileResult> refs = new ArrayList<TableProfileResult>(); 
                for (TableProfileResult tpr : getResults()) {
                    if (tpr.getProfiledObject().getParentDatabase() != null && tpr.getProfiledObject().getParentDatabase().equals(db)) {
                        refs.add(tpr);
                    }
                }
                if (!refs.isEmpty()) {
                    UserPromptResponse response = up.promptUser(refs.size(), db.getName());
                    if (response == UserPromptResponse.OK) {
                        logger.debug("We got the ok to delete.");
                        // disconnect those columns' source columns
                        for (TableProfileResult tpr : refs) {
                            results.remove(tpr);
                        }
                    } else if (response == UserPromptResponse.NOT_OK) {
                        e.veto();
                    } else if (response == UserPromptResponse.CANCEL) {
                        e.veto();
                    }
                }
            }
        }

    }

    private static final Logger logger = Logger.getLogger(ProfileManagerImpl.class);
    
    /**
     * The current list of listeners who want to know when the contents
     * of this profile manager change.
     */
    private final List<ProfileChangeListener> profileChangeListeners = new ArrayList<ProfileChangeListener>();

    /**
     * All profile results in this profile manager. IMPORTANT: Do not modify this list
     * directly. Always use {@link #addResults(List, boolean)},
     * {@link #removeProfile(TableProfileResult)}, and {@link #clear()}.
     */
    private final List<TableProfileResult> results = new ArrayList<TableProfileResult>();
    
    /**
     * The defaults that new profile results will be created with.
     * XXX these are specific to the remote database profiler!
     */
    private ProfileSettings defaultProfileSettings = new ProfileSettings();

    /**
     * The Profile Executor manages the thread that actually does the work
     * of creating the profiles.
     */
    private ExecutorService profileExecutor = Executors.newSingleThreadExecutor();

    /**
     * The creator that will be used to create profiles.
     */
    private TableProfileCreator creator = new RemoteDatabaseProfileCreator(getDefaultProfileSettings());
    
    /**
     * Watches for database removals and updates the manager accordingly.
     */
    private final DatabaseRemovalWatcher databaseRemovalWatcher = new DatabaseRemovalWatcher();
    
    /**
     * A list of the different existing profile creators that can be used.
     */
    private List<TableProfileCreator> profileCreators = Arrays.asList(
            (TableProfileCreator)new RemoteDatabaseProfileCreator(getDefaultProfileSettings()),
            new LocalReservoirProfileCreator(getDefaultProfileSettings()));

    /**
     * A Callable interface which populates a single profile result then returns
     * it. Profile results don't throw the exceptions their populate() methods
     * encounter, but this callable wrapper knows about that, and digs up and
     * rethrows the exceptions encountered by populate(). This is more normal,
     * and is also compatible with the ExecutorService implementation provided
     * in the standard Java library (it handles exceptions and restarts the work
     * queue properly).
     * <p>
     * To protect the profiling from threading problems all of the objects that
     * exist in the {@link SPObject} tree are duplicated before being passed to
     * the profiler. This way the profiler can do as it desires with the objects
     * and these objects cannot be modified by outside classes. When the
     * profiling is one the profiles themselves will be updated on the
     * foreground thread so the object model stays single threaded.
     */
    private class ProfileResultCallable implements Callable<TableProfileResult> {
        
        /**
         * The table profile result this Callable populates.
         */
        private TableProfileResult actualTPR;

        /**
         * The table profile that is a copy of the real one that will be
         * populated on a background thread. This prevents any problems with
         * threading as the real profile result will not be modified until we
         * are back on the foreground thread.
         */
        private final TableProfileResult tpr;

        ProfileResultCallable(TableProfileResult actualTPR) {
            if (actualTPR == null) throw new NullPointerException("Can't populate a null profile result!");
            this.actualTPR = actualTPR;
            SQLTable table;
            TableProfileResult tempTPR;
            try {
                SQLTable profileTable = actualTPR.getProfiledObject();
                table = new SQLTable(profileTable.getParentDatabase(), true);
                table.setUUID(profileTable.getUUID());
                table.updateToMatch(profileTable);
                for (SQLColumn col : profileTable.getColumns()) {
                    SQLColumn newCol = new SQLColumn();
                    newCol.updateToMatch(col);
                    newCol.setUUID(col.getUUID());
                    table.addColumn(newCol);
                }
                tempTPR = new TableProfileResult(actualTPR, table);
                
                //Setting the profile result UUID to match which allows the
                //persister to update the actual tpr at the end.
                tempTPR.setUUID(actualTPR.getUUID());
                //Need to do the same for the parent pointer but do not want
                //to attach it to the actual profile manager.
                ProfileManager backgroundPM = new ProfileManagerImpl();
                backgroundPM.setUUID(actualTPR.getParent().getUUID());
                tempTPR.setParent(backgroundPM);
            } catch (Exception e) {
                //If an exception is thrown during setup define the profile to have an exception on
                //it and handle appropriately when doing the profile.
                tempTPR = null;
                actualTPR.setException(e);
            }
            tpr = tempTPR;
        }
        
        /**
         * Populates the profile result, throwing any exceptions encountered.
         */
        public TableProfileResult call() throws Exception {
            //Exception occurred during setup.
            if (actualTPR.getException() != null) {
                throw actualTPR.getException();
            }
            creator.doProfile(tpr);
            Runnable runner = new Runnable() {
                public void run() {
                    //None of the profiling creates or saves any data source information so an
                    //empty data source is used for the converter. If the profiling stores
                    //data source information in the future we may need the data source collection
                    //in the project.
                    DataSourceCollection<SPDataSource> dsCollection = new PlDotIni();
                    
                    SPObject root = actualTPR.getWorkspaceContainer().getWorkspace();
                    SessionPersisterSuperConverter converter = 
                        new SessionPersisterSuperConverter(dsCollection, root);
                    SPSessionPersister persister = 
                        new SPSessionPersister("Profiling persister", root, converter) {
                            @Override
                            protected void refreshRootNode(PersistedSPObject pso) {
                                //do nothing, this is not needed for the profiling update.
                            }
                    };
                    persister.setWorkspaceContainer(actualTPR.getWorkspaceContainer());
                    SPPersisterListener eventCreator = new SPPersisterListener(persister, converter);
                    eventCreator.persistObject(tpr, 
                            actualTPR.getParent().getChildren(TableProfileResult.class).indexOf(actualTPR),
                            false);
                }
            };
            try {
                actualTPR.getRunnableDispatcher().runInForeground(runner);
            } catch (SessionNotFoundException e) {
                runner.run();
            }
            
            if (tpr.getException() != null) {
                throw tpr.getException();
            }
            
            return actualTPR;
        }
    }
    
    @Constructor
    public ProfileManagerImpl() {
        defaultProfileSettings.setParent(this);
        setName("Profile Manager");
    }
    
    @Override @Mutator
    public void setParent(SPObject parent) {
        SPObject oldParent = getParent();
        if (getParent() != null) {
            ((ArchitectProject) getParent()).getRootObject().removeSQLObjectPreEventListener(databaseRemovalWatcher);
        }
        super.setParent(parent);
        final ArchitectProject architectProject = (ArchitectProject) parent;
        if (parent != null && architectProject.getRootObject() != null) {
            architectProject.getRootObject().addSQLObjectPreEventListener(databaseRemovalWatcher);
        }
        firePropertyChange("parent", oldParent, parent);
    }
    
    @Override @Accessor
    public ArchitectProject getParent() {
        return (ArchitectProject) super.getParent();
    }
    
    public void addTableProfileResult(TableProfileResult child) {
        results.add(child);
        child.setParent(this);
        fireProfilesAdded(Collections.singletonList(child));
    }
    
    /**
     * This is the method that everything which wants to add a profile result
     * must call in order to add the result. It takes care of setting SQLObject
     * client properties, firing events, and actually adding the profile to the
     * set of profile results in this profile manager.
     * @param newResults The results to add
     * @param overrideCompletion Whether the results have already been calculated,
     * such as when loading from an Architect server.
     */
    private void addResults(List<TableProfileResult> newResults, boolean overrideCompletion) {
        results.addAll(newResults);
        for (TableProfileResult tpr : newResults) {
            tpr.setParent(this);
        }
        fireProfilesAdded(newResults);
        for (TableProfileResult newResult : newResults) {
            if (overrideCompletion) {
                ((MonitorableImpl)(newResult.getProgressMonitor())).setFinished(true);
            }
            SQLTable table = newResult.getProfiledObject();
            table.putClientProperty(ProfileManager.class, PROFILE_COUNT_PROPERTY, getResults(table).size());
        }
    }
    
    /* docs inherited from interface */
    public TableProfileResult createProfile(SQLTable table) throws SQLObjectException {
        TableProfileResult tpr = new TableProfileResult(table, getDefaultProfileSettings());
        addResults(Collections.singletonList(tpr), false);
        
        try {
            profileExecutor.submit(new ProfileResultCallable(tpr)).get();
            assert (tpr.getProgressMonitor().isFinished());
        } catch (InterruptedException ex) {
            logger.info("Profiling was interrupted (likely because this manager is being shut down)");
        } catch (ExecutionException ex) {
            throw new SQLObjectException("Profile execution failed", ex);
        }
        
        return tpr;
    }

    /* docs inherited from interface */
    public Collection<Future<TableProfileResult>> asynchCreateProfiles(Collection<SQLTable> tables) {
        
        List<TableProfileResult> profiles = new ArrayList<TableProfileResult>();
        for (SQLTable t : tables) {
            profiles.add(new TableProfileResult(t, getDefaultProfileSettings()));
        }
        
        addResults(profiles, false);
        
        List<Future<TableProfileResult>> results = new ArrayList<Future<TableProfileResult>>();
        for (TableProfileResult tpr : profiles) {
            results.add(scheduleProfile(tpr));
        }
        return results;
    }

    /* docs inherited from interface */
    public Future<TableProfileResult> scheduleProfile(TableProfileResult result) {
        return profileExecutor.submit(new ProfileResultCallable(result));
    }
    
    /* docs inherited from interface */
    public void clear() {
        List<TableProfileResult> oldResults = new ArrayList<TableProfileResult>(results);
        results.clear();
        fireProfilesRemoved(oldResults);
        int index = 0;
        try {
            begin("Removing all profiles");
            for (TableProfileResult oldResult : oldResults) {
                fireChildRemoved(TableProfileResult.class, oldResult, index);
                SQLTable table = oldResult.getProfiledObject();
                table.putClientProperty(ProfileManager.class, PROFILE_COUNT_PROPERTY, 0);
                index++;
            }
            commit();
        } catch (RuntimeException e) {
            rollback(e.getMessage());
            throw e;
        } catch (Throwable t) {
            rollback(t.getMessage());
            throw new RuntimeException(t);
        }
    }

    /* docs inherited from interface */
    @NonProperty
    public List<TableProfileResult> getResults() {
        // this could be optimized by caching the current result list snapshot, but enh.
        return Collections.unmodifiableList(new ArrayList<TableProfileResult>(results));
    }

    /* docs inherited from interface */
    @NonProperty
    public List<TableProfileResult> getResults(SQLTable t) {
        List<TableProfileResult> someResults = new ArrayList<TableProfileResult>();
        for (TableProfileResult tpr : results) {
            if (tpr.getProfiledObject().equals(t)) {
                someResults.add(tpr);
            }
        }
        return Collections.unmodifiableList(someResults);
    }

    /* docs inherited from interface */
    public boolean removeProfile(TableProfileResult victim) {
        int index = results.indexOf(victim);
        boolean removed = results.remove(victim);
        if (removed) {
            fireChildRemoved(TableProfileResult.class, victim, index);
            fireProfilesRemoved(Collections.singletonList(victim));
        }
        SQLTable table = victim.getProfiledObject();
        table.putClientProperty(ProfileManager.class, PROFILE_COUNT_PROPERTY, getResults(table).size());
        return removed;
    }

    /* docs inherited from interface */
    @NonProperty
    public ProfileSettings getDefaultProfileSettings() {
        return defaultProfileSettings;
    }

    /* docs inherited from interface */
    @NonProperty
    public void setDefaultProfileSettings(ProfileSettings settings) {
        ProfileSettings oldSettings = defaultProfileSettings;
        defaultProfileSettings = settings;
        defaultProfileSettings.setParent(this);
        if (oldSettings != null) {
            fireChildRemoved(ProfileSettings.class, oldSettings, 0);
        }
        if (settings != null) {
            fireChildAdded(ProfileSettings.class, settings, 0);
        }
    }

    /* docs inherited from interface */
    @NonBound
    public void setProcessingOrder(List<TableProfileResult> tpr) {
        
    }

    /**
     * This is a hook designed so the SwingUIProject can insert profile results
     * into this profile manager as it is reading in a project file.  It is
     * not appropriate to use otherwise.
     * <p>
     * This method fires an event every time it adds a table profile result, because
     * not doing so makes it necessary to create the profile manager view after
     * loading in the profile results, and it is impossible to guarantee that policy
     * from here.
     * <p>
     * The idea is, the SwingUIProject stores all profile results in a flat space
     * (table and column results are sibling elements) so it needs our help to
     * put everything back together into the original hierarchy.  This method hangs
     * onto all TableProfileResult objects given to it, and ignores all other result
     * types, assuming the client code will do the appropriate hookups.
     * <p>
     * You might be asking yourself, "why not store the profile results in the
     * same hierarchy as they had when they were originally created, so we don't
     * need any more error-prone code to recreate what we already had and then
     * threw away?  Beside reducing bugs, it would eliminate the need for this public
     * method and accompanying docs that warn you against using it."  So was I.
     * Though I fixed it, this method still has to hang around for backwards
     * compatibility reasons.
     */
    public void loadResult(ProfileResult<? extends SQLObject> pr) {
        if (pr instanceof TableProfileResult) {
            TableProfileResult tpr = (TableProfileResult) pr;
            addResults(Collections.singletonList(tpr), true);
        }
        // the column results will get added to the table result by
        // the project's profile result factory class
    }
    
    /**
     * Adds the given listener to this profile manager.  The listener will be notified
     * of additions and removals of results in this profile manager.
     */
    public void addProfileChangeListener(ProfileChangeListener listener) {
        profileChangeListeners.add(listener);
    }

    /**
     * Removes the given listener.  After removal, the listener will no longer be notified
     * of changes to this profile manager.
     */
    public void removeProfileChangeListener(ProfileChangeListener listener) {
        profileChangeListeners.remove(listener);
    }

    /**
     * Creates and fires a "profilesAdded" event for the given profile results.
     */
    private void fireProfilesAdded(List<TableProfileResult> results) {
        if (results == null) throw new NullPointerException("Can't fire event for null profile list");
        for (TableProfileResult tpr : results) {
            fireChildAdded(TableProfileResult.class, tpr, results.indexOf(tpr));
        }
        ProfileChangeEvent e = new ProfileChangeEvent(this, results);
        for (int i = profileChangeListeners.size() - 1; i >= 0; i--) {
            profileChangeListeners.get(i).profilesAdded(e);
        }
    }

    /**
     * Creates and fires a "profilesRemoved" event for the given list of profile results.
     */
    private void fireProfilesRemoved(List<TableProfileResult> removedList) {
        if (removedList == null) throw new NullPointerException("Null list not allowed");
        ProfileChangeEvent e = new ProfileChangeEvent(this, removedList);
        for (int i = profileChangeListeners.size() - 1; i >= 0; i--) {
            profileChangeListeners.get(i).profilesRemoved(e);
        }
    }

    public void close() {
        profileExecutor.shutdown();
    }

    @NonBound
    public List<TableProfileCreator> getProfileCreators() {
        return Collections.unmodifiableList(profileCreators);
    }

    @NonBound
    public TableProfileCreator getCreator() {
        return creator;
    }

    @NonBound
    public void setCreator(TableProfileCreator tpc) {
        this.creator = tpc;
    }

    @Override
    protected boolean removeChildImpl(SPObject child) {        
        if (child.getClass().isAssignableFrom(TableProfileResult.class)) {
            if (removeProfile((TableProfileResult)child)) {
                return true;
            } else {
                throw new IllegalArgumentException("Table does not exist in this profile manager");
            }
        } else {
            throw new IllegalArgumentException("Child must be of type TableProfileResult");
        }
    }
    
    @Override
    protected void addChildImpl(SPObject child, int index) {
        if (child instanceof ProfileSettings) {
            setDefaultProfileSettings((ProfileSettings) child);
        } else if (child instanceof TableProfileResult) {
            //XXX make a new add method that can add one result.
            addResults(Collections.singletonList((TableProfileResult) child), true);
        }
    }

    public boolean allowsChildren() {
        return true;
    }

    public int childPositionOffset(Class<? extends SPObject> childType) {
        if (childType.isAssignableFrom(ProfileSettings.class)) {
            return 0;
        } else if (childType.isAssignableFrom(TableProfileResult.class)) {
            if (defaultProfileSettings != null) {
                return 1;
            } else {
                return 0;
            }
        } else {
            throw new IllegalArgumentException();
        }
    }

    @Transient @Accessor
    public List<Class<? extends SPObject>> getAllowedChildTypes() {
        return allowedChildTypes;
    }

    @NonProperty
    public List<? extends SPObject> getChildren() {
        List<SPObject> allChildren = new ArrayList<SPObject>();        
        allChildren.add(defaultProfileSettings);
        allChildren.addAll(results);
        return allChildren;
    }

    @NonBound
    public List<? extends SPObject> getDependencies() {
        return Collections.emptyList();
    }

    public void removeDependency(SPObject dependency) {
        
    }

} public class OpenProjectAction extends AbstractArchitectAction {

    private static final Logger logger = Logger.getLogger(OpenProjectAction.class);
    
    public static interface FileLoader {
        public void open(ArchitectSwingSession newSession, File f, ArchitectSwingSession openingSession, boolean separateThread);
        public void open(ArchitectSwingSession newSession, InputStream in, ArchitectSwingSession openingSession, boolean separateThread);
    }

    /**
     * Opens a project file into the given session using a separate worker
     * thread. A dialog box with a progress bar will be displayed during the
     * load process, and any errors that are encountered during the load will be
     * displayed in additional dialogs.
     * <p>
     * Note that this method always returns immediately, so as the caller of
     * this method you have no way of knowing if the load has worked/will work.
     * 
     */
    private static FileLoader fileLoader = new FileLoader() {
        public void open(ArchitectSwingSession newSession, File f, ArchitectSwingSession openingSession, boolean separateThread) {
            LoadFileWorker worker;
            try {
                worker = new LoadFileWorker(f, newSession, openingSession);
                if (separateThread) {
                    new Thread(worker).start();
                } else {
                    worker.run();
                }
            } catch (Exception e1) {
                ASUtils.showExceptionDialogNoReport(Messages.getString("OpenProjectAction.errorLoadingFile"), e1); //$NON-NLS-1$
            }
        }
        
        public void open(ArchitectSwingSession newSession, InputStream in, ArchitectSwingSession openingSession, boolean separateThread) {
            LoadFileWorker worker;
            try {
                worker = new LoadFileWorker(in, newSession, openingSession);
                if (separateThread) {
                    new Thread(worker).start();
                } else {
                    worker.run();
                }
            } catch (Exception e1) {
                ASUtils.showExceptionDialogNoReport(Messages.getString("OpenProjectAction.errorLoadingFile"), e1); //$NON-NLS-1$
            }
        }
    };
    
    public static void setFileLoader(FileLoader loader) {
        fileLoader = loader;
    }
    
    public static FileLoader getFileLoader() {
        return fileLoader;
    }

    public OpenProjectAction(ArchitectFrame frame) {
        super(frame, Messages.getString("OpenProjectAction.name"), Messages.getString("OpenProjectAction.description"), "folder"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
        putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_O, Toolkit.getDefaultToolkit()
                .getMenuShortcutKeyMask()));
    }

    public void actionPerformed(ActionEvent e) {
        File f;
        if (e.getActionCommand() == null || !e.getActionCommand().startsWith("file:")) {
            JFileChooser chooser = new JFileChooser(getSession().getRecentMenu().getMostRecentFile());
            chooser.addChoosableFileFilter(SPSUtils.ARCHITECT_FILE_FILTER);
            int returnVal = chooser.showOpenDialog(frame);
            if (returnVal == JFileChooser.APPROVE_OPTION) {
                f = chooser.getSelectedFile();
            } else {
                return;
            }
        } else {
            f = new File(e.getActionCommand().substring("file:".length()));
        }
        try {
            fileLoader.open(getSession().getContext().createSession(), f, getSession(), true);
        } catch (SQLObjectException ex) {
            SPSUtils.showExceptionDialogNoReport(getSession().getArchitectFrame(),
                    Messages.getString("OpenProjectAction.failedToOpenProjectFile"), ex); //$NON-NLS-1$
        }
    }
    
    /**
     * A worker for asynchronously loading a new project file.
     */
    public static class LoadFileWorker extends SPSwingWorker {
        private final ArchitectSwingSessionContext context;

        private final InputStream in;

        private final File file;

        private final RecentMenu recent;

        private final ArchitectSwingSession openingSession;

        /**
         * The session that will get created if loading the file in doStuff() is
         * successful.
         */
        private ArchitectSwingSession session;

        /**
         * Load file worker creates a new worker and opens the given file.
         * 
         * @param file
         *            this file gets opened in the constructor
         * @param newSession
         *            The session in which the project file should be opened
         * @param openingSession
         *            The session from which the open project operation is being
         *            called. Should not be null and should have a frame. If the
         *            openingSession.isNew() returns true, (i.e. it's an new,
         *            empty, and unmodified project) then openingSession.close()
         *            will be called once the project is finished loading.
         * @throws SQLObjectException
         *             when the project creation fails.
         * @throws FileNotFoundException
         *             if file doesn't exist
         */
        public LoadFileWorker(File file, ArchitectSwingSession newSession, ArchitectSwingSession openingSession)
                throws SQLObjectException, FileNotFoundException {
            // The super constructor registers the LoadFileWorker with the
            // session.
            super(newSession);
            this.context = newSession.getContext();
            this.file = file;
            this.recent = newSession.getRecentMenu();
            this.openingSession = openingSession;

            this.session = newSession;

            // This assumes that the opening session has a frame. 
            // To get rid of this requirement, we need to create an
            // invisible owner frame in the context. we can set its icon
            // to the architect icon, and use it as the owner of all
            // "unowned" dialogs like this one.
            in = new BufferedInputStream(new ProgressMonitorInputStream(openingSession.getArchitectFrame(),
                    Messages.getString("OpenProjectAction.reading") + file.getName(), new FileInputStream(file))); //$NON-NLS-1$
        }
        
        public LoadFileWorker(InputStream in, ArchitectSwingSession newSession, ArchitectSwingSession openingSession) {
            super(newSession);
            this.context = newSession.getContext();
            file = null;
            this.recent = newSession.getRecentMenu();
            this.openingSession = openingSession;

            this.session = newSession;
            
            this.in = in;
        }

        @Override
        public void doStuff() throws Exception {
            session.getProjectLoader().load(in, session.getDataSources(), openingSession);
            session.getProjectLoader().setFile(file);
        }

        @Override
        public void cleanup() throws SQLObjectException {
            if (getDoStuffException() != null) {
                Throwable cause = getDoStuffException().getCause();
                // This if clause is to prevent an error from being thrown if
                // the user cancelled the file load,
                // in which ProgressMonitorInputStream throws an
                // InterruptedIOException with message "progress",
                // or if the user cancelled because of a warning when
                // they tried to open an incompatible file
                if (!(getDoStuffException() instanceof InterruptedIOException) &&
                        (!(cause instanceof InterruptedIOException) || !(cause.getMessage().equals("progress")))) { //$NON-NLS-1$
                    // We have to use the non-session exception dialogue here,
                    // because there is no session available (we just failed to
                    // create one!)
                    ASUtils.showExceptionDialogNoReport(Messages.getString("OpenProjectAction.cannotOpenProjectFile") + file.getAbsolutePath(), //$NON-NLS-1$
                            getDoStuffException());
                    logger.error("Got exception while opening a project", getDoStuffException()); //$NON-NLS-1$
                }
                session.removeSwingWorker(this);
                if (session.getContext().getSessions().size() > 1) {
                    //Prevents the save dialog from being displayed if it contained an error
                    session.getProjectLoader().setModified(false); 
                    session.close();
                }
            } else {
                if (file != null) {
                    recent.putRecentFileName(file.getAbsolutePath());
                }
                if (openingSession != null) {
                    openingSession.getArchitectFrame().addSession(session);
                    openingSession.getArchitectFrame().setCurrentSession(session);
                }
                ((DBTreeModel) session.getDBTree().getModel()).refreshTreeStructure();
            }

            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException ie) {
                logger.error("got exception while closing project file", ie); //$NON-NLS-1$
            }
        }
    }
} public class OLAPEditSession extends AbstractSPListener {

    private final OLAPTree tree;
    
    /**
     * This is the playpen used within OLAP schema editor.
     */
    private final PlayPen pp;
    
    /**
     * The frame this edit session lives in.
     */
    private JFrame frame;
    
    /**
     * The preferences node for OLAP user settings.
     */
    private final Preferences prefs = Preferences.userNodeForPackage(OLAPEditSession.class);
    
    private static enum PrefKeys {
        /** The current split pane divider location. */
        DIVIDER_LOCATION
    }
    
    /**
     * The scroll pane of which the PlayPen is in.
     */
    private JScrollPane ppScrollPane;
    
    /**
     * OLAPSession associated with the schema that this edit session edits.
     */
    private final OLAPSession olapSession;
    
    /**
     * The undo manager for this OLAP edit session.
     */
    private final OLAPUndoManager undoManager;
    
    public static final double ZOOM_STEP = 0.25;
    
    private ZoomAction zoomInAction;
    private ZoomAction zoomOutAction;
    private ZoomResetAction zoomNormalAction;
    private ZoomToFitAction zoomToFitAction;
    private CreateDimensionAction createDimensionAction;
    private CreateCubeAction createCubeAction;
    private CreateVirtualCubeAction createVirtualCubeAction;
    private CreateMeasureAction createMeasureAction;
    private CreateHierarchyAction createHierarchyAction;
    private CreateLevelAction createLevelAction;
    private CreateDimensionUsageAction createDimensionUsageAction;
    private CreateCubeUsageAction createCubeUsageAction;
    private CreateCalculatedMemberAction createCalculatedMemberAction;
    private ExportSchemaAction exportSchemaAction;
    private OLAPDeleteSelectedAction olapDeleteSelectedAction;
    private AutoLayoutAction autoLayoutAction;
    private UndoAction undoAction;
    private RedoAction redoAction;
    
    private final ArchitectSwingSession swingSession;
    
    private static final Logger logger = Logger.getLogger(OLAPPlayPenFactory.class);


    /**
     * Creates a new editor for the given OLAP schema. The schema's OLAPObjects should
     * all belong to the given session's dbtree and playpen. This should only be called by the
     * ArchitectSwingSession, see {@link ArchitectSwingSession#getOLAPEditSession(OLAPSession)}.
     * 
     * @param swingSession The architect swing session this editor and the given schema belong to.
     * @param olapSession The OLAPSession of the schema to edit
     */
    public OLAPEditSession(ArchitectSwingSession swingSession, OLAPSession olapSession) {
        this.olapSession = olapSession;
        this.swingSession = swingSession;

        // listen for when to remove this from the architect session.
        swingSession.getOLAPRootObject().addSPListener(this);
        
        // add to the architect session's list of edit sessions.
        swingSession.getOLAPEditSessions().add(this);

        tree = new OLAPTree(swingSession, this, olapSession.getSchema());
        tree.setCellRenderer(new OLAPTreeCellRenderer());
        undoManager = new OLAPUndoManager(olapSession);
        undoManager.addChangeListener(new ChangeListener() {           
            public void stateChanged(ChangeEvent e) {
                // this can be called before initGUI() has had a chance to create the frame
                if (frame != null) {
                    frame.setTitle(generateDialogTitle());
                }
            }
        });
        pp = OLAPPlayPenFactory.createPlayPen(swingSession, this, undoManager);
        
        
        swingSession.addSessionLifecycleListener(new SessionLifecycleListener<ArchitectSession>() {
            public void sessionClosing(SessionLifecycleEvent<ArchitectSession> e) {
                close();
            }

            public void sessionOpening(SessionLifecycleEvent<ArchitectSession> e) {
            }
        });
        //Don't initialize GUI here for cases where the Architect's frame has not been created (like loading).
    }
    
    /**
     * Returns the OLAP Schema Editor dialog. It does not come with location and
     * visibility set.
     */
    public JFrame getFrame() {
        if (frame == null) {
            initGUI();
        }
        return frame;
    }
    
    /**
     * Builds the gui components used by this edit session. The PlayPen and OLAPTree
     * will not work properly until this has been called.
     */
    private void initGUI() {
        frame = new JFrame(generateDialogTitle());
        Schema schema = olapSession.getSchema();
        zoomInAction = new ZoomAction(swingSession, pp, ZOOM_STEP);
        zoomOutAction = new ZoomAction(swingSession, pp, ZOOM_STEP * -1.0);
        zoomNormalAction = new ZoomResetAction(swingSession, pp);
        zoomToFitAction = new ZoomToFitAction(swingSession, pp);
        zoomToFitAction.putValue(AbstractAction.SHORT_DESCRIPTION, Messages.getString("ArchitectFrame.zoomToFitActionDescription")); //$NON-NLS-1$
        createDimensionAction = new CreateDimensionAction(swingSession, schema, pp);
        createCubeAction = new CreateCubeAction(swingSession, schema, pp);
        createVirtualCubeAction = new CreateVirtualCubeAction(swingSession, schema, pp);
        createMeasureAction = new CreateMeasureAction(swingSession, pp);
        createHierarchyAction = new CreateHierarchyAction(swingSession, pp);
        createLevelAction = new CreateLevelAction(swingSession, pp);
        createDimensionUsageAction = new CreateDimensionUsageAction(swingSession, pp);
        createCubeUsageAction = new CreateCubeUsageAction(swingSession, pp);
        createCalculatedMemberAction = new CreateCalculatedMemberAction(swingSession, pp);
        exportSchemaAction = new ExportSchemaAction(swingSession, schema);
        olapDeleteSelectedAction = new OLAPDeleteSelectedAction(swingSession, this);
        autoLayoutAction = new AutoLayoutAction(swingSession, pp, "Automatic Layout", "Automatic Layout", "auto_layout"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
        autoLayoutAction.setLayout(new FruchtermanReingoldForceLayout());

        undoAction = new UndoAction(swingSession, undoManager);
        redoAction = new RedoAction(swingSession, undoManager);
        
        JToolBar toolbar = new JToolBar(JToolBar.VERTICAL);
        toolbar.add(zoomInAction);
        toolbar.add(zoomOutAction);
        toolbar.add(zoomNormalAction);
        toolbar.add(zoomToFitAction);
        
        toolbar.addSeparator();

        toolbar.add(undoAction);
        toolbar.add(redoAction);
        
        toolbar.addSeparator();

        toolbar.add(autoLayoutAction);

        toolbar.addSeparator();

        toolbar.add(createDimensionAction);
        toolbar.add(createCubeAction);
        toolbar.add(createVirtualCubeAction);
        toolbar.add(createMeasureAction);
        toolbar.add(createHierarchyAction);
        toolbar.add(createLevelAction);
        toolbar.add(createDimensionUsageAction);
        toolbar.add(createCubeUsageAction);
        toolbar.add(createCalculatedMemberAction);
        
        toolbar.addSeparator();
        
        toolbar.add(exportSchemaAction);
        
        ppScrollPane = new JScrollPane(pp);
        JPanel panel = new JPanel(new BorderLayout());
        final JSplitPane splitPane = new JSplitPane(
                JSplitPane.HORIZONTAL_SPLIT,
                new JScrollPane(tree),
                ppScrollPane);
        splitPane.setDividerLocation(prefs.getInt(PrefKeys.DIVIDER_LOCATION.name(), 240));
        splitPane.addPropertyChangeListener("dividerLocation", new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent evt) {
                prefs.putInt(PrefKeys.DIVIDER_LOCATION.name(), splitPane.getDividerLocation());
            }
        });
        panel.add(splitPane, BorderLayout.CENTER);
        panel.add(toolbar, BorderLayout.EAST);
        
        olapSession.getSchema().addSPListener(new AbstractSPListener() {
            public void propertyChanged(PropertyChangeEvent evt) {
                if (evt.getPropertyName().equals("name")) {
                    frame.setTitle(generateDialogTitle());
                }
            }
        });
        frame.setContentPane(panel);
        frame.pack();
        
        frame.setIconImage(OSUtils.SCHEMA_ICON.getImage());
        
        // show a new top menu bar for a new OLAP Frame if on MacOSX, as it will be 
        // gone otherwise
        ArchitectSwingSessionContext context = swingSession.getContext();
        if (context.isMacOSX()) {                   
            frame.setJMenuBar(swingSession.getArchitectFrame().createNewMenuBar());
        }
        
        OLAPPlayPenFactory.setupOLAPKeyboardActions(pp, this);
        
        undoManager.rememberPosition();
    }
    
    /**
     * Returns the playpen used within OLAP Schema Editor
     */
    public PlayPen getOlapPlayPen() {
        return pp;
    }
    
    public OLAPTree getOlapTree() {
        return tree;
    }
    
    /**
     * Returns the scroll pane that the PlayPen is in.
     */
    public JScrollPane getPPScrollPane() {
        return ppScrollPane;
    }

    /**
     * Cleans up all resources this session was using. This method will be
     * called automatically when the Architect swing session closes.
     */
    public void close() {
        if (frame != null) {
            frame.dispose();
        }
    }
    
    /**
     * This method must be called just after this session has been saved with
     * the project. It resets the save state, and might do other stuff that
     * client code shouldn't care about.
     */
    public void saveNotify() {
        undoManager.rememberPosition();
    }
    
    /**
     * returns true if any aspect of this session has been modified since it was
     * created or since the last time {@link #saveNotify()} was called, whichever
     * is more recent.
     */
    public boolean isModified() {
        return !undoManager.isAtRememberedPosition();
    }

    /**
     * Returns the schema edit dialog's title that includes the schema's name and
     * an asterisk if the schema has unsaved changes.
     */
    private String generateDialogTitle() {
        StringBuilder title = new StringBuilder();
        if (!undoManager.isAtRememberedPosition() && !swingSession.isEnterpriseSession()) {
            title.append("*");
        }
        title.append(olapSession.getSchema().getName());
        title.append(" - OLAP Schema Editor");
        return title.toString();
    }

    /**
     * Returns the OLAPSession this this edit session edits.
     */
    public OLAPSession getOlapSession() {
        return olapSession;
    }

    // ------ Action getter methods ------ //
    
    public CreateDimensionAction getCreateDimensionAction() {
        return createDimensionAction;
    }

    public CreateCubeAction getCreateCubeAction() {
        return createCubeAction;
    }

    public CreateVirtualCubeAction getCreateVirtualCubeAction() {
        return createVirtualCubeAction;
    }

    public CreateMeasureAction getCreateMeasureAction() {
        return createMeasureAction;
    }

    public CreateHierarchyAction getCreateHierarchyAction() {
        return createHierarchyAction;
    }

    public CreateLevelAction getCreateLevelAction() {
        return createLevelAction;
    }

    public ExportSchemaAction getExportSchemaAction() {
        return exportSchemaAction;
    }
    
    public OLAPDeleteSelectedAction getOLAPDeleteSelectedAction() {
        return olapDeleteSelectedAction;
    }

    public AutoLayoutAction getAutoLayoutAction() {
        return autoLayoutAction;
    }
    
    // ------ OLAPChildListener methods ------ //

    public void childRemoved(SPChildEvent e) {
        if (e.getChild() == olapSession) {
            // remove from architect's list of edit sessions and stop listening
            swingSession.getOLAPEditSessions().remove(this);
            swingSession.getOLAPRootObject().removeSPListener(this);
            frame.dispose();
        }
    }
    
    // -------------------------------------- //
    
    public ZoomAction getZoomInAction() {
        return zoomInAction;
    }

    public ZoomAction getZoomOutAction() {
        return zoomOutAction;
    }

    public ZoomResetAction getZoomNormalAction() {
        return zoomNormalAction;
    }

    public ZoomToFitAction getZoomToFitAction() {
        return zoomToFitAction;
    }

    public CreateDimensionUsageAction getCreateDimensionUsageAction() {
        return createDimensionUsageAction;
    }
    
    public CreateCubeUsageAction getCreateCubeUsageAction() {
        return createCubeUsageAction;
    }
    
    public CreateCalculatedMemberAction getCreateCalculatedMemberAction() {
        return createCalculatedMemberAction;
    }
} public class ArchitectUndoManager extends SPObjectUndoManager implements NotifyingUndoManager {

    /**
     * Creates a new UndoManager and attaches it to the given PlayPen's
     * component and SQL Object model events.
     * 
     * @param playPen
     *            The play pen to track undo/redo history for.
     */
    public ArchitectUndoManager(PlayPen playPen) {
        super(playPen.getSession().getTargetDatabase());
        SQLObjectRoot rootObject = playPen.getSession().getRootObject();
        final SPObjectUndoableEventAdapter undoListener = new SPObjectUndoableEventAdapter(false);
        rootObject.addSPListener(undoListener);
        undoListener.attachToObject(rootObject);
        final ArchitectSwingProject workspace = playPen.getSession().getWorkspace();
        playPen.getContentPane().addSPListener(eventAdapter);
        eventAdapter.attachToObject(playPen.getContentPane());
        SQLPowerUtils.listenToHierarchy(workspace.getSnapshotCollection(), eventAdapter);
        workspace.addSPListener(new AbstractSPListener() {

            /**
             * The current play pen content pane. This is assigned from the
             * ArchitectProject to ensure if the way the content pane is defined
             * in the project is changed this will be changed as well.
             */
            private PlayPenContentPane lastPPCP = 
                workspace.getPlayPenContentPane();
            
            @Override
            public void childAdded(SPChildEvent e) {
                if (e.getChild() != null && e.getChild().equals(workspace.getPlayPenContentPane())) {
                    lastPPCP = workspace.getPlayPenContentPane();
                    lastPPCP.addSPListener(eventAdapter);
                }
            }
            
            @Override
            public void childRemoved(SPChildEvent e) {
                if (e.getChild().equals(lastPPCP)) {
                    lastPPCP.removeSPListener(eventAdapter);
                }
            }
        });
        if (playPen != null) {
            playPen.addUndoEventListener(eventAdapter);
        }
    }

    public ArchitectUndoManager(SQLObject sqlObjectRoot) throws SQLObjectException {
        super(sqlObjectRoot);
    }
    
}
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import javax.swing.Action;
import javax.swing.JColorChooser;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.ProgressMonitor;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import org.apache.log4j.Logger;
import ca.sqlpower.architect.ArchitectSession;
import ca.sqlpower.architect.ArchitectSessionImpl;
import ca.sqlpower.architect.CoreUserSettings;
import ca.sqlpower.architect.ProjectLoader;
import ca.sqlpower.architect.ProjectSettings;
import ca.sqlpower.architect.ProjectSettings.ColumnVisibility;
import ca.sqlpower.architect.UserSettings;
import ca.sqlpower.architect.ddl.DDLGenerator;
import ca.sqlpower.architect.ddl.LiquibaseSettings;
import ca.sqlpower.architect.enterprise.ArchitectClientSideSession;
import ca.sqlpower.architect.etl.kettle.KettleJob;
import ca.sqlpower.architect.olap.OLAPRootObject;
import ca.sqlpower.architect.olap.OLAPSession;
import ca.sqlpower.architect.profile.ProfileManager;
import ca.sqlpower.architect.profile.ProfileManagerImpl;
import ca.sqlpower.architect.swingui.action.AboutAction;
import ca.sqlpower.architect.swingui.action.AddDataSourceAction;
import ca.sqlpower.architect.swingui.action.NewDataSourceAction;
import ca.sqlpower.architect.swingui.action.OpenProjectAction;
import ca.sqlpower.architect.swingui.action.PreferencesAction;
import ca.sqlpower.architect.swingui.dbtree.DBTreeCellRenderer;
import ca.sqlpower.architect.swingui.olap.OLAPEditSession;
import ca.sqlpower.architect.swingui.olap.OLAPSchemaManager;
import ca.sqlpower.architect.undo.ArchitectUndoManager;
import ca.sqlpower.enterprise.AbstractNetworkConflictResolver;
import ca.sqlpower.object.AbstractPoolingSPListener;
import ca.sqlpower.object.AbstractSPListener;
import ca.sqlpower.object.SPChildEvent;
import ca.sqlpower.object.SPListener;
import ca.sqlpower.object.SPObjectSnapshot;
import ca.sqlpower.sql.DataSourceCollection;
import ca.sqlpower.sql.JDBCDataSource;
import ca.sqlpower.sql.Olap4jDataSource;
import ca.sqlpower.sql.SPDataSource;
import ca.sqlpower.sqlobject.SQLDatabase;
import ca.sqlpower.sqlobject.SQLObjectException;
import ca.sqlpower.sqlobject.SQLObjectRoot;
import ca.sqlpower.sqlobject.UserDefinedSQLType;
import ca.sqlpower.swingui.DialogUserPrompter;
import ca.sqlpower.swingui.RecentMenu;
import ca.sqlpower.swingui.SPSUtils;
import ca.sqlpower.swingui.SPSwingWorker;
import ca.sqlpower.swingui.SwingUIUserPrompterFactory;
import ca.sqlpower.swingui.db.DataSourceDialogFactory;
import ca.sqlpower.swingui.db.DataSourceTypeDialogFactory;
import ca.sqlpower.swingui.db.DatabaseConnectionManager;
import ca.sqlpower.swingui.event.SessionLifecycleEvent;
import ca.sqlpower.swingui.event.SessionLifecycleListener;
import ca.sqlpower.util.DefaultUserPrompterFactory;
import ca.sqlpower.util.SQLPowerUtils;
import ca.sqlpower.util.UserPrompter;
import ca.sqlpower.util.UserPrompter.UserPromptOptions;
import ca.sqlpower.util.UserPrompter.UserPromptResponse;
import ca.sqlpower.util.UserPrompterFactory;
import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.layout.FormLayout;
 /*
 * Copyright (c) 2008, SQL Power Group Inc.
 *
 * This file is part of Power*Architect.
 *
 * Power*Architect is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Power*Architect is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 */
package ca.sqlpower.architect.swingui;






public class ArchitectSwingSessionImpl implements ArchitectSwingSession {

    private static final Logger logger = Logger.getLogger(ArchitectSwingSessionImpl.class);
    
    private static final Executor saveExecutor = new ScheduledThreadPoolExecutor(1);
    
    private final ArchitectSwingSessionContext context;

    /**
     * This is the core session that some tasks are delegated to.
     */
    private final ArchitectSession delegateSession;

    /**
     * The frame that this project may appear in
     */
    private ArchitectFrame frame;
    
    /**
     * The panel where the GUI for this session appears
     */
    private JComponent projectPanel;
    
    private JScrollPane playPenScrollPane;

    private Saver saveBehaviour = new Saver() {
        public boolean save(ArchitectSwingSession session, boolean showChooser, boolean separateThread) {
            final boolean finalSeparateThread = separateThread;
            final ProgressMonitor pm = new ProgressMonitor
            (frame, Messages.getString("ArchitectSwingSessionImpl.saveProgressDialogTitle"), "", 0, 100); //$NON-NLS-1$ //$NON-NLS-2$

            class SaverTask implements Runnable {
                boolean success;

                public void run() {
                    SwingUIProjectLoader project = getProjectLoader();
                    try {
                        success = false;
                        if (finalSeparateThread) {
                            SwingUtilities.invokeAndWait(new Runnable() {
                                public void run() {
                                    getArchitectFrame().setEnableSaveOption(false);
                                }
                            });
                        }
                        project.setSaveInProgress(true);
                        project.save(finalSeparateThread ? pm : null);
                        success = true;
                    } catch (Exception ex) {
                        success = false;
                        ASUtils.showExceptionDialog(
                                ArchitectSwingSessionImpl.this,
                                Messages.getString("ArchitectSwingSessionImpl.cannotSaveProject")+ex.getMessage(), ex); //$NON-NLS-1$
                    } finally {
                        project.setSaveInProgress(false);
                        if (finalSeparateThread) {
                            SwingUtilities.invokeLater(new Runnable() {
                                public void run() {
                                    getArchitectFrame().setEnableSaveOption(true);
                                }
                            });
                        } 
                    }
                }
            }
            SaverTask saveTask = new SaverTask();
            if (separateThread) {
                saveExecutor.execute(saveTask);
                return true; // this is an optimistic lie
            } else {
                saveTask.run();
                return saveTask.success;
            }
        }

        @Override
        public void saveToStream(ArchitectSwingSession session, OutputStream out) throws IOException {
            session.getProjectLoader().save(out, "utf-8");
        }
    };
    
    /**
     * The menu of recently-opened project files on this system.
     */
    private RecentMenu recent;

    /** the dialog that contains the small ProfileManagerView */
    private JDialog profileDialog;

    /**
     * Keeps track of whether or not the profile manager dialog has been packed yet.
     * We only want to do this the first time we make it visible, since doing it over
     * and over will annoy users.
     */
    private boolean profileDialogPacked = false;

    private final PlayPen playPen;

    /** the small dialog that lists the profiles */
    private ProfileManagerView profileManagerView;

    private CompareDMSettings compareDMSettings;

    private ArchitectUndoManager undoManager;