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

Java tutorial

Introduction

Here is the source code for com.sap.prd.mobile.ios.mios.XCodeVerificationCheckMojo.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 java.lang.String.format;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.LogManager;
import java.util.logging.Logger;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.CharSet;
import org.apache.commons.lang.StringUtils;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.codehaus.plexus.classworlds.ClassWorld;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
import org.sonatype.aether.RepositorySystem;
import org.sonatype.aether.RepositorySystemSession;
import org.sonatype.aether.collection.DependencyCollectionException;
import org.sonatype.aether.graph.Dependency;
import org.sonatype.aether.repository.RemoteRepository;
import org.sonatype.aether.util.artifact.DefaultArtifact;

import com.sap.prd.mobile.ios.mios.XCodeContext.SourceCodeLocation;
import com.sap.prd.mobile.ios.mios.verificationchecks.v_1_0_0.Check;
import com.sap.prd.mobile.ios.mios.verificationchecks.v_1_0_0.Checks;

/**
 * Provides the possibility to perform verification checks.<br>
 * The check classes and their severities are described in an additional xml document, defined in
 * <code>xcode.verification.checks.definitionFile</code>.<br>
 * The specific checks have to be implemented in separate projects. These projects define dependency
 * to Xcode Maven Pugin Verification API and must not reference the xcode-maven-plugin project.
 * The Xcode Maven Plugin Verification API project could be found <a href=https://github.com/sap-production/xcode-maven-plugin-verification-api>here</a>
 * The coordinates of that projects need to be provided on the
 * <code>check</code> node belonging to the test as attributes <code>groupId</code>,
 * <code>artifactId</code> and <code>version</code>.<br>
 * The classpath for this goal will be extended by the jars found under the specified GAVs. <br>
 * Example checks definition:
 * 
 * <pre>
 * &lt;checks&gt;
 *   &lt;check groupId="my.group.id" artifactId="artifactId" version="1.0.0" severity="ERROR" class="com.my.MyVerificationCheck1"/&gt;
 *   &lt;check groupId="my.group.id" artifactId="artifactId" version="1.0.0" severity="WARNING" class="com.my.MyVerificationCheck2"/&gt;
 * &lt;/checks&gt;
 * </pre>
 * 
 * @goal verification-check
 * 
 */
public class XCodeVerificationCheckMojo extends BuildContextAwareMojo {
    private final static String COLON = ":", DOUBLE_SLASH = "//";
    private static final Logger log = LogManager.getLogManager().getLogger(XCodePluginLogger.getLoggerName());

    private enum Protocol {

        HTTP() {

            @Override
            Reader getCheckDefinitions(String location) throws IOException {
                HttpClient httpClient = new DefaultHttpClient();
                HttpGet get = new HttpGet(getName() + COLON + DOUBLE_SLASH + location);

                String response = httpClient.execute(get, new BasicResponseHandler());
                return new StringReader(response);
            }

        },
        HTTPS() {

            @Override
            Reader getCheckDefinitions(String location) throws IOException {
                HttpClient httpClient = new DefaultHttpClient();
                try {
                    SSLContext sslcontext = SSLContext.getInstance("TLS");
                    X509TrustManager trustManager = new X509TrustManager() {

                        @Override
                        public X509Certificate[] getAcceptedIssuers() {
                            return null;
                        }

                        @Override
                        public void checkServerTrusted(X509Certificate[] chain, String authType)
                                throws CertificateException {
                        }

                        @Override
                        public void checkClientTrusted(X509Certificate[] chain, String authType)
                                throws CertificateException {
                        }
                    };
                    X509HostnameVerifier hostNameVerifier = new X509HostnameVerifier() {

                        @Override
                        public boolean verify(String arg0, SSLSession arg1) {
                            return true;
                        }

                        @Override
                        public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException {
                        }

                        @Override
                        public void verify(String host, X509Certificate cert) throws SSLException {
                        }

                        @Override
                        public void verify(String host, SSLSocket ssl) throws IOException {
                        }
                    };

                    final int port = new URL(getName() + COLON + DOUBLE_SLASH + location).getPort();
                    sslcontext.init(null, new TrustManager[] { trustManager }, null);
                    SSLSocketFactory sslSocketFactory = new SSLSocketFactory(sslcontext);
                    sslSocketFactory.setHostnameVerifier(hostNameVerifier);
                    ClientConnectionManager clientConnectionManager = httpClient.getConnectionManager();
                    SchemeRegistry sr = clientConnectionManager.getSchemeRegistry();
                    sr.register(new Scheme(getName(), sslSocketFactory, port));
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (KeyManagementException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                HttpGet get = new HttpGet(getName() + COLON + DOUBLE_SLASH + location);

                String response = httpClient.execute(get, new BasicResponseHandler());
                return new StringReader(response);
            }
        },
        FILE() {

            @Override
            Reader getCheckDefinitions(String location) throws IOException {
                if (location.startsWith(DOUBLE_SLASH))
                    location = location.substring(DOUBLE_SLASH.length());
                final File f = new File(location);
                if (!f.canRead()) {
                    throw new IOException("Cannot read checkDefintionFile '" + f + "'.");
                }

                return new InputStreamReader((new FileInputStream(f)), "UTF-8");
            }
        };
        abstract Reader getCheckDefinitions(String location) throws IOException;

        String getName() {
            return name().toLowerCase(Locale.ENGLISH);
        }

        static String getProtocols() {
            final StringBuilder sb = new StringBuilder(16);
            for (Protocol p : Protocol.values()) {
                if (sb.length() != 0)
                    sb.append(", ");
                sb.append(p.getName());
            }
            return sb.toString();
        }

        static Protocol getProtocol(String protocol) throws InvalidProtocolException {
            try {
                return Protocol.valueOf(protocol.toUpperCase(Locale.ENGLISH));
            } catch (final IllegalArgumentException ex) {
                throw new InvalidProtocolException(protocol, ex);
            }
        }
    }

    static class NoProtocolException extends XCodeException {

        private static final long serialVersionUID = -5510547403353575108L;

        NoProtocolException(String message, Throwable cause) {
            super(message, cause);
        }
    };

    static class InvalidProtocolException extends XCodeException {

        private static final long serialVersionUID = -5510547403353515108L;

        InvalidProtocolException(String message, Throwable cause) {
            super(message, cause);
        }
    };

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

    /**
     * Parameter, which controls the verification goal execution. By default, the verification goal
     * will be skipped.
     * 
     * @parameter expression="${xcode.verification.checks.skip}" default-value="true"
     * @since 1.9.3
     */
    private boolean skip;

    /**
     * The location where the check definition file is present. Could be a file on the local file
     * system or a remote located file, accessed via http or https. <br>
     * Examples:
     * <ul>
     * <li>-Dxcode.verification.checks.definitionFile=file:./checkDefinitionFile.xml
     * <li>-Dxcode.verification.checks.definitionFile=http://example.com/checkDefinitionFile.xml
     * <li>-Dxcode.verification.checks.definitionFile=https://example.com/checkDefinitionFile.xml
     * </ul>
     * 
     * @parameter expression="${xcode.verification.checks.definitionFile}"
     * @since 1.9.3
     */
    private String checkDefinitionFile;

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

        if (skip) {

            getLog().info(String.format(
                    "Verification check goal has been skipped intentionally since parameter 'xcode.verification.checks.skip' is '%s'.",
                    skip));
            return;
        }

        try {
            PackagingType.getByMavenType(packaging);
        } catch (PackagingType.UnknownPackagingTypeException ex) {
            getLog().info("Packaging type is " + packaging
                    + ". There is no need to apply verification checks for this packaging type.");
            return;
        }

        try {

            final Checks checks = getChecks(checkDefinitionFile);

            if (checks.getCheck().isEmpty()) {
                getLog().warn(String.format("No checks configured in '%s'.", checkDefinitionFile));
            }

            Map<Check, Exception> failedChecks = new HashMap<Check, Exception>();

            for (Check check : checks.getCheck()) {
                try {
                    final ClassRealm verificationCheckRealm = extendClasspath(check);
                    final Exception ex = performCheck(verificationCheckRealm, check);
                    if (ex != null) {
                        failedChecks.put(check, ex);
                    }
                } catch (DuplicateRealmException ex) {
                    throw new MojoExecutionException(ex.getMessage(), ex);
                }
            }

            handleExceptions(failedChecks);

        } catch (XCodeException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        } catch (IOException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        } catch (JAXBException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        } catch (DependencyCollectionException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        }
    }

    private Exception performCheck(ClassRealm verificationCheckRealm, final Check checkDesc)
            throws MojoExecutionException {

        getLog().info(String.format("Performing verification check '%s'.", checkDesc.getClazz()));

        if (getLog().isDebugEnabled()) {

            final Charset defaultCharset = Charset.defaultCharset();
            final ByteArrayOutputStream byteOs = new ByteArrayOutputStream();
            final PrintStream ps;
            try {
                ps = new PrintStream(byteOs, true, defaultCharset.name());
            } catch (UnsupportedEncodingException ex) {
                throw new MojoExecutionException(
                        String.format("Charset '%s' cannot be found.", defaultCharset.name()));
            }

            try {
                verificationCheckRealm.display(ps);
                ps.close();
                getLog().debug(String.format("Using classloader for loading verification check '%s':%s%s",
                        checkDesc.getClazz(), System.getProperty("line.separator"),
                        new String(byteOs.toByteArray(), defaultCharset)));
            } finally {
                IOUtils.closeQuietly(ps);
            }
        }

        try {
            final Class<?> verificationCheckClass = Class.forName(checkDesc.getClazz(), true,
                    verificationCheckRealm);

            getLog().debug(String.format("Verification check class %s has been loaded by %s.",
                    verificationCheckClass.getName(), verificationCheckClass.getClassLoader()));
            getLog().debug(String.format("Verification check super class %s has been loaded by %s.",
                    verificationCheckClass.getSuperclass().getName(),
                    verificationCheckClass.getSuperclass().getClassLoader()));
            getLog().debug(String.format("%s class used by this class (%s) has been loaded by %s.",
                    VerificationCheck.class.getName(), this.getClass().getName(),
                    VerificationCheck.class.getClassLoader()));

            for (final String configuration : getConfigurations()) {
                for (final String sdk : getSDKs()) {
                    getLog().info(
                            String.format("Executing verification check: '%s' for configuration '%s' and sdk '%s'.",
                                    verificationCheckClass.getName(), configuration, sdk));
                    final VerificationCheck verificationCheck = (VerificationCheck) verificationCheckClass
                            .newInstance();
                    verificationCheck
                            .setXcodeContext(getXCodeContext(SourceCodeLocation.WORKING_COPY, configuration, sdk));
                    verificationCheck.setMavenProject(project);
                    verificationCheck.setEffectiveBuildSettings(new EffectiveBuildSettings());
                    try {
                        verificationCheck.check();
                    } catch (VerificationException ex) {
                        return ex;
                    } catch (RuntimeException ex) {
                        return ex;
                    }
                }
            }
            return null;
        } catch (ClassNotFoundException ex) {
            throw new MojoExecutionException("Could not load verification check '" + checkDesc.getClazz()
                    + "'. May be your classpath has not been properly extended. "
                    + "Provide the GAV of the project containing the check as attributes as part of the check defintion in the check configuration file.",
                    ex);
        } catch (NoClassDefFoundError err) {
            getLog().error(String.format(
                    "Could not load verification check '%s'. "
                            + "May be your classpath has not been properly extended. "
                            + "Additional dependencies need to be declard inside the check definition file: %s",
                    checkDesc.getClazz(), err.getMessage()), err);
            throw err;
        } catch (InstantiationException ex) {
            throw new MojoExecutionException(String.format("Could not instanciate verification check '%s': %s",
                    checkDesc.getClazz(), ex.getMessage()), ex);
        } catch (IllegalAccessException ex) {
            throw new MojoExecutionException(String.format("Could not access verification check '%s': %s",
                    checkDesc.getClazz(), ex.getMessage()), ex);
        }

    }

    private void handleExceptions(Map<Check, Exception> failedChecks) throws MojoExecutionException {
        boolean mustFailedTheBuild = false;
        for (Map.Entry<Check, Exception> entry : failedChecks.entrySet()) {
            handleException(entry.getKey(), entry.getValue());
            if (entry.getKey().getSeverity().equalsIgnoreCase("ERROR")) {
                mustFailedTheBuild = true;
            }
        }
        if (mustFailedTheBuild) {
            throw new MojoExecutionException("Verification checks failed. See the log file for details.");
        }
    }

    private void handleException(Check failedCheck, final Exception e) {
        final String message;
        if (e instanceof VerificationException) {
            message = "Verification check '" + failedCheck.getClazz() + " failed. " + e.getMessage();
        } else {
            message = "Cannot perform check: " + failedCheck.getClazz() + ". Error during test setup "
                    + e.getMessage();
        }
        if (failedCheck.getSeverity().equalsIgnoreCase("WARNING")) {
            getLog().warn(message);
        } else {
            getLog().error(message);
        }
    }

    private ClassRealm extendClasspath(Check check)
            throws XCodeException, DependencyCollectionException, DuplicateRealmException, MalformedURLException {
        final org.sonatype.aether.artifact.Artifact artifact = parseDependency(check);

        final ClassLoader loader = this.getClass().getClassLoader();

        if (!(loader instanceof ClassRealm)) {

            throw new XCodeException("Could not add jar to classpath. Class loader '" + loader
                    + "' is not an instance of '" + ClassRealm.class.getName() + "'.");
        }

        final ClassRealm classRealm = (ClassRealm) loader;

        if (artifact == null) {
            return classRealm;
        }

        final Set<String> scopes = new HashSet<String>(Arrays.asList(
                org.apache.maven.artifact.Artifact.SCOPE_COMPILE, org.apache.maven.artifact.Artifact.SCOPE_PROVIDED,
                org.apache.maven.artifact.Artifact.SCOPE_RUNTIME, org.apache.maven.artifact.Artifact.SCOPE_SYSTEM)); // do not resolve dependencies with scope "test".

        final XCodeDownloadManager downloadManager = new XCodeDownloadManager(projectRepos, repoSystem,
                repoSession);

        final Set<org.sonatype.aether.artifact.Artifact> theEmptyOmitsSet = Collections.emptySet();
        final Set<org.sonatype.aether.artifact.Artifact> omits = downloadManager
                .resolveArtifactWithTransitveDependencies(
                        new Dependency(getVerificationAPIGav(), org.apache.maven.artifact.Artifact.SCOPE_COMPILE),
                        scopes, theEmptyOmitsSet);

        omits.add(getVerificationAPIGav());

        final Set<org.sonatype.aether.artifact.Artifact> artifacts = downloadManager
                .resolveArtifactWithTransitveDependencies(
                        new Dependency(artifact, org.apache.maven.artifact.Artifact.SCOPE_COMPILE), scopes, omits);

        final ClassRealm childClassRealm = classRealm.createChildRealm(
                getUniqueRealmId(classRealm.getWorld(), classRealm.getId() + "-" + check.getClazz()));

        addDependencies(childClassRealm, artifacts);

        return childClassRealm;
    }

    private String getUniqueRealmId(final ClassWorld world, final String realmIdPrefix) {
        String uniqueRealmIdCandidate = null;
        int i = 0;
        while (true) {
            uniqueRealmIdCandidate = realmIdPrefix + "-" + i;
            if (world.getClassRealm(uniqueRealmIdCandidate) == null) {
                return uniqueRealmIdCandidate;
            }
            i++;
        }
    }

    private void addDependencies(final ClassRealm childClassRealm,
            Set<org.sonatype.aether.artifact.Artifact> artifacts) throws MalformedURLException {
        for (org.sonatype.aether.artifact.Artifact a : artifacts) {
            childClassRealm.addURL(a.getFile().toURI().toURL());
        }
    }

    static org.sonatype.aether.artifact.Artifact parseDependency(final Check check) throws XCodeException {
        final String groupId = check.getGroupId();
        final String artifactId = check.getArtifactId();
        final String version = check.getVersion();

        if (StringUtils.isEmpty(groupId) && StringUtils.isEmpty(artifactId) && StringUtils.isEmpty(version)) {
            log.info("No coordinates maintained for check represented by class '" + check.getClazz()
                    + "'. Assuming this check is already contained in the classpath.");
            return null;
        }

        if (StringUtils.isEmpty(groupId))
            throw new XCodeException(String.format("groupId for check %s is null or emtpy", check.getClazz()));

        if (StringUtils.isEmpty(artifactId))
            throw new XCodeException(String.format("artifactId for check %s is null or emtpy", check.getClazz()));

        if (StringUtils.isEmpty(version))
            throw new XCodeException(String.format("version for check %s is null or emtpy", check.getClazz()));

        return new DefaultArtifact(groupId, artifactId, "jar", version);
    }

    static Checks getChecks(final String checkDefinitionFileLocation)
            throws XCodeException, IOException, JAXBException {
        Reader checkDefinitions = null;

        try {
            checkDefinitions = getChecksDescriptor(checkDefinitionFileLocation);
            return (Checks) JAXBContext.newInstance(Checks.class).createUnmarshaller().unmarshal(checkDefinitions);
        } finally {
            IOUtils.closeQuietly(checkDefinitions);
        }
    }

    org.sonatype.aether.artifact.Artifact getVerificationAPIGav() throws XCodeException {

        InputStream is = null;

        try {
            is = XCodeVerificationCheckMojo.class.getResourceAsStream("/misc/project.properties");

            if (is == null) {
                throw new XCodeException("Cannot get the GAV of the xcode-maven-plugin");
            }

            Properties props = new Properties();
            props.load(is);

            final String groupId = props.getProperty("verification.api.groupId");
            final String artifactId = props.getProperty("verification.api.artifactId");
            final String version = props.getProperty("verification.api.version");
            return new DefaultArtifact(groupId, artifactId, "jar", version);
        } catch (final IOException ex) {
            throw new XCodeException("Cannot get the GAV for the verification API", ex);
        } finally {
            IOUtils.closeQuietly(is);
        }
    }

    static Reader getChecksDescriptor(final String checkDefinitionFileLocation) throws XCodeException, IOException {
        if (checkDefinitionFileLocation == null || checkDefinitionFileLocation.trim().isEmpty()) {
            throw new XCodeException(
                    "CheckDefinitionFile was not configured. Cannot perform verification checks. Define check definition file with paramater 'xcode.verification.checks.definitionFile'.");
        }

        final Location location = Location.getLocation(checkDefinitionFileLocation);

        try {
            Protocol protocol = Protocol.valueOf(location.protocol);
            return protocol.getCheckDefinitions(location.location);
        } catch (IllegalArgumentException ex) {
            throw new InvalidProtocolException(format("Invalid protocol provided: '%s'. Supported values are:'%s'.",
                    location.protocol, Protocol.getProtocols()), ex);
        } catch (IOException ex) {
            throw new IOException(format("Cannot get check definitions from '%s'.", checkDefinitionFileLocation),
                    ex);
        }
    }

    static class Location {
        static Location getLocation(final String locationUriString)
                throws InvalidProtocolException, NoProtocolException, MalformedURLException {
            final URL url;

            try {
                url = new URL(locationUriString.trim());
            } catch (MalformedURLException ex) {

                //
                // trouble with protocol ???
                //

                try {

                    if (URI.create(locationUriString).getScheme() == null) {
                        throw new NoProtocolException(String.format(
                                "Provide a protocol [%s] for parameter 'xcode.verification.checks.definitionFile'",
                                Protocol.getProtocols()), ex);
                    }
                } catch (RuntimeException ignore) {
                    //
                    // in this case we throw already the MalformedUrlExcpetion that indicates a problem with 
                    // the URL
                    //
                }

                throw ex;

            }

            final Protocol protocol = Protocol.getProtocol(url.getProtocol());
            final String location;
            if (protocol == Protocol.FILE) {
                location = url.getPath();
            } else if (protocol == Protocol.HTTP || protocol == Protocol.HTTPS) {
                location = locationUriString.trim()
                        .substring(protocol.getName().length() + COLON.length() + DOUBLE_SLASH.length());
            } else {
                throw new IllegalStateException(String.format("Unknown protocol: '%s'." + url.getProtocol()));
            }
            return new Location(protocol.getName(), location);
        }

        final String protocol;
        final String location;

        public Location(String protocol, String location) {
            this.protocol = protocol.toUpperCase(Locale.ENGLISH);
            this.location = location;
        }
    }
}