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-2012 Pentaho and others
008// All Rights Reserved.
009*/
010package mondrian.tui;
011
012import mondrian.olap.*;
013import mondrian.olap.Util.ByteMatcher;
014import mondrian.rolap.RolapConnectionProperties;
015import mondrian.server.StringRepositoryContentFinder;
016import mondrian.spi.CatalogLocator;
017import mondrian.spi.impl.CatalogLocatorImpl;
018import mondrian.util.LockBox;
019import mondrian.xmla.*;
020import mondrian.xmla.Enumeration;
021import mondrian.xmla.impl.*;
022
023import org.apache.log4j.Logger;
024
025import org.eigenbase.xom.*;
026import org.eigenbase.xom.Parser;
027
028import org.w3c.dom.*;
029import org.xml.sax.SAXException;
030
031import java.io.*;
032import java.util.*;
033
034import javax.servlet.Servlet;
035import javax.servlet.ServletException;
036import javax.xml.parsers.ParserConfigurationException;
037import javax.xml.transform.TransformerConfigurationException;
038import javax.xml.transform.TransformerException;
039
040/**
041 * Support for making XMLA requests and looking at the responses.
042 *
043 * @author Richard M. Emberson
044 */
045public class XmlaSupport {
046    private static final Logger LOGGER = Logger.getLogger(XmlaSupport.class);
047
048    public static final String nl = Util.nl;
049    public static final String SOAP_PREFIX = XmlaConstants.SOAP_PREFIX;
050
051    private static final ByteMatcher UTF8_BOM_MATCHER =
052        new ByteMatcher(
053            new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF});
054
055    private static final ByteMatcher UTF16_LE_BOM_MATCHER =
056        new ByteMatcher(
057            new byte[]{ (byte)0xFF, (byte)0xFE });
058
059    private static final ByteMatcher UTF16_BE_BOM_MATCHER =
060        new ByteMatcher(
061            new byte[]{ (byte)0xFE, (byte)0xFF });
062
063    private static final ByteMatcher UTF32_LE_BOM_MATCHER =
064        new ByteMatcher(
065            new byte[]{ (byte)0xFF, (byte)0xFE, (byte)0x00, (byte)0x00 });
066
067    private static final ByteMatcher UTF32_BE_BOM_MATCHER =
068        new ByteMatcher(
069            new byte[]{ (byte)0x00, (byte)0x00, (byte)0xFE, (byte)0xFF });
070
071    public static final String CATALOG_NAME = "FoodMart";
072    public static final String DATASOURCE_NAME = "FoodMart";
073    public static final String DATASOURCE_DESCRIPTION =
074            "Mondrian FoodMart data source";
075    public static final String DATASOURCE_INFO =
076            "Provider=Mondrian;DataSource=" + CATALOG_NAME + ";";
077
078    public static final Map<String, String> ENV;
079
080    // Setup the Map used to instantiate XMLA template documents.
081    // Have to see if we need to be able to dynamically change these values.
082    static {
083        ENV = new HashMap<String, String>();
084        ENV.put("catalog", CATALOG_NAME);
085        ENV.put("datasource", DATASOURCE_INFO);
086    }
087
088    /**
089     * This is a parameterized XSLT.
090     * The parameters are:
091     *   "soap" with values "none" or empty
092     *   "content" with values "schemadata", "schema", "data" or empty
093     * With these setting one can extract from an XMLA SOAP message
094     * the soap wrapper plus body or simply the body; the complete
095     * body (schema and data), only the schema of the body, only the
096     * data of the body or none of the body
097     *
098     */
099    public static String getXmlaTransform(String xmlaPrefix) {
100        return
101        "<?xml version='1.0'?>"
102        + "<xsl:stylesheet "
103        + "  xmlns:xsl='http://www.w3.org/1999/XSL/Transform' "
104        + "  xmlns:xalan='http://xml.apache.org/xslt'"
105        + "  xmlns:xsd='http://www.w3.org/2001/XMLSchema'"
106        + "  xmlns:ROW='urn:schemas-microsoft-com:xml-analysis:rowset'"
107        + "  xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' "
108        + "  xmlns:" + xmlaPrefix + "='urn:schemas-microsoft-com:xml-analysis'"
109        + "  version='1.0'"
110        + ">"
111        + "<xsl:output method='xml' "
112        + "  encoding='UTF-8'"
113        + "  indent='yes' "
114        + "  xalan:indent-amount='2'/>"
115        + "<xsl:param name='content'/>"
116        + "<xsl:param name='soap'/>"
117        + "<!-- consume '/' and apply -->"
118        + "<xsl:template match='/'>"
119        + "  <xsl:apply-templates/>"
120        + "</xsl:template>"
121        + "<!-- copy 'Envelope' unless soap==none --> "
122        + "<xsl:template match='SOAP-ENV:Envelope'> "
123        + "  <xsl:choose> "
124        + "    <xsl:when test=\"$soap='none'\"> "
125        + "      <xsl:apply-templates/> "
126        + "    </xsl:when> "
127        + "    <xsl:otherwise> "
128        + "      <xsl:copy> "
129        + "        <xsl:apply-templates select='@*|node()'/> "
130        + "      </xsl:copy> "
131        + "    </xsl:otherwise>  "
132        + "  </xsl:choose> "
133        + "</xsl:template> "
134        + "<!-- copy 'Header' unless soap==none --> "
135        + "<xsl:template match='SOAP-ENV:Header'> "
136        + "  <xsl:choose> "
137        + "    <xsl:when test=\"$soap='none'\"> "
138        + "      <xsl:apply-templates/> "
139        + "    </xsl:when> "
140        + "    <xsl:otherwise>  "
141        + "      <xsl:copy> "
142        + "        <xsl:apply-templates select='@*|node()'/> "
143        + "      </xsl:copy> "
144        + "    </xsl:otherwise>  "
145        + "  </xsl:choose> "
146        + "</xsl:template> "
147        + "<!-- copy 'Body' unless soap==none --> "
148        + "<xsl:template match='SOAP-ENV:Body'> "
149        + "  <xsl:choose> "
150        + "    <xsl:when test=\"$soap='none'\"> "
151        + "      <xsl:apply-templates/> "
152        + "    </xsl:when> "
153        + "    <xsl:otherwise>  "
154        + "      <xsl:copy> "
155        + "        <xsl:apply-templates select='@*|node()'/> "
156        + "      </xsl:copy> "
157        + "    </xsl:otherwise>  "
158        + "  </xsl:choose> "
159        + "</xsl:template> "
160        + "<!-- copy 'DiscoverResponse' unless soap==none --> "
161        + "<xsl:template match='" + xmlaPrefix + ":DiscoverResponse'> "
162        + "  <xsl:choose> "
163        + "    <xsl:when test=\"$soap='none'\"> "
164        + "      <xsl:apply-templates/> "
165        + "    </xsl:when> "
166        + "    <xsl:otherwise> "
167        + "      <xsl:copy> "
168        + "        <xsl:apply-templates select='@*|node()'/> "
169        + "      </xsl:copy> "
170        + "    </xsl:otherwise>  "
171        + "  </xsl:choose> "
172        + "</xsl:template> "
173        + "<!-- copy 'return' unless soap==none --> "
174        + "<xsl:template match='" + xmlaPrefix + ":return'> "
175        + "  <xsl:choose> "
176        + "    <xsl:when test=\"$soap='none'\"> "
177        + "      <xsl:apply-templates/> "
178        + "    </xsl:when> "
179        + "    <xsl:otherwise> "
180        + "      <xsl:copy> "
181        + "        <xsl:apply-templates select='@*|node()'/> "
182        + "      </xsl:copy> "
183        + "    </xsl:otherwise>  "
184        + "  </xsl:choose> "
185        + "</xsl:template> "
186        + "<!-- copy 'root' unless soap==none --> "
187        + "<xsl:template match='ROW:root'> "
188        + "  <xsl:choose> "
189        + "    <xsl:when test=\"$soap='none'\"> "
190        + "      <xsl:apply-templates/> "
191        + "    </xsl:when> "
192        + "    <xsl:otherwise> "
193        + "      <xsl:copy> "
194        + "        <xsl:apply-templates select='@*|node()'/> "
195        + "      </xsl:copy> "
196        + "    </xsl:otherwise > "
197        + "  </xsl:choose> "
198        + "</xsl:template> "
199        + "<!-- copy 'schema' if content==schema or schemadata --> "
200        + "<xsl:template match='xsd:schema'> "
201        + "  <xsl:choose> "
202        + "    <xsl:when test=\"$content='schemadata'\"> "
203        + "      <xsl:copy> "
204        + "        <xsl:apply-templates select='@*|node()'/> "
205        + "      </xsl:copy> "
206        + "    </xsl:when> "
207        + "    <xsl:when test=\"$content='schema'\"> "
208        + "      <xsl:copy> "
209        + "        <xsl:apply-templates select='@*|node()'/> "
210        + "      </xsl:copy> "
211        + "    </xsl:when> "
212        + "  <xsl:otherwise/>  "
213        + "  </xsl:choose> "
214        + "</xsl:template> "
215        + "<!-- copy 'row' if content==data or schemadata --> "
216        + "<xsl:template match='ROW:row'> "
217        + "  <xsl:choose> "
218        + "    <xsl:when test=\"$content='schemadata'\"> "
219        + "      <xsl:copy> "
220        + "        <xsl:apply-templates select='@*|node()'/> "
221        + "      </xsl:copy> "
222        + "    </xsl:when> "
223        + "    <xsl:when test=\"$content='data'\"> "
224        + "      <xsl:copy> "
225        + "        <xsl:apply-templates select='@*|node()'/> "
226        + "      </xsl:copy> "
227        + "    </xsl:when> "
228        + "    <xsl:otherwise/>  "
229        + "  </xsl:choose> "
230        + "</xsl:template> "
231        + "<!-- copy everything else --> "
232        + "<xsl:template match='*|@*'> "
233        + "  <xsl:copy> "
234        + "    <xsl:apply-templates select='@*|node()'/> "
235        + "  </xsl:copy> "
236        + "</xsl:template> "
237        + "</xsl:stylesheet>";
238    }
239
240    /**
241     * This is the prefix used in xpath and transforms for the xmla rowset
242     * namespace "urn:schemas-microsoft-com:xml-analysis:rowset".
243     */
244    public static final String ROW_SET_PREFIX = "ROW";
245
246    private static CatalogLocator CATALOG_LOCATOR = null;
247    private static String soapFaultXPath = null;
248    private static String soapHeaderAndBodyXPath = null;
249    private static String soapBodyXPath = null;
250    private static String soapXmlaRootXPath = null;
251    private static String xmlaRootXPath = null;
252
253
254    /////////////////////////////////////////////////////////////////////////
255    // xmla help
256    /////////////////////////////////////////////////////////////////////////
257    public static CatalogLocator getCatalogLocator() {
258        if (CATALOG_LOCATOR == null) {
259            CATALOG_LOCATOR = new CatalogLocatorImpl();
260        }
261        return CATALOG_LOCATOR;
262    }
263
264    public static DataSourcesConfig.DataSources getDataSources(
265        String connectString,
266        Map<String, String> catalogNameUrls)
267        throws XOMException
268    {
269        String str = getDataSourcesText(connectString, catalogNameUrls);
270        StringReader dsConfigReader = new StringReader(str);
271
272        final Parser xmlParser = XOMUtil.createDefaultParser();
273        final DOMWrapper def = xmlParser.parse(dsConfigReader);
274
275        return new DataSourcesConfig.DataSources(def);
276    }
277
278    public static DataSourcesConfig.DataSources parseDataSources(
279        String dataSourcesConfigString,
280        Logger logger)
281    {
282        try {
283            if (dataSourcesConfigString == null) {
284                logger.warn("XmlaSupport.parseDataSources: null input");
285                return null;
286            }
287            dataSourcesConfigString =
288                Util.replaceProperties(
289                    dataSourcesConfigString,
290                    Util.toMap(System.getProperties()));
291
292            if (logger.isDebugEnabled()) {
293                logger.debug(
294                    "XmlaSupport.parseDataSources: dataSources="
295                    + dataSourcesConfigString);
296            }
297            final Parser parser =
298                XOMUtil.createDefaultParser();
299            final DOMWrapper doc = parser.parse(dataSourcesConfigString);
300            return new DataSourcesConfig.DataSources(doc);
301        } catch (XOMException e) {
302            throw Util.newError(
303                e,
304                "Failed to parse data sources config: "
305                + dataSourcesConfigString);
306        }
307    }
308
309    /**
310     * With a connection string, generate the DataSource xml. Since this
311     * is used by directly, same process, communicating with XMLA
312     * Mondrian, the fact that the URL contains "localhost" is not
313     * important.
314     *
315     * @param connectString Connect string
316     * @param catalogNameUrls array of catalog names, catalog url pairs
317     */
318    public static String getDataSourcesText(
319        String connectString,
320        Map<String, String> catalogNameUrls)
321    {
322        StringBuilder buf = new StringBuilder(500);
323        if (false) {
324        buf.append("<?xml version=\"1.0\"?>");
325        buf.append(nl);
326        buf.append("<DataSources>");
327        buf.append(nl);
328        buf.append("   <DataSource>");
329        buf.append(nl);
330        buf.append("       <DataSourceName>");
331        buf.append(DATASOURCE_NAME);
332        buf.append("</DataSourceName>");
333        buf.append(nl);
334        buf.append("       <DataSourceDescription>");
335        buf.append(DATASOURCE_DESCRIPTION);
336        buf.append("</DataSourceDescription>");
337        buf.append(nl);
338        buf.append("       <URL>http://localhost:8080/mondrian/xmla</URL>");
339        buf.append(nl);
340        buf.append("       <DataSourceInfo><![CDATA[");
341        buf.append(connectString);
342        buf.append("]]></DataSourceInfo>");
343        buf.append(nl);
344
345        buf.append("       <ProviderName>Mondrian</ProviderName>");
346        buf.append(nl);
347        buf.append("       <ProviderType>MDP</ProviderType>");
348        buf.append(nl);
349        buf.append(
350            "       <AuthenticationMode>Unauthenticated</AuthenticationMode>");
351        buf.append(nl);
352        buf.append("       <Catalogs>");
353        buf.append(nl);
354        for (Map.Entry<String, String> catalogNameUrl
355            : catalogNameUrls.entrySet())
356        {
357            String name = catalogNameUrl.getKey();
358            String url = catalogNameUrl.getValue();
359            buf.append("           <Catalog name='");
360            buf.append(name);
361            buf.append("'>");
362            if (url != null) {
363                buf.append("<Definition>");
364                buf.append(url);
365                buf.append("</Definition>");
366            }
367            buf.append("</Catalog>");
368        }
369        buf.append("       </Catalogs>");
370        buf.append(nl);
371        buf.append("   </DataSource>");
372        buf.append(nl);
373        buf.append("</DataSources>");
374        buf.append(nl);
375        } else {
376            buf.append("<?xml version=\"1.0\"?>");
377            buf.append(nl);
378            buf.append("<DataSources>");
379            buf.append(nl);
380            buf.append("   <DataSource>");
381            buf.append(nl);
382            buf.append("       <DataSourceName>");
383            buf.append(DATASOURCE_NAME);
384            buf.append("</DataSourceName>");
385            buf.append(nl);
386            buf.append("       <DataSourceDescription>");
387            buf.append(DATASOURCE_DESCRIPTION);
388            buf.append("</DataSourceDescription>");
389            buf.append(nl);
390            buf.append("       <URL>http://localhost:8080/mondrian/xmla</URL>");
391            buf.append(nl);
392            buf.append("       <DataSourceInfo><![CDATA[");
393            buf.append(connectString);
394            buf.append("]]></DataSourceInfo>");
395            buf.append(nl);
396
397            buf.append("       <ProviderName>Mondrian</ProviderName>");
398            buf.append(nl);
399            buf.append("       <ProviderType>MDP</ProviderType>");
400            buf.append(nl);
401            buf.append(
402                "       <AuthenticationMode>Unauthenticated</AuthenticationMode>");
403            buf.append(nl);
404            buf.append("       <Catalogs>");
405            for (Map.Entry<String, String> catalogNameUrl
406                : catalogNameUrls.entrySet())
407            {
408                String name = catalogNameUrl.getKey();
409                String url = catalogNameUrl.getValue();
410
411                buf.append(nl);
412                buf.append("           <Catalog name='");
413                buf.append(name);
414                buf.append("'>");
415                if (url != null) {
416                    buf.append("<Definition>");
417                    buf.append(url);
418                    buf.append("</Definition>");
419                }
420                buf.append("</Catalog>");
421            }
422
423            buf.append(nl);
424            buf.append("       </Catalogs>");
425            buf.append(nl);
426            buf.append("   </DataSource>");
427            buf.append(nl);
428            buf.append("</DataSources>");
429            buf.append(nl);
430        }
431        String datasources = buf.toString();
432        if (LOGGER.isDebugEnabled()) {
433            LOGGER.debug(
434                "XmlaSupport.getDataSources: datasources=" + datasources);
435        }
436        return datasources;
437    }
438
439    public static String getSoapFaultXPath() {
440        if (XmlaSupport.soapFaultXPath == null) {
441            StringBuilder buf = new StringBuilder(100);
442            buf.append('/');
443            buf.append(SOAP_PREFIX);
444            buf.append(":Envelope");
445            buf.append('/');
446            buf.append(SOAP_PREFIX);
447            buf.append(":Body");
448            buf.append('/');
449            buf.append(SOAP_PREFIX);
450            buf.append(":Fault");
451            buf.append("/*");
452            String xpath = buf.toString();
453            XmlaSupport.soapFaultXPath = xpath;
454
455            if (LOGGER.isDebugEnabled()) {
456                LOGGER.debug(
457                    "XmlaSupport.getSoapFaultXPath: xpath=" + xpath);
458            }
459        }
460        return XmlaSupport.soapFaultXPath;
461    }
462
463    public static String getSoapHeaderAndBodyXPath() {
464        if (XmlaSupport.soapHeaderAndBodyXPath == null) {
465            StringBuilder buf = new StringBuilder(100);
466            buf.append('/');
467            buf.append(SOAP_PREFIX);
468            buf.append(":Envelope");
469            buf.append("/*");
470            String xpath = buf.toString();
471            XmlaSupport.soapHeaderAndBodyXPath = xpath;
472
473            if (LOGGER.isDebugEnabled()) {
474                LOGGER.debug(
475                    "XmlaSupport.getSoapHeaderAndBodyXPath: xpath=" + xpath);
476            }
477        }
478        return XmlaSupport.soapHeaderAndBodyXPath;
479    }
480    public static String getSoapBodyXPath() {
481        if (XmlaSupport.soapBodyXPath == null) {
482            StringBuilder buf = new StringBuilder(100);
483            buf.append('/');
484            buf.append(SOAP_PREFIX);
485            buf.append(":Envelope");
486            buf.append('/');
487            buf.append(SOAP_PREFIX);
488            buf.append(":Body");
489            buf.append("/*");
490            String xpath = buf.toString();
491            XmlaSupport.soapBodyXPath = xpath;
492
493            if (LOGGER.isDebugEnabled()) {
494                LOGGER.debug("XmlaSupport.getSoapBodyXPath: xpath=" + xpath);
495            }
496        }
497        return XmlaSupport.soapBodyXPath;
498    }
499
500    public static String getSoapXmlaRootXPath(String xmlaPrefix) {
501        if (XmlaSupport.soapXmlaRootXPath == null) {
502            StringBuilder buf = new StringBuilder(20);
503            buf.append('/');
504            buf.append(SOAP_PREFIX);
505            buf.append(":Envelope");
506            buf.append('/');
507            buf.append(SOAP_PREFIX);
508            buf.append(":Body");
509            buf.append("/").append(xmlaPrefix).append(":DiscoverResponse");
510            buf.append("/").append(xmlaPrefix).append(":return");
511            buf.append('/');
512            buf.append(ROW_SET_PREFIX);
513            buf.append(":root");
514            buf.append("/*");
515            String xpath = buf.toString();
516            XmlaSupport.soapXmlaRootXPath = xpath;
517
518            if (LOGGER.isDebugEnabled()) {
519                LOGGER.debug(
520                    "XmlaSupport.getSoapXmlaRootXPath: xpath="
521                    + xpath);
522            }
523        }
524        return XmlaSupport.soapXmlaRootXPath;
525    }
526
527    public static String getXmlaRootXPath(String xmlaPrefix) {
528        if (XmlaSupport.xmlaRootXPath == null) {
529            StringBuilder buf = new StringBuilder(20);
530            buf.append("/").append(xmlaPrefix).append(":DiscoverResponse");
531            buf.append("/").append(xmlaPrefix).append(":return");
532            buf.append('/');
533            buf.append(ROW_SET_PREFIX);
534            buf.append(":root");
535            buf.append("/*");
536            String xpath = buf.toString();
537            XmlaSupport.xmlaRootXPath = xpath;
538
539            if (LOGGER.isDebugEnabled()) {
540                LOGGER.debug("XmlaSupport.getXmlaRootXPath: xpath=" + xpath);
541            }
542        }
543        return XmlaSupport.xmlaRootXPath;
544    }
545
546    public static Node[] extractNodesFromSoapXmla(byte[] bytes)
547        throws SAXException, IOException
548    {
549        Document doc = XmlUtil.parse(bytes);
550        return extractNodesFromSoapXmla(doc);
551    }
552
553    public static Node[] extractNodesFromSoapXmla(Document doc)
554        throws SAXException, IOException
555    {
556        final String xmlaPrefix = "xmla";
557        String xpath = getSoapXmlaRootXPath(xmlaPrefix);
558
559        // Note that this is SOAP 1.1 version uri
560        String[][] nsArray = new String[][] {
561             {
562                 SOAP_PREFIX, XmlaConstants.NS_SOAP_ENV_1_1
563             },
564             {
565                 xmlaPrefix, XmlaConstants.NS_XMLA
566             },
567             {
568                 ROW_SET_PREFIX, XmlaConstants.NS_XMLA_ROWSET
569             }
570        };
571
572        return extractNodes(doc, xpath, nsArray);
573    }
574
575    public static Node[] extractNodesFromXmla(byte[] bytes)
576        throws SAXException, IOException
577    {
578        Document doc = XmlUtil.parse(bytes);
579        return extractNodesFromXmla(doc);
580    }
581
582    public static Node[] extractNodesFromXmla(Document doc)
583        throws SAXException, IOException
584    {
585        final String xmlaPrefix = "xmla";
586        String xpath = getXmlaRootXPath(xmlaPrefix);
587
588        String[][] nsArray = new String[][] {
589             {
590                 xmlaPrefix, XmlaConstants.NS_XMLA
591             },
592             {
593                 ROW_SET_PREFIX, XmlaConstants.NS_XMLA_ROWSET
594             }
595        };
596
597        return extractNodes(doc, xpath, nsArray);
598    }
599
600    public static Node[] extractFaultNodesFromSoap(byte[] bytes)
601        throws SAXException, IOException
602    {
603        Document doc = XmlUtil.parse(bytes);
604        return extractFaultNodesFromSoap(doc);
605    }
606
607    public static Node[] extractFaultNodesFromSoap(Document doc)
608        throws SAXException, IOException
609    {
610        String xpath = getSoapFaultXPath();
611
612        String[][] nsArray = {
613             { SOAP_PREFIX, XmlaConstants.NS_SOAP_ENV_1_1 },
614        };
615
616        return extractNodes(doc, xpath, nsArray);
617    }
618
619    public static Node[] extractHeaderAndBodyFromSoap(byte[] bytes)
620        throws SAXException, IOException
621    {
622        Document doc = XmlUtil.parse(bytes);
623        return extractHeaderAndBodyFromSoap(doc);
624    }
625
626    public static Node[] extractHeaderAndBodyFromSoap(Document doc)
627        throws SAXException, IOException
628    {
629        String xpath = getSoapHeaderAndBodyXPath();
630
631        String[][] nsArray = {
632             { SOAP_PREFIX, XmlaConstants.NS_SOAP_ENV_1_1 },
633        };
634
635        return extractNodes(doc, xpath, nsArray);
636    }
637
638    public static Document extractBodyFromSoap(Document doc)
639        throws SAXException, IOException
640    {
641        String xpath = getSoapBodyXPath();
642
643        String[][] nsArray = new String[][] {
644             { SOAP_PREFIX, XmlaConstants.NS_SOAP_ENV_1_1 },
645        };
646
647        Node[] nodes = extractNodes(doc, xpath, nsArray);
648        return (nodes.length == 1)
649            ? XmlUtil.newDocument(nodes[0], true) : null;
650    }
651
652    /**
653     * Given a Document and an xpath/namespace-array pair, extract and return
654     * the Nodes resulting from applying the xpath.
655     *
656     * @throws SAXException on error
657     * @throws IOException on error
658     */
659    public static Node[] extractNodes(
660        Node node,
661        String xpath,
662        String[][] nsArray)
663        throws SAXException, IOException
664    {
665        Document contextDoc = XmlUtil.createContextDocument(nsArray);
666        Node[] nodes = XmlUtil.selectAsNodes(node, xpath, contextDoc);
667
668        if (LOGGER.isDebugEnabled()) {
669            StringBuilder buf = new StringBuilder(1024);
670            buf.append("XmlaSupport.extractNodes: ");
671            buf.append("nodes.length=");
672            buf.append(nodes.length);
673            buf.append(nl);
674            for (Node n : nodes) {
675                String str = XmlUtil.toString(n, false);
676                buf.append(str);
677                buf.append(nl);
678            }
679            LOGGER.debug(buf.toString());
680        }
681
682        return nodes;
683    }
684    /////////////////////////////////////////////////////////////////////////
685    // soap xmla file
686    /////////////////////////////////////////////////////////////////////////
687    /**
688     * Process the given input file as a SOAP-XMLA request.
689     *
690     */
691    public static byte[] processSoapXmla(
692        File file,
693        String connectString,
694        Map<String, String> catalogNameUrls,
695        String cbClassName)
696        throws IOException, ServletException, SAXException
697    {
698        String requestText = XmlaSupport.readFile(file);
699        return
700            processSoapXmla(
701                requestText, connectString,
702                catalogNameUrls, cbClassName, null, null);
703    }
704
705    public static byte[] processSoapXmla(
706        File file,
707        String connectString,
708        Map<String, String> catalogNameUrls,
709        String cbClassName,
710        Map<List<String>, Servlet> servletCache)
711        throws IOException, ServletException, SAXException
712    {
713        String requestText = XmlaSupport.readFile(file);
714        return
715            processSoapXmla(
716                requestText, connectString, catalogNameUrls, cbClassName,
717                null, servletCache);
718    }
719
720    public static byte[] processSoapXmla(
721        Document doc,
722        String connectString,
723        Map<String, String> catalogNameUrls,
724        String cbClassName,
725        Role role)
726        throws IOException, ServletException, SAXException
727    {
728        return processSoapXmla(
729            doc, connectString, catalogNameUrls, cbClassName, role,
730            null);
731    }
732
733    public static byte[] processSoapXmla(
734        Document doc,
735        String connectString,
736        Map<String, String> catalogNameUrls,
737        String cbClassName,
738        Role role,
739        Map<List<String>, Servlet> servletCache)
740        throws IOException, ServletException, SAXException
741    {
742        String requestText = XmlUtil.toString(doc, false);
743        return processSoapXmla(
744            requestText, connectString, catalogNameUrls, cbClassName, role,
745            servletCache);
746    }
747
748    public static byte[] processSoapXmla(
749        String requestText,
750        String connectString,
751        Map<String, String> catalogNameUrls,
752        String cbClassName,
753        Role role)
754        throws IOException, ServletException, SAXException
755    {
756        return
757            processSoapXmla(
758                requestText, connectString,
759                catalogNameUrls, cbClassName, role, null);
760    }
761
762    public static byte[] processSoapXmla(
763        String requestText,
764        String connectString,
765        Map<String, String> catalogNameUrls,
766        String cbClassName,
767        Role role,
768        Map<List<String>, Servlet> servletCache)
769        throws IOException, ServletException, SAXException
770    {
771        // read soap file
772        String dataSourceText =
773            XmlaSupport.getDataSourcesText(connectString, catalogNameUrls);
774        Util.PropertyList propertyList = Util.parseConnectString(connectString);
775        String roleName =
776            propertyList.get(RolapConnectionProperties.Role.name());
777
778        LockBox.Entry entry = null;
779        if (role != null) {
780            // We happen to know that the lock box is shared between servers.
781            // So we can create any old server; it doesn't need to pertain to
782            // this particular catalog.
783            final MondrianServer server = MondrianServer.forId(null);
784
785            entry = server.getLockBox().register(role);
786            roleName = entry.getMoniker();
787        }
788
789        byte[] reqBytes = requestText.getBytes();
790        MockHttpServletRequest req = new MockHttpServletRequest(reqBytes);
791        req.setMethod("POST");
792        req.setContentType("text/xml");
793        if (roleName != null) {
794            req.setUserInRole(roleName, true);
795        }
796        MockHttpServletResponse res = new MockHttpServletResponse();
797        res.setCharacterEncoding("UTF-8");
798        Servlet servlet = getServlet(cbClassName, dataSourceText, servletCache);
799        servlet.service(req, res);
800
801        // Even though it is not used, it is important that entry is in scope
802        // until after request has returned. Prevents role's lock box entry from
803        // being garbage collected.
804        Util.discard(entry);
805
806        return res.toByteArray();
807    }
808
809    public static Servlet makeServlet(
810        String connectString,
811        Map<String, String> catalogNameUrls,
812        String cbClassName)
813        throws IOException, ServletException, SAXException
814    {
815        return makeServlet(connectString, catalogNameUrls, cbClassName, null);
816    }
817
818    public static Servlet makeServlet(
819        String connectString,
820        Map<String, String> catalogNameUrls,
821        String cbClassName,
822        Map<List<String>, Servlet> servletCache)
823        throws IOException, ServletException, SAXException
824    {
825        // Create datasource file and put datasource xml into it.
826        // Mark it as delete on exit.
827        String dataSourceText =
828            XmlaSupport.getDataSourcesText(connectString, catalogNameUrls);
829
830        return getServlet(cbClassName, dataSourceText, servletCache);
831    }
832
833    private static Servlet getServlet(
834        String cbClassName,
835        String dataSourceText,
836        Map<List<String>, Servlet> cache)
837        throws ServletException
838    {
839        final List<String> key =
840            Arrays.asList(
841                dataSourceText);
842        Servlet servlet = cache.get(key);
843        if (servlet != null) {
844            return servlet;
845        }
846        MockServletContext servletContext = new MockServletContext();
847        MockServletConfig servletConfig = new MockServletConfig(servletContext);
848        servletConfig.addInitParameter(
849            XmlaServlet.PARAM_CALLBACKS, cbClassName);
850        servletConfig.addInitParameter(
851            XmlaServlet.PARAM_CHAR_ENCODING, "UTF-8");
852        servletConfig.addInitParameter(
853            XmlaServlet.PARAM_DATASOURCES_CONFIG,
854            "inline:" + dataSourceText);
855        servlet = new MondrianXmlaServlet();
856        servlet.init(servletConfig);
857        if (cache != null) {
858            cache.put(key, servlet);
859        }
860        return servlet;
861    }
862
863    public static byte[] processSoapXmla(
864        File file,
865        Servlet servlet)
866        throws IOException, ServletException, SAXException
867    {
868        String requestText = XmlaSupport.readFile(file);
869        return processSoapXmla(requestText, servlet);
870    }
871
872    public static byte[] processSoapXmla(
873        Document doc,
874        Servlet servlet)
875        throws IOException, ServletException, SAXException
876    {
877        String requestText = XmlUtil.toString(doc, false);
878        return processSoapXmla(requestText, servlet);
879    }
880
881    public static byte[] processSoapXmla(
882        String requestText,
883        Servlet servlet)
884        throws IOException, ServletException, SAXException
885    {
886        byte[] reqBytes = requestText.getBytes();
887        // make request
888        MockHttpServletRequest req = new MockHttpServletRequest(reqBytes);
889        req.setMethod("POST");
890        req.setContentType("text/xml");
891
892        // make response
893        MockHttpServletResponse res = new MockHttpServletResponse();
894        res.setCharacterEncoding("UTF-8");
895
896        servlet.service(req, res);
897
898        return res.toByteArray();
899    }
900
901
902
903    /**
904     * Check is a byte array containing a SOAP-XMLA response method is valid.
905     * Schema validation occurs if the XMLA response contains both a content
906     * and schmema section. This includes both the SOAP elements and the
907     * SOAP body content, the XMLA response.
908     *
909     */
910    public static boolean validateSchemaSoapXmla(byte[] bytes)
911        throws SAXException, IOException,
912               ParserConfigurationException,
913               TransformerException
914    {
915        return validateEmbeddedSchema(
916            bytes,
917            XmlUtil.getSoapXmlaXds2xs("xmla"),
918            XmlUtil.getSoapXmlaXds2xd("xmla"));
919    }
920
921
922    /////////////////////////////////////////////////////////////////////////
923    // xmla file
924    /////////////////////////////////////////////////////////////////////////
925
926    /**
927     * Processes the given input file as an XMLA request (no SOAP elements).
928     */
929    public static byte[] processXmla(
930        File file,
931        String connectString,
932        Map<String, String> catalogNameUrls)
933        throws IOException, SAXException, XOMException
934    {
935        return
936            processXmla(
937                file, connectString, catalogNameUrls,
938                (Map<List<String>, MondrianServer>)null);
939    }
940
941    public static byte[] processXmla(
942        File file,
943        String connectString,
944        Map<String, String> catalogNameUrls,
945        Map<List<String>, MondrianServer> cache)
946        throws IOException, SAXException, XOMException
947    {
948        return
949            processXmla(
950                file, connectString, catalogNameUrls, null, cache);
951    }
952
953    public static byte[] processXmla(
954        File file,
955        String connectString,
956        Map<String, String> catalogNameUrls,
957        Role role)
958        throws IOException, SAXException, XOMException
959    {
960        return
961            processXmla(
962                file,
963                connectString,
964                catalogNameUrls,
965                role,
966                null);
967    }
968
969    public static byte[] processXmla(
970        File file,
971        String connectString,
972        Map<String, String> catalogNameUrls,
973        Role role,
974        Map<List<String>, MondrianServer> cache)
975        throws IOException, SAXException, XOMException
976    {
977        String requestText = XmlaSupport.readFile(file);
978        return processXmla(requestText, connectString, catalogNameUrls, cache);
979    }
980
981    public static byte[] processXmla(
982        String requestText,
983        String connectString,
984        Map<String, String> catalogNameUrls)
985        throws IOException, SAXException, XOMException
986    {
987        return
988            processXmla(
989                requestText, connectString, catalogNameUrls,
990                (Map<List<String>, MondrianServer>)null);
991    }
992
993    public static byte[] processXmla(
994        String requestText,
995        String connectString,
996        Map<String, String> catalogNameUrls,
997        Map<List<String>, MondrianServer> cache)
998        throws IOException, SAXException, XOMException
999    {
1000        Document requestDoc = XmlUtil.parseString(requestText);
1001        return
1002            processXmla(
1003                requestDoc, connectString, catalogNameUrls, null, cache);
1004    }
1005
1006    public static byte[] processXmla(
1007        Document requestDoc,
1008        String connectString,
1009        Map<String, String> catalogNameUrls,
1010        Role role)
1011        throws IOException, XOMException
1012    {
1013        return
1014            processXmla(
1015                requestDoc, connectString,
1016                catalogNameUrls, role, null);
1017    }
1018
1019    public static byte[] processXmla(
1020        Document requestDoc,
1021        String connectString,
1022        Map<String, String> catalogNameUrls,
1023        Role role,
1024        Map<List<String>, MondrianServer> cache)
1025        throws IOException, XOMException
1026    {
1027        Element requestElem = requestDoc.getDocumentElement();
1028        return
1029            processXmla(
1030                requestElem, connectString, catalogNameUrls, role, cache);
1031    }
1032
1033    public static byte[] processXmla(
1034        Element requestElem,
1035        String connectString,
1036        Map<String, String> catalogNameUrls,
1037        Role role)
1038        throws IOException, XOMException
1039    {
1040        return
1041            processXmla(
1042                requestElem, connectString,
1043                catalogNameUrls, role, null);
1044    }
1045
1046    public static byte[] processXmla(
1047        Element requestElem,
1048        String connectString,
1049        Map<String, String> catalogNameUrls,
1050        Role role,
1051        Map<List<String>, MondrianServer> cache)
1052        throws IOException, XOMException
1053    {
1054        Util.PropertyList propertyList =
1055            Util.parseConnectString(connectString);
1056        final List<String> cacheKey =
1057            Arrays.asList(
1058                propertyList.toString(),
1059                catalogNameUrls.toString());
1060        MondrianServer server = cache.get(cacheKey);
1061        if (server == null) {
1062            server = MondrianServer.createWithRepository(
1063                new StringRepositoryContentFinder(
1064                    getDataSourcesText(
1065                        connectString, catalogNameUrls)),
1066                getCatalogLocator());
1067        }
1068        if (cache != null) {
1069            cache.put(
1070                cacheKey,
1071                server);
1072        }
1073
1074        // make request
1075        final XmlaHandler handler =
1076            new XmlaHandler(
1077                (XmlaHandler.ConnectionFactory) server,
1078                "xmla");
1079
1080        String roleName =
1081            propertyList.get(RolapConnectionProperties.Role.name());
1082
1083        LockBox.Entry entry = null;
1084        if (role != null) {
1085            entry = server.getLockBox().register(role);
1086            roleName = entry.getMoniker();
1087        }
1088
1089        XmlaRequest request =
1090            new DefaultXmlaRequest(requestElem, roleName, null, null, null);
1091
1092        Enumeration.ResponseMimeType responseMimeType =
1093            Enumeration.ResponseMimeType.MAP.get(
1094                request.getProperties().get(
1095                    PropertyDefinition.ResponseMimeType.name()));
1096        if (responseMimeType == null) {
1097            responseMimeType = Enumeration.ResponseMimeType.SOAP;
1098        }
1099
1100        // make response
1101        ByteArrayOutputStream resBuf = new ByteArrayOutputStream();
1102        XmlaResponse response =
1103            new DefaultXmlaResponse(resBuf, "UTF-8", responseMimeType);
1104
1105        handler.process(request, response);
1106
1107        // Even though it is not used, it is important that entry is in scope
1108        // until after request has returned. Prevents role's lock box entry from
1109        // being garbage collected.
1110        Util.discard(entry);
1111
1112        return resBuf.toByteArray();
1113    }
1114
1115    /**
1116     * Check is a byte array containing a XMLA response method is valid.
1117     * Schema validation occurs if the XMLA response contains both a content
1118     * and schmema section. This should not be used when the byte array
1119     * contains both the SOAP elements and content, but only for the content.
1120     *
1121     */
1122    public static boolean validateSchemaXmla(byte[] bytes)
1123        throws SAXException, IOException,
1124               ParserConfigurationException,
1125               TransformerException
1126    {
1127        return validateEmbeddedSchema(
1128            bytes,
1129            XmlUtil.getXmlaXds2xs("xmla"),
1130            XmlUtil.getXmlaXds2xd("xmla"));
1131    }
1132
1133    /////////////////////////////////////////////////////////////////////////
1134    // helpers
1135    /////////////////////////////////////////////////////////////////////////
1136
1137    /**
1138     * This validates a SOAP-XMLA response using xpaths to extract the
1139     * schema and data parts. In addition, it does a little surgery on
1140     * the DOMs removing the schema nodes from the XMLA root node.
1141     */
1142    public static boolean validateSoapXmlaUsingXpath(byte[] bytes)
1143        throws SAXException, IOException
1144    {
1145        if (! XmlUtil.supportsValidation()) {
1146            return false;
1147        }
1148
1149        // Remove the UTF BOM for proper validation.
1150        bytes = removeUtfBom(bytes);
1151
1152        Node[] nodes = extractNodesFromSoapXmla(bytes);
1153        return validateNodes(nodes);
1154    }
1155
1156    private static byte[] removeUtfBom(byte[] s) {
1157        byte[] response = removeUtfBom(s, UTF8_BOM_MATCHER);
1158        if (response != null) {
1159            return response;
1160        }
1161        response = removeUtfBom(s, UTF16_BE_BOM_MATCHER);
1162        if (response != null) {
1163            return response;
1164        }
1165        response = removeUtfBom(s, UTF16_LE_BOM_MATCHER);
1166        if (response != null) {
1167            return response;
1168        }
1169        response = removeUtfBom(s, UTF32_BE_BOM_MATCHER);
1170        if (response != null) {
1171            return response;
1172        }
1173        response = removeUtfBom(s, UTF32_LE_BOM_MATCHER);
1174        if (response != null) {
1175            return response;
1176        }
1177        return s;
1178    }
1179
1180    private static byte[] removeUtfBom(byte[] s, ByteMatcher matcher) {
1181        byte[] firstBytes = new byte[matcher.key.length];
1182        System.arraycopy(s, 0, firstBytes, 0, matcher.key.length);
1183        if (s.length >= matcher.key.length
1184            && matcher.match(firstBytes) == 0)
1185        {
1186            byte[] result = new byte[s.length - matcher.key.length];
1187            System.arraycopy(s, 0, result, 0, s.length - matcher.key.length);
1188            return result;
1189        }
1190        return null;
1191    }
1192
1193    /**
1194     * This validates a XMLA response using xpaths to extract the
1195     * schema and data parts. In addition, it does a little surgery on
1196     * the DOMs removing the schema nodes from the XMLA root node.
1197     *
1198     */
1199    public static boolean validateXmlaUsingXpath(byte[] bytes)
1200        throws SAXException, IOException
1201    {
1202        if (! XmlUtil.supportsValidation()) {
1203            return false;
1204        }
1205
1206        // Remove the UTF BOM for proper validation.
1207        bytes = removeUtfBom(bytes);
1208
1209        Node[] nodes = extractNodesFromXmla(bytes);
1210        return validateNodes(nodes);
1211    }
1212
1213    /**
1214     * Validate Nodes with throws an error if validation was attempted but
1215     * failed, returns true if validation was successful and false if
1216     * validation was not tried.
1217     *
1218     * @return  true if validation succeeded, false if validation was not tried
1219     */
1220    public static boolean validateNodes(Node[] nodes)
1221        throws SAXException, IOException
1222    {
1223        if (! XmlUtil.supportsValidation()) {
1224            return false;
1225        }
1226        if (nodes.length == 0) {
1227            // no nodes
1228            return false;
1229        } else if (nodes.length == 1) {
1230            // only data or schema but not both
1231            return false;
1232        } else if (nodes.length > 2) {
1233            // TODO: error
1234            return false;
1235        }
1236
1237        Node schemaNode = nodes[0];
1238        Node rowNode = nodes[1];
1239
1240        // This is the "root" node that contains both the schemaNode and
1241        // the rowNode.
1242        Node rootNode = rowNode.getParentNode();
1243        // Remove the schemaNode from the root Node.
1244        rootNode.removeChild(schemaNode);
1245
1246        // Convert nodes to Documents.
1247        Document schemaDoc = XmlUtil.newDocument(schemaNode, true);
1248        Document dataDoc = XmlUtil.newDocument(rootNode, true);
1249
1250        String xmlns = XmlUtil.getNamespaceAttributeValue(dataDoc);
1251        String schemaLocationPropertyValue = xmlns + ' ' + "xmlschema";
1252        org.xml.sax.EntityResolver resolver = new XmlUtil.Resolver(schemaDoc);
1253        XmlUtil.validate(dataDoc, schemaLocationPropertyValue, resolver);
1254
1255        return true;
1256    }
1257
1258
1259    /**
1260     * See next method for JavaDoc {@link #validateEmbeddedSchema(org.w3c.dom.Document, String, String)}.
1261     *
1262     */
1263    public static boolean validateEmbeddedSchema(
1264        byte[] bytes,
1265        String schemaTransform,
1266        String dataTransform)
1267        throws SAXException, IOException,
1268               ParserConfigurationException,
1269               TransformerException,
1270               TransformerConfigurationException
1271    {
1272        if (! XmlUtil.supportsValidation()) {
1273            return false;
1274        }
1275
1276        Document doc = XmlUtil.parse(bytes);
1277        return validateEmbeddedSchema(doc, schemaTransform, dataTransform);
1278    }
1279
1280    /**
1281     * A given Document has both content and an embedded schema (where
1282     * the schema has a single root node and the content has a single
1283     * root node - they are not interwoven). A single xsl transform is
1284     * provided to extract the schema part of the Document and another
1285     * xsl transform is provided to extract the content part and then
1286     * the content is validated against the schema.
1287     * <p>
1288     * If the content is valid, then nothing happens, but if the content
1289     * is not valid an execption is thrown (currently a RuntimeException).
1290     * <p>
1291     * When Mondrian moves to Java 5 or includes the JAXP 1.3 jar, then
1292     * there is a utility in JAXP that does something like this (but allows
1293     * for multiple schema/content parts).
1294     *
1295     */
1296    public static boolean validateEmbeddedSchema(
1297        Document doc,
1298        String schemaTransform,
1299        String dataTransform)
1300        throws SAXException, IOException,
1301               ParserConfigurationException,
1302               TransformerException,
1303               TransformerConfigurationException
1304    {
1305        if (! XmlUtil.supportsValidation()) {
1306            return false;
1307        }
1308
1309        Node dataDoc = XmlUtil.transform(
1310            doc,
1311            new BufferedReader(new StringReader(dataTransform)));
1312        if (dataDoc == null) {
1313            LOGGER.debug("XmlaSupport.validateEmbeddedSchema: dataDoc is null");
1314            return false;
1315        }
1316        if (! dataDoc.hasChildNodes()) {
1317            LOGGER.debug(
1318                "XmlaSupport.validateEmbeddedSchema: dataDoc has no children");
1319            return false;
1320        }
1321        String dataStr = XmlUtil.toString(dataDoc, false);
1322        if (LOGGER.isDebugEnabled()) {
1323            LOGGER.debug(
1324                "XmlaSupport.validateEmbeddedSchema: dataDoc:\n=" + dataStr);
1325        }
1326        if (! (dataDoc instanceof Document)) {
1327            LOGGER.warn(
1328                "XmlaSupport.validateEmbeddedSchema: dataDoc not Document");
1329            return false;
1330        }
1331
1332
1333        Node schemaDoc = XmlUtil.transform(
1334            doc,
1335            new BufferedReader(new StringReader(schemaTransform)));
1336        if (schemaDoc == null) {
1337            LOGGER.debug(
1338                "XmlaSupport.validateEmbeddedSchema: schemaDoc is null");
1339            return false;
1340        }
1341        if (! schemaDoc.hasChildNodes()) {
1342            LOGGER.debug(
1343                "XmlaSupport.validateEmbeddedSchema: "
1344                + "schemaDoc has no children");
1345            return false;
1346        }
1347        String schemaStr = XmlUtil.toString(schemaDoc, false);
1348        if (LOGGER.isDebugEnabled()) {
1349            LOGGER.debug(
1350                "XmlaSupport.validateEmbeddedSchema: schemaDoc:\n="
1351                + schemaStr);
1352        }
1353        if (! (schemaDoc instanceof Document)) {
1354            LOGGER.warn(
1355                "XmlaSupport.validateEmbeddedSchema: schemaDoc not Document");
1356            return false;
1357        }
1358
1359
1360        String xmlns = XmlUtil.getNamespaceAttributeValue((Document)dataDoc);
1361        String schemaLocationPropertyValue = xmlns + ' ' + "xmlschema";
1362        org.xml.sax.EntityResolver resolver = new XmlUtil.Resolver(schemaStr);
1363        XmlUtil.validate(dataStr, schemaLocationPropertyValue, resolver);
1364
1365        return true;
1366    }
1367
1368    public static Document transformSoapXmla(
1369        Document doc, String[][] namevalueParameters, String ns)
1370        throws SAXException, IOException,
1371        ParserConfigurationException,
1372        TransformerException
1373    {
1374        Node node = XmlUtil.transform(
1375            doc,
1376            new BufferedReader(new StringReader(getXmlaTransform(ns))),
1377            namevalueParameters);
1378
1379        return (node instanceof Document) ? (Document) node : null;
1380    }
1381
1382
1383    /**
1384     * Reads a file line by line, adds a '\n' after each line and
1385     * returns in a String.
1386     *
1387     */
1388    public static String readFile(File file) throws IOException {
1389        StringBuilder buf = new StringBuilder(1024);
1390        BufferedReader reader = null;
1391        try {
1392            reader = new BufferedReader(new FileReader(file));
1393            String line;
1394            while ((line = reader.readLine()) != null) {
1395                buf.append(line);
1396                buf.append('\n');
1397            }
1398        } finally {
1399            if (reader != null) {
1400                try {
1401                    reader.close();
1402                } catch (Exception ignored) {
1403                }
1404            }
1405        }
1406
1407        return buf.toString();
1408    }
1409
1410
1411    private XmlaSupport() {
1412    }
1413}
1414
1415// End XmlaSupport.java