001    /*
002    // $Id: //open/mondrian/src/main/mondrian/xmla/XmlaUtil.java#25 $
003    // This software is subject to the terms of the Common Public License
004    // Agreement, available at the following URL:
005    // http://www.opensource.org/licenses/cpl.html.
006    // Copyright (C) 2003-2008 Julian Hyde
007    // All Rights Reserved.
008    // You must accept the terms of that agreement to use this software.
009    //
010    // jhyde, May 2, 2003
011    */
012    package mondrian.xmla;
013    
014    import mondrian.olap.*;
015    import mondrian.xmla.impl.DefaultXmlaResponse;
016    import org.apache.log4j.Logger;
017    import org.w3c.dom.*;
018    import org.xml.sax.InputSource;
019    
020    import javax.xml.parsers.DocumentBuilder;
021    import javax.xml.parsers.DocumentBuilderFactory;
022    import javax.xml.transform.Transformer;
023    import javax.xml.transform.TransformerFactory;
024    import javax.xml.transform.dom.DOMSource;
025    import javax.xml.transform.stream.StreamResult;
026    import java.io.*;
027    import java.util.*;
028    import java.util.regex.Pattern;
029    import java.nio.charset.Charset;
030    
031    /**
032     * Utility methods for XML/A implementation.
033     *
034     * @author Gang Chen
035     * @version $Id: //open/mondrian/src/main/mondrian/xmla/XmlaUtil.java#25 $
036     */
037    public class XmlaUtil implements XmlaConstants {
038    
039        private static final Logger LOGGER = Logger.getLogger(XmlaUtil.class);
040        /**
041         * Invalid characters for XML element name.
042         *
043         * <p>XML element name:
044         *
045         * Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
046         * S ::= (#x20 | #x9 | #xD | #xA)+
047         * NameChar ::= Letter | Digit | '.' | '-' | '_' | ':' | CombiningChar | Extender
048         * Name ::= (Letter | '_' | ':') (NameChar)*
049         * Names ::= Name (#x20 Name)*
050         * Nmtoken ::= (NameChar)+
051         * Nmtokens ::= Nmtoken (#x20 Nmtoken)*
052         *
053         */
054        private static final String[] CHAR_TABLE = new String[256];
055        private static final Pattern LOWERCASE_PATTERN = Pattern.compile(".*[a-z].*");
056    
057        static {
058            initCharTable(" \t\r\n(){}[]+/*%!,?");
059        }
060    
061        private static void initCharTable(String charStr) {
062            char[] chars = charStr.toCharArray();
063            for (char c : chars) {
064                CHAR_TABLE[c] = encodeChar(c);
065            }
066        }
067    
068        private static String encodeChar(char c) {
069            StringBuilder buf = new StringBuilder();
070            buf.append("_x");
071            String str = Integer.toHexString(c);
072            for (int i = 4 - str.length(); i > 0; i--) {
073                buf.append("0");
074            }
075            return buf.append(str).append("_").toString();
076        }
077    
078        /**
079         * Encodes an XML element name.
080         *
081         * <p>This function is mainly for encode element names in result of Drill
082         * Through execute, because its element names come from database, we cannot
083         * make sure they are valid XML contents.
084         *
085         * <p>Quoth the <a href="http://xmla.org">XML/A specification</a>, version
086         * 1.1:
087         * <blockquote>
088         * XML does not allow certain characters as element and attribute names.
089         * XML for Analysis supports encoding as defined by SQL Server 2000 to
090         * address this XML constraint. For column names that contain invalid XML
091         * name characters (according to the XML 1.0 specification), the nonvalid
092         * Unicode characters are encoded using the corresponding hexadecimal
093         * values. These are escaped as _x<i>HHHH_</i> where <i>HHHH</i> stands for
094         * the four-digit hexadecimal UCS-2 code for the character in
095         * most-significant bit first order. For example, the name "Order Details"
096         * is encoded as Order_<i>x0020</i>_Details, where the space character is
097         * replaced by the corresponding hexadecimal code.
098         * </blockquote>
099         *
100         * @param name Name of XML element
101         * @return encoded name
102         */
103        public static String encodeElementName(String name) {
104            StringBuilder buf = new StringBuilder();
105            char[] nameChars = name.toCharArray();
106            for (char ch : nameChars) {
107                String encodedStr = (ch >= CHAR_TABLE.length ? null : CHAR_TABLE[ch]);
108                if (encodedStr == null) {
109                    buf.append(ch);
110                } else {
111                    buf.append(encodedStr);
112                }
113            }
114            return buf.toString();
115        }
116    
117    
118        public static String element2Text(Element elem)
119                throws XmlaException {
120            StringWriter writer = new StringWriter();
121            try {
122                TransformerFactory factory = TransformerFactory.newInstance();
123                Transformer transformer = factory.newTransformer();
124                transformer.transform(new DOMSource(elem), new StreamResult(writer));
125            } catch (Exception e) {
126                throw new XmlaException(
127                    CLIENT_FAULT_FC,
128                    USM_DOM_PARSE_CODE,
129                    USM_DOM_PARSE_FAULT_FS,
130                    e);
131            }
132            return writer.getBuffer().toString();
133        }
134    
135        public static Element text2Element(String text)
136                throws XmlaException {
137            return _2Element(new InputSource(new StringReader(text)));
138        }
139    
140        public static Element stream2Element(InputStream stream)
141                throws XmlaException {
142            return _2Element(new InputSource(stream));
143        }
144    
145        private static Element _2Element(InputSource source)
146                throws XmlaException {
147            try {
148                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
149                factory.setIgnoringElementContentWhitespace(true);
150                factory.setIgnoringComments(true);
151                factory.setNamespaceAware(true);
152                DocumentBuilder builder = factory.newDocumentBuilder();
153                Document doc = builder.parse(source);
154                return doc.getDocumentElement();
155    
156            } catch (Exception e) {
157                throw new XmlaException(
158                    CLIENT_FAULT_FC,
159                    USM_DOM_PARSE_CODE,
160                    USM_DOM_PARSE_FAULT_FS,
161                    e);
162            }
163        }
164    
165        /**
166         * Returns the first child element of an XML element, or null if there is
167         * no first child.
168         *
169         * @param parent XML element
170         * @param ns     Namespace
171         * @param lname  Local name of child
172         * @return First child, or null if there is no child element
173         */
174        public static Element firstChildElement(
175            Element parent,
176            String ns,
177            String lname)
178        {
179            if (LOGGER.isDebugEnabled()) {
180                StringBuilder buf = new StringBuilder(100);
181                buf.append("XmlaUtil.firstChildElement: ");
182                buf.append(" ns=\"");
183                buf.append(ns);
184                buf.append("\", lname=\"");
185                buf.append(lname);
186                buf.append("\"");
187                LOGGER.debug(buf.toString());
188            }
189            NodeList nlst = parent.getChildNodes();
190            for (int i = 0, nlen = nlst.getLength(); i < nlen; i++) {
191                Node n = nlst.item(i);
192                if (n instanceof Element) {
193                    Element e = (Element) n;
194    
195                    if (LOGGER.isDebugEnabled()) {
196                        StringBuilder buf = new StringBuilder(100);
197                        buf.append("XmlaUtil.firstChildElement: ");
198                        buf.append(" e.getNamespaceURI()=\"");
199                        buf.append(e.getNamespaceURI());
200                        buf.append("\", e.getLocalName()=\"");
201                        buf.append(e.getLocalName());
202                        buf.append("\"");
203                        LOGGER.debug(buf.toString());
204                    }
205    
206                    if ((ns == null || ns.equals(e.getNamespaceURI())) &&
207                        (lname == null || lname.equals(e.getLocalName()))) {
208                        return e;
209                    }
210                }
211            }
212            return null;
213        }
214    
215        public static Element[] filterChildElements(Element parent,
216                                                    String ns,
217                                                    String lname) {
218    
219    /*
220    way too noisy
221            if (LOGGER.isDebugEnabled()) {
222                StringBuilder buf = new StringBuilder(100);
223                buf.append("XmlaUtil.filterChildElements: ");
224                buf.append(" ns=\"");
225                buf.append(ns);
226                buf.append("\", lname=\"");
227                buf.append(lname);
228                buf.append("\"");
229                LOGGER.debug(buf.toString());
230            }
231    */
232    
233            List<Element> elems = new ArrayList<Element>();
234            NodeList nlst = parent.getChildNodes();
235            for (int i = 0, nlen = nlst.getLength(); i < nlen; i++) {
236                Node n = nlst.item(i);
237                if (n instanceof Element) {
238                    Element e = (Element) n;
239    
240    /*
241                    if (LOGGER.isDebugEnabled()) {
242                        StringBuilder buf = new StringBuilder(100);
243                        buf.append("XmlaUtil.filterChildElements: ");
244                        buf.append(" e.getNamespaceURI()=\"");
245                        buf.append(e.getNamespaceURI());
246                        buf.append("\", e.getLocalName()=\"");
247                        buf.append(e.getLocalName());
248                        buf.append("\"");
249                        LOGGER.debug(buf.toString());
250                    }
251    */
252    
253                    if ((ns == null || ns.equals(e.getNamespaceURI())) &&
254                        (lname == null || lname.equals(e.getLocalName()))) {
255                        elems.add(e);
256                    }
257                }
258            }
259            return elems.toArray(new Element[elems.size()]);
260        }
261    
262        public static String textInElement(Element elem) {
263            StringBuilder buf = new StringBuilder(100);
264            elem.normalize();
265            NodeList nlst = elem.getChildNodes();
266            for (int i = 0, nlen = nlst.getLength(); i < nlen ; i++) {
267                Node n = nlst.item(i);
268                if (n instanceof Text) {
269                    final String data = ((Text) n).getData();
270                    buf.append(data);
271                }
272            }
273            return buf.toString();
274        }
275    
276        /**
277         * Finds root MondrianException in exception chain if exists,
278         * otherwise the input throwable.
279         *
280         * @param throwable Exception
281         * @return Root exception
282         */
283        public static Throwable rootThrowable(Throwable throwable) {
284            Throwable rootThrowable = throwable.getCause();
285            if (rootThrowable != null && rootThrowable instanceof MondrianException) {
286                return rootThrowable(rootThrowable);
287            }
288            return throwable;
289        }
290    
291        /**
292         * Corrects for the differences between numeric strings arising because
293         * JDBC drivers use different representations for numbers
294         * ({@link Double} vs. {@link java.math.BigDecimal}) and
295         * these have different toString() behavior.
296         *
297         * <p>If it contains a decimal point, then
298         * strip off trailing '0's. After stripping off
299         * the '0's, if there is nothing right of the
300         * decimal point, then strip off decimal point.
301         *
302         * @param numericStr Numeric string
303         * @return Normalized string
304         */
305        public static String normalizeNumericString(String numericStr) {
306            int index = numericStr.indexOf('.');
307            if (index > 0) {
308                // If it uses exponential notation, 1.0E4, then it could
309                // have a trailing '0' that should not be stripped of,
310                // e.g., 1.0E10. This would be rather bad.
311                if (numericStr.indexOf('e') != -1) {
312                    return numericStr;
313                } else if (numericStr.indexOf('E') != -1) {
314                    return numericStr;
315                }
316    
317                boolean found = false;
318                int p = numericStr.length();
319                char c = numericStr.charAt(p - 1);
320                while (c == '0') {
321                    found = true;
322                    p--;
323                    c = numericStr.charAt(p - 1);
324                }
325                if (c == '.') {
326                    p--;
327                }
328                if (found) {
329                    return numericStr.substring(0, p);
330                }
331            }
332            return numericStr;
333        }
334    
335        /**
336         * Returns a set of column headings and rows for a given metadata request.
337         *
338         * <p/>Leverages mondrian's implementation of the XML/A specification, and
339         * is exposed here for use by mondrian's olap4j driver.
340         *
341         * @param connection Connection
342         * @param catalogName Catalog name
343         * @param methodName Metadata method name per XMLA (e.g. "MDSCHEMA_CUBES")
344         * @param restrictionMap Restrictions
345         * @return Set of rows and column headings
346         */
347        public static MetadataRowset getMetadataRowset(
348            final Connection connection,
349            String catalogName,
350            String methodName,
351            final Map<String, Object> restrictionMap)
352        {
353            RowsetDefinition rowsetDefinition = RowsetDefinition.valueOf(methodName);
354    
355            final Map<String, String> propertyMap = new HashMap<String, String>();
356            final String dataSourceName = "xxx";
357            propertyMap.put(
358                PropertyDefinition.DataSourceInfo.name(),
359                dataSourceName);
360    
361            DataSourcesConfig.DataSource dataSource =
362                new DataSourcesConfig.DataSource();
363            dataSource.name = dataSourceName;
364            DataSourcesConfig.DataSources dataSources =
365                new DataSourcesConfig.DataSources();
366            dataSources.dataSources =
367                new DataSourcesConfig.DataSource[] {dataSource};
368            DataSourcesConfig.Catalog catalog = new DataSourcesConfig.Catalog();
369            catalog.name = catalogName;
370            catalog.definition = "dummy"; // any not-null value will do
371            dataSource.catalogs =
372                new DataSourcesConfig.Catalogs();
373            dataSource.catalogs.catalogs =
374                new DataSourcesConfig.Catalog[] {catalog};
375    
376            Rowset rowset =
377                rowsetDefinition.getRowset(
378                    new XmlaRequest() {
379                        public int getMethod() {
380                            return METHOD_DISCOVER;
381                        }
382    
383                        public Map<String, String> getProperties() {
384                            return propertyMap;
385                        }
386    
387                        public Map<String, Object> getRestrictions() {
388                            return restrictionMap;
389                        }
390    
391                        public String getStatement() {
392                            return null;
393                        }
394    
395                        public String getRoleName() {
396                            return null;
397                        }
398    
399                        public Role getRole() {
400                            return connection.getRole();
401                        }
402    
403                        public String getRequestType() {
404                            throw new UnsupportedOperationException();
405                        }
406    
407                        public boolean isDrillThrough() {
408                            throw new UnsupportedOperationException();
409                        }
410    
411                        public int drillThroughMaxRows() {
412                            throw new UnsupportedOperationException();
413                        }
414    
415                        public int drillThroughFirstRowset() {
416                            throw new UnsupportedOperationException();
417                        }
418                    },
419                    new XmlaHandler(
420                        dataSources,
421                        null,
422                        "xmla") {
423                        protected Connection getConnection(
424                            final DataSourcesConfig.Catalog catalog,
425                            final Role role,
426                            final String roleName)
427                            throws XmlaException
428                        {
429                            return connection;
430                        }
431                    }
432                );
433            List<Rowset.Row> rowList = new ArrayList<Rowset.Row>();
434            rowset.populate(
435                new DefaultXmlaResponse(
436                    new ByteArrayOutputStream(),
437                    Charset.defaultCharset().name()),
438                rowList);
439            MetadataRowset result = new MetadataRowset();
440            for (Rowset.Row row : rowList) {
441                Object[] values =
442                    new Object[rowsetDefinition.columnDefinitions.length];
443                int k = -1;
444                for (RowsetDefinition.Column columnDefinition
445                    : rowsetDefinition.columnDefinitions)
446                {
447                    Object o = row.get(columnDefinition.name);
448                    if (o instanceof List) {
449                        o = toString((List<String>) o);
450                    } else if (o instanceof String[]) {
451                        o = toString(Arrays.asList((String []) o));
452                    }
453                    values[++k] = o;
454                }
455                result.rowList.add(Arrays.asList(values));
456            }
457            for (RowsetDefinition.Column columnDefinition
458                : rowsetDefinition.columnDefinitions)
459            {
460                String columnName = columnDefinition.name;
461                if (LOWERCASE_PATTERN.matcher(columnName).matches()) {
462                    columnName = Util.camelToUpper(columnName);
463                }
464                // VALUE is a SQL reserved word
465                if (columnName.equals("VALUE")) {
466                    columnName = "PROPERTY_VALUE";
467                }
468                result.headerList.add(columnName);
469            }
470            return result;
471        }
472    
473        private static <T> String toString(List<T> list) {
474            StringBuilder buf = new StringBuilder();
475            int k = -1;
476            for (T t : list) {
477                if (++k > 0) {
478                    buf.append(", ");
479                }
480                buf.append(t);
481            }
482            return buf.toString();
483        }
484    
485        /**
486         * Result of a metadata query.
487         */
488        public static class MetadataRowset {
489            public final List<String> headerList = new ArrayList<String>();
490            public final List<List<Object>> rowList = new ArrayList<List<Object>>();
491        }
492    
493        /**
494         * Wrapper which indicates that a restriction is to be treated as a
495         * SQL-style wildcard match.
496         */
497        public static class Wildcard {
498            public final String pattern;
499    
500            public Wildcard(String pattern) {
501                this.pattern = pattern;
502            }
503        }
504    
505        /**
506         * Generates descriptions of the columns returned by each metadata query,
507         * in javadoc format, suitable for pasting into
508         * <code>OlapDatabaseMetaData</code>.
509         */
510        public static void generateMetamodelJavadoc() throws IOException {
511            PrintWriter pw =
512                new PrintWriter(
513                    new FileWriter("C:/open/mondrian/olap4j_javadoc.java"));
514            pw.println("    /**");
515            String prefix = "     * ";
516            for (RowsetDefinition o : RowsetDefinition.values()) {
517                pw.println(prefix);
518                pw.println(prefix + "<p>" + o.name() + "</p>");
519                pw.println(prefix + "<ol>");
520                for (RowsetDefinition.Column columnDefinition : o.columnDefinitions) {
521                    String columnName = columnDefinition.name;
522                    if (LOWERCASE_PATTERN.matcher(columnName).matches()) {
523                        columnName = Util.camelToUpper(columnName);
524                    }
525                    if (columnName.equals("VALUE")) {
526                        columnName = "PROPERTY_VALUE";
527                    }
528                    String type;
529                    switch (columnDefinition.type) {
530                    case StringSometimesArray:
531                    case EnumString:
532                    case UUID:
533                        type = "String";
534                        break;
535                    case DateTime:
536                        type = "Timestamp";
537                        break;
538                    case Boolean:
539                        type = "boolean";
540                        break;
541                    case UnsignedLong:
542                    case Long:
543                        type = "long";
544                        break;
545                    case UnsignedInteger:
546                    case Integer:
547                        type = "int";
548                        break;
549                    default:
550                        type = columnDefinition.type.name();
551                    }
552                    String s = prefix + "<li><b>" + columnName + "</b> "
553                        + type
554                        + (columnDefinition.nullable
555                        ? " (may be <code>null</code>)"
556                        : "")
557                        + " => " + columnDefinition.description
558                        + "</li>";
559                    s = s.replaceAll("\n|\r|\r\n", "<br/>");
560                    final String between =
561                        System.getProperty("line.separator") + prefix + "        ";
562                    pw.println(breakLines(s, 79, between));
563                }
564                pw.println(prefix + "</ol>");
565            }
566            pw.close();
567        }
568    
569        private static String breakLines(
570            String s, int lineLen, String between)
571        {
572            StringBuilder buf = new StringBuilder();
573            final int originalLineLen = lineLen;
574            while (s.length() > 0) {
575                if (s.length() > lineLen) {
576                    int space = s.lastIndexOf(' ', lineLen);
577                    if (space >= 0) {
578                        // best, break at last space before line limit
579                        buf.append(s.substring(0, space));
580                        buf.append(between);
581                        lineLen = originalLineLen - between.length();
582                        s = s.substring(space + 1);
583                    } else {
584                        // next best, break at first space after line limit
585                        space = s.indexOf(' ', lineLen);
586                        if (space >= 0) {
587                            buf.append(s.substring(0, space));
588                            buf.append(between);
589                            lineLen = originalLineLen - between.length();
590                            s = s.substring(space + 1);
591                        } else {
592                            // worst, keep the whole line
593                            buf.append(s);
594                            s = "";
595                        }
596                    }
597                } else {
598                    buf.append(s);
599                    s = "";
600                }
601            }
602            return buf.toString();
603        }
604    }
605    
606    // End XmlaUtil.java