XCodeVerificationCheckMojo.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 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>
* <checks>
* <check groupId="my.group.id" artifactId="artifactId" version="1.0.0" severity="ERROR" class="com.my.MyVerificationCheck1"/>
* <check groupId="my.group.id" artifactId="artifactId" version="1.0.0" severity="WARNING" class="com.my.MyVerificationCheck2"/>
* </checks>
* </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;
}
}
}