package de.is24.deadcode4j.plugin; import org.apache.commons.io.IOUtils; import org.apache.maven.plugin.LegacySupport; import org.apache.maven.shared.runtime.MavenProjectProperties; import org.apache.maven.shared.runtime.MavenRuntime; import org.apache.maven.shared.runtime.MavenRuntimeException; import org.codehaus.plexus.component.annotations.Component; import org.codehaus.plexus.component.annotations.Requirement; import org.codehaus.plexus.components.interactivity.Prompter; import org.codehaus.plexus.components.interactivity.PrompterException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.util.*; import static com.google.common.base.Strings.emptyToNull; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Maps.newHashMapWithExpectedSize; import static java.util.Arrays.asList; /** * Sends usage statistics (via Google Forms). * * @since 2.0.0 */ @Component(role = UsageStatisticsManager.class) public class UsageStatisticsManager { @Requirement private LegacySupport legacySupport; @Requirement private MavenRuntime mavenRuntime; @Requirement private Prompter prompter; /** * This is around for testing. * * @since 2.0.0 */ protected HttpURLConnection openUrlConnection() throws IOException { URL url = new URL("https://docs.google.com/forms/d/1-XZeeAyHrucUMREQLHZEnZ5mhywYZi5Dk9nfEv7U2GU/formResponse"); return HttpURLConnection.class.cast(url.openConnection()); } public void sendUsageStatistics(DeadCodeStatistics deadCodeStatistics) { final Logger logger = getLogger(); if (Boolean.TRUE.equals(deadCodeStatistics.getSkipSendingUsageStatistics())) { logger.debug("Configuration wants to me to skip sending usage statistics."); return; } if (legacySupport.getSession().isOffline()) { logger.info("Running in offline mode; skipping sending of usage statistics."); return; } SystemProperties systemProperties = SystemProperties.from(legacySupport, mavenRuntime); if (Boolean.FALSE.equals(deadCodeStatistics.getSkipSendingUsageStatistics())) { logger.debug("Configured to send usage statistics."); } else { if (!legacySupport.getSession().getRequest().isInteractiveMode()) { logger.info("Running in non-interactive mode; skipping sending of usage statistics."); return; } if (!askForPermissionAndComment(deadCodeStatistics, systemProperties)) { return; } } Map<String, String> parameters = getParameters(deadCodeStatistics, systemProperties); sendUsageStatistics(parameters); } private Logger getLogger() { return LoggerFactory.getLogger(getClass()); } private void sendUsageStatistics(Map<String, String> parameters) { final Logger logger = getLogger(); HttpURLConnection urlConnection = null; try { urlConnection = openUrlConnection(); urlConnection.setAllowUserInteraction(false); urlConnection.setConnectTimeout(2000); urlConnection.setDoInput(true); urlConnection.setDoOutput(true); urlConnection.setInstanceFollowRedirects(false); urlConnection.setReadTimeout(5000); urlConnection.setRequestProperty("content-type", "application/x-www-form-urlencoded"); urlConnection.connect(); writeParameters(parameters, urlConnection); processResponse(urlConnection); } catch (IOException e) { logger.debug("Failed to send statistics!", e); logger.info("Failed sending usage statistics."); } finally { IOUtils.close(urlConnection); } } private void processResponse(HttpURLConnection urlConnection) throws IOException { final Logger logger = getLogger(); int responseCode = urlConnection.getResponseCode(); if (responseCode / 100 == 2) { logger.info("Usage statistics have been transferred."); return; } logger.info("Could not transfer usage statistics: {}/{}", responseCode, urlConnection.getResponseMessage()); if (logger.isDebugEnabled()) { InputStream inputStream = urlConnection.getInputStream(); try { List<String> response = IOUtils.readLines(inputStream, "UTF-8"); for (String line : response) { logger.debug(line); } } finally { IOUtils.closeQuietly(inputStream); } } } private void writeParameters(Map<String, String> parameters, HttpURLConnection urlConnection) throws IOException { StringBuilder buffy = new StringBuilder(); for (Map.Entry<String, String> entry : parameters.entrySet()) { buffy.append(entry.getKey()).append('=').append(URLEncoder.encode(entry.getValue(), "UTF-8")).append('&'); } OutputStream outputStream = urlConnection.getOutputStream(); try { outputStream.write(buffy.toString().getBytes("UTF-8")); } finally { IOUtils.closeQuietly(outputStream); } } private Map<String, String> getParameters(DeadCodeStatistics deadCodeStatistics, SystemProperties systemProperties) { HashMap<String, String> parameters = newHashMap(); deadCodeStatistics.addRequestParameters(parameters); systemProperties.addRequestParameters(parameters); return parameters; } /** * @return {@code null} if sending statistics should be aborted */ private boolean askForPermissionAndComment(DeadCodeStatistics deadCodeStatistics, SystemProperties systemProperties) { final Logger logger = getLogger(); StringBuilder buffy = listStatistics(deadCodeStatistics, systemProperties); try { buffy.append("\nMay I report those usage statistics (via HTTPS)?"); String answer = prompter.prompt(buffy.toString(), asList("Y", "N"), "Y"); if ("N".equals(answer)) { logger.info("Sending usage statistics is aborted."); logger.info("You may configure deadcode4j to permanently disable sending usage statistics."); return false; } if (deadCodeStatistics.getUsageStatisticsComment() == null) { deadCodeStatistics.setUsageStatisticsComment(prompter.prompt( "Awesome! Would you like to state a testimonial or give a comment? Here you can")); } return true; } catch (PrompterException e) { logger.debug("Prompter failed!", e); logger.info("Failed to interact with the user!"); return false; } } private StringBuilder listStatistics(DeadCodeStatistics deadCodeStatistics, SystemProperties systemProperties) { StringBuilder buffy = new StringBuilder(1024).append("I gathered the following system properties:"); for (String key : new TreeSet<String>(SystemProperties.KEYS.keySet())) { buffy.append("\n ").append(key).append(": ").append(systemProperties.values.get(key)); } buffy.append("\nextracted this from your configuration: "); if (deadCodeStatistics.getUsageStatisticsComment() != null) { buffy.append("\n your comment to deadcode4j: "). append(deadCodeStatistics.getUsageStatisticsComment()); } buffy.append("\n value for ignoreMainClasses: "). append(deadCodeStatistics.config_ignoreMainClasses) .append("\n value for skipUpdateCheck: "). append(deadCodeStatistics.config_skipUpdateCheck) .append("\n number of classes to ignore: "). append(deadCodeStatistics.config_numberOfClassesToIgnore) .append("\n number of custom annotations: "). append(deadCodeStatistics.config_numberOfCustomAnnotations) .append("\n number of custom interfaces: "). append(deadCodeStatistics.config_numberOfCustomInterfaces) .append("\n number of custom superclasses: "). append(deadCodeStatistics.config_numberOfCustomSuperclasses) .append("\n number of custom XML definitions: "). append(deadCodeStatistics.config_numberOfCustomXmlDefinitions) .append("\n number of modules to skip: "). append(deadCodeStatistics.config_numberOfModulesToSkip) .append("\nand gathered those results: ") .append("\n analyzed classes: ").append(deadCodeStatistics.numberOfAnalyzedClasses) .append("\n analyzed modules: ").append(deadCodeStatistics.numberOfAnalyzedModules) .append("\n found dead classes: ").append(deadCodeStatistics.numberOfDeadClassesFound); return buffy; } static final class DeadCodeStatistics { private final Boolean skipSendingUsageStatistics; private String usageStatisticsComment; public int numberOfAnalyzedClasses; public int numberOfAnalyzedModules; public int numberOfDeadClassesFound; public boolean config_ignoreMainClasses; public int config_numberOfClassesToIgnore; public int config_numberOfCustomAnnotations; public int config_numberOfCustomInterfaces; public int config_numberOfCustomSuperclasses; public int config_numberOfCustomXmlDefinitions; public int config_numberOfModulesToSkip; public boolean config_skipUpdateCheck; /** * Creates a new instance of {@code DeadCodeStatistics}. * * @param skipSendingUsageStatistics the configuration value indicating if statistics should be sent or not * @param usageStatisticsComment the configured comment */ public DeadCodeStatistics(Boolean skipSendingUsageStatistics, String usageStatisticsComment) { this.skipSendingUsageStatistics = skipSendingUsageStatistics; setUsageStatisticsComment(usageStatisticsComment); } public Boolean getSkipSendingUsageStatistics() { return skipSendingUsageStatistics; } public String getUsageStatisticsComment() { return usageStatisticsComment; } public void setUsageStatisticsComment(String usageStatisticsComment) { this.usageStatisticsComment = emptyToNull(usageStatisticsComment); } public void addRequestParameters(Map<String, String> parameters) { parameters.put("entry.1074756797", String.valueOf(numberOfAnalyzedClasses)); parameters.put("entry.1318897553", String.valueOf(numberOfAnalyzedModules)); parameters.put("entry.582394579", String.valueOf(numberOfDeadClassesFound)); parameters.put("entry.2113716156", String.valueOf(config_ignoreMainClasses)); parameters.put("entry.1255607340", String.valueOf(config_numberOfClassesToIgnore)); parameters.put("entry.837156809", String.valueOf(config_numberOfCustomAnnotations)); parameters.put("entry.1900438860", String.valueOf(config_numberOfCustomInterfaces)); parameters.put("entry.2138491452", String.valueOf(config_numberOfCustomSuperclasses)); parameters.put("entry.1308824804", String.valueOf(config_numberOfCustomXmlDefinitions)); parameters.put("entry.1094908901", String.valueOf(config_numberOfModulesToSkip)); parameters.put("entry.1760639029", String.valueOf(config_skipUpdateCheck)); if (this.skipSendingUsageStatistics != null) { parameters.put("entry.1975817511", String.valueOf(skipSendingUsageStatistics)); } if (this.usageStatisticsComment != null) { parameters.put("entry.2135548690", this.usageStatisticsComment); } } } public static class SystemProperties { public static final Map<String, String> KEYS; static { Map<String, String> keys = newHashMap(); keys.put("deadcode4j.version", "entry.1472283741"); keys.put("java.class.version", "entry.1951632658"); keys.put("java.runtime.name", "entry.890615248"); keys.put("java.runtime.version", "entry.120214478"); keys.put("java.specification.version", "entry.1933178438"); keys.put("java.version", "entry.344342021"); keys.put("java.vm.specification.version", "entry.1718484204"); keys.put("maven.build.version", "entry.1702626216"); keys.put("maven.version", "entry.131773189"); keys.put("os.name", "entry.1484769972"); keys.put("os.version", "entry.1546424580"); keys.put("user.country", "entry.1667669021"); keys.put("user.language", "entry.1213472042"); KEYS = Collections.unmodifiableMap(keys); } @SuppressWarnings("PMD.FieldDeclarationsShouldBeAtStartOfClass") private final Map<String, String> values = newHashMapWithExpectedSize(KEYS.size()); private SystemProperties() { values.put("deadcode4j.version", "2.0.0"); } public static SystemProperties from(LegacySupport legacySupport, MavenRuntime mavenRuntime) { SystemProperties systemProperties = new SystemProperties(); try { MavenProjectProperties projectProperties = mavenRuntime.getProjectProperties(SystemProperties.class); systemProperties.values.put("deadcode4j.version", projectProperties.getVersion()); } catch (MavenRuntimeException e) { LoggerFactory.getLogger(SystemProperties.class).debug("Failed to determine MavenRuntime.", e); } Properties properties = legacySupport.getSession().getRequest().getSystemProperties(); for (String key : KEYS.keySet()) { String property = emptyToNull(properties.getProperty(key)); if (property != null) { systemProperties.values.put(key, property); } } return systemProperties; } public void addRequestParameters(Map<String, String> parameters) { for (Map.Entry<String, String> entry : values.entrySet()) { parameters.put(KEYS.get(entry.getKey()), entry.getValue()); } } } }