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