/*! ******************************************************************************
*
* Pentaho Data Integration
*
* Copyright (C) 2002-2017 by Pentaho : http://www.pentaho.com
*
*******************************************************************************
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/
package org.pentaho.di.trans.steps.mondrianinput;
/*
// $Id: $
// This software is subject to the terms of the Common Public License
// Agreement, available at the following URL:
// http://www.opensource.org/licenses/cpl.html.
// Copyright (C) 2007-2007 Julian Hyde
// All Rights Reserved.
// You must accept the terms of that agreement to use this software.
*/
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import javax.sql.DataSource;
import org.pentaho.di.core.DBCache;
import org.pentaho.di.core.DBCacheEntry;
import org.pentaho.di.core.database.DatabaseMeta;
import org.pentaho.di.core.database.util.DatabaseUtil;
import org.pentaho.di.core.exception.KettleDatabaseException;
import org.pentaho.di.core.logging.LogChannelInterface;
import org.pentaho.di.core.row.RowMeta;
import org.pentaho.di.core.row.RowMetaInterface;
import org.pentaho.di.core.row.ValueMetaInterface;
import org.pentaho.di.core.row.value.ValueMetaBigNumber;
import org.pentaho.di.core.row.value.ValueMetaBoolean;
import org.pentaho.di.core.row.value.ValueMetaDate;
import org.pentaho.di.core.row.value.ValueMetaFactory;
import org.pentaho.di.core.row.value.ValueMetaInteger;
import org.pentaho.di.core.row.value.ValueMetaNumber;
import org.pentaho.di.core.row.value.ValueMetaString;
import org.pentaho.di.core.util.Utils;
import org.pentaho.di.core.variables.VariableSpace;
import org.pentaho.di.i18n.BaseMessages;
import org.pentaho.di.trans.step.StepInterface;
import mondrian.olap.Axis;
import mondrian.olap.Cell;
import mondrian.olap.Connection;
import mondrian.olap.DriverManager;
import mondrian.olap.Hierarchy;
import mondrian.olap.Member;
import mondrian.olap.Position;
import mondrian.olap.Query;
import mondrian.olap.Result;
/**
* <code>Mondrian Helper class</code> ...
*
* @author jhyde
* @since Mar 12, 2007
*/
public class MondrianHelper {
private DatabaseMeta databaseMeta;
private String catalog;
private String queryString;
private RowMetaInterface outputRowMeta;
private Result result;
private Query query;
private VariableSpace space;
private String role;
private List<List<Object>> rows;
private List<String> headings;
private LogChannelInterface log;
private Connection connection;
private static Class<?> PKG = MondrianInputMeta.class; // for i18n purposes, needed by Translator2!!
public MondrianHelper( DatabaseMeta databaseMeta, String catalog, String queryString, VariableSpace space ) {
this.databaseMeta = databaseMeta;
this.catalog = catalog;
this.queryString = queryString;
this.space = space;
// The following may appear to be a hack, but I don't want to change the constructor for backward compatibility sake.
if ( space instanceof StepInterface ) {
log = ( (StepInterface) space ).getLogChannel();
}
}
@SuppressWarnings( "deprecation" )
public void openQuery() throws KettleDatabaseException {
connection = null;
String realRole = space.environmentSubstitute( role );
if ( databaseMeta.getAccessType() == DatabaseMeta.TYPE_ACCESS_JNDI ) {
DataSource dataSource = ( new DatabaseUtil() ).getNamedDataSource( databaseMeta.getDatabaseName() );
mondrian.olap.Util.PropertyList propList = new mondrian.olap.Util.PropertyList();
propList.put( "Provider", "mondrian" );
propList.put( "Catalog", space.environmentSubstitute( catalog ) );
if ( !Utils.isEmpty( realRole ) ) {
propList.put( "Role", realRole );
}
connection = DriverManager.getConnection( propList, null, dataSource );
} else {
String connectString =
"Provider=mondrian;"
+ "Jdbc='" + space.environmentSubstitute( databaseMeta.getURL() ) + "';" + "Catalog='"
+ space.environmentSubstitute( catalog ) + "';" + "JdbcDrivers="
+ space.environmentSubstitute( databaseMeta.getDriverClass() ) + ";";
if ( !Utils.isEmpty( databaseMeta.getUsername() ) ) {
connectString += "JdbcUser=" + space.environmentSubstitute( databaseMeta.getUsername() ) + ";";
}
String password = databaseMeta.getPassword();
if ( !Utils.isEmpty( password ) ) {
String realPassword = Utils.resolvePassword( space, password );
connectString += "JdbcPassword=" + space.environmentSubstitute( realPassword ) + ";";
}
if ( !Utils.isEmpty( realRole ) ) {
connectString += "Role=" + realRole + ";";
}
connection = DriverManager.getConnection( connectString, null );
}
query = connection.parseQuery( queryString );
result = connection.execute( query );
}
public void close() {
if ( result != null ) {
try {
result.close();
} catch ( Exception ignored ) {
// Purposely ignored, cannot do anything else here but log it.
if ( ( log != null ) && ( log.isDebug() ) ) {
log.logDebug( ignored.getMessage() );
}
}
result = null;
}
if ( query != null ) {
try {
query.close();
} catch ( Exception ignored ) {
// Purposely ignored, cannot do anything else here but log it.
if ( ( log != null ) && ( log.isDebug() ) ) {
log.logDebug( ignored.getMessage() );
}
}
query = null;
}
if ( connection != null ) {
try {
connection.close();
} catch ( Exception ignored ) {
// Purposely ignored, cannot do anything else here but log it.
if ( ( log != null ) && ( log.isDebug() ) ) {
log.logDebug( ignored.getMessage() );
}
}
connection = null;
}
}
/**
* Outputs one row per tuple on the rows axis.
*
* @throws KettleDatabaseException
* in case some or other error occurs
*/
public void createRectangularOutput() throws KettleDatabaseException {
final Axis[] axes = result.getAxes();
if ( axes.length != 2 ) {
throw new KettleDatabaseException( BaseMessages.getString( PKG, "MondrianInputErrorOnlyTabular" ) );
}
headings = new ArrayList<>();
rows = new ArrayList<>();
final Axis rowsAxis = axes[1];
final Axis columnsAxis = axes[0];
int rowOrdinal = -1;
int[] coords = { 0, 0 };
for ( Position rowPos : rowsAxis.getPositions() ) {
++rowOrdinal;
coords[1] = rowOrdinal;
if ( rowOrdinal == 0 ) {
// Generate headings on the first row. Note that if there are
// zero rows, we don't have enough metadata to generate
// headings.
// First headings are for the members on the rows axis.
for ( Member rowMember : rowPos ) {
headings.add( rowMember.getHierarchy().getUniqueName() );
}
// Rest of the headings are for the members on the columns axis.
// If there are more than one member at each postition,
// concatenate the unique names.
for ( Position columnPos : columnsAxis.getPositions() ) {
String heading = "";
for ( Member columnMember : columnPos ) {
if ( !heading.equals( "" ) ) {
heading += ", ";
}
heading += columnMember.getUniqueName();
}
headings.add( heading );
}
}
List<Object> rowValues = new ArrayList<>();
// The first row values describe the members on the rows axis.
for ( Member rowMember : rowPos ) {
rowValues.add( rowMember.getUniqueName() );
}
// Rest of the row values are the raw cell values.
// NOTE: Could also/instead output formatted cell values here.
// NOTE: Could also output all properties of each cell.
for ( int columnOrdinal = 0; columnOrdinal < columnsAxis.getPositions().size(); ++columnOrdinal ) {
coords[0] = columnOrdinal;
final Cell cell = result.getCell( coords );
rowValues.add( cell.getValue() );
}
rows.add( rowValues );
}
outputRowMeta = new RowMeta();
// Scan the rows for the data types. Important case: if we get null on a
// column, keep scanning until we find one line that has an actual value
if ( rows.size() > 0 ) {
int columnCount = rows.get( 0 ).size();
HashMap<Integer, ValueMetaInterface> valueMetaHash = new HashMap<>();
for ( int i = 0; i < rows.size(); i++ ) {
List<Object> rowValues = rows.get( i );
for ( int c = 0; c < rowValues.size(); c++ ) {
if ( valueMetaHash.containsKey( new Integer( c ) ) ) {
continue; // we have this value already
}
Object valueData = rowValues.get( c );
if ( valueData == null ) {
continue; // skip this row and look for the metadata in a new one
}
String valueName = headings.get( c );
ValueMetaInterface valueMeta;
if ( valueData instanceof String ) {
valueMeta = new ValueMetaString( valueName );
} else if ( valueData instanceof Date ) {
valueMeta = new ValueMetaDate( valueName );
} else if ( valueData instanceof Boolean ) {
valueMeta = new ValueMetaBoolean( valueName );
} else if ( valueData instanceof Integer ) {
valueMeta = new ValueMetaInteger( valueName );
valueData = Long.valueOf( ( (Integer) valueData ).longValue() );
} else if ( valueData instanceof Short ) {
valueMeta = new ValueMetaInteger( valueName );
valueData = Long.valueOf( ( (Short) valueData ).longValue() );
} else if ( valueData instanceof Byte ) {
valueMeta = new ValueMetaInteger( valueName );
valueData = Long.valueOf( ( (Byte) valueData ).longValue() );
} else if ( valueData instanceof Long ) {
valueMeta = new ValueMetaInteger( valueName );
} else if ( valueData instanceof Double ) {
valueMeta = new ValueMetaNumber( valueName );
} else if ( valueData instanceof Float ) {
valueMeta = new ValueMetaNumber( valueName );
valueData = Double.valueOf( ( (Float) valueData ).doubleValue() );
} else if ( valueData instanceof BigDecimal ) {
valueMeta = new ValueMetaBigNumber( valueName );
} else {
throw new KettleDatabaseException( BaseMessages.getString( PKG, "MondrianInputErrorUnhandledType", valueData.getClass().toString() ) );
}
valueMetaHash.put( c, valueMeta );
}
if ( valueMetaHash.size() == columnCount ) {
break; // we're done
}
}
// Build the list of valueMetas
List<ValueMetaInterface> valueMetaList = new ArrayList<>();
for ( int c = 0; c < columnCount; c++ ) {
if ( valueMetaHash.containsKey( new Integer( c ) ) ) {
valueMetaList.add( valueMetaHash.get( new Integer( c ) ) );
} else {
// If the entire column is null, assume the missing data as String.
// Irrelevant, anyway
ValueMetaInterface valueMeta = new ValueMetaString( headings.get( c ) );
valueMetaList.add( valueMeta );
}
}
outputRowMeta.setValueMetaList( valueMetaList );
}
// Now that we painstakingly found the meta data that comes out of the
// Mondrian database, cache it please...
//
DBCacheEntry cacheEntry = new DBCacheEntry( databaseMeta.getName(), queryString );
DBCache.getInstance().put( cacheEntry, outputRowMeta );
}
/**
* Retrieve the rows from the opened query. Also create a description of the flattened output of the query. This call
* populated rowMetaInterface and rows The query needs to be opened beforehand.
*
* @throws KettleDatabaseException
* in case something goes wrong
*
* TODO: this is not quite working for our purposes.
*/
public void createFlattenedOutput() throws KettleDatabaseException {
final Axis[] axes = result.getAxes();
rows = new ArrayList<>();
headings = new ArrayList<>();
// Compute headings. Each heading is a hierarchy name. If there are say
// 2 members on the columns, and 3 members on the rows axis, then there
// will be 5 headings.
//
for ( Axis axis : axes ) {
final List<Position> positions = axis.getPositions();
if ( positions.isEmpty() ) {
// Result set is empty. There is no data to print, and we cannot
// even deduce column headings.
return;
}
for ( Member member : positions.get( 0 ) ) {
Hierarchy hierarchy = member.getHierarchy();
headings.add( hierarchy.getUniqueName() );
}
}
int[] coords = new int[axes.length];
outputFlattenedRecurse( result, rows, new ArrayList<>(), coords, 0 );
outputRowMeta = new RowMeta();
// Just scan the first row to see what data types we received...
//
for ( int i = 0; i < rows.size() && i < 1; i++ ) {
List<Object> rowValues = rows.get( i );
for ( int c = 0; c < rowValues.size(); c++ ) {
Object valueData = rowValues.get( c );
int valueType;
if ( valueData instanceof String ) {
valueType = ValueMetaInterface.TYPE_STRING;
} else if ( valueData instanceof Date ) {
valueType = ValueMetaInterface.TYPE_DATE;
} else if ( valueData instanceof Boolean ) {
valueType = ValueMetaInterface.TYPE_BOOLEAN;
} else if ( valueData instanceof Integer ) {
valueType = ValueMetaInterface.TYPE_INTEGER;
valueData = Long.valueOf( ( (Integer) valueData ).longValue() );
} else if ( valueData instanceof Short ) {
valueType = ValueMetaInterface.TYPE_INTEGER;
valueData = Long.valueOf( ( (Short) valueData ).longValue() );
} else if ( valueData instanceof Byte ) {
valueType = ValueMetaInterface.TYPE_INTEGER;
valueData = Long.valueOf( ( (Byte) valueData ).longValue() );
} else if ( valueData instanceof Long ) {
valueType = ValueMetaInterface.TYPE_INTEGER;
} else if ( valueData instanceof Double ) {
valueType = ValueMetaInterface.TYPE_NUMBER;
} else if ( valueData instanceof Float ) {
valueType = ValueMetaInterface.TYPE_NUMBER;
valueData = Double.valueOf( ( (Float) valueData ).doubleValue() );
} else if ( valueData instanceof BigDecimal ) {
valueType = ValueMetaInterface.TYPE_BIGNUMBER;
} else {
throw new KettleDatabaseException( BaseMessages.getString( PKG, "MondrianInputErrorUnhandledType", valueData.getClass().toString() ) );
}
try {
ValueMetaInterface valueMeta = ValueMetaFactory.createValueMeta( headings.get( c ), valueType );
outputRowMeta.addValueMeta( valueMeta );
rowValues.set( i, valueData );
} catch ( Exception e ) {
throw new KettleDatabaseException( e );
}
}
}
// Now that we painstakingly found the metadata that comes out of the Mondrian database, cache it please...
//
DBCacheEntry cacheEntry = new DBCacheEntry( databaseMeta.getName(), queryString );
DBCache.getInstance().put( cacheEntry, outputRowMeta );
}
public RowMetaInterface getCachedRowMeta() {
DBCacheEntry cacheEntry = new DBCacheEntry( databaseMeta.getName(), queryString );
return DBCache.getInstance().get( cacheEntry );
}
private static void outputFlattenedRecurse( Result result, List<List<Object>> rows, List<Object> rowValues,
int[] coords, int axisOrdinal ) {
final Axis[] axes = result.getAxes();
if ( axisOrdinal == axes.length ) {
final Cell cell = result.getCell( coords );
// Output the raw (unformatted) value of the cell.
// NOTE: We could output other properties of the cell here, such as its
// formatted value, too.
rowValues.add( cell.getValue() );
// Add a copy of the completed row to the list of rows.
rows.add( new ArrayList<>( rowValues ) );
} else {
final Axis axis = axes[axisOrdinal];
int k = -1;
int saveLength = rowValues.size();
for ( Position position : axis.getPositions() ) {
coords[axisOrdinal] = ++k;
for ( Member member : position ) {
rowValues.add( member.getUniqueName() );
}
outputFlattenedRecurse( result, rows, rowValues, coords, axisOrdinal + 1 );
while ( rowValues.size() > saveLength ) {
rowValues.remove( rowValues.size() - 1 );
}
}
}
}
/**
* @return the databaseMeta
*/
public DatabaseMeta getDatabaseMeta() {
return databaseMeta;
}
/**
* @param databaseMeta
* the databaseMeta to set
*/
public void setDatabaseMeta( DatabaseMeta databaseMeta ) {
this.databaseMeta = databaseMeta;
}
/**
* @return the catalog
*/
public String getCatalog() {
return catalog;
}
/**
* @param catalog
* the catalog to set
*/
public void setCatalog( String catalog ) {
this.catalog = catalog;
}
/**
* @return the queryString
*/
public String getQueryString() {
return queryString;
}
/**
* @param queryString
* the queryString to set
*/
public void setQueryString( String queryString ) {
this.queryString = queryString;
}
/**
* @return the outputRowMeta
*/
public RowMetaInterface getOutputRowMeta() {
return outputRowMeta;
}
/**
* @return the result
*/
public Result getResult() {
return result;
}
/**
* @return the query
*/
public Query getQuery() {
return query;
}
/**
* @return the rows
*/
public List<List<Object>> getRows() {
return rows;
}
/**
* @return the headings
*/
public List<String> getHeadings() {
return headings;
}
public String getRole() {
return role;
}
public void setRole( String role ) {
this.role = role;
}
}