Bug 436872 - [category] Specify download stats URL and type of artifacts
to monitor in category.xml 

Adds an argument to MirroringApplication to mirror artifact repository
properties. This is needed in order to properly export the p2.statsURI
property generated in the publisher.

Change-Id: I0947d6320f24ef09e67f650b765bbe3908dd382e
Signed-off-by: Pascal Rapicault <pascal@rapicorp.com>
diff --git a/bundles/org.eclipse.equinox.p2.repository.tools/src/org/eclipse/equinox/p2/internal/repository/mirroring/Mirroring.java b/bundles/org.eclipse.equinox.p2.repository.tools/src/org/eclipse/equinox/p2/internal/repository/mirroring/Mirroring.java
index 69ca5e9..ab37668 100644
--- a/bundles/org.eclipse.equinox.p2.repository.tools/src/org/eclipse/equinox/p2/internal/repository/mirroring/Mirroring.java
+++ b/bundles/org.eclipse.equinox.p2.repository.tools/src/org/eclipse/equinox/p2/internal/repository/mirroring/Mirroring.java
@@ -14,6 +14,7 @@
 
 import java.util.*;
 import org.eclipse.core.runtime.*;
+import org.eclipse.equinox.internal.p2.artifact.repository.CompositeArtifactRepository;
 import org.eclipse.equinox.internal.p2.artifact.repository.RawMirrorRequest;
 import org.eclipse.equinox.internal.p2.repository.Transport;
 import org.eclipse.equinox.p2.core.ProvisionException;
@@ -46,6 +47,7 @@
 	private IArtifactMirrorLog comparatorLog;
 	private Transport transport;
 	private boolean includePacked = true;
+	private boolean mirrorProperties = false;
 
 	private IArtifactComparator getComparator() {
 		if (comparator == null)
@@ -79,6 +81,10 @@
 		this.validate = validate;
 	}
 
+	public void setMirrorProperties(boolean properties) {
+		this.mirrorProperties = properties;
+	}
+
 	public MultiStatus run(boolean failOnError, boolean verbose) {
 		if (!destination.isModifiable())
 			throw new IllegalStateException(NLS.bind(Messages.exception_destinationNotModifiable, destination.getLocation()));
@@ -111,6 +117,23 @@
 					return multiStatus;
 			}
 		}
+
+		// mirror the source repository's properties unless they are already set up
+		// in the destination repository
+		if (mirrorProperties) {
+			IArtifactRepository toCopyFrom = source;
+			if (toCopyFrom instanceof CompositeArtifactRepository) {
+				List<IArtifactRepository> children = ((CompositeArtifactRepository) toCopyFrom).getLoadedChildren();
+				if (children.size() > 0)
+					toCopyFrom = children.get(0);
+			}
+			Map<String, String> sourceProperties = toCopyFrom.getProperties();
+			for (String key : sourceProperties.keySet()) {
+				if (!destination.getProperties().containsKey(key))
+					destination.setProperty(key, sourceProperties.get(key));
+			}
+		}
+
 		if (validate) {
 			// Simple validation of the mirror
 			IStatus validation = validateMirror(verbose);
diff --git a/bundles/org.eclipse.equinox.p2.repository.tools/src/org/eclipse/equinox/p2/internal/repository/tools/MirrorApplication.java b/bundles/org.eclipse.equinox.p2.repository.tools/src/org/eclipse/equinox/p2/internal/repository/tools/MirrorApplication.java
index 30dfeb0..80bf3bf 100644
--- a/bundles/org.eclipse.equinox.p2.repository.tools/src/org/eclipse/equinox/p2/internal/repository/tools/MirrorApplication.java
+++ b/bundles/org.eclipse.equinox.p2.repository.tools/src/org/eclipse/equinox/p2/internal/repository/tools/MirrorApplication.java
@@ -51,6 +51,7 @@
 	private String metadataOrArtifacts = null;
 	private String[] rootIUs = null;
 	private boolean includePacked = true;
+	private boolean mirrorProperties = false;
 
 	private File mirrorLogFile; // file to log mirror output to (optional)
 	private File comparatorLogFile; // file to comparator output to (optional)
@@ -130,6 +131,8 @@
 				validate = true;
 			else if (args[i].equalsIgnoreCase("-references")) //$NON-NLS-1$
 				mirrorReferences = true;
+			else if (args[i].equalsIgnoreCase("-properties")) //$NON-NLS-1$
+				mirrorProperties = true;
 
 			// check for args with parameters. If we are at the last argument or 
 			// if the next one has a '-' as the first character, then we can't have 
@@ -233,6 +236,7 @@
 		mirror.setCompareExclusions(compareExclusions);
 		mirror.setTransport((Transport) agent.getService(Transport.SERVICE_NAME));
 		mirror.setIncludePacked(includePacked);
+		mirror.setMirrorProperties(mirrorProperties);
 
 		// If IUs have been specified then only they should be mirrored, otherwise mirror everything.
 		if (iusSpecified)
@@ -464,4 +468,8 @@
 	public void setIncludePacked(boolean includePacked) {
 		this.includePacked = includePacked;
 	}
+
+	public void setMirrorProperties(boolean mirrorProperties) {
+		this.mirrorProperties = mirrorProperties;
+	}
 }
diff --git a/bundles/org.eclipse.equinox.p2.repository.tools/src_ant/org/eclipse/equinox/p2/internal/repository/tools/tasks/MirrorTask.java b/bundles/org.eclipse.equinox.p2.repository.tools/src_ant/org/eclipse/equinox/p2/internal/repository/tools/tasks/MirrorTask.java
index f9d5a94..002d53e 100644
--- a/bundles/org.eclipse.equinox.p2.repository.tools/src_ant/org/eclipse/equinox/p2/internal/repository/tools/tasks/MirrorTask.java
+++ b/bundles/org.eclipse.equinox.p2.repository.tools/src_ant/org/eclipse/equinox/p2/internal/repository/tools/tasks/MirrorTask.java
@@ -141,4 +141,8 @@
 	public void setReferences(boolean value) {
 		((MirrorApplication) application).setReferences(value);
 	}
+
+	public void setMirrorProperties(boolean value) {
+		((MirrorApplication) application).setMirrorProperties(value);
+	}
 }
diff --git a/bundles/org.eclipse.equinox.p2.updatesite/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.p2.updatesite/META-INF/MANIFEST.MF
index a07ca0c..6aea12f 100644
--- a/bundles/org.eclipse.equinox.p2.updatesite/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.p2.updatesite/META-INF/MANIFEST.MF
@@ -18,6 +18,7 @@
  org.eclipse.equinox.app;version="1.1.0",
  org.eclipse.equinox.internal.p2.artifact.repository.simple,
  org.eclipse.equinox.internal.p2.core.helpers,
+ org.eclipse.equinox.internal.p2.metadata,
  org.eclipse.equinox.internal.p2.publisher.eclipse,
  org.eclipse.equinox.internal.p2.repository,
  org.eclipse.equinox.p2.core;version="[2.0.0,3.0.0)",
diff --git a/bundles/org.eclipse.equinox.p2.updatesite/src/org/eclipse/equinox/internal/p2/updatesite/CategoryParser.java b/bundles/org.eclipse.equinox.p2.updatesite/src/org/eclipse/equinox/internal/p2/updatesite/CategoryParser.java
index ece98b7..e8a94ec 100644
--- a/bundles/org.eclipse.equinox.p2.updatesite/src/org/eclipse/equinox/internal/p2/updatesite/CategoryParser.java
+++ b/bundles/org.eclipse.equinox.p2.updatesite/src/org/eclipse/equinox/internal/p2/updatesite/CategoryParser.java
@@ -47,6 +47,7 @@
 	private static final String EXPRESSION = "expression"; //$NON-NLS-1$
 	private static final String PARAM = "param"; //$NON-NLS-1$
 	private static final String REPOSITORY_REF = "repository-reference"; //$NON-NLS-1$
+	private static final String STATS_URI = "stats"; //$NON-NLS-1$
 
 	private static final int STATE_ARCHIVE = 3;
 	private static final int STATE_CATEGORY = 4;
@@ -63,6 +64,7 @@
 	private static final int STATE_QUERY = 11;
 	private static final int STATE_SITE = 1;
 	private static final int STATE_REPOSITORY_REF = 13;
+	private static final int STATE_STATS = 14;
 
 	private boolean DESCRIPTION_SITE_ALREADY_SEEN = false;
 	// Current object stack (used to hold the current object we are
@@ -245,6 +247,11 @@
 				// do not pop object as we did not push the reference
 				break;
 
+			case STATE_STATS :
+				stateStack.pop();
+				// do not pop object stack because we didn't push anything
+				break;
+
 			case STATE_DESCRIPTION_SITE :
 				stateStack.pop();
 				text = ""; //$NON-NLS-1$
@@ -392,6 +399,9 @@
 			case STATE_REPOSITORY_REF :
 				return "Repository Reference"; //$NON-NLS-1$
 
+			case STATE_STATS :
+				return "Stats Repository"; //$NON-NLS-1$
+
 			default :
 				return Messages.DefaultSiteParser_UnknownState;
 		}
@@ -472,6 +482,20 @@
 		} else if (elementName.equals(REPOSITORY_REF)) {
 			stateStack.push(new Integer(STATE_REPOSITORY_REF));
 			processRepositoryReference(attributes);
+		} else if (elementName.equals(STATS_URI)) {
+			stateStack.push(new Integer(STATE_STATS));
+			processStatsInfo(attributes);
+		} else
+			internalErrorUnknownTag(NLS.bind(Messages.DefaultSiteParser_UnknownElement, (new String[] {elementName, getState(currentState())})));
+	}
+
+	private void handleStatsState(String elementName, Attributes attributes) {
+		if (elementName.equals(FEATURE)) {
+			stateStack.push(STATE_FEATURE);
+			processStatsFeature(attributes);
+		} else if (elementName.equals(BUNDLE)) {
+			stateStack.push(STATE_BUNDLE);
+			processStatsBundle(attributes);
 		} else
 			internalErrorUnknownTag(NLS.bind(Messages.DefaultSiteParser_UnknownElement, (new String[] {elementName, getState(currentState())})));
 	}
@@ -648,12 +672,83 @@
 		} catch (URISyntaxException e) {
 			// UI should have already caught this
 		}
+	}
+
+	/*
+	 * process stats top level element
+	 */
+	private void processStatsInfo(Attributes attributes) {
+		String location = attributes.getValue("location"); //$NON-NLS-1$
+		try {
+			// One final validation but UI should have already done this.
+			URIUtil.fromString(location);
+			SiteModel site = (SiteModel) objectStack.peek();
+			site.setStatsURIString(location);
+		} catch (URISyntaxException e) {
+			// Ignore if not valid.
+		}
 
 		if (Tracing.DEBUG_GENERATOR_PARSING)
 			debug("End processing Repository Reference: location:" + location); //$NON-NLS-1$
 	}
 
 	/*
+	 * process stats feature artifact
+	 */
+	private void processStatsFeature(Attributes attributes) {
+		SiteFeature feature = new SiteFeature();
+
+		// identifier and version
+		String id = attributes.getValue("id"); //$NON-NLS-1$
+		String ver = attributes.getValue("version"); //$NON-NLS-1$
+
+		boolean noId = (id == null || id.trim().equals("")); //$NON-NLS-1$
+
+		// We need to have id and version, or the url, or both.
+		if (noId)
+			internalError(NLS.bind(Messages.DefaultSiteParser_Missing, (new String[] {"url", getState(currentState())}))); //$NON-NLS-1$
+
+		feature.setFeatureIdentifier(id);
+		feature.setFeatureVersion(ver);
+
+		SiteModel site = (SiteModel) objectStack.peek();
+		site.addStatsFeature(feature);
+		objectStack.push(feature);
+		feature.setSiteModel(site);
+
+		if (Tracing.DEBUG_GENERATOR_PARSING)
+			debug("End Processing Stats Feature Tag: id:" + id + " version:" + ver); //$NON-NLS-1$ //$NON-NLS-2$	}
+	}
+
+	/*
+	 * process stats bundle artifact info
+	 */
+	private void processStatsBundle(Attributes attributes) {
+		SiteBundle bundle = new SiteBundle();
+
+		// identifier and version
+		String id = attributes.getValue("id"); //$NON-NLS-1$
+		String ver = attributes.getValue("version"); //$NON-NLS-1$
+
+		boolean noId = (id == null || id.trim().equals("")); //$NON-NLS-1$
+
+		// We need to have id and version, or the url, or both.
+		if (noId)
+			internalError(NLS.bind(Messages.DefaultSiteParser_Missing, (new String[] {"url", getState(currentState())}))); //$NON-NLS-1$
+
+		bundle.setBundleIdentifier(id);
+		bundle.setBundleVersion(ver);
+
+		SiteModel site = (SiteModel) objectStack.peek();
+		site.addStatsBundle(bundle);
+		objectStack.push(bundle);
+		bundle.setSiteModel(site);
+
+		if (Tracing.DEBUG_GENERATOR_PARSING)
+			debug("End Processing Stats Bundle Tag: id:" + id + " version:" + ver); //$NON-NLS-1$ //$NON-NLS-2$
+	}
+
+	/*
 	 * process feature info
 	 */
 	private void processFeature(Attributes attributes) {
@@ -857,6 +952,10 @@
 				handleSiteState(localName, attributes);
 				break;
 
+			case STATE_STATS :
+				handleStatsState(localName, attributes);
+				break;
+
 			default :
 				internalErrorUnknownTag(NLS.bind(Messages.DefaultSiteParser_UnknownStartState, (new String[] {getState(currentState())})));
 				break;
diff --git a/bundles/org.eclipse.equinox.p2.updatesite/src/org/eclipse/equinox/internal/p2/updatesite/SiteModel.java b/bundles/org.eclipse.equinox.p2.updatesite/src/org/eclipse/equinox/internal/p2/updatesite/SiteModel.java
index d2c1c2c..0876596 100644
--- a/bundles/org.eclipse.equinox.p2.updatesite/src/org/eclipse/equinox/internal/p2/updatesite/SiteModel.java
+++ b/bundles/org.eclipse.equinox.p2.updatesite/src/org/eclipse/equinox/internal/p2/updatesite/SiteModel.java
@@ -38,6 +38,7 @@
 	private URI locationURI;
 	private String locationURIString;
 	private String mirrorsURIString;
+	private String statsURIString;
 	private boolean supportsPack200;
 	private String type;
 	private URLEntry[] associateSites;
@@ -45,6 +46,8 @@
 	private List<String> messageKeys;
 	private Map<Locale, Map<String, String>> localizations;
 	private List<RepositoryReference> repositoryReferences;
+	private List<SiteFeature> statsFeatures;
+	private List<SiteBundle> statsBundles;
 
 	/**
 	 * Creates an uninitialized site model object.
@@ -107,6 +110,28 @@
 	}
 
 	/**
+	 * Adds a feature reference model to site stats artifacts.
+	 * 
+	 * @param featureReference feature reference model
+	 */
+	public void addStatsFeature(SiteFeature featureReference) {
+		if (this.statsFeatures == null)
+			this.statsFeatures = new ArrayList<SiteFeature>();
+		this.statsFeatures.add(featureReference);
+	}
+
+	/**
+	 * Adds a bundle reference model to site stats artifacts
+	 * 
+	 * @param bundleReference bundle reference model
+	 */
+	public void addStatsBundle(SiteBundle bundleReference) {
+		if (this.statsBundles == null)
+			this.statsBundles = new ArrayList<SiteBundle>();
+		this.statsBundles.add(bundleReference);
+	}
+
+	/**
 	 * Adds a iu model to site.
 	 * 
 	 * @param iu iu model
@@ -199,6 +224,28 @@
 	}
 
 	/**
+	 * Returns an array of feature reference models for stats on this site.
+	 * 
+	 * @return an array of feature reference models, or an empty array.
+	 */
+	public SiteFeature[] getStatsFeatures() {
+		if (statsFeatures == null || statsFeatures.size() == 0)
+			return new SiteFeature[0];
+		return statsFeatures.toArray(new SiteFeature[0]);
+	}
+
+	/**
+	 * Returns an array of bundle reference models for stats on this site.
+	 * 
+	 * @return an array of bundle reference models, or an empty array.
+	 */
+	public SiteBundle[] getStatsBundles() {
+		if (statsBundles == null || statsBundles.size() == 0)
+			return new SiteBundle[0];
+		return statsBundles.toArray(new SiteBundle[0]);
+	}
+
+	/**
 	 * Returns an array of IU models on this site.
 	 * 
 	 * @return an array of IU models, or an empty array.
@@ -221,6 +268,15 @@
 	}
 
 	/**
+	 * Returns the URI of the stats repository that tracks downloads.
+	 * 
+	 * @return a String representation of the stats URI.
+	 */
+	public String getStatsURI() {
+		return statsURIString;
+	}
+
+	/**
 	 * Gets the localizations for the site as a map from locale
 	 * to the set of translated properties for that locale.
 	 * 
@@ -379,4 +435,13 @@
 		return digestURIString;
 	}
 
+	/**
+	 * Sets the URI of the stats repository used to track downloads. 
+	 * 
+	 * @param statsURI a String describing the stats URI
+	 */
+	public void setStatsURIString(String statsURI) {
+		this.statsURIString = statsURI;
+	}
+
 }
diff --git a/bundles/org.eclipse.equinox.p2.updatesite/src/org/eclipse/equinox/internal/p2/updatesite/SiteXMLAction.java b/bundles/org.eclipse.equinox.p2.updatesite/src/org/eclipse/equinox/internal/p2/updatesite/SiteXMLAction.java
index df31e30..9651102 100644
--- a/bundles/org.eclipse.equinox.p2.updatesite/src/org/eclipse/equinox/internal/p2/updatesite/SiteXMLAction.java
+++ b/bundles/org.eclipse.equinox.p2.updatesite/src/org/eclipse/equinox/internal/p2/updatesite/SiteXMLAction.java
@@ -26,10 +26,12 @@
 import org.eclipse.equinox.p2.metadata.expression.ExpressionUtil;
 import org.eclipse.equinox.p2.metadata.expression.IExpression;
 import org.eclipse.equinox.p2.publisher.*;
-import org.eclipse.equinox.p2.publisher.eclipse.URLEntry;
+import org.eclipse.equinox.p2.publisher.eclipse.*;
 import org.eclipse.equinox.p2.query.*;
 import org.eclipse.equinox.p2.repository.IRepository;
 import org.eclipse.equinox.p2.repository.IRepositoryReference;
+import org.eclipse.equinox.p2.repository.artifact.IArtifactDescriptor;
+import org.eclipse.equinox.p2.repository.artifact.spi.ArtifactDescriptor;
 import org.eclipse.equinox.p2.repository.spi.RepositoryReference;
 import org.eclipse.equinox.spi.p2.publisher.LocalizationHelper;
 import org.eclipse.equinox.spi.p2.publisher.PublisherHelper;
@@ -40,6 +42,8 @@
  */
 public class SiteXMLAction extends AbstractPublisherAction {
 	static final private String QUALIFIER = "qualifier"; //$NON-NLS-1$
+	static final private String P_STATS_URI = "p2.statsURI"; //$NON-NLS-1$
+	static final private String P_STATS_MARKER = "download.stats"; //$NON-NLS-1$
 	private static final VersionSuffixGenerator versionSuffixGenerator = new VersionSuffixGenerator();
 	protected UpdateSite updateSite;
 	private SiteCategory defaultCategory;
@@ -99,7 +103,57 @@
 		}
 		initialize();
 		initializeRepoFromSite(publisherInfo);
-		return generateCategories(publisherInfo, results, monitor);
+		IStatus markingStats = markStatsArtifacts(publisherInfo, results, monitor);
+		if (markingStats.isOK()) {
+			return generateCategories(publisherInfo, results, monitor);
+		}
+		return markingStats;
+	}
+
+	private IStatus markStatsArtifacts(IPublisherInfo publisherInfo, IPublisherResult results, IProgressMonitor monitor) {
+		SiteModel site = updateSite.getSite();
+		// process all features listed and mark artifacts
+		SiteFeature[] features = site.getStatsFeatures();
+		if (features != null) {
+			for (SiteFeature feature : features) {
+				if (monitor.isCanceled())
+					return Status.CANCEL_STATUS;
+				Collection<IInstallableUnit> ius = getFeatureIU(feature, publisherInfo, results);
+				if (ius != null) {
+					for (IInstallableUnit iu : ius) {
+						IArtifactKey key = FeaturesAction.createFeatureArtifactKey(feature.getFeatureIdentifier(), iu.getVersion().toString());
+						IArtifactDescriptor[] descriptors = publisherInfo.getArtifactRepository().getArtifactDescriptors(key);
+						if (descriptors.length > 0 && descriptors[0] instanceof ArtifactDescriptor) {
+							HashMap<String, String> map = new HashMap<String, String>();
+							map.put(P_STATS_MARKER, feature.getFeatureIdentifier());
+							((ArtifactDescriptor) descriptors[0]).addProperties(map);
+						}
+					}
+				}
+			}
+		}
+		SiteBundle[] bundles = site.getStatsBundles();
+		if (bundles != null) {
+			for (SiteBundle bundle : bundles) {
+				if (monitor.isCanceled())
+					return Status.CANCEL_STATUS;
+				Collection<IInstallableUnit> ius = getBundleIU(bundle, publisherInfo, results);
+				if (ius != null) {
+					for (IInstallableUnit iu : ius) {
+						IArtifactKey key = BundlesAction.createBundleArtifactKey(iu.getId(), iu.getVersion().toString());
+						IArtifactDescriptor[] descriptors = publisherInfo.getArtifactRepository().getArtifactDescriptors(key);
+						if (descriptors.length > 0 && descriptors[0] instanceof ArtifactDescriptor) {
+							HashMap<String, String> map = new HashMap<String, String>();
+							map.put(P_STATS_MARKER, iu.getId());
+							((ArtifactDescriptor) descriptors[0]).addProperties(map);
+						}
+					}
+				}
+			}
+		}
+		// Process all ius that should be marked for download stat tracking
+		return Status.OK_STATUS;
+
 	}
 
 	private IStatus generateCategories(IPublisherInfo publisherInfo, IPublisherResult results, IProgressMonitor monitor) {
@@ -408,6 +462,13 @@
 			publisherInfo.getMetadataRepository().addReferences(toAdd);
 		}
 
+		// publish download stats URL from category file
+		String statsURI = site.getStatsURI();
+		if (statsURI != null && statsURI.length() > 0) {
+			if (publisherInfo.getArtifactRepository() != null)
+				publisherInfo.getArtifactRepository().setProperty(P_STATS_URI, statsURI);
+		}
+
 		File siteFile = URIUtil.toFile(updateSite.getLocation());
 		if (siteFile != null && siteFile.exists()) {
 			File siteParent = siteFile.getParentFile();