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