/*
// $Id: //open/mondrian-release/3.2/src/main/mondrian/xmla/XmlaServlet.java#4 $
// This software is subject to the terms of the Eclipse Public License v1.0
// Agreement, available at the following URL:
// http://www.eclipse.org/legal/epl-v10.html.
// Copyright (C) 2003-2010 Julian Hyde
// All Rights Reserved.
// You must accept the terms of that agreement to use this software.
//
// jhyde, May 2, 2003
*/
package mondrian.xmla;
import mondrian.olap.Util;
import mondrian.spi.CatalogLocator;
import mondrian.spi.impl.ServletContextCatalogLocator;
import org.apache.log4j.Logger;
import org.eigenbase.xom.*;
import org.w3c.dom.Element;
import javax.servlet.ServletContext;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
/**
* Base XML/A servlet.
*
* @author Gang Chen
* @since December, 2005
* @version $Id: //open/mondrian-release/3.2/src/main/mondrian/xmla/XmlaServlet.java#4 $
*/
public abstract class XmlaServlet
extends HttpServlet
implements XmlaConstants
{
private static final Logger LOGGER = Logger.getLogger(XmlaServlet.class);
public static final String PARAM_DATASOURCES_CONFIG = "DataSourcesConfig";
public static final String PARAM_OPTIONAL_DATASOURCE_CONFIG =
"OptionalDataSourceConfig";
public static final String PARAM_CHAR_ENCODING = "CharacterEncoding";
public static final String PARAM_CALLBACKS = "Callbacks";
public static final String DEFAULT_DATASOURCE_FILE = "datasources.xml";
public enum Phase {
VALIDATE_HTTP_HEAD,
INITIAL_PARSE,
CALLBACK_PRE_ACTION,
PROCESS_HEADER,
PROCESS_BODY,
CALLBACK_POST_ACTION,
SEND_RESPONSE,
SEND_ERROR
}
/**
* Returns true if paramName's value is not null and 'true'.
*/
public static boolean getBooleanInitParameter(
ServletConfig servletConfig,
String paramName)
{
String paramValue = servletConfig.getInitParameter(paramName);
return paramValue != null && Boolean.valueOf(paramValue);
}
public static boolean getParameter(
HttpServletRequest req,
String paramName)
{
String paramValue = req.getParameter(paramName);
return paramValue != null && Boolean.valueOf(paramValue);
}
protected CatalogLocator catalogLocator = null;
protected DataSourcesConfig.DataSources dataSources = null;
protected XmlaHandler xmlaHandler = null;
protected String charEncoding = null;
private final List<XmlaRequestCallback> callbackList =
new ArrayList<XmlaRequestCallback>();
public XmlaServlet() {
}
/**
* Initializes servlet and XML/A handler.
*
*/
public void init(ServletConfig servletConfig) throws ServletException {
super.init(servletConfig);
// init: charEncoding
initCharEncodingHandler(servletConfig);
// init: callbacks
initCallbacks(servletConfig);
// make: catalogLocator
// A derived class can alter how the calalog locator object is
// created.
this.catalogLocator = makeCatalogLocator(servletConfig);
DataSourcesConfig.DataSources dataSources =
makeDataSources(servletConfig);
addToDataSources(dataSources);
}
/**
* Gets (creating if needed) the XmlaHandler.
*
* @return XMLA handler
*/
protected XmlaHandler getXmlaHandler() {
if (this.xmlaHandler == null) {
this.xmlaHandler =
new XmlaHandler(this.dataSources, this.catalogLocator, "cxmla");
}
return this.xmlaHandler;
}
/**
* Registers a callback.
*/
protected final void addCallback(XmlaRequestCallback callback) {
callbackList.add(callback);
}
/**
* Returns the list of callbacks. The list is immutable.
*
* @return list of callbacks
*/
protected final List<XmlaRequestCallback> getCallbacks() {
return Collections.unmodifiableList(callbackList);
}
/**
* Main entry for HTTP post method
*
*/
protected void doPost(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
// Request Soap Header and Body
// header [0] and body [1]
Element[] requestSoapParts = new Element[2];
// Response Soap Header and Body
// An array allows response parts to be passed into callback
// and possible modifications returned.
// response header in [0] and response body in [1]
byte[][] responseSoapParts = new byte[2][];
Phase phase = Phase.VALIDATE_HTTP_HEAD;
Enumeration.ResponseMimeType mimeType =
Enumeration.ResponseMimeType.SOAP;
try {
if (charEncoding != null) {
try {
request.setCharacterEncoding(charEncoding);
response.setCharacterEncoding(charEncoding);
} catch (UnsupportedEncodingException uee) {
charEncoding = null;
LOGGER.warn(
"Unsupported character encoding '" + charEncoding
+ "': Use default character encoding from HTTP client "
+ "for now");
}
}
response.setContentType(mimeType.getMimeType());
Map<String, Object> context = new HashMap<String, Object>();
try {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Invoking validate http header callbacks");
}
for (XmlaRequestCallback callback : getCallbacks()) {
if (!callback.processHttpHeader(
request,
response,
context))
{
return;
}
}
} catch (XmlaException xex) {
LOGGER.error(
"Errors when invoking callbacks validateHttpHeader", xex);
handleFault(response, responseSoapParts, phase, xex);
phase = Phase.SEND_ERROR;
marshallSoapMessage(response, responseSoapParts, mimeType);
return;
} catch (Exception ex) {
LOGGER.error(
"Errors when invoking callbacks validateHttpHeader", ex);
handleFault(
response, responseSoapParts,
phase,
new XmlaException(
SERVER_FAULT_FC,
CHH_CODE,
CHH_FAULT_FS,
ex));
phase = Phase.SEND_ERROR;
marshallSoapMessage(response, responseSoapParts, mimeType);
return;
}
phase = Phase.INITIAL_PARSE;
try {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Unmarshalling SOAP message");
}
// check request's content type
String contentType = request.getContentType();
if (contentType == null
|| contentType.indexOf("text/xml") == -1)
{
throw new IllegalArgumentException(
"Only accepts content type 'text/xml', not '"
+ contentType + "'");
}
// are they asking for a JSON response?
String accept = request.getHeader("Accept");
if (accept != null) {
mimeType = XmlaUtil.chooseResponseMimeType(accept);
if (mimeType == null) {
throw new IllegalArgumentException(
"Accept header '" + accept + "' is not a supported"
+ " response content type. Allowed values:"
+ " text/xml, application/xml, application/json.");
}
if (mimeType != Enumeration.ResponseMimeType.SOAP) {
response.setContentType(mimeType.getMimeType());
}
}
context.put(CONTEXT_MIME_TYPE, mimeType);
unmarshallSoapMessage(request, requestSoapParts);
} catch (XmlaException xex) {
LOGGER.error("Unable to unmarshall SOAP message", xex);
handleFault(response, responseSoapParts, phase, xex);
phase = Phase.SEND_ERROR;
marshallSoapMessage(response, responseSoapParts, mimeType);
return;
}
phase = Phase.PROCESS_HEADER;
try {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Handling XML/A message header");
}
// process application specified SOAP header here
handleSoapHeader(
response,
requestSoapParts,
responseSoapParts,
context);
} catch (XmlaException xex) {
LOGGER.error("Errors when handling XML/A message", xex);
handleFault(response, responseSoapParts, phase, xex);
phase = Phase.SEND_ERROR;
marshallSoapMessage(response, responseSoapParts, mimeType);
return;
}
phase = Phase.CALLBACK_PRE_ACTION;
try {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Invoking callbacks preAction");
}
for (XmlaRequestCallback callback : getCallbacks()) {
callback.preAction(request, requestSoapParts, context);
}
} catch (XmlaException xex) {
LOGGER.error("Errors when invoking callbacks preaction", xex);
handleFault(response, responseSoapParts, phase, xex);
phase = Phase.SEND_ERROR;
marshallSoapMessage(response, responseSoapParts, mimeType);
return;
} catch (Exception ex) {
LOGGER.error("Errors when invoking callbacks preaction", ex);
handleFault(
response, responseSoapParts,
phase,
new XmlaException(
SERVER_FAULT_FC,
CPREA_CODE,
CPREA_FAULT_FS,
ex));
phase = Phase.SEND_ERROR;
marshallSoapMessage(response, responseSoapParts, mimeType);
return;
}
phase = Phase.PROCESS_BODY;
try {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Handling XML/A message body");
}
// process XML/A request
handleSoapBody(
response,
requestSoapParts,
responseSoapParts,
context);
} catch (XmlaException xex) {
LOGGER.error("Errors when handling XML/A message", xex);
handleFault(response, responseSoapParts, phase, xex);
phase = Phase.SEND_ERROR;
marshallSoapMessage(response, responseSoapParts, mimeType);
return;
}
mimeType =
(Enumeration.ResponseMimeType) context.get(CONTEXT_MIME_TYPE);
phase = Phase.CALLBACK_POST_ACTION;
try {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Invoking callbacks postAction");
}
for (XmlaRequestCallback callback : getCallbacks()) {
callback.postAction(
request, response,
responseSoapParts, context);
}
} catch (XmlaException xex) {
LOGGER.error("Errors when invoking callbacks postaction", xex);
handleFault(response, responseSoapParts, phase, xex);
phase = Phase.SEND_ERROR;
marshallSoapMessage(response, responseSoapParts, mimeType);
return;
} catch (Exception ex) {
LOGGER.error("Errors when invoking callbacks postaction", ex);
handleFault(
response,
responseSoapParts,
phase,
new XmlaException(
SERVER_FAULT_FC,
CPOSTA_CODE,
CPOSTA_FAULT_FS,
ex));
phase = Phase.SEND_ERROR;
marshallSoapMessage(response, responseSoapParts, mimeType);
return;
}
phase = Phase.SEND_RESPONSE;
try {
response.setStatus(HttpServletResponse.SC_OK);
marshallSoapMessage(response, responseSoapParts, mimeType);
} catch (XmlaException xex) {
LOGGER.error("Errors when handling XML/A message", xex);
handleFault(response, responseSoapParts, phase, xex);
phase = Phase.SEND_ERROR;
marshallSoapMessage(response, responseSoapParts, mimeType);
return;
}
} catch (Throwable t) {
LOGGER.error("Unknown Error when handling XML/A message", t);
handleFault(response, responseSoapParts, phase, t);
marshallSoapMessage(response, responseSoapParts, mimeType);
}
}
/**
* Implement to provide application specified SOAP unmarshalling algorithm.
*/
protected abstract void unmarshallSoapMessage(
HttpServletRequest request,
Element[] requestSoapParts) throws XmlaException;
/**
* Implement to handle application specified SOAP header.
*/
protected abstract void handleSoapHeader(
HttpServletResponse response,
Element[] requestSoapParts,
byte[][] responseSoapParts,
Map<String, Object> context) throws XmlaException;
/**
* Implement to handle XML/A request.
*/
protected abstract void handleSoapBody(
HttpServletResponse response,
Element[] requestSoapParts,
byte[][] responseSoapParts,
Map<String, Object> context) throws XmlaException;
/**
* Implement to provide application specified SOAP marshalling algorithm.
*/
protected abstract void marshallSoapMessage(
HttpServletResponse response,
byte[][] responseSoapParts,
Enumeration.ResponseMimeType responseMimeType) throws XmlaException;
/**
* Implement to application specified handler of SOAP fualt.
*/
protected abstract void handleFault(
HttpServletResponse response,
byte[][] responseSoapParts,
Phase phase,
Throwable t);
/**
* Make catalog locator. Derived classes can roll their own
*/
protected CatalogLocator makeCatalogLocator(ServletConfig servletConfig) {
ServletContext servletContext = servletConfig.getServletContext();
return new ServletContextCatalogLocator(servletContext);
}
/**
* Make DataSourcesConfig.DataSources instance. Derived classes
* can roll their own
* <p>
* If there is an initParameter called "DataSourcesConfig"
* get its value, replace any "${key}" content with "value" where
* "key/value" are System properties, and try to create a URL
* instance out of it. If that fails, then assume its a
* real filepath and if the file exists then create a URL from it
* (but only if the file exists).
* If there is no initParameter with that name, then attempt to
* find the file called "datasources.xml" under "WEB-INF/"
* and if it exists, use it.
*/
protected DataSourcesConfig.DataSources makeDataSources(
ServletConfig servletConfig)
{
String paramValue =
servletConfig.getInitParameter(PARAM_DATASOURCES_CONFIG);
// if false, then do not throw exception if the file/url
// can not be found
boolean optional =
getBooleanInitParameter(
servletConfig, PARAM_OPTIONAL_DATASOURCE_CONFIG);
URL dataSourcesConfigUrl = null;
try {
if (paramValue == null) {
// fallback to default
String defaultDS = "WEB-INF/" + DEFAULT_DATASOURCE_FILE;
ServletContext servletContext =
servletConfig.getServletContext();
File realPath = new File(servletContext.getRealPath(defaultDS));
if (realPath.exists()) {
// only if it exists
dataSourcesConfigUrl = realPath.toURL();
}
} else {
paramValue = Util.replaceProperties(
paramValue,
Util.toMap(System.getProperties()));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"XmlaServlet.makeDataSources: paramValue="
+ paramValue);
}
// is the parameter a valid URL
MalformedURLException mue = null;
try {
dataSourcesConfigUrl = new URL(paramValue);
} catch (MalformedURLException e) {
// not a valid url
mue = e;
}
if (dataSourcesConfigUrl == null) {
// see if its a full valid file path
File f = new File(paramValue);
if (f.exists()) {
// yes, a real file path
dataSourcesConfigUrl = f.toURL();
} else if (mue != null) {
// neither url or file,
// is it not optional
if (! optional) {
throw mue;
}
}
}
}
} catch (MalformedURLException mue) {
throw Util.newError(mue, "invalid URL path '" + paramValue + "'");
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"XmlaServlet.makeDataSources: dataSourcesConfigUrl="
+ dataSourcesConfigUrl);
}
// don't try to parse a null
return (dataSourcesConfigUrl == null)
? null : parseDataSourcesUrl(dataSourcesConfigUrl);
}
protected void addToDataSources(DataSourcesConfig.DataSources dataSources) {
if (this.dataSources == null) {
this.dataSources = dataSources;
} else if (dataSources != null) {
DataSourcesConfig.DataSource[] ds1 = this.dataSources.dataSources;
int len1 = ds1.length;
DataSourcesConfig.DataSource[] ds2 = dataSources.dataSources;
int len2 = ds2.length;
DataSourcesConfig.DataSource[] tmp =
new DataSourcesConfig.DataSource[len1 + len2];
System.arraycopy(ds1, 0, tmp, 0, len1);
System.arraycopy(ds2, 0, tmp, len1, len2);
this.dataSources.dataSources = tmp;
} else {
LOGGER.warn("XmlaServlet.addToDataSources: DataSources is null");
}
}
protected DataSourcesConfig.DataSources parseDataSourcesUrl(
URL dataSourcesConfigUrl)
{
try {
String dataSourcesConfigString =
readDataSourcesContent(dataSourcesConfigUrl);
return parseDataSources(dataSourcesConfigString);
} catch (Exception e) {
throw Util.newError(
e,
"Failed to parse data sources config '"
+ dataSourcesConfigUrl.toExternalForm() + "'");
}
}
protected String readDataSourcesContent(URL dataSourcesConfigUrl)
throws IOException
{
return Util.readURL(
dataSourcesConfigUrl,
Util.toMap(System.getProperties()));
}
protected DataSourcesConfig.DataSources parseDataSources(
String dataSourcesConfigString)
{
try {
if (dataSourcesConfigString == null) {
LOGGER.warn("XmlaServlet.parseDataSources: null input");
return null;
}
dataSourcesConfigString =
Util.replaceProperties(
dataSourcesConfigString,
Util.toMap(System.getProperties()));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"XmlaServlet.parseDataSources: dataSources="
+ dataSourcesConfigString);
}
final Parser parser = XOMUtil.createDefaultParser();
final DOMWrapper doc = parser.parse(dataSourcesConfigString);
return new DataSourcesConfig.DataSources(doc);
} catch (XOMException e) {
throw Util.newError(
e,
"Failed to parse data sources config: "
+ dataSourcesConfigString);
}
}
/**
* Initialize character encoding
*/
protected void initCharEncodingHandler(ServletConfig servletConfig) {
String paramValue = servletConfig.getInitParameter(PARAM_CHAR_ENCODING);
if (paramValue != null) {
this.charEncoding = paramValue;
} else {
this.charEncoding = null;
LOGGER.warn("Use default character encoding from HTTP client");
}
}
/**
* Registers callbacks configured in web.xml.
*/
protected void initCallbacks(ServletConfig servletConfig) {
String callbacksValue = servletConfig.getInitParameter(PARAM_CALLBACKS);
if (callbacksValue != null) {
String[] classNames = callbacksValue.split(";");
int count = 0;
nextCallback:
for (String className1 : classNames) {
String className = className1.trim();
try {
Class<?> cls = Class.forName(className);
if (XmlaRequestCallback.class.isAssignableFrom(cls)) {
XmlaRequestCallback callback =
(XmlaRequestCallback) cls.newInstance();
try {
callback.init(servletConfig);
} catch (Exception e) {
LOGGER.warn(
"Failed to initialize callback '"
+ className + "'",
e);
continue nextCallback;
}
addCallback(callback);
count++;
if (LOGGER.isDebugEnabled()) {
LOGGER.info(
"Register callback '" + className + "'");
}
} else {
LOGGER.warn(
"'" + className + "' is not an implementation of '"
+ XmlaRequestCallback.class + "'");
}
} catch (ClassNotFoundException cnfe) {
LOGGER.warn(
"Callback class '" + className + "' not found",
cnfe);
} catch (InstantiationException ie) {
LOGGER.warn(
"Can't instantiate class '" + className + "'",
ie);
} catch (IllegalAccessException iae) {
LOGGER.warn(
"Can't instantiate class '" + className + "'",
iae);
}
}
LOGGER.debug(
"Registered " + count + " callback" + (count > 1 ? "s" : ""));
}
}
}
// End XmlaServlet.java