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-2012 Pentaho
009// All Rights Reserved.
010*/
011package mondrian.xmla;
012
013import mondrian.olap.Util;
014
015import org.apache.log4j.Logger;
016
017import org.olap4j.OlapConnection;
018import org.olap4j.impl.LcidLocale;
019import org.olap4j.metadata.Catalog;
020
021import java.sql.SQLException;
022import java.util.*;
023import java.util.regex.Matcher;
024import java.util.regex.Pattern;
025
026/**
027 * Base class for an XML for Analysis schema rowset. A concrete derived class
028 * should implement {@link #populateImpl}, calling {@link #addRow} for each row.
029 *
030 * @author jhyde
031 * @see mondrian.xmla.RowsetDefinition
032 * @since May 2, 2003
033 */
034abstract class Rowset implements XmlaConstants {
035    protected static final Logger LOGGER = Logger.getLogger(Rowset.class);
036
037    protected final RowsetDefinition rowsetDefinition;
038    protected final Map<String, Object> restrictions;
039    protected final Map<String, String> properties;
040    protected final Map<String, String> extraProperties =
041        new HashMap<String, String>();
042    protected final XmlaRequest request;
043    protected final XmlaHandler handler;
044    private final RowsetDefinition.Column[] restrictedColumns;
045    protected final boolean deep;
046
047    /**
048     * Creates a Rowset.
049     *
050     * <p>The exceptions thrown in this constructor are not produced during the
051     * execution of an XMLA request and so can be ordinary exceptions and not
052     * XmlaException (which are specifically for generating SOAP Fault xml).
053     */
054    Rowset(
055        RowsetDefinition definition,
056        XmlaRequest request,
057        XmlaHandler handler)
058    {
059        this.rowsetDefinition = definition;
060        this.restrictions = request.getRestrictions();
061        this.properties = request.getProperties();
062        this.request = request;
063        this.handler = handler;
064        ArrayList<RowsetDefinition.Column> list =
065            new ArrayList<RowsetDefinition.Column>();
066        for (Map.Entry<String, Object> restrictionEntry
067            : restrictions.entrySet())
068        {
069            String restrictedColumn = restrictionEntry.getKey();
070            LOGGER.debug(
071                "Rowset<init>: restrictedColumn=\"" + restrictedColumn + "\"");
072            final RowsetDefinition.Column column = definition.lookupColumn(
073                restrictedColumn);
074            if (column == null) {
075                throw Util.newError(
076                    "Rowset '" + definition.name()
077                    + "' does not contain column '" + restrictedColumn + "'");
078            }
079            if (!column.restriction) {
080                throw Util.newError(
081                    "Rowset '" + definition.name()
082                    + "' column '" + restrictedColumn
083                    + "' does not allow restrictions");
084            }
085            // Check that the value is of the right type.
086            final Object restriction = restrictionEntry.getValue();
087            if (restriction instanceof List
088                && ((List) restriction).size() > 1)
089            {
090                final RowsetDefinition.Type type = column.type;
091                switch (type) {
092                case StringArray:
093                case EnumerationArray:
094                case StringSometimesArray:
095                    break; // OK
096                default:
097                    throw Util.newError(
098                        "Rowset '" + definition.name()
099                        + "' column '" + restrictedColumn
100                        + "' can only be restricted on one value at a time");
101                }
102            }
103            list.add(column);
104        }
105        list = pruneRestrictions(list);
106        this.restrictedColumns =
107            list.toArray(
108                new RowsetDefinition.Column[list.size()]);
109        boolean deep = false;
110        for (Map.Entry<String, String> propertyEntry : properties.entrySet()) {
111            String propertyName = propertyEntry.getKey();
112            final PropertyDefinition propertyDef =
113                Util.lookup(PropertyDefinition.class, propertyName);
114            if (propertyDef == null) {
115                throw Util.newError(
116                    "Rowset '" + definition.name()
117                    + "' does not support property '" + propertyName + "'");
118            }
119            final String propertyValue = propertyEntry.getValue();
120            setProperty(propertyDef, propertyValue);
121            if (propertyDef == PropertyDefinition.Deep) {
122                deep = Boolean.valueOf(propertyValue);
123            }
124        }
125        this.deep = deep;
126    }
127
128    protected ArrayList<RowsetDefinition.Column> pruneRestrictions(
129        ArrayList<RowsetDefinition.Column> list)
130    {
131        return list;
132    }
133
134    /**
135     * Sets a property for this rowset. Called by the constructor for each
136     * supplied property.<p/>
137     *
138     * A derived class should override this method and intercept each
139     * property it supports. Any property it does not support, it should forward
140     * to the base class method, which will probably throw an error.<p/>
141     */
142    protected void setProperty(PropertyDefinition propertyDef, String value) {
143        switch (propertyDef) {
144        case Format:
145            break;
146        case DataSourceInfo:
147            break;
148        case Catalog:
149            break;
150        case LocaleIdentifier:
151            if (value != null) {
152                try {
153                    // First check for a numeric locale id (LCID) as used by
154                    // Windows.
155                    final short lcid = Short.valueOf(value);
156                    final Locale locale = LcidLocale.lcidToLocale(lcid);
157                    if (locale != null) {
158                        extraProperties.put(
159                            XmlaHandler.JDBC_LOCALE, locale.toString());
160                        return;
161                    }
162                } catch (NumberFormatException nfe) {
163                    // Since value is not a valid LCID, now see whether it is a
164                    // locale name, e.g. "en_US". This behavior is an
165                    // extension to the XMLA spec.
166                    try {
167                        Locale locale = Util.parseLocale(value);
168                        extraProperties.put(
169                            XmlaHandler.JDBC_LOCALE, locale.toString());
170                        return;
171                    } catch (RuntimeException re) {
172                        // probably a bad locale string; fall through
173                    }
174                }
175                return;
176            }
177            // fall through
178        default:
179            LOGGER.warn(
180                "Warning: Rowset '" + rowsetDefinition.name()
181                + "' does not support property '" + propertyDef.name()
182                + "' (value is '" + value + "')");
183        }
184    }
185
186    /**
187     * Writes the contents of this rowset as a series of SAX events.
188     */
189    public final void unparse(XmlaResponse response)
190        throws XmlaException, SQLException
191    {
192        final List<Row> rows = new ArrayList<Row>();
193        populate(response, null, rows);
194        final Comparator<Row> comparator = rowsetDefinition.getComparator();
195        if (comparator != null) {
196            Collections.sort(rows, comparator);
197        }
198        final SaxWriter writer = response.getWriter();
199        writer.startSequence(null, "row");
200        for (Row row : rows) {
201            emit(row, response);
202        }
203        writer.endSequence();
204    }
205
206    /**
207     * Gathers the set of rows which match a given set of the criteria.
208     */
209    public final void populate(
210        XmlaResponse response,
211        OlapConnection connection,
212        List<Row> rows)
213        throws XmlaException
214    {
215        boolean ourConnection = false;
216        try {
217            if (needConnection() && connection == null) {
218                connection = handler.getConnection(request, extraProperties);
219                ourConnection = true;
220            }
221            populateImpl(response, connection, rows);
222        } catch (SQLException e) {
223            throw new XmlaException(
224                UNKNOWN_ERROR_CODE,
225                UNKNOWN_ERROR_FAULT_FS,
226                "SqlException:",
227                e);
228        } finally {
229            if (connection != null && ourConnection) {
230                try {
231                    connection.close();
232                } catch (SQLException e) {
233                    // ignore
234                }
235            }
236        }
237    }
238
239    protected boolean needConnection() {
240        return true;
241    }
242
243    /**
244     * Gathers the set of rows which match a given set of the criteria.
245     */
246    protected abstract void populateImpl(
247        XmlaResponse response,
248        OlapConnection connection,
249        List<Row> rows)
250        throws XmlaException, SQLException;
251
252    /**
253     * Adds a {@link Row} to a result, provided that it meets the necessary
254     * criteria. Returns whether the row was added.
255     *
256     * @param row Row
257     * @param rows List of result rows
258     */
259    protected final boolean addRow(
260        Row row,
261        List<Row> rows)
262        throws XmlaException
263    {
264        return rows.add(row);
265    }
266
267    /**
268     * Emits a row for this rowset, reading fields from a
269     * {@link mondrian.xmla.Rowset.Row} object.
270     *
271     * @param row Row
272     * @param response XMLA response writer
273     */
274    protected void emit(Row row, XmlaResponse response)
275        throws XmlaException, SQLException
276    {
277        SaxWriter writer = response.getWriter();
278
279        writer.startElement("row");
280        for (RowsetDefinition.Column column
281            : rowsetDefinition.columnDefinitions)
282        {
283            Object value = row.get(column.name);
284            if (value == null) {
285                if (!column.nullable) {
286                    throw new XmlaException(
287                        CLIENT_FAULT_FC,
288                        HSB_BAD_NON_NULLABLE_COLUMN_CODE,
289                        HSB_BAD_NON_NULLABLE_COLUMN_FAULT_FS,
290                        Util.newInternal(
291                            "Value required for column "
292                            + column.name
293                            + " of rowset "
294                            + rowsetDefinition.name()));
295                }
296            } else if (value instanceof XmlElement[]) {
297                XmlElement[] elements = (XmlElement[]) value;
298                for (XmlElement element : elements) {
299                    emitXmlElement(writer, element);
300                }
301            } else if (value instanceof Object[]) {
302                Object[] values = (Object[]) value;
303                for (Object value1 : values) {
304                    writer.startElement(column.name);
305                    writer.characters(value1.toString());
306                    writer.endElement();
307                }
308            } else if (value instanceof List) {
309                List values = (List) value;
310                for (Object value1 : values) {
311                    if (value1 instanceof XmlElement) {
312                        XmlElement xmlElement = (XmlElement) value1;
313                        emitXmlElement(writer, xmlElement);
314                    } else {
315                        writer.startElement(column.name);
316                        writer.characters(value1.toString());
317                        writer.endElement();
318                    }
319                }
320            } else if (value instanceof Rowset) {
321                Rowset rowset = (Rowset) value;
322                final List<Row> rows = new ArrayList<Row>();
323                rowset.populate(response, null, rows);
324                writer.startSequence(column.name, "row");
325                for (Row row1 : rows) {
326                    rowset.emit(row1, response);
327                }
328                writer.endSequence();
329            } else {
330                writer.textElement(column.name, value);
331            }
332        }
333        writer.endElement();
334    }
335
336    private void emitXmlElement(SaxWriter writer, XmlElement element) {
337        if (element.attributes == null) {
338            writer.startElement(element.tag);
339        } else {
340            writer.startElement(element.tag, element.attributes);
341        }
342
343        if (element.text == null) {
344            for (XmlElement aChildren : element.children) {
345                emitXmlElement(writer, aChildren);
346            }
347        } else {
348            writer.characters(element.text);
349        }
350
351        writer.endElement();
352    }
353
354    /**
355     * Populates all of the values in an enumeration into a list of rows.
356     */
357    protected <E> void populate(
358        Class<E> clazz, List<Row> rows,
359        final Comparator<E> comparator)
360        throws XmlaException
361    {
362        final E[] enumsSortedByName = clazz.getEnumConstants().clone();
363        Arrays.sort(enumsSortedByName, comparator);
364        for (E anEnum : enumsSortedByName) {
365            Row row = new Row();
366            for (RowsetDefinition.Column column
367                : rowsetDefinition.columnDefinitions)
368            {
369                row.names.add(column.name);
370                row.values.add(column.get(anEnum));
371            }
372            rows.add(row);
373        }
374    }
375
376    /**
377     * Creates a condition functor based on the restrictions on a given metadata
378     * column specified in an XMLA request.
379     *
380     * <p>A condition is a {@link mondrian.olap.Util.Functor1} whose return
381     * type is boolean.
382     *
383     * Restrictions are used in each Rowset's discovery request. If there is no
384     * restriction then the passes method always returns true.
385     *
386     * <p>It is known at the beginning of a
387     * {@link Rowset#populate(XmlaResponse, org.olap4j.OlapConnection, java.util.List)}
388     * method whether the restriction is not specified (null), a single value
389     * (String) or an array of values (String[]). So, creating the conditions
390     * just once at the beginning is faster than having to determine the
391     * restriction status each time it is needed.
392     *
393     * @param column Metadata column
394     * @param <E> Element type, e.g. {@link org.olap4j.metadata.Catalog} or
395     *     {@link org.olap4j.metadata.Level}
396     * @return Condition functor
397     */
398    <E> Util.Functor1<Boolean, E> makeCondition(
399        RowsetDefinition.Column column)
400    {
401        return makeCondition(
402            Util.<E>identityFunctor(),
403            column);
404    }
405
406    /**
407     * Creates a condition functor using an accessor.
408     *
409     * <p>The accessor gets a particular property of the element in question
410     * for the column restrictions to act upon.
411     *
412     * @param getter Attribute accessor
413     * @param column Metadata column
414     * @param <E> Element type, e.g. {@link org.olap4j.metadata.Catalog} or
415     *     {@link org.olap4j.metadata.Level}
416     * @param <V> Value type; often {@link String}, since many restrictions
417     *     work on the name or unique name of elements
418     * @return Condition functor
419     */
420    <E, V> Util.Functor1<Boolean, E> makeCondition(
421        final Util.Functor1<V, ? super E> getter,
422        RowsetDefinition.Column column)
423    {
424        final Object restriction = restrictions.get(column.name);
425
426        if (restriction == null) {
427            return Util.trueFunctor();
428        } else if (restriction instanceof XmlaUtil.Wildcard) {
429            XmlaUtil.Wildcard wildcard = (XmlaUtil.Wildcard) restriction;
430            String regexp =
431                Util.wildcardToRegexp(
432                    Collections.singletonList(wildcard.pattern));
433            final Matcher matcher = Pattern.compile(regexp).matcher("");
434            return new Util.Functor1<Boolean, E>() {
435                public Boolean apply(E element) {
436                    V value = getter.apply(element);
437                    return matcher.reset(String.valueOf(value)).matches();
438                }
439            };
440        } else if (restriction instanceof List) {
441            final List<V> requiredValues = (List) restriction;
442            return new Util.Functor1<Boolean, E>() {
443                public Boolean apply(E element) {
444                    if (element == null) {
445                        return requiredValues.contains("");
446                    }
447                    V value = getter.apply(element);
448                    return requiredValues.contains(value);
449                }
450            };
451        } else {
452            throw Util.newInternal(
453                "unexpected restriction type: " + restriction.getClass());
454        }
455    }
456
457    /**
458     * Returns the restriction if it is a String, or null otherwise. Does not
459     * attempt two determine if the restriction is an array of Strings
460     * if all members of the array have the same value (in which case
461     * one could return, again, simply a single String).
462     */
463    String getRestrictionValueAsString(RowsetDefinition.Column column) {
464        final Object restriction = restrictions.get(column.name);
465        if (restriction instanceof List) {
466            List<String> rval = (List<String>) restriction;
467            if (rval.size() == 1) {
468                return rval.get(0);
469            }
470        }
471        return null;
472    }
473
474    /**
475     * Returns a column's restriction as an <code>int</code> if it
476     * exists, -1 otherwise.
477     */
478    int getRestrictionValueAsInt(RowsetDefinition.Column column) {
479        final Object restriction = restrictions.get(column.name);
480        if (restriction instanceof List) {
481            List<String> rval = (List<String>) restriction;
482            if (rval.size() == 1) {
483                try {
484                    return Integer.parseInt(rval.get(0));
485                } catch (NumberFormatException ex) {
486                    LOGGER.info(
487                        "Rowset.getRestrictionValue: "
488                        + "bad integer restriction \"" + rval + "\"");
489                    return -1;
490                }
491            }
492        }
493        return -1;
494    }
495
496    /**
497     * Returns true if there is a restriction for the given column
498     * definition.
499     *
500     */
501    protected boolean isRestricted(RowsetDefinition.Column column) {
502        return (restrictions.get(column.name) != null);
503    }
504
505    protected Util.Functor1<Boolean, Catalog> catNameCond() {
506        Map<String, String> properties = request.getProperties();
507        final String catalogName =
508            properties.get(PropertyDefinition.Catalog.name());
509        if (catalogName != null) {
510            return new Util.Functor1<Boolean, Catalog>() {
511                public Boolean apply(Catalog catalog) {
512                    return catalog.getName().equals(catalogName);
513                }
514            };
515        } else {
516            return Util.trueFunctor();
517        }
518    }
519
520    /**
521     * A set of name/value pairs, which can be output using
522     * {@link Rowset#addRow}. This uses less memory than simply
523     * using a HashMap and for very big data sets memory is
524     * a concern.
525     */
526    protected static class Row {
527        private final ArrayList<String> names;
528        private final ArrayList<Object> values;
529        Row() {
530            this.names = new ArrayList<String>();
531            this.values = new ArrayList<Object>();
532        }
533
534        void set(String name, Object value) {
535            this.names.add(name);
536            this.values.add(value);
537        }
538
539        /**
540         * Retrieves the value of a field with a given name, or null if the
541         * field's value is not defined.
542         */
543        public Object get(String name) {
544            int i = this.names.indexOf(name);
545            return (i < 0) ? null : this.values.get(i);
546        }
547    }
548
549    /**
550     * Holder for non-scalar column values of a
551     * {@link mondrian.xmla.Rowset.Row}.
552     */
553    protected static class XmlElement {
554        private final String tag;
555        private final Object[] attributes;
556        private final String text;
557        private final XmlElement[] children;
558
559        XmlElement(String tag, Object[] attributes, String text) {
560            this(tag, attributes, text, null);
561        }
562
563        XmlElement(String tag, Object[] attributes, XmlElement[] children) {
564            this(tag, attributes, null, children);
565        }
566
567        private XmlElement(
568            String tag,
569            Object[] attributes,
570            String text,
571            XmlElement[] children)
572        {
573            assert attributes == null || attributes.length % 2 == 0;
574            this.tag = tag;
575            this.attributes = attributes;
576            this.text = text;
577            this.children = children;
578        }
579    }
580}
581
582// End Rowset.java