/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program 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 Lesser General Public License for more details. * * Copyright 2005 - 2008 Pentaho Corporation. All rights reserved. * * @created Jun 17, 2005 * @author James Dixon * @author Marc Batchelor */ package org.pentaho.platform.util.xml; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.URIResolver; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.platform.api.engine.IDocumentResourceLoader; import org.pentaho.platform.util.FileHelper; import org.pentaho.platform.util.logging.Logger; import org.pentaho.platform.util.messages.LocaleHelper; import org.pentaho.platform.util.messages.Messages; /** * A set of static methods for performing various operations on DOM Documents * and XML text (in the form of streams, Strings, and files). The operations include * creating DOM Documents (dom4j) * transforming DOM Documents * creating XML from Objects, Lists and Maps * creating Lists or Maps from XML * getting an XML node's text * * @author mbatchel/jdixon * */ public class XmlHelper { /* * this regular expression pattern should match the value of the encoding * pseudo-attribute in the xml processing instruction in an xml document. e.g. * <?xml version="1.0" encoding="UTF-8" ?> */ private static final Pattern RE_ENCODING = Pattern.compile( "<\\?xml.*encoding=('|\")([^'\"]*)\\1.*\\?>.*", Pattern.DOTALL); //$NON-NLS-1$ private static final String DEFAULT_XSL_FOLDER = "system/custom/xsl/"; //$NON-NLS-1$ private static final Log logger = LogFactory.getLog(XmlHelper.class); public static String listToXML(final List l) throws UnsupportedOperationException { return XmlHelper.listToXML(l, ""); //$NON-NLS-1$ } public static String listToXML(final List l, final String indent) throws UnsupportedOperationException { StringBuffer sb = new StringBuffer(); sb.append(indent).append("<list>\r"); //$NON-NLS-1$ String newIndent = indent + " "; //$NON-NLS-1$ Object obj; for (int i = 0; i < l.size(); i++) { obj = l.get(i); sb.append(newIndent).append("<list-element>\r"); //$NON-NLS-1$ XmlHelper.objToXML(obj, sb, newIndent); sb.append(newIndent).append("</list-element>\r"); //$NON-NLS-1$ } sb.append(indent).append("</list>\r"); //$NON-NLS-1$ return sb.toString(); } public static String mapToXML(final Map m) throws UnsupportedOperationException { return XmlHelper.mapToXML(m, ""); //$NON-NLS-1$ } public static String mapToXML(final Map mp, final String indent) throws UnsupportedOperationException { StringBuffer sb = new StringBuffer(); sb.append(indent).append("<map>\r"); //$NON-NLS-1$ String newIndent = indent + " "; //$NON-NLS-1$ Iterator it = mp.entrySet().iterator(); Map.Entry ent; Object obj; while (it.hasNext()) { ent = (Map.Entry) it.next(); if (!(ent.getKey() instanceof String)) { throw new UnsupportedOperationException(Messages.getInstance().getErrorString("XMLUTL.ERROR_0011_MAP_KEYS")); //$NON-NLS-1$ } sb.append(newIndent).append("<map-entry>\r"); //$NON-NLS-1$ sb.append(newIndent).append("<key>\r"); //$NON-NLS-1$ sb.append(newIndent).append("<![CDATA[").append(ent.getKey().toString()).append("]]>\r"); //$NON-NLS-1$ //$NON-NLS-2$ sb.append(newIndent).append("</key>\r"); //$NON-NLS-1$ obj = ent.getValue(); XmlHelper.objToXML(obj, sb, newIndent); sb.append(newIndent).append("</map-entry>\r"); //$NON-NLS-1$ } sb.append(indent).append("</map>\r"); //$NON-NLS-1$ return sb.toString(); } private static void objToXML(final Object obj, final StringBuffer sb, final String newIndent) { if (obj instanceof String) { sb.append(newIndent).append("<string-value>\r"); //$NON-NLS-1$ sb.append(newIndent).append("<![CDATA[").append((String) obj).append("]]>\r"); //$NON-NLS-1$ //$NON-NLS-2$ sb.append(newIndent).append("</string-value>\r"); //$NON-NLS-1$ } else if (obj instanceof StringBuffer) { sb.append(newIndent).append("<stringbuffer-value>\r"); //$NON-NLS-1$ sb.append(newIndent).append("<![CDATA[").append(obj.toString()).append("]]>\r"); //$NON-NLS-1$ //$NON-NLS-2$ sb.append(newIndent).append("</stringbuffer-value>\r"); //$NON-NLS-1$ } else if (obj instanceof BigDecimal) { sb.append(newIndent).append("<bigdecimal-value>").append(obj.toString()).append("</bigdecimal-value>\r"); //$NON-NLS-1$ //$NON-NLS-2$ } else if (obj instanceof Date) { SimpleDateFormat fmt = new SimpleDateFormat(); fmt.setTimeZone(TimeZone.getTimeZone("GMT")); //$NON-NLS-1$ sb.append(newIndent).append("<date-value>"); //$NON-NLS-1$ sb.append(fmt.format((Date) obj)).append("</date-value>\r"); //$NON-NLS-1$ } else if (obj instanceof Long) { sb.append(newIndent).append("<long-value>"); //$NON-NLS-1$ sb.append(obj.toString()).append("</long-value>\r"); //$NON-NLS-1$ } else if (obj instanceof Map) { sb.append(newIndent).append("<map-value>\r"); //$NON-NLS-1$ sb.append(XmlHelper.mapToXML((Map) obj, newIndent)); sb.append(newIndent).append("</map-value>\r"); //$NON-NLS-1$ } else if (obj instanceof List) { sb.append(newIndent).append("<list-value>\r"); //$NON-NLS-1$ sb.append(XmlHelper.listToXML((List) obj, newIndent)); sb.append(newIndent).append("</list-value>\r"); //$NON-NLS-1$ } else { throw new UnsupportedOperationException(Messages.getInstance().getErrorString( "XMLUTL.ERROR_0012_DATA_TYPE", obj.getClass().getName())); //$NON-NLS-1$ } } public static void decode(final String[] strings) { if (strings != null) { for (int i = 0; i < strings.length; ++i) { strings[i] = XmlHelper.decode(strings[i]); } } } public static String decode(String string) { // TODO replace this is a more robust encoder if (string != null) { string = string.replaceAll("<", "<") //$NON-NLS-1$ //$NON-NLS-2$ .replaceAll(">", ">") //$NON-NLS-1$ //$NON-NLS-2$ .replaceAll("'", "'") //$NON-NLS-1$ //$NON-NLS-2$ .replaceAll(""", "\"") //$NON-NLS-1$ //$NON-NLS-2$ .replaceAll("&", "&"); //$NON-NLS-1$ //$NON-NLS-2$ // DO THE & LAST!!!! } return string; } public static void encode(final String[] strings) { if (strings != null) { for (int i = 0; i < strings.length; ++i) { strings[i] = XmlHelper.encode(strings[i]); } } } public static String encode(final String string) { return StringEscapeUtils.escapeXml(string); } private static final int BUFF_SIZE = 512; public static String getEncoding(final File f) throws IOException { char[] cbuf = new char[XmlHelper.BUFF_SIZE]; Reader rdr = null; try { rdr = new FileReader(f); rdr.read(cbuf); } finally { rdr.close(); } String strEnc = String.valueOf(cbuf); return XmlHelper.getEncoding(strEnc); } public static String getEncoding(final InputStream inStream) throws IOException { String encodingPI = XmlHelper.readEncodingProcessingInstruction(inStream); return XmlHelper.getEncoding(encodingPI); } /** * Find the character encoding specification in the xml String. If it exists, * return the character encoding. Otherwise, return null. * * @param xml * String containing the xml * @return String containing the character encoding in the xml processing * instruction if it exists, else null. */ public static String getEncoding(final String xml) { Matcher m = XmlHelper.RE_ENCODING.matcher(xml); boolean bMatches = m.matches(); if (bMatches && (m.groupCount() == 2)) { return m.group(2); } // no encoding found return null; } /** * Find the character encoding specification in the xml String. If it exists, return * the character encoding. Otherwise, return the system encoding. * * @param xml String containing the xml * @param defaultEncoding Encoding to use if there is no encoding in the xml document * @return String containing the character encoding in the xml processing instruction, * or defaultEncoding if there is no encoding in the xml document. If * defaultEncoding is also null, then it returns the value in * LocaleHelper.getSystemEncoding(). * if it exists, else the system encoding. */ public static String getEncoding(final String xml, final String defaultEncoding) { String enc = XmlHelper.getEncoding(xml); return null != enc ? enc : (defaultEncoding != null ? defaultEncoding : LocaleHelper.getSystemEncoding()); } /** * WARNING: if the <param>inStream</param> instance does not support mark/reset, * when this method returns, subsequent reads on <param>inStream</param> will be * 256 bytes into the stream. This may not be the expected behavior. FileInputStreams * are an example of an InputStream that does not support mark/reset. InputStreams * that do support mark/reset will be reset to the beginning of the stream * when this method returns. * * @param inStream * @return * @throws IOException */ public static String readEncodingProcessingInstruction(final InputStream inStream) throws IOException { final int BUFF_SZ = 256; if (inStream.markSupported()) { inStream.mark(BUFF_SZ + 1); // BUFF_SZ+1 forces mark to NOT be forgotten } byte[] buf = new byte[BUFF_SZ]; int totalBytesRead = 0; int bytesRead; do { bytesRead = inStream.read(buf, totalBytesRead, BUFF_SZ - totalBytesRead); if (bytesRead == -1) { break; } totalBytesRead += bytesRead; } while (totalBytesRead < BUFF_SZ); if (inStream.markSupported()) { inStream.reset(); } return new String(buf); } /** * Use the transform specified by xslName and transform the document specified * by docInStrm, and return the resulting document. * * @param xslName String containing the name of a file in the repository containing the xsl transform * @param xslPath String containing the path to the file identifyied by <code>xslName</code> * @param uri String containing the URI of a resource containing the document to be transformed * @param params Map of properties to set on the transform * @param session IPentahoSession containing a URIResolver instance to resolve URI's in the output document. * * @return StringBuffer containing the XML results of the transform. Null if there was an error. * @throws TransformerException If attempt to transform the document fails. */ public static final StringBuffer transformXml(final String xslName, final String xslPath, final String strDocument, final Map params, final IDocumentResourceLoader loader) throws TransformerException { InputStream inStrm = null; try { // Read the encoding from the XML file - see BISERVER-895 String encoding = XmlHelper.getEncoding(strDocument, null); inStrm = new ByteArrayInputStream(strDocument.getBytes(encoding)); } catch (UnsupportedEncodingException e) { if (XmlHelper.logger.isErrorEnabled()) { XmlHelper.logger.error(e); } } StringBuffer result = XmlHelper.transformXml(xslName, xslPath, inStrm, params, loader); FileHelper.closeInputStream(inStrm); return result; } /** * Use the transform specified by xslPath and xslName and transform the document specified * by docInStrm, and return the resulting document. * * @param xslSrc StreamSrc containing the xsl transform * @param docSrc StreamSrc containing the document to be transformed * @param params Map of properties to set on the transform * @param session IPentahoSession containing a URIResolver instance to resolve URI's in the output document. * * @return StringBuffer containing the XML results of the transform. Null if there was an error. * @throws TransformerException If attempt to transform the document fails. */ @SuppressWarnings({"unchecked"}) public static final StringBuffer transformXml(final String xslName, final String xslPath, final InputStream docInStrm, Map params, final IDocumentResourceLoader loader) throws TransformerException { StringBuffer result = null; InputStream xslInStrm = XmlHelper.getLocalizedXsl(xslPath, xslName, loader); if (null == xslInStrm) { Logger.error(XmlHelper.class.getName(), Messages.getInstance().getErrorString("XmlHelper.ERROR_0003_NULL_XSL_SOURCE")); //$NON-NLS-1$ } else if (null == docInStrm) { Logger.error(XmlHelper.class.getName(), Messages.getInstance().getErrorString("XmlHelper.ERROR_0004_NULL_DOCUMENT")); //$NON-NLS-1$ } else { // at this point, we have both of our InputStreams // Add encoding for any xsl that may set/use it if (params == null) { params = new HashMap(); } params.put("output-encoding", LocaleHelper.getSystemEncoding()); //$NON-NLS-1$ try { result = XmlHelper.transformXml(xslInStrm, docInStrm, params, loader); } catch (TransformerException e) { Logger.error(XmlHelper.class.getName(), Messages.getInstance().getErrorString( "XmlHelper.ERROR_0006_TRANSFORM_XML_ERROR", e.getMessage(), xslName), e); //$NON-NLS-1$ throw e; } finally { FileHelper.closeInputStream(xslInStrm); } } return result; } /** * Use the transform specified by xslSrc and transform the document specified * by docSrc, and return the resulting document. * * @param xslInStream * InputStream containing the xsl transform * @param docInStrm * InputStream containing the document to be transformed * @param params * Map of properties to set on the transform * @param resolver * URIResolver instance to resolve URI's in the output document. * * @return StringBuffer containing the XML results of the transform * @throws TransformerConfigurationException * if the TransformerFactory fails to create a Transformer. * @throws TransformerException * if actual transform fails. */ public static final StringBuffer transformXml(final InputStream xslInStream, final InputStream docInStrm, final Map params, final URIResolver resolver) throws TransformerConfigurationException, TransformerException { StreamSource xslSrc = new StreamSource(xslInStream); StreamSource docSrc = new StreamSource(docInStrm); return XmlHelper.transformXml(xslSrc, docSrc, params, resolver); } /** * Use the transform specified by xslSrc and transform the document specified * by docSrc, and return the resulting document. * * @param xslSrc * StreamSrc containing the xsl transform * @param docSrc * StreamSrc containing the document to be transformed * @param params * Map of properties to set on the transform * @param resolver * URIResolver instance to resolve URI's in the output document. * * @return StringBuffer containing the XML results of the transform * @throws TransformerConfigurationException * if the TransformerFactory fails to create a Transformer. * @throws TransformerException * if actual transform fails. */ protected static final StringBuffer transformXml(final StreamSource xslSrc, final StreamSource docSrc, final Map params, final URIResolver resolver) throws TransformerConfigurationException, TransformerException { StringBuffer sb = null; StringWriter writer = new StringWriter(); TransformerFactory tf = TransformerFactory.newInstance(); if (null != resolver) { tf.setURIResolver(resolver); } // TODO need to look into compiling the XSLs... Transformer t = tf.newTransformer(xslSrc); // can throw // TransformerConfigurationException // Start the transformation if (params != null) { Set keys = params.keySet(); Iterator it = keys.iterator(); String key, val; while (it.hasNext()) { key = (String) it.next(); val = (String) params.get(key); if (val != null) { t.setParameter(key, val); } } } t.transform(docSrc, new StreamResult(writer)); // can throw // TransformerException sb = writer.getBuffer(); return sb; } /** * Get the File object corresponding to the path, filename (xslName), and * locale. The path is relative to the solution path. * * @param path * @param xslName * @return */ public static final InputStream getLocalizedXsl(final String path, final String xslName, final IDocumentResourceLoader loader) { String fullPath = null; String defaultPath = null; InputStream file = null; if (null != path) { // try to find it on the specified path fullPath = (path + File.separator + xslName).replace('\\', '/'); file = XmlHelper.getLocalizedFile(fullPath, LocaleHelper.getLocale(), loader); } if (null == file) { // didn't find the file, let's try default path defaultPath = (XmlHelper.DEFAULT_XSL_FOLDER + xslName).replace('\\', '/'); file = XmlHelper.getLocalizedFile(defaultPath, LocaleHelper.getLocale(), loader); } if (null == file) { // we should not get this far... Logger.error(XmlHelper.class.getName(), Messages.getInstance().getErrorString( "XmlHelper.ERROR_0011_TRANSFORM_XSL_DOES_NOT_EXIST", xslName, fullPath, defaultPath)); //$NON-NLS-1$ } return file; } public static InputStream getLocalizedFile(final String fullPath, final Locale locale, final IDocumentResourceLoader loader) { String language = locale.getLanguage(); String country = locale.getCountry(); String variant = locale.getVariant(); // File file = new File(fullPath); String fileName = fullPath; int dotIndex = fileName.indexOf('.'); String baseName = dotIndex == -1 ? fileName : fileName.substring(0, dotIndex); // These two lines fix an index out of bounds String extension = dotIndex == -1 ? "" : fileName.substring(dotIndex); // Exception that occurs when a filename has no extension //$NON-NLS-1$ InputStream in = null; try { if (!variant.equals("")) { //$NON-NLS-1$ in = loader.loadXsl(baseName + "_" + language + "_" + country + "_" + variant + extension); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } if (in == null) { in = loader.loadXsl(baseName + "_" + language + "_" + country + extension); //$NON-NLS-1$//$NON-NLS-2$ } if (in == null) { in = loader.loadXsl(baseName + "_" + language + extension); //$NON-NLS-1$ } if (in == null) { in = loader.loadXsl(baseName + extension); } } catch (Exception e) { Logger.error(XmlHelper.class.getName(), "Error loading localized file: " + fullPath); //$NON-NLS-1$ } return in; } /** * * @param version * @param encoding * @return String Xml Processing instruction text with the specified version (usually 1.0) * and encoding (for instance, UTF-8) */ public static String createXmlProcessingInstruction(final String version, final String encoding) { return "<?xml version=\"" + version + "\" encoding = \"" + encoding + "\" ?>"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } }