package org.jboss.loom.utils; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.jboss.loom.ex.MigrationException; import org.jboss.loom.migrators.Origin; import org.jboss.loom.migrators.mail.MailServiceBean; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.Locator; import org.xml.sax.SAXException; /** * * @author Ondrej Zizka, ozizka at redhat.com */ public class XmlUtils { private static final org.slf4j.Logger log = LoggerFactory.getLogger( XmlUtils.class ); /** * Creates this app's standard marshaller. * TODO: Use it in methods below. */ public static JAXBContext createJaxbContext( Class cls ) throws JAXBException { Map<String, Object> props = new HashMap(); props.put( Marshaller.JAXB_FORMATTED_OUTPUT, true ); props.put( Marshaller.JAXB_ENCODING, "UTF-8"); //marshaller.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, "http://www.jboss.org/schema/as-migration.xsd as-migration.xsd"); JAXBContext jaxbCtx = org.eclipse.persistence.jaxb.JAXBContextFactory.createContext(new Class[]{cls}, props); return jaxbCtx; // JDK way: Marshaller mar = JAXBContext.newInstance(MigrationReportJaxbBean.class).createMarshaller(); } public static Marshaller createMarshaller( Class cls ) throws JAXBException { return createJaxbContext( cls ).createMarshaller(); } /** * Convenience - calls the override with must = true. */ public static <T> T unmarshallBean( File xmlFile, Class<T> cls ) throws MigrationException { try { Unmarshaller unmarshaller = JAXBContext.newInstance(cls).createUnmarshaller(); return (T) unmarshaller.unmarshal(xmlFile); } catch( JAXBException ex ) { throw new MigrationException("Failed unmarshalling "+xmlFile+" to "+cls.getSimpleName()+": " + ex.getMessage(), ex); } } /** * Convenience - calls the override with must = true. */ public static <T> T unmarshallBean( File docFile, String xpath, Class<T> cls ) throws MigrationException{ return unmarshallBean( true, docFile, xpath, cls ); } /** * Read XML from the File, look for nodes by XPath, and unmarshall them into given Class. * If Class is Origin.Wise, the origin is stored. */ public static <T> T unmarshallBean( boolean must, File docFile, String xpath, Class<T> cls ) throws MigrationException{ if( ! docFile.exists() ){ if( must ) throw new MigrationException("File to unmarshall not found: " + docFile); else return null; } List<T> beans = unmarshallBeans( docFile, xpath, cls ); if( beans.isEmpty() ) throw new MigrationException("XPath "+xpath+" returned no nodes from " + docFile); return beans.get(0); } /** * Read XML from the File, look for nodes by XPath, and unmarshall them into given Class. * If Class is Origin.Wise, the origin is stored. * * Caution: Uses JDK's XPathFactoryImpl - Saxon doesn't do well with namespaces. */ public static <T> List<T> unmarshallBeans( File docFile, String xpath, Class<T> cls ) throws MigrationException{ List<T> beans = new LinkedList(); DocumentBuilder docBuilder = createXmlDocumentBuilder(); try { // Parse Document doc = docBuilder.parse(docFile); // XPath //XPathFactory xpf = XPath xp = XPathFactory.newInstance(); //XPathFactory xpf = new net.sf.saxon.xpath.XPathFactoryImpl(); // We need Sun's XPath as it ignores namespaces if not specified. //XPathFactory xpf = new com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl(); // Warning XPathFactory xpf; try { xpf = (XPathFactory) Class.forName("com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl").newInstance(); } catch( Exception ex ){ throw new IllegalStateException("Shouldn't happen: " + ex.getMessage(), ex ); } XPath xp = xpf.newXPath(); NodeList nodes = (NodeList) xp.evaluate(xpath, doc, XPathConstants.NODESET); // Unmarshall //JAXBContext.newInstance(cls).createUnmarshaller(); //Unmarshaller unmarshaller = org.eclipse.persistence.jaxb.JAXBContext.newInstance(cls).createUnmarshaller(); Unmarshaller unmarshaller = createJaxbContext( cls ).createUnmarshaller(); for( int i = 0; i < nodes.getLength(); i++ ) { Node node = nodes.item( i ); T bean = (T) unmarshaller.unmarshal(node); beans.add( bean ); // Origin - set File and XPath. if( bean instanceof Origin.Wise ){ final Origin orig = new Origin( docFile, xpath ).setOffset( i ); ((Origin.Wise) bean).setOrigin( orig ); } } } catch( SAXException | IOException | XPathExpressionException | JAXBException ex ) { throw new MigrationException("Failed parsing bean from a XML file " + docFile.getPath() + ":\n " + ex.getMessage(), ex); } return beans; } /** * Convenience - calls the override with must = true. */ public static <T> T readXmlConfigFile( File file, String xpath, Class<T> cls, String confAreaDesc ) throws MigrationException{ return readXmlConfigFile( true, file, xpath, cls, confAreaDesc ); } /** * Reads given XML file, finds the first node matching given XPath, and reads it using given JAXB class. * @param confAreaDesc Used for exception message. * @throws MigrationException wrapping any Exception. */ public static <T> T readXmlConfigFile( boolean must, File file, String xpath, Class<T> cls, String confAreaDesc ) throws MigrationException{ if( ! file.exists() ){ if( must ) throw new MigrationException("File to unmarshall not found: " + file); else return null; } try { return XmlUtils.unmarshallBean( must, file, xpath, cls ); } catch( Exception ex ) { throw new MigrationException("Failed loading "+confAreaDesc+" config from "+file.getPath()+":\n " + ex.getMessage(), ex); } } public static <T> List<T> readXmlConfigFileMulti( File file, String xpath, Class<T> cls, String confAreaDesc ) throws MigrationException{ return readXmlConfigFileMulti( true, file, xpath, cls, confAreaDesc ); } /** * Reads given XML file, finds all nodes matching given XPath, and reads them into a list using given JAXB class. * @param confAreaDesc Used for exception message. * @throws MigrationException wrapping any Exception. */ public static <T> List<T> readXmlConfigFileMulti( boolean must, File file, String xpath, Class<T> cls, String confAreaDesc ) throws MigrationException{ if( ! file.exists() ){ if( must ) throw new MigrationException("File to unmarshall not found: " + file); else return null; } try { return XmlUtils.unmarshallBeans( file, xpath, cls ); } catch( Exception ex ) { throw new MigrationException("Failed loading "+confAreaDesc+" config from "+file.getPath()+":\n " + ex.getMessage(), ex); } } public static <T> List<T> readXmlConfigFiles( File baseDir, String filesPattern, String xpath, Class<? extends T> cls, String confAreaDesc ) throws MigrationException{ if( ! baseDir.exists() ) return Collections.EMPTY_LIST; List<File> files; try { //files = new PatternDirWalker( filesPattern ).list( baseDir ); files = new DirScanner( filesPattern ).listAsFiles( baseDir ); } catch( IOException ex ) { throw new MigrationException("Failed finding files matching '"+filesPattern+"' in " + baseDir + ":\n " + ex.getMessage(), ex); } List<T> res = new LinkedList(); for( File file : files ) { try { //res.addAll( XmlUtils.unmarshallBeans( new File(baseDir, file.getPath()), xpath, cls ) ); res.addAll( XmlUtils.unmarshallBeans( file, xpath, cls ) ); } catch( Exception ex ) { throw new MigrationException("Failed loading "+confAreaDesc+" config from "+file.getPath()+":\n " + ex.getMessage(), ex); } } return res; } /** * Creates a new default document builder. * * */ public static DocumentBuilder createXmlDocumentBuilder() { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware( false ); String feat = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; try { dbf.setFeature( feat, false ); } catch( ParserConfigurationException ex ) { log.warn( "Couldn't set " + feat + " to false. The parser may attempt to load DTD." ); } try { return dbf.newDocumentBuilder(); } catch( ParserConfigurationException ex ) { throw new RuntimeException( ex ); } } /** * Saves given XML Document into given File. */ public static File transformDocToFile( Document srcDoc, File dest ) throws TransformerException { return transformDocToFile( srcDoc, dest, null ); } /** * Transforms given Document into given File, using given XSLT. * @param xsltIS may be null -> just saves. */ public static File transformDocToFile( Document srcDoc, File dest, InputStream xsltIS ) throws TransformerException { Transformer transformer = createTransformer( xsltIS ); transformer.transform( new DOMSource(srcDoc), new StreamResult(dest) ); return dest; } public static File transform( File src, File dest, InputStream xsltIS ) throws TransformerException { Transformer transformer = createTransformer( xsltIS ); transformer.transform( new StreamSource( src ), new StreamResult(dest) ); return dest; } private static Transformer createTransformer( InputStream xsltIS ) throws TransformerConfigurationException { //final TransformerFactory tf = TransformerFactory.newInstance(); final TransformerFactory tf = new net.sf.saxon.TransformerFactoryImpl(); // XSLT 2.0 final Transformer transformer = xsltIS == null ? tf.newTransformer() : tf.newTransformer( new StreamSource( xsltIS ) ); transformer.setOutputProperty( OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); return transformer; } /** * Calls transformDocToFile(), wraps exception to MigrationException. */ public static void saveXmlToFile( Document doc, File file ) throws MigrationException { try { transformDocToFile( doc, file ); } catch( TransformerException ex ) { throw new MigrationException("Failed saving XML document to " + file.getPath()+":\n " + ex.getMessage(), ex); } } /** * Creates clean Document used in other classes for working with XML * * @return clean Document * @throws ParserConfigurationException if creation of document fails */ public static Document createDoc() throws ParserConfigurationException { DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance(); domFactory.setIgnoringComments( true ); DocumentBuilder builder = domFactory.newDocumentBuilder(); Document doc = builder.getDOMImplementation().createDocument( null, null, null ); return doc; } /** * @deprecated TODO: useless? */ public static Document parseFileToXmlDoc( File file ) throws SAXException, IOException { DocumentBuilder db = XmlUtils.createXmlDocumentBuilder(); Document doc = db.parse( file ); return doc; } /** * "toString()" for @XmlLocator. */ public static String formatLocation( Locator location ) { if( location == null ) return "(unknown location)"; return location.getPublicId() == null ? String.format("line %d, col %d in %s", location.getLineNumber(), location.getColumnNumber(), location.getSystemId() ) : String.format("Pub: %s Sys: %s Line: %d Col: %d", location.getPublicId(), location.getSystemId(), location.getLineNumber(), location.getColumnNumber() ); } }// class