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) 2003-2005 Julian Hyde
008// Copyright (C) 2005-2011 Pentaho
009// All Rights Reserved.
010*/
011package mondrian.xmla;
012
013import mondrian.olap.MondrianException;
014import mondrian.olap.Util;
015import mondrian.xmla.impl.DefaultXmlaResponse;
016
017import org.olap4j.OlapConnection;
018import org.olap4j.OlapException;
019
020import org.w3c.dom.*;
021import org.xml.sax.InputSource;
022
023import java.io.*;
024import java.nio.charset.Charset;
025import java.sql.SQLException;
026import java.util.*;
027import java.util.concurrent.ConcurrentHashMap;
028import java.util.regex.Pattern;
029import javax.xml.parsers.DocumentBuilder;
030import javax.xml.parsers.DocumentBuilderFactory;
031import javax.xml.transform.Transformer;
032import javax.xml.transform.TransformerFactory;
033import javax.xml.transform.dom.DOMSource;
034import javax.xml.transform.stream.StreamResult;
035
036import static org.olap4j.metadata.XmlaConstants.Format;
037import static org.olap4j.metadata.XmlaConstants.Method;
038
039/**
040 * Utility methods for XML/A implementation.
041 *
042 * @author Gang Chen
043 */
044public class XmlaUtil implements XmlaConstants {
045
046    /**
047     * Invalid characters for XML element name.
048     *
049     * <p>XML element name:
050     *
051     * Char ::= #x9 | #xA | #xD | [#x20-#xD7FF]
052     *        | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
053     * S ::= (#x20 | #x9 | #xD | #xA)+
054     * NameChar ::= Letter | Digit | '.' | '-' | '_' | ':' | CombiningChar
055     *        | Extender
056     * Name ::= (Letter | '_' | ':') (NameChar)*
057     * Names ::= Name (#x20 Name)*
058     * Nmtoken ::= (NameChar)+
059     * Nmtokens ::= Nmtoken (#x20 Nmtoken)*
060     *
061     */
062    private static final String[] CHAR_TABLE = new String[256];
063    private static final Pattern LOWERCASE_PATTERN =
064        Pattern.compile(".*[a-z].*");
065
066    static {
067        initCharTable(" \t\r\n(){}[]+/*%!,?");
068    }
069
070    private static void initCharTable(String charStr) {
071        char[] chars = charStr.toCharArray();
072        for (char c : chars) {
073            CHAR_TABLE[c] = encodeChar(c);
074        }
075    }
076
077    private static String encodeChar(char c) {
078        StringBuilder buf = new StringBuilder();
079        buf.append("_x");
080        String str = Integer.toHexString(c);
081        for (int i = 4 - str.length(); i > 0; i--) {
082            buf.append("0");
083        }
084        return buf.append(str).append("_").toString();
085    }
086
087    /**
088     * Encodes an XML element name.
089     *
090     * <p>This function is mainly for encode element names in result of Drill
091     * Through execute, because its element names come from database, we cannot
092     * make sure they are valid XML contents.
093     *
094     * <p>Quoth the <a href="http://xmla.org">XML/A specification</a>, version
095     * 1.1:
096     * <blockquote>
097     * XML does not allow certain characters as element and attribute names.
098     * XML for Analysis supports encoding as defined by SQL Server 2000 to
099     * address this XML constraint. For column names that contain invalid XML
100     * name characters (according to the XML 1.0 specification), the nonvalid
101     * Unicode characters are encoded using the corresponding hexadecimal
102     * values. These are escaped as _x<i>HHHH_</i> where <i>HHHH</i> stands for
103     * the four-digit hexadecimal UCS-2 code for the character in
104     * most-significant bit first order. For example, the name "Order Details"
105     * is encoded as Order_<i>x0020</i>_Details, where the space character is
106     * replaced by the corresponding hexadecimal code.
107     * </blockquote>
108     *
109     * @param name Name of XML element
110     * @return encoded name
111     */
112    private static String encodeElementName(String name) {
113        StringBuilder buf = new StringBuilder();
114        char[] nameChars = name.toCharArray();
115        for (char ch : nameChars) {
116            String encodedStr =
117                (ch >= CHAR_TABLE.length ? null : CHAR_TABLE[ch]);
118            if (encodedStr == null) {
119                buf.append(ch);
120            } else {
121                buf.append(encodedStr);
122            }
123        }
124        return buf.toString();
125    }
126
127
128    public static void element2Text(Element elem, final StringWriter writer)
129        throws XmlaException
130    {
131        try {
132            TransformerFactory factory = TransformerFactory.newInstance();
133            Transformer transformer = factory.newTransformer();
134            transformer.transform(
135                new DOMSource(elem),
136                new StreamResult(writer));
137        } catch (Exception e) {
138            throw new XmlaException(
139                CLIENT_FAULT_FC,
140                USM_DOM_PARSE_CODE,
141                USM_DOM_PARSE_FAULT_FS,
142                e);
143        }
144    }
145
146    public static Element text2Element(String text)
147        throws XmlaException
148    {
149        return _2Element(new InputSource(new StringReader(text)));
150    }
151
152    public static Element stream2Element(InputStream stream)
153        throws XmlaException
154    {
155        return _2Element(new InputSource(stream));
156    }
157
158    private static Element _2Element(InputSource source)
159        throws XmlaException
160    {
161        try {
162            DocumentBuilderFactory factory =
163                DocumentBuilderFactory.newInstance();
164            factory.setIgnoringElementContentWhitespace(true);
165            factory.setIgnoringComments(true);
166            factory.setNamespaceAware(true);
167            DocumentBuilder builder = factory.newDocumentBuilder();
168            Document doc = builder.parse(source);
169            return doc.getDocumentElement();
170        } catch (Exception e) {
171            throw new XmlaException(
172                CLIENT_FAULT_FC,
173                USM_DOM_PARSE_CODE,
174                USM_DOM_PARSE_FAULT_FS,
175                e);
176        }
177    }
178
179    public static Element[] filterChildElements(
180        Element parent,
181        String ns,
182        String lname)
183    {
184/*
185way too noisy
186        if (LOGGER.isDebugEnabled()) {
187            StringBuilder buf = new StringBuilder(100);
188            buf.append("XmlaUtil.filterChildElements: ");
189            buf.append(" ns=\"");
190            buf.append(ns);
191            buf.append("\", lname=\"");
192            buf.append(lname);
193            buf.append("\"");
194            LOGGER.debug(buf.toString());
195        }
196*/
197
198        List<Element> elems = new ArrayList<Element>();
199        NodeList nlst = parent.getChildNodes();
200        for (int i = 0, nlen = nlst.getLength(); i < nlen; i++) {
201            Node n = nlst.item(i);
202            if (n instanceof Element) {
203                Element e = (Element) n;
204                if ((ns == null || ns.equals(e.getNamespaceURI()))
205                    && (lname == null || lname.equals(e.getLocalName())))
206                {
207                    elems.add(e);
208                }
209            }
210        }
211        return elems.toArray(new Element[elems.size()]);
212    }
213
214    public static String textInElement(Element elem) {
215        StringBuilder buf = new StringBuilder(100);
216        elem.normalize();
217        NodeList nlst = elem.getChildNodes();
218        for (int i = 0, nlen = nlst.getLength(); i < nlen ; i++) {
219            Node n = nlst.item(i);
220            if (n instanceof Text) {
221                final String data = ((Text) n).getData();
222                buf.append(data);
223            }
224        }
225        return buf.toString();
226    }
227
228    /**
229     * Finds root MondrianException in exception chain if exists,
230     * otherwise the input throwable.
231     *
232     * @param throwable Exception
233     * @return Root exception
234     */
235    public static Throwable rootThrowable(Throwable throwable) {
236        Throwable rootThrowable = throwable.getCause();
237        if (rootThrowable != null
238            && rootThrowable instanceof MondrianException)
239        {
240            return rootThrowable(rootThrowable);
241        }
242        return throwable;
243    }
244
245    /**
246     * Corrects for the differences between numeric strings arising because
247     * JDBC drivers use different representations for numbers
248     * ({@link Double} vs. {@link java.math.BigDecimal}) and
249     * these have different toString() behavior.
250     *
251     * <p>If it contains a decimal point, then
252     * strip off trailing '0's. After stripping off
253     * the '0's, if there is nothing right of the
254     * decimal point, then strip off decimal point.
255     *
256     * @param numericStr Numeric string
257     * @return Normalized string
258     */
259    public static String normalizeNumericString(String numericStr) {
260        int index = numericStr.indexOf('.');
261        if (index > 0) {
262            // If it uses exponential notation, 1.0E4, then it could
263            // have a trailing '0' that should not be stripped of,
264            // e.g., 1.0E10. This would be rather bad.
265            if (numericStr.indexOf('e') != -1) {
266                return numericStr;
267            } else if (numericStr.indexOf('E') != -1) {
268                return numericStr;
269            }
270
271            boolean found = false;
272            int p = numericStr.length();
273            char c = numericStr.charAt(p - 1);
274            while (c == '0') {
275                found = true;
276                p--;
277                c = numericStr.charAt(p - 1);
278            }
279            if (c == '.') {
280                p--;
281            }
282            if (found) {
283                return numericStr.substring(0, p);
284            }
285        }
286        return numericStr;
287    }
288
289    /**
290     * Returns a set of column headings and rows for a given metadata request.
291     *
292     * <p/>Leverages mondrian's implementation of the XML/A specification, and
293     * is exposed here for use by mondrian's olap4j driver.
294     *
295     * @param connection Connection
296     * @param methodName Metadata method name per XMLA (e.g. "MDSCHEMA_CUBES")
297     * @param restrictionMap Restrictions
298     * @return Set of rows and column headings
299     */
300    public static MetadataRowset getMetadataRowset(
301        final OlapConnection connection,
302        String methodName,
303        final Map<String, Object> restrictionMap)
304        throws OlapException
305    {
306        RowsetDefinition rowsetDefinition =
307            RowsetDefinition.valueOf(methodName);
308
309        final XmlaHandler.ConnectionFactory connectionFactory =
310            new XmlaHandler.ConnectionFactory() {
311                public OlapConnection getConnection(
312                    String catalog, String schema, String roleName,
313                    Properties props)
314                    throws SQLException
315                {
316                    return connection;
317                }
318
319                public Map<String, Object>
320                getPreConfiguredDiscoverDatasourcesResponse()
321                {
322                    // This method should not be used by the olap4j xmla
323                    // servlet. For the mondrian xmla servlet we don't provide
324                    // the "pre configured discover datasources" feature.
325                    return null;
326                }
327            };
328        final XmlaRequest request = new XmlaRequest() {
329            public Method getMethod() {
330                return Method.DISCOVER;
331            }
332
333            public Map<String, String> getProperties() {
334                return Collections.emptyMap();
335            }
336
337            public Map<String, Object> getRestrictions() {
338                return restrictionMap;
339            }
340
341            public String getStatement() {
342                return null;
343            }
344
345            public String getRoleName() {
346                return null;
347            }
348
349            public String getRequestType() {
350                throw new UnsupportedOperationException();
351            }
352
353            public boolean isDrillThrough() {
354                throw new UnsupportedOperationException();
355            }
356
357            public Format getFormat() {
358                throw new UnsupportedOperationException();
359            }
360
361            public String getUsername() {
362                return null;
363            }
364
365            public String getPassword() {
366                return null;
367            }
368
369            public String getSessionId() {
370                return null;
371            }
372        };
373        final Rowset rowset =
374            rowsetDefinition.getRowset(
375                request,
376                new XmlaHandler(
377                    connectionFactory,
378                    "xmla")
379                {
380                    @Override
381                    public OlapConnection getConnection(
382                        XmlaRequest request,
383                        Map<String, String> propMap)
384                    {
385                        return connection;
386                    }
387                }
388            );
389        List<Rowset.Row> rowList = new ArrayList<Rowset.Row>();
390        rowset.populate(
391            new DefaultXmlaResponse(
392                new ByteArrayOutputStream(),
393                Charset.defaultCharset().name(),
394                Enumeration.ResponseMimeType.SOAP),
395            connection,
396            rowList);
397        MetadataRowset result = new MetadataRowset();
398        final List<RowsetDefinition.Column> colDefs =
399            new ArrayList<RowsetDefinition.Column>();
400        for (RowsetDefinition.Column columnDefinition
401            : rowsetDefinition.columnDefinitions)
402        {
403            if (columnDefinition.type == RowsetDefinition.Type.Rowset) {
404                // olap4j does not support the extended columns, e.g.
405                // Cube.Dimensions
406                continue;
407            }
408            colDefs.add(columnDefinition);
409        }
410        for (Rowset.Row row : rowList) {
411            Object[] values = new Object[colDefs.size()];
412            int k = -1;
413            for (RowsetDefinition.Column colDef : colDefs) {
414                Object o = row.get(colDef.name);
415                if (o instanceof List) {
416                    o = toString((List<String>) o);
417                } else if (o instanceof String[]) {
418                    o = toString(Arrays.asList((String []) o));
419                }
420                values[++k] = o;
421            }
422            result.rowList.add(Arrays.asList(values));
423        }
424        for (RowsetDefinition.Column colDef : colDefs) {
425            String columnName = colDef.name;
426            if (LOWERCASE_PATTERN.matcher(columnName).matches()) {
427                columnName = Util.camelToUpper(columnName);
428            }
429            // VALUE is a SQL reserved word
430            if (columnName.equals("VALUE")) {
431                columnName = "PROPERTY_VALUE";
432            }
433            result.headerList.add(columnName);
434        }
435        return result;
436    }
437
438    private static <T> String toString(List<T> list) {
439        StringBuilder buf = new StringBuilder();
440        int k = -1;
441        for (T t : list) {
442            if (++k > 0) {
443                buf.append(", ");
444            }
445            buf.append(t);
446        }
447        return buf.toString();
448    }
449
450    /**
451     * Chooses the appropriate response mime type given an HTTP "Accept" header.
452     *
453     * <p>The header can contain a list of mime types and optional qualities,
454     * for example "text/html,application/xhtml+xml,application/xml;q=0.9"
455     *
456     * @param accept Accept header
457     * @return Mime type, or null if none is acceptable
458     */
459    public static Enumeration.ResponseMimeType chooseResponseMimeType(
460        String accept)
461    {
462        for (String s : accept.split(",")) {
463            s = s.trim();
464            final int semicolon = s.indexOf(";");
465            if (semicolon >= 0) {
466                s = s.substring(0, semicolon);
467            }
468            Enumeration.ResponseMimeType mimeType =
469                Enumeration.ResponseMimeType.MAP.get(s);
470            if (mimeType != null) {
471                return mimeType;
472            }
473        }
474        return null;
475    }
476
477    /**
478     * Returns whether an XMLA request should return invisible members.
479     *
480     * <p>According to the XMLA spec, it should not. But we allow the client to
481     * specify different behavior. In particular, the olap4j driver for XMLA
482     * may need to access invisible members.
483     *
484     * <p>Returns true if the EmitInvisibleMembers property is specified and
485     * equal to "true".
486     *
487     * @param request XMLA request
488     * @return Whether to return invisible members
489     */
490    public static boolean shouldEmitInvisibleMembers(XmlaRequest request) {
491        final String value =
492            request.getProperties().get(
493                PropertyDefinition.EmitInvisibleMembers.name());
494        return Boolean.parseBoolean(value);
495    }
496
497    /**
498     * Result of a metadata query.
499     */
500    public static class MetadataRowset {
501        public final List<String> headerList = new ArrayList<String>();
502        public final List<List<Object>> rowList = new ArrayList<List<Object>>();
503    }
504
505    /**
506     * Wrapper which indicates that a restriction is to be treated as a
507     * SQL-style wildcard match.
508     */
509    public static class Wildcard {
510        public final String pattern;
511
512        public Wildcard(String pattern) {
513            this.pattern = pattern;
514        }
515    }
516
517    public static class ElementNameEncoder {
518        private final Map<String, String> map =
519            new ConcurrentHashMap<String, String>();
520        public static final ElementNameEncoder INSTANCE =
521            new ElementNameEncoder();
522
523        public String encode(String name) {
524            String encoded = map.get(name);
525            if (encoded == null) {
526                encoded = encodeElementName(name);
527                map.put(name, encoded);
528            }
529            return encoded;
530        }
531    }
532}
533
534// End XmlaUtil.java