001    /*
002    // $Id: //open/mondrian/src/main/mondrian/xmla/Rowset.java#29 $
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-2007 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 java.util.*;
015    import java.util.regex.Pattern;
016    import java.util.regex.Matcher;
017    
018    import mondrian.olap.Util;
019    
020    import org.apache.log4j.Logger;
021    
022    /**
023     * Base class for an XML for Analysis schema rowset. A concrete derived class
024     * should implement {@link #populate}, calling {@link #addRow} for each row.
025     *
026     * @author jhyde
027     * @see mondrian.xmla.RowsetDefinition
028     * @since May 2, 2003
029     * @version $Id: //open/mondrian/src/main/mondrian/xmla/Rowset.java#29 $
030     */
031    abstract class Rowset implements XmlaConstants {
032        protected static final Logger LOGGER = Logger.getLogger(Rowset.class);
033    
034        protected final RowsetDefinition rowsetDefinition;
035        protected final Map<String, Object> restrictions;
036        protected final Map<String, String> properties;
037        protected final XmlaRequest request;
038        protected final XmlaHandler handler;
039        private final RowsetDefinition.Column[] restrictedColumns;
040    
041        /**
042         * The exceptions thrown in this constructor are not produced during
043         * the execution of an XMLA request and so can be ordinary exceptions and
044         * not XmlaException (which are  specifically for generating SOAP Fault
045         * xml).
046         *
047         */
048        Rowset(RowsetDefinition definition, XmlaRequest request, XmlaHandler handler) {
049            this.rowsetDefinition = definition;
050            this.restrictions = request.getRestrictions();
051            this.properties = request.getProperties();
052            this.request = request;
053            this.handler = handler;
054            ArrayList<RowsetDefinition.Column> list =
055                new ArrayList<RowsetDefinition.Column>();
056            for (Map.Entry<String, Object> restrictionEntry :
057                restrictions.entrySet())
058            {
059                String restrictedColumn = restrictionEntry.getKey();
060                LOGGER.debug(
061                    "Rowset<init>: restrictedColumn=\"" + restrictedColumn + "\"");
062                final RowsetDefinition.Column column = definition.lookupColumn(
063                    restrictedColumn);
064                if (column == null) {
065                    throw Util.newError("Rowset '" + definition.name() +
066                        "' does not contain column '" + restrictedColumn + "'");
067                }
068                if (!column.restriction) {
069                    throw Util.newError("Rowset '" + definition.name() +
070                        "' column '" + restrictedColumn +
071                        "' does not allow restrictions");
072                }
073                // Check that the value is of the right type.
074                final Object restriction = restrictionEntry.getValue();
075                if (restriction instanceof List
076                    && ((List) restriction).size() > 1)
077                {
078                    final RowsetDefinition.Type type = column.type;
079                    switch (type) {
080                    case StringArray:
081                    case EnumerationArray:
082                    case StringSometimesArray:
083                        break; // OK
084                    default:
085                        throw Util.newError("Rowset '" + definition.name() +
086                            "' column '" + restrictedColumn +
087                            "' can only be restricted on one value at a time");
088                    }
089                }
090                list.add(column);
091            }
092            list = pruneRestrictions(list);
093            this.restrictedColumns =
094                list.toArray(
095                    new RowsetDefinition.Column[list.size()]);
096            for (Map.Entry<String, String> propertyEntry : properties.entrySet()) {
097                String propertyName = propertyEntry.getKey();
098                final PropertyDefinition propertyDef =
099                    Util.lookup(PropertyDefinition.class, propertyName);
100                if (propertyDef == null) {
101                    throw Util.newError("Rowset '" + definition.name() +
102                        "' does not support property '" + propertyName + "'");
103                }
104                final String propertyValue = propertyEntry.getValue();
105                setProperty(propertyDef, propertyValue);
106            }
107        }
108    
109        protected ArrayList<RowsetDefinition.Column> pruneRestrictions(
110            ArrayList<RowsetDefinition.Column> list)
111        {
112            return list;
113        }
114    
115        /**
116         * Sets a property for this rowset. Called by the constructor for each
117         * supplied property.<p/>
118         *
119         * A derived class should override this method and intercept each
120         * property it supports. Any property it does not support, it should forward
121         * to the base class method, which will probably throw an error.<p/>
122         */
123        protected void setProperty(PropertyDefinition propertyDef, String value) {
124            switch (propertyDef) {
125            case Format:
126                break;
127            case DataSourceInfo:
128                break;
129            case Catalog:
130                break;
131            case LocaleIdentifier:
132                // locale ids:
133                // http://krafft.com/scripts/deluxe-calendar/lcid_chart.htm
134                // 1033 is US English
135                if ((value != null) && (value.equals("1033"))) {
136                    return;
137                }
138                // fall through
139            default:
140                LOGGER.warn("Warning: Rowset '" + rowsetDefinition.name() +
141                        "' does not support property '" + propertyDef.name() +
142                        "' (value is '" + value + "')");
143            }
144        }
145    
146        /**
147         * Writes the contents of this rowset as a series of SAX events.
148         */
149        public final void unparse(XmlaResponse response) throws XmlaException
150        {
151            List<Row> rows = new ArrayList<Row>();
152            populate(response, rows);
153            Comparator<Row> comparator = rowsetDefinition.getComparator();
154            if (comparator != null) {
155                Collections.sort(rows, comparator);
156            }
157            for (Row row : rows) {
158                emit(row, response);
159            }
160        }
161    
162        /**
163         * Gathers the set of rows which match a given set of the criteria.
164         */
165        public abstract void populate(XmlaResponse response, List<Row> rows) throws XmlaException;
166    
167        /**
168         * Adds a {@link Row} to a result, provided that it meets the necessary
169         * criteria. Returns whether the row was added.
170         *
171         * @param row Row
172         * @param rows List of result rows
173         */
174        protected final boolean addRow(Row row, List<Row> rows) throws XmlaException {
175            return rows.add(row);
176        }
177    
178        /**
179         * Emits a row for this rowset, reading fields from a
180         * {@link mondrian.xmla.Rowset.Row} object.
181         *
182         * @param row Row
183         * @param response XMLA response writer
184         */
185        protected void emit(Row row, XmlaResponse response) throws XmlaException {
186    
187            SaxWriter writer = response.getWriter();
188    
189            writer.startElement("row");
190            for (RowsetDefinition.Column column : rowsetDefinition.columnDefinitions) {
191                Object value = row.get(column.name);
192                if (value == null) {
193                    if (!column.nullable) {
194                        throw new XmlaException(
195                            CLIENT_FAULT_FC,
196                            HSB_BAD_NON_NULLABLE_COLUMN_CODE,
197                            HSB_BAD_NON_NULLABLE_COLUMN_FAULT_FS,
198                            Util.newInternal("Value required for column " +
199                                column.name +
200                                " of rowset " +
201                                rowsetDefinition.name()));
202                    }
203                } else if (value instanceof XmlElement[]) {
204                    XmlElement[] elements = (XmlElement[]) value;
205                    for (XmlElement element : elements) {
206                        emitXmlElement(writer, element);
207                    }
208                } else if (value instanceof Object[]) {
209                    Object[] values = (Object[]) value;
210                    for (Object value1 : values) {
211                        writer.startElement(column.name);
212                        writer.characters(value1.toString());
213                        writer.endElement();
214                    }
215                } else if (value instanceof List) {
216                    List values = (List) value;
217                    for (Object value1 : values) {
218                        if (value1 instanceof XmlElement) {
219                            XmlElement xmlElement = (XmlElement) value1;
220                            emitXmlElement(writer, xmlElement);
221                        } else {
222                            writer.startElement(column.name);
223                            writer.characters(value1.toString());
224                            writer.endElement();
225                        }
226                    }
227                } else {
228                    writer.startElement(column.name);
229                    writer.characters(value.toString());
230                    writer.endElement();
231                }
232            }
233            writer.endElement();
234        }
235    
236        private void emitXmlElement(SaxWriter writer, XmlElement element) {
237            if (element.attributes == null) {
238                writer.startElement(element.tag);
239            } else {
240                writer.startElement(element.tag, element.attributes);
241            }
242    
243            if (element.text == null) {
244                for (XmlElement aChildren : element.children) {
245                    emitXmlElement(writer, aChildren);
246                }
247            } else {
248                writer.characters(element.text);
249            }
250    
251            writer.endElement();
252        }
253    
254        /**
255         * Populates all of the values in an enumeration into a list of rows.
256         */
257        protected <E extends Enum<E>> void populate(
258            Class<E> clazz,
259            List<Row> rows)
260            throws XmlaException
261        {
262            final E[] enumsSortedByName = clazz.getEnumConstants().clone();
263            Arrays.sort(
264                enumsSortedByName,
265                new Comparator<E>() {
266                    public int compare(E o1, E o2) {
267                        return o1.name().compareTo(o2.name());
268                    }
269                });
270            for (E anEnum : enumsSortedByName) {
271                Row row = new Row();
272                for (RowsetDefinition.Column column : rowsetDefinition.columnDefinitions) {
273                    row.names.add(column.name);
274                    row.values.add(column.get(anEnum));
275                }
276                rows.add(row);
277            }
278        }
279    
280        /**
281         * Extensions to this abstract class implement a restriction test
282         * for each Rowset's discovery request. If there is no restriction
283         * then the passes method always returns true.
284         * Since whether the restriction is not specified (null), a single
285         * value (String) or an array of values (String[]) is known at
286         * the beginning of a Rowset's populate() method, creating these
287         * just once at the beginning is faster than having to determine
288         * the restriction status each time it is needed.
289         */
290        static abstract class RestrictionTest {
291            public abstract boolean passes(Object value);
292        }
293    
294        RestrictionTest getRestrictionTest(RowsetDefinition.Column column) {
295            final Object restriction = restrictions.get(column.name);
296    
297            if (restriction == null) {
298                return new RestrictionTest() {
299                    public boolean passes(Object value) {
300                        return true;
301                    }
302                };
303            } else if (restriction instanceof XmlaUtil.Wildcard) {
304                XmlaUtil.Wildcard wildcard = (XmlaUtil.Wildcard) restriction;
305                String regexp =
306                    Util.wildcardToRegexp(
307                        Collections.singletonList(wildcard.pattern));
308                final Matcher matcher = Pattern.compile(regexp).matcher("");
309                return new RestrictionTest() {
310                    public boolean passes(Object value) {
311                        return matcher.reset(String.valueOf(value)).matches();
312                    }
313                };
314            } else if (restriction instanceof List) {
315                final List<String> requiredValues = (List<String>) restriction;
316                return new RestrictionTest() {
317                    public boolean passes(Object value) {
318                        return requiredValues.contains(value);
319                    }
320                };
321            } else {
322                throw Util.newInternal(
323                    "unexpected restriction type: " + restriction.getClass());
324            }
325        }
326    
327        /**
328         * Returns the restriction if it is a String, or null otherwise. Does not
329         * attempt two determine if the restriction is an array of Strings
330         * if all members of the array have the same value (in which case
331         * one could return, again, simply a single String).
332         */
333        String getRestrictionValueAsString(RowsetDefinition.Column column) {
334            final Object restriction = restrictions.get(column.name);
335            if (restriction instanceof List) {
336                List<String> rval = (List<String>) restriction;
337                if (rval.size() == 1) {
338                    return rval.get(0);
339                }
340            }
341            return null;
342        }
343    
344        /**
345         * Returns a column's restriction as an <code>int</code> if it
346         * exists, -1 otherwise.
347         */
348        int getRestrictionValueAsInt(RowsetDefinition.Column column) {
349            final Object restriction = restrictions.get(column.name);
350            if (restriction instanceof List) {
351                List<String> rval = (List<String>) restriction;
352                if (rval.size() == 1) {
353                    try {
354                        return Integer.parseInt(rval.get(0));
355                    } catch (NumberFormatException ex) {
356                        LOGGER.info("Rowset.getRestrictionValue: "+
357                            "bad integer restriction \""+
358                            rval+
359                            "\"");
360                        return -1;
361                    }
362                }
363            }
364            return -1;
365        }
366    
367        /**
368         * Returns true if there is a restriction for the given column
369         * definition.
370         *
371         */
372        protected boolean isRestricted(RowsetDefinition.Column column) {
373            return (restrictions.get(column.name) != null);
374        }
375    
376        /**
377         * A set of name/value pairs, which can be output using
378         * {@link Rowset#addRow}. This uses less memory than simply
379         * using a HashMap and for very big data sets memory is
380         * a concern.
381         */
382        protected static class Row {
383            private final ArrayList<String> names;
384            private final ArrayList<Object> values;
385            Row() {
386                this.names = new ArrayList<String>();
387                this.values = new ArrayList<Object>();
388            }
389    
390            void set(String name, Object value) {
391                this.names.add(name);
392                this.values.add(value);
393            }
394    
395            void set(String name, boolean value) {
396                set(name, value ? "true" : "false");
397            }
398    
399            /**
400             * Retrieves the value of a field with a given name, or null if the
401             * field's value is not defined.
402             */
403            public Object get(String name) {
404                int i = this.names.indexOf(name);
405                return (i < 0) ? null : this.values.get(i);
406            }
407        }
408    
409        /**
410         * Holder for non-scalar column values of a {@link mondrian.xmla.Rowset.Row}.
411         */
412        protected static class XmlElement {
413            private final String tag;
414            private final String[] attributes;
415            private final String text;
416            private final XmlElement[] children;
417    
418            XmlElement(String tag, String[] attributes) {
419                this(tag, attributes, null, null);
420            }
421    
422            XmlElement(String tag, String[] attributes, String text) {
423                this(tag, attributes, text, null);
424            }
425    
426            XmlElement(String tag, String[] attributes, XmlElement[] children) {
427                this(tag, attributes, null, children);
428            }
429    
430            private XmlElement(String tag, String[] attributes, String text, XmlElement[] children) {
431                this.tag = tag;
432                this.attributes = attributes;
433                this.text = text;
434                this.children = children;
435            }
436        }
437    }
438    
439    // End Rowset.java