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