czwartek, 10 kwietnia 2014

Updating xml file in war/ear

Recently I needed to update some xml files inside war and ear archives. They were xml descriptors, so I did it with Groovy because of its great xml support.

The XmlSlurper is a great thing. It allows you to operate on xml, using node names as properties of the class. The only thing left is just to read in the xml, which in Groovy is also really simple.
The only concern might be that the file is inside war/ear. But they're just zip files, so it's easy to unzip, modify what you want and zip it back.
At first I thought about modifying file directly inside the war/ear. But that's way too complicated (you can easily break the zip, and Java API does not have a good support for manipulating files directly in the zip).
Here is the code:
import groovy.xml.StreamingMarkupBuilder;

import java.io.File;
import java.util.zip.ZipEntry;

import net.lingala.zip4j.core.ZipFile;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.util.Zip4jConstants;


public class ArchiveUpdater {

 static String JBOSS_WEB_FILE = "jboss-web.xml"
 static String PERSISTENCE_XML_FILE = "persistence.xml"
 static String APPLICATION_XML_FILE = "application.xml"

 String extractedPath
 String extractedModulesPath
 String trigram
 String inputArchivePath
 boolean deepSearch
 String outputDir

 File workingDir
 File workingModulesDir
 
 /**
  * @param trigram - additional string to add to context root, datasource name etc
         * @param archivePath - path to archive to modify
         * @param deepSearch - if archive to modify is ear this should be true, to enable changing also war inside ear
         * @param tempFolder - temp path to use for unpacking
         * @param outputDir - dir to store the processed archive
  */
 public ArchiveUpdater(String trigram, String archivePath, boolean deepSearch, String tempFolder, String outputDir){
  this.trigram = trigram
  this.inputArchivePath = archivePath
  this.deepSearch = deepSearch
  this.extractedPath = tempFolder+"/extracted/"
  this.extractedModulesPath = tempFolder+"/extractedModules/"
  this.workingDir = new File(extractedPath)
  this.workingModulesDir = new File(extractedModulesPath)
  this.outputDir = outputDir
 }
 
 public void makeArchiveWithTrigram() {
    
    extractArchive()
    
    modifyFiles()
    
    makeNewArchive()
 }
 
 private modifyFiles(){
  workingDir.traverse {
   modifyFile(it)
  }
  if(deepSearch){
   workingModulesDir.traverse {
    modifyFile(it)
   }
  }
 }
 
 private modifyFile(File file){
  if (file.name.endsWith(JBOSS_WEB_FILE)){
   modifyJbossWebFile(file)
  } else if( file.name.endsWith(PERSISTENCE_XML_FILE)) {
   modifyJbossPersistenceXmlFile(file)
  } else if( file.name.endsWith(APPLICATION_XML_FILE)){
   modifyApplicationXmlFile(file)
  }
 }
 
 private extractArchive(){
  ZipFile inputZip = new ZipFile(inputArchivePath)
  inputZip.extractAll(extractedPath)
  if(deepSearch){
   workingModulesDir = new File(extractedModulesPath)
   workingDir.eachFile {
    if(it.isFile()){
     ZipFile archiveModule = new ZipFile(it)
     archiveModule.extractAll(extractedModulesPath+"/"+it.name)
    }
   }
  }
 }
 
 private modifyApplicationXmlFile(File entry){
  String fileContent = entry.text
  def root = new XmlSlurper(false, false).parseText(fileContent)
  def modules =  root.module
  modules.each{
   def webModule = it.web
   if(webModule != null){
    def currentRoot =  webModule.'context-root'
    webModule.'context-root'= '/'+trigram+currentRoot
   }
  }
  def outputBuilder = new StreamingMarkupBuilder()
  String result = outputBuilder.bind{ mkp.yield root }
  entry.write(result)
 }

 private modifyJbossWebFile(File entry){
  String fileContent = entry.text
  def root = new XmlSlurper().parseText(fileContent)
  def currentRoot =  root.'context-root'
  root.'context-root'= '/'+trigram+currentRoot
  def outputBuilder = new StreamingMarkupBuilder()
  String result = outputBuilder.bind{ mkp.yield root }
  entry.write(result)
 }

 private modifyJbossPersistenceXmlFile(File entry){
  String fileContent = entry.text
  def root = new XmlSlurper().parseText(fileContent)
  def persistenceUnits =  root.'persistence-unit'
  persistenceUnits.each{
   def currentDS = it.'jta-data-source'
   it.'jta-data-source'= ''+currentDS+trigram
  }
  def outputBuilder = new StreamingMarkupBuilder()
  String result = outputBuilder.bind{ mkp.yield root }
  entry.write(result)
 }

 private makeNewArchive(){
  if(deepSearch){
   workingModulesDir.eachDir {
    File originalModule = new File(workingDir, it.name)
    originalModule.delete()
    ZipParameters parameters = createZipParams()
    ZipFile moduleOutputZip = new ZipFile(originalModule)
    for (File file : it.listFiles()) {
     if (file.isFile()) {
      moduleOutputZip.addFile(file, parameters);
     } else if (file.isDirectory()) {
      moduleOutputZip.addFolder(file, parameters);
     }
    }
   }
  }
  String archiveName = outputDir+"/"+trigram+"-"+ new File(inputArchivePath).getName()
  new File(archiveName).delete()
  ZipFile outputZip = new ZipFile(archiveName)
  ZipParameters parameters = createZipParams()
  for (File file : workingDir.listFiles()) {
   if (file.isFile()) {
    outputZip.addFile(file, parameters);
   } else if (file.isDirectory()) {
    outputZip.addFolder(file, parameters);
   }
  }
  workingDir.deleteDir()
  workingModulesDir.deleteDir()
 }
 /**
  * Creates zip params.
  * For each zip, params must be a new object since zip4j modifies this durining zipping
  * Using same object for multiple zips ends with unexpected behaviour and errors.
  * @return new zip params
  */
 private static ZipParameters createZipParams(){
  ZipParameters parameters = new ZipParameters();
  parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE);
  parameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL);
  return parameters
 }
}
Most important things in this code:
  •  extractArchive/makeNewArchive methods is where I use zip4j lib to extract/zip back the archives
  • modify<FileType>File is where the XmlSlurper does magic. As you can see the reading of the file is done by calling text on the File. Write is done by calling write(value). Those are nice Groovy extensions to standard Java File class
So let's assume that the xml to modify looks like this
<jboss-web>
     <context-root>/navigator/central-monitoring-module</context-root>
</jboss-web>
The XmlSlurper.parseText() gives us handle to the top level element of the xml. So to access context-root element we just need to call
root.'context-root'
Do you wonder why context-root is in quotes? The answer: because of the - sign in the element name. Without the quotes Groovy will try to substract two strings. So if your node has some special char it's safer to put it inside quotes to avoid strange errors.

So, if you ever need to modify the xml, remember, Groovy is your friend :)

Brak komentarzy:

Prześlij komentarz