/*! ****************************************************************************** * * Pentaho Data Integration * * Copyright (C) 2002-2013 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.luciddbstreamingloader; import java.io.IOException; import java.io.ObjectOutputStream; import java.net.Socket; import java.net.SocketException; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.SQLWarning; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import org.pentaho.di.core.database.Database; import org.pentaho.di.core.exception.KettleDatabaseException; import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.row.ValueMetaInterface; import org.pentaho.di.core.xml.XMLHandler; import org.pentaho.di.i18n.BaseMessages; import org.pentaho.di.trans.Trans; import org.pentaho.di.trans.TransMeta; import org.pentaho.di.trans.step.BaseStep; import org.pentaho.di.trans.step.StepDataInterface; import org.pentaho.di.trans.step.StepInterface; import org.pentaho.di.trans.step.StepMeta; import org.pentaho.di.trans.step.StepMetaInterface; /** * Perform main transformation.<br> * The logic is below:<br> * 1. Execute remote_rows udx thru jdbc (initialize database and generate sql_statment on remote_rows).<br> * 2. When incoming row is the 1st row, cache the index of every field and create header format object.<br> * 3. Send out data one row/per time thru socket based on TCP/IP.<br> * 4. Once all rows are sent out, close socket connection and return.<br> * * @author Ray Zhang * @since Jan-05-2010 */ public class LucidDBStreamingLoader extends BaseStep implements StepInterface { private static Class<?> PKG = LucidDBStreamingLoaderMeta.class; private LucidDBStreamingLoaderMeta meta; private LucidDBStreamingLoaderData data; public LucidDBStreamingLoader( StepMeta stepMeta, StepDataInterface stepDataInterface, int copyNr, TransMeta transMeta, Trans trans ) { super( stepMeta, stepDataInterface, copyNr, transMeta, trans ); } // When user want to stop it, it will execute logic here. public void stopRunning( StepMetaInterface smi, StepDataInterface sdi ) throws KettleException { meta = (LucidDBStreamingLoaderMeta) smi; data = (LucidDBStreamingLoaderData) sdi; if ( data.objOut != null ) { try { data.objOut.flush(); data.objOut.close(); } catch ( IOException e ) { // Already closed or other issue... log silent error logError( "Error while closing Remote LucidDB connection - likely already closed by earlier exception" ); } finally { data.objOut = null; } if ( data.client != null ) { try { data.client.close(); } catch ( IOException e ) { // Already closed or other issue... log silent error logError( "Error while closing Remote client connection - likely already closed by earlier exception" ); } finally { data.client = null; } } } try { if ( data.sqlRunner != null ) { data.sqlRunner.join(); } } catch ( InterruptedException e ) { // Issue converging thread logError( "Error while trying to rejoin/end SQLRunner thread from LucidDB" ); } } public boolean processRow( StepMetaInterface smi, StepDataInterface sdi ) throws KettleException { meta = (LucidDBStreamingLoaderMeta) smi; data = (LucidDBStreamingLoaderData) sdi; try { Object[] r = getRow(); // Get row from input rowset & set row // busy! if ( r == null ) { // no more input to be expected... if ( data.objOut != null ) { data.objOut.flush(); data.objOut.close(); if ( data.client != null ) { data.client.close(); } } return false; } if ( first ) { first = false; // For anything other than Custom operations, check to see if the table exists if ( meta.getOperation() != LucidDBStreamingLoaderMeta.OPERATION_CUSTOM ) { if ( log.isDebug() ) { logDebug( "Connected to LucidDB" ); } String qualifiedTableName = meta.getDatabaseMeta().getQuotedSchemaTableCombination( environmentSubstitute( meta.getSchemaName() ), environmentSubstitute( meta.getTableName() ) ); if ( !data.db.checkTableExists( qualifiedTableName ) ) { throw new KettleException( "Error: Table " + qualifiedTableName + " doesn't existing in LucidDB" ); } } String sql = meta.getDMLStatement( this, getInputRowMeta() ); PreparedStatement ps = data.db.prepareSQL( sql ); if ( log.isDebug() ) { logDebug( "Executing sql statements..." ); } data.sqlRunner = new SqlRunner( data, ps ); data.sqlRunner.start(); if ( log.isDebug() ) { logDebug( "Remote rows is up now..." ); } if ( log.isDebug() ) { logDebug( "Sleeping for 1 second" ); } Thread.sleep( 1000 ); if ( log.isDebug() ) { logDebug( "Initialize local socket connection..." ); } if ( log.isDebug() ) { logDebug( "Parameters for socket: Host: " + meta.getHost() + " Port: " + meta.getPort() ); } int try_cnt = 0; // Add a check whether remote rows is up. // If it is not up, it will sleep 5 second and then try to // connect. // Totally, we will try 5 times. while ( true ) { try { data.client = new Socket( meta.getHost(), Integer.valueOf( meta.getPort() ) ); data.objOut = new ObjectOutputStream( data.client.getOutputStream() ); if ( log.isDebug() ) { logDebug( "Local socket connection is ready" ); } break; } catch ( SocketException se ) { if ( try_cnt < 5 ) { logBasic( "Local socket connection is not ready, so try to connect in 5 second" ); Thread.sleep( 5000 ); data.client = null; try_cnt++; } else { throw new KettleException( "Fatal Error: Remote_rows UDX can't be connected! Please check..." ); } } catch ( Exception ex ) { throw ex; } } // Get combined set of incoming fields, reducing duplicates ArrayList<String> combined = new ArrayList<String>(); // Add all keys for ( int i = 0; i < meta.getFieldStreamForKeys().length; i++ ) { combined.add( meta.getFieldStreamForKeys()[i] ); } // Add all fields that are NOT already in keys for ( int i = 0; i < meta.getFieldStreamForFields().length; i++ ) { if ( !meta.isInKeys( meta.getFieldStreamForFields()[i] ) ) { combined.add( meta.getFieldStreamForFields()[i] ); } } // Get length and create two arrays (data.keynrs and data.format) data.keynrs = new int[combined.size()]; data.format = new String[combined.size()]; // Iterate over combined set for ( int i = 0; i < combined.size(); i++ ) { data.keynrs[i] = getInputRowMeta().indexOfValue( combined.get( i ) ); ValueMetaInterface v = getInputRowMeta().getValueMeta( data.keynrs[i] ); data.format[i] = meta.getDatabaseMeta().getFieldDefinition( v, null, null, false ); // data.format[i] = meta.getSQLDataType(getInputRowMeta().getValueMeta(data.keynrs[i])); } if ( isDetailed() ) { logDetailed( Arrays.toString( data.format ) ); } // Create head format object. List<Object> header = new ArrayList<Object>(); header.add( "1" ); // version List<String> format = new ArrayList<String>(); for ( int i = 0; i < data.format.length; i++ ) { format.add( data.format[i] ); } header.add( format ); data.objOut.writeObject( header ); } // End if ( first ) // If there's been errors in the DML thread (exception with headers, etc) if ( data.sqlRunner.ex != null ) { throw new KettleException( data.sqlRunner.ex ); } List<Object> entity = new ArrayList<Object>(); for ( int i = 0; i < data.keynrs.length; i++ ) { int index = data.keynrs[i]; ValueMetaInterface valueMeta = getInputRowMeta().getValueMeta( index ); Object valueData = r[index]; // Support NULL values. if ( r[i] == null ) { entity.add( null ); } else { switch ( valueMeta.getType() ) { case ValueMetaInterface.TYPE_NUMBER: if ( log.isRowLevel() ) { logRowlevel( valueMeta.getNumber( valueData ) + ":" + valueMeta.getLength() + ":" + valueMeta.getTypeDesc() ); } entity.add( valueMeta.getNumber( valueData ) ); break; case ValueMetaInterface.TYPE_STRING: if ( log.isRowLevel() ) { logRowlevel( valueMeta.getString( valueData ) + ":" + valueMeta.getLength() + ":" + valueMeta.getTypeDesc() ); } entity.add( valueMeta.getString( valueData ) ); break; case ValueMetaInterface.TYPE_DATE: Date date = valueMeta.getDate( valueData ); if ( log.isRowLevel() ) { logRowlevel( XMLHandler.date2string( date ) + ":" + valueMeta.getLength() ); } java.sql.Date sqlDate = new java.sql.Date( date.getTime() ); entity.add( sqlDate ); break; case ValueMetaInterface.TYPE_BOOLEAN: if ( log.isRowLevel() ) { logRowlevel( Boolean.toString( valueMeta.getBoolean( valueData ) ) + ":" + valueMeta.getLength() ); } entity.add( valueMeta.getBoolean( valueData ) ); break; case ValueMetaInterface.TYPE_INTEGER: if ( log.isRowLevel() ) { logRowlevel( valueMeta.getInteger( valueData ) + ":" + valueMeta.getLength() + ":" + valueMeta.getTypeDesc() ); } entity.add( valueMeta.getInteger( valueData ) ); break; case ValueMetaInterface.TYPE_BIGNUMBER: if ( log.isRowLevel() ) { logRowlevel( valueMeta.getBigNumber( valueData ) + ":" + valueMeta.getLength() + ":" + valueMeta.getTypeDesc() ); } entity.add( valueMeta.getBigNumber( valueData ) ); break; case ValueMetaInterface.TYPE_BINARY: if ( log.isRowLevel() ) { logRowlevel( valueMeta.getBinary( valueData ) + ":" + valueMeta.getLength() + ":" + valueMeta.getTypeDesc() ); } entity.add( valueMeta.getBinary( valueData ) ); break; default: // Unknown datatype - it's worth a try?!? ;) entity.add( r[i] ); break; } } } data.objOut.writeObject( entity ); incrementLinesOutput(); // NG: Are these both necessary? data.objOut.reset(); data.objOut.flush(); return true; } catch ( Exception e ) { logError( BaseMessages.getString( PKG, "LucidDBStreamingLoader.Log.ErrorInStep" ), e ); setErrors( 1 ); stopAll(); setOutputDone(); // signal end to receiver(s) return false; } } public boolean init( StepMetaInterface smi, StepDataInterface sdi ) { meta = (LucidDBStreamingLoaderMeta) smi; data = (LucidDBStreamingLoaderData) sdi; // implementation for DDB28 // System.out.println("ZZZZZZZZZZZ" + getTransMeta().getName() + " // "+getStepname() + " " + getTrans().getBatchId() + "" + // System.getProperty("user.name")); if ( super.init( smi, sdi ) ) { try { // 1. Initialize databases connection. if ( log.isDebug() ) { logDebug( "Connecting to LucidDB..." ); } if ( meta.getDatabaseMeta() == null ) { logError( BaseMessages.getString( PKG, "LucidDBStreamingLoaderDialog.Init.ConnectionMissing", getStepname() ) ); return false; } data.db = new Database( this, meta.getDatabaseMeta() ); data.db.shareVariablesWith( this ); // Connect to the database if ( getTransMeta().isUsingUniqueConnections() ) { synchronized ( getTrans() ) { data.db.connect( getTrans().getTransactionId(), getPartitionID() ); } } else { data.db.connect( getPartitionID() ); } data.db.setAutoCommit( true ); } catch ( NumberFormatException e ) { // TODO Auto-generated catch block e.printStackTrace(); logError( e.getMessage() ); return false; } catch ( KettleDatabaseException e ) { // TODO Auto-generated catch block e.printStackTrace(); logError( e.getMessage() ); return false; } return true; } return false; } public void dispose( StepMetaInterface smi, StepDataInterface sdi ) { meta = (LucidDBStreamingLoaderMeta) smi; data = (LucidDBStreamingLoaderData) sdi; // Close the output streams if still needed. // try { // Stop the SQL execution thread if ( data.sqlRunner != null ) { data.sqlRunner.join(); data.sqlRunner = null; } // And finally, release the database connection if ( data.db != null ) { data.db.disconnect(); data.db = null; } } catch ( Exception e ) { setErrors( 1L ); logError( "Unexpected error encountered while closing the client connection", e ); } super.dispose( smi, sdi ); } static class SqlRunner extends Thread { private LucidDBStreamingLoaderData data; private PreparedStatement ps; private SQLException ex; List<String> warnings; SqlRunner( LucidDBStreamingLoaderData data, PreparedStatement ps ) { this.data = data; this.ps = ps; warnings = new ArrayList<String>(); ex = null; } public void run() { try { // TODO cross-check result against actual // number // of rows sent. ps.executeUpdate(); // Pump out any warnings and save them. SQLWarning warning = ps.getWarnings(); while ( warning != null ) { warnings.add( warning.getMessage() ); warning = warning.getNextWarning(); } } catch ( SQLException ex ) { this.ex = ex; } finally { try { data.db.closePreparedStatement( ps ); } catch ( KettleException ke ) { // not much we can do with this } finally { ps = null; } } } void checkExcn() throws SQLException { // This is called from the main thread context to rethrow any // saved // excn. if ( ex != null ) { throw ex; } } } }