org.springframework.ide.eclipse.internal.uaa.UaaManager.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.ide.eclipse.internal.uaa.UaaManager.java

Source

/*******************************************************************************
 * Copyright (c) 2010, 2011 Spring IDE Developers
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Spring IDE Developers - initial API and implementation
 *******************************************************************************/
package org.springframework.ide.eclipse.internal.uaa;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.prefs.Preferences;

import javax.xml.parsers.DocumentBuilderFactory;

import org.eclipse.core.net.proxy.IProxyData;
import org.eclipse.core.net.proxy.IProxyService;
import org.eclipse.core.runtime.IBundleGroup;
import org.eclipse.core.runtime.IBundleGroupProvider;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.springframework.ide.eclipse.internal.uaa.client.QueueingUaaServiceExtension;
import org.springframework.ide.eclipse.uaa.IUaa;
import org.springframework.ide.eclipse.uaa.UaaPlugin;
import org.springframework.ide.eclipse.uaa.UaaUtils;
import org.springframework.uaa.client.TransmissionAwareUaaService;
import org.springframework.uaa.client.TransmissionEventListener;
import org.springframework.uaa.client.UaaService;
import org.springframework.uaa.client.VersionHelper;
import org.springframework.uaa.client.internal.BasicProxyService;
import org.springframework.uaa.client.internal.JdkUrlTransmissionServiceImpl;
import org.springframework.uaa.client.protobuf.UaaClient.FeatureUse;
import org.springframework.uaa.client.protobuf.UaaClient.Privacy.PrivacyLevel;
import org.springframework.uaa.client.protobuf.UaaClient.Product;
import org.springframework.uaa.client.util.Base64;
import org.springframework.uaa.client.util.PgpUtils;
import org.springframework.uaa.client.util.PreferencesUtils;
import org.springframework.uaa.client.util.StreamUtils;
import org.springframework.uaa.client.util.StringUtils;
import org.springframework.uaa.client.util.XmlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * Helper class that coordinates with the Spring UAA service implementation.
 * <p>
 * This implementation primarily serves as wrapper around the {@link UaaService}.
 * @author Christian Dupuis
 * @since 2.5.2
 */
public class UaaManager implements IUaa {

    private static final String DETECTED_PRODUCTS_KEY = "detected_eclipse_products";
    private static final String DETECTED_PRODUCTS_SIGNATURE_KEY = "detected_eclipse_products_signature";
    private static final byte[] EMPTY = {};
    private static final String EMPTY_STRING = "";
    private static final String EMPTY_VERSION = "0.0.0.RELEASE";
    private static final long MIN_REPORTING_INTERVAL = 1000L * 60L * 60L * 12L; // report a unique usage record only all 12h
    private static final Preferences P = PreferencesUtils.getPreferencesFor(UaaManager.class);

    private static final URL SIGNATURE_URL;
    private static final URL UAA_URL;

    static {
        // Produce the urls safely
        try {
            UAA_URL = new URL("http://uaa.springsource.org/uaa-eclipse.xml");
            SIGNATURE_URL = new URL("http://uaa.springsource.org/uaa-eclipse.xml.asc");
        } catch (MalformedURLException neverHappens) {
            throw new IllegalStateException(neverHappens);
        }
    }

    private static final String THREAD_NAME_TEMPLATE = "Reporting Thread-%s (%s/%s.%s.%s)";
    private final AtomicInteger threadCount = new AtomicInteger(0);
    private final ExecutorService executorService = Executors.newFixedThreadPool(1, new ThreadFactory() {

        public Thread newThread(Runnable runnable) {
            Product uaaProduct = VersionHelper.getUaa();
            Thread reportingThread = new Thread(runnable,
                    String.format(THREAD_NAME_TEMPLATE, threadCount.incrementAndGet(), uaaProduct.getName(),
                            uaaProduct.getMajorVersion(), uaaProduct.getMinorVersion(),
                            uaaProduct.getPatchVersion()));
            reportingThread.setDaemon(true);
            return reportingThread;
        }
    });

    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();

    private List<ProductDescriptor> productDescriptors = new CopyOnWriteArrayList<UaaManager.ProductDescriptor>();
    private List<RegistrationAttempt> registrationAttempts = new CopyOnWriteArrayList<RegistrationAttempt>();
    private QueueingUaaServiceExtension service = new QueueingUaaServiceExtension(
            new JdkUrlTransmissionServiceImpl(new EclipseProxyService()));

    public UaaService getUaaService() {
        return service;
    }

    /**
     * {@inheritDoc}
     */
    public int getPrivacyLevel() {
        try {
            r.lock();
            return this.service.getPrivacyLevel().getNumber();
        } finally {
            r.unlock();
        }
    }

    /**
     * {@inheritDoc}
     */
    public String getReadablePayload() {
        try {
            r.lock();
            return StringUtils.toString(service.getPayload(), true, 2);
        } finally {
            r.unlock();
        }
    }

    /**
     * {@inheritDoc}
     */
    public void registerFeatureUse(String plugin) {
        registerFeatureUse(plugin, null);
    }

    /**
     * {@inheritDoc}
     */
    public void registerFeatureUse(final String plugin, final Map<String, String> featureData) {
        if (plugin != null) {

            // Before we trigger eventually expensive background reporting, check if this
            // feature hasn't recently been reported; if so just skip
            final RegistrationAttempt attempt = new FeatureUseRegistrationAttempt(plugin, featureData);
            if (shouldSkipRegistrationAttempt(attempt)) {
                return;
            }

            executorService.execute(new Runnable() {

                public void run() {
                    try {
                        w.lock();

                        for (ProductDescriptor productDescriptor : productDescriptors) {
                            if (productDescriptor.registerFeatureUseIfMatch(plugin, featureData)) {
                                registrationAttempts.add(attempt);
                                return;
                            }
                        }
                    } catch (IllegalArgumentException e) {
                        // Ignore as it may sporadically come up from the preferences API
                    } finally {
                        w.unlock();
                    }
                }
            });
        }
    }

    /**
     * {@inheritDoc}
     */
    public void registerProductUse(String productId, String version) {
        registerProductUse(productId, version, null);
    }

    /**
     * {@inheritDoc}
     */
    public void registerProductUse(final String productId, String version, final String projectId) {
        if (version == null) {
            version = EMPTY_VERSION;
        }

        final String versionString = version;

        if (productId != null) {

            // Before we trigger eventually expensive background reporting, check if this
            // product hasn't recently been reported; if so just skip
            final RegistrationAttempt attempt = new ProductRegistrationAttempt(productId, versionString, projectId);
            if (shouldSkipRegistrationAttempt(attempt)) {
                return;
            }

            executorService.execute(new Runnable() {

                public void run() {
                    try {
                        w.lock();

                        Product product = null;
                        try {
                            Version version = Version.parseVersion(versionString);
                            Product.Builder productBuilder = Product.newBuilder();
                            productBuilder.setName(productId);
                            productBuilder.setMajorVersion(version.getMajor());
                            productBuilder.setMinorVersion(version.getMinor());
                            productBuilder.setPatchVersion(version.getMicro());
                            productBuilder.setReleaseQualifier(version.getQualifier());
                            // product.setSourceControlIdentifier();

                            product = productBuilder.build();
                        } catch (IllegalArgumentException e) {
                            // As a fallback we use the Spring UAA way of producing products
                            product = VersionHelper.getProduct(productId, versionString);
                        }

                        if (projectId == null) {
                            service.registerProductUsage(product);
                        } else {
                            service.registerProductUsage(product, projectId);
                        }
                        registrationAttempts.add(attempt);
                    } finally {
                        w.unlock();
                    }
                }
            });
        }
    }

    /**
     * {@inheritDoc}
     */
    public void registerProjectUsageForProduct(final String feature, final String projectId,
            final Map<String, String> featureData) {
        if (feature != null) {

            // Before we trigger eventually expensive background reporting, check if this
            // project hasn't recently been reported; if so just skip
            final RegistrationAttempt attempt = new ProjectUsageRegistrationAttempt(feature, projectId,
                    featureData);
            if (shouldSkipRegistrationAttempt(attempt)) {
                return;
            }

            executorService.execute(new Runnable() {

                public void run() {
                    try {
                        w.lock();

                        for (ProductDescriptor productDescriptor : productDescriptors) {
                            if (productDescriptor.registerProjectUsage(feature, projectId, featureData)) {
                                registrationAttempts.add(attempt);
                                return;
                            }
                        }
                    } catch (IllegalArgumentException e) {
                        // Ignore as it may sporadically come up from the preferences API
                    } finally {
                        w.unlock();
                    }
                }
            });
        }
    }

    /**
     * {@inheritDoc}
     */
    public void setPrivacyLevel(int level) {
        try {
            w.lock();
            service.setPrivacyLevel(PrivacyLevel.valueOf(level));
        } finally {
            w.unlock();
        }
    }

    /**
     * Starts up this {@link UaaManager} instance.
     */
    public void start() {
        // Since we run in an restricted environment we need to obtain the builder factory from the OSGi service
        // registry instead of trying to create a new one from the API
        DocumentBuilderFactory documentBuilderFactory = UaaUtils.getDocumentBuilderFactory();
        documentBuilderFactory.setExpandEntityReferences(false);
        XmlUtils.setDocumentBuilderFactory(documentBuilderFactory);

        try {
            initProductDescriptions(getDefaultDetectedProducts(), getDefaultDetectedProductsSignaturer());
        } catch (IOException e) {
        }

        // Add Transmission listener so that we can download the list of detected products periodically.
        service.addTransmissionEventListener(new TransmissionEventListener() {

            public void afterTransmission(TransmissionType type, boolean success) {
                if (type == TransmissionType.DOWNLOAD && success) {
                    InputStream configuration = null;
                    InputStream configurationSignature = null;
                    try {
                        configuration = service.getTransmissionService().download(UAA_URL);
                        configurationSignature = service.getTransmissionService().download(SIGNATURE_URL);
                        initProductDescriptions(configuration, configurationSignature);
                    } catch (IOException e) {
                    } finally {

                        // Safely close the streams
                        if (configuration != null) {
                            try {
                                configuration.close();
                            } catch (IOException e) {
                            }
                        }
                        if (configurationSignature != null) {
                            try {
                                configurationSignature.close();
                            } catch (IOException e) {
                            }
                        }
                    }
                }
            }

            public void beforeTransmission(TransmissionType type) {
            }
        });

        // After starting up and reporting the initial state we should send the data
        Job transmissionJob = new Job("Initializing Spring UAA") {

            @Override
            protected IStatus run(IProgressMonitor monitor) {
                if (service instanceof TransmissionAwareUaaService) {
                    ((TransmissionAwareUaaService) service).requestTransmission();
                }
                return Status.OK_STATUS;
            }
        };
        transmissionJob.setSystem(true);
        transmissionJob.setPriority(Job.DECORATE);
        // Schedule this for 10 minutes into the running instance
        transmissionJob.schedule(10L * 60L * 1000L);
    }

    /**
     * Stops this {@link UaaManager} instance. Shuts down all internal resources like thread pools etc.
     */
    public void stop() {
        try {
            w.lock();
            executorService.shutdown();
            service.stop();
        } finally {
            w.unlock();
        }
    }

    /**
     * Returns the default products that is being shipped with Spring IDE. 
     */
    private InputStream getDefaultDetectedProducts() throws IOException {
        return UaaManager.class
                .getResourceAsStream("/org/springframework/ide/eclipse/internal/uaa/uaa-eclipse.xml");
    }

    /**
     * Returns the signature of default products file that is being shipped with Spring IDE. 
     */
    private InputStream getDefaultDetectedProductsSignaturer() throws IOException {
        return UaaManager.class
                .getResourceAsStream("/org/springframework/ide/eclipse/internal/uaa/uaa-eclipse.xml.asc");
    }

    /**
     * Returns the id of the feature that owns the given <code>plugin</code>.  
     */
    private String getOwningEclipseFeature(String plugin) {
        IBundleGroupProvider[] providers = Platform.getBundleGroupProviders();
        for (IBundleGroupProvider provider : providers) {
            for (IBundleGroup group : provider.getBundleGroups()) {
                if (group.getIdentifier().startsWith("org.eclipse")) {
                    for (Bundle bundle : group.getBundles()) {
                        if (plugin.equals(bundle.getSymbolicName())) {
                            return group.getIdentifier();
                        }
                    }
                }
            }
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    private byte[] mergeData(byte[] existingData, byte[] data) {
        // Quick sanity check to prevent doing too much in case no new data has been presented
        if (data == null || data.length == 0) {
            return existingData;
        }

        // Load existing feature data
        JSONObject existingFeatureData = new JSONObject();
        if (existingData != null && existingData.length > 0) {
            Object existingJson = JSONValue.parse(new String(existingData));
            if (existingJson instanceof JSONObject) {
                existingFeatureData.putAll(((JSONObject) existingJson));
            }
        }

        // Load new data into JSON object
        Map<String, String> featureData = new JSONObject();
        if (data != null && data.length > 0) {
            Object json = JSONValue.parse(new String(data));
            if (json instanceof JSONObject) {
                featureData.putAll((JSONObject) json);
            }
        }

        // Merge feature data: merge those values whose keys already exist
        featureData = new HashMap<String, String>(featureData);
        for (Map.Entry<String, Object> existingEntry : new HashMap<String, Object>(existingFeatureData)
                .entrySet()) {
            if (featureData.containsKey(existingEntry.getKey())) {
                String newValue = featureData.get(existingEntry.getKey());
                Object existingValue = existingEntry.getValue();
                if (!newValue.equals(existingValue)) {
                    if (existingValue instanceof List) {
                        List<String> existingValues = (List<String>) existingValue;
                        if (!existingValues.contains(newValue)) {
                            existingValues.add(newValue);
                        }
                    } else {
                        List<String> value = new ArrayList<String>();
                        value.add((String) existingValue);
                        value.add(featureData.get(existingEntry.getKey()));
                        existingFeatureData.put(existingEntry.getKey(), value);
                    }
                }
                featureData.remove(existingEntry.getKey());
            }
        }

        // Merge the remaining new values
        existingFeatureData.putAll(featureData);

        try {
            return existingFeatureData.toJSONString().getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }

    /**
     * Initializes the {@link ProductDescriptor}s based on the content of <code>inputStream</code>.
     * A local copy stored in Preferences API is checked and only overriden if the given <code>inputStream</code>
     * represents a newer version. 
     */
    private boolean initProductDescriptions(InputStream inputStream, InputStream signatureStream) {
        try {

            if (inputStream == null || signatureStream == null)
                return false;

            // Convert the incoming input stream into a byte[] to start with (simplifies subsequent storage etc)
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            StreamUtils.copy(inputStream, baos);
            byte[] incoming = baos.toByteArray();

            baos = new ByteArrayOutputStream();
            StreamUtils.copy(signatureStream, baos);
            byte[] incomingSignature = baos.toByteArray();

            if (incoming.length == 0 || incomingSignature.length == 0)
                return false;

            // Retrieve the existing document from the Preference API (if any) so we can consider version upgrade issues
            byte[] existing = Base64.decode(P.get(DETECTED_PRODUCTS_KEY, EMPTY_STRING));
            byte[] existingSignature = Base64.decode(P.get(DETECTED_PRODUCTS_SIGNATURE_KEY, EMPTY_STRING));

            // Check signatures of both to see if need to discard a stream
            if (!PgpUtils.validateConfigurationSignature(new ByteArrayInputStream(incoming),
                    new ByteArrayInputStream(incomingSignature))) {
                incoming = EMPTY;
                incomingSignature = EMPTY;
            }
            if (!PgpUtils.validateConfigurationSignature(new ByteArrayInputStream(existing),
                    new ByteArrayInputStream(existingSignature))) {
                existing = EMPTY;
                existingSignature = EMPTY;
            }

            if (existing != EMPTY && existing.length > 0 && existingSignature != EMPTY
                    && existingSignature.length > 0) {
                // We need to consider version differences
                int existingVersion = getVersion(existing);
                int incomingVersion = getVersion(incoming);
                if (existingVersion >= incomingVersion) {
                    // We're not going to do any replacement as the new version is no better than our current version
                    // Instead we'll treat our existing version as the newest version and continue running but only if the signature is valid.
                    incoming = existing;
                    incomingSignature = existingSignature;
                }
            }

            if (incoming == EMPTY && incoming.length == 0) {
                return false;
            }

            // Parse the incoming bytes into an XML document
            Document d = XmlUtils.parse(new ByteArrayInputStream(incoming));
            Element docElement = d.getDocumentElement();

            // Build products list
            List<ProductDescriptor> newProductDescriptors = new ArrayList<UaaManager.ProductDescriptor>();
            NodeList nodeList = docElement.getElementsByTagName("product");
            for (int i = 0; i < nodeList.getLength(); i++) {
                newProductDescriptors.add(new ExtensionProductDescriptor((Element) nodeList.item(i)));
            }
            newProductDescriptors.add(new ProductDescriptor());

            // Override the global list
            productDescriptors.clear();
            productDescriptors.addAll(newProductDescriptors);

            // Replace the existing cached version if required
            if (incoming != existing) {
                // We need to do a replacement, as we didn't simply parse the existing one
                String incomingBase64 = Base64.encodeBytes(incoming, Base64.GZIP);
                String incomingSignatureBase64 = Base64.encodeBytes(incomingSignature, Base64.GZIP);
                P.put(DETECTED_PRODUCTS_KEY, incomingBase64);
                P.put(DETECTED_PRODUCTS_SIGNATURE_KEY, incomingSignatureBase64);
                P.flush();
                return true;
            }

            return false;
        } catch (Throwable e) {
        }
        return false;
    }

    /**
     * Checks if a given {@link RegistrationAttempt} should be allowed.
     * Returns <code>true</code> if no similar {@link RegistrationAttempt} has already been queued. 
     */
    private boolean shouldSkipRegistrationAttempt(RegistrationAttempt attempt) {
        int ix = registrationAttempts.indexOf(attempt);
        return ix >= 0 && !registrationAttempts.get(ix).shouldRegisterAgain();
    }

    /**
     * Looks up the version attribute from the incoming XML document's <product version='someInteger'>.
     * @param document to parse (required)
     * @return the version number (or an exception of something went wrong, eg invalid document etc)
     */
    private static int getVersion(byte[] document) {
        try {
            Document d = XmlUtils.parse(new ByteArrayInputStream(document));
            Element docElement = d.getDocumentElement();
            String ver = docElement.getAttribute("version");
            return new Integer(ver);
        } catch (Exception e) {
            return -1;
        }
    }

    /**
     * Extension to {@link BasicProxyService} that hooks in the Eclipse {@link IProxyService}.
     * @since 2.6.0
     */
    private class EclipseProxyService extends BasicProxyService {

        /**
         * {@inheritDoc}
         */
        @Override
        public Proxy setupProxy(URL url) {
            IProxyData proxy = getProxy(url);
            if (proxy != null && proxy.getHost() != null && proxy.getPort() >= 0 && proxy.getPort() <= 65535) {
                try {
                    return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxy.getHost(), proxy.getPort()));
                } catch (IllegalArgumentException e) {
                }
            }
            return super.setupProxy(url);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Authenticator setupProxyAuthentication(URL url, Proxy proxy) {
            final IProxyData selectedProxy = getProxy(url);
            if (selectedProxy != null
                    && (selectedProxy.getUserId() != null || selectedProxy.getPassword() != null)) {
                return new Authenticator() {
                    @Override
                    protected PasswordAuthentication getPasswordAuthentication() {
                        return new PasswordAuthentication(selectedProxy.getUserId(),
                                (selectedProxy.getPassword() != null ? selectedProxy.getPassword().toCharArray()
                                        : null));
                    }
                };
            }
            return super.setupProxyAuthentication(url, proxy);
        }

        /**
         * Resolves a proxy from the {@link IProxyService} for the given <code>url</code>. 
         */
        private IProxyData getProxy(URL url) {
            IProxyService proxyService = (UaaPlugin.getDefault() != null ? UaaPlugin.getDefault().getProxyService()
                    : null);
            if (url != null && service != null & proxyService != null && proxyService.isProxiesEnabled()) {
                try {
                    URI uri = url.toURI();
                    IProxyData[] proxies = proxyService.select(uri);
                    return selectProxy(uri.getScheme(), proxies);
                } catch (URISyntaxException e) {
                    // ignore this
                }
            }
            return null;
        }

        /**
         * Select a proxy from the list of available proxies. 
         */
        private IProxyData selectProxy(String protocol, IProxyData[] proxies) {
            if (proxies == null || proxies.length == 0)
                return null;
            // If only one proxy is available, then use that
            if (proxies.length == 1) {
                return proxies[0];
            }
            // If more than one proxy is available, then if http/https protocol then look for that one...
            // if not found then use first
            if (protocol.equalsIgnoreCase("http")) {
                for (int i = 0; i < proxies.length; i++) {
                    if (proxies[i].getType().equals(IProxyData.HTTP_PROXY_TYPE))
                        return proxies[i];
                }
            } else if (protocol.equalsIgnoreCase("https")) {
                for (int i = 0; i < proxies.length; i++) {
                    if (proxies[i].getType().equals(IProxyData.HTTPS_PROXY_TYPE))
                        return proxies[i];
                }
            }
            // If we haven't found it yet, then return the first one.
            return proxies[0];
        }
    }

    private class ExtensionProductDescriptor extends ProductDescriptor {

        private Map<String, String> pluginsToFeatureMapping;

        private String productId;

        private String rootPlugin;

        private String sourceControlIdentifier;

        public ExtensionProductDescriptor(Element element) {
            init(element);
        }

        private void init(Element element) {
            productId = element.getAttribute("id");
            if (element.getAttribute("source-control-identifier") != null
                    && element.getAttribute("source-control-identifier").length() > 0) {
                sourceControlIdentifier = element.getAttribute("source-control-identifier");
            }
            if (element.getAttribute("root-plugin") != null && element.getAttribute("root-plugin").length() > 0) {
                rootPlugin = element.getAttribute("root-plugin");
            }

            pluginsToFeatureMapping = new HashMap<String, String>();
            NodeList features = element.getElementsByTagName("feature");
            for (int i = 0; i < features.getLength(); i++) {
                Element featureElement = (Element) features.item(i);
                String feature = featureElement.getAttribute("id");
                if (feature != null) {
                    NodeList plugins = featureElement.getElementsByTagName("plugin");
                    for (int j = 0; j < plugins.getLength(); j++) {
                        Element pluginElement = (Element) plugins.item(j);
                        String pluginId = pluginElement.getAttribute("id");
                        if (pluginId != null) {
                            // Verify that the plugin does not belong to another eclipse feature. This is 
                            // required to associate plugins patched through feature patches to the correct
                            // root feature (e.g. Groovy Eclipse patching JDT core)
                            String owningFeature = getOwningEclipseFeature(pluginId);
                            if (owningFeature == null || feature.equals(owningFeature)) {
                                pluginsToFeatureMapping.put(pluginElement.getAttribute("id"), feature);
                            }
                        }
                    }
                }
            }

            // Try to create the product; we'll try again later if this one fails
            buildProduct(rootPlugin, productId, sourceControlIdentifier);
        }

        protected boolean canRegister(String usedPlugin) {
            return pluginsToFeatureMapping.containsKey(usedPlugin);
        }

        protected void registerProductIfRequired(String project) {
            // If we initially failed to create the product it was probably because it wasn't installed
            // when the workbench started; but now it is so try again
            if (product == null) {
                buildProduct(rootPlugin, productId, sourceControlIdentifier);
            }

            // Check if the product is already registered; if not register it before we capture feature usage
            if (!registered) {
                if (project != null) {
                    service.registerProductUsage(product, project);
                } else {
                    service.registerProductUsage(product);
                }
                registered = true;
            }
        }

    }

    private static class FeatureUseRegistrationAttempt extends RegistrationAttempt {

        private final Map<String, String> featureData;
        private final String plugin;

        public FeatureUseRegistrationAttempt(String plugin, Map<String, String> featureData) {
            this.plugin = plugin;
            this.featureData = featureData;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (!(obj instanceof FeatureUseRegistrationAttempt)) {
                return false;
            }
            FeatureUseRegistrationAttempt other = (FeatureUseRegistrationAttempt) obj;
            if (featureData == null) {
                if (other.featureData != null) {
                    return false;
                }
            } else if (!featureData.equals(other.featureData)) {
                return false;
            }
            if (plugin == null) {
                if (other.plugin != null) {
                    return false;
                }
            } else if (!plugin.equals(other.plugin)) {
                return false;
            }
            return true;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((featureData == null) ? 0 : featureData.hashCode());
            result = prime * result + ((plugin == null) ? 0 : plugin.hashCode());
            return result;
        }

    }

    private class ProductDescriptor {

        protected Product product;

        protected boolean registered = false;

        public ProductDescriptor() {
            init();
        }

        public final boolean registerFeatureUseIfMatch(String usedPlugin, Map<String, String> featureData) {
            if (canRegister(usedPlugin)) {

                registerProductIfRequired(null);
                registerFeature(usedPlugin, featureData);

                return true;
            }
            return false;
        }

        public final boolean registerProjectUsage(String usedPlugin, String project,
                Map<String, String> featureData) {
            if (canRegister(usedPlugin) && project != null) {

                registerProductIfRequired(project);
                registerFeature(usedPlugin, featureData);
                service.registerProductUsage(product, project);

                return true;
            }
            return false;
        }

        private void init() {
            buildProduct(Platform.PI_RUNTIME, "Eclipse", null);
        }

        protected void buildProduct(String symbolicName, String name, String sourceControlIdentifier) {
            Bundle bundle = Platform.getBundle(symbolicName);
            if (bundle != null) {
                Version version = bundle.getVersion();

                Product.Builder b = Product.newBuilder();
                b.setName(name);
                b.setMajorVersion(version.getMajor());
                b.setMinorVersion(version.getMinor());
                b.setPatchVersion(version.getMicro());
                b.setReleaseQualifier(version.getQualifier());

                if (sourceControlIdentifier != null && sourceControlIdentifier.length() > 0) {
                    b.setSourceControlIdentifier(sourceControlIdentifier);
                } else {
                    String sourceControlId = (String) bundle.getHeaders().get("Source-Control-Identifier");
                    if (sourceControlId != null && sourceControlId.length() > 0) {
                        b.setSourceControlIdentifier(sourceControlId);
                    }
                    sourceControlId = (String) bundle.getHeaders().get("Git-Commit-Hash");
                    if (sourceControlId != null && sourceControlId.length() > 0) {
                        b.setSourceControlIdentifier(sourceControlId);
                    }
                }

                product = b.build();
            }
        }

        protected boolean canRegister(String usedPlugin) {
            // Due to privacy considerations only org.eclipse plugins and features will get recorded
            return usedPlugin.startsWith("org.eclipse");
        }

        protected void registerFeature(String usedPlugin, Map<String, String> featureData) {
            // Initialize new map in case null was supplied
            if (featureData == null) {
                featureData = Collections.emptyMap();
            }

            // Get the feature version from the plugin
            String featureVersion = null;
            Bundle bundle = Platform.getBundle(usedPlugin);
            if (bundle != null) {
                String version = (String) bundle.getHeaders().get(Constants.BUNDLE_VERSION);
                if (version != null) {
                    featureVersion = version.toString();
                }
            }

            // Obtain the FeatureUse record
            FeatureUse feature = VersionHelper.getFeatureUse(usedPlugin, featureVersion);

            try {
                // Get existing feature data to merge
                byte[] existingData = service.getFeatureUseData(product, feature);
                byte[] newData = JSONObject.toJSONString(featureData).getBytes("UTF-8");

                // If additional feature data was supplied or is already registered pass it to UAA
                service.registerFeatureUsage(product, feature, mergeData(existingData, newData));
            } catch (UnsupportedEncodingException e) {
                // Cannot happen 
            }
        }

        protected void registerProductIfRequired(String project) {
            if (!registered) {

                // Populate a map of product data with details of the hosting Eclipse runtime
                Map<String, String> productData = new HashMap<String, String>();
                productData.put("platform",
                        String.format("%s.%s.%s", Platform.getOS(), Platform.getWS(), Platform.getOSArch()));

                if (System.getProperty("eclipse.buildId") != null) {
                    productData.put("buildId", System.getProperty("eclipse.buildId"));
                }
                if (System.getProperty("eclipse.product") != null) {
                    productData.put("product", System.getProperty("eclipse.product"));
                }
                if (System.getProperty("eclipse.application") != null) {
                    productData.put("application", System.getProperty("eclipse.application"));
                }

                try {
                    if (project != null) {
                        service.registerProductUsage(product,
                                JSONObject.toJSONString(productData).getBytes("UTF-8"), project);
                    } else {
                        service.registerProductUsage(product,
                                JSONObject.toJSONString(productData).getBytes("UTF-8"));
                    }
                } catch (UnsupportedEncodingException e) {
                    // cannot happen 
                }
                registered = true;
            }
        }
    }

    private static class ProductRegistrationAttempt extends RegistrationAttempt {

        private final String productId;
        private final String projectId;
        private final String version;

        public ProductRegistrationAttempt(String productId, String version, String projectId) {
            this.productId = productId;
            this.version = version;
            this.projectId = projectId;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (!(obj instanceof ProductRegistrationAttempt)) {
                return false;
            }
            ProductRegistrationAttempt other = (ProductRegistrationAttempt) obj;
            if (productId == null) {
                if (other.productId != null) {
                    return false;
                }
            } else if (!productId.equals(other.productId)) {
                return false;
            }
            if (projectId == null) {
                if (other.projectId != null) {
                    return false;
                }
            } else if (!projectId.equals(other.projectId)) {
                return false;
            }
            if (version == null) {
                if (other.version != null) {
                    return false;
                }
            } else if (!version.equals(other.version)) {
                return false;
            }
            return true;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((productId == null) ? 0 : productId.hashCode());
            result = prime * result + ((projectId == null) ? 0 : projectId.hashCode());
            result = prime * result + ((version == null) ? 0 : version.hashCode());
            return result;
        }
    }

    private static class ProjectUsageRegistrationAttempt extends RegistrationAttempt {

        private final String feature;
        private final Map<String, String> featureData;
        private final String projectId;

        public ProjectUsageRegistrationAttempt(String feature, String projectId, Map<String, String> featureData) {
            this.feature = feature;
            this.projectId = projectId;
            this.featureData = featureData;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (!(obj instanceof ProjectUsageRegistrationAttempt)) {
                return false;
            }
            ProjectUsageRegistrationAttempt other = (ProjectUsageRegistrationAttempt) obj;
            if (feature == null) {
                if (other.feature != null) {
                    return false;
                }
            } else if (!feature.equals(other.feature)) {
                return false;
            }
            if (featureData == null) {
                if (other.featureData != null) {
                    return false;
                }
            } else if (!featureData.equals(other.featureData)) {
                return false;
            }
            if (projectId == null) {
                if (other.projectId != null) {
                    return false;
                }
            } else if (!projectId.equals(other.projectId)) {
                return false;
            }
            return true;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((feature == null) ? 0 : feature.hashCode());
            result = prime * result + ((featureData == null) ? 0 : featureData.hashCode());
            result = prime * result + ((projectId == null) ? 0 : projectId.hashCode());
            return result;
        }
    }

    private static abstract class RegistrationAttempt {

        private final long registrationAttemptTime = new Date().getTime();

        public boolean shouldRegisterAgain() {
            return System.currentTimeMillis() > registrationAttemptTime + MIN_REPORTING_INTERVAL;
        }
    }

}