001/*
002// This software is subject to the terms of the Eclipse Public License v1.0
003// Agreement, available at the following URL:
004// http://www.eclipse.org/legal/epl-v10.html.
005// You must accept the terms of that agreement to use this software.
006//
007// Copyright (C) 2005-2011 Pentaho and others
008// All Rights Reserved.
009*/
010package mondrian.tui;
011
012import mondrian.olap.Util;
013
014import org.apache.xerces.dom.DocumentImpl;
015import org.apache.xerces.impl.Constants;
016import org.apache.xerces.parsers.DOMParser;
017import org.apache.xml.serialize.OutputFormat;
018import org.apache.xml.serialize.XMLSerializer;
019import org.apache.xpath.domapi.XPathEvaluatorImpl;
020
021import org.w3c.dom.*;
022import org.w3c.dom.xpath.*;
023import org.xml.sax.*;
024
025import java.io.*;
026import java.util.ArrayList;
027import java.util.List;
028import javax.xml.parsers.*;
029import javax.xml.transform.*;
030import javax.xml.transform.dom.DOMResult;
031import javax.xml.transform.dom.DOMSource;
032
033/**
034 * Some XML parsing, validation and transform utility methods used
035 * to valiate XMLA responses.
036 *
037 * @author Richard M. Emberson
038 */
039public class XmlUtil {
040
041    public static final String LINE_SEP =
042        System.getProperty("line.separator", "\n");
043
044    public static final String SOAP_PREFIX = "SOAP-ENV";
045    public static final String XSD_PREFIX = "xsd";
046
047    public static final String XMLNS = "xmlns";
048
049    public static final String NAMESPACES_FEATURE_ID =
050        "http://xml.org/sax/features/namespaces";
051    public static final String VALIDATION_FEATURE_ID =
052        "http://xml.org/sax/features/validation";
053
054    public static final String SCHEMA_VALIDATION_FEATURE_ID =
055        "http://apache.org/xml/features/validation/schema";
056    public static final String FULL_SCHEMA_VALIDATION_FEATURE_ID =
057        "http://apache.org/xml/features/validation/schema-full-checking";
058
059    public static final String DEFER_NODE_EXPANSION =
060        "http://apache.org/xml/features/dom/defer-node-expansion";
061
062    public static final String SCHEMA_LOCATION =
063        Constants.XERCES_PROPERTY_PREFIX + Constants.SCHEMA_LOCATION;
064
065    /**
066     * This is the xslt that can extract the "data" part of a SOAP
067     * XMLA response.
068     */
069    public static final String getSoapXmlaXds2xd(String xmlaPrefix) {
070        return
071            "<?xml version='1.0'?>" + LINE_SEP
072            + "<xsl:stylesheet " + LINE_SEP
073            + "xmlns:xsl='http://www.w3.org/1999/XSL/Transform' " + LINE_SEP
074            + "xmlns:xalan='http://xml.apache.org/xslt' " + LINE_SEP
075            + "xmlns:xsd='http://www.w3.org/2001/XMLSchema' " + LINE_SEP
076            + "xmlns:ROW='urn:schemas-microsoft-com:xml-analysis:rowset' "
077            + LINE_SEP
078            + "xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' "
079            + LINE_SEP
080            + "xmlns:xmla='urn:schemas-microsoft-com:xml-analysis' " + LINE_SEP
081            + "version='1.0' " + LINE_SEP
082            + "> " + LINE_SEP
083            + "<xsl:output method='xml'  " + LINE_SEP
084            + "encoding='UTF-8' " + LINE_SEP
085            + "indent='yes'  " + LINE_SEP
086            + "xalan:indent-amount='2'/> " + LINE_SEP
087            + "  " + LINE_SEP
088            + "<!-- consume '/' and apply --> " + LINE_SEP
089            + "<xsl:template match='/'> " + LINE_SEP
090            + "<xsl:apply-templates/> " + LINE_SEP
091            + "</xsl:template> " + LINE_SEP
092            + "<!-- consume 'Envelope' and apply --> " + LINE_SEP
093            + "<xsl:template match='SOAP-ENV:Envelope'> " + LINE_SEP
094            + "<xsl:apply-templates/> " + LINE_SEP
095            + "</xsl:template> " + LINE_SEP
096            + "<!-- consume 'Header' --> " + LINE_SEP
097            + "<xsl:template match='SOAP-ENV:Header'> " + LINE_SEP
098            + "</xsl:template> " + LINE_SEP
099            + "<!-- consume 'Body' and apply --> " + LINE_SEP
100            + "<xsl:template match='SOAP-ENV:Body'> " + LINE_SEP
101                + "<xsl:apply-templates/> " + LINE_SEP
102            + "</xsl:template> " + LINE_SEP
103            + "<!-- consume 'DiscoverResponse' and apply --> " + LINE_SEP
104            + "<xsl:template match='" + xmlaPrefix + ":DiscoverResponse'> "
105            + LINE_SEP
106            + "<xsl:apply-templates/> " + LINE_SEP
107            + "</xsl:template> " + LINE_SEP
108            + "<!-- consume 'return' and apply --> " + LINE_SEP
109            + "<xsl:template match='" + xmlaPrefix + ":return'> " + LINE_SEP
110            + "<xsl:apply-templates/> " + LINE_SEP
111            + "</xsl:template> " + LINE_SEP
112            + "<!-- consume 'xsd:schema' --> " + LINE_SEP
113            + "<xsl:template match='xsd:schema'> " + LINE_SEP
114            + "</xsl:template> " + LINE_SEP
115            + "<!-- copy everything else --> " + LINE_SEP
116            + "<xsl:template match='*|@*'> " + LINE_SEP
117            + "<xsl:copy> " + LINE_SEP
118            + "<xsl:apply-templates select='@*|node()'/> " + LINE_SEP
119            + "</xsl:copy> " + LINE_SEP
120            + "</xsl:template> " + LINE_SEP
121            + "</xsl:stylesheet>";
122    }
123
124    /**
125     * This is the xslt that can extract the "schema" part of a SOAP
126     * XMLA response.
127     */
128    public static final String getSoapXmlaXds2xs(String xmlaPrefix) {
129        return
130            "<?xml version='1.0'?> " + LINE_SEP
131            + "<xsl:stylesheet  " + LINE_SEP
132            + "xmlns:xsl='http://www.w3.org/1999/XSL/Transform'  " + LINE_SEP
133            + "xmlns:xalan='http://xml.apache.org/xslt' " + LINE_SEP
134            + "xmlns:xsd='http://www.w3.org/2001/XMLSchema' " + LINE_SEP
135            + "xmlns:ROW='urn:schemas-microsoft-com:xml-analysis:rowset' "
136            + LINE_SEP
137            + "xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/'  "
138            + LINE_SEP
139            + "xmlns:xmla='urn:schemas-microsoft-com:xml-analysis' " + LINE_SEP
140            + "version='1.0' " + LINE_SEP
141            + "> " + LINE_SEP
142            + "<xsl:output method='xml'  " + LINE_SEP
143            + "encoding='UTF-8' " + LINE_SEP
144            + "indent='yes'  " + LINE_SEP
145            + "xalan:indent-amount='2'/> " + LINE_SEP
146            + "<!-- consume '/' and apply --> " + LINE_SEP
147            + "<xsl:template match='/'> " + LINE_SEP
148            + "<xsl:apply-templates/> " + LINE_SEP
149            + "</xsl:template> " + LINE_SEP
150            + "<!-- consume 'Envelope' and apply --> " + LINE_SEP
151            + "<xsl:template match='SOAP-ENV:Envelope'> " + LINE_SEP
152            + "<xsl:apply-templates/> " + LINE_SEP
153            + "</xsl:template> " + LINE_SEP
154            + "<!-- consume 'Header' --> " + LINE_SEP
155            + "<xsl:template match='SOAP-ENV:Header'> " + LINE_SEP
156            + "</xsl:template> " + LINE_SEP
157            + "<!-- consume 'Body' and apply --> " + LINE_SEP
158            + "<xsl:template match='SOAP-ENV:Body'> " + LINE_SEP
159            + "<xsl:apply-templates/> " + LINE_SEP
160            + "</xsl:template> " + LINE_SEP
161            + "<!-- consume 'DiscoverResponse' and apply --> " + LINE_SEP
162            + "<xsl:template match='" + xmlaPrefix + ":DiscoverResponse'> "
163            + LINE_SEP
164            + "<xsl:apply-templates/> " + LINE_SEP
165            + "</xsl:template> " + LINE_SEP
166            + "<!-- consume 'return' and apply --> " + LINE_SEP
167            + "<xsl:template match='" + xmlaPrefix + ":return'> " + LINE_SEP
168            + "<xsl:apply-templates/> " + LINE_SEP
169            + "</xsl:template> " + LINE_SEP
170            + "<!-- consume 'root' and apply --> " + LINE_SEP
171            + "<xsl:template match='ROW:root'> " + LINE_SEP
172            + "<xsl:apply-templates/> " + LINE_SEP
173            + "</xsl:template> " + LINE_SEP
174            + "<!-- consume 'row' --> " + LINE_SEP
175            + "<xsl:template match='ROW:row'> " + LINE_SEP
176            + "</xsl:template> " + LINE_SEP
177            + "<!-- copy everything else --> " + LINE_SEP
178            + "<xsl:template match='*|@*'> " + LINE_SEP
179            + "<xsl:copy> " + LINE_SEP
180            + "<xsl:apply-templates select='@*|node()'/> " + LINE_SEP
181            + "</xsl:copy> " + LINE_SEP
182            + "</xsl:template> " + LINE_SEP
183            + "</xsl:stylesheet>";
184    }
185
186    /**
187     * This is the xslt that can extract the "data" part of a XMLA response.
188     */
189    public static final String getXmlaXds2xd(String ns) {
190        String xmlaPrefix = (ns == null) ? "" : (ns + ":");
191        return
192            "<?xml version='1.0'?>" + LINE_SEP
193            + "<xsl:stylesheet " + LINE_SEP
194            + "xmlns:xsl='http://www.w3.org/1999/XSL/Transform' " + LINE_SEP
195            + "xmlns:xalan='http://xml.apache.org/xslt' " + LINE_SEP
196            + "xmlns:xsd='http://www.w3.org/2001/XMLSchema' " + LINE_SEP
197            + "xmlns:ROW='urn:schemas-microsoft-com:xml-analysis:rowset' "
198            + LINE_SEP
199            + "xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' "
200            + LINE_SEP
201            + "xmlns:xmla='urn:schemas-microsoft-com:xml-analysis' " + LINE_SEP
202            + "version='1.0' " + LINE_SEP
203            + "> " + LINE_SEP
204            + "<xsl:output method='xml'  " + LINE_SEP
205            + "encoding='UTF-8' " + LINE_SEP
206            + "indent='yes'  " + LINE_SEP
207            + "xalan:indent-amount='2'/> " + LINE_SEP
208            + "  " + LINE_SEP
209            + "<!-- consume '/' and apply --> " + LINE_SEP
210            + "<xsl:template match='/'> " + LINE_SEP
211            + "<xsl:apply-templates/> " + LINE_SEP
212            + "</xsl:template> " + LINE_SEP
213            + "<!-- consume 'DiscoverResponse' and apply --> " + LINE_SEP
214            + "<xsl:template match='" + xmlaPrefix + "DiscoverResponse'> "
215            + LINE_SEP
216            + "<xsl:apply-templates/> " + LINE_SEP
217            + "</xsl:template> " + LINE_SEP
218            + "<!-- consume 'return' and apply --> " + LINE_SEP
219            + "<xsl:template match='" + xmlaPrefix + "return'> " + LINE_SEP
220            + "<xsl:apply-templates/> " + LINE_SEP
221            + "</xsl:template> " + LINE_SEP
222            + "<!-- consume 'xsd:schema' --> " + LINE_SEP
223            + "<xsl:template match='xsd:schema'> " + LINE_SEP
224            + "</xsl:template> " + LINE_SEP
225            + "<!-- copy everything else --> " + LINE_SEP
226            + "<xsl:template match='*|@*'> " + LINE_SEP
227            + "<xsl:copy> " + LINE_SEP
228            + "<xsl:apply-templates select='@*|node()'/> " + LINE_SEP
229            + "</xsl:copy> " + LINE_SEP
230            + "</xsl:template> " + LINE_SEP
231            + "</xsl:stylesheet>";
232    }
233
234    /**
235     * This is the xslt that can extract the "schema" part of a XMLA response.
236     */
237    public static final String getXmlaXds2xs(String ns) {
238        String xmlaPrefix = (ns == null) ? "" : (ns + ":");
239        return
240            "<?xml version='1.0'?> " + LINE_SEP
241            + "<xsl:stylesheet  " + LINE_SEP
242            + "xmlns:xsl='http://www.w3.org/1999/XSL/Transform'  " + LINE_SEP
243            + "xmlns:xalan='http://xml.apache.org/xslt' " + LINE_SEP
244            + "xmlns:xsd='http://www.w3.org/2001/XMLSchema' " + LINE_SEP
245            + "xmlns:ROW='urn:schemas-microsoft-com:xml-analysis:rowset' "
246            + LINE_SEP
247            + "xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/'  "
248            + LINE_SEP
249            + "xmlns:xmla='urn:schemas-microsoft-com:xml-analysis' " + LINE_SEP
250            + "version='1.0' " + LINE_SEP
251            + "> " + LINE_SEP
252            + "<xsl:output method='xml'  " + LINE_SEP
253            + "encoding='UTF-8' " + LINE_SEP
254            + "indent='yes'  " + LINE_SEP
255            + "xalan:indent-amount='2'/> " + LINE_SEP
256            + "<!-- consume '/' and apply --> " + LINE_SEP
257            + "<xsl:template match='/'> " + LINE_SEP
258            + "<xsl:apply-templates/> " + LINE_SEP
259            + "</xsl:template> " + LINE_SEP
260            + "<!-- consume 'DiscoverResponse' and apply --> " + LINE_SEP
261            + "<xsl:template match='" + xmlaPrefix + "DiscoverResponse'> "
262            + LINE_SEP
263            + "<xsl:apply-templates/> " + LINE_SEP
264            + "</xsl:template> " + LINE_SEP
265            + "<!-- consume 'return' and apply --> " + LINE_SEP
266            + "<xsl:template match='" + xmlaPrefix + "return'> " + LINE_SEP
267            + "<xsl:apply-templates/> " + LINE_SEP
268            + "</xsl:template> " + LINE_SEP
269            + "<!-- consume 'root' and apply --> " + LINE_SEP
270            + "<xsl:template match='ROW:root'> " + LINE_SEP
271            + "<xsl:apply-templates/> " + LINE_SEP
272            + "</xsl:template> " + LINE_SEP
273            + "<!-- consume 'row' --> " + LINE_SEP
274            + "<xsl:template match='ROW:row'> " + LINE_SEP
275            + "</xsl:template> " + LINE_SEP
276            + "<!-- copy everything else --> " + LINE_SEP
277            + "<xsl:template match='*|@*'> " + LINE_SEP
278            + "<xsl:copy> " + LINE_SEP
279            + "<xsl:apply-templates select='@*|node()'/> " + LINE_SEP
280            + "</xsl:copy> " + LINE_SEP
281            + "</xsl:template> " + LINE_SEP
282            + "</xsl:stylesheet>";
283    }
284
285    /**
286     * Error handler plus helper methods.
287     */
288    public static class SaxErrorHandler implements ErrorHandler {
289        public static final String WARNING_STRING        = "WARNING";
290        public static final String ERROR_STRING          = "ERROR";
291        public static final String FATAL_ERROR_STRING    = "FATAL";
292
293        // DOMError values
294        public static final short SEVERITY_WARNING      = 1;
295        public static final short SEVERITY_ERROR        = 2;
296        public static final short SEVERITY_FATAL_ERROR  = 3;
297
298        public void printErrorInfos(PrintStream out) {
299            if (errors != null) {
300                for (ErrorInfo error : errors) {
301                    out.println(formatErrorInfo(error));
302                }
303            }
304        }
305
306        public static String formatErrorInfos(SaxErrorHandler saxEH) {
307            if (! saxEH.hasErrors()) {
308                return "";
309            }
310            StringBuilder buf = new StringBuilder(512);
311            for (ErrorInfo error : saxEH.getErrors()) {
312                buf.append(formatErrorInfo(error));
313                buf.append(LINE_SEP);
314            }
315            return buf.toString();
316        }
317
318        public static String formatErrorInfo(ErrorInfo ei) {
319            StringBuilder buf = new StringBuilder(128);
320            buf.append("[");
321            switch (ei.severity) {
322            case SEVERITY_WARNING:
323                buf.append(WARNING_STRING);
324                break;
325            case SEVERITY_ERROR:
326                buf.append(ERROR_STRING);
327                break;
328            case SEVERITY_FATAL_ERROR:
329                buf.append(FATAL_ERROR_STRING);
330                break;
331            }
332            buf.append(']');
333            String systemId = ei.exception.getSystemId();
334            if (systemId != null) {
335                int index = systemId.lastIndexOf('/');
336                if (index != -1) {
337                    systemId = systemId.substring(index + 1);
338                }
339                buf.append(systemId);
340            }
341            buf.append(':');
342            buf.append(ei.exception.getLineNumber());
343            buf.append(':');
344            buf.append(ei.exception.getColumnNumber());
345            buf.append(": ");
346            buf.append(ei.exception.getMessage());
347            return buf.toString();
348        }
349
350        public static class ErrorInfo {
351            public SAXParseException exception;
352            public short severity;
353            ErrorInfo(short severity, SAXParseException exception) {
354                this.severity = severity;
355                this.exception = exception;
356            }
357        }
358
359        private List<ErrorInfo> errors;
360
361        public SaxErrorHandler() {
362        }
363
364        public List<ErrorInfo> getErrors() {
365            return this.errors;
366        }
367
368        public boolean hasErrors() {
369            return (this.errors != null);
370        }
371
372        public void warning(SAXParseException exception) throws SAXException {
373            addError(new ErrorInfo(SEVERITY_WARNING, exception));
374        }
375
376        public void error(SAXParseException exception) throws SAXException {
377            addError(new ErrorInfo(SEVERITY_ERROR, exception));
378        }
379
380        public void fatalError(SAXParseException exception)
381            throws SAXException
382        {
383            addError(new ErrorInfo(SEVERITY_FATAL_ERROR, exception));
384        }
385
386        protected void addError(ErrorInfo ei) {
387            if (this.errors == null) {
388                this.errors = new ArrayList<ErrorInfo>();
389            }
390            this.errors.add(ei);
391        }
392
393        public String getFirstError() {
394            return (hasErrors())
395                ? formatErrorInfo(errors.get(0))
396                : "";
397        }
398    }
399
400    public static Document newDocument(Node firstElement, boolean deepcopy) {
401        Document newDoc = new DocumentImpl();
402        newDoc.appendChild(newDoc.importNode(firstElement, deepcopy));
403        return newDoc;
404    }
405
406
407    //////////////////////////////////////////////////////////////////////////
408    // parse
409    //////////////////////////////////////////////////////////////////////////
410
411    /**
412     * Get your non-cached DOM parser which can be configured to do schema
413     * based validation of the instance Document.
414     *
415     */
416    public static DOMParser getParser(
417        String schemaLocationPropertyValue,
418        EntityResolver entityResolver,
419        boolean validate)
420        throws SAXNotRecognizedException, SAXNotSupportedException
421    {
422        boolean doingValidation =
423            (validate || (schemaLocationPropertyValue != null));
424
425        DOMParser parser = new DOMParser();
426
427        parser.setEntityResolver(entityResolver);
428        parser.setErrorHandler(new SaxErrorHandler());
429        parser.setFeature(DEFER_NODE_EXPANSION, false);
430        parser.setFeature(NAMESPACES_FEATURE_ID, true);
431        parser.setFeature(SCHEMA_VALIDATION_FEATURE_ID, doingValidation);
432        parser.setFeature(VALIDATION_FEATURE_ID, doingValidation);
433
434        if (schemaLocationPropertyValue != null) {
435            parser.setProperty(
436                SCHEMA_LOCATION,
437                schemaLocationPropertyValue.replace('\\', '/'));
438        }
439
440        return parser;
441    }
442
443    /**
444     * See if the DOMParser after parsing a Document has any errors and,
445     * if so, throw a RuntimeException exception containing the errors.
446     *
447     */
448    private static void checkForParseError(
449        final DOMParser parser,
450        Throwable t)
451    {
452        if (Util.IBM_JVM) {
453            // IBM JDK returns lots of errors. Not sure whether they are
454            // real errors, but ignore for now.
455            return;
456        }
457
458        final ErrorHandler errorHandler = parser.getErrorHandler();
459        if (errorHandler instanceof SaxErrorHandler) {
460            final SaxErrorHandler saxEH = (SaxErrorHandler) errorHandler;
461            final List<SaxErrorHandler.ErrorInfo> errors = saxEH.getErrors();
462
463            if (errors != null && errors.size() > 0) {
464                String errorStr = SaxErrorHandler.formatErrorInfos(saxEH);
465                throw new RuntimeException(errorStr, t);
466            }
467        } else {
468            System.out.println("errorHandler=" + errorHandler);
469        }
470    }
471
472    private static void checkForParseError(final DOMParser parser) {
473        checkForParseError(parser, null);
474    }
475
476
477    /**
478     * Parse a String into a Document (no validation).
479     *
480     */
481    public static Document parseString(String s)
482        throws SAXException, IOException
483    {
484        // Hack to workaround bug #622 until 1.4.2_08
485        final int length = s.length();
486
487        if (length > 16777216 && length % 4 == 1) {
488            final StringBuilder buf = new StringBuilder(length + 1);
489
490            buf.append(s);
491            buf.append('\n');
492            s = buf.toString();
493        }
494
495        return XmlUtil.parse(s.getBytes());
496    }
497
498    /**
499     * Parse a byte array into a Document (no validation).
500     *
501     */
502    public static Document parse(byte[] bytes)
503        throws SAXException, IOException
504    {
505        return XmlUtil.parse(new ByteArrayInputStream(bytes));
506    }
507
508    public static Document parse(File file)
509        throws SAXException, IOException
510    {
511        return parse(new FileInputStream(file));
512    }
513
514    /**
515     * Parse a stream into a Document (no validation).
516     *
517     */
518    public static Document parse(InputStream in)
519        throws SAXException, IOException
520    {
521        InputSource source = new InputSource(in);
522
523        DOMParser parser = XmlUtil.getParser(null, null, false);
524        try {
525            parser.parse(source);
526            checkForParseError(parser);
527        } catch (SAXParseException ex) {
528            checkForParseError(parser, ex);
529        }
530
531        Document document = parser.getDocument();
532        return document;
533    }
534
535
536    //////////////////////////////////////////////////////////////////////////
537    // xpath
538    //////////////////////////////////////////////////////////////////////////
539    /**
540     * Create a context document for use in performing XPath operations.
541     * An array of prefix/namespace-urls are provided as input. These
542     * namespace-urls should be all of those that will appear in the
543     * document against which an xpath is to be applied.
544     * Importantly, it is, in fact, each element of the Document that
545     * has a default namespace these namespaces MUST have
546     * prefix/namespace-urls pairs in the context document and the prefix
547     * provided MUST also be used in the xpath.
548     * Elements with explicit namespaces don't have to have pairs in the
549     * context Document as long as the xpath uses the same prefixes
550     * that appear in the target Document.
551     *
552     */
553    public static Document createContextDocument(String[][] nsArray)
554        throws SAXException, IOException
555    {
556        StringBuilder buf = new StringBuilder(256);
557        buf.append("<?xml version='1.0' encoding='utf-8'?>");
558        buf.append("<DOES_NOT_MATTER");
559        for (int i = 0; i < nsArray.length; i++) {
560            String prefix = nsArray[i][0];
561            String nsURI = nsArray[i][1];
562
563            buf.append(" xmlns:");
564            buf.append(prefix);
565            buf.append("=\"");
566            buf.append(nsURI);
567            buf.append("\"");
568        }
569        buf.append(" />");
570
571        String docStr = buf.toString();
572        return parseString(docStr);
573    }
574
575    public static String makeSoapPath() {
576        return XmlUtil.makeSoapPath(SOAP_PREFIX);
577    }
578
579    // '/soapX:Envelope/soapX:Body/*'
580    public static String makeSoapPath(String prefix) {
581        StringBuilder buf = new StringBuilder(20);
582        buf.append('/');
583        if (prefix != null) {
584            buf.append(prefix);
585            buf.append(':');
586        }
587        buf.append("Envelope");
588        buf.append('/');
589        if (prefix != null) {
590            buf.append(prefix);
591            buf.append(':');
592        }
593        buf.append("Body");
594        buf.append('/');
595        buf.append('*');
596
597        return buf.toString();
598    }
599
600    public static String makeRootPathInSoapBody() {
601        return makeRootPathInSoapBody("xmla", XSD_PREFIX);
602    }
603
604    // '/xmla:DiscoverResponse/xmla:return/ROW/root/*'
605    public static String makeRootPathInSoapBody(
606        String xmlaPrefix,
607        String xsdPrefix)
608    {
609        StringBuilder buf = new StringBuilder(20);
610        buf.append("/").append(xmlaPrefix).append(":DiscoverResponse");
611        buf.append("/").append(xmlaPrefix).append(":return");
612        buf.append("/ROW:root");
613        buf.append('/');
614        buf.append('*');
615/*
616        if (xsdPrefix != null) {
617            buf.append(xsdPrefix);
618            buf.append(':');
619        }
620        buf.append("schema");
621*/
622
623        return buf.toString();
624    }
625
626    public static String selectAsString(
627        Node node,
628        String xpath)
629        throws XPathException
630    {
631        return XmlUtil.selectAsString(node, xpath, node);
632    }
633
634    public static String selectAsString(
635        Node node,
636        String xpath,
637        Node namespaceNode)
638        throws XPathException
639    {
640        XPathResult xpathResult = XmlUtil.select(node, xpath, namespaceNode);
641        return XmlUtil.convertToString(xpathResult, false);
642    }
643
644    public static Node[] selectAsNodes(
645        Node node,
646        String xpath)
647        throws XPathException
648    {
649        return XmlUtil.selectAsNodes(node, xpath, node);
650    }
651
652    public static Node[] selectAsNodes(
653        Node node,
654        String xpath,
655        Node namespaceNode)
656        throws XPathException
657    {
658        XPathResult xpathResult = XmlUtil.select(node, xpath, namespaceNode);
659        return XmlUtil.convertToNodes(xpathResult);
660    }
661
662    public static XPathResult select(
663        Node contextNode,
664        String xpath,
665        Node namespaceNode)
666        throws XPathException
667    {
668        XPathEvaluator evaluator = new XPathEvaluatorImpl();
669        XPathNSResolver resolver = evaluator.createNSResolver(namespaceNode);
670
671        return (XPathResult) evaluator.evaluate(
672            xpath, contextNode, resolver,
673            XPathResult.ANY_TYPE, null);
674    }
675
676    /**
677     * Convert an XPathResult object to String.
678     *
679     */
680    public static String convertToString(
681        XPathResult xpathResult,
682        boolean prettyPrint)
683    {
684        switch (xpathResult.getResultType()) {
685        case XPathResult.NUMBER_TYPE:
686            double d = xpathResult.getNumberValue();
687            return Double.toString(d);
688
689        case XPathResult.STRING_TYPE:
690            String s = xpathResult.getStringValue();
691            return s;
692
693        case XPathResult.BOOLEAN_TYPE:
694            boolean b = xpathResult.getBooleanValue();
695            return String.valueOf(b);
696
697        case XPathResult.FIRST_ORDERED_NODE_TYPE:
698        case XPathResult.ANY_UNORDERED_NODE_TYPE:
699        {
700            Node node = xpathResult.getSingleNodeValue();
701            return XmlUtil.toString(node, prettyPrint);
702        }
703        case XPathResult.ORDERED_NODE_ITERATOR_TYPE:
704        case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
705        {
706            StringBuilder buf = new StringBuilder(512);
707            Node node = xpathResult.iterateNext();
708            while (node != null) {
709                buf.append(XmlUtil.toString(node, prettyPrint));
710                node = xpathResult.iterateNext();
711            }
712            return buf.toString();
713        }
714        case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
715        case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:
716        {
717            StringBuilder buf = new StringBuilder(512);
718            int len = xpathResult.getSnapshotLength();
719            for (int i = 0; i < len; i++) {
720                Node node = xpathResult.snapshotItem(i);
721                buf.append(XmlUtil.toString(node, prettyPrint));
722            }
723            return buf.toString();
724        }
725        default:
726            String msg = "Unknown xpathResult.type = "
727                + xpathResult.getResultType();
728            throw new XPathException(XPathException.TYPE_ERR, msg);
729        }
730    }
731
732    private static final Node[] NULL_NODE_ARRAY = new Node[0];
733
734    /**
735     * Convert an XPathResult to an array of Nodes.
736     *
737     */
738    public static Node[] convertToNodes(XPathResult xpathResult) {
739        switch (xpathResult.getResultType()) {
740        case XPathResult.NUMBER_TYPE:
741            return NULL_NODE_ARRAY;
742
743        case XPathResult.STRING_TYPE:
744            return NULL_NODE_ARRAY;
745
746        case XPathResult.BOOLEAN_TYPE:
747            return NULL_NODE_ARRAY;
748
749        case XPathResult.FIRST_ORDERED_NODE_TYPE:
750        case XPathResult.ANY_UNORDERED_NODE_TYPE:
751        {
752            Node node = xpathResult.getSingleNodeValue();
753            return new Node[] { node };
754        }
755        case XPathResult.ORDERED_NODE_ITERATOR_TYPE:
756        case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
757        {
758            List<Node> list = new ArrayList<Node>();
759            Node node = xpathResult.iterateNext();
760            while (node != null) {
761                list.add(node);
762                node = xpathResult.iterateNext();
763            }
764            return (Node[]) list.toArray(NULL_NODE_ARRAY);
765        }
766        case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
767        case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:
768        {
769            int len = xpathResult.getSnapshotLength();
770            Node[] nodes = new Node[len];
771            for (int i = 0; i < len; i++) {
772                Node node = xpathResult.snapshotItem(i);
773                nodes[i] = node;
774            }
775            return nodes;
776        }
777        default:
778            String msg = "Unknown xpathResult.type = "
779                + xpathResult.getResultType();
780            throw new XPathException(XPathException.TYPE_ERR, msg);
781        }
782    }
783
784    /**
785     * Convert a Node to a String.
786     *
787     */
788    public static String toString(Node node, boolean prettyPrint) {
789        if (node == null) {
790            return null;
791        }
792        try {
793            Document doc = node.getOwnerDocument();
794            OutputFormat format;
795            if (doc != null) {
796                format = new OutputFormat(doc, null, prettyPrint);
797            } else {
798                format = new OutputFormat("xml", null, prettyPrint);
799                format.setLineWidth(0); // don't wrap lines
800            }
801            if (prettyPrint) {
802                format.setLineSeparator(LINE_SEP);
803            } else {
804                format.setLineSeparator("");
805            }
806            StringWriter writer = new StringWriter(1000);
807            XMLSerializer serial = new XMLSerializer(writer, format);
808            serial.asDOMSerializer();
809            if (node instanceof Document) {
810                serial.serialize((Document) node);
811            } else if (node instanceof Element) {
812                format.setOmitXMLDeclaration(true);
813                serial.serialize((Element) node);
814            } else if (node instanceof DocumentFragment) {
815                format.setOmitXMLDeclaration(true);
816                serial.serialize((DocumentFragment) node);
817            } else if (node instanceof Text) {
818                Text text = (Text) node;
819                return text.getData();
820            } else if (node instanceof Attr) {
821                Attr attr = (Attr) node;
822                String name = attr.getName();
823                String value = attr.getValue();
824                writer.write(name);
825                writer.write("=\"");
826                writer.write(value);
827                writer.write("\"");
828                if (prettyPrint) {
829                    writer.write(LINE_SEP);
830                }
831            } else {
832                writer.write("node class = " + node.getClass().getName());
833                if (prettyPrint) {
834                    writer.write(LINE_SEP);
835                } else {
836                    writer.write(' ');
837                }
838                writer.write("XmlUtil.toString: fix me: ");
839                writer.write(node.toString());
840                if (prettyPrint) {
841                    writer.write(LINE_SEP);
842                }
843            }
844            return writer.toString();
845        } catch (Exception ex) {
846            // ignore
847            return null;
848        }
849    }
850
851    //////////////////////////////////////////////////////////////////////////
852    // validate
853    //////////////////////////////////////////////////////////////////////////
854
855    /**
856     * This can be extened to have a map from publicId/systemId
857     * to InputSource.
858     */
859    public static class Resolver implements EntityResolver {
860        private InputSource source;
861
862        protected Resolver() {
863            this((InputSource) null);
864        }
865        public Resolver(Document doc) {
866            this(XmlUtil.toString(doc, false));
867        }
868        public Resolver(String str) {
869            this(new InputSource(new StringReader(str)));
870        }
871        public Resolver(InputSource source) {
872            this.source = source;
873        }
874        public InputSource resolveEntity(
875            String publicId,
876            String systemId)
877            throws SAXException, IOException
878        {
879            return source;
880        }
881    }
882
883    /**
884     * Get the Xerces version being used.
885     *
886     * @return Xerces version being used
887     */
888    public static String getXercesVersion() {
889        try {
890            return org.apache.xerces.impl.Version.getVersion();
891        } catch (java.lang.NoClassDefFoundError ex) {
892            return "Xerces-J 2.2.0";
893        }
894    }
895
896    /**
897     * Get the number part of the Xerces Version string.
898     *
899     * @return number part of the Xerces Version string
900     */
901    public static String getXercesVersionNumberString() {
902        String version = getXercesVersion();
903        int index = version.indexOf(' ');
904        return (index == -1)
905            ? "0.0.0" : version.substring(index + 1);
906    }
907
908    private static int[] versionNumbers = null;
909
910    /**
911     * Gets the Xerces version numbers as a three part array of ints where
912     * the first element is the major release number, the second is the
913     * minor release number, and the third is the patch number.
914     *
915     * @return Xerces version number as int array
916     */
917    public static synchronized int[] getXercesVersionNumbers() {
918        if (versionNumbers == null) {
919            int[] verNums = new int[3];
920            String verNumStr = getXercesVersionNumberString();
921            int index = verNumStr.indexOf('.');
922            verNums[0] = Integer.parseInt(verNumStr.substring(0, index));
923
924            verNumStr = verNumStr.substring(index + 1);
925            index = verNumStr.indexOf('.');
926            verNums[1] = Integer.parseInt(verNumStr.substring(0, index));
927
928            verNumStr = verNumStr.substring(index + 1);
929            verNums[2] = Integer.parseInt(verNumStr);
930
931            versionNumbers = verNums;
932        }
933
934        return versionNumbers;
935    }
936
937    /**
938     * I could not get Xerces 2.2 to validate. So, I hard coded allowing
939     * Xerces 2.6 and above to validate and by setting the following
940     * System property one can test validating with earlier versions
941     * of Xerces.
942     */
943    private static final String ALWAYS_ATTEMPT_VALIDATION =
944        "mondrian.xml.always.attempt.validation";
945
946    private static final boolean alwaysAttemptValidation;
947    static {
948        alwaysAttemptValidation =
949            Boolean.getBoolean(ALWAYS_ATTEMPT_VALIDATION);
950    }
951
952    /**
953     * Returns whether the XML parser supports validation.
954     *
955     * <p>I could not get validation to work with Xerces 2.2 so I put in
956     * this check. If you want to test on an earlier version of Xerces
957     * simply define the above property:
958     *   "mondrian.xml.always.attempt.validation",
959     * to true.
960     *
961     * @return whether the XML parser supports validation
962     */
963    public static boolean supportsValidation() {
964        if (alwaysAttemptValidation) {
965            return true;
966        } else {
967            int[] verNums = getXercesVersionNumbers();
968            return (verNums[0] >= 3)
969                || ((verNums[0] == 2) && (verNums[1] >= 6));
970        }
971    }
972
973    public static void validate(
974        Document doc,
975        String schemaLocationPropertyValue,
976        EntityResolver resolver)
977        throws IOException,
978        SAXException
979    {
980        OutputFormat format  = new OutputFormat(doc, null, true);
981        StringWriter writer = new StringWriter(1000);
982        XMLSerializer serial = new XMLSerializer(writer, format);
983        serial.asDOMSerializer();
984        serial.serialize(doc);
985        String docString = writer.toString();
986
987        validate(docString, schemaLocationPropertyValue, resolver);
988    }
989
990    public static void validate(
991        String docStr,
992        String schemaLocationPropertyValue,
993        EntityResolver resolver)
994        throws IOException,
995        SAXException
996    {
997        if (resolver == null) {
998            resolver = new Resolver();
999        }
1000        DOMParser parser =
1001            getParser(schemaLocationPropertyValue, resolver, true);
1002
1003        try {
1004            parser.parse(new InputSource(new StringReader(docStr)));
1005            checkForParseError(parser);
1006        } catch (SAXParseException ex) {
1007            checkForParseError(parser, ex);
1008        }
1009    }
1010
1011    /**
1012     * This is used to get a Document's namespace attribute value.
1013     *
1014     */
1015    public static String getNamespaceAttributeValue(Document doc) {
1016        Element el = doc.getDocumentElement();
1017        String prefix = el.getPrefix();
1018        Attr attr = (prefix == null)
1019                    ? el.getAttributeNode(XMLNS)
1020                    : el.getAttributeNode(XMLNS + ':' + prefix);
1021        return (attr == null) ? null : attr.getValue();
1022    }
1023
1024    //////////////////////////////////////////////////////////////////////////
1025    // transform
1026    //////////////////////////////////////////////////////////////////////////
1027
1028    private static TransformerFactory tfactory = null;
1029
1030    public static TransformerFactory getTransformerFactory()
1031        throws TransformerFactoryConfigurationError
1032    {
1033        if (tfactory == null) {
1034            tfactory = TransformerFactory.newInstance();
1035        }
1036        return tfactory;
1037    }
1038
1039    /**
1040     * Transform a Document and return the transformed Node.
1041     *
1042     */
1043    public static Node transform(
1044        Document inDoc,
1045        String xslFileName,
1046        String[][] namevalueParameters)
1047        throws ParserConfigurationException,
1048               SAXException,
1049               IOException,
1050               TransformerConfigurationException,
1051               TransformerException
1052    {
1053        DocumentBuilderFactory dfactory = DocumentBuilderFactory.newInstance();
1054        dfactory.setNamespaceAware(true);
1055
1056        DocumentBuilder docBuilder = dfactory.newDocumentBuilder();
1057        Node xslDOM = docBuilder.parse(new InputSource(xslFileName));
1058
1059        TransformerFactory tfactory = getTransformerFactory();
1060        Templates stylesheet = tfactory.newTemplates(
1061            new DOMSource(xslDOM, xslFileName));
1062        Transformer transformer = stylesheet.newTransformer();
1063        if (namevalueParameters != null) {
1064            for (int i = 0; i < namevalueParameters.length; i++) {
1065                String name = namevalueParameters[i][0];
1066                String value = namevalueParameters[i][1];
1067
1068                transformer.setParameter(name, value);
1069            }
1070        }
1071        DOMResult domResult = new DOMResult();
1072        transformer.transform(new DOMSource(inDoc, null), domResult);
1073
1074        return domResult.getNode();
1075    }
1076
1077    public static Node transform(
1078        Document inDoc,
1079        String xslFileName)
1080        throws ParserConfigurationException,
1081               SAXException,
1082               IOException,
1083               TransformerConfigurationException,
1084               TransformerException
1085    {
1086        return transform(inDoc, xslFileName, null);
1087    }
1088
1089    /**
1090     * Transform a Document and return the transformed Node.
1091     *
1092     */
1093    public static Node transform(
1094        Document inDoc,
1095        Reader xslReader,
1096        String[][] namevalueParameters)
1097        throws ParserConfigurationException,
1098               SAXException,
1099               IOException,
1100               TransformerConfigurationException,
1101               TransformerException
1102    {
1103        DocumentBuilderFactory dfactory = DocumentBuilderFactory.newInstance();
1104        dfactory.setNamespaceAware(true);
1105
1106        DocumentBuilder docBuilder = dfactory.newDocumentBuilder();
1107        Node xslDOM = docBuilder.parse(new InputSource(xslReader));
1108
1109        TransformerFactory tfactory = getTransformerFactory();
1110        Templates stylesheet = tfactory.newTemplates(new DOMSource(xslDOM));
1111        Transformer transformer = stylesheet.newTransformer();
1112        if (namevalueParameters != null) {
1113            for (int i = 0; i < namevalueParameters.length; i++) {
1114                String name = namevalueParameters[i][0];
1115                String value = namevalueParameters[i][1];
1116
1117                transformer.setParameter(name, value);
1118            }
1119        }
1120
1121        DOMResult domResult = new DOMResult();
1122        transformer.transform(new DOMSource(inDoc, null), domResult);
1123
1124        return domResult.getNode();
1125    }
1126
1127    public static Node transform(
1128        Document inDoc,
1129        Reader xslReader)
1130        throws ParserConfigurationException,
1131               SAXException,
1132               IOException,
1133               TransformerConfigurationException,
1134               TransformerException
1135    {
1136        return transform(inDoc, xslReader, null);
1137    }
1138
1139
1140    private XmlUtil() {
1141    }
1142}
1143
1144// End XmlUtil.java