BuildContextAwareMojo.java

/*
 * #%L
 * xcode-maven-plugin
 * %%
 * Copyright (C) 2012 SAP AG
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package com.sap.prd.mobile.ios.mios;

import static com.sap.prd.mobile.ios.mios.XCodeVersionUtil.checkVersions;
import static com.sap.prd.mobile.ios.mios.XCodeVersionUtil.getVersion;
import static com.sap.prd.mobile.ios.mios.XCodeVersionUtil.getXCodeVersionString;

import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.MojoExecutionException;

/**
 * Contains all parameters and methods that are needed for mojos that invoke the 'xcodebuild'
 * command.
 * 
 */
public abstract class BuildContextAwareMojo extends AbstractXCodeMojo
{

  private static final String PREFIX_XCODE_OPTIONS = "xcode.options.";
  private static final String PREFIX_XCODE_SETTINGS = "xcode.settings.";
  public final static String MIN_XCODE_VERSION = "8.0";


  protected final static List<String> DEFAULT_BUILD_ACTIONS = Collections.unmodifiableList(Arrays.asList("clean",
        "build"));

  /**
   * The Xcode build action to to execute (e.g. clean, build, install). By default
   * <code>clean</code> and <code>build</code> are executed.
   * 
   * @parameter
   */
  protected List<String> buildActions;

  /**
   * The code sign identity is used to select the provisioning profile (e.g.
   * <code>iPhone Distribution</code>, <code>iPhone Developer</code>).
   * 
   * @parameter expression="${xcode.codeSignIdentity}"
   * @since 1.2.0
   */
  protected String codeSignIdentity;

  /**
   * The code signing required is used to disable code signing when no
   * developer provisioning certificate is available (e.g.
   * <code>NO</code>, <code>YES</code>).
   * 
   * @parameter expression="${xcode.codeSigningRequired}" default-value = "true"
   * @since 1.14.1
   */
  protected boolean codeSigningRequired;
  
  /**
   * Can be used to override the provisioning profile defined in the Xcode project target. You can
   * set it to an empty String if you want to use the default provisioning profile.
   * 
   * @parameter expression="${xcode.provisioningProfile}"
   * @since 1.2.1
   */
  protected String provisioningProfile;

  /**
   * The Xcode target to be built. If not specified, the default target (the first target) will be
   * built.
   * 
   * @parameter expression="${xcode.target}"
   * @since 1.4.1
   */
  protected String target;

  /**
   * @parameter expression="${product.name}"
   */
  private String productName;

  /**
   * Settings to pass to XCode - if any are explicitly defined here, this plugin will not provide
   * default settings to XCode.
   * 
   * @parameter
   * @since 1.6.2
   */
  private Map<String, String> settings;

  /**
   * Options to pass to XCode - if any are explicitly defined here, this plugin will not provide
   * default options to XCode.
   * 
   * @parameter
   * @since 1.6.2
   */
  private Map<String, String> options;

  /**
   * @parameter expression="${session}"
   * @required
   * @readonly
   */
  private MavenSession session;

  /**
   * @parameter expression="${xcode.watchapp}"
   * For watchos2.0 we should not send the sdk value to xcodebuild, To differentiate between regular app and watch2.0 (Now it's specific)
   * expecting this property from developer in pom.xml, on demand this will be considered.
   *
   * pom.xml entry:
   *
   * <pre>
   * {@code
   * <properties>
   *  <xcode.watchapp>watchos2.0</xcode.watchapp>
   * </properties>
   * }
   * </pre>
   *
   * @since 1.14.3
   */
  private String watchapp;

  /**
   * Allowed developers to override the SYMROOT settings
   *
   * @parameter expression="${xcode.symroot}" default-value = "build"
   * @since 1.14.4
   */
  private String symRootDir;
  /**
   * Allowed developers to override the xcconfig settings through pom.xml
   * Signing methodology has been changed with xcode8 onwards, to enable this we are expecting dev to give singing related parameters
   * This is only for the Apps with Entitlements like Push notification, Wallet etc...
   * @parameter expression="${xcode.xcconfig}"
   *
   * pom.xml entry:
   *
   * <pre>
   * {@code
   * <properties>
   *  <xcode.xcconfig>$XCCONFG_FILE_PATH</xcode.xcconfig>
   * </properties>
   * }
   * </pre>
   * where XCCONFG_FILE_PATH = Relative path to xcconfig file, ideally we expect developer to keep in xcode project level
   *
   * @since 1.14.5
   */
  private String xcconfigDir;

  /**
   * For simple application central team manages the xcconfig.
   * This will be managed in settings.xml
   *
   * @parameter expression="${xcode.xcconfig.default}"
   * @since 1.14.5
   */
  private String defaultxcconfig;

  protected XCodeContext getXCodeContext(final XCodeContext.SourceCodeLocation sourceCodeLocation,
        String configuration, String sdk)
  {
    final String projectName = project.getArtifactId();
    File projectDirectory = null;

    if (sourceCodeLocation == XCodeContext.SourceCodeLocation.WORKING_COPY) {
      projectDirectory = getXCodeCompileDirectory();
    }
    else if (sourceCodeLocation == XCodeContext.SourceCodeLocation.ORIGINAL) {
      projectDirectory = getXCodeSourceDirectory();
    }
    else {
      throw new IllegalStateException("Invalid source code location: '" + sourceCodeLocation + "'");
    }

    HashMap<String, String> managedSettings = new HashMap<String, String>();
    if (codeSignIdentity != null)
      managedSettings.put(Settings.ManagedSetting.CODE_SIGN_IDENTITY.name(), codeSignIdentity);

    if (symRootDir != null)
        managedSettings.put(Settings.ManagedSetting.SYMROOT.name(), symRootDir);

    if (!codeSigningRequired)
      managedSettings.put(Settings.ManagedSetting.CODE_SIGNING_REQUIRED.name(), "NO");

    if (provisioningProfile != null)
      managedSettings.put(Settings.ManagedSetting.PROVISIONING_PROFILE.name(), provisioningProfile);

    HashMap<String, String> managedOptions = new HashMap<String, String>();

    if (configuration != null && !configuration.trim().isEmpty())
      managedOptions.put(Options.ManagedOption.CONFIGURATION.getOptionName(), configuration);
		try {

			String xCodeVersionString = getXCodeVersionString();
			DefaultArtifactVersion version = getVersion(xCodeVersionString);
			File file;
			if (checkVersions(version, MIN_XCODE_VERSION)) {

				if (xcconfigDir != null) {
					getLog().info("Using xccconfig provided by the dev team: " + xcconfigDir);

					file = new File(xcconfigDir);
					if (file.exists()) {
						managedOptions.put(Options.ManagedOption.XCCONFIG.getOptionName(), xcconfigDir);
					} else {
						getLog().error("xcconfig file not found in locaion " + xcconfigDir);
					}
				} else if (defaultxcconfig != null) {
					getLog().info("Using xccconfig provided by the central team: " + defaultxcconfig);

					file = new File(defaultxcconfig);
					if (file.exists()) {
						managedOptions.put(Options.ManagedOption.XCCONFIG.getOptionName(), defaultxcconfig);
					} else {
						getLog().error("xcconfig file not found in locaion " + defaultxcconfig);
					}
				} else
					getLog().info(
							"To build the application using Xcode 8 and above, plugin expects xcconfig file /n For simple app: Central team manages it in settings.xml "
									+ "For apps with entitlement dev needs to provide the xcconfig content in pom.xml");
			}
		} catch (XCodeException e) {
			throw new IllegalStateException("Could not get xcodebuild version", e);
		}

    /**
     * No specific check has been done here, If property specified then sdk entry will be ignored
     * This can be extended with the specific check. I kept this as Generic because watchosX.X should support with this check,
     * without confusing developers it will serve the purpose
     */
    if (sdk != null && !sdk.trim().isEmpty() && watchapp ==null)
      managedOptions.put(Options.ManagedOption.SDK.getOptionName(), sdk);
    if (target != null && !target.trim().isEmpty())
      managedOptions.put(Options.ManagedOption.TARGET.getOptionName(), target);

    Map<String, String> _settings = new HashMap<String, String>(settings == null ? new HashMap<String, String>()
          : settings);

    for (String key : getKeys(PREFIX_XCODE_SETTINGS)) {
      _settings.put(key.substring(PREFIX_XCODE_SETTINGS.length()), getProperty(key));
    }

    Map<String, String> _options = new HashMap<String, String>(options == null ? new HashMap<String, String>()
          : options);

    for (String key : getKeys(PREFIX_XCODE_OPTIONS)) {
      _options.put(key.substring(PREFIX_XCODE_OPTIONS.length()), getProperty(key));
    }

    if (null == _options.get("scheme"))
    managedOptions.put(Options.ManagedOption.PROJECT.getOptionName(), projectName + ".xcodeproj");


    return new XCodeContext(getBuildActions(), projectDirectory, System.out, new Settings(_settings, managedSettings),
          new Options(_options, managedOptions));
  }

  protected List<String> getBuildActions()
  {
    return (buildActions == null || buildActions.isEmpty()) ? DEFAULT_BUILD_ACTIONS : Collections
      .unmodifiableList(buildActions);
  }

  /**
   * Retrieves the Info Plist out of the effective Xcode project settings and returns the accessor
   * to it.
   */
  protected PListAccessor getInfoPListAccessor(XCodeContext.SourceCodeLocation location, String configuration,
        String sdk)
        throws MojoExecutionException, XCodeException
  {
    File plistFile = getPListFile(location, configuration, sdk);
    if (!plistFile.isFile()) {
      throw new MojoExecutionException("The Xcode project refers to the Info.plist file '" + plistFile
            + "' that does not exist.");
    }
    return new PListAccessor(plistFile);
  }

  protected File getPListFile(XCodeContext.SourceCodeLocation location, String configuration, String sdk)
        throws XCodeException
  {

    XCodeContext context = getXCodeContext(location, configuration, sdk);

    String plistFileName = EffectiveBuildSettings.getBuildSetting(context, EffectiveBuildSettings.INFOPLIST_FILE);
    File srcRoot = new File(EffectiveBuildSettings.getBuildSetting(context, EffectiveBuildSettings.SRC_ROOT));

    final File plistFile = new File(plistFileName);

    if (!plistFile.isAbsolute()) {
      return new File(srcRoot, plistFileName);
    }

    if (FileUtils.isChild(srcRoot, plistFile))
      return plistFile;

    throw new IllegalStateException("Plist file " + plistFile + " is not located inside the xcode project " + srcRoot
          + ".");

  }

  protected File getProjectRootDirectory(XCodeContext.SourceCodeLocation location, String configuration, String sdk)
		throws XCodeException {
	XCodeContext context = getXCodeContext(location, configuration, sdk);

	File srcRoot = new File(EffectiveBuildSettings.getBuildSetting(context, EffectiveBuildSettings.SRC_ROOT));
	return srcRoot;
  }

  protected String getProductName(final String configuration, final String sdk) throws MojoExecutionException
  {

    final String productName;

    if (this.productName != null) {
      productName = this.productName;
      getLog().info("Production name obtained from pom file");
    }
    else {

      try {
        productName = EffectiveBuildSettings.getBuildSetting(
              getXCodeContext(XCodeContext.SourceCodeLocation.WORKING_COPY, configuration, sdk), EffectiveBuildSettings.PRODUCT_NAME);
        getLog().info("Product name obtained from effective build settings file");

      }
      catch (final XCodeException ex) {
        throw new MojoExecutionException("Cannot get product name: " + ex.getMessage(), ex);
      }
    }

    if (productName == null || productName.trim().length() == 0)
      throw new MojoExecutionException("Invalid product name. Was null or empty.");

    return productName;
  }

  /**
   * Returns all keys of project properties and user properties matching the <code>prefix</code>.
   * 
   * @param prefix
   *          all keys if null
   * @return
   */
  @SuppressWarnings("unchecked")
  protected Set<String> getKeys(String prefix)
  {

    Set<String> result = new HashSet<String>();

    @SuppressWarnings("rawtypes")
    final Set keys = new HashSet();
    keys.addAll(session.getUserProperties().keySet());
    keys.addAll(project.getProperties().keySet());

    if (prefix == null) return keys;

    for (Object key : keys) {
      if (((String) key).startsWith(prefix))
        result.add((String) key);
    }

    return result;
  }

  protected String getProperty(String key)
  {
    String value = session.getUserProperties().getProperty(key);

    if (value == null)
    {
      value = project.getProperties().getProperty(key);
    }

    return value;
  }

}