Source
Eclipse Update Siteartifacts.xsl
Introduction
My company wanted to add a plug-in to Eclipse for our internal bug-tracking system. No problem I say and I build that sucker. I used Spring Tool Suite 3.5.1 (which is just Eclipse 4.3 with a fancy paint job) with Maven, and deployed to a Linux Red Hat server, mostly following the vogella tutorials. But now they want to know adoption rates and that will be part of my annual review! Holy shit I better find out how to get some metrics quick! Oh look here is the p2 documentation: Equinox p2 download statsUmm... okay well thanks for nothing really. The idea is to add 2 properties to artifacts.xml, which is packaged inside of artifacts.jar, that tells the update site to perform an HTTP HEAD request (it's like a HTTP GET request but without body content) whenever your plug-in is downloaded with the url you provide. However if you use Eclipse (for your Eclipse plug-in) then we are immediately going to run into some problems. The p2 instructions would like us to modify artifacts.xml, however that file is generated automajically and there are no way to set properties in your project and have it appear in artifacts.xml. Well not yet at least: Eclipse Project 4.5 M1 - New and Noteworthy. Sonofabitch!
So what is suggested is to perform the following steps:
- Unpack artifacts.jar
- Modify artifacts.xml and add the 'p2.statsURI' and 'download.stats' properties.
- Repackage artifacts.jar
Configuring the Eclipse Update Site
- p2.statsURI=www.foo.com/updatesite/stats/
- download.stats=index.jsp
So in your update site project create a file called 'artifacts.xsl'. For my example I created a subfolder called 'xslt' and placed artifacts.xsl there.
artifacts.xsl
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" />
<xsl:param name="p2.statsURI"></xsl:param>
<xsl:param name="download.stats"></xsl:param>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="/repository/properties">
<xsl:element name="properties">
<xsl:attribute name="size">
<xsl:value-of select="@size + 1" />
</xsl:attribute>
<xsl:apply-templates select="node()" />
<xsl:element name="property">
<xsl:attribute name="name">p2.statsURI</xsl:attribute>
<xsl:attribute name="value">
<xsl:value-of select="$p2.statsURI" />
</xsl:attribute>
</xsl:element>
</xsl:element>
</xsl:template>
<xsl:template match="artifact[@classifier='org.eclipse.update.feature']">
<xsl:element name="artifact">
<xsl:attribute name="classifier">
<xsl:value-of select="@classifier"></xsl:value-of>
</xsl:attribute>
<xsl:attribute name="id">
<xsl:value-of select="@id"></xsl:value-of>
</xsl:attribute>
<xsl:attribute name="version">
<xsl:value-of select="@version"></xsl:value-of>
</xsl:attribute>
<xsl:apply-templates select="node()" mode="artifact" />
</xsl:element>
</xsl:template>
<xsl:template match="properties" mode="artifact">
<xsl:element name="properties">
<xsl:attribute name="size">
<xsl:value-of select="@size + 1" />
</xsl:attribute>
<xsl:apply-templates select="node()" mode="artifact" />
<xsl:element name="property">
<xsl:attribute name="name">download.stats</xsl:attribute>
<xsl:attribute name="value">
<xsl:value-of select="$download.stats" />
</xsl:attribute>
</xsl:element>
</xsl:element>
</xsl:template>
<xsl:template match="property" mode="artifact">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Cool. Next we need to modify the update site's pom.xml to add the Maven Ant plugin. This will allow us to hook into a maven life-cycle phase and run some Ant tasks that we specify. OBEY ME ANT! MAVEN IS NO LONGER YOUR MASTER! Right. So in a nutshell we will demand Ant to do the following after the update site has been compiled and packaged into their jars.
- Unpack artifacts.jar in a temporary folder called 'output'. Artifacts.jar contains only 1 file, artifacts.xml.
- Modify artifacts.xml and add the 'p2.statsURI' and 'download.stats' properties using the transform above, artifacts.xsl. In the param nodes set your site's values in the expression attributes.
- For 'p2.statsURI' we will be eventually creating a subfolder in your update site's location called 'stats' that will contain the files used to track downloads. Don't forget the slash at the end.
- For 'download.stats' this will be the page that will process the HTTP HEAD request. For my example I will use JSP but you can use PHP, Perl, or whatever I don't care I'm not your boss.
- Repackage artifacts.jar.
- Remove the temp folder 'output'.
pom.xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<id>prepare</id>
<phase>package</phase>
<configuration>
<tasks>
<echo message="Add download stats to artifacts.jar" />
<unzip src="${project.build.directory}/repository/artifacts.jar" dest="output/" />
<xslt in="output/artifacts.xml" out="output/artifacts2.xml" style="${basedir}/xslt/artifacts.xsl">
<param name="p2.statsURI" expression="http://yoursite/updatesite/stats/"/>
<param name="download.stats" expression="insert.jsp"/>
</xslt>
<move file="output/artifacts2.xml" tofile="output/artifacts.xml" overwrite="true" />
<zip destfile="${project.build.directory}/repository/artifacts.jar" basedir="output" update="true" />
<delete dir="output"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
Processing the HTTP Request
Yay we're done! Not so fast. We're not done. Not at all. Now we have to process that page the update site will call when your plug-in is downloaded. My example will be in JSP with a MySQL database to store the data. Also here is an example in PHP: p2-stats-recorderFirst let's set up the MySQL database to hold the information of the folks who download the plug-in. Create a new database called 'p2_stats'.
CREATE DATABASE p2_stats;
Create a user account named 'p2' with the password 'p2' (or you can be smart and name it something stronger, please be smart!)
CREATE USER 'p2'@'localhost' IDENTIFIED BY 'p2';
Next let's create our lone table.
CREATE TABLE `installations` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`package` VARCHAR(255),
`version` VARCHAR(255),
`os` VARCHAR(255),
`host` VARCHAR(255),
`created_on` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(`id`));
Finally let's give the 'p2' user access to the 'installations' table.
GRANT INSERT, SELECT, UPDATE, DELETE ON `installations` TO 'p2'@'localhost';
That's it. This is identical to schema.sql in the p2-stats-recorder, except I use 'created_on' as the name of my timestamp column, removed the ui column that I personally won't need, and added a primary key column... because... you know... database normalization.
Now on your server in your update site directory create a folder called 'stats'. This is where all of our scripts to track downloads will be.
Next is to create the script that will process the HTTP HEAD request. As I mentioned before I am using JSP which requires the MySQL Java connector to be installed. Just go here Download Connector/J, unzip that guy and place the jar in a your updatesite folder in the location 'WEB-INF\lib', with 'WEB-INF' being a sibling of 'features', 'plugins', and your newly created 'stats' folder. MySQL says to fuck around with the path but fuck that shit as this method is preferred for so many reasons. Also don't be stupid like me and waste hours trying to figure out why a connection to MySQL cannot be established and first make sure that your MySQL server is started.
Create a new file in your 'stats' folder called 'insert.jsp' or whatever value you gave your 'download.stats' xslt parameter. Essentially what we want to do is establish a connection to our 'p2_stats' MySQL database, then insert a new record with data gathered from the HTTP header. Here is the code for that guy.:
insert.jsp
<%@ page import="java.sql.*" %>
<%@ page import="java.io.*" %>
<html>
<head>
<title>My Eclipse Plug-in Download Counter</title>
<body>
<h2>Insert download record</h2>
<%
try {
String connectionURL = "jdbc:mysql://localhost:3306/p2_stats";
Connection connection = null;
PreparedStatement pstatement = null;
Class.forName("com.mysql.jdbc.Driver").newInstance();
connection = DriverManager.getConnection(connectionURL, "p2", "p2");
if(!connection.isClosed()) {
String queryString = "INSERT INTO installations(package, version, os, host) VALUES(?, ?, ?, ?)";
pstatement = connection.prepareStatement(queryString);
pstatement.setString(1, request.getParameter("project.name"));
pstatement.setString(2, request.getParameter("project.version"));
pstatement.setString(3, request.getParameter("os.name"));
pstatement.setString(4, request.getRemoteHost());
int updateQuery = pstatement.executeUpdate();
if (updateQuery != 0) {
out.println("Successfully inserted record.<BR/>");
out.println("project.name: " + request.getParameter("project.name") + "<BR/>");
out.println("project.version: " + request.getParameter("project.version") + "<BR/>");
out.println("os.name: " + request.getParameter("os.name") + "<BR/>");
out.println("host: " + request.getRemoteHost() + "<BR/>");
}
}
connection.close();
}catch(Exception ex){
out.println("Unable to connect to database. " + ex);
}
%>
</body>
</html>
Now we can test the page by opening a browser and using the url:
http://yoursite/updatesite/stats/insert.jsp
You can ensure a record was created by looking at your 'installations' table in the database. You can also check your server logs. For example I am using Tomcat so in the directory '/var/log/tomcat' I can look in the file 'localhost_access_log' and I should see an entry:
"HEAD /eclipse-updatesite/stats/insert.jsp HTTP/1.1" 200 240
Hooray it worked! You now have a new record in your database. Right now some of the columns are blank and I'm sorry I may have deceived you... but look the basics are working and I promise I will fix that in a later "advanced reporting" section.
That Later Advanced Reporting Section
AW FUCK THIS SECTION IS WRONG. COME BACK TOMORROW AND I'LL HAVE IT FIXED.Add url parameters to the download.stats page in the pom.xmlCapture those parameters in our download.stats pageInsert the parameters into the database.
<properties> <project.name.url>${project.name}</project.name.url> <project.version.url>${project.version}</project.version.url> </properties>
<plugin> <groupId>org.codehaus.gmaven</groupId> <artifactId>gmaven-plugin</artifactId> <version>1.5</version> <executions> <execution> <id>setup-groovy</id> <phase>initialize</phase> <goals> <goal>execute</goal> </goals> <configuration> <source> project.properties["project.name.url"] = java.net.URLEncoder.encode(project.properties['project.name.url']) project.properties["project.version.url"] = java.net.URLEncoder.encode(project.properties['project.version.url']) project.properties["os.name.url"] = java.net.URLEncoder.encode(System.properties['os.name']) </source> </configuration> </execution> </executions> </plugin>
<param name="download.stats" expression="insert.jsp" />
<param name="download.stats" expression="insert.jsp?project.name=${project.name.url}&project.version=${project.version.url}&os.name=${os.name.url}" />
I just want to say this took me two days to work through. If this saved you any time I suggest you stop what you are doing and enjoy whatever time I saved you with friends, family, a good book, binge watch that Netflix show, whatever.