com.sic.bb.jenkins.plugins.sicci_for_xcode.XcodeBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.sic.bb.jenkins.plugins.sicci_for_xcode.XcodeBuilder.java

Source

/*
 * SICCI for Xcode - Jenkins Plugin for Xcode projects
 * 
 * Copyright (C) 2011 Benedikt Biallowons, SIC! Software GmbH
 *
 * This program 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
 * any later version.
 *
 * This program 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 com.sic.bb.jenkins.plugins.sicci_for_xcode;

import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.ARCHIVE_APP_ARG;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.BUILD_ARG;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.BUILD_CONFIGURATIONS_ARG;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.BUILD_FOLDER_NAME;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.BUILD_TARGETS_ARG;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.CLEAN_BEFORE_BUILD_ARG;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.CREATE_IPA_ARG;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.DEFAULT_FILENAME_TEMPLATE;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.DEFAULT_XCODE_PROJECT_SEARCH_DEPTH;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.FIELD_DELIMITER;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.FILENAME_TEMPLATE_ARG;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.MAX_XCODE_PROJECT_SEARCH_DEPTH;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.MIN_XCODE_PROJECT_SEARCH_DEPTH;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.PROJECT_DIR_ARG;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.PROJECT_DIRS_ARG;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.TRUE;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.UNIT_TEST_TARGET_ARG;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.XCODE_PLATFORM_ARG;
import static com.sic.bb.jenkins.plugins.sicci_for_xcode.util.Constants.XCODE_PROJECT_SEARCH_DEPTH_ARG;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.BuildListener;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Computer;
import hudson.model.FreeStyleProject;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.util.FormValidation;

import java.io.IOException;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
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 javax.servlet.ServletException;

import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

import com.sic.bb.jenkins.plugins.sicci_for_xcode.callables.AppArchiverCallable;
import com.sic.bb.jenkins.plugins.sicci_for_xcode.callables.IpaPackagerCallable;
import com.sic.bb.jenkins.plugins.sicci_for_xcode.callables.XcodeProjectSearchCallable;
import com.sic.bb.jenkins.plugins.sicci_for_xcode.cli.SecurityCommandCaller;
import com.sic.bb.jenkins.plugins.sicci_for_xcode.cli.XcodebuildCommandCaller;
import com.sic.bb.jenkins.plugins.sicci_for_xcode.io.XcodebuildOutputParser;
import com.sic.bb.jenkins.plugins.sicci_for_xcode.util.PluginUtils;
import com.sic.bb.jenkins.plugins.sicci_for_xcode.util.XcodePlatform;

public class XcodeBuilder extends Builder {
    private Map<String, String> data;
    private Map<String, List<String>> dataList;

    private transient FilePath currentProjectDirectory;
    private transient String currentUsername;
    private transient String currentPassword;
    private transient boolean currentBuildIsUnitTest;

    public XcodeBuilder(Map<String, String> data, Map<String, List<String>> dataList) {
        this.data = data;
        this.dataList = dataList;
    }

    public String getProjectDir() {
        if (this.data == null || !this.data.containsKey(PROJECT_DIR_ARG))
            return null;

        return this.data.get(PROJECT_DIR_ARG);
    }

    public String getXcodePlatform() {
        if (this.data == null || !this.data.containsKey(XCODE_PLATFORM_ARG))
            return null;

        return this.data.get(XCODE_PLATFORM_ARG);
    }

    public String getFilenameTemplate() {
        if (this.data == null || !this.data.containsKey(FILENAME_TEMPLATE_ARG))
            return null;

        String filenameTemplate = this.data.get(FILENAME_TEMPLATE_ARG);

        if (StringUtils.isBlank(filenameTemplate))
            return null;

        return filenameTemplate;
    }

    public String getXcodeProjectSearchDepth() {
        if (this.data == null || !this.data.containsKey(XCODE_PROJECT_SEARCH_DEPTH_ARG))
            return null;

        String xcodeProjectSearchDepth = this.data.get(XCODE_PROJECT_SEARCH_DEPTH_ARG);

        if (StringUtils.isBlank(xcodeProjectSearchDepth))
            return null;

        return xcodeProjectSearchDepth;
    }

    public boolean getBooleanPreference(String key) {
        if (this.data == null || !this.data.containsKey(key))
            return false;

        return this.data.get(key).equals(TRUE);
    }

    public boolean subMenuUsed(String target) {
        if (this.data == null)
            return false;

        for (String key : this.data.keySet())
            if (key.contains(target))
                return true;

        return false;
    }

    private void setData(String key, String[] values) {
        if (values.length == 0)
            return;

        if (values.length == 1) {
            if (this.data == null)
                this.data = new HashMap<String, String>();

            this.data.put(key, values[0]);
            return;
        }

        List<String> dataList = new ArrayList<String>();

        for (String value : values)
            dataList.add(value);

        if (this.dataList == null)
            this.dataList = new HashMap<String, List<String>>();

        this.dataList.put(key, dataList);
    }

    public String[] getProjectDirs(FilePath workspace) {
        String[] projectDirs;

        if (this.data != null && this.data.containsKey(PROJECT_DIRS_ARG)) {
            projectDirs = new String[] { this.data.get(PROJECT_DIRS_ARG) };
        } else if (this.dataList != null && this.dataList.containsKey(PROJECT_DIRS_ARG)) {
            List<String> projectDirsList = this.dataList.get(PROJECT_DIRS_ARG);
            projectDirs = (String[]) projectDirsList.toArray(new String[projectDirsList.size()]);
        } else {
            projectDirs = getProjectDirsNoCache(workspace);
            setData(PROJECT_DIRS_ARG, projectDirs);
        }

        return projectDirs;
    }

    private String[] getProjectDirsNoCache(FilePath workspace) {
        int searchDepth = MIN_XCODE_PROJECT_SEARCH_DEPTH - 1;

        try {
            searchDepth = Integer.parseInt(getXcodeProjectSearchDepth());
        } catch (NumberFormatException e) {
        }

        if (searchDepth < MIN_XCODE_PROJECT_SEARCH_DEPTH || searchDepth > MAX_XCODE_PROJECT_SEARCH_DEPTH
                || getDescriptor().getXcodeProjectSearchDepthGlobal())
            return getDescriptor().getProjectDirs(workspace);

        return getDescriptor().getProjectDirs(workspace, searchDepth);
    }

    public String[] getBuildTargets(FilePath workspace) {
        String[] buildTargets;

        if (this.data != null && this.data.containsKey(BUILD_TARGETS_ARG)) {
            buildTargets = new String[] { this.data.get(BUILD_TARGETS_ARG) };
        } else if (this.dataList != null && this.dataList.containsKey(BUILD_TARGETS_ARG)) {
            List<String> buildTargetsList = this.dataList.get(BUILD_TARGETS_ARG);
            buildTargets = (String[]) buildTargetsList.toArray(new String[buildTargetsList.size()]);
        } else {
            if (getProjectDir() != null)
                buildTargets = XcodebuildOutputParser.getBuildTargets(workspace.child(getProjectDir()));
            else
                buildTargets = XcodebuildOutputParser.getBuildTargets(workspace);

            setData(BUILD_TARGETS_ARG, buildTargets);
        }

        return buildTargets;
    }

    public String[] getBuildConfigurations(FilePath workspace) {
        String[] buildConfigurations;

        if (this.data != null && this.data.containsKey(BUILD_CONFIGURATIONS_ARG)) {
            buildConfigurations = new String[] { this.data.get(BUILD_CONFIGURATIONS_ARG) };
        } else if (this.dataList != null && this.dataList.containsKey(BUILD_CONFIGURATIONS_ARG)) {
            List<String> buildConfigurationsList = this.dataList.get(BUILD_CONFIGURATIONS_ARG);
            buildConfigurations = (String[]) buildConfigurationsList
                    .toArray(new String[buildConfigurationsList.size()]);
        } else {
            if (getProjectDir() != null)
                buildConfigurations = XcodebuildOutputParser
                        .getBuildConfigurations(workspace.child(getProjectDir()));
            else
                buildConfigurations = XcodebuildOutputParser.getBuildConfigurations(workspace);

            setData(BUILD_CONFIGURATIONS_ARG, buildConfigurations);
        }

        return buildConfigurations;
    }

    @Override
    public XcodeBuilderDescriptor getDescriptor() {
        return (XcodeBuilderDescriptor) super.getDescriptor();
    }

    @Override
    public boolean prebuild(AbstractBuild<?, ?> build, BuildListener listener) {
        Computer curComputer = Computer.currentComputer();
        FilePath workspace = build.getWorkspace();

        listener.getLogger().println("\n" + Messages.XcodeBuilder_prebuild_started() + "\n");

        // TODO: check if it's OS X
        if (!curComputer.getNode().createLauncher(listener).isUnix()) {
            listener.fatalError(Messages.XcodeBuilder_prebuild_unixOnly());
            return false;
        }

        if (!XcodebuildCommandCaller.getInstance().check(workspace.getChannel(), listener))
            return false;

        if (!SecurityCommandCaller.getInstance().check(workspace.getChannel(), listener))
            return false;

        try {
            String[] projectDirs = getProjectDirsNoCache(workspace);
            setData(PROJECT_DIRS_ARG, projectDirs);

            if (getProjectDir() != null)
                workspace = workspace.child(getProjectDir());
            else if (build.getNumber() == 1 && !projectDirs[0].isEmpty())
                workspace = workspace.child(projectDirs[0]);

            if (!workspace.exists()) {
                listener.fatalError(Messages.XcodeBuilder_prebuild_projectDirNotFound() + ": " + workspace);
                return false;
            } else
                this.currentProjectDirectory = workspace;

            setData(BUILD_TARGETS_ARG, XcodebuildOutputParser.getBuildTargetsNoCache(workspace));
            setData(BUILD_CONFIGURATIONS_ARG, XcodebuildOutputParser.getBuildConfigurationsNoCache(workspace));
        } catch (Exception e) {
            listener.fatalError(Messages.XcodeBuilder_prebuild_projectDirNotFound() + ": " + workspace + "\n");
            return false;
        }

        XcodeUserNodeProperty property = XcodeUserNodeProperty.getCurrentNodesProperties();

        if (property != null) {
            this.currentUsername = property.getUsername();
            this.currentPassword = property.getPassword();

            if (this.currentUsername == null) {
                listener.fatalError(Messages.XcodeBuilder_prebuild_keychainUsernameNotSet() + "\n");

                if (XcodePlatform.fromString(getXcodePlatform()) == XcodePlatform.IOS)
                    return false;
            }

            if (this.currentPassword == null) {
                listener.fatalError(Messages.XcodeBuilder_prebuild_keychainPasswordNotSet() + "\n");

                if (XcodePlatform.fromString(getXcodePlatform()) == XcodePlatform.IOS)
                    return false;
            }
        } else {
            listener.fatalError(Messages.XcodeBuilder_prebuild_keychainCredentialsNotSet() + "\n");

            if (XcodePlatform.fromString(getXcodePlatform()) == XcodePlatform.IOS)
                return false;
        }

        listener.getLogger().println(Messages.XcodeBuilder_prebuild_finished() + "\n");

        return true;
    }

    @Override
    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) {
        PrintStream logger = listener.getLogger();
        XcodeBuilderDescriptor descr = getDescriptor();
        FilePath workspace = this.currentProjectDirectory;

        List<String> blackList = new ArrayList<String>();
        List<Boolean> returnCodes = new ArrayList<Boolean>();

        boolean rcode = true;

        logger.println("\n" + Messages.XcodeBuilder_perform_started() + "\n");

        try {
            EnvVars envVars = build.getEnvironment(listener);

            logger.println(Messages.XcodeBuilder_perform_cleanStarted() + "\n");

            if (!(descr.getCleanBeforeBuildGlobal() && !descr.getCleanBeforeBuild()))
                for (String toClean : getToPerformStep(CLEAN_BEFORE_BUILD_ARG,
                        (descr.getCleanBeforeBuildGlobal() && descr.getCleanBeforeBuild())))
                    returnCodes.add(XcodebuildCommandCaller.getInstance().clean(launcher, envVars, listener,
                            workspace, createArgs(toClean)));

            logger.println(Messages.XcodeBuilder_perform_cleanFinished() + "\n\n");

            if (this.currentUsername != null && this.currentPassword != null) {
                if (!SecurityCommandCaller.getInstance().unlockKeychain(envVars, listener, workspace,
                        this.currentUsername, this.currentPassword)
                        && XcodePlatform.fromString(getXcodePlatform()) == XcodePlatform.IOS)
                    return false;

                logger.print("\n");
            }

            logger.println(Messages.XcodeBuilder_perform_buildStarted() + "\n");

            for (String toBuild : getToPerformStep(BUILD_ARG, true)) {
                rcode = XcodebuildCommandCaller.getInstance().build(launcher, envVars, listener, workspace,
                        this.currentBuildIsUnitTest, createArgs(toBuild));

                if (!rcode)
                    blackList.add(toBuild);

                returnCodes.add(rcode);
            }

            logger.println(Messages.XcodeBuilder_perform_buildFinished() + "\n\n");

            FilePath buildDir = workspace.child(BUILD_FOLDER_NAME);

            logger.println(Messages.XcodeBuilder_perform_archiveAppsStarted() + "\n");

            if (!(descr.getArchiveAppGlobal() && !descr.getArchiveApp())) {
                for (String toArchiveApp : getToPerformStep(ARCHIVE_APP_ARG,
                        (descr.getArchiveAppGlobal() && descr.getArchiveApp()))) {
                    if (blackList.contains(toArchiveApp))
                        continue;

                    String[] array = toArchiveApp.split(PluginUtils.stringToPattern(FIELD_DELIMITER));

                    if (getBooleanPreference(array[0] + FIELD_DELIMITER + UNIT_TEST_TARGET_ARG))
                        continue;

                    logger.print(Messages.XcodeBuilder_perform_archivingApp() + ": " + array[0] + " " + array[1]);

                    FilePath tempBuildDir = getBuildDir(buildDir, array[1]);

                    if (tempBuildDir != null && tempBuildDir
                            .act(new AppArchiverCallable(array[0], createFilename(build, array[0], array[1])))) {
                        logger.println(" " + Messages.XcodeBuilder_perform_archivingAppDone());
                        returnCodes.add(true);
                    } else {
                        logger.println(" " + Messages.XcodeBuilder_perform_archivingAppFailed());
                        returnCodes.add(false);
                    }
                }
            }

            logger.println("\n" + Messages.XcodeBuilder_perform_archiveAppsFinished() + "\n\n");
            logger.println(Messages.XcodeBuilder_perform_createIpasStarted() + "\n");

            if (!(descr.getCreateIpaGlobal() && !descr.getCreateIpa())) {
                for (String toCreateIpa : getToPerformStep(CREATE_IPA_ARG,
                        (descr.getCreateIpaGlobal() && descr.getCreateIpa()))) {
                    if (blackList.contains(toCreateIpa))
                        continue;

                    String[] array = toCreateIpa.split(PluginUtils.stringToPattern(FIELD_DELIMITER));

                    if (getBooleanPreference(array[0] + FIELD_DELIMITER + UNIT_TEST_TARGET_ARG))
                        continue;

                    logger.print(Messages.XcodeBuilder_perform_creatingIpa() + ": " + array[0] + " " + array[1]);

                    FilePath tempBuildDir = getBuildDir(buildDir, array[1]);

                    if (tempBuildDir != null && tempBuildDir
                            .act(new IpaPackagerCallable(array[0], createFilename(build, array[0], array[1])))) {
                        logger.println(" " + Messages.XcodeBuilder_perform_creatingIpaDone());
                        returnCodes.add(true);
                    } else {
                        logger.println(" " + Messages.XcodeBuilder_perform_creatingIpaFailed());
                        returnCodes.add(false);
                    }
                }
            }

            logger.println("\n" + Messages.XcodeBuilder_perform_createIpasFinished() + "\n\n");

            logger.println(Messages.XcodeBuilder_perform_finished() + "\n\n");

            if (returnCodes.contains(false))
                return false;

            return true;
        } catch (Exception e) {
            logger.println(e.getStackTrace());
        }

        return false;
    }

    private Set<String> getToPerformStep(String cmd, boolean force) {
        Set<String> toPerformStep = new HashSet<String>();

        for (String key : this.data.keySet()) {
            if (StringUtils.countMatches(key, FIELD_DELIMITER) < 2)
                continue;

            String[] fields = key.split(PluginUtils.stringToPattern(FIELD_DELIMITER));

            if (!cmd.equals(BUILD_ARG)
                    && (!fields[fields.length - 1].equals(cmd) || (!force && !this.data.get(key).equals(TRUE))))
                continue;

            toPerformStep.add(fields[0] + FIELD_DELIMITER + fields[1]);
        }

        return toPerformStep;
    }

    private FilePath getBuildDir(FilePath buildDir, String configurationName) {
        String configBuildDirName = null;

        try {
            for (String configBuildDirNameTemp : XcodePlatform.fromString(getXcodePlatform())
                    .getProjectBuildDirNames(configurationName)) {
                if (buildDir.child(configBuildDirNameTemp).isDirectory()) {
                    configBuildDirName = configBuildDirNameTemp;
                    break;
                }
            }
        } catch (Exception e) {
            return null;
        }

        if (configBuildDirName == null)
            return null;
        else
            return buildDir.child(configBuildDirName);
    }

    private List<String> createArgs(String arg) {
        List<String> cmds = new ArrayList<String>();
        String[] args = arg.split(PluginUtils.stringToPattern(FIELD_DELIMITER));

        cmds.add("-target");
        cmds.add(args[0]);
        cmds.add("-configuration");
        cmds.add(args[1]);

        if (getBooleanPreference(args[0] + FIELD_DELIMITER + UNIT_TEST_TARGET_ARG)) {
            // iOS unit test have to be run with iOS Simulator platform
            if (getXcodePlatform().equals(XcodePlatform.IOS.getXcodePlatformName())) {
                cmds.add("-sdk");
                cmds.add(XcodePlatform.IOS_SIMULATOR.getXcodePlatformSdkName());
            }

            this.currentBuildIsUnitTest = true;
        } else
            this.currentBuildIsUnitTest = false;

        return cmds;
    }

    private String createFilename(AbstractBuild<?, ?> build, String targetName, String configurationName) {
        String filename;

        if (getDescriptor().filenameTemplateGlobal)
            filename = getDescriptor().getFilenameTemplate();
        else
            filename = getFilenameTemplate();

        if (filename == null)
            filename = DEFAULT_FILENAME_TEMPLATE;

        Date buildTimeStamp = build.getTimestamp().getTime();

        filename = filename.replaceAll("<SECOND>", new SimpleDateFormat("ss").format(buildTimeStamp));
        filename = filename.replaceAll("<MINUTE>", new SimpleDateFormat("mm").format(buildTimeStamp));
        filename = filename.replaceAll("<HOUR>", new SimpleDateFormat("HH").format(buildTimeStamp));
        filename = filename.replaceAll("<DAY>", new SimpleDateFormat("dd").format(buildTimeStamp));
        filename = filename.replaceAll("<MONTH>", new SimpleDateFormat("MM").format(buildTimeStamp));
        filename = filename.replaceAll("<YEAR>", new SimpleDateFormat("yyyy").format(buildTimeStamp));
        filename = filename.replaceAll("<TIME>", new SimpleDateFormat("HH_mm_ss").format(buildTimeStamp));
        filename = filename.replaceAll("<DATE>", new SimpleDateFormat("yyyy_MM_dd").format(buildTimeStamp));
        filename = filename.replaceAll("<DATETIME>",
                new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss").format(buildTimeStamp));
        filename = filename.replaceAll("<BUILD>", String.valueOf(build.getNumber()));
        filename = filename.replaceAll("<TARGET>", targetName);
        filename = filename.replaceAll("<CONFIG>", configurationName);

        return filename;
    }

    @Extension
    public static final class XcodeBuilderDescriptor extends BuildStepDescriptor<Builder> {
        private String filenameTemplate;
        private boolean filenameTemplateGlobal;
        private int xcodeProjectSearchDepth;
        private boolean xcodeProjectSearchDepthGlobal;
        private boolean cleanBeforeBuild, cleanBeforeBuildGlobal;
        private boolean archiveApp, archiveAppGlobal;
        private boolean createIpa, createIpaGlobal;

        private transient Map<String, FilePath> projectWorkspaceMap;
        private transient FilePath currentProjectDir;

        public XcodeBuilderDescriptor() {
            super(XcodeBuilder.class);
            load();

            this.projectWorkspaceMap = new HashMap<String, FilePath>();
        }

        public String getDisplayName() {
            return Messages.XcodeBuilderDescriptor_getDisplayName();
        }

        @Override
        public XcodeBuilder newInstance(StaplerRequest req, JSONObject formData) throws FormException {
            Map<String, String> stringMap = new HashMap<String, String>();
            Map<String, List<String>> stringListMap = new HashMap<String, List<String>>();

            collectFormData(formData, stringMap, stringListMap);

            return new XcodeBuilder(stringMap, stringListMap);
        }

        @Override
        public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
            req.bindJSON(this, formData);
            save();

            return super.configure(req, formData);
        }

        @SuppressWarnings("rawtypes")
        public boolean isApplicable(Class<? extends AbstractProject> jobType) {
            return jobType == FreeStyleProject.class;
        }

        @SuppressWarnings("unchecked")
        private void collectFormData(JSONObject formData, Map<String, String> stringMap,
                Map<String, List<String>> stringListMap) {
            Iterator<String> it = formData.keys();

            while (it.hasNext()) {
                String key = it.next();

                if (formData.get(key).getClass() == JSONArray.class) {
                    for (int i = 0; i < formData.getJSONArray(key).size(); i++) {
                        if (formData.getJSONArray(key).get(i).getClass() == JSONObject.class)
                            collectFormData(formData.getJSONArray(key).getJSONObject(i), stringMap, stringListMap);
                        else if (formData.getJSONArray(key).get(i).getClass() == String.class)
                            collectFormData(new JSONObject().element(key, formData.getJSONArray(key).getString(i)),
                                    stringMap, stringListMap);
                    }
                } else if (formData.get(key).getClass() == JSONObject.class) {
                    collectFormData(formData.getJSONObject(key), stringMap, stringListMap);
                } else {
                    if (stringListMap.containsKey(key)) {
                        stringListMap.get(key).add(StringUtils.strip(formData.getString(key)));
                    } else if (stringMap.containsKey(key)) {
                        List<String> stringList = new ArrayList<String>();
                        stringList.add(stringMap.remove(key));
                        stringList.add(StringUtils.strip(formData.getString(key)));
                        stringListMap.put(key, stringList);
                    } else
                        stringMap.put(key, StringUtils.strip(formData.getString(key)));
                }
            }
        }

        public void setXcodeProjectSearchDepth(String searchDepth) {
            try {
                this.xcodeProjectSearchDepth = Integer.parseInt(StringUtils.strip(searchDepth));

                if (this.xcodeProjectSearchDepth < MIN_XCODE_PROJECT_SEARCH_DEPTH
                        || this.xcodeProjectSearchDepth > MAX_XCODE_PROJECT_SEARCH_DEPTH)
                    this.xcodeProjectSearchDepth = DEFAULT_XCODE_PROJECT_SEARCH_DEPTH;
            } catch (NumberFormatException e) {
                this.xcodeProjectSearchDepth = DEFAULT_XCODE_PROJECT_SEARCH_DEPTH;
            }
        }

        public String getXcodeProjectSearchDepth() {
            if (this.xcodeProjectSearchDepth < MIN_XCODE_PROJECT_SEARCH_DEPTH
                    || this.xcodeProjectSearchDepth > MAX_XCODE_PROJECT_SEARCH_DEPTH)
                return String.valueOf(DEFAULT_XCODE_PROJECT_SEARCH_DEPTH);

            return String.valueOf(this.xcodeProjectSearchDepth);
        }

        public void setXcodeProjectSearchDepthGlobal(boolean searchDepthGlobal) {
            this.xcodeProjectSearchDepthGlobal = searchDepthGlobal;
        }

        public boolean getXcodeProjectSearchDepthGlobal() {
            return this.xcodeProjectSearchDepthGlobal;
        }

        public void setCleanBeforeBuild(boolean cleanBeforeBuild) {
            this.cleanBeforeBuild = cleanBeforeBuild;
        }

        public boolean getCleanBeforeBuild() {
            return this.cleanBeforeBuild;
        }

        public void setCleanBeforeBuildGlobal(boolean cleanBeforeBuildGlobal) {
            this.cleanBeforeBuildGlobal = cleanBeforeBuildGlobal;
        }

        public boolean getCleanBeforeBuildGlobal() {
            return this.cleanBeforeBuildGlobal;
        }

        public void setArchiveApp(boolean archiveApp) {
            this.archiveApp = archiveApp;
        }

        public boolean getArchiveApp() {
            return this.archiveApp;
        }

        public void setArchiveAppGlobal(boolean archiveAppGlobal) {
            this.archiveAppGlobal = archiveAppGlobal;
        }

        public boolean getArchiveAppGlobal() {
            return this.archiveAppGlobal;
        }

        public void setCreateIpa(boolean createIpa) {
            this.createIpa = createIpa;
        }

        public boolean getCreateIpa() {
            return this.createIpa;
        }

        public void setCreateIpaGlobal(boolean createIpaGlobal) {
            this.createIpaGlobal = createIpaGlobal;
        }

        public boolean getCreateIpaGlobal() {
            return this.createIpaGlobal;
        }

        public void setFilenameTemplate(String filenameTemplate) {
            if (StringUtils.isBlank(filenameTemplate))
                this.filenameTemplate = DEFAULT_FILENAME_TEMPLATE;
            else
                this.filenameTemplate = StringUtils.strip(filenameTemplate);
        }

        public String getFilenameTemplate() {
            if (StringUtils.isBlank(this.filenameTemplate))
                return DEFAULT_FILENAME_TEMPLATE;

            return this.filenameTemplate;
        }

        public void setFilenameTemplateGlobal(boolean filenameTemplateGlobal) {
            this.filenameTemplateGlobal = filenameTemplateGlobal;
        }

        public boolean getFilenameTemplateGlobal() {
            return this.filenameTemplateGlobal;
        }

        public FilePath getProjectDir() {
            return this.currentProjectDir;
        }

        public String[] getXcodePlatformNames() {
            return XcodePlatform.getXcodePlatformNames();
        }

        public String getXcodePlatform() {
            return null;
        }

        public boolean subMenuUsed(String target) {
            return false;
        }

        public boolean getBooleanPreference(String key) {
            return false;
        }

        public void doAjaxTargets(StaplerRequest req, StaplerResponse rsp, @QueryParameter String jobName,
                @QueryParameter String projectDir) throws IOException, ServletException {
            if (this.projectWorkspaceMap.containsKey(jobName))
                this.currentProjectDir = this.projectWorkspaceMap.get(jobName).child(projectDir);

            req.getView(this, '/' + XcodeBuilder.class.getName().replaceAll("\\.", "\\/") + "/targets.jelly")
                    .forward(req, rsp);

            this.currentProjectDir = null;
        }

        public FormValidation doCheckXcodeProjectSearchDepth(@QueryParameter String value)
                throws IOException, ServletException {
            if (StringUtils.isBlank(value))
                return FormValidation.error(
                        Messages.XcodeBuilderDescriptor_doCheckXcodeProjectSearchDepth_emptyValue() + " (min "
                                + MIN_XCODE_PROJECT_SEARCH_DEPTH + ", max " + MAX_XCODE_PROJECT_SEARCH_DEPTH + ')');

            int xcodeProjectSearchDepth;

            try {
                xcodeProjectSearchDepth = Integer.parseInt(StringUtils.strip(value));
            } catch (NumberFormatException e) {
                return FormValidation
                        .error(Messages.XcodeBuilderDescriptor_doCheckXcodeProjectSearchDepth_valueNotANumber());
            }

            if (xcodeProjectSearchDepth < MIN_XCODE_PROJECT_SEARCH_DEPTH)
                return FormValidation
                        .error(Messages.XcodeBuilderDescriptor_doCheckXcodeProjectSearchDepth_valueTooSmall()
                                + " (min " + MIN_XCODE_PROJECT_SEARCH_DEPTH + ')');
            else if (xcodeProjectSearchDepth > MAX_XCODE_PROJECT_SEARCH_DEPTH)
                return FormValidation
                        .error(Messages.XcodeBuilderDescriptor_doCheckXcodeProjectSearchDepth_valueTooBig()
                                + " (max " + MAX_XCODE_PROJECT_SEARCH_DEPTH + ')');

            return FormValidation.ok();
        }

        public FormValidation doCheckFilenameTemplate(@QueryParameter String value)
                throws IOException, ServletException {
            if (StringUtils.isBlank(value))
                return FormValidation
                        .error(Messages.XcodeBuilderDescriptor_doCheckFilenameTemplate_setDefaultFilename());

            return FormValidation.ok();
        }

        public String[] getProjectDirs(FilePath workspace) {
            if (this.xcodeProjectSearchDepth < MIN_XCODE_PROJECT_SEARCH_DEPTH
                    || this.xcodeProjectSearchDepth > MAX_XCODE_PROJECT_SEARCH_DEPTH)
                return getProjectDirs(workspace, DEFAULT_XCODE_PROJECT_SEARCH_DEPTH);

            return getProjectDirs(workspace, this.xcodeProjectSearchDepth);
        }

        @SuppressWarnings("deprecation")
        public String[] getProjectDirs(FilePath workspace, int searchDepth) {
            List<String> projectDirs = new ArrayList<String>();

            if (workspace.isRemote())
                this.projectWorkspaceMap.put(workspace.getName(), workspace);
            else
                this.projectWorkspaceMap.put(workspace.getParent().getName(), workspace);

            try {
                projectDirs.addAll(workspace.act(new XcodeProjectSearchCallable(searchDepth)));
            } catch (Exception e) {
            }

            String[] projectDirsArray = new String[projectDirs.size()];

            for (int i = 0; i < projectDirs.size(); i++) {
                String path = projectDirs.get(i);

                // TODO don't use this deprecated method
                projectDirsArray[i] = path.substring(workspace.toString().length() + 1, path.length());
            }

            return projectDirsArray;
        }

        public static String[] getBuildTargets(FilePath workspace) {
            return XcodebuildOutputParser.getBuildTargets(workspace);
        }

        public static String[] getBuildConfigurations(FilePath workspace) {
            return XcodebuildOutputParser.getBuildConfigurations(workspace);
        }

        public static String replaceSpaces(String withSpaces) {
            return withSpaces.replaceAll("\\s", "_");
        }

        /* <j:getStatic> doesn't work -> classloader problem */
        public static String getProjectDirArg() {
            return PROJECT_DIR_ARG;
        }

        /* <j:getStatic> doesn't work -> classloader problem */
        public static String getXcodePlatformArg() {
            return XCODE_PLATFORM_ARG;
        }

        /* <j:getStatic> doesn't work -> classloader problem */
        public static String getFilenameTemplateArg() {
            return FILENAME_TEMPLATE_ARG;
        }

        /* <j:getStatic> doesn't work -> classloader problem */
        public static String getXcodeProjectSearchDepthArg() {
            return XCODE_PROJECT_SEARCH_DEPTH_ARG;
        }

        /* <j:getStatic> doesn't work -> classloader problem */
        public static String getProjectDirsArg() {
            return PROJECT_DIRS_ARG;
        }

        /* <j:getStatic> doesn't work -> classloader problem */
        public static String getBuildTargetsArg() {
            return BUILD_TARGETS_ARG;
        }

        /* <j:getStatic> doesn't work -> classloader problem */
        public static String getBuildConfigurationsArg() {
            return BUILD_CONFIGURATIONS_ARG;
        }

        /* <j:getStatic> doesn't work -> classloader problem */
        public static String getFieldDelimiter() {
            return FIELD_DELIMITER;
        }

        /* <j:getStatic> doesn't work -> classloader problem */
        public static String getUnitTestTargetArg() {
            return UNIT_TEST_TARGET_ARG;
        }

        /* <j:getStatic> doesn't work -> classloader problem */
        public static String getCleanBeforeBuildArg() {
            return CLEAN_BEFORE_BUILD_ARG;
        }

        /* <j:getStatic> doesn't work -> classloader problem */
        public static String getCreateIpaArg() {
            return CREATE_IPA_ARG;
        }

        /* <j:getStatic> doesn't work -> classloader problem */
        public static String getArchiveAppArg() {
            return ARCHIVE_APP_ARG;
        }
    }
}