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