/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright 2009 Pentaho Corporation. All rights reserved.
*
*/
package org.pentaho.platform.plugin.action.pentahometadata;
import java.io.File;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.commons.connection.IPentahoConnection;
import org.pentaho.commons.connection.IPentahoMetaData;
import org.pentaho.commons.connection.IPentahoResultSet;
import org.pentaho.commons.connection.memory.MemoryResultSet;
import org.pentaho.di.core.database.DatabaseInterface;
import org.pentaho.di.core.database.DatabaseMeta;
import org.pentaho.metadata.model.IPhysicalModel;
import org.pentaho.metadata.model.InlineEtlPhysicalModel;
import org.pentaho.metadata.model.SqlPhysicalModel;
import org.pentaho.metadata.query.impl.ietl.InlineEtlQueryExecutor;
import org.pentaho.metadata.query.impl.sql.MappedQuery;
import org.pentaho.metadata.query.impl.sql.SqlGenerator;
import org.pentaho.metadata.query.model.Parameter;
import org.pentaho.metadata.query.model.Query;
import org.pentaho.metadata.query.model.util.QueryXmlHelper;
import org.pentaho.metadata.repository.IMetadataDomainRepository;
import org.pentaho.metadata.util.DatabaseMetaUtil;
import org.pentaho.metadata.util.ThinModelConverter;
import org.pentaho.platform.api.engine.IPentahoSession;
import org.pentaho.platform.engine.core.system.PentahoSystem;
import org.pentaho.platform.engine.services.connection.PentahoConnectionFactory;
import org.pentaho.platform.engine.services.runtime.TemplateUtil;
import org.pentaho.platform.plugin.action.messages.Messages;
import org.pentaho.platform.plugin.services.connections.sql.SQLConnection;
import org.pentaho.platform.plugin.services.connections.sql.SQLResultSet;
import org.pentaho.platform.util.logging.SimpleLogger;
import org.pentaho.platform.util.messages.LocaleHelper;
/**
* This is the BI Platform Pojo Component for Pentaho Metadata Queries. It currently supports
* executing the inline etl and sql physical models.
*
*
*
* TODO: We should eventually move the copy and pasted code that executes the SQL into a pojo SQL Component.
*
* @author Will Gorman
*
*/
public class MetadataQueryComponent {
public static final String DEFAULT_RELATIVE_UPLOAD_FILE_PATH = File.separatorChar + "system" + File.separatorChar + "metadata" + File.separatorChar + "csvfiles" + File.separatorChar; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
static final Log logger = LogFactory.getLog(MetadataQueryComponent.class);
String query;
Integer maxRows; //-1;
Integer timeout; // -1;
Boolean readOnly; // false;
boolean live = false;
boolean useForwardOnlyResultSet = false;
boolean logSql = false;
boolean forceDbDialect = false;
IPentahoSession session = null;
IPentahoResultSet resultSet = null;
String xmlHelperClass = "org.pentaho.metadata.query.model.util.QueryXmlHelper"; //$NON-NLS-1$
String sqlGeneratorClass = "org.pentaho.metadata.query.impl.sql.SqlGenerator"; //$NON-NLS-1$
Map<String,Object> inputs = null;
/*
* The list of inputs to this component, used when resolving parameter values.
*
* @param inputs map of inputs
*/
public void setInputs(Map<String,Object> inputs) {
this.inputs = inputs;
}
public void setLogSql(boolean logSql) {
this.logSql = logSql;
}
public void setQuery(String query) {
this.query = query;
}
public void setMaxRows(Integer maxRows) {
this.maxRows = maxRows;
}
public void setTimeout(Integer timeout) {
this.timeout = timeout;
}
public void setLive(boolean live) {
this.live = live;
}
/**
* This sets the read only property in the Pentaho SQLConnection API
*
* @param readOnly true if read only
*/
public void setReadOnly(Boolean readOnly) {
this.readOnly = readOnly;
}
public void setUseForwardOnlyResultSet(boolean useForwardOnlyResultSet) {
this.useForwardOnlyResultSet = useForwardOnlyResultSet;
}
public void setQueryModelXmlHelper(String xmlHelperClass) {
this.xmlHelperClass = xmlHelperClass;
}
public void setQueryModelSqlGenerator(String sqlGeneratorClass) {
this.sqlGeneratorClass = sqlGeneratorClass;
}
public void setForceDbDialect(boolean forceDbDialect) {
this.forceDbDialect = forceDbDialect;
}
@SuppressWarnings("unchecked")
private QueryXmlHelper createQueryXmlHelper() throws Exception {
Class clazz = Class.forName(xmlHelperClass);
return (QueryXmlHelper)clazz.getConstructor(new Class[]{}).newInstance(new Object[]{});
}
@SuppressWarnings("unchecked")
private SqlGenerator createSqlGenerator() throws Exception {
Class clazz = Class.forName(sqlGeneratorClass);
return (SqlGenerator)clazz.getConstructor(new Class[]{}).newInstance(new Object[]{});
}
public boolean execute() {
// get the xml parser
QueryXmlHelper helper = null;
try {
helper = createQueryXmlHelper();
} catch (Exception e) {
logger.error("error", e); //$NON-NLS-1$
return false;
}
// parse the metadata query
IMetadataDomainRepository repo = PentahoSystem.get(IMetadataDomainRepository.class, null);
// apply templates to the query
String templatedQuery = null;
if (inputs != null) {
Properties properties = new Properties();
for (String name : inputs.keySet()) {
properties.put(name, inputs.get(name).toString());
}
templatedQuery = TemplateUtil.applyTemplate(query, properties, null);
} else {
templatedQuery = query;
}
Query queryObject = null;
try {
queryObject = helper.fromXML(repo, templatedQuery);
} catch (Exception e) {
logger.error("error", e); //$NON-NLS-1$
return false;
}
if (queryObject == null) {
logger.error("error query object null"); //$NON-NLS-1$
return false;
}
// Read metadata for new timeout/max_rows and set in superclass
// Can still be overridden in the action sequence
if (timeout == null) {
Object timeoutProperty = queryObject.getLogicalModel().getProperty("timeout"); //$NON-NLS-1$
if (timeoutProperty != null && timeoutProperty instanceof Number) {
int timeoutVal = ((Number)timeoutProperty).intValue();
this.setTimeout(timeoutVal);
}
}
if (maxRows == null) {
Object maxRowsProperty = queryObject.getLogicalModel().getProperty("max_rows"); //$NON-NLS-1$
if (maxRowsProperty != null && maxRowsProperty instanceof Number) {
int maxRowsVal = ((Number)maxRowsProperty).intValue();
this.setMaxRows(maxRowsVal);
}
}
IPhysicalModel physicalModel = queryObject.getLogicalModel().getPhysicalModel();
// determine parameter values
Map<String, Object> parameters = null;
if (queryObject.getParameters() != null) {
for (Parameter param : queryObject.getParameters()) {
if (parameters == null) {
parameters = new HashMap<String, Object>();
}
Object value = null;
if (inputs != null) {
value = inputs.get(param.getName());
}
if (value != null) {
// convert object to correct type based on input here?
if (physicalModel instanceof InlineEtlPhysicalModel) {
Object paramVal = convertParameterValue(param, inputs);
parameters.put(param.getName(), paramVal);
} else {
parameters.put(param.getName(), value);
}
} else {
parameters.put(param.getName(), param.getDefaultValue());
}
}
}
if (physicalModel instanceof SqlPhysicalModel) {
return executeSqlPhysicalModel(queryObject, repo, parameters);
} else if (physicalModel instanceof InlineEtlPhysicalModel) {
return executeInlineEtlPhysicalModel(queryObject, repo, parameters);
} else {
logger.error("Physical model not supported " + physicalModel); //$NON-NLS-1$
return false;
}
}
protected boolean executeInlineEtlPhysicalModel(Query queryObject, IMetadataDomainRepository repo, Map<String, Object> parameters) {
InlineEtlQueryExecutor executor = new InlineEtlQueryExecutor();
String relativePath = PentahoSystem.getSystemSetting("file-upload-defaults/relative-path", String.valueOf(DEFAULT_RELATIVE_UPLOAD_FILE_PATH)); //$NON-NLS-1$
String csvFileLoc = PentahoSystem.getApplicationContext().getSolutionPath(relativePath);
try {
resultSet = executor.executeQuery(queryObject, csvFileLoc, parameters);
return true;
} catch (Exception e ) {
logger.error("error", e); //$NON-NLS-1$
return false;
}
}
protected SQLConnection getConnection(DatabaseMeta databaseMeta) {
// use the connection specified in the query
SQLConnection localConnection = null;
try {
if (databaseMeta.getAccessType() == DatabaseMeta.TYPE_ACCESS_JNDI) {
String jndiName = databaseMeta.getDatabaseName();
if (jndiName != null) {
SimpleLogger simpleLogger = new SimpleLogger(this);
localConnection = (SQLConnection)PentahoConnectionFactory.getConnection(
IPentahoConnection.SQL_DATASOURCE, jndiName, session, simpleLogger);
}
}
if (localConnection == null) {
String driver = databaseMeta.getDriverClass();
String userId = databaseMeta.getUsername();
String password = databaseMeta.getPassword();
String connectionInfo = databaseMeta.getURL();
if ((driver == null) && (connectionInfo == null)) {
// TODO raise an error
}
SimpleLogger simpleLogger = new SimpleLogger(this);
localConnection = (SQLConnection)PentahoConnectionFactory.getConnection(
IPentahoConnection.SQL_DATASOURCE, driver,
connectionInfo, userId, password, session, simpleLogger);
}
// This no longer is functional, it used to work with the old MQLRelationalDataComponent
// try the parent to allow the connection to be overridden
// localConnection = getConnection(localConnection);
return localConnection;
} catch (Exception e) {
logger.error(Messages.getInstance().getErrorString("MetadataQueryComponent.ERROR_0006_EXECUTE_FAILED"), e); //$NON-NLS-1$
}
return null;
}
protected DatabaseInterface getDatabaseInterface(final SQLConnection conn) {
String prod = null;
try {
prod = conn.getNativeConnection().getMetaData().getDatabaseProductName();
DatabaseInterface di = DatabaseMetaUtil.getDatabaseInterface(prod);
if (prod != null && di == null) {
logger.warn(Messages.getInstance().getString("MQLRelationalDataComponent.WARN_0001_NO_DIALECT_DETECTED", prod)); //$NON-NLS-1$
}
return di;
} catch (SQLException e) {
logger.warn(Messages.getInstance().getString("MQLRelationalDataComponent.WARN_0002_DIALECT_EXCEPTION", prod), e); //$NON-NLS-1$
}
return null;
}
protected DatabaseMeta getActiveDatabaseMeta(DatabaseMeta databaseMeta) {
if (forceDbDialect) {
return databaseMeta;
}
// retrieve a temporary connection to determine if a dialect change is necessary
// for generating the MQL Query.
SQLConnection tempConnection = getConnection(databaseMeta);
try {
// if the connection type is not of the current dialect, regenerate the query
DatabaseInterface di = getDatabaseInterface(tempConnection);
if ((di != null) && (databaseMeta.getDatabaseType() != di.getDatabaseType())) {
// we need to reinitialize our mqlQuery object and reset the query.
// note that using this di object wipes out connection info
DatabaseMeta meta = (DatabaseMeta)databaseMeta.clone();
DatabaseInterface di2 = (DatabaseInterface) di.clone();
di2.setAccessType(databaseMeta.getAccessType());
di2.setDatabaseName(databaseMeta.getDatabaseName());
di2.setAttributes(databaseMeta.getAttributes());
meta.setDatabaseInterface(di2);
return meta;
} else {
return databaseMeta;
}
} finally {
if (tempConnection != null) {
tempConnection.close();
}
}
}
protected boolean executeSqlPhysicalModel(Query queryObject, IMetadataDomainRepository repo, Map<String, Object> parameters) {
// need to get the correct DatabaseMeta
SqlPhysicalModel sqlModel = (SqlPhysicalModel)queryObject.getLogicalModel().getPhysicalModel();
DatabaseMeta databaseMeta = ThinModelConverter.convertToLegacy(sqlModel.getId(), sqlModel.getDatasource());
// this connection needs closed
boolean closeConnection = true;
DatabaseMeta activeDatabaseMeta = getActiveDatabaseMeta(databaseMeta);
SQLConnection sqlConnection = getConnection(activeDatabaseMeta);
String sql = null;
try {
if ((sqlConnection == null) || !sqlConnection.initialized()) {
logger.error(Messages.getInstance().getErrorString("SQLBaseComponent.ERROR_0007_NO_CONNECTION")); //$NON-NLS-1$
// TODO: throw an exception up the stack.
return false;
}
MappedQuery mappedQuery = null;
try {
SqlGenerator sqlGenerator = createSqlGenerator();
mappedQuery = sqlGenerator.generateSql(queryObject, LocaleHelper.getLocale().toString(), repo, activeDatabaseMeta, parameters, true);
} catch (Exception e) {
// TODO: throw an exception up the stack.
logger.error(Messages.getInstance().getErrorString("MetadataQueryComponent.ERROR_0001_ERROR_EXECUTING_QUERY", e.getLocalizedMessage())); //$NON-NLS-1$
logger.debug("error", e); //$NON-NLS-1$
return false;
}
if (timeout != null && timeout >= 0 ) {
sqlConnection.setQueryTimeout(timeout);
}
if (maxRows != null && maxRows >= 0) {
sqlConnection.setMaxRows(maxRows);
}
if (readOnly != null && readOnly.booleanValue()) {
sqlConnection.setReadOnly(true);
}
IPentahoResultSet localResultSet = null;
sql = mappedQuery.getQuery();
if (logger.isDebugEnabled()) {
logger.debug("SQL: " + sql); //$NON-NLS-1$
}
if (logSql) {
logger.info("SQL: " + sql); //$NON-NLS-1$
}
// populate prepared sql params
List<Object> sqlParams = null;
if (mappedQuery.getParamList() != null) {
sqlParams = new ArrayList<Object>();
for (String param : mappedQuery.getParamList()) {
sqlParams.add(parameters.get(param));
}
}
try {
if (!useForwardOnlyResultSet) {
if (sqlParams != null) {
localResultSet = sqlConnection.prepareAndExecuteQuery(sql, sqlParams);
} else {
localResultSet = sqlConnection.executeQuery(sql);
}
} else {
if (sqlParams != null) {
localResultSet = sqlConnection.prepareAndExecuteQuery(sql, sqlParams, SQLConnection.RESULTSET_FORWARDONLY, SQLConnection.CONCUR_READONLY);
} else {
localResultSet = sqlConnection.executeQuery(sql, SQLConnection.RESULTSET_FORWARDONLY, SQLConnection.CONCUR_READONLY);
}
}
IPentahoMetaData metadata = mappedQuery.generateMetadata(localResultSet.getMetaData());
if (live) {
((SQLResultSet) localResultSet).setMetaData(metadata);
// live, don't close the connection
closeConnection = false;
} else {
// read the results and cache them
try {
MemoryResultSet cachedResultSet = new MemoryResultSet(metadata);
Object[] rowObjects = localResultSet.next();
while (rowObjects != null) {
cachedResultSet.addRow(rowObjects);
rowObjects = localResultSet.next();
}
localResultSet = cachedResultSet;
} finally {
sqlConnection.close();
sqlConnection = null;
}
}
} catch (Exception e) {
logger.error(Messages.getInstance().getErrorString("MetadataQueryComponent.ERROR_0001_ERROR_EXECUTING_QUERY", e.getLocalizedMessage(), sql)); //$NON-NLS-1$
logger.debug("error", e); //$NON-NLS-1$
return false;
}
if (localResultSet != null) {
resultSet = localResultSet;
return true;
} else {
logger.error(Messages.getInstance().getErrorString("SQLBaseComponent.ERROR_0006_EXECUTE_FAILED")); //$NON-NLS-1$
return false;
}
} finally {
if (closeConnection && sqlConnection != null) {
sqlConnection.close();
}
}
}
public boolean validate() {
if (query == null) {
logger.error("no query specified"); //$NON-NLS-1$
return false;
}
return true;
}
public IPentahoResultSet getResultSet() {
return resultSet;
}
/**
* Convert a parameter to it's expected query input type.
*
* @param param the expected query parameter
* @param parameters the list of inputs
*
* @return the converted value
*/
private Object convertParameterValue(Parameter param, Map<String, Object> parameters) {
Object paramObj = null;
if (parameters != null) {
paramObj = parameters.get(param.getName());
if (paramObj == null) {
return null;
}
// convert the input parameter to the right parameter type
switch(param.getType()) {
case NUMERIC:
if (!(paramObj instanceof Number)) {
try {
paramObj = Double.parseDouble(paramObj.toString());
} catch (NumberFormatException e) {
// ignore failed conversion
}
}
break;
case BOOLEAN:
if (!(paramObj instanceof Boolean)) {
paramObj = Boolean.parseBoolean(paramObj.toString());
}
break;
case STRING:
if (!(paramObj instanceof String)) {
paramObj = paramObj.toString();
}
break;
}
}
return paramObj;
}
}