/*
 * Copyright (c) 2002-2004
 * All rights reserved.
 */

package com.atlassian.jira.util;

import com.atlassian.jira.ComponentManager;
import com.atlassian.jira.ManagerFactory;
import com.atlassian.jira.action.project.ProjectUtils;
import com.atlassian.jira.bc.JiraServiceContext;
import com.atlassian.jira.bc.JiraServiceContextImpl;
import com.atlassian.jira.bc.project.ProjectService;
import com.atlassian.jira.bc.project.component.ProjectComponent;
import com.atlassian.jira.bc.project.component.ProjectComponentManager;
import com.atlassian.jira.exception.CreateException;
import com.atlassian.jira.exception.DataAccessException;
import com.atlassian.jira.external.ExternalUtils;
import com.atlassian.jira.external.beans.ExternalUser;
import com.atlassian.jira.issue.AttachmentManager;
import com.atlassian.jira.issue.CustomFieldManager;
import com.atlassian.jira.issue.IssueFactory;
import com.atlassian.jira.issue.IssueFieldConstants;
import com.atlassian.jira.issue.IssueImpl;
import com.atlassian.jira.issue.IssueManager;
import com.atlassian.jira.issue.MutableIssue;
import com.atlassian.jira.issue.attachment.Attachment;
import com.atlassian.jira.issue.cache.CacheManager;
import com.atlassian.jira.issue.comments.CommentManager;
import com.atlassian.jira.issue.context.GlobalIssueContext;
import com.atlassian.jira.issue.context.IssueContextImpl;
import com.atlassian.jira.issue.customfields.CustomFieldSearcher;
import com.atlassian.jira.issue.customfields.CustomFieldType;
import com.atlassian.jira.issue.customfields.manager.OptionsManager;
import com.atlassian.jira.issue.customfields.option.Options;
import com.atlassian.jira.issue.fields.CustomField;
import com.atlassian.jira.issue.fields.config.FieldConfig;
import com.atlassian.jira.issue.fields.screen.issuetype.IssueTypeScreenSchemeManager;
import com.atlassian.jira.issue.history.ChangeItemBean;
import com.atlassian.jira.issue.history.ChangeLogUtils;
import com.atlassian.jira.issue.index.IndexException;
import com.atlassian.jira.issue.index.IssueIndexManager;
import com.atlassian.jira.permission.PermissionSchemeManager;
import com.atlassian.jira.project.ProjectManager;
import com.atlassian.jira.project.version.Version;
import com.atlassian.jira.project.version.VersionManager;
import com.atlassian.jira.scheme.SchemeManager;
import com.atlassian.jira.security.PermissionManager;
import com.atlassian.jira.security.Permissions;
import com.atlassian.jira.user.util.UserUtil;
import com.atlassian.jira.web.action.admin.customfields.CreateCustomField;
import com.atlassian.jira.web.action.util.DatabaseConnectionBean;
import com.atlassian.jira.workflow.WorkflowFunctionUtils;

import com.atlassian.core.ofbiz.CoreFactory;
import com.atlassian.core.ofbiz.util.EntityUtils;
import com.atlassian.core.user.GroupUtils;
import com.atlassian.core.user.UserUtils;
import com.atlassian.core.util.FileUtils;
import com.atlassian.core.util.collection.EasyList;
import com.atlassian.core.util.map.EasyMap;

import com.opensymphony.user.EntityNotFoundException;
import com.opensymphony.user.User;
import com.opensymphony.util.TextUtils;
import com.opensymphony.workflow.InvalidInputException;
import com.opensymphony.workflow.InvalidRoleException;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.log4j.Logger;
import org.ofbiz.core.entity.GenericDelegator;
import org.ofbiz.core.entity.GenericEntityException;
import org.ofbiz.core.entity.GenericValue;
import org.ofbiz.core.util.UtilDateTime;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

/**
 * Mantis importer.
 */
public class MantisImportBean
{
    private static final Logger log4jLog = Logger.getLogger(MantisImportBean.class);
    private static final String MANTIS_CHANGE_ITEM_FIELD = "Mantis Import Key";

    // Mantis custom field types, defined (in 1.0) with:
    // $s_custom_field_type_enum_string = '0:String,1:Numeric,2:Float,3:Enumeration,4:Email,5:Checkbox,6:List,7:Multiselection list,8:Date';
    public static final Integer MANTIS_CF_TYPE_STRING = new Integer(0);
    public static final Integer MANTIS_CF_TYPE_NUMERIC = new Integer(1);
    public static final Integer MANTIS_CF_TYPE_FLOAT = new Integer(2);
    public static final Integer MANTIS_CF_TYPE_ENUM = new Integer(3);
    public static final Integer MANTIS_CF_TYPE_EMAIL = new Integer(4);
    public static final Integer MANTIS_CF_TYPE_CHECKBOX = new Integer(5);
    public static final Integer MANTIS_CF_TYPE_LIST = new Integer(6);
    public static final Integer MANTIS_CF_TYPE_MULTILIST = new Integer(7);
    public static final Integer MANTIS_CF_TYPE_DATE = new Integer(8);
    /**
     * Mapping from Mantis custom field types (Integers) to JIRA custom field types (Strings). Populated statically below.
     */
    public static final Map MANTIS_TO_JIRA_CF_TYPE_MAPPINGS;
    /**
     * Mapping from Mantis custom field types (Integers) to JIRA custom field searchers (Strings). Populated statically below.
     */
    public static final Map MANTIS_TO_JIRA_CF_SEARCHER_MAPPINGS;
    /**
     * {{@link CustomField} to {@link Object} mappings, storing 'defaulted' custom fields and their default JIRA value objects.
     */
    private Map defaultedCustomFieldValues = new HashMap();

    static
    {
        final Map typeMappings = new HashMap();
        final Map searcherMappings = new HashMap();
        // Which JIRA custom field types map to Mantis custom field types, and which searchers to use.
        typeMappings.put(MANTIS_CF_TYPE_STRING, "textfield");
        searcherMappings.put(MANTIS_CF_TYPE_STRING, "textsearcher");
        typeMappings.put(MANTIS_CF_TYPE_NUMERIC, "textfield"); // This should be 'float' but Mantis doesn't prevent people abusing 'numeric' for storing things like versions
        searcherMappings.put(MANTIS_CF_TYPE_NUMERIC, "textsearcher"); // should be 'exactnumber' if 'float' is used above.
        typeMappings.put(MANTIS_CF_TYPE_FLOAT, "float");
        searcherMappings.put(MANTIS_CF_TYPE_FLOAT, "exactnumber");
        typeMappings.put(MANTIS_CF_TYPE_ENUM, "radiobuttons"); // displayed in Mantis a 1-size select list.
        searcherMappings.put(MANTIS_CF_TYPE_ENUM, "radiosearcher");
        typeMappings.put(MANTIS_CF_TYPE_EMAIL, "textarea");
        searcherMappings.put(MANTIS_CF_TYPE_EMAIL, "textsearcher");
        typeMappings.put(MANTIS_CF_TYPE_CHECKBOX, "multicheckboxes");
        searcherMappings.put(MANTIS_CF_TYPE_CHECKBOX, "checkboxsearcher");
        typeMappings.put(MANTIS_CF_TYPE_LIST, "select");
        searcherMappings.put(MANTIS_CF_TYPE_LIST, "selectsearcher");
        typeMappings.put(MANTIS_CF_TYPE_MULTILIST, "multiselect");
        searcherMappings.put(MANTIS_CF_TYPE_MULTILIST, "multiselectsearcher");
        typeMappings.put(MANTIS_CF_TYPE_DATE, "datepicker");
        searcherMappings.put(MANTIS_CF_TYPE_DATE, "daterange");

        MANTIS_TO_JIRA_CF_TYPE_MAPPINGS = Collections.unmodifiableMap(typeMappings);
        MANTIS_TO_JIRA_CF_SEARCHER_MAPPINGS = Collections.unmodifiableMap(searcherMappings);
    }

    private final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");

    /**
     * Id of to-be-added "Not A Bug" resolution type.  Common across imports, so static.
     */
    private static int priorityResolutionId;
    private final IssueIndexManager indexManager;
    private final GenericDelegator genericDelegator;
    private final ProjectManager projectManager;
    private final SchemeManager permissionSchemeManager;
    private final CacheManager cacheManager;
    private final VersionManager versionManager;
    private final CustomFieldManager customFieldManager;
    private final IssueTypeScreenSchemeManager issueTypeScreenSchemeManager;
    private final PermissionManager permissionManager;
    StringBuffer importLog = null;

    //these are cached lookups to save us having to go to the database each time.
    private Map userKeys = new HashMap(255);
    private Map projectKeys = new HashMap();
    private Map versionKeys = new HashMap();
    private Map componentKeys = new HashMap();

    /**
     * Map from Mantis issue ids (Integers) to JIRA issue ids (Longs).
     */
    private Map issueKeys = new HashMap();

    // Map of Mantis CF Ids to Mantis type integers
    private Map mantisCustomFieldTypes = new HashMap();
    // Map of Mantis CF Ids to JIRA CustomCustomFields
    private Map mantisToJiraCustomFieldInstanceMap = new HashMap();

    private User importer;
    private MappingBean mappingBean;
    private boolean reuseExistingUsers;
    private boolean addToDevelopersGroup;
    private CustomField mantisIdCustomField;
    private IssueManager issueManager;
    private ProjectComponentManager projectComponentManager;
    public static final String MANTIS_ID_CF_NAME = "Mantis ID";
    public static final DecimalFormat MANTIS_ID_FORMAT = new DecimalFormat("0000");
    public static final String MANTIS_ID_TYPE = "importid";
    private static final String MANTIS_ID_SEARCHER = "exactnumber";
    private OptionsManager optionsManager;
    private ExternalUtils externalUtils;
    private CommentManager commentManager;
    private IssueFactory issueFactory;
    private final ProjectService projectService;
    private final UserUtil userUtil;

    public MantisImportBean(IssueIndexManager indexManager,
                            GenericDelegator genericDelegator,
                            ProjectManager projectManager,
                            PermissionSchemeManager permissionSchemeManager,
                            CacheManager cacheManager,
                            IssueManager issueManager,
                            VersionManager versionManager,
                            ProjectComponentManager projectComponentManager,
                            CustomFieldManager customFieldManager,
                            IssueTypeScreenSchemeManager issueTypeScreenSchemeManager,
                            PermissionManager permissionManager,
                            OptionsManager optionsManager,
                            ExternalUtils externalUtils,
                            CommentManager commentManager,
                            IssueFactory issueFactory,
                            ProjectService projectService,
                            UserUtil userUtil)
    {
        this.indexManager = indexManager;
        this.genericDelegator = genericDelegator;
        this.projectManager = projectManager;
        this.permissionSchemeManager = permissionSchemeManager;
        this.cacheManager = cacheManager;
        this.issueManager = issueManager;
        this.versionManager = versionManager;
        this.projectComponentManager = projectComponentManager;
        this.customFieldManager = customFieldManager;
        this.issueTypeScreenSchemeManager = issueTypeScreenSchemeManager;
        this.permissionManager = permissionManager;
        this.optionsManager = optionsManager;
        this.externalUtils = externalUtils;
        this.commentManager = commentManager;
        this.issueFactory = issueFactory;
        this.projectService = projectService;
        this.userUtil = userUtil;
    }

    /**
     * Get a map of available Mantis projects and their id's.
     *
     * @param connectionBean connection bean
     * @return map of product names to product IDs, never null
     * @throws SQLException if database problem occurs
     */
    public static Map /* <String, Integer> */ getAllMantisProjects(DatabaseConnectionBean connectionBean)
            throws SQLException
    {
        PreparedStatement preparedStatement = null;
        try
        {
            preparedStatement = connectionBean.getConnection().prepareStatement("select * from mantis_project_table");
            ResultSet resultSet = preparedStatement.executeQuery();
            Map projects = new HashMap();
            while (resultSet.next())
            {
                String product = resultSet.getString("name");
                Integer productId = new Integer(resultSet.getInt("id"));
                projects.put(product, productId);
            }
            return projects;
        }
        finally
        {
            if (preparedStatement != null)
            {
                try
                {
                    preparedStatement.close();
                }
                catch (SQLException e)
                {
                    log4jLog.warn("Could not close prepared statement for getAllMantisProjects", e);
                }
            }
            connectionBean.closeConnection();
        }
    }

    /**
     * Main method of this bean.  Creates JIRA projects mirroring those found in a Mantis database.
     *
     * @param mappingBean          Mappings from Mantis to JIRA, including project key, statuses, etc
     * @param enableNotifications  Whether to send email notifications for newly created issues
     * @param reuseExistingUsers   Do we try to reuse existing users, or doImport a unique user for every Mantis user?
     * @param addToDevelopersGroup Whether to add new users to the 'jira-developers' group
     * @param reindex              Whether to reindex after the import
     * @param projectNames         Array of Mantis project names to import
     * @param importer             User doing the import
     */
    public void doImport(MappingBean mappingBean, DatabaseConnectionBean connectionBean, boolean enableNotifications, boolean reuseExistingUsers, boolean addToDevelopersGroup, boolean reindex, String[] projectNames, User importer) throws Exception, IndexException, GenericEntityException
    {
        importLog = new StringBuffer(1024 * 30);

        this.mappingBean = mappingBean;
        this.reuseExistingUsers = reuseExistingUsers;
        this.addToDevelopersGroup = addToDevelopersGroup;

        this.importer = importer;

        if (projectNames.length == 0)
        {
            log("No projects selected for import");
            return;
        }

        createOrFindMantisIdCustomField();
        createOrFindCustomResolution();

        try
        {
            ImportUtils.setSubvertSecurityScheme(true);  // before the reindex, so pico-instantiated-during-reindex components don't get the wrong thing. JRA-7464
            if (reindex)
            {
                ImportUtils.setIndexIssues(false);
            }
            ImportUtils.setEnableNotifications(enableNotifications);

            long starttime = System.currentTimeMillis();
            String selectedProjects = getProjectList(projectNames);
            Connection conn = connectionBean.getConnection();

            createOrFindCustomFields(conn);

            importProjects(conn, projectNames, selectedProjects);

            rewriteBugLinks();

            ImportUtils.setSubvertSecurityScheme(false);
            if (reindex)
            {
                ImportUtils.setIndexIssues(true);
                log("Reindexing (this may take a while)...");
                indexManager.reIndexAll();
            }

            long endtime = System.currentTimeMillis();
            log("\nImport SUCCESS and took: " + (endtime - starttime) + " ms.");
        }
        finally
        {
            connectionBean.closeConnection();
            ImportUtils.setSubvertSecurityScheme(false); // do again just in case we failed before the reindex
            ImportUtils.setIndexIssues(true);
            if (!enableNotifications)
            {
                ImportUtils.setEnableNotifications(true);
            }
        }
    }

    /**
     * This method will determine all the users that will need to exist in JIRA to successfully import the
     * specified projects and will return the users that do not yet exist.
     *
     * @param connectionBean initialized connection bean
     * @param projectNames   the projects, by bugzilla project name, that you want to import.
     * @return Set <ExternalUser> all the users that will need to exist in JIRA but do not yet.
     */
    public Set getNonExistentAssociatedUsers(DatabaseConnectionBean connectionBean, String[] projectNames)
    {
        return ImportUtils.getNonExistentUsers(getAssociatedUsers(connectionBean, projectNames));
    }

    /**
     * Creates a 'Mantis Id' custom field displaying the Mantis ID that a JIRA issue originated from, linking to the Mantis instance.
     */
    private void createOrFindMantisIdCustomField() throws GenericEntityException
    {
        CustomFieldType numericFieldCFType = customFieldManager.getCustomFieldType(CreateCustomField.FIELD_TYPE_PREFIX + MANTIS_ID_TYPE);
        CustomFieldSearcher numericSearcher = customFieldManager.getCustomFieldSearcher(CreateCustomField.FIELD_TYPE_PREFIX + MANTIS_ID_SEARCHER);

        if (numericFieldCFType != null)
        {
            mantisIdCustomField = customFieldManager.getCustomFieldObjectByName(MANTIS_ID_CF_NAME);
            if (mantisIdCustomField == null)
            {
                mantisIdCustomField = customFieldManager.createCustomField(MANTIS_ID_CF_NAME,
                        MANTIS_ID_CF_NAME,
                        numericFieldCFType,
                        numericSearcher, EasyList.build(GlobalIssueContext.getInstance()), EasyList.buildNull());
                externalUtils.associateCustomFieldWithScreen(mantisIdCustomField, null);
            }
        }
        else
        {
            log("WARNING: FieldType '" + MANTIS_ID_CF_NAME + "' is required for Mantis Ids but has not been configured. ID fields will not be created");
        }
    }

    /**
     * Create custom fields matching those in Mantis.
     */
    private void createOrFindCustomFields(Connection conn) throws SQLException, GenericEntityException
    {
        log("\n\nRecreating Mantis custom fields in JIRA..");
        mantisCustomFieldTypes.clear();
        mantisToJiraCustomFieldInstanceMap.clear();
        defaultedCustomFieldValues.clear();

        PreparedStatement customfieldOptionsPS = null;
        PreparedStatement customfieldPS = null;
        ResultSet resultSet = null;
        try
        {
            customfieldOptionsPS = conn.prepareStatement("select distinct value from mantis_custom_field_string_table where field_id=?");

            customfieldPS = conn.prepareStatement("select * from mantis_custom_field_table");
            resultSet = customfieldPS.executeQuery();
            while (resultSet.next())
            {
                Integer mantisCFId = new Integer(resultSet.getInt("id"));
                String mantisCFName = resultSet.getString("name");
                Integer mantisCFType = new Integer(resultSet.getInt("type"));

                // Look up the JIRA CF type for this custom field and add it to a mapping
                String jiraCFTypeName = (String) MANTIS_TO_JIRA_CF_TYPE_MAPPINGS.get(mantisCFType);
                if (jiraCFTypeName == null)
                {
                    throw new RuntimeException("No JIRA custom field type mapped for Mantis type " + mantisCFType);
                }
                mantisCustomFieldTypes.put(mantisCFId, mantisCFType);

                CustomField cf = customFieldManager.getCustomFieldObjectByName(mantisCFName);
                if (cf == null)
                {
                    // Custom field doesn't yet exist in JIRA; create it
                    CustomFieldType jiraCFType = customFieldManager.getCustomFieldType(CreateCustomField.FIELD_TYPE_PREFIX + jiraCFTypeName);
                    Object jiraCFSearcherName = MANTIS_TO_JIRA_CF_SEARCHER_MAPPINGS.get(mantisCFType);
                    if (jiraCFSearcherName == null)
                    {
                        throw new RuntimeException("No JIRA custom field searcher mapped for mantis type " + mantisCFType);
                    }
                    CustomFieldSearcher jcfSearcher = customFieldManager.getCustomFieldSearcher(CreateCustomField.FIELD_TYPE_PREFIX + jiraCFSearcherName);

                    log("Custom field '" + mantisCFName + "' not found; creating..");
                    cf = customFieldManager.createCustomField(
                            mantisCFName,
                            null,
                            jiraCFType,
                            jcfSearcher,
                            EasyList.build(GlobalIssueContext.getInstance()), EasyList.buildNull()
                    );

                    createCustomFieldOptions(cf, customfieldOptionsPS, mantisCFType, mantisCFId);

                    externalUtils.associateCustomFieldWithScreen(cf, null);
                    mantisToJiraCustomFieldInstanceMap.put(mantisCFId, cf);
                }
                else
                {
                    // Custom field already exists; check its type and add to the map
                    if (!(CreateCustomField.FIELD_TYPE_PREFIX + jiraCFTypeName).equals(cf.getCustomFieldType().getKey()))
                    {
                        throw new RuntimeException("ERROR: Found existing JIRA custom field '" + mantisCFName + "', but it is not of expected type " + jiraCFTypeName + " (mantis type " + mantisCFType + ")");
                    }

                    log("Using existing JIRA custom field '" + mantisCFName + "'");
                    mantisToJiraCustomFieldInstanceMap.put(mantisCFId, cf);
                }
                // Need to record default values, and set CFs to this value for bugs that don't have an explicit value
                // String defaultValue = resultSet.getString("default_value");
                String cfDefaultValue = resultSet.getString("default_value");
                if (!"".equals(cfDefaultValue))
                {
                    Object jiraCFValue = mantisToJIRACustomFieldValueConverter(mantisCFType, cfDefaultValue, cf);
                    defaultedCustomFieldValues.put(cf, jiraCFValue);
                }
            }
        }
        finally
        {
            if (resultSet != null)
            {
                resultSet.close();
            }
            if (customfieldPS != null)
            {
                customfieldPS.close();
            }
            if (customfieldOptionsPS != null)
            {
                customfieldOptionsPS.close();
            }
        }
    }

    /**
     * Given a custom field, check if its values are discrete (eg. select-list) and create JIRA equivalents (eg. select-list options).
     */
    private void createCustomFieldOptions(CustomField cf, PreparedStatement ps, Integer mantisCFType, Integer mantisCFId)
            throws SQLException
    {
        if (customFieldTypeHasDiscreteValues(mantisCFType))
        {
            ResultSet rs = null;
            try
            {
                ps.setInt(1, mantisCFId.intValue());
                rs = ps.executeQuery();
                IssueContextImpl globalIssueContext = new IssueContextImpl((Long) null, (String) null);
                FieldConfig fieldConfig = cf.getRelevantConfig(globalIssueContext);
                Options options = optionsManager.getOptions(fieldConfig);
                List addedOptions = new ArrayList(); // I can't figure out how to query Options to see what it has, so we'll use a separate list for that.
                while (rs.next())

                {
                    String opts = rs.getString("value");
                    List optList = parseCustomFieldOptionValue(opts);
                    Iterator iter = optList.iterator();
                    while (iter.hasNext())
                    {
                        String opt = (String) iter.next();
                        opt = opt.trim();
                        if (!addedOptions.contains(opt))
                        {
                            log("\tAdding option '" + opt + "' to custom field '" + cf.getName() + "' of type " + cf.getCustomFieldType().getName());
                            options.addOption(null, opt);
                            addedOptions.add(opt);
                        }
                        else
                        {
                            log("\tAlready have option '" + opt + "'");
                        }
                    }
                }
                optionsManager.updateOptions(options);
                log("\tUpdated options for " + cf.getName());
            }
            finally
            {
                if (rs != null)
                {
                    rs.close();
                }
            }
        }
    }

    /**
     * Parse Mantis's internal representation of a select-list field value.
     *
     * @param opt Eg. '|a|b|c'
     * @return List of strings {"a","b","c"}
     */
    private List parseCustomFieldOptionValue(String opt)
    {
        StringTokenizer tok = new StringTokenizer(opt, "|");
        ArrayList opts = new ArrayList();
        while (tok.hasMoreElements())
        {
            opts.add(tok.nextElement());
        }
        return opts;
    }

    /**
     * Parse Mantis's internal representation of a date.
     *
     * @param opt Time in seconds since 1970, '1147615200'
     * @return Timestamp representing the date.
     */
    private Timestamp parseCustomFieldDateValue(String opt)
    {
        if ("".equals(opt))
        {
            return null;
        }
        return new Timestamp(Long.parseLong(opt) * 1000);
    }

    private boolean customFieldTypeHasDiscreteValues(Integer mantisCFType)
    {
        return (mantisCFType.equals(MANTIS_CF_TYPE_CHECKBOX) ||
                mantisCFType.equals(MANTIS_CF_TYPE_ENUM) ||
                mantisCFType.equals(MANTIS_CF_TYPE_LIST) ||
                mantisCFType.equals(MANTIS_CF_TYPE_MULTILIST));
    }

    /**
     * Creates the 'Not A Bug' custom resolution type.
     */
    private void createOrFindCustomResolution() throws GenericEntityException
    {
        String type = "Resolution";
        String name = "Not A Bug";
        if (CoreFactory.getGenericDelegator().findByAnd(type, EasyMap.build("name", name)).size() == 0)
        {
            priorityResolutionId = Integer.parseInt(EntityUtils.getNextStringId(type));

            Map fields = new HashMap();
            fields.put("id", "" + priorityResolutionId);
            fields.put("name", name);
            fields.put("description", "The issue is not a bug");
            fields.put("sequence", new Long(priorityResolutionId)); // fixme: here we assume id == sequence
            EntityUtils.createValue(type, fields);
        }
    }

    /**
     * Generate SQL-friendly quoted comma-separated list of projects.
     */
    public String getProjectList(String[] selectList)
    {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < selectList.length; i++)
        {
            sb.append("'").append(selectList[i]).append("'");
            if (i != selectList.length - 1)
            {
                sb.append(", ");
            }
        }
        return sb.toString();
    }

    protected void importIssues(Connection conn, String projectName, int projectId) throws Exception, InvalidRoleException, InvalidInputException, CreateException, SQLException
    {
        int count = 0;
        // For debugging - set to limit the number of imported issues
        int maxCount = -1;
        log("\n\nImporting Issues from project " + projectName);

        // use the changeItem importLog to retrieve the list of issues previously imported from Mantis
        issueKeys = retrieveImportedIssues();

        PreparedStatement preparedStatement = conn.prepareStatement("SELECT * FROM mantis_bug_table where project_id = ?");
        preparedStatement.setInt(1, projectId);

        ResultSet resultSet = preparedStatement.executeQuery();
        PreparedStatement textPrepStatement = conn.prepareStatement("SELECT * FROM mantis_bug_text_table WHERE id = ?");
        PreparedStatement attachPrepStatement = conn.prepareStatement("SELECT * FROM mantis_bug_file_table WHERE bug_id = ? ORDER BY date_added ASC");
        PreparedStatement cfPrepStatement = conn.prepareStatement("SELECT * FROM mantis_custom_field_string_table WHERE bug_id = ?");
        while (resultSet.next())
        {
            log("Importing Issue: \"" + resultSet.getString("summary") + "\"");

            //if this is a mantis issue that hasn't been imported before
            int issueId = resultSet.getInt("id");
            int bugTextID = resultSet.getInt("bug_text_id");
            if (!issueKeys.containsKey(new Integer(issueId)))
            {
                textPrepStatement.setInt(1, bugTextID);

                ResultSet textResultSet = textPrepStatement.executeQuery();
                textResultSet.next();

                GenericValue issue = importIssue(resultSet, conn, textResultSet, projectName);
                importComments(conn, resultSet.getInt("id"), issue);
                importAttachments(conn, attachPrepStatement, resultSet.getInt("id"), issue);
                importCustomFieldValues(conn, cfPrepStatement, resultSet.getInt("id"), issue);

                //reindex the issue.
                indexManager.reIndex(issue);

                count++;
                if (maxCount > 0 && count > maxCount)
                {
                    log("Imported maximum of " + maxCount + " issues");
                    break;
                }
            }
            else
            {
                log("Issue: \"" + resultSet.getString("summary") + "\" already exists. Not Imported");
            }
        }
        log(count + " issues imported.");

        closePS(preparedStatement);
        closePS(textPrepStatement);
        closePS(attachPrepStatement);
        closePS(cfPrepStatement);
    }

    /**
     * Import an issue from Mantis
     *
     * @param resultSet     mantis_bug_table row
     * @param conn          JDBC Connection
     * @param textResultSet mantis_bug_text_table row
     * @param projectName   Name of issue's project
     * @return Imported Issue
     */
    protected GenericValue importIssue(ResultSet resultSet, Connection conn, ResultSet textResultSet, String projectName) throws Exception, InvalidRoleException, InvalidInputException, CreateException, SQLException
    {
        String componentName = resultSet.getString("category");

        MutableIssue issueObject = IssueImpl.getIssueObject(null);
        issueObject.setProject(getProject(projectName));
        issueObject.setDescription(getDescription(textResultSet));

        User reporter = getUser(conn, resultSet.getInt("reporter_id"));
        if (reporter != null)
        {
            // Anonymous bugs may have any reporter_id value - perhaps this happens when users are deleted?
            issueObject.setReporter(reporter);
        }

        if (resultSet.getInt("handler_id") != 0)
        {
            User assignee = getUser(conn, resultSet.getInt("handler_id"));
            if (assignee != null)
            {
                issueObject.setAssignee(assignee);
            }
        }

        // A severity of 10 means this is a feature request, not a bug
        if (resultSet.getInt("severity") == 10) // todo put this in the mappingbean
        {
            issueObject.setIssueTypeId("" + IssueFieldConstants.NEWFEATURE_TYPE_ID);
            issueObject.setPriorityId("" + getMantisFeaturePriority());
        }
        else
        {
            issueObject.setIssueTypeId("" + IssueFieldConstants.BUG_TYPE_ID);
            issueObject.setPriorityId(mappingBean.getPriority(resultSet.getInt("severity")));
        }
        issueObject.setSummary(escapeMantisString(resultSet.getString("summary")));
        issueObject.setUpdated(resultSet.getTimestamp("last_updated"));
        issueObject.setCreated(resultSet.getTimestamp("date_submitted"));

        // setup the associations with components/versions
        String version = resultSet.getString("version");
        String fixForVersion = resultSet.getString("fixed_in_version");
        createVersionAndComponentAssociations(issueObject, projectName, version, fixForVersion, componentName);

        Map fields = new HashMap();
        fields.put("issue", issueObject);
        GenericValue origianlIssueGV = ComponentManager.getInstance().getIssueManager().getIssue(issueObject.getId());
        fields.put(WorkflowFunctionUtils.ORIGINAL_ISSUE_KEY, IssueImpl.getIssueObject(origianlIssueGV));

        GenericValue issue = issueManager.createIssue(importer, fields);

        int jiraBugStatus = mappingBean.getStatus(resultSet.getInt("status"));
        issue.set(IssueFieldConstants.STATUS, "" + jiraBugStatus);

        // make sure no resolution if the issue is unresolved
        if (jiraBugStatus != IssueFieldConstants.RESOLVED_STATUS_ID && jiraBugStatus != IssueFieldConstants.CLOSED_STATUS_ID)
        {
            issue.set(IssueFieldConstants.RESOLUTION, null);
        }
        else
        {
            final String resolution = mappingBean.getResolution(resultSet.getInt("resolution"));
            issue.set(IssueFieldConstants.RESOLUTION, resolution);
            //If the issue is resolved, also set the resolution date (the mapping may return null meaning unresolved).
            //We'll use the last updated time for this, since mantis doesn't seem to store a resolution date.
            if(resolution != null)
            {
                issue.set(IssueFieldConstants.RESOLUTION_DATE, resultSet.getTimestamp("last_updated"));
            }
        }

        issue.store();
        setCurrentWorkflowStep(issue);

        createChangeHistory(resultSet.getInt("id"), issue);
        createMantisIdCustomFieldValue(resultSet.getInt("id"), issue);
        issueKeys.put(new Integer(resultSet.getInt("id")), issue.getLong("id"));

        return issue;
    }

    /**
     * In Mantis, 'features' are just 'bugs' with a priority of 'feature'.
     * In JIRA we have a separate 'Feature' entity, which needs to be assigned a priority.
     * This method chooses that default priority.
     */
    protected int getMantisFeaturePriority()
    {
        return IssueFieldConstants.MAJOR_PRIORITY_ID;
    }

    //    private String getFixForVersions(ResultSet resultSet)
    //    {
    //    }
    private String getDescription(ResultSet textResultSet) throws SQLException
    {
        StringBuffer buf = new StringBuffer();
        buf.append(textResultSet.getString("description"));
        if (!"".equals(textResultSet.getString("steps_to_reproduce").trim()))
        {
            buf.append("\r\n\r\n****** STEPS TO REPRODUCE ******\r\n\r\n").append(textResultSet.getString("steps_to_reproduce"));
        }
        if (!"".equals(textResultSet.getString("additional_information").trim()))
        {
            buf.append("\r\n\r\n****** ADDITIONAL INFORMATION ******\r\n\r\n").append(textResultSet.getString("additional_information"));
        }
        return escapeMantisString(buf.toString());
    }

    public String escapeMantisString(String str)
    {
        String newString = null;
        newString = RegexpUtils.replaceAll(str, "<a (?:target=\"_new\" )?href=['\"](?:mailto:)?(.*?)['\"](?: target=\"_new\")?>.*</a>", "$1");

        newString = RegexpUtils.replaceAll(newString, "&quot;", "\"");
        newString = RegexpUtils.replaceAll(newString, "&lt;", "<");
        newString = RegexpUtils.replaceAll(newString, "&gt;", ">");
        newString = RegexpUtils.replaceAll(newString, "&amp;", ">");
        return newString;
    }

    public String rewriteMantisBuglinksInText(String str)
    {
        String newStr = RegexpUtils.replaceAll(str, "Bug#", "#");
        StringBuffer buf = new StringBuffer(str.length() + 100);
        StringTokenizer tok = new StringTokenizer(newStr, "#", true);
        boolean inLink = false;
        while (tok.hasMoreElements())
        {
            String s = tok.nextToken();
            if (!inLink && "#".equals(s) && tok.hasMoreTokens())
            {
                inLink = true;
            }
            else if (inLink) // Previous token was a '#'
            {
                Integer mantisId = getMantisIdFromString(s);
                if (mantisId != null)  // Digits after # are integers
                {
                    Long jiraId = (Long) issueKeys.get(mantisId);
                    if (jiraId != null) // Digits are the ID of an imported issue
                    {
                        try
                        {
                            buf.append(issueManager.getIssue(jiraId).getString("key"));
                            buf.append(s.substring(("" + mantisId).length()));
                        }
                        catch (Exception e)
                        {
                            String err = "Error looking up issue " + jiraId + " (from mantis id " + mantisId + ")";
                            log(err);
                            buf.append("#").append(s);
                        }
                    }
                    else // Digits don't match ID of imported issue
                    {
                        String err = "Could not find JIRA bug corresponding to referenced mantis id " + mantisId;
                        log(err);
                        buf.append("#").append(s);
                    }
                }
                else
                {
                    // No Mantis ID matching string
                    buf.append("#").append(s);
                }
                inLink = false;
            }
            else // Regular non-link text
            {
                buf.append(s);
            }
        }
        return buf.toString();
    }

    /**
     * Return an integer prefix of a string, if any.
     */
    public Integer getMantisIdFromString(String s)
    {
        if (s.length() == 0 || !Character.isDigit(s.charAt(0)))
        {
            return null;
        }

        StringBuffer buf = new StringBuffer(5);
        for (int i = 0; i < Math.min(6, s.length()); i++)
        {
            char c = s.charAt(i);
            if (Character.isDigit(c))
            {
                buf.append(c);
            }
            else if (Character.isLetter(c))
            {
                return null;
            }
            else
            {
                break;
            }
        }
        return new Integer(Integer.parseInt(buf.toString()));
    }

    private void createVersionAndComponentAssociations(MutableIssue issue, String project, String version, String fixForVersion, String component) throws GenericEntityException
    {
        Version verKey = getVersion(project + ":" + version);
        if (verKey != null)
        {
            Version affectsVersion = versionManager.getVersion(verKey.getLong("id"));
            issue.setAffectedVersions(EasyList.build(affectsVersion));
        }

        Version fixForVerKey = getVersion(project + ":" + fixForVersion);
        if (fixForVerKey != null)
        {
            Version ffVersion = versionManager.getVersion(fixForVerKey.getLong("id"));
            issue.setFixVersions(EasyList.build(ffVersion));
        }

        GenericValue comp = getComponent(project + ":" + component);
        if (comp != null)
        {
            GenericValue affectsComponent = projectManager.getComponent(comp.getLong("id"));
            issue.setComponents(EasyList.build(affectsComponent));
        }
    }

    /**
     * Given an issue, update the underlying workflow, so that it matches the issues status.
     */
    private void setCurrentWorkflowStep(GenericValue issue) throws GenericEntityException
    {
        // retrieve the wfCurrentStep for this issue and change it
        Collection wfCurrentStepCollection = genericDelegator.findByAnd("OSCurrentStep", EasyMap.build("entryId", issue.getLong("workflowId")));
        GenericValue wfCurrentStep = (GenericValue) getOnly(wfCurrentStepCollection);
        wfCurrentStep.set("stepId", new Integer(mappingBean.getWorkflowStep(Integer.parseInt(issue.getString("status")))));
        wfCurrentStep.set("status", "" + mappingBean.getWorkflowStatus(Integer.parseInt(issue.getString("status"))));
        wfCurrentStep.store();
    }

    protected void importComments(Connection conn, int bug_id, GenericValue issue) throws Exception, GenericEntityException
    {
        PreparedStatement bugnotePS = conn.prepareStatement("SELECT * FROM mantis_bugnote_table WHERE bug_id = ? ORDER BY date_submitted ASC");
        PreparedStatement bugnoteTextPS = conn.prepareStatement("SELECT * FROM mantis_bugnote_text_table WHERE id = ?");
        bugnotePS.setInt(1, bug_id);

        ResultSet resultSet = bugnotePS.executeQuery();
        while (resultSet.next())
        {
            bugnoteTextPS.setInt(1, resultSet.getInt("bugnote_text_id"));

            ResultSet bugnoteTextRS = bugnoteTextPS.executeQuery();
            if (!bugnoteTextRS.next())
            {
                continue;
            }

            String bugnoteText = bugnoteTextRS.getString("note");
            String escapedBugnoteText = escapeMantisString(bugnoteText);
            if (!permissionManager.hasPermission(Permissions.COMMENT_ISSUE, issue, getUser(conn, resultSet.getInt("reporter_id"))))
            {
                User user = getUser(conn, resultSet.getInt("reporter_id"));
                GenericValue project = projectManager.getProject(issue.getLong("project"));
                log("You (" + (user == null ? "null" : user.getFullName()) + ") do not have permission to comment on an issue in project: " + (project == null ? "null" : project.getString("name")));
            }
            else
            {
                int reporterId = resultSet.getInt("reporter_id");
                User reporter = getUser(conn, reporterId);
                String reporterName = (reporter != null ? reporter.getName() : "");
                if (reporter == null)
                {
                    log("Error: bug " + bug_id + " (issue " + issue.getString("key") + ") has comment with unimported/nonexistent reporter id " + reporterId + "; commenting anonymously.");
                }
                Date timePerformed = resultSet.getTimestamp("date_submitted");
                commentManager.create(
                        issueFactory.getIssue(issue),
                        reporterName,
                        reporterName,
                        escapedBugnoteText,
                        null,
                        null,
                        timePerformed,
                        timePerformed,
                        false,
                        false);
            }
        }

        issue.store();
        closePS(bugnotePS);
        closePS(bugnoteTextPS);
    }

    /**
     * Store the original mantis bug id in the change history.
     */
    protected void createChangeHistory(int bug_id, GenericValue issue) throws Exception
    {
        // doImport a change group and change item for each issue imported to record the original Mantis id.
        // change items used to make sure issues are not duplicated
        List changeItems = EasyList.build(new ChangeItemBean(ChangeItemBean.STATIC_FIELD, MANTIS_CHANGE_ITEM_FIELD, null, Integer.toString(bug_id), null, issue.getLong("id").toString()));
        ChangeLogUtils.createChangeGroup(importer, issue, issue, changeItems, true);
    }

    protected void createMantisIdCustomFieldValue(int bug_id, GenericValue issue) throws Exception
    {
        // doImport a change group and change item for each issue imported to record the original Mantis id.
        // change items used to make sure issues are not duplicated
        if (mantisIdCustomField != null)
        {
            mantisIdCustomField.createValue(IssueImpl.getIssueObject(issue), new Double(bug_id));
        }

    }

    /**
     * Return a map of mantisKey (Integer) -> Jira Issues Id (Integer).
     * <p/>
     * It does this by looking through the change items for the mantis import key.
     *
     * @throws org.ofbiz.core.entity.GenericEntityException
     *
     */
    protected Map retrieveImportedIssues() throws GenericEntityException
    {
        Map previousKeys = new HashMap();

        // get the issues previously imported from Mantis via the change items.
        Collection changeItems = genericDelegator.findByAnd("ChangeItem", EasyMap.build("field", MANTIS_CHANGE_ITEM_FIELD));
        for (Iterator iterator = changeItems.iterator(); iterator.hasNext();)
        {
            GenericValue changeItem = (GenericValue) iterator.next();
            previousKeys.put(new Integer(changeItem.getString("oldstring")), new Long(changeItem.getString("newstring")));
        }
        return previousKeys;
    }

    protected void importComponents(Connection conn, String projectName, int projectId) throws SQLException, GenericEntityException
    {
        int count = 0;
        log("\n\nImporting Components from project(s) " + projectName + "\n");

        PreparedStatement preparedStatement = conn.prepareStatement("SELECT * FROM mantis_project_category_table where project_id = ?");
        preparedStatement.setInt(1, projectId);

        ResultSet resultSet = preparedStatement.executeQuery();
        while (resultSet.next())
        {
            boolean created = createComponent(projectName, resultSet.getString("category"), "");
            if (created)
            {
                count++;
            }
        }
        log(count + " components imported.");
        closePS(preparedStatement);
    }

    protected boolean createComponent(String projectName, String componentName, String description) throws GenericEntityException
    {
        log("Importing Component: " + componentName);

        GenericValue project = getProject(projectName);
        GenericValue existingComponent = projectManager.getComponent(project, componentName);

        // if the componentName exists already, do not import
        if (existingComponent != null)
        {
            log("Component " + componentName + " in Project: " + projectName + " already exists. Not imported");
            componentKeys.put(projectName + ":" + componentName, existingComponent);
            return false;
        }
        else
        {
            GenericValue componentGV = null;
            try
            {
                final ProjectComponent projectComponent = projectComponentManager.create(componentName, null, null, 0, project.getLong("id"));
                componentGV = projectComponentManager.convertToGenericValue(projectComponent);

                // imported components are stored for use later
                componentKeys.put(projectName + ":" + componentName, componentGV);
                return true;
            }
            catch (Exception e)
            {
                log("Error importing Component: " + componentName);
                log(ExceptionUtils.getStackTrace(e));
                return false;
            }
        }
    }

    protected void importVersions(Connection conn, String projectName, int projectId) throws SQLException, GenericEntityException
    {
        log("\n\nImporting Versions for " + projectName + "\n");

        int count = 0;
        PreparedStatement preparedStatement = conn.prepareStatement("SELECT version FROM mantis_project_version_table where project_id = ?");
        preparedStatement.setInt(1, projectId);

        ResultSet resultSet = preparedStatement.executeQuery();
        while (resultSet.next())
        {
            String versionName = resultSet.getString("version");
            log("Importing Version: " + versionName);

            //            boolean created = createVersion(getProductName(resultSet, conn, false), versionName);
            if (versionName == null || "".equals(versionName))
            {
                throw new RuntimeException("Empty version for project " + projectName);
            }

            boolean created = createVersion(projectName, versionName);
            if (created)
            {
                count++;
            }
        }
        log(count + " versions imported.");
        closePS(preparedStatement);
    }

    protected boolean createVersion(String project, String versionName) throws GenericEntityException
    {
        Version existingVersion = versionManager.getVersion(getProject(project), versionName);
        if (existingVersion != null)
        {
            log("Version: " + versionName + " in Project: " + project + " already exists. Not imported");
            versionKeys.put(project + ":" + versionName, existingVersion);
            return false;
        }
        else
        {
            Version version = null;
            try
            {
                version = versionManager.createVersion(versionName, null, null, getProject(project), null);
                versionKeys.put(project + ":" + versionName, version);
                return true;
            }
            catch (Exception e)
            {
                log("Error importing Version: " + versionName);
                log(ExceptionUtils.getStackTrace(e));
                return false;
            }
        }
    }

    protected void importProjects(Connection conn, String[] selectedProjects, String selectedProjectsDisplay) throws Exception, GenericEntityException, InvalidInputException
    {
        int count = 0;
        log("\n\nImporting project(s) " + selectedProjectsDisplay);

        PreparedStatement preparedStatement;
        ResultSet resultSet;
        StringBuffer projectNameTokens = new StringBuffer();
        for (int i = 0; i < selectedProjects.length; i++)
        {
            projectNameTokens.append(" ? ");
            if ((i + 1) < selectedProjects.length)
            {
                projectNameTokens.append(", ");
            }
        }
        preparedStatement = conn.prepareStatement("Select * from mantis_project_table where name in (" + projectNameTokens.toString() + ")");

        for (int i = 0; i < selectedProjects.length; i++)
        {
            String projectName = selectedProjects[i];
            preparedStatement.setString(i + 1, projectName);
        }

        resultSet = preparedStatement.executeQuery();
        while (resultSet.next())
        {
            String project = resultSet.getString("name");
            String description = resultSet.getString("description");
            int id = resultSet.getInt("id");

            log("Importing Project: " + project);

            boolean created = createProject(project, description);
            if (created)
            {
                count++;
                importVersions(conn, project, id);
                importComponents(conn, project, id);
                importIssues(conn, project, id);
            }
        }
        log(count + " projects imported.");
        closePS(preparedStatement);
    }

    protected boolean createProject(String product, String description) throws GenericEntityException
    {
        GenericValue existingProject = projectManager.getProjectByName(product);
        if (existingProject != null)
        {
            log("Project: " + product + " already exists. Not imported");
            projectKeys.put(product, existingProject);
            return false;
        }
        else
        {
            GenericValue project = null;
            try
            {
                String key = mappingBean.getProjectKey(product);
                String lead = mappingBean.getProjectLead(product);
                String projName = mappingBean.getProjectName(product);

                ErrorCollection errorCollection = new SimpleErrorCollection();

                JiraServiceContext serviceContext = new JiraServiceContextImpl(importer, errorCollection);
                if (projectService.isValidRequiredProjectData(serviceContext, projName, key, lead))
                {
                    project = ProjectUtils.createProject(EasyMap.build("key", key, "lead", lead, "name", projName, "description", description));

                    //Add the default permission scheme for this project
                    permissionSchemeManager.addDefaultSchemeToProject(project);
                    // Add the default issue type screen scheme for this project
                    issueTypeScreenSchemeManager.associateWithDefaultScheme(project);
                    projectKeys.put(product, project);
                    return true;
                }
                else
                {
                    log("Error importing Project: " + product);
                    log(errorCollection.toString());
                    return false;
                }
            }
            catch (Exception e)
            {
                log("Error importing Project: " + product);
                log(ExceptionUtils.getStackTrace(e));
                return false;
            }
        }
    }


    protected void importUser(Connection conn, int mantisId) throws SQLException
    {
        PreparedStatement preparedStatement = conn.prepareStatement("SELECT * FROM mantis_user_table where id = ?");
        preparedStatement.setInt(1, mantisId);

        ResultSet resultSet = preparedStatement.executeQuery();
        importUserFrom(resultSet);
        closePS(preparedStatement);
    }

    private int importUserFrom(ResultSet resultSet) throws SQLException
    {
        int count = 0;
        String mantisEmail = null;
        String mantisUsername = null;
        String password = null;
        while (resultSet.next())
        {
            // user name is mantis's email/login and changed into lower case
            mantisEmail = TextUtils.noNull(resultSet.getString("email")).trim();
            mantisUsername = getMantisUsername(resultSet);

            int mantisUserId = resultSet.getInt("id");

            password = null;
            password = TextUtils.noNull(resultSet.getString("password")).trim(); // we can't yet handle non-plaintext passwords

            String fullName = null;
            try
            {
                fullName = resultSet.getString("realname");
            }
            catch (SQLException e)
            {
                // Mantis 0.17 or earlier - no real name stored, so try to infer it from the email
                fullName = getFullNameFromEmail(mantisEmail, mantisUsername);
            }

            User createdUser = createUser(mantisEmail, mantisUsername, mantisUserId, fullName, password);
            if (createdUser != null)
            {
                count++;
            }
        }
        return count;
    }

    /**
     * Return a JIRA username from a row in the Mantis user table.
     * This can be overridden to map usernames from Mantis to JIRA (see {@link CustomMantisImportBean} for an example.
     */
    protected String getMantisUsername(ResultSet resultSet) throws SQLException
    {
        return TextUtils.noNull(resultSet.getString("username")).trim();
    }

    protected User createUser(String mantisEmail, String mantisUsername, int mantisUserid, String fullName, String password)
    {
        log("Importing User: " + mantisEmail);

        User user = null;
        try
        {
            user = UserUtils.getUser(mantisUsername);
            if (user != null)
            {
                log("User: " + mantisUsername + " already exists. Not imported");
                userKeys.put(new Integer(mantisUserid), user);
                return user;
            }
        }
        catch (EntityNotFoundException e)
        {
            try
            {
                // Mantis uses the email address as user id, whereas JIRA has a distinct string for this.
                // Here we check if the email address is currently owned by a user, to prevent a new user being
                // created if a logically identical one exists
                if (reuseExistingUsers)
                {
                    try
                    {
                        User existingUser = UserUtils.getUserByEmail(mantisEmail);
                        if (existingUser != null)
                        {
                            log("User with email '" + mantisEmail + "' already exists (" + existingUser.getName() + "). Not imported");
                            userKeys.put(new Integer(mantisUserid), existingUser);
                            return null;
                        }
                    }
                    catch (EntityNotFoundException ignored)
                    {
                    }
                }

                // JRA-10393: if Jira is running with a user based license, the active user count will be
                // recalculated every time a user is created. Depending on how many users there are in the system
                // this may incur a performance penalty. If this becomes a problem in the future, we will need
                // to devise a way of creating multiple users without incrementally recalculating the active
                // user count.
                // Also, if the user is going to be created inactive, add an extra log message
                if (!userUtil.canActivateNumberOfUsers(1))
                {
                    log("User with email '" + mantisEmail + "' will be created as an inactive user; user will not be able to log in to JIRA.");
                }                
                user = userUtil.createUserNoEvent(
                        mantisUsername,
                        password,
                        mantisEmail,
                        fullName);
                userKeys.put(new Integer(mantisUserid), user);
                return user;
            }
            catch (Exception exception)
            {
                log("User: " + mantisEmail + " not imported. An error occurred. " + exception.getMessage());
            }
        }
        return null;
    }

    protected void importAttachments(Connection conn, PreparedStatement attachPrepStatement, int bug_id, GenericValue issue) throws Exception
    {
        ResultSet resultSet = null;
        try
        {
            attachPrepStatement.clearParameters();
            attachPrepStatement.setInt(1, bug_id);
            resultSet = attachPrepStatement.executeQuery();
            while (resultSet.next())
            {
                AttachmentManager attachmentManager = ManagerFactory.getAttachmentManager();
                Attachment attachment;

                final Blob fileData = resultSet.getBlob("content");
                boolean attachmentOnDisk = fileData.length() == 0;
                if (attachmentOnDisk)
                {
                    attachment = attachmentManager.createAttachment(issue, null, resultSet.getString("file_type"), cleanMantisFilename(resultSet.getString("filename")), new Long(resultSet.getInt("filesize")), null, UtilDateTime.nowTimestamp());
                }
                else
                {
                    attachment = attachmentManager.createAttachment(issue, null, resultSet.getString("file_type"), resultSet.getString("filename"), new Long(fileData.length()), null, UtilDateTime.nowTimestamp());
                }

                //we need to set the created date back to when it was created in the original system.+
                Date attachmentAdded = resultSet.getTimestamp("date_added");
                attachment.getGenericValue().set("created", attachmentAdded);
                attachment.store();
                log("  Added attachment: \"" + resultSet.getString("filename") + "\" from " + (attachmentOnDisk ? "disk" : "database") + " to " + issue.getString("key"));

                Date issueUpdated = attachmentAdded != null ? attachmentAdded : UtilDateTime.nowTimestamp();
                issue.set("updated", issueUpdated);
                genericDelegator.storeAll(EasyList.build(issue));
                cacheManager.flush(CacheManager.ISSUE_CACHE, issue);

                File jiraAttachFile = AttachmentUtils.getAttachmentFile(attachment);

                //                File jiraAttachFile = new File(AttachmentUtils.getAttachmentDirectory(issue), attachment.getLong("id") + "_" + mungedMantisFilename);
                BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(jiraAttachFile));
                String mantisAttachFilePath = mappingBean.getAttachmentPath() + System.getProperty("file.separator") + resultSet.getString("diskfile");
                logAttachmentLocation(resultSet.getString("diskfile"), jiraAttachFile);

                if (attachmentOnDisk)
                {
                    try
                    {
                        FileUtils.copy(new FileInputStream(mantisAttachFilePath), out);
                    }
                    catch (Exception e)
                    {
                        log("Couldn't find attachment " + mantisAttachFilePath);
                    }
                }
                else
                {
                    File realAttachFile = AttachmentUtils.getAttachmentFile(attachment);
                    BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(realAttachFile));
                    FileUtils.copy(new BufferedInputStream(fileData.getBinaryStream()), bout);
                    bout.close();
                }
                out.close();
            }
        }
        catch (SQLException e)
        {
            log("Error on importing attachments for bug " + bug_id + ". Error:" + e.getMessage());
            e.printStackTrace();
        }
        finally
        {
            resultSet.close();
        }
    }

    /**
     * Create an issue's custom field values.
     * <p/>
     * " @param cfPrepStatement PreparedStatement for "SELECT * FROM mantis_custom_field_string_table WHERE bug_id = ?"
     */
    private void importCustomFieldValues(Connection conn, PreparedStatement cfPrepStatement, int bug_id, GenericValue issue) throws SQLException
    {
        ResultSet resultSet = null;
        try
        {
            cfPrepStatement.clearParameters();
            cfPrepStatement.setInt(1, bug_id);
            resultSet = cfPrepStatement.executeQuery();
            // Loop through custom field values for this issue
            MutableIssue issueObject = IssueImpl.getIssueObject(issue);
            List usedCFs = new ArrayList();
            while (resultSet.next())
            {
                Integer cfId = new Integer(resultSet.getInt("field_id"));
                Integer mantisCFType = (Integer) mantisCustomFieldTypes.get(cfId);
                if (mantisCFType == null)
                {
                    throw new RuntimeException("No Mantis custom field type mapped for custom field with id " + cfId);
                }
                CustomField cf = (CustomField) mantisToJiraCustomFieldInstanceMap.get(cfId);
                if (cf == null)
                {
                    throw new RuntimeException("Mantis bug " + bug_id + " has a custom field with id " + cfId + " (type " + mantisCFType + ") which is not mapped to a custom field in JIRA.");
                }

                String mantisCFObject = resultSet.getString("value");
                log("\tCreating custom field value of type " + mantisCFType + ": " + mantisCFObject);
                Object jiraCFValue = mantisToJIRACustomFieldValueConverter(mantisCFType, mantisCFObject, cf);
                log("\t\tMantis CF value '" + mantisCFObject + "' of type " + mantisCFType + " converting to JIRA CF value '" + jiraCFValue + "'");
                cf.createValue(issueObject, jiraCFValue);
                usedCFs.add(cf);
            }
            // Mantis lets string custom fields have 'default' values which display if a value isn't chosen
            // This isn't possible in JIRA, so we need to create value instances for defaulted custom fields.
            Iterator iter = defaultedCustomFieldValues.keySet().iterator();
            while (iter.hasNext())
            {
                CustomField cf = (CustomField) iter.next();
                if (!usedCFs.contains(cf)) // CF hasn't been populated already - use default
                {
                    Object defaultValue = defaultedCustomFieldValues.get(cf);
                    log("\t\tCreating defaulted custom field value '" + defaultValue + "'");
                    cf.createValue(issueObject, defaultValue);
                }
            }
        }
        finally
        {
            if (resultSet != null)
            {
                resultSet.close();
            }
        }
    }


    //We need to pass through the Mantis CF type rather than the JIRA CF type, since we have the Mantis CF Types defined as constants
    private Object mantisToJIRACustomFieldValueConverter(Integer customFieldType, String mantisCFString, CustomField cf)
    {
        if (customFieldType.equals(MANTIS_CF_TYPE_STRING))
        {
            return cf.getCustomFieldType().getSingularObjectFromString(mantisCFString);
        }
        else if (customFieldType.equals(MANTIS_CF_TYPE_LIST))
        {
            cf.getCustomFieldType().getSingularObjectFromString(mantisCFString);
        }
        else if (customFieldType.equals(MANTIS_CF_TYPE_CHECKBOX))
        {
            return parseCustomFieldOptionValue(mantisCFString);
        }
        else if (customFieldType.equals(MANTIS_CF_TYPE_MULTILIST))
        {
            return parseCustomFieldOptionValue(mantisCFString);
        }
        else if (customFieldType.equals(MANTIS_CF_TYPE_DATE))
        {
            return parseCustomFieldDateValue(mantisCFString);
        }
        return cf.getCustomFieldType().getSingularObjectFromString(mantisCFString);
    }


    /**
     * Return a clean filename from Mantis <bugid>-<filename> internal format.
     */
    public String cleanMantisFilename(String filename)
    {
        if (!Character.isDigit(filename.charAt(0)))
        {
            log4jLog.error("Attachment filename '" + filename + "' does not follow the standard <bugid>-<filename> pattern");
            return filename;
        }
        return filename.substring(filename.indexOf("-") + 1); // strip the '<mantisid>-' prefix from attachment filenames
    }

    /**
     * Hook for recording which Mantis attachment filename maps to which JIRA attachment filename.
     */
    protected void logAttachmentLocation(String diskfile, File jiraAttachFile) throws IOException
    {
        // Noop default implementation
    }

    private GenericValue getProject(String project)
    {
        return (GenericValue) projectKeys.get(project);
    }

    private Version getVersion(String value)
    {
        return (Version) versionKeys.get(value);
    }

    private GenericValue getComponent(String value)
    {
        return (GenericValue) componentKeys.get(value);
    }

    private User getUser(Connection conn, final int mantisUserId) throws SQLException
    {
        final Integer idInt = new Integer(mantisUserId);
        User user = (User) userKeys.get(idInt);
        if (user == null)
        {
            importUser(conn, mantisUserId);
            user = (User) userKeys.get(idInt);

            // user could be null if they were deleted after interacting with a bug
            if (addToDevelopersGroup && user != null)
            {
                user.addToGroup(GroupUtils.getGroup("jira-developers"));
            }
        }

        return (User) userKeys.get(idInt);
    }

    private String getProjectKey(String name, int keylength) throws GenericEntityException
    {
        String potentialKey;
        if (name.length() < keylength)
        {
            potentialKey = name + generatePaddingString(keylength - name.length());
        }
        else
        {
            potentialKey = name.substring(0, keylength);
        }

        if (projectManager.getProjectByKey(potentialKey) != null)
        {
            return getProjectKey(name, ++keylength);
        }
        else
        {
            return potentialKey;
        }
    }

    public String getProjectKey(String name)
    {
        try
        {
            return getProjectKey(name.toUpperCase(), 3); //minimum key length of 3
        }
        catch (Exception e)
        {
            return null; // this should never happen.
        }
    }

    private String generatePaddingString(int length)
    {
        char[] padarray = new char[length];
        for (int i = 0; i < length; i++)
        {
            padarray[i] = 'J';
        }
        return String.valueOf(padarray);
    }

    /**
     * Infers the user's full name from other information.
     * This default implementation returns 'John Smith' for john.smith@example.com, and otherwise defaults to the
     * Mantis username.
     */
    public String getFullNameFromEmail(String email, String username)
    {
        if (email == null)
        {
            return "";
        }
        if (email.indexOf('@') == -1)
        {
            return username;
        }

        String firstPart = email.substring(0, email.indexOf('@'));
        int i = firstPart.indexOf('.');
        if (i == -1)
        {
            return username;
        }
        else if (i + 1 <= firstPart.length() && firstPart.length() > 2)
        {
            StringBuffer buf = new StringBuffer(firstPart.length());
            buf.append(Character.toUpperCase(firstPart.charAt(0)));
            buf.append(firstPart.substring(1, i));
            buf.append(' ');
            buf.append(Character.toUpperCase(firstPart.charAt(i + 1)));
            buf.append(firstPart.substring(i + 2));
            return buf.toString();
        }
        else
        {
            return username;
        }
    }

    protected void log(String s)
    {
        if (importLog != null)
        {
            importLog.append("[" + sdf.format(new Date()) + "] ");
            importLog.append(s);
            importLog.append("\n");
        }
        log4jLog.info(s);
    }

    protected static void closePS(PreparedStatement ps)
    {
        try
        {
            ps.close();
        }
        catch (SQLException e)
        {
            log4jLog.error("Error closing PreparedStatement in Mantis Import", e);
        }
    }

    private static Object getOnly(Collection singleCol)
    {
        if (singleCol == null)
        {
            return null;
        }
        else if (singleCol.size() > 1)
        {
            throw new IllegalArgumentException("Passes Collection with more than one element");
        }
        else if (singleCol.isEmpty())
        {
            throw new IllegalArgumentException("Passed Collection with no elements");
        }
        else
        {
            return singleCol.iterator().next();
        }
    }

    public String getImportLog()
    {
        return importLog.toString();
    }

    /**
     * Goes through imported issues and rewrites Mantis inline links (of the form #1234 or Bug#1234) to JIRA inline
     * links (JRA-XXXX).
     */
    private void rewriteBugLinks() throws GenericEntityException
    {
        log("Rewriting bug links");

        Iterator newIssues = issueKeys.values().iterator();
        while (newIssues.hasNext())
        {
            Object o = newIssues.next();
            Long issueId;
            if (o instanceof Long)
            {
                issueId = (Long) o;
            }
            else if (o instanceof Integer)
            {
                issueId = new Long(((Integer) o).intValue());
            }
            else
            {
                throw new RuntimeException("Unexpected issue type: " + o.getClass().getName());
            }

            GenericValue issue = issueManager.getIssue(issueId);
            if (issue != null)
            {
                String desc = issue.getString("description");
                String newDesc = rewriteMantisBuglinksInText(desc);
                if (!desc.equals(newDesc))
                {
                    log("Rewriting description for " + issue.getString("key"));
                    issue.setString("description", newDesc);
                    issue.store();
                }


                Collection comments = CoreFactory.getGenericDelegator().findByAnd("Action", EasyMap.build("type", "comment", "issue", issueId));
                Iterator commentIter = comments.iterator();
                while (commentIter.hasNext())
                {
                    GenericValue comment = (GenericValue) commentIter.next();
                    String commentStr = comment.getString("body");
                    if (commentStr != null)
                    {
                        comment.setString("body", rewriteMantisBuglinksInText(commentStr));
                        comment.store();
                    }
                }
            }
        }
        cacheManager.flush(CacheManager.ISSUE_CACHE);
    }

    private Set getAssociatedUsers(DatabaseConnectionBean connectionBean, String[] projectNames)
    {
        try
        {
            final Connection connection = connectionBean.getConnection();
            UserNameCollator collator = new UserNameCollator(projectNames, connection);
            return collator.getAllUsers();
        }
        catch (SQLException e)
        {
            throw new DataAccessException(e);
        }
    }


    protected static interface MappingBean
    {
        public String getProjectKey(String project);

        public String getPriority(int originalPriority);

        public String getResolution(int originalResolution);

        public int getStatus(int originalStatus);

        public int getWorkflowStep(final int originalWorkflowStep);

        public String getWorkflowStatus(final int originalWorkflowStatus);

        public String getProjectLead(String project);

        public String getProjectName(String project);

        public String getAttachmentPath();
    }

    /**
     * Class mapping Mantis ids (see lang/strings_english.txt in the Mantis source) to JIRA IDs
     */
    public static abstract class DefaultMappingBean implements MappingBean
    {
        /**
         * Maps mantis severity to jira priority. Default in config_defaults_inc.php is:
         * $g_severity_enum_string = '10:feature,20:trivial,30:text,40:tweak,50:minor,60:major,70:crash,80:block';
         */
        public String getPriority(int originalPriority)
        {
            switch (originalPriority)
            {
                case 80:
                    return "" + IssueFieldConstants.BLOCKER_PRIORITY_ID; // block

                case 70:
                    return "" + IssueFieldConstants.CRITICAL_PRIORITY_ID; // crash

                case 60:
                    return "" + IssueFieldConstants.MAJOR_PRIORITY_ID; // major

                case 50:
                    return "" + IssueFieldConstants.MINOR_PRIORITY_ID; // minor

                case 40:
                    return "" + IssueFieldConstants.MINOR_PRIORITY_ID; // tweak

                case 30:
                    return "" + IssueFieldConstants.TRIVIAL_PRIORITY_ID; // text

                case 20:
                    return "" + IssueFieldConstants.TRIVIAL_PRIORITY_ID; // trivial

                default:
                    return "undefined";
            }
        }

        /**
         * Maps mantis resolution to jira resolution.
         * Default in config_defaults_inc.php is:
         * $g_resolution_enum_string = '10:open,20:fixed,30:reopened,40:unable to duplicate,50:not fixable,60:duplicate,70:not a bug,80:suspended,90:wont fix';
         */
        public String getResolution(int originalResolution)
        {
            switch (originalResolution)
            {
                case 10:
                    return "-1"; // open

                case 20:
                    return "" + IssueFieldConstants.FIXED_RESOLUTION_ID; // fixed

                case 30:
                    return "-1"; // reopened

                case 40:
                    return "" + IssueFieldConstants.CANNOTREPRODUCE_RESOLUTION_ID; // unable to duplicate

                case 50:
                    return "" + IssueFieldConstants.WONTFIX_RESOLUTION_ID; // not fixable

                case 60:
                    return "" + IssueFieldConstants.DUPLICATE_RESOLUTION_ID; // duplicate

                case 70:
                    return "" + priorityResolutionId; // not a bug

                case 80:
                    return "-1"; // suspended

                case 90:
                    return "" + IssueFieldConstants.WONTFIX_RESOLUTION_ID; // won't fix

                default:
                    return "undefined";
            }
        }

        public int getStatus(int originalStatus)
        {
            switch (originalStatus)
            {
                case 10:
                    return IssueFieldConstants.OPEN_STATUS_ID; // new

                case 20:
                    return IssueFieldConstants.OPEN_STATUS_ID; // feedback

                case 30:
                    return IssueFieldConstants.OPEN_STATUS_ID; // acknowledged

                case 40:
                    return IssueFieldConstants.OPEN_STATUS_ID; // confirmed

                case 50:
                    return IssueFieldConstants.INPROGRESS_STATUS_ID; // assigned

                case 80:
                    return IssueFieldConstants.RESOLVED_STATUS_ID; // resolved

                case 90:
                    return IssueFieldConstants.CLOSED_STATUS_ID; // closed

                default:
                    return -1;
            }
        }

        public int getWorkflowStep(final int originalWorkflowStep)
        {
            return IssueFieldConstants.getWorkflowStatusFromIssueStatus(originalWorkflowStep);
        }

        public String getWorkflowStatus(final int originalWorkflowStatus)
        {
            return IssueFieldConstants.getStatusFromId(originalWorkflowStatus);
        }

        public String getProjectName(String project)
        {
            return project;
        }

        public abstract String getProjectKey(String project);
    }

    public static final class UserRole
    {
        public static final String REPORTER = "reporter";
        public static final String ASSIGNEE = "assignee";
        public static final String USER = "user";
    }

    /**
     * responsible for getting a Set of user names
     */
    private static class UserNameCollator
    {
        private final String projectIds;
        private final Connection conn;

        UserNameCollator(String[] projectNames, Connection conn) throws SQLException
        {
            this.conn = conn;
            PreparedStatement preparedStatement = null;
            ResultSet rs = null;
            try
            {
                preparedStatement = conn.prepareStatement("Select id from mantis_project_table where name in (" + ImportUtils.getSQLTokens(projectNames) + ")");
                for (int i = 0; i < projectNames.length; i++)
                {
                    String projectName = projectNames[i];
                    preparedStatement.setString(i + 1, projectName);
                }

                rs = preparedStatement.executeQuery();
                final StringBuffer buffer = new StringBuffer();
                int i = 0;
                while (rs.next())
                {
                    if (i++ > 0)
                    {
                        buffer.append(", ");
                    }
                    buffer.append(rs.getLong(1));
                }
                this.projectIds = buffer.toString();
            }
            finally
            {
                ImportUtils.close(preparedStatement, rs);
            }
        } // end ctor

        public Set getAllUsers() throws SQLException
        {
            final Set result = new HashSet();

            // comment author
            result.addAll(getUsers("SELECT u.username, u.realname, u.email FROM mantis_bug_table AS b JOIN mantis_bugnote_table AS n ON (n.bug_id = b.id) JOIN mantis_user_table AS u ON (u.id = n.reporter_id) WHERE project_id IN (" + projectIds + ") GROUP BY 1, 2, 3"));
            // reporters and assignees
            result.addAll(getUsers("SELECT u.username, u.realname, u.email FROM mantis_user_table AS u JOIN mantis_bug_table AS b ON (u.id = b.reporter_id OR u.id = b.handler_id) WHERE project_id IN (" + projectIds + ") GROUP BY 1, 2, 3"));
            return result;
        }

        private Set getUsers(String sql) throws SQLException
        {
            PreparedStatement ps = null;
            ResultSet rs = null;
            try
            {
                ps = conn.prepareStatement(sql);
                rs = ps.executeQuery();
                final Set result = new HashSet();
                while (rs.next())
                {
                    result.add(new ExternalUser(rs.getString(1), rs.getString(2), rs.getString(3)));
                }
                return result;
            }
            finally
            {
                ImportUtils.close(ps, rs);
            }
        }
    }
}