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 org.apache.log4j.Logger;
014
015import org.w3c.dom.Element;
016
017import java.io.IOException;
018import java.io.UnsupportedEncodingException;
019import java.util.*;
020import javax.servlet.ServletConfig;
021import javax.servlet.ServletException;
022import javax.servlet.http.*;
023
024/**
025 * Base XML/A servlet.
026 *
027 * @author Gang Chen
028 * @since December, 2005
029 */
030public abstract class XmlaServlet
031    extends HttpServlet
032    implements XmlaConstants
033{
034    protected static final Logger LOGGER = Logger.getLogger(XmlaServlet.class);
035
036    public static final String PARAM_DATASOURCES_CONFIG = "DataSourcesConfig";
037    public static final String PARAM_OPTIONAL_DATASOURCE_CONFIG =
038        "OptionalDataSourceConfig";
039    public static final String PARAM_CHAR_ENCODING = "CharacterEncoding";
040    public static final String PARAM_CALLBACKS = "Callbacks";
041
042    protected XmlaHandler xmlaHandler = null;
043    protected String charEncoding = null;
044    private final List<XmlaRequestCallback> callbackList =
045        new ArrayList<XmlaRequestCallback>();
046
047    private XmlaHandler.ConnectionFactory connectionFactory;
048
049    public enum Phase {
050        VALIDATE_HTTP_HEAD,
051        INITIAL_PARSE,
052        CALLBACK_PRE_ACTION,
053        PROCESS_HEADER,
054        PROCESS_BODY,
055        CALLBACK_POST_ACTION,
056        SEND_RESPONSE,
057        SEND_ERROR
058    }
059
060    /**
061     * Returns true if paramName's value is not null and 'true'.
062     */
063    public static boolean getBooleanInitParameter(
064        ServletConfig servletConfig,
065        String paramName)
066    {
067        String paramValue = servletConfig.getInitParameter(paramName);
068        return paramValue != null && Boolean.valueOf(paramValue);
069    }
070
071    public static boolean getParameter(
072        HttpServletRequest req,
073        String paramName)
074    {
075        String paramValue = req.getParameter(paramName);
076        return paramValue != null && Boolean.valueOf(paramValue);
077    }
078
079    public XmlaServlet() {
080    }
081
082
083    /**
084     * Initializes servlet and XML/A handler.
085     *
086     */
087    public void init(ServletConfig servletConfig)
088        throws ServletException
089    {
090        super.init(servletConfig);
091
092        // init: charEncoding
093        initCharEncodingHandler(servletConfig);
094
095        // init: callbacks
096        initCallbacks(servletConfig);
097
098        this.connectionFactory = createConnectionFactory(servletConfig);
099    }
100
101    protected abstract XmlaHandler.ConnectionFactory createConnectionFactory(
102        ServletConfig servletConfig)
103        throws ServletException;
104
105    /**
106     * Gets (creating if needed) the XmlaHandler.
107     *
108     * @return XMLA handler
109     */
110    protected XmlaHandler getXmlaHandler() {
111        if (this.xmlaHandler == null) {
112            this.xmlaHandler =
113                new XmlaHandler(
114                    connectionFactory,
115                    "cxmla");
116        }
117        return this.xmlaHandler;
118    }
119
120    /**
121     * Registers a callback.
122     */
123    protected final void addCallback(XmlaRequestCallback callback) {
124        callbackList.add(callback);
125    }
126
127    /**
128     * Returns the list of callbacks. The list is immutable.
129     *
130     * @return list of callbacks
131     */
132    protected final List<XmlaRequestCallback> getCallbacks() {
133        return Collections.unmodifiableList(callbackList);
134    }
135
136    /**
137     * Main entry for HTTP post method
138     *
139     */
140    protected void doPost(
141        HttpServletRequest request,
142        HttpServletResponse response)
143        throws ServletException, IOException
144    {
145        // Request Soap Header and Body
146        // header [0] and body [1]
147        Element[] requestSoapParts = new Element[2];
148
149        // Response Soap Header and Body
150        // An array allows response parts to be passed into callback
151        // and possible modifications returned.
152        // response header in [0] and response body in [1]
153        byte[][] responseSoapParts = new byte[2][];
154
155        Phase phase = Phase.VALIDATE_HTTP_HEAD;
156        Enumeration.ResponseMimeType mimeType =
157            Enumeration.ResponseMimeType.SOAP;
158
159        try {
160            if (charEncoding != null) {
161                try {
162                    request.setCharacterEncoding(charEncoding);
163                    response.setCharacterEncoding(charEncoding);
164                } catch (UnsupportedEncodingException uee) {
165                    charEncoding = null;
166                    LOGGER.warn(
167                        "Unsupported character encoding '" + charEncoding
168                        + "': Use default character encoding from HTTP client "
169                        + "for now");
170                }
171            }
172
173            response.setContentType(mimeType.getMimeType());
174
175            Map<String, Object> context = new HashMap<String, Object>();
176
177            try {
178                if (LOGGER.isDebugEnabled()) {
179                    LOGGER.debug("Invoking validate http header callbacks");
180                }
181                for (XmlaRequestCallback callback : getCallbacks()) {
182                    if (!callback.processHttpHeader(
183                            request,
184                            response,
185                            context))
186                    {
187                        return;
188                    }
189                }
190            } catch (XmlaException xex) {
191                LOGGER.error(
192                    "Errors when invoking callbacks validateHttpHeader", xex);
193                handleFault(response, responseSoapParts, phase, xex);
194                phase = Phase.SEND_ERROR;
195                marshallSoapMessage(response, responseSoapParts, mimeType);
196                return;
197            } catch (Exception ex) {
198                LOGGER.error(
199                    "Errors when invoking callbacks validateHttpHeader", ex);
200                handleFault(
201                    response, responseSoapParts,
202                    phase,
203                    new XmlaException(
204                        SERVER_FAULT_FC,
205                        CHH_CODE,
206                        CHH_FAULT_FS,
207                        ex));
208                phase = Phase.SEND_ERROR;
209                marshallSoapMessage(response, responseSoapParts, mimeType);
210                return;
211            }
212
213
214            phase = Phase.INITIAL_PARSE;
215
216            try {
217                if (LOGGER.isDebugEnabled()) {
218                    LOGGER.debug("Unmarshalling SOAP message");
219                }
220
221                // check request's content type
222                String contentType = request.getContentType();
223                if (contentType == null
224                    || !contentType.contains("text/xml"))
225                {
226                    throw new IllegalArgumentException(
227                        "Only accepts content type 'text/xml', not '"
228                        + contentType + "'");
229                }
230
231                // are they asking for a JSON response?
232                String accept = request.getHeader("Accept");
233                if (accept != null) {
234                    mimeType = XmlaUtil.chooseResponseMimeType(accept);
235                    if (mimeType == null) {
236                        throw new IllegalArgumentException(
237                            "Accept header '" + accept + "' is not a supported"
238                            + " response content type. Allowed values:"
239                            + " text/xml, application/xml, application/json.");
240                    }
241                    if (mimeType != Enumeration.ResponseMimeType.SOAP) {
242                        response.setContentType(mimeType.getMimeType());
243                    }
244                }
245                context.put(CONTEXT_MIME_TYPE, mimeType);
246
247                unmarshallSoapMessage(request, requestSoapParts);
248            } catch (XmlaException xex) {
249                LOGGER.error("Unable to unmarshall SOAP message", xex);
250                handleFault(response, responseSoapParts, phase, xex);
251                phase = Phase.SEND_ERROR;
252                marshallSoapMessage(response, responseSoapParts, mimeType);
253                return;
254            }
255
256            phase = Phase.PROCESS_HEADER;
257
258            try {
259                if (LOGGER.isDebugEnabled()) {
260                    LOGGER.debug("Handling XML/A message header");
261                }
262
263                // process application specified SOAP header here
264                handleSoapHeader(
265                    response,
266                    requestSoapParts,
267                    responseSoapParts,
268                    context);
269            } catch (XmlaException xex) {
270                LOGGER.error("Errors when handling XML/A message", xex);
271                handleFault(response, responseSoapParts, phase, xex);
272                phase = Phase.SEND_ERROR;
273                marshallSoapMessage(response, responseSoapParts, mimeType);
274                return;
275            }
276
277            phase = Phase.CALLBACK_PRE_ACTION;
278
279
280            try {
281                if (LOGGER.isDebugEnabled()) {
282                    LOGGER.debug("Invoking callbacks preAction");
283                }
284
285                for (XmlaRequestCallback callback : getCallbacks()) {
286                    callback.preAction(request, requestSoapParts, context);
287                }
288            } catch (XmlaException xex) {
289                LOGGER.error("Errors when invoking callbacks preaction", xex);
290                handleFault(response, responseSoapParts, phase, xex);
291                phase = Phase.SEND_ERROR;
292                marshallSoapMessage(response, responseSoapParts, mimeType);
293                return;
294            } catch (Exception ex) {
295                LOGGER.error("Errors when invoking callbacks preaction", ex);
296                handleFault(
297                    response, responseSoapParts,
298                    phase,
299                    new XmlaException(
300                        SERVER_FAULT_FC,
301                        CPREA_CODE,
302                        CPREA_FAULT_FS,
303                        ex));
304                phase = Phase.SEND_ERROR;
305                marshallSoapMessage(response, responseSoapParts, mimeType);
306                return;
307            }
308
309            phase = Phase.PROCESS_BODY;
310
311            try {
312                if (LOGGER.isDebugEnabled()) {
313                    LOGGER.debug("Handling XML/A message body");
314                }
315
316                // process XML/A request
317                handleSoapBody(
318                    response,
319                    requestSoapParts,
320                    responseSoapParts,
321                    context);
322            } catch (XmlaException xex) {
323                LOGGER.error("Errors when handling XML/A message", xex);
324                handleFault(response, responseSoapParts, phase, xex);
325                phase = Phase.SEND_ERROR;
326                marshallSoapMessage(response, responseSoapParts, mimeType);
327                return;
328            }
329
330            mimeType =
331                (Enumeration.ResponseMimeType) context.get(CONTEXT_MIME_TYPE);
332
333            phase = Phase.CALLBACK_POST_ACTION;
334
335            try {
336                if (LOGGER.isDebugEnabled()) {
337                    LOGGER.debug("Invoking callbacks postAction");
338                }
339
340                for (XmlaRequestCallback callback : getCallbacks()) {
341                    callback.postAction(
342                        request, response,
343                        responseSoapParts, context);
344                }
345            } catch (XmlaException xex) {
346                LOGGER.error("Errors when invoking callbacks postaction", xex);
347                handleFault(response, responseSoapParts, phase, xex);
348                phase = Phase.SEND_ERROR;
349                marshallSoapMessage(response, responseSoapParts, mimeType);
350                return;
351            } catch (Exception ex) {
352                LOGGER.error("Errors when invoking callbacks postaction", ex);
353                handleFault(
354                    response,
355                    responseSoapParts,
356                    phase,
357                    new XmlaException(
358                        SERVER_FAULT_FC,
359                        CPOSTA_CODE,
360                        CPOSTA_FAULT_FS,
361                        ex));
362                phase = Phase.SEND_ERROR;
363                marshallSoapMessage(response, responseSoapParts, mimeType);
364                return;
365            }
366
367            phase = Phase.SEND_RESPONSE;
368
369            try {
370                response.setStatus(HttpServletResponse.SC_OK);
371                marshallSoapMessage(response, responseSoapParts, mimeType);
372            } catch (XmlaException xex) {
373                LOGGER.error("Errors when handling XML/A message", xex);
374                handleFault(response, responseSoapParts, phase, xex);
375                phase = Phase.SEND_ERROR;
376                marshallSoapMessage(response, responseSoapParts, mimeType);
377            }
378        } catch (Throwable t) {
379            LOGGER.error("Unknown Error when handling XML/A message", t);
380            handleFault(response, responseSoapParts, phase, t);
381            marshallSoapMessage(response, responseSoapParts, mimeType);
382        }
383    }
384
385    /**
386     * Implement to provide application specified SOAP unmarshalling algorithm.
387     */
388    protected abstract void unmarshallSoapMessage(
389        HttpServletRequest request,
390        Element[] requestSoapParts)
391        throws XmlaException;
392
393    /**
394     * Implement to handle application specified SOAP header.
395     */
396    protected abstract void handleSoapHeader(
397        HttpServletResponse response,
398        Element[] requestSoapParts,
399        byte[][] responseSoapParts,
400        Map<String, Object> context)
401        throws XmlaException;
402
403    /**
404     * Implement to handle XML/A request.
405     */
406    protected abstract void handleSoapBody(
407        HttpServletResponse response,
408        Element[] requestSoapParts,
409        byte[][] responseSoapParts,
410        Map<String, Object> context)
411        throws XmlaException;
412
413    /**
414     * Implement to provide application specified SOAP marshalling algorithm.
415     */
416    protected abstract void marshallSoapMessage(
417        HttpServletResponse response,
418        byte[][] responseSoapParts,
419        Enumeration.ResponseMimeType responseMimeType)
420        throws XmlaException;
421
422    /**
423     * Implement to application specified handler of SOAP fualt.
424     */
425    protected abstract void handleFault(
426        HttpServletResponse response,
427        byte[][] responseSoapParts,
428        Phase phase,
429        Throwable t);
430
431    /**
432     * Initialize character encoding
433     */
434    protected void initCharEncodingHandler(ServletConfig servletConfig) {
435        String paramValue = servletConfig.getInitParameter(PARAM_CHAR_ENCODING);
436        if (paramValue != null) {
437            this.charEncoding = paramValue;
438        } else {
439            this.charEncoding = null;
440            LOGGER.warn("Use default character encoding from HTTP client");
441        }
442    }
443
444    /**
445     * Registers callbacks configured in web.xml.
446     */
447    protected void initCallbacks(ServletConfig servletConfig) {
448        String callbacksValue = servletConfig.getInitParameter(PARAM_CALLBACKS);
449
450        if (callbacksValue != null) {
451            String[] classNames = callbacksValue.split(";");
452
453            int count = 0;
454            nextCallback:
455            for (String className1 : classNames) {
456                String className = className1.trim();
457
458                try {
459                    Class<?> cls = Class.forName(className);
460                    if (XmlaRequestCallback.class.isAssignableFrom(cls)) {
461                        XmlaRequestCallback callback =
462                            (XmlaRequestCallback) cls.newInstance();
463
464                        try {
465                            callback.init(servletConfig);
466                        } catch (Exception e) {
467                            LOGGER.warn(
468                                "Failed to initialize callback '"
469                                + className + "'",
470                                e);
471                            continue nextCallback;
472                        }
473
474                        addCallback(callback);
475                        count++;
476
477                        if (LOGGER.isDebugEnabled()) {
478                            LOGGER.debug(
479                                "Register callback '" + className + "'");
480                        }
481                    } else {
482                        LOGGER.warn(
483                            "'" + className + "' is not an implementation of '"
484                            + XmlaRequestCallback.class + "'");
485                    }
486                } catch (ClassNotFoundException cnfe) {
487                    LOGGER.warn(
488                        "Callback class '" + className + "' not found",
489                        cnfe);
490                } catch (InstantiationException ie) {
491                    LOGGER.warn(
492                        "Can't instantiate class '" + className + "'",
493                        ie);
494                } catch (IllegalAccessException iae) {
495                    LOGGER.warn(
496                        "Can't instantiate class '" + className + "'",
497                        iae);
498                }
499            }
500            LOGGER.debug(
501                "Registered " + count + " callback" + (count > 1 ? "s" : ""));
502        }
503    }
504}
505
506// End XmlaServlet.java