Project: Aion-Extreme
/*
 * This file is part of aion-emu <aion-emu.com>. 
 * 
 *  aion-emu is free software: you can redistribute it and/or modify 
 *  it under the terms of the GNU General Public License as published by 
 *  the Free Software Foundation, either version 3 of the License, or 
 *  (at your option) any later version. 
 * 
 *  aion-emu is distributed in the hope that it will be useful, 
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of 
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 *  GNU General Public License for more details. 
 * 
 *  You should have received a copy of the GNU General Public License 
 *  along with aion-emu.  If not, see <http://www.gnu.org/licenses/>. 
 */
package com.aionemu.gameserver.dataholders.loadingutils; 
 
import static org.apache.commons.io.filefilter.FileFilterUtils.andFileFilter; 
import static org.apache.commons.io.filefilter.FileFilterUtils.makeSVNAware; 
import static org.apache.commons.io.filefilter.FileFilterUtils.notFileFilter; 
import static org.apache.commons.io.filefilter.FileFilterUtils.prefixFileFilter; 
import static org.apache.commons.io.filefilter.FileFilterUtils.suffixFileFilter; 
 
import java.io.BufferedWriter; 
import java.io.File; 
import java.io.FileNotFoundException; 
import java.io.FileReader; 
import java.io.FileWriter; 
import java.io.IOException; 
import java.util.Collection; 
import java.util.Properties; 
 
import javax.xml.namespace.QName; 
import javax.xml.parsers.ParserConfigurationException; 
import javax.xml.parsers.SAXParser; 
import javax.xml.parsers.SAXParserFactory; 
import javax.xml.stream.XMLEventFactory; 
import javax.xml.stream.XMLEventReader; 
import javax.xml.stream.XMLEventWriter; 
import javax.xml.stream.XMLInputFactory; 
import javax.xml.stream.XMLOutputFactory; 
import javax.xml.stream.XMLStreamException; 
import javax.xml.stream.events.Attribute; 
import javax.xml.stream.events.Comment; 
import javax.xml.stream.events.StartElement; 
import javax.xml.stream.events.XMLEvent; 
 
import org.apache.commons.io.FileUtils; 
import org.apache.commons.io.IOUtils; 
import org.apache.commons.io.filefilter.HiddenFileFilter; 
import org.apache.commons.io.filefilter.IOFileFilter; 
import org.apache.log4j.Logger; 
import org.xml.sax.Attributes; 
import org.xml.sax.Locator; 
import org.xml.sax.SAXException; 
import org.xml.sax.SAXParseException; 
import org.xml.sax.helpers.DefaultHandler; 
 
/**
 * <p><code>XmlMerger</code> is a utility that writes XML document onto an other document 
 * with resolving all <code>import</code> elements. 
 * </p> 
 * <p> 
 * Schema: 
 * <pre> 
 * <xs:element name="import"> 
 * <xs:annotation> 
 * <xs:documentation><![CDATA[ 
 *      Attributes: 
 *          'file' : 
 *              Required attribute. 
 *              Specified path to imported file or directory. 
 *          'skipRoot' : 
 *              Optional attribute. 
 *              Default value: 'false'. 
 *              If enabled, then root tags of imported files are ignored. 
 *          'recirsiveImport': 
 *              Optional attribute. 
 *              Default value: 'true'. 
 *              If enabled and attribute 'file' points to the directory, then all xml files in that 
 *              directory ( and deeper - recursively ) will be imported, otherwise only files inside 
 *              that directory (without it subdirectories) 
 *  ]]></xs:documentation> 
 * </xs:annotation> 
 * <xs:complexType> 
 * <xs:attribute type="xs:string" name="file" use="required"/> 
 * <xs:attribute type="xs:boolean" name="skipRoot" use="optional" default="false"/> 
 * <xs:attribute type="xs:boolean" name="recursiveImport" use="optional" default="true" /> 
 * </xs:complexType> 
 * </xs:element> 
 * </pre> 
 * </p> 
 * <p/> 
 * Created on: 23.07.2009 12:55:14 
 * 
 * @author Aquanox 
 */
 
public class XmlMerger 
 private static final Logger logger = Logger.getLogger(XmlMerger.class); 
 
 private final File  baseDir; 
 
 private final File  sourceFile; 
 private final File  destFile; 
 
 private final File  metaDataFile; 
 
 private XMLInputFactory     inputFactory    = XMLInputFactory.newInstance(); 
 private XMLOutputFactory    outputFactory   = XMLOutputFactory.newInstance(); 
 private XMLEventFactory     eventFactory    = XMLEventFactory.newInstance(); 
 
 /**
  * Create new instance of <tt>XmlMerger </tt>. 
  * Base directory is set to directory which contains source file. 
  * 
  * @param source Source file. 
  * @param target Destination file. 
  */
 
 public XmlMerger(File source, File target) 
 
  this(source, target, source.getParentFile()); 
 
 
 /**
  * Create new instance of <tt>XmlMerger </tt> 
  * 
  * @param source  Source file. 
  * @param target  Destination file. 
  * @param baseDir Root directory. 
  * 
  */
 
 public XmlMerger(File source, File target, File baseDir) 
 
  this.baseDir = baseDir; 
 
  this.sourceFile = source; 
  this.destFile = target; 
   
  this.metaDataFile = new File(target.getParent(), target.getName() + ".properties"); 
 
 
 /**
  * This method creates a result document if it is missing, 
  * or updates existing one if the source file has modification.<br /> 
  * If there are no changes - nothing happens. 
  * 
  * @throws FileNotFoundException when source file doesn't exists. 
  * @throws XMLStreamException    when XML processing error was occurred. 
  */
 
 public void process() throws Exception 
 
  logger.debug("Processing " + sourceFile + " files into " + destFile); 
 
  if (!sourceFile.exists()) 
   throw new FileNotFoundException("Source file " + sourceFile.getPath() + " not found."); 
 
  boolean needUpdate = false
 
  if (!destFile.exists()) 
  
   logger.debug("Dest file not found - creating new file"); 
   needUpdate = true
  
  else if (!metaDataFile.exists()) 
  
   logger.debug("Meta file not found - creating new file"); 
   needUpdate = true
  
  else 
  
   logger.debug("Dest file found - checking file modifications"); 
   needUpdate = checkFileModifications(); 
  
 
  if (needUpdate) 
  
   logger.debug("Modifications found. Updating..."); 
   try 
   
    doUpdate(); 
   
   catch (Exception e) 
   
    FileUtils.deleteQuietly(destFile); 
    FileUtils.deleteQuietly(metaDataFile); 
    throw e; 
   
  
  else 
  
   logger.debug("Files are up-to-date"); 
  
 
 
 /**
  * Check for modifications of included files. 
  * 
  * @return <code>true</code> if at least one of included files has modifications. 
  * 
  * @throws IOException                  IO Error. 
  * @throws SAXException                 Document parsing error. 
  * @throws ParserConfigurationException if a SAX parser cannot 
  *                                      be created which satisfies the requested configuration. 
  */
 
 private boolean checkFileModifications() throws Exception 
 
  long destFileTime = destFile.lastModified(); 
 
  if (sourceFile.lastModified() > destFileTime) 
  
   logger.debug("Source file was modified "); 
   return true
  
 
  Properties metadata = restoreFileModifications(metaDataFile); 
 
  if (metadata == null// new file or smth else. 
   return true
 
  SAXParserFactory parserFactory = SAXParserFactory.newInstance(); 
 
  SAXParser parser = parserFactory.newSAXParser(); 
 
  TimeCheckerHandler handler = new TimeCheckerHandler(baseDir, metadata); 
 
  parser.parse(sourceFile, handler); 
 
  return handler.isModified(); 
 
 
 /**
  * This method processes the source file, replacing all of 
  * the 'import' tags by the data from the relevant files. 
  * 
  * @throws XMLStreamException    on event writing error. 
     * @throws IOException  if the destination file exists but is a directory rather than 
     *                  a regular file, does not exist but cannot be created, 
     *                  or cannot be opened for any other reason 
  */
 
 private void doUpdate() throws XMLStreamException, IOException 
 
  XMLEventReader reader = null
  XMLEventWriter writer = null
 
  Properties metadata = new Properties(); 
   
  try 
  
   writer = outputFactory.createXMLEventWriter(new BufferedWriter(new FileWriter(destFile, false))); 
   reader = inputFactory.createXMLEventReader(new FileReader(sourceFile)); 
 
   while (reader.hasNext()) 
   
    final XMLEvent xmlEvent = reader.nextEvent(); 
 
    if (xmlEvent.isStartElement() && isImportQName(xmlEvent.asStartElement().getName())) 
    
     processImportElement(xmlEvent.asStartElement(), writer, metadata); 
     continue
    
 
    if (xmlEvent.isEndElement() && isImportQName(xmlEvent.asEndElement().getName())) 
     continue
 
    if (xmlEvent instanceof Comment)// skip comments. 
     continue
 
    if (xmlEvent.isCharacters())// skip whitespaces. 
     if (xmlEvent.asCharacters().isWhiteSpace() || xmlEvent.asCharacters().isIgnorableWhiteSpace())// skip whitespaces. 
      continue
 
    writer.add(xmlEvent); 
 
    if (xmlEvent.isStartDocument()) { 
     writer.add(eventFactory.createComment("\nThis file is machine-generated. DO NOT MODIFY IT!\n")); 
    
   
 
   storeFileModifications(metadata, metaDataFile); 
  
  finally 
  
   if (writer != null
    try { writer.close(); } catch (Exception ignored) {} 
   if (reader != null
    try { reader.close(); } catch (Exception ignored) {} 
  
 
 
 private boolean isImportQName(QName name) 
 
  return "import".equals(name.getLocalPart()); 
 
 
 private static final QName qNameFile = new QName("file"); 
 private static final QName qNameSkipRoot = new QName("skipRoot"); 
 
 /**
  * If this option is enabled you import the directory, and all its subdirectories. 
  * Default is 'true'. 
  */
 
 private static final QName qNameRecursiveImport = new QName("recursiveImport"); 
 
 /**
  * This method processes the 'import' element, replacing it 
  * by the data from the relevant files. 
  * 
  * @throws XMLStreamException    on event writing error. 
  * @throws FileNotFoundException of imported file was not found. 
  */
 
 private void processImportElement(StartElement element, XMLEventWriter writer, Properties metadata) throws XMLStreamException, IOException 
 
  File file = new File(baseDir, getAttributeValue(element, qNameFile, null"Attribute 'file' is missing or empty.")); 
 
  if (!file.exists()) 
   throw new FileNotFoundException("Missing file to import:" + file.getPath()); 
 
  boolean skipRoot = Boolean.valueOf(getAttributeValue(element, qNameSkipRoot, "false"null)); 
  boolean recImport = Boolean.valueOf(getAttributeValue(element, qNameRecursiveImport, "true"null)); 
 
  if (file.isFile()) 
  
   importFile(file, skipRoot, writer, metadata); 
  
  else 
  
   logger.debug("Processing dir " + file); 
 
   Collection<File> files = listFiles(file, recImport); 
 
   for (File childFile : files) 
   
    importFile(childFile, skipRoot, writer, metadata); 
   
  
 
 
 @SuppressWarnings("unchecked"
 private static Collection<File> listFiles(File root, boolean recursive) 
 
  IOFileFilter dirFilter = recursive ? makeSVNAware(HiddenFileFilter.VISIBLE) : null
 
  return FileUtils.listFiles(root, 
    andFileFilter( 
      andFileFilter( 
        notFileFilter(prefixFileFilter("new")), suffixFileFilter(".xml")), 
      HiddenFileFilter.VISIBLE), 
    dirFilter); 
 
 
 /**
  * Extract an attribute value from a <code>StartElement </code> event. 
  * 
  * @param element        Event object. 
  * @param name           Attribute QName 
  * @param def            Default value. 
  * @param onErrorMessage On error message. 
  * 
  * @return attribute value 
  * 
  * @throws XMLStreamException if attribute is missing and there is no default value set. 
  */
 
 private String getAttributeValue(StartElement element, QName name, String def, String onErrorMessage) 
   throws XMLStreamException 
 
  Attribute attribute = element.getAttributeByName(name); 
 
  if (attribute == null
  
   if (def == null
    throw new XMLStreamException(onErrorMessage, element.getLocation()); 
 
   return def; 
  
 
  return attribute.getValue(); 
 
 
 /**
  * Read all {@link javax.xml.stream.events.XMLEvent}'s from specified file and write them onto the {@link javax.xml.stream.XMLEventWriter} 
  * 
  * @param file     File to import 
  * @param skipRoot Skip-root flag 
  * @param writer   Destenation writer 
  * 
  * @throws XMLStreamException    On event reading/writing error. 
  * @throws FileNotFoundException if the reading file does not exist, 
  *                               is a directory rather than a regular file, 
  *                               or for some other reason cannot be opened for 
  *                               reading. 
  */
 
 private void importFile(File file, boolean skipRoot, XMLEventWriter writer, Properties metadata) throws XMLStreamException, IOException 
 
  logger.debug("Appending file " + file); 
  metadata.setProperty(file.getPath(), makeHash(file)); 
 
  XMLEventReader reader = null
 
  try 
  
   reader = inputFactory.createXMLEventReader(new FileReader(file)); 
 
   QName firstTagQName = null
 
   while (reader.hasNext()) 
   
    XMLEvent event = reader.nextEvent(); 
 
    // skip start and end of document. 
    if (event.isStartDocument() || event.isEndDocument()) 
     continue
    // skip all comments. 
    if (event instanceof Comment) 
     continue
    // skip white-spaces and all ignoreable white-spaces. 
    if (event.isCharacters()) 
    
     if (event.asCharacters().isWhiteSpace() || event.asCharacters().isIgnorableWhiteSpace()) 
      continue
    
 
    // modify root-tag of imported file. 
    if (firstTagQName == null && event.isStartElement()) 
    
     firstTagQName = event.asStartElement().getName(); 
 
     if (skipRoot) 
     
      continue
     
     else 
     
      StartElement old = event.asStartElement(); 
 
      event = eventFactory.createStartElement(old.getName(), old.getAttributes(), null); 
     
    
 
    // if root was skipped - skip root end too. 
    if (event.isEndElement() && skipRoot && event.asEndElement().getName().equals(firstTagQName)) 
     continue
 
    // finally - write tag 
    writer.add(event); 
   
  
  finally 
  
   if (reader != null
    try { reader.close(); } catch (Exception ignored) {} 
  
 
 
 private static class TimeCheckerHandler extends DefaultHandler 
 
  private File basedir; 
  private Properties metadata; 
 
  private boolean isModified = false
 
  private Locator locator; 
 
  private TimeCheckerHandler(File basedir, Properties metadata) 
  
   this.basedir = basedir; 
   this.metadata = metadata; 
  
 
  @Override 
  public void setDocumentLocator(Locator locator) 
  
   this.locator = locator; 
  
 
  @Override 
  public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException 
  
   if (isModified || !"import".equals(qName)) 
    return
 
   String value = attributes.getValue(qNameFile.getLocalPart()); 
 
   if (value == null
    throw new SAXParseException("Attribute 'file' is missing", locator); 
 
   File file = new File(basedir, value); 
 
   if (!file.exists()) 
    //noinspection ThrowableInstanceNeverThrown 
    throw new SAXParseException("Imported file not found. file=" + file.getPath(), locator); 
 
   if (file.isFile() && checkFile(file))// if file - just check it. 
   
    isModified = true
    return
   
 
   if (file.isDirectory())// otherwise check all files inside 
   
    String rec = attributes.getValue(qNameRecursiveImport.getLocalPart()); 
 
    Collection<File> files = listFiles(file, rec == null ? true : Boolean.valueOf(rec)); 
 
    for (File childFile : files) 
    
     if (checkFile(childFile)) 
     
      isModified = true
      return
     
    
   
  
 
  private boolean checkFile(File file) 
  
   String data = metadata.getProperty(file.getPath()); 
 
   if (data == null// file was added. 
    return true
 
   try 
   
    String hash = makeHash(file); 
 
    if (!data.equals(hash))// file|dir was changed. 
     return true
   
   catch (IOException e) 
   
    logger.warn("File varification error. File: " + file.getPath() 
      ", location="+locator.getLineNumber()+":"+locator.getColumnNumber(), e); 
    return true;// was modified. 
   
 
   return false
  
 
  public boolean isModified() 
  
   return isModified; 
  
 
 
 private Properties restoreFileModifications(File file) 
 
  if (!file.exists() || !file.isFile()) 
   return null
 
  FileReader reader = null
 
  try 
  
   Properties props = new Properties(); 
 
   reader = new FileReader(file); 
 
   props.load(reader); 
 
   return props; 
  
  catch (IOException e)// properties 
  
   logger.debug("File modfications restoring error. ", e); 
   return null
  
  finally 
  
   IOUtils.closeQuietly(reader); 
  
 
 
 private void storeFileModifications(Properties props, File file) 
   throws IOException 
 
  FileWriter writer = null
  try 
  
   writer = new FileWriter(file, false); 
   props.store(writer, " This file is machine-generated. DO NOT EDIT!"); 
  
  catch (IOException e) 
  
   logger.error("Failed to store file modification data."); 
   throw e; 
  
  finally 
  
   IOUtils.closeQuietly(writer); 
  
 
 
 /**
  * Create a unique identifier of file and it contents. 
  * 
  * @param file  the file to checksum, must not be <code>null</code> 
  * @return String identifier 
  * @throws IOException if an IO error occurs reading the file 
  */
 
 private static String makeHash(File file) 
   throws IOException 
 
  return String.valueOf(FileUtils.checksumCRC32(file)); 
 
}