In a previous blog post I obtained the subversion XML log output. Now I need to convert that into Java objects in order to provide some special reporting requirements. Below I just present the unmarshal code.
Dec 3, 2014: cloned code into a github repository
As mentioned before, by using a high-level Java API to Subversion, like SVNKit, we can generate logs and have those already unmarshaled into objects. This is the recommended approach.
Let’s continue with the “brute force” way of accessing the output XML log output.
In listing one the Unmarshal class is a utility that hides the JAXB unmarshal code. The actual use of JAXB is a few lines, but this class provides methods that accepts various sources. For example using a file path:
Log theLog = new Unmarshal().path(“src/test/resources/log.xml”);
package com.octodecillion.svn; import java.io.File; import java.io.IOException; import java.io.Reader; import java.net.URL; import java.nio.charset.Charset; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import javax.xml.transform.stream.StreamSource; import org.xml.sax.SAXException; import com.google.common.base.Preconditions; import com.google.common.io.CharSource; import com.google.common.io.Resources; /** * @author jbetancourt */ public class Unmarshal { /** */ public Log path(String path) throws JAXBException, SAXException, IOException { Preconditions.checkNotNull(path, "'path' param is null"); return url(new File(path).toURI().toURL()); } /** */ public Log url(String url) throws JAXBException, SAXException, IOException { Preconditions.checkNotNull(url, "'url' param is null"); CharSource charSrc = Resources.asCharSource(new URL(url), Charset.defaultCharset()); return unmarshall(charSrc); } /** */ public Log url(URL url) throws JAXBException, SAXException, IOException { Preconditions.checkNotNull(url, "'url' param is null"); CharSource charSrc = Resources.asCharSource(url, Charset.defaultCharset()); return unmarshall(charSrc); } /** */ public Log string(String xml) throws JAXBException, SAXException, IOException { Preconditions.checkNotNull(xml, "'xml' param is null"); return unmarshall(CharSource.wrap(xml)); } /** */ public Log unmarshall(CharSource in) throws JAXBException, SAXException, IOException { Preconditions.checkNotNull(in, "'in' param is null"); JAXBContext jaxbContext = JAXBContext.newInstance(Log.class); Log theLog = null; try(Reader reader = in.openStream()){ StreamSource source = new StreamSource(reader); Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); JAXBElement<Log> jxbElement = unmarshaller.unmarshal(source, Log.class); theLog = jxbElement.getValue(); } return theLog; } }
Listing 1, Unmarshal class
The Classes tree that captures the SVN log output XML
The log is in this format:
<log> <logentry revision="20950"> <author>jbetancourt</author> <date>2014-11-10T20:12:11.910891Z</date> <paths> <path text-mods="false" kind="file" action="D" prop-mods="false">/2014/Acme/branches/rabbit-trap/www/images/beep.png </path> </paths> <msg>initial commit</msg> </logentry> <logentry revision="20948"> ..... </log>
Listing 0, example log contents
To use JAXB we create annotated classes to match the log XML.
These are simple use of JAXB. I’m sure there are better approaches. Note, the getter/setters
were not put in. Does a JAXB processor need these? Can’t it just use reflection?
The toString() in these classes return a JSON marshaling of the object. This is helpful for unit testing and debug.
Log.javapackage com.octodecillion.svn; import java.util.List; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; /** * @author j.betancourt */ @XmlRootElement public class Log { @XmlElement(name = "logentry") List<LogEntry> entries; @Override public String toString() { StringBuilder bld = new StringBuilder(); for(LogEntry entry : entries){ bld.append(entry.toString()); } return bld.toString(); } }
Listing 2, Log class
LogEntry.javapackage com.octodecillion.svn; import java.util.List; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlRootElement; import com.google.common.base.Joiner; /** * @author j.betancourt */ @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement public class LogEntry { @XmlAttribute String revision; String author; String date; @XmlElementWrapper(name="paths") @XmlElement(name="path") List<Path>paths; String msg; // getter/setters not shown @Override public String toString() { StringBuilder bld = new StringBuilder("["); bld.append(Joiner.on(",").join(this.paths)).append("]"); return String.format("{revision:%s,author:%s,date:%s,paths:%s,msg:%s}", this.revision,this.author,this.date,bld.toString(),this.msg); } }
Listing 3, LogEntry class Path.java
package com.octodecillion.svn; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlValue; /** * @author j.betancourt */ @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement public class Path { @XmlValue String value; @XmlAttribute(name = "text-mods") String textmods; @XmlAttribute(name="kind") String kind; @XmlAttribute(name="action") String action; @XmlAttribute(name="prop-mods") String propmods; @Override public String toString() { return String.format("{value:%s,kind:%s,action:%s,textmods:%s,propmods:%s}", value.replaceAll("\\n+",""),kind,action,textmods,propmods); } }
Listing 4, Path class
A JUnit 4 test
package com.octodecillion.svn; import java.io.IOException; import javax.xml.bind.JAXBException; import org.junit.Assert; import org.junit.Test; import org.xml.sax.SAXException; /** * @author j.betancourt */ public class UnMarshallTest { @Test public final void test() throws Exception { Log theLog = new Unmarshal().path("src/test/resources/log.xml"); String actual = toSingleLine(theLog.toString()); String expected1 = "{revision:20950,author:jbetancourt,date:2014-11-10T20:12:11.910891Z,paths:[{value:/2014/Acme/branches/rabbit-trap/www/images/beep.png,kind:file,action:D,textmods:false,propmods:false}],msg:initialcommit}{revision:20948,author:jbetancourt,date:2014-11-10T19:55:58.629641Z,paths:[{value:/2014/Acme/branches/rabbit-trap/www/images/desert.png,kind:file,action:D,textmods:false,propmods:false}],msg:changedicontint}{revision:20942,author:jbetancourt,date:2014-11-10T15:30:08.770266Z,paths:[{value:/2014/Acme/branches/rabbit-trap/www/scripts/acme/traps/rocket.js,kind:file,action:M,textmods:true,propmods:false},{value:/2014/Acme/branches/rabbit-trap/www/scripts/acme/traps/sled.js,kind:file,action:M,textmods:true,propmods:false}],msg:Added'usestrict'.}{revision:20941,author:rsmith,date:2014-11-10T15:20:41.707766Z,paths:[{value:/2014/Acme/branches/rabbit-trap/www/ads/umbrella/promo.html,kind:file,action:M,textmods:true,propmods:false},{value:/2014/Acme/branches/rabbit-trap/www/ads/images/umbrella.jpg,kind:file,action:A,textmods:true,propmods:true}],msg:promotionMerge}"; String expected = toSingleLine(expected1); Assert.assertEquals("Created wrong object structure", expected, actual); } /** */ String toSingleLine(String s) { String s1 = s.replaceAll("\\n+", ""); return s1.replaceAll("\\s+", ""); } }
Listing 5, JUnit test
pom.xml<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.octodecillion</groupId> <artifactId>svnunmarshal</artifactId> <version>0.0.1-SNAPSHOT</version> <name>SvnUnMarshal</name> <description>Example of how to use JAXB to unmarshal svn log</description> <!-- <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.2</version> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> </plugins> </build> --> <dependencies> <dependency> <groupId>org.tmatesoft.svnkit</groupId> <artifactId>svnkit</artifactId> <version>1.8.5</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> <scope>runtime</scope> </dependency> <dependency> <groupId>xmlunit</groupId> <artifactId>xmlunit</artifactId> <version>1.5</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-exec</artifactId> <version>1.3</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.jmockit</groupId> <artifactId>jmockit</artifactId> <version>1.13</version> <scope>test</scope> </dependency> </dependencies> </project>
Listing 6, Maven POM
Further reading
