001 /* 002 // $Id: //open/mondrian/src/main/mondrian/xmla/impl/DefaultXmlaServlet.java#19 $ 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) 2005-2008 Julian Hyde 007 // All Rights Reserved. 008 // You must accept the terms of that agreement to use this software. 009 */ 010 package mondrian.xmla.impl; 011 012 import java.io.ByteArrayInputStream; 013 import java.io.ByteArrayOutputStream; 014 import java.io.IOException; 015 import java.io.InputStream; 016 import java.io.OutputStream; 017 import java.io.UnsupportedEncodingException; 018 import java.nio.ByteBuffer; 019 import java.nio.channels.Channels; 020 import java.nio.channels.ReadableByteChannel; 021 import java.nio.channels.WritableByteChannel; 022 import java.util.Map; 023 import java.util.List; 024 025 import javax.servlet.ServletConfig; 026 import javax.servlet.ServletException; 027 import javax.servlet.http.HttpServletRequest; 028 import javax.servlet.http.HttpServletResponse; 029 import javax.xml.parsers.DocumentBuilder; 030 import javax.xml.parsers.DocumentBuilderFactory; 031 import javax.xml.parsers.ParserConfigurationException; 032 033 import mondrian.xmla.SaxWriter; 034 import mondrian.xmla.XmlaRequest; 035 import mondrian.xmla.XmlaResponse; 036 import mondrian.xmla.XmlaServlet; 037 import mondrian.xmla.XmlaUtil; 038 import mondrian.olap.Util; 039 import mondrian.olap.Role; 040 import mondrian.xmla.XmlaRequestCallback; 041 import mondrian.xmla.XmlaException; 042 043 import org.apache.log4j.Logger; 044 import org.w3c.dom.Attr; 045 import org.w3c.dom.Document; 046 import org.w3c.dom.Node; 047 import org.w3c.dom.NodeList; 048 import org.w3c.dom.Element; 049 import org.xml.sax.InputSource; 050 import org.xml.sax.SAXException; 051 052 /** 053 * Default implementation of XML/A servlet. 054 * 055 * @author Gang Chen 056 */ 057 public class DefaultXmlaServlet extends XmlaServlet { 058 059 private static final Logger LOGGER = Logger.getLogger(DefaultXmlaServlet.class); 060 protected static final String nl = Util.nl; 061 062 private DocumentBuilderFactory domFactory = null; 063 064 public void init(ServletConfig servletConfig) throws ServletException { 065 super.init(servletConfig); 066 domFactory = getDocumentBuilderFactory(); 067 } 068 069 protected DocumentBuilderFactory getDocumentBuilderFactory() { 070 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 071 factory.setIgnoringComments(true); 072 factory.setIgnoringElementContentWhitespace(true); 073 factory.setNamespaceAware(true); 074 return factory; 075 } 076 077 protected void unmarshallSoapMessage( 078 HttpServletRequest request, 079 Element[] requestSoapParts) 080 throws XmlaException { 081 082 try { 083 InputStream inputStream; 084 try { 085 inputStream = request.getInputStream(); 086 } catch (IllegalStateException ex) { 087 throw new XmlaException( 088 SERVER_FAULT_FC, 089 USM_REQUEST_STATE_CODE, 090 USM_REQUEST_STATE_FAULT_FS, 091 ex); 092 } catch (IOException ex) { 093 // This is either Client or Server 094 throw new XmlaException( 095 SERVER_FAULT_FC, 096 USM_REQUEST_INPUT_CODE, 097 USM_REQUEST_INPUT_FAULT_FS, 098 ex); 099 } 100 101 DocumentBuilder domBuilder; 102 try { 103 domBuilder = domFactory.newDocumentBuilder(); 104 } catch (ParserConfigurationException ex) { 105 throw new XmlaException( 106 SERVER_FAULT_FC, 107 USM_DOM_FACTORY_CODE, 108 USM_DOM_FACTORY_FAULT_FS, 109 ex); 110 } 111 112 Document soapDoc; 113 try { 114 soapDoc = domBuilder.parse(new InputSource(inputStream)); 115 } catch (IOException ex) { 116 // This is either Client or Server 117 throw new XmlaException( 118 SERVER_FAULT_FC, 119 USM_DOM_PARSE_IO_CODE, 120 USM_DOM_PARSE_IO_FAULT_FS, 121 ex); 122 } catch (SAXException ex) { 123 // Assume client passed bad xml 124 throw new XmlaException( 125 CLIENT_FAULT_FC, 126 USM_DOM_PARSE_CODE, 127 USM_DOM_PARSE_FAULT_FS, 128 ex); 129 } 130 131 /* Check SOAP message */ 132 Element envElem = soapDoc.getDocumentElement(); 133 134 if (LOGGER.isDebugEnabled()) { 135 StringBuilder buf = new StringBuilder(100); 136 buf.append("XML/A request content").append(nl); 137 buf.append(XmlaUtil.element2Text(envElem)); 138 LOGGER.debug(buf.toString()); 139 } 140 141 if ("Envelope".equals(envElem.getLocalName())) { 142 if (!(NS_SOAP_ENV_1_1.equals(envElem.getNamespaceURI()))) { 143 String msg = "Invalid SOAP message: " + 144 "Envelope element not in SOAP namespace"; 145 throw new XmlaException( 146 CLIENT_FAULT_FC, 147 USM_DOM_PARSE_CODE, 148 USM_DOM_PARSE_FAULT_FS, 149 new SAXException(msg)); 150 } 151 } else { 152 String msg = "Invalid SOAP message: " + 153 "Top element not Envelope"; 154 throw new XmlaException( 155 CLIENT_FAULT_FC, 156 USM_DOM_PARSE_CODE, 157 USM_DOM_PARSE_FAULT_FS, 158 new SAXException(msg)); 159 } 160 161 Element[] childs = 162 XmlaUtil.filterChildElements(envElem, NS_SOAP_ENV_1_1, "Header"); 163 if (childs.length > 1) { 164 String msg = "Invalid SOAP message: " + 165 "More than one Header elements"; 166 throw new XmlaException( 167 CLIENT_FAULT_FC, 168 USM_DOM_PARSE_CODE, 169 USM_DOM_PARSE_FAULT_FS, 170 new SAXException(msg)); 171 } 172 requestSoapParts[0] = childs.length == 1 ? childs[0] : null; 173 174 childs = XmlaUtil.filterChildElements(envElem, NS_SOAP_ENV_1_1, "Body"); 175 if (childs.length != 1) { 176 String msg = "Invalid SOAP message: " + 177 "Does not have one Body element"; 178 throw new XmlaException( 179 CLIENT_FAULT_FC, 180 USM_DOM_PARSE_CODE, 181 USM_DOM_PARSE_FAULT_FS, 182 new SAXException(msg)); 183 } 184 requestSoapParts[1] = childs[0]; 185 186 } catch (XmlaException xex) { 187 throw xex; 188 } catch (Exception ex) { 189 throw new XmlaException( 190 SERVER_FAULT_FC, 191 USM_UNKNOWN_CODE, 192 USM_UNKNOWN_FAULT_FS, 193 ex); 194 } 195 } 196 197 /** 198 * See if there is a "mustUnderstand" header element. 199 * If there is a BeginSession element, then generate a session id and 200 * add to context Map. 201 * <p> 202 * Excel 2000 and Excel XP generate both a BeginSession, Session and 203 * EndSession mustUnderstand==1 204 * in the "urn:schemas-microsoft-com:xml-analysis" namespace 205 * Header elements and a NamespaceCompatibility mustUnderstand==0 206 * in the "http://schemas.microsoft.com/analysisservices/2003/xmla" 207 * namespace. Here we handle only the session Header elements 208 * 209 */ 210 protected void handleSoapHeader( 211 HttpServletResponse response, 212 Element[] requestSoapParts, 213 byte[][] responseSoapParts, 214 Map<String, Object> context) throws XmlaException { 215 216 try { 217 Element hdrElem = requestSoapParts[0]; 218 if ((hdrElem == null) || (! hdrElem.hasChildNodes())) { 219 return; 220 } 221 String encoding = response.getCharacterEncoding(); 222 223 byte[] bytes = null; 224 225 NodeList nlst = hdrElem.getChildNodes(); 226 int nlen = nlst.getLength(); 227 for (int i = 0; i < nlen; i++) { 228 Node n = nlst.item(i); 229 if (n instanceof Element) { 230 Element e = (Element) n; 231 232 // does the Element have a mustUnderstand attribute 233 Attr attr = e.getAttributeNode(SOAP_MUST_UNDERSTAND_ATTR); 234 if (attr == null) { 235 continue; 236 } 237 // Is its value "1" 238 String mustUnderstandValue = attr.getValue(); 239 if ((mustUnderstandValue == null) || 240 (! mustUnderstandValue.equals("1"))) { 241 continue; 242 } 243 244 // We've got a mustUnderstand attribute 245 246 // Is it an XMLA element 247 if (! NS_XMLA.equals(e.getNamespaceURI())) { 248 continue; 249 } 250 // So, an XMLA mustUnderstand-er 251 // Do we know what to do with it 252 // We understand: 253 // BeginSession 254 // Session 255 // EndSession 256 257 String sessionIdStr; 258 String localName = e.getLocalName(); 259 if (localName.equals(XMLA_BEGIN_SESSION)) { 260 // generate SessionId 261 262 sessionIdStr = generateSessionId(context); 263 264 context.put(CONTEXT_XMLA_SESSION_ID, sessionIdStr); 265 context.put(CONTEXT_XMLA_SESSION_STATE, 266 CONTEXT_XMLA_SESSION_STATE_BEGIN); 267 268 } else if (localName.equals(XMLA_SESSION)) { 269 // extract the SessionId attrs value and put into context 270 sessionIdStr = getSessionId(e, context); 271 272 context.put(CONTEXT_XMLA_SESSION_ID, sessionIdStr); 273 context.put(CONTEXT_XMLA_SESSION_STATE, 274 CONTEXT_XMLA_SESSION_STATE_WITHIN); 275 276 } else if (localName.equals(XMLA_END_SESSION)) { 277 // extract the SessionId attrs value and put into context 278 sessionIdStr = getSessionId(e, context); 279 280 context.put(CONTEXT_XMLA_SESSION_ID, sessionIdStr); 281 context.put(CONTEXT_XMLA_SESSION_STATE, 282 CONTEXT_XMLA_SESSION_STATE_END); 283 284 } else { 285 // error 286 String msg = "Invalid XML/A message: " + 287 " Unknown \"mustUnderstand\" XMLA Header element \"" + 288 localName + 289 "\""; 290 throw new XmlaException( 291 MUST_UNDERSTAND_FAULT_FC, 292 HSH_MUST_UNDERSTAND_CODE, 293 HSH_MUST_UNDERSTAND_FAULT_FS, 294 new RuntimeException(msg)); 295 } 296 297 StringBuilder buf = new StringBuilder(100); 298 buf.append("<Session "); 299 buf.append(XMLA_SESSION_ID); 300 buf.append("=\""); 301 buf.append(sessionIdStr); 302 buf.append("\" "); 303 buf.append("xmlns=\""); 304 buf.append(NS_XMLA); 305 buf.append("\" />"); 306 bytes = buf.toString().getBytes(encoding); 307 308 } 309 } 310 responseSoapParts[0] = bytes; 311 312 } catch (XmlaException xex) { 313 throw xex; 314 } catch (Exception ex) { 315 throw new XmlaException( 316 SERVER_FAULT_FC, 317 HSH_UNKNOWN_CODE, 318 HSH_UNKNOWN_FAULT_FS, 319 ex); 320 } 321 } 322 323 protected String generateSessionId(Map<String, Object> context) { 324 List<XmlaRequestCallback> callbacks = getCallbacks(); 325 if (callbacks.size() > 0) { 326 // get only the first callback if it exists 327 XmlaRequestCallback callback = callbacks.get(0); 328 return (String) callback.generateSessionId(context); 329 } else { 330 // what to do here, should Mondrian generate a Session Id? 331 // TODO: Maybe Mondrian ought to generate all Session Ids and 332 // not the callback. 333 return ""; 334 } 335 } 336 337 protected String getSessionId(Element e, Map<String, Object> context) 338 throws Exception { 339 // extract the SessionId attrs value and put into context 340 Attr attr = e.getAttributeNode(XMLA_SESSION_ID); 341 if (attr == null) { 342 String msg = "Invalid XML/A message: " + 343 XMLA_SESSION + 344 " Header element with no " + 345 XMLA_SESSION_ID + 346 " attribute"; 347 throw new SAXException(msg); 348 } 349 String value = attr.getValue(); 350 if (value == null) { 351 String msg = "Invalid XML/A message: " + 352 XMLA_SESSION + 353 " Header element with " + 354 XMLA_SESSION_ID + 355 " attribute but no attribute value"; 356 throw new SAXException(msg); 357 } 358 return value; 359 } 360 361 protected void handleSoapBody( 362 HttpServletResponse response, 363 Element[] requestSoapParts, 364 byte[][] responseSoapParts, 365 Map<String, Object> context) 366 throws XmlaException 367 { 368 try { 369 String encoding = response.getCharacterEncoding(); 370 Element hdrElem = requestSoapParts[0]; 371 Element bodyElem = requestSoapParts[1]; 372 Element[] dreqs = XmlaUtil.filterChildElements(bodyElem, NS_XMLA, "Discover"); 373 Element[] ereqs = XmlaUtil.filterChildElements(bodyElem, NS_XMLA, "Execute"); 374 if (dreqs.length + ereqs.length != 1) { 375 String msg = "Invalid XML/A message: " + 376 " Body has " + 377 dreqs.length + 378 " Discover Requests and " + 379 ereqs.length + 380 " Execute Requests"; 381 throw new XmlaException( 382 CLIENT_FAULT_FC, 383 HSB_BAD_SOAP_BODY_CODE, 384 HSB_BAD_SOAP_BODY_FAULT_FS, 385 new RuntimeException(msg)); 386 } 387 388 Element xmlaReqElem = (dreqs.length == 0 ? ereqs[0] : dreqs[0]); 389 390 ByteArrayOutputStream osBuf = new ByteArrayOutputStream(); 391 392 // use context variable `role' as this request's XML/A role 393 String roleName = (String) context.get(CONTEXT_ROLE_NAME); 394 Role role = (Role) context.get(CONTEXT_ROLE); 395 396 XmlaRequest xmlaReq = null; 397 if (role != null) { 398 xmlaReq = new DefaultXmlaRequest(xmlaReqElem, role); 399 } else if (roleName != null) { 400 xmlaReq = new DefaultXmlaRequest(xmlaReqElem, roleName); 401 } else { 402 xmlaReq = new DefaultXmlaRequest(xmlaReqElem); 403 } 404 405 XmlaResponse xmlaRes = new DefaultXmlaResponse(osBuf, encoding); 406 407 try { 408 getXmlaHandler().process(xmlaReq, xmlaRes); 409 } catch (XmlaException ex) { 410 throw ex; 411 } catch (Exception ex) { 412 throw new XmlaException( 413 SERVER_FAULT_FC, 414 HSB_PROCESS_CODE, 415 HSB_PROCESS_FAULT_FS, 416 ex); 417 } 418 419 responseSoapParts[1] = osBuf.toByteArray(); 420 421 } catch (XmlaException xex) { 422 throw xex; 423 } catch (Exception ex) { 424 throw new XmlaException( 425 SERVER_FAULT_FC, 426 HSB_UNKNOWN_CODE, 427 HSB_UNKNOWN_FAULT_FS, 428 ex); 429 } 430 } 431 432 protected void marshallSoapMessage( 433 HttpServletResponse response, 434 byte[][] responseSoapParts) 435 throws XmlaException { 436 437 try { 438 // If CharacterEncoding was set in web.xml, use this value 439 String encoding = (charEncoding != null) 440 ? charEncoding : response.getCharacterEncoding(); 441 442 /* 443 * Since we just reset response, encoding and content-type were 444 * reset too 445 */ 446 if (charEncoding != null) { 447 response.setCharacterEncoding(charEncoding); 448 } 449 response.setContentType("text/xml"); 450 451 /* 452 * The setCharacterEncoding, setContentType, or setLocale method 453 * must be called BEFORE getWriter or getOutputStream and before 454 * committing the response for the character encoding to be used. 455 * http://tomcat.apache.org/tomcat-5.5-doc/servletapi/javax/servlet/ServletResponse.html 456 */ 457 OutputStream outputStream = response.getOutputStream(); 458 459 460 byte[] soapHeader = responseSoapParts[0]; 461 byte[] soapBody = responseSoapParts[1]; 462 463 Object[] byteChunks = new Object[5]; 464 465 try { 466 StringBuilder buf = new StringBuilder(500); 467 buf.append("<?xml version=\"1.0\" encoding=\""); 468 buf.append(encoding); 469 buf.append("\"?>"); 470 buf.append(nl); 471 472 buf.append("<"); 473 buf.append(SOAP_PREFIX); 474 buf.append(":Envelope xmlns:"); 475 buf.append(SOAP_PREFIX); 476 buf.append("=\""); 477 buf.append(NS_SOAP_ENV_1_1); 478 buf.append("\" "); 479 buf.append(SOAP_PREFIX); 480 buf.append(":encodingStyle=\""); 481 buf.append(NS_SOAP_ENC_1_1); 482 buf.append("\" >"); 483 buf.append(nl); 484 buf.append("<"); 485 buf.append(SOAP_PREFIX); 486 buf.append(":Header>"); 487 buf.append(nl); 488 byteChunks[0] = buf.toString().getBytes(encoding); 489 490 byteChunks[1] = soapHeader; 491 492 buf.setLength(0); 493 buf.append("</"); 494 buf.append(SOAP_PREFIX); 495 buf.append(":Header>"); 496 buf.append(nl); 497 buf.append("<"); 498 buf.append(SOAP_PREFIX); 499 buf.append(":Body>"); 500 buf.append(nl); 501 502 byteChunks[2] = buf.toString().getBytes(encoding); 503 504 byteChunks[3] = soapBody; 505 506 buf.setLength(0); 507 buf.append(nl); 508 buf.append("</"); 509 buf.append(SOAP_PREFIX); 510 buf.append(":Body>"); 511 buf.append(nl); 512 buf.append("</"); 513 buf.append(SOAP_PREFIX); 514 buf.append(":Envelope>"); 515 buf.append(nl); 516 517 byteChunks[4] = buf.toString().getBytes(encoding); 518 519 } catch (UnsupportedEncodingException uee) { 520 LOGGER.warn("This should be handled at begin of processing request", uee); 521 } 522 523 if (LOGGER.isDebugEnabled()) { 524 StringBuilder buf = new StringBuilder(100); 525 buf.append("XML/A response content").append(nl); 526 try { 527 for (Object byteChunk : byteChunks) { 528 byte[] chunk = (byte[]) byteChunk; 529 if (chunk != null && chunk.length > 0) { 530 buf.append(new String(chunk, encoding)); 531 } 532 } 533 } catch (UnsupportedEncodingException uee) { 534 LOGGER.warn("This should be handled at begin of processing request", uee); 535 } 536 LOGGER.debug(buf.toString()); 537 } 538 539 if (LOGGER.isDebugEnabled()) { 540 StringBuilder buf = new StringBuilder(); 541 buf.append("XML/A response content").append(nl); 542 } 543 try { 544 int bufferSize = 4096; 545 ByteBuffer buffer = ByteBuffer.allocate(bufferSize); 546 WritableByteChannel wch = Channels.newChannel(outputStream); 547 ReadableByteChannel rch; 548 for (Object byteChunk : byteChunks) { 549 if (byteChunk == null || ((byte[]) byteChunk).length == 0) { 550 continue; 551 } 552 rch = Channels 553 .newChannel(new ByteArrayInputStream((byte[]) byteChunk)); 554 555 int readSize; 556 do { 557 buffer.clear(); 558 readSize = rch.read(buffer); 559 buffer.flip(); 560 561 int writeSize = 0; 562 while ((writeSize += wch.write(buffer)) < readSize) { 563 ; 564 } 565 } while (readSize == bufferSize); 566 rch.close(); 567 } 568 outputStream.flush(); 569 } catch (IOException ioe) { 570 LOGGER.error("Damn exception when transferring bytes over sockets", ioe); 571 } 572 } catch (XmlaException xex) { 573 throw xex; 574 } catch (Exception ex) { 575 throw new XmlaException( 576 SERVER_FAULT_FC, 577 MSM_UNKNOWN_CODE, 578 MSM_UNKNOWN_FAULT_FS, 579 ex); 580 } 581 } 582 583 /** 584 * This produces a SOAP 1.1 version Fault element - not a 1.2 version. 585 * 586 */ 587 protected void handleFault( 588 HttpServletResponse response, 589 byte[][] responseSoapParts, 590 Phase phase, 591 Throwable t) { 592 593 // Regardless of whats been put into the response so far, clear 594 // it out. 595 response.reset(); 596 597 // NOTE: if you can think of better/other status codes to use 598 // for the various phases, please make changes. 599 // I think that XMLA faults always returns OK. 600 switch (phase) { 601 case VALIDATE_HTTP_HEAD: 602 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 603 break; 604 case INITIAL_PARSE: 605 case CALLBACK_PRE_ACTION: 606 case PROCESS_HEADER: 607 case PROCESS_BODY: 608 case CALLBACK_POST_ACTION: 609 case SEND_RESPONSE: 610 response.setStatus(HttpServletResponse.SC_OK); 611 break; 612 } 613 614 String code; 615 String faultCode; 616 String faultString; 617 String detail; 618 if (t instanceof XmlaException) { 619 XmlaException xex = (XmlaException) t; 620 code = xex.getCode(); 621 faultString = xex.getFaultString(); 622 faultCode = XmlaException.formatFaultCode(xex); 623 detail = XmlaException.formatDetail(xex.getDetail()); 624 625 } else { 626 // some unexpected Throwable 627 t = XmlaException.getRootCause(t); 628 code = UNKNOWN_ERROR_CODE; 629 faultString = UNKNOWN_ERROR_FAULT_FS; 630 faultCode = XmlaException.formatFaultCode( 631 SERVER_FAULT_FC, code); 632 detail = XmlaException.formatDetail(t.getMessage()); 633 } 634 635 String encoding = response.getCharacterEncoding(); 636 637 ByteArrayOutputStream osBuf = new ByteArrayOutputStream(); 638 try { 639 SaxWriter writer = new DefaultSaxWriter(osBuf, encoding); 640 writer.startDocument(); 641 writer.startElement(SOAP_PREFIX + ":Fault"); 642 643 // The faultcode element is intended for use by software to provide 644 // an algorithmic mechanism for identifying the fault. The faultcode 645 // MUST be present in a SOAP Fault element and the faultcode value 646 // MUST be a qualified name 647 writer.startElement("faultcode"); 648 writer.characters(faultCode); 649 writer.endElement(); 650 651 // The faultstring element is intended to provide a human readable 652 // explanation of the fault and is not intended for algorithmic 653 // processing. 654 writer.startElement("faultstring"); 655 writer.characters(faultString); 656 writer.endElement(); 657 658 // The faultactor element is intended to provide information about 659 // who caused the fault to happen within the message path 660 writer.startElement("faultactor"); 661 writer.characters(FAULT_ACTOR); 662 writer.endElement(); 663 664 // The detail element is intended for carrying application specific 665 // error information related to the Body element. It MUST be present 666 // if the contents of the Body element could not be successfully 667 // processed. It MUST NOT be used to carry information about error 668 // information belonging to header entries. Detailed error 669 // information belonging to header entries MUST be carried within 670 // header entries. 671 if (phase != Phase.PROCESS_HEADER) { 672 writer.startElement("detail"); 673 writer.startElement(FAULT_NS_PREFIX + ":error", new String[] { 674 "xmlns:" + FAULT_NS_PREFIX, MONDRIAN_NAMESPACE 675 }); 676 writer.startElement("code"); 677 writer.characters(code); 678 writer.endElement(); // code 679 writer.startElement("desc"); 680 writer.characters(detail); 681 writer.endElement(); // desc 682 writer.endElement(); // error 683 writer.endElement(); // detail 684 } 685 686 writer.endElement(); // </Fault> 687 writer.endDocument(); 688 } catch (UnsupportedEncodingException uee) { 689 LOGGER.warn("This should be handled at begin of processing request", uee); 690 } catch (Exception e) { 691 LOGGER.error("Unexcepted runimt exception when handing SOAP fault :("); 692 } 693 694 responseSoapParts[1] = osBuf.toByteArray(); 695 } 696 697 } 698 699 // End DefaultXmlaServlet.java