Fremde Libs in Java-Projekten
30th of October 2004
Jeder kennt es. Jedes mittlere Java-Projekt benötigt eine Menge von JARs von Dritten (jedes natürlich mit seiner eigenen Lizenz) zur Laufzeit und evtl. eine Menge JARs für den Übersetzungsprozess (wie z.B. XDoclet).
Dummerweise können sich die Bibliotheken, die zur Laufzeit und zur Übersetzungszeit benötigt weden, auch noch überschneiden.
Bei dem Wust an Lizenzen und besonderen Regeln die zu beachten sind, kann man schon mal die Übersicht verlieren. Bei meinem momentanen Projekt sieht es da nicht anders aus.
Um dem Ganzen besser Herr zur werden, habe ich mich entschlossen eine XML-Datei zu pflegen, die einen symbolischen Bibiotheks-Namen (wie z.B. log4j.jar) auf ihren jeweiligen konkreten Bibiotheks-Namen (mit Version; z.B. log4j-1.2.8.jar) abbildet. In einem seperaten Verzeichnis (legal) verwalte ich pro Bibliothek einen Datei mit der zugehörigen Lizenz. Der Name der Lizenz-Datei ergibt sich aus dem konkreten Bibliotheks-Namen mit dem angehängten Kürzel ".LICENSE.txt" oder ".LICENSE.html". In der XML-Datei kann man optional angeben, ob die Lizenz als HTML- oder als Text-Datei vorliegt.
Dadurch kann ich in meinem lib-Verzeichnis beliebig Laufzeit- und Übersetzungs-Bibliotheken mischen. Alles was nicht in der XML-Datei erfasst ist, ist also eine Übersetzungs-Bibliothek.
Interessant wird das ganze, wenn man eine Binärdistribution vom Projekt erstellen will. Hier sind natürlich nur die zur Laufzeit benötigten Bibliotheken erforderlich.
Zusätzlich kann man mit Hilfe eines zweiten Stylesheets eine property-Datei erzeugen, die als key die symbolischen Bibiotheksnamen enthält und als Wert den konkreten Lib-Namen. Diese Property-Datei kann man dann in Ant mit <property file="file.name"/> laden und alle Bibliotheksnamen stehen fortan den Tasks zur Verfügung. Dadurch muss das Buildfile nicht angepasst werden, wenn eine Lib in einer neuen Version veröffentlicht wird, sondern nur das externe XML-File muss angepasst werden, wodurch man zusätzlich gezwungen wird, die Lizenz der Bibliothek erneut zu prüfen.
Da ich für den ganzen Projektprozess Ant einsetze, ist die Lösung ganz einfach: Aus der XML-Datei wird mit XSL ein ant-task erstellt, der überprüft, ob jede Bibliothek eine Lizenz hat und zusätzlich die Lizenzen und libs in ein build-Verzeichnis kopiert, das in das distribution-jar inkludiert wird.
Zur besserren Übersicht hier die einzelnen Dateien:
build-jars.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE jars SYSTEM "build-jars.dtd"> <jars> <jar name="asm.jar" file="asm-1.4.3.jar"/> <jar name="asm-util.jar" file="asm-util-1.4.3.jar"/> <jar name="castor-xml.jar" file="castor-0.9.5.3-xml.jar"/> <jar name="commons-beanutils-core.jar" file="commons-beanutils-core-1.7.0.jar"/> <jar name="commons-collections.jar" file="commons-collections-3.1.jar"/> <jar name="commons-dbcp.jar" file="commons-dbcp-1.2.1.jar"/> <jar name="commons-dbutils.jar" file="commons-dbutils-1.0.jar"/> <jar name="commons-jxpath.jar" file="commons-jxpath-1.2.jar"/> <jar name="commons-logging.jar" file="commons-logging-1.0.4.jar"/> <jar name="commons-pool.jar" file="commons-pool-1.2.jar"/> <jar name="groovy.jar" file="groovy-1.0-beta-7.jar"/> <jar name="junit.jar" file="junit-3.8.1.jar" license="html"/> <jar name="log4j.jar" file="log4j-1.2.8.jar"/> <jar name="pg74-jdbc3.jar" file="pg74.215.jdbc3.jar"/> <jar name="pircbot.jar" file="pircbot-1.4.2.jar"/> <jar name="xercesImpl.jar" file="xercesImpl-2.6.2.jar"/> <jar name="xml-apis.jar" file="xml-apis-2.6.2.jar"/> </jars>
Transformation in den oben beschriebenen Ant-Task mit build-distribution-tasks.xsl:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<project name="not-important">
<xsl:apply-templates select="/jars"/>
</project>
</xsl:template>
<xsl:template match="jars">
<target name="prepare-libs"
description="prepares all libs that are required at runtime">
<xsl:variable name="ready">required.libs.legal.and.available</xsl:variable>
<xsl:apply-templates select="jar">
<xsl:with-param name="generate">available</xsl:with-param>
</xsl:apply-templates>
<xsl:variable name="lib.dir">${lib.dir}</xsl:variable>
<xsl:variable name="legal.dir">${legal.dir}</xsl:variable>
<xsl:variable name="dst.lib.dir">${build.generated.dir}/lib</xsl:variable>
<xsl:variable name="dst.legal.dir">${build.generated.dir}/legal</xsl:variable>
<mkdir dir="{$dst.lib.dir}"/>
<mkdir dir="{$dst.legal.dir}"/>
<copy todir="{$dst.lib.dir}">
<fileset dir="{$lib.dir}">
<xsl:apply-templates select="jar">
<xsl:with-param name="generate">copyJar</xsl:with-param>
</xsl:apply-templates>
</fileset>
</copy>
<copy todir="{$dst.legal.dir}">
<fileset dir="{$legal.dir}">
<xsl:apply-templates select="jar">
<xsl:with-param name="generate">copyLegal</xsl:with-param>
</xsl:apply-templates>
</fileset>
</copy>
</target>
</xsl:template>
<xsl:template match="jar">
<xsl:param name="generate"/>
<xsl:variable name="jarAvailableProperty"
select="concat(@name, '.available')"/>
<xsl:variable name="jarAvailableFile"
select="concat('${lib.dir}/', @file)"/>
<xsl:variable name="jarLegalAvailableProperty"
select="concat(@name, '.legal.available')"/>
<xsl:variable name="jarLegalAvailableFile">
<xsl:choose>
<xsl:when test="@license = 'html'">
<xsl:value-of
select="concat('${legal.dir}/', @file, '.LICENSE.html')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of
select="concat('${legal.dir}/', @file, '.LICENSE.txt')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="jarFile" select="@file"/>
<xsl:variable name="licenseFile">
<xsl:choose>
<xsl:when test="@license = 'html'">
<xsl:value-of select="concat(@file, '.LICENSE.html')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat(@file, '.LICENSE.txt')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:when test="$generate = 'available'">
<available property="{$jarAvailableProperty}"
file="{$jarAvailableFile}"/>
<fail unless="{$jarAvailableProperty}"
message="Required library {$jarAvailableFile} is missing"/>
<available property="{$jarLegalAvailableProperty}"
file="{$jarLegalAvailableFile}"/>
<fail unless="{$jarLegalAvailableProperty}"
message="Required LICENSE {$jarLegalAvailableFile} is missing"/>
</xsl:when>
<xsl:when test="$generate = 'copyJar'">
<include name="{$jarFile}"/>
</xsl:when>
<xsl:when test="$generate = 'copyLegal'">
<include name="{$licenseFile}"/>
</xsl:when>
<xsl:otherwise>
<error generate-is-set-to-but-unknown="{$generate}"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Die Vorbereitung der Distribution erfolgt dann simpel in einem Ant-Task:
<xslt basedir="${basedir}" destdir="${basedir}" in="${dist.bin.jars.rules}"
style="${dist.bin.jars.xsl}"
out="${dist.bin.jars.task}">
<outputproperty name="method" value="xml"/>
<outputproperty name="indent" value="yes"/>
</xslt>
<ant antfile="${dist.bin.jars.task}" target="prepare-libs"/>
Im init-Target (wird von jedem anderen Target vorausgesetzt) kann nun mit Hilfe des Stylesheets build-jar-properties.xsl ein Property-File erstellt werden:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<xsl:text># auto generated, do not edit
</xsl:text>
<xsl:apply-templates select="/jars"/>
</xsl:template>
<xsl:template match="jars">
<xsl:apply-templates select="jar"/>
</xsl:template>
<xsl:template match="jar">
<xsl:value-of select="@name"/>=<xsl:value-of select="@file"/><xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
Das Ant-init-Target sieht so aus (gekürzt):
<target name="init" description="setup build environment">
<tstamp/>
<!-- Generate a property file from our jar defintions -->
<xslt in="${dist.bin.jars.rules}"
style="${jar.properties.xsl}"
out="${jar.properties.file}">
<outputproperty name="method" value="text"/>
</xslt>
<!-- make jar properties available -->
<property file="${jar.properties.file}"/>
</target>
Der ganze Crap hier steht btw. unter der http://www.apache.org/licenses/LICENSE-2.0 ;-)