com.sap.prd.mobile.ios.mios.XCodeVersionInfoMojo.java Source code

Java tutorial

Introduction

Here is the source code for com.sap.prd.mobile.ios.mios.XCodeVersionInfoMojo.java

Source

/*
 * #%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 static java.lang.String.format;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.xml.bind.JAXBException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProjectHelper;
import org.sonatype.aether.RepositorySystem;
import org.sonatype.aether.RepositorySystemSession;
import org.sonatype.aether.repository.RemoteRepository;
import org.xml.sax.SAXException;

import com.sap.prd.mobile.ios.mios.CodeSignManager.ExecResult;
import com.sap.prd.mobile.ios.mios.CodeSignManager.ExecutionResultVerificationException;
import com.sap.prd.mobile.ios.mios.versioninfo.v_1_2_2.Dependency;

/**
 * Generates a <artifact-id>-<version>-version.xml for reproducibility reasons. This
 * versions.xml contains information about the scm location and revision of the built project and
 * all its dependencies. Expects a sync.info file in the root folder of the project as input.
 * 
 * 
 * The sync.info file is a property file. If used with perforce it must contain the following entries: 
 * <code>
 * <ul>
 *   <li> type=perforce
 *   <li> port=&lt;The url of the perforce server&gt;
 *   <li> depotpath=&lt;The path synced on the perforce server&gt;
 *   <li> changelist=&lt;The changelist of the change that is being built&gt
 * </ul>
 * </code>
 *
 * 
 * If used with git it must contain the following entries:
 * 
 * <code>
 * <ul>
 *   <li> type=git
 *   <li> repo=&lt;The git repository&gt;
 *   <li> commitId=&lt;The commitId of the change that is being built&gt;
 * </ul>
 * </code>
 * 
 * For git based projects the sync.info file can be created with the following code snipped executed before the xcode-maven-plugin is triggered.
 * 
 * <pre>
 * echo "type=git" > sync.info
 * echo "repo=scm:git:$(git remote -v |awk '/fetch/ {print $2;}')" >> sync.info
 * echo "commitId=$(git rev-parse HEAD)" >> sync.info
 * </pre>
 * 
 * @goal attach-version-info
 * @requiresDependencyResolution
 */
public class XCodeVersionInfoMojo extends BuildContextAwareMojo {

    private final static String MIN_XCODE_VERSION_NO_STRICT_VERIFY = "6.0.0";
    private final static boolean DEFAULT_NO_STRICT_VERIFY_FOR_OLD_XCODE = false;
    private final static boolean DEFAULT_NO_STRICT_VERIFY_FOR_NEW_XCODE = true;

    /**
     * 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;

    /**
     * 
     * @parameter default-value="${session}"
     * @required
     * @readonly
     */
    protected MavenSession mavenSession;

    /**
     * The entry point to Aether, i.e. the component doing all the work.
     * 
     * @component
     */
    protected RepositorySystem repoSystem;

    /**
     * The current repository/network configuration of Maven.
     * 
     * @parameter default-value="${repositorySystemSession}"
     * @readonly
     */
    protected RepositorySystemSession repoSession;

    /**
     * The project's remote repositories to use for the resolution of project dependencies.
     * 
     * @parameter default-value="${project.remoteProjectRepositories}"
     * @readonly
     */
    protected List<RemoteRepository> projectRepos;

    /**
     * @component
     */
    private MavenProjectHelper projectHelper;

    /**
     * @parameter expression="${sync.info.file}" default-value="sync.info"
     */
    private String syncInfo;

    /**
     * If <code>true</code> the build fails if it does not find a sync.info file in the root directory
     * 
     * @parameter expression="${xcode.failOnMissingSyncInfo}" default-value="false"
     */
    private boolean failOnMissingSyncInfo;

    /**
     * If <code>true</code> the codesign --verify will be called with --no-strict option
     * 
     * @parameter expression="${xcode.noStrictVerify}"
     */
    private String noStrictVerify;

    /**
     * If <code>true</code> confidential information is removed from artifacts to be released.
     * 
     * @parameter expression="${xcode.hideConfidentialInformation}" default-value="true"
     */
    private boolean hideConfidentialInformation;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {

        final File syncInfoFile = new File(mavenSession.getExecutionRootDirectory(), syncInfo);

        if (!syncInfoFile.exists()) {

            if (failOnMissingSyncInfo) {
                throw new MojoExecutionException("Sync info file '" + syncInfoFile.getAbsolutePath()
                        + "' not found. Please configure your SCM plugin accordingly.");
            }

            getLog().info("The optional sync info file '" + syncInfoFile.getAbsolutePath()
                    + "' not found. Cannot attach versions.xml to build results.");
            return;
        }

        getLog().info(
                "Sync info file found: '" + syncInfoFile.getAbsolutePath() + "'. Creating versions.xml file.");

        final File versionsXmlFile = new File(project.getBuild().getDirectory(), "versions.xml");

        FileOutputStream os = null;

        try {
            os = new FileOutputStream(versionsXmlFile);

            new VersionInfoXmlManager().createVersionInfoFile(project.getGroupId(), project.getArtifactId(),
                    project.getVersion(), syncInfoFile, getDependencies(), os);

        } catch (IOException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        } finally {
            IOUtils.closeQuietly(os);
        }

        final File versionsPlistFile = new File(project.getBuild().getDirectory(), "versions.plist");
        if (versionsPlistFile.exists()) {
            if (!versionsPlistFile.delete()) {
                throw new IllegalStateException(
                        String.format("Cannot delete already existing plist file (%s)", versionsPlistFile));
            }
        }
        try {
            new VersionInfoPListManager().createVersionInfoPlistFile(project.getGroupId(), project.getArtifactId(),
                    project.getVersion(), syncInfoFile, getDependencies(), versionsPlistFile,
                    hideConfidentialInformation);
        } catch (IOException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        }

        try {
            if (PackagingType.getByMavenType(packaging) == PackagingType.APP) {
                try {
                    copyVersionsFilesAndSign();
                } catch (IOException e) {
                    throw new MojoExecutionException(e.getMessage(), e);
                } catch (ExecutionResultVerificationException e) {
                    throw new MojoExecutionException(e.getMessage(), e);
                } catch (XCodeException e) {
                    throw new MojoExecutionException(e.getMessage(), e);
                }
            }
        } catch (PackagingType.UnknownPackagingTypeException ex) {
            getLog().warn("Unknown packaing type detected.", ex);

        }

        projectHelper.attachArtifact(project, "xml", "versions", versionsXmlFile);
        getLog().info("versions.xml '" + versionsXmlFile + " attached as additional artifact.");

    }

    private void copyVersionsFilesAndSign()
            throws IOException, ExecutionResultVerificationException, XCodeException, MojoExecutionException {
        for (final String configuration : getConfigurations()) {
            for (final String sdk : getSDKs()) {
                if (sdk.startsWith("iphoneos")) {
                    File versionsXmlInBuild = new File(project.getBuild().getDirectory(), "versions.xml");
                    File versionsPListInBuild = new File(project.getBuild().getDirectory(), "versions.plist");

                    File rootDir = XCodeBuildLayout.getAppFolder(getXCodeCompileDirectory(), configuration, sdk);

                    String productName = getProductName(configuration, sdk);
                    File appFolder = new File(rootDir, productName + ".app");
                    File versionsXmlInApp = new File(appFolder, "versions.xml");
                    File versionsPListInApp = new File(appFolder, "versions.plist");

                    boolean isCodeSignActive = true;

                    if ((codeSignIdentity == null || codeSignIdentity.isEmpty()) && !codeSigningRequired)
                        isCodeSignActive = false;

                    ExecResult originalCodesignEntitlementsInfo = null;
                    ExecResult originalSecurityCMSMessageInfo = null;
                    if (isCodeSignActive) {
                        CodeSignManager.verify(appFolder, defineNoStrictVerifyBasedOnXcodeVersion());
                        originalCodesignEntitlementsInfo = CodeSignManager
                                .getCodesignEntitlementsInformation(appFolder);
                        originalSecurityCMSMessageInfo = CodeSignManager.getSecurityCMSInformation(appFolder);
                    } else {
                        getLog().info("CODE_SIGNING_REQUIRED=\"NO\"");
                        getLog().info("value of codeSignIdentity is " + codeSignIdentity);
                        getLog().info("value of codeSigningRequired is " + codeSigningRequired);
                    }
                    try {
                        if (hideConfidentialInformation) {
                            transformVersionsXml(versionsXmlInBuild, versionsXmlInApp);
                        } else {
                            FileUtils.copyFile(versionsXmlInBuild, versionsXmlInApp);
                        }
                    } catch (Exception e) {
                        throw new MojoExecutionException("Could not transform versions.xml: " + e.getMessage(), e);
                    }

                    getLog().info("Versions.xml file copied from: '" + versionsXmlInBuild + " ' to ' "
                            + versionsXmlInApp);
                    FileUtils.copyFile(versionsPListInBuild, versionsPListInApp);
                    getLog().info("Versions.plist file copied from: '" + versionsPListInBuild + " ' to ' "
                            + versionsPListInApp);

                    if (isCodeSignActive) {
                        sign(rootDir, configuration, sdk);
                        final ExecResult resignedCodesignEntitlementsInfo = CodeSignManager
                                .getCodesignEntitlementsInformation(appFolder);
                        final ExecResult resignedSecurityCMSMessageInfo = CodeSignManager
                                .getSecurityCMSInformation(appFolder);
                        CodeSignManager.verify(appFolder, defineNoStrictVerifyBasedOnXcodeVersion());
                        CodeSignManager.verify(originalCodesignEntitlementsInfo, resignedCodesignEntitlementsInfo);
                        CodeSignManager.verify(originalSecurityCMSMessageInfo, resignedSecurityCMSMessageInfo);
                    }

                }
            }
        }
    }

    private boolean defineNoStrictVerifyBasedOnXcodeVersion() throws XCodeException {
        if (noStrictVerify != null
                && (noStrictVerify.equalsIgnoreCase("true") || noStrictVerify.equalsIgnoreCase("false"))) {
            return Boolean.parseBoolean(noStrictVerify);
        } else {
            String xCodeVersionString = getXCodeVersionString();
            DefaultArtifactVersion version = getVersion(xCodeVersionString);
            if (checkVersions(version, MIN_XCODE_VERSION_NO_STRICT_VERIFY)) {
                return DEFAULT_NO_STRICT_VERIFY_FOR_NEW_XCODE;
            } else {
                return DEFAULT_NO_STRICT_VERIFY_FOR_OLD_XCODE;
            }
        }
    }

    void transformVersionsXml(File versionsXmlInBuild, File versionsXmlInApp) throws ParserConfigurationException,
            SAXException, IOException, TransformerFactoryConfigurationError, TransformerException, XCodeException {
        final InputStream transformerRule = getClass().getClassLoader()
                .getResourceAsStream("versionInfoCensorTransformation.xml");

        if (transformerRule == null) {
            throw new XCodeException("Could not read transformer rule.");
        }

        try {
            final Transformer transformer = TransformerFactory.newInstance()
                    .newTransformer(new StreamSource(transformerRule));
            transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
            transformer.transform(new StreamSource(versionsXmlInBuild), new StreamResult(versionsXmlInApp));
        } finally {
            IOUtils.closeQuietly(transformerRule);
        }
    }

    private void sign(File rootDir, String configuration, String sdk) throws IOException, XCodeException {
        String csi = (codeSignIdentity != null && codeSignIdentity.trim().length() > 0) ? codeSignIdentity
                : EffectiveBuildSettings.getBuildSetting(
                        getXCodeContext(XCodeContext.SourceCodeLocation.WORKING_COPY, configuration, sdk),
                        EffectiveBuildSettings.CODE_SIGN_IDENTITY);
        File appFolder = new File(EffectiveBuildSettings.getBuildSetting(
                getXCodeContext(XCodeContext.SourceCodeLocation.WORKING_COPY, configuration, sdk),
                EffectiveBuildSettings.CODESIGNING_FOLDER_PATH));
        CodeSignManager.sign(csi, appFolder, true);
        getLog().info(
                "value of codeSignIdentity choosed for resign app:  " + EffectiveBuildSettings.CODE_SIGN_IDENTITY);
        getLog().info("value of codeSigningRequired during resign " + codeSigningRequired);
    }

    private List<Dependency> getDependencies() throws IOException {

        List<Dependency> result = new ArrayList<Dependency>();

        for (@SuppressWarnings("rawtypes")
        final Iterator it = project.getDependencyArtifacts().iterator(); it.hasNext();) {

            final Artifact mainArtifact = (Artifact) it.next();

            try {

                org.sonatype.aether.artifact.Artifact sideArtifact = new XCodeDownloadManager(projectRepos,
                        repoSystem, repoSession).resolveSideArtifact(mainArtifact, "versions", "xml");

                getLog().info("Version information retrieved for artifact: " + mainArtifact);

                addParsedVersionsXmlDependency(result, sideArtifact);

            } catch (SideArtifactNotFoundException e) {
                getLog().warn("Could not retrieve version information for artifact:" + mainArtifact);
            }
        }
        return result;
    }

    void addParsedVersionsXmlDependency(List<Dependency> result, org.sonatype.aether.artifact.Artifact sideArtifact)
            throws IOException {
        try {
            result.add(VersionInfoXmlManager.parseDependency(sideArtifact.getFile()));
        } catch (SAXException e) {
            getLog().warn(format(
                    "Version file '%s' for artifact '%s' contains invalid content (Non parsable XML). Ignoring this file.",
                    (sideArtifact.getFile() != null ? sideArtifact.getFile() : "<n/a>"), sideArtifact));
        } catch (JAXBException e) {
            getLog().warn(format(
                    "Version file '%s' for artifact '%s' contains invalid content (Scheme violation). Ignoring this file.",
                    (sideArtifact.getFile() != null ? sideArtifact.getFile() : "<n/a>"), sideArtifact));
        }
    }

}