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