001 /* 002 // $Id: //open/mondrian/src/main/mondrian/xmla/XmlaServlet.java#28 $ 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.Util; 015 import mondrian.spi.CatalogLocator; 016 import mondrian.spi.impl.ServletContextCatalogLocator; 017 018 import org.apache.log4j.Logger; 019 import org.eigenbase.xom.*; 020 import org.w3c.dom.Element; 021 022 import javax.servlet.ServletContext; 023 import javax.servlet.ServletConfig; 024 import javax.servlet.ServletException; 025 import javax.servlet.http.HttpServlet; 026 import javax.servlet.http.HttpServletRequest; 027 import javax.servlet.http.HttpServletResponse; 028 import java.io.*; 029 import java.net.MalformedURLException; 030 import java.net.URL; 031 import java.util.*; 032 033 /** 034 * Base XML/A servlet. 035 * 036 * @author Gang Chen 037 * @since December, 2005 038 * @version $Id: //open/mondrian/src/main/mondrian/xmla/XmlaServlet.java#28 $ 039 */ 040 public abstract class XmlaServlet extends HttpServlet 041 implements XmlaConstants { 042 043 private static final Logger LOGGER = Logger.getLogger(XmlaServlet.class); 044 045 public static final String PARAM_DATASOURCES_CONFIG = "DataSourcesConfig"; 046 public static final String PARAM_OPTIONAL_DATASOURCE_CONFIG = 047 "OptionalDataSourceConfig"; 048 public static final String PARAM_CHAR_ENCODING = "CharacterEncoding"; 049 public static final String PARAM_CALLBACKS = "Callbacks"; 050 051 public static final String DEFAULT_DATASOURCE_FILE = "datasources.xml"; 052 053 public enum Phase { 054 VALIDATE_HTTP_HEAD, 055 INITIAL_PARSE, 056 CALLBACK_PRE_ACTION, 057 PROCESS_HEADER, 058 PROCESS_BODY, 059 CALLBACK_POST_ACTION, 060 SEND_RESPONSE, 061 SEND_ERROR 062 } 063 064 /** 065 * If paramName's value is not null and 'true', then return true. 066 * 067 */ 068 public static boolean getBooleanInitParameter( 069 ServletConfig servletConfig, 070 String paramName) { 071 String paramValue = servletConfig.getInitParameter(paramName); 072 return paramValue != null && Boolean.valueOf(paramValue); 073 } 074 075 public static boolean getParameter( 076 HttpServletRequest req, 077 String paramName) { 078 String paramValue = req.getParameter(paramName); 079 return paramValue != null && Boolean.valueOf(paramValue); 080 } 081 082 protected CatalogLocator catalogLocator = null; 083 protected DataSourcesConfig.DataSources dataSources = null; 084 protected XmlaHandler xmlaHandler = null; 085 protected String charEncoding = null; 086 private final List<XmlaRequestCallback> callbackList = 087 new ArrayList<XmlaRequestCallback>(); 088 089 public XmlaServlet() { 090 } 091 092 093 /** 094 * Initializes servlet and XML/A handler. 095 * 096 */ 097 public void init(ServletConfig servletConfig) throws ServletException { 098 super.init(servletConfig); 099 100 // init: charEncoding 101 initCharEncodingHandler(servletConfig); 102 103 // init: callbacks 104 initCallbacks(servletConfig); 105 106 // make: catalogLocator 107 // A derived class can alter how the calalog locator object is 108 // created. 109 this.catalogLocator = makeCatalogLocator(servletConfig); 110 111 DataSourcesConfig.DataSources dataSources = 112 makeDataSources(servletConfig); 113 addToDataSources(dataSources); 114 } 115 116 /** 117 * Gets (creating if needed) the XmlaHandler. 118 * 119 * @return XMLA handler 120 */ 121 protected XmlaHandler getXmlaHandler() { 122 if (this.xmlaHandler == null) { 123 this.xmlaHandler = 124 new XmlaHandler(this.dataSources, this.catalogLocator, "cxmla"); 125 } 126 return this.xmlaHandler; 127 } 128 129 /** 130 * Registers a callback. 131 */ 132 protected final void addCallback(XmlaRequestCallback callback) { 133 callbackList.add(callback); 134 } 135 136 /** 137 * Returns the list of callbacks. The list is immutable. 138 * 139 * @return list of callbacks 140 */ 141 protected final List<XmlaRequestCallback> getCallbacks() { 142 return Collections.unmodifiableList(callbackList); 143 } 144 145 /** 146 * Main entry for HTTP post method 147 * 148 */ 149 protected void doPost( 150 HttpServletRequest request, 151 HttpServletResponse response) 152 throws ServletException, IOException { 153 154 // Request Soap Header and Body 155 // header [0] and body [1] 156 Element[] requestSoapParts = new Element[2]; 157 158 // Response Soap Header and Body 159 // An array allows response parts to be passed into callback 160 // and possible modifications returned. 161 // response header in [0] and response body in [1] 162 byte[][] responseSoapParts = new byte[2][]; 163 164 Phase phase = Phase.VALIDATE_HTTP_HEAD; 165 166 try { 167 168 if (charEncoding != null) { 169 try { 170 request.setCharacterEncoding(charEncoding); 171 response.setCharacterEncoding(charEncoding); 172 } catch (UnsupportedEncodingException uee) { 173 charEncoding = null; 174 String msg = "Unsupported character encoding '" + 175 charEncoding + 176 "': " + 177 "Use default character encoding from HTTP client for now"; 178 LOGGER.warn(msg); 179 } 180 } 181 182 response.setContentType("text/xml"); 183 184 Map<String, Object> context = new HashMap<String, Object>(); 185 186 try { 187 if (LOGGER.isDebugEnabled()) { 188 LOGGER.debug("Invoking validate http header callbacks"); 189 } 190 for (XmlaRequestCallback callback : getCallbacks()) { 191 if (!callback.processHttpHeader( 192 request, 193 response, 194 context)) { 195 return; 196 } 197 } 198 199 } catch (XmlaException xex) { 200 LOGGER.error("Errors when invoking callbacks validateHttpHeader", xex); 201 handleFault(response, responseSoapParts, phase, xex); 202 phase = Phase.SEND_ERROR; 203 marshallSoapMessage(response, responseSoapParts); 204 return; 205 206 } catch (Exception ex) { 207 LOGGER.error("Errors when invoking callbacks validateHttpHeader", ex); 208 handleFault(response, responseSoapParts, 209 phase, new XmlaException( 210 SERVER_FAULT_FC, 211 CHH_CODE, 212 CHH_FAULT_FS, 213 ex)); 214 phase = Phase.SEND_ERROR; 215 marshallSoapMessage(response, responseSoapParts); 216 return; 217 } 218 219 220 phase = Phase.INITIAL_PARSE; 221 222 try { 223 if (LOGGER.isDebugEnabled()) { 224 LOGGER.debug("Unmarshalling SOAP message"); 225 } 226 227 // check request's content type 228 String contentType = request.getContentType(); 229 if (contentType == null || 230 contentType.indexOf("text/xml") == -1) { 231 throw new IllegalArgumentException("Only accepts content type 'text/xml', not '" + contentType + "'"); 232 } 233 234 unmarshallSoapMessage(request, requestSoapParts); 235 236 } catch (XmlaException xex) { 237 LOGGER.error("Unable to unmarshall SOAP message", xex); 238 handleFault(response, responseSoapParts, phase, xex); 239 phase = Phase.SEND_ERROR; 240 marshallSoapMessage(response, responseSoapParts); 241 return; 242 } 243 244 phase = Phase.PROCESS_HEADER; 245 246 try { 247 if (LOGGER.isDebugEnabled()) { 248 LOGGER.debug("Handling XML/A message header"); 249 } 250 251 // process application specified SOAP header here 252 handleSoapHeader(response, 253 requestSoapParts, 254 responseSoapParts, 255 context); 256 } catch (XmlaException xex) { 257 LOGGER.error("Errors when handling XML/A message", xex); 258 handleFault(response, responseSoapParts, phase, xex); 259 phase = Phase.SEND_ERROR; 260 marshallSoapMessage(response, responseSoapParts); 261 return; 262 } 263 264 phase = Phase.CALLBACK_PRE_ACTION; 265 266 267 try { 268 if (LOGGER.isDebugEnabled()) { 269 LOGGER.debug("Invoking callbacks preAction"); 270 } 271 272 for (XmlaRequestCallback callback : getCallbacks()) { 273 callback.preAction(request, requestSoapParts, context); 274 } 275 } catch (XmlaException xex) { 276 LOGGER.error("Errors when invoking callbacks preaction", xex); 277 handleFault(response, responseSoapParts, phase, xex); 278 phase = Phase.SEND_ERROR; 279 marshallSoapMessage(response, responseSoapParts); 280 return; 281 282 } catch (Exception ex) { 283 LOGGER.error("Errors when invoking callbacks preaction", ex); 284 handleFault(response, responseSoapParts, 285 phase, new XmlaException( 286 SERVER_FAULT_FC, 287 CPREA_CODE, 288 CPREA_FAULT_FS, 289 ex)); 290 phase = Phase.SEND_ERROR; 291 marshallSoapMessage(response, responseSoapParts); 292 return; 293 } 294 295 phase = Phase.PROCESS_BODY; 296 297 try { 298 if (LOGGER.isDebugEnabled()) { 299 LOGGER.debug("Handling XML/A message body"); 300 } 301 302 // process XML/A request 303 handleSoapBody(response, 304 requestSoapParts, 305 responseSoapParts, 306 context); 307 308 } catch (XmlaException xex) { 309 LOGGER.error("Errors when handling XML/A message", xex); 310 handleFault(response, responseSoapParts, phase, xex); 311 phase = Phase.SEND_ERROR; 312 marshallSoapMessage(response, responseSoapParts); 313 return; 314 } 315 316 phase = Phase.CALLBACK_POST_ACTION; 317 318 try { 319 if (LOGGER.isDebugEnabled()) { 320 LOGGER.debug("Invoking callbacks postAction"); 321 } 322 323 for (XmlaRequestCallback callback : getCallbacks()) { 324 callback.postAction( 325 request, response, 326 responseSoapParts, context); 327 } 328 } catch (XmlaException xex) { 329 LOGGER.error("Errors when invoking callbacks postaction", xex); 330 handleFault(response, responseSoapParts, phase, xex); 331 phase = Phase.SEND_ERROR; 332 marshallSoapMessage(response, responseSoapParts); 333 return; 334 335 } catch (Exception ex) { 336 LOGGER.error("Errors when invoking callbacks postaction", ex); 337 handleFault(response, responseSoapParts, 338 phase, new XmlaException( 339 SERVER_FAULT_FC, 340 CPOSTA_CODE, 341 CPOSTA_FAULT_FS, 342 ex)); 343 phase = Phase.SEND_ERROR; 344 marshallSoapMessage(response, responseSoapParts); 345 return; 346 } 347 348 phase = Phase.SEND_RESPONSE; 349 350 try { 351 352 response.setStatus(HttpServletResponse.SC_OK); 353 marshallSoapMessage(response, responseSoapParts); 354 355 } catch (XmlaException xex) { 356 LOGGER.error("Errors when handling XML/A message", xex); 357 handleFault(response, responseSoapParts, phase, xex); 358 phase = Phase.SEND_ERROR; 359 marshallSoapMessage(response, responseSoapParts); 360 return; 361 } 362 363 } catch (Throwable t) { 364 LOGGER.error("Unknown Error when handling XML/A message", t); 365 handleFault(response, responseSoapParts, phase, t); 366 marshallSoapMessage(response, responseSoapParts); 367 } 368 369 } 370 371 /** 372 * Implement to provide application specified SOAP unmarshalling algorithm. 373 */ 374 protected abstract void unmarshallSoapMessage( 375 HttpServletRequest request, 376 Element[] requestSoapParts) throws XmlaException; 377 378 /** 379 * Implement to handle application specified SOAP header. 380 */ 381 protected abstract void handleSoapHeader( 382 HttpServletResponse response, 383 Element[] requestSoapParts, 384 byte[][] responseSoapParts, 385 Map<String, Object> context) throws XmlaException; 386 387 /** 388 * Implement to handle XML/A request. 389 */ 390 protected abstract void handleSoapBody( 391 HttpServletResponse response, 392 Element[] requestSoapParts, 393 byte[][] responseSoapParts, 394 Map<String, Object> context) throws XmlaException; 395 396 /** 397 * Implement to privode application specified SOAP marshalling algorithm. 398 */ 399 protected abstract void marshallSoapMessage( 400 HttpServletResponse response, 401 byte[][] responseSoapParts) throws XmlaException; 402 403 /** 404 * Implement to application specified handler of SOAP fualt. 405 */ 406 protected abstract void handleFault( 407 HttpServletResponse response, 408 byte[][] responseSoapParts, 409 Phase phase, 410 Throwable t); 411 412 413 414 /** 415 * Make catalog locator. Derived classes can roll their own 416 */ 417 protected CatalogLocator makeCatalogLocator(ServletConfig servletConfig) { 418 ServletContext servletContext = servletConfig.getServletContext(); 419 return new ServletContextCatalogLocator(servletContext); 420 } 421 422 /** 423 * Make DataSourcesConfig.DataSources instance. Derived classes 424 * can roll their own 425 * <p> 426 * If there is an initParameter called "DataSourcesConfig" 427 * get its value, replace any "${key}" content with "value" where 428 * "key/value" are System properties, and try to create a URL 429 * instance out of it. If that fails, then assume its a 430 * real filepath and if the file exists then create a URL from it 431 * (but only if the file exists). 432 * If there is no initParameter with that name, then attempt to 433 * find the file called "datasources.xml" under "WEB-INF/" 434 * and if it exists, use it. 435 */ 436 protected DataSourcesConfig.DataSources makeDataSources( 437 ServletConfig servletConfig) { 438 439 String paramValue = 440 servletConfig.getInitParameter(PARAM_DATASOURCES_CONFIG); 441 // if false, then do not throw exception if the file/url 442 // can not be found 443 boolean optional = 444 getBooleanInitParameter(servletConfig, PARAM_OPTIONAL_DATASOURCE_CONFIG); 445 446 URL dataSourcesConfigUrl = null; 447 try { 448 if (paramValue == null) { 449 // fallback to default 450 String defaultDS = "WEB-INF/" + DEFAULT_DATASOURCE_FILE; 451 ServletContext servletContext = servletConfig.getServletContext(); 452 File realPath = new File(servletContext.getRealPath(defaultDS)); 453 if (realPath.exists()) { 454 // only if it exists 455 dataSourcesConfigUrl = realPath.toURL(); 456 } 457 } else { 458 paramValue = Util.replaceProperties( 459 paramValue, 460 Util.toMap(System.getProperties())); 461 if (LOGGER.isDebugEnabled()) { 462 String msg = "XmlaServlet.makeDataSources: " + 463 "paramValue=" + paramValue; 464 LOGGER.debug(msg); 465 } 466 // is the parameter a valid URL 467 MalformedURLException mue = null; 468 try { 469 dataSourcesConfigUrl = new URL(paramValue); 470 } catch (MalformedURLException e) { 471 // not a valid url 472 mue = e; 473 } 474 if (dataSourcesConfigUrl == null) { 475 // see if its a full valid file path 476 File f = new File(paramValue); 477 if (f.exists()) { 478 // yes, a real file path 479 dataSourcesConfigUrl = f.toURL(); 480 } else if (mue != null) { 481 // neither url or file, 482 // is it not optional 483 if (! optional) { 484 throw mue; 485 } 486 } 487 } 488 } 489 } catch (MalformedURLException mue) { 490 throw Util.newError(mue, "invalid URL path '" + paramValue + "'"); 491 } 492 493 if (LOGGER.isDebugEnabled()) { 494 String msg = "XmlaServlet.makeDataSources: " + 495 "dataSourcesConfigUrl=" + dataSourcesConfigUrl; 496 LOGGER.debug(msg); 497 } 498 // don't try to parse a null 499 return (dataSourcesConfigUrl == null) 500 ? null : parseDataSourcesUrl(dataSourcesConfigUrl); 501 } 502 503 protected void addToDataSources(DataSourcesConfig.DataSources dataSources) { 504 if (this.dataSources == null) { 505 this.dataSources = dataSources; 506 } else if (dataSources != null) { 507 DataSourcesConfig.DataSource[] ds1 = this.dataSources.dataSources; 508 int len1 = ds1.length; 509 DataSourcesConfig.DataSource[] ds2 = dataSources.dataSources; 510 int len2 = ds2.length; 511 512 DataSourcesConfig.DataSource[] tmp = 513 new DataSourcesConfig.DataSource[len1 + len2]; 514 515 System.arraycopy(ds1, 0, tmp, 0, len1); 516 System.arraycopy(ds2, 0, tmp, len1, len2); 517 518 this.dataSources.dataSources = tmp; 519 } else { 520 LOGGER.warn("XmlaServlet.addToDataSources: DataSources is null"); 521 } 522 } 523 524 protected DataSourcesConfig.DataSources parseDataSourcesUrl( 525 URL dataSourcesConfigUrl) { 526 527 try { 528 String dataSourcesConfigString = 529 readDataSourcesContent(dataSourcesConfigUrl); 530 return parseDataSources(dataSourcesConfigString); 531 532 } catch (Exception e) { 533 throw Util.newError(e, "Failed to parse data sources config '" + 534 dataSourcesConfigUrl.toExternalForm() + "'"); 535 } 536 } 537 538 protected String readDataSourcesContent(URL dataSourcesConfigUrl) 539 throws IOException { 540 return Util.readURL( 541 dataSourcesConfigUrl, 542 Util.toMap(System.getProperties())); 543 } 544 545 protected DataSourcesConfig.DataSources parseDataSources( 546 String dataSourcesConfigString) { 547 548 try { 549 if (dataSourcesConfigString == null) { 550 LOGGER.warn("XmlaServlet.parseDataSources: null input"); 551 return null; 552 } 553 dataSourcesConfigString = 554 Util.replaceProperties( 555 dataSourcesConfigString, 556 Util.toMap(System.getProperties())); 557 558 if (LOGGER.isDebugEnabled()) { 559 String msg = "XmlaServlet.parseDataSources: " + 560 "dataSources=" + dataSourcesConfigString; 561 LOGGER.debug(msg); 562 } 563 final Parser parser = XOMUtil.createDefaultParser(); 564 final DOMWrapper doc = parser.parse(dataSourcesConfigString); 565 return new DataSourcesConfig.DataSources(doc); 566 567 } catch (XOMException e) { 568 throw Util.newError(e, "Failed to parse data sources config: " + 569 dataSourcesConfigString); 570 } 571 } 572 573 /** 574 * Initialize character encoding 575 */ 576 protected void initCharEncodingHandler(ServletConfig servletConfig) { 577 String paramValue = servletConfig.getInitParameter(PARAM_CHAR_ENCODING); 578 if (paramValue != null) { 579 this.charEncoding = paramValue; 580 } else { 581 this.charEncoding = null; 582 LOGGER.warn("Use default character encoding from HTTP client"); 583 } 584 } 585 586 /** 587 * Registers callbacks configured in web.xml. 588 */ 589 protected void initCallbacks(ServletConfig servletConfig) { 590 String callbacksValue = servletConfig.getInitParameter(PARAM_CALLBACKS); 591 592 if (callbacksValue != null) { 593 String[] classNames = callbacksValue.split(";"); 594 595 int count = 0; 596 nextCallback: 597 for (String className1 : classNames) { 598 String className = className1.trim(); 599 600 try { 601 Class<?> cls = Class.forName(className); 602 if (XmlaRequestCallback.class.isAssignableFrom(cls)) { 603 XmlaRequestCallback callback = 604 (XmlaRequestCallback) cls.newInstance(); 605 606 try { 607 callback.init(servletConfig); 608 } catch (Exception e) { 609 LOGGER.warn("Failed to initialize callback '" + 610 className + "'", e); 611 continue nextCallback; 612 } 613 614 addCallback(callback); 615 count++; 616 617 if (LOGGER.isDebugEnabled()) { 618 LOGGER.info("Register callback '" + 619 className + "'"); 620 } 621 } else { 622 LOGGER.warn("'" + className + 623 "' is not an implementation of '" + 624 XmlaRequestCallback.class + "'"); 625 } 626 } catch (ClassNotFoundException cnfe) { 627 LOGGER.warn("Callback class '" + className + "' not found", 628 cnfe); 629 } catch (InstantiationException ie) { 630 LOGGER.warn("Can't instantiate class '" + className + "'", 631 ie); 632 } catch (IllegalAccessException iae) { 633 LOGGER.warn("Can't instantiate class '" + className + "'", 634 iae); 635 } 636 } 637 LOGGER.debug("Registered " + count + " callback" + (count > 1 ? "s" : "")); 638 } 639 } 640 641 } 642 643 // End XmlaServlet.java