package OpenRate.adapter.jdbc; import java.io.ByteArrayInputStream; import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; import java.util.Collection; import java.util.Iterator; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import org.apache.commons.lang.StringUtils; import OpenRate.CommonConfig; import OpenRate.OpenRate; import OpenRate.adapter.AbstractTransactionalOutputAdapter; import OpenRate.configurationmanager.ClientManager; import OpenRate.configurationmanager.IEventInterface; import OpenRate.db.DBUtil; import OpenRate.exception.InitializationException; import OpenRate.exception.ProcessingException; import OpenRate.logging.LogUtil; import OpenRate.record.DBRecord; import OpenRate.record.HeaderRecord; import OpenRate.record.IRecord; import OpenRate.utils.PropertyUtils; import org.postgresql.PGConnection; import org.postgresql.copy.CopyManager; /** * Output Adapter module that uses COPY command to populate PostgreSQL database. * * @author ddijak * */ public abstract class PgSQLCopyOutputAdapter extends AbstractTransactionalOutputAdapter implements IEventInterface { /** * Data holder */ protected ConcurrentHashMap<String, CopyOnWriteArrayList<String>> dataHolder; /** * The copy statement */ protected String CopyStatement; /** * Partition identification string */ protected String partitionIdent; /** * This is the name of the data source */ protected String dataSourceName; // default partition name private static final String DEFAULT_PARTITON_NAME = "Default"; // this is the connection from the connection pool that we are using private static final String DATASOURCE_KEY = "DataSource"; // The SQL statements from the properties that are used to process records private static final String COPY_STMT_KEY = "CopyStatement"; // Partition identification used to identify what needs to be replaced in copy statement private static final String PARTITION_IDENT_KEY = "PartitionIdent"; // List of Services that this Client supports private final static String SERVICE_DATASOURCE_KEY = "DataSource"; private final static String SERVICE_COPY_STMT_KEY = "CopyStatement"; private final static String SERVICE_PARTITION_IDENT_KEY = "PartitionIdent"; private final static String SERVICE_STATUS_KEY = "PrintStatus"; /** * This is our connection object */ protected Connection JDBCcon; /** * This is our CopyManager */ protected CopyManager cpManager; /** * Default constructor */ public PgSQLCopyOutputAdapter() { super(); } /** * Initialize the module. Called during pipeline creation. Initialize the * Logger, and load the SQL statements. * * @param PipelineName The name of the pipeline this module is in * @param ModuleName The module symbolic name of this module * @throws OpenRate.exception.InitializationException */ @Override public void init(String PipelineName, String ModuleName) throws InitializationException { String ConfigHelper; super.init(PipelineName, ModuleName); registerClientManager(); // Register ourself with the client manager setSymbolicName(ModuleName); // Get copy statement from properties ConfigHelper = initCopyStatement(); processControlEvent(SERVICE_COPY_STMT_KEY, true, ConfigHelper); // Get partition identification from properties ConfigHelper = initPartitionIdentStatement(); processControlEvent(SERVICE_PARTITION_IDENT_KEY, true, ConfigHelper); // The data source property was added to allow database to database // JDBC adapters to work properly using 1 configuration file. ConfigHelper = initDataSourceName(); processControlEvent(SERVICE_DATASOURCE_KEY, true, ConfigHelper); // prepare the data source - this does not open a connection if (DBUtil.initDataSource(dataSourceName) == null) { message = "Could not initialise DB connection <" + dataSourceName + "> to in module <" + getSymbolicName() + ">."; getPipeLog().error(message); throw new InitializationException(message, getSymbolicName()); } } /** * Process the stream header. Get the file base name and open the transaction. * * @param r The record we are working on * @return The processed record * @throws ProcessingException */ @Override public HeaderRecord procHeader(HeaderRecord r) throws ProcessingException { // perform any parent processing first super.procHeader(r); // Initialize dataHolder dataHolder = new ConcurrentHashMap<>(2); return r; } /** * Prepare good records for writing to the defined output stream. * * @param r The current record we are working on * @return The prepared record * @throws ProcessingException */ @Override public IRecord prepValidRecord(IRecord r) throws ProcessingException { DBRecord outRec; String recordPartitionIdent = null; Iterator<DBRecord> outRecIter; Collection<DBRecord> outRecCol = null; try { outRecCol = procValidRecord(r); recordPartitionIdent = this.getPartitionIdent(r); } catch (ProcessingException pe) { // Pass the exception up passUpErrorMessageAbortTransaction("Processing exception preparing valid record", pe); } catch (ArrayIndexOutOfBoundsException aiex) { // Not good. Abort the transaction setErrorMessageAbortTransaction("Column Index preparing valid record", aiex); } catch (Exception ex) { // Not good. Abort the transaction setErrorMessageAbortTransaction("Unknown Exception preparing valid record", ex); } // Null return means "do not bother to process" if (outRecCol != null && CopyStatement != null) { outRecIter = outRecCol.iterator(); try { String assignedPartition; while (outRecIter.hasNext()) { outRec = outRecIter.next(); if (recordPartitionIdent != null && !recordPartitionIdent.isEmpty()) { assignedPartition = recordPartitionIdent; } else { assignedPartition = DEFAULT_PARTITON_NAME; } if (dataHolder.containsKey(assignedPartition)) { dataHolder.get(assignedPartition).add(outRec.getDataString()); } else { CopyOnWriteArrayList<String> newEntry = new CopyOnWriteArrayList<>(); newEntry.add(outRec.getDataString()); dataHolder.put(assignedPartition, newEntry); } } } catch (NullPointerException npe) { setErrorMessageAbortTransaction("Null value is not a vailid SQL statement", npe); } } return r; } /** * Prepare bad records for writing to the defined output stream. * * @param r The current record we are working on * @return The prepared record * @throws ProcessingException */ @Override public IRecord prepErrorRecord(IRecord r) throws ProcessingException { // Just return - no processing needed return r; } /** * This is called when a data record is encountered. You should do any normal * processing here. Note that the result is a collection for the case that we * have to re-expand after a record compression input adapter has done * compression on the input stream. * * @param r The record we are working on * @return The collection of processed records * @throws ProcessingException */ public abstract Collection<DBRecord> procValidRecord(IRecord r) throws ProcessingException; /** * This is called when a data record with errors is encountered. You should do * any processing here that you have to do for error records, e.g. statistics, * special handling, even error correction! * * @param r The record we are working on * @return The collection of processed records * @throws ProcessingException */ public abstract Collection<DBRecord> procErrorRecord(IRecord r) throws ProcessingException; /** * Perform data copy into database * * @param transactionNumber * @return true if successful, false if not */ public boolean performCopy(int transactionNumber) { try { // Get connection JDBCcon = DBUtil.getConnection(dataSourceName); // Initialize copy manager cpManager = JDBCcon.unwrap(PGConnection.class).getCopyAPI(); //################################### // Start with copy steps for (String copyRecords : dataHolder.keySet()) { // Prepare copy data byte[] CopyData = StringUtils.join(dataHolder.get(copyRecords), System.getProperty("line.separator")).getBytes(); // Start with COPY operation/s long numOfRowsEffected = cpManager.copyIn(this.prepareCopyStatement(copyRecords), new ByteArrayInputStream(CopyData)); getPipeLog().debug("Copy effected " + numOfRowsEffected + " rows in module <" + getSymbolicName() + ">"); } } catch (InitializationException iex) { // Not good. Abort the transaction setErrorMessageAbortTransaction("Error acquiring connection from DataSource", iex); } catch (SQLException Sex) { // Not good. Abort the transaction setErrorMessageAbortTransaction("Error performing copy to database", Sex); } catch (IOException ioe) { // Not good. Abort the transaction setErrorMessageAbortTransaction("Error closing InputStream", ioe); } finally { // Close the connection DBUtil.close(JDBCcon); } // We have errors. Abort. if (getExceptionHandler().hasError()) { return false; } // Everything went well return true; } /** * Prepare copy statement * * @param partition * @return copy statement */ private String prepareCopyStatement(String partition) { if (partitionIdent != null) { // Copy to different partition tables return CopyStatement.replace(partitionIdent, partition); } else { // Copy to specific table return CopyStatement; } } /** * Returns table partition identification if set * * @param OutRec * @return table partition identification */ protected abstract String getPartitionIdent(IRecord OutRec); // ----------------------------------------------------------------------------- // ------------------ Custom connection management functions ------------------- // ----------------------------------------------------------------------------- /* * closeStream() is called by the pipeline when no more information comes * down it. We must perform a transaction state change here to FLUSHED */ @Override public void closeStream(int TransactionNumber) { // Nothing at the moment } /** * Used to skip to the end of the stream in the case that the transaction is * aborted. * * @return True if the rest of the transaction was skipped otherwise false */ @Override public boolean SkipRestOfStream() { return getTransactionAborted(getTransactionNumber()); } // ----------------------------------------------------------------------------- // ------------- Start of inherited IEventInterface functions ------------------ // ----------------------------------------------------------------------------- /** * processControlEvent is the event processing hook for the External Control * Interface (ECI). This allows interaction with the external world, for * example turning the dumping on and off. * * @param Command The command that we are to work on * @param Init True if the pipeline is currently being constructed * @param Parameter The parameter value for the command * @return The result message of the operation */ @Override public String processControlEvent(String Command, boolean Init, String Parameter) { int ResultCode = -1; if (Command.equalsIgnoreCase(SERVICE_DATASOURCE_KEY)) { if (Init) { dataSourceName = Parameter; ResultCode = 0; } else { if (Parameter.equals("")) { return dataSourceName; } else { return CommonConfig.NON_DYNAMIC_PARAM; } } } if (Command.equalsIgnoreCase(SERVICE_COPY_STMT_KEY)) { if (Init) { CopyStatement = Parameter; ResultCode = 0; } else { if (Parameter.equals("")) { return CopyStatement; } else { return CommonConfig.NON_DYNAMIC_PARAM; } } } if (Command.equalsIgnoreCase(SERVICE_PARTITION_IDENT_KEY)) { if (Init) { partitionIdent = Parameter; ResultCode = 0; } else { if (Parameter.equals("")) { return partitionIdent; } else { return CommonConfig.NON_DYNAMIC_PARAM; } } } if (Command.equalsIgnoreCase(SERVICE_STATUS_KEY)) { return "OK"; } if (ResultCode == 0) { getPipeLog().debug(LogUtil.LogECIPipeCommand(getSymbolicName(), getPipeName(), Command, Parameter)); return "OK"; } else { // This is not our event, pass it up the stack return super.processControlEvent(Command, Init, Parameter); } } /** * registerClientManager registers this class as a client of the ECI listener * and publishes the commands that the plug in understands. The listener is * responsible for delivering only these commands to the plug in. * * @throws OpenRate.exception.InitializationException */ @Override public void registerClientManager() throws InitializationException { // Set the client reference and the base services first super.registerClientManager(); //Register services for this Client ClientManager.getClientManager().registerClientService(getSymbolicName(), SERVICE_DATASOURCE_KEY, ClientManager.PARAM_MANDATORY); ClientManager.getClientManager().registerClientService(getSymbolicName(), SERVICE_COPY_STMT_KEY, ClientManager.PARAM_MANDATORY); ClientManager.getClientManager().registerClientService(getSymbolicName(), SERVICE_PARTITION_IDENT_KEY, ClientManager.PARAM_DYNAMIC); ClientManager.getClientManager().registerClientService(getSymbolicName(), SERVICE_STATUS_KEY, ClientManager.PARAM_DYNAMIC); } // ----------------------------------------------------------------------------- // --------------- Start of transactional layer functions ---------------------- // ----------------------------------------------------------------------------- /** * When a transaction is started, the transactional layer calls this method to * see if we have any reason to stop the transaction being started, and to do * any preparation work that may be necessary before we start. * * @param transactionNumber The transaction to start * @return The transaction number */ @Override public int startTransaction(int transactionNumber) { return 0; } /** * Perform any processing that needs to be done when we are committing the * transaction; * * @param transactionNumber The transaction to commit */ @Override public void commitTransaction(int transactionNumber) { // no op } /** * Perform any processing that needs to be done when we are rolling back the * transaction; * * @param transactionNumber The transaction to rollback */ @Override public void rollbackTransaction(int transactionNumber) { // Something went wrong, abort transaction this.setTransactionAbort(transactionNumber); } /** * Perform any processing that needs to be done when we are flushing the * transaction; * * @param transactionNumber The transaction to flush * @return The transaction number */ @Override public int flushTransaction(int transactionNumber) { // close the input stream if (performCopy(transactionNumber)) { return 0; } else { return -1; } } /** * Close Transaction is the trigger to clean up transaction related * information such as variables, status etc. * * Close down the statements we opened. Because the commit and rollback * statements are optional, we check if they have been defined before we ry to * close them. * * @param transactionNumber The transaction we are working on */ @Override public void closeTransaction(int transactionNumber) { // Clear CopyManager cpManager = null; // Clear data holder dataHolder.clear(); } // ----------------------------------------------------------------------------- // --------------- Start of custom initialisation functions --------------------- // ----------------------------------------------------------------------------- /** * Initialization of copy statement * * @return The copy statement string * @throws OpenRate.exception.InitializationException */ public String initCopyStatement() throws InitializationException { String copyStmt; // Get the initialization statement from the properties copyStmt = PropertyUtils.getPropertyUtils().getBatchOutputAdapterPropertyValueDef(getPipeName(), getSymbolicName(), COPY_STMT_KEY, "None"); if ((copyStmt == null) || copyStmt.equalsIgnoreCase("None")) { message = "Output <" + getSymbolicName() + "> - Initialisation statement not found from <" + COPY_STMT_KEY + ">"; getPipeLog().error(message); throw new InitializationException(message, getSymbolicName()); } return copyStmt; } public String initPartitionIdentStatement() throws InitializationException { // Get the initialization statement from the properties return PropertyUtils.getPropertyUtils().getBatchOutputAdapterPropertyValueDef(getPipeName(), getSymbolicName(), PARTITION_IDENT_KEY, null); } /** * Get the data source name from the properties * * @return The query string * @throws OpenRate.exception.InitializationException */ public String initDataSourceName() throws InitializationException { String DSN; DSN = PropertyUtils.getPropertyUtils().getBatchOutputAdapterPropertyValueDef(getPipeName(), getSymbolicName(), DATASOURCE_KEY, "None"); if ((DSN == null) || DSN.equalsIgnoreCase("None")) { message = "Output <" + getSymbolicName() + "> - Datasource name not found from <" + DATASOURCE_KEY + ">"; getPipeLog().error(message); throw new InitializationException(message, getSymbolicName()); } return DSN; } /** * Report the error and abort current Transaction * * @param message * @param err */ private void setErrorMessageAbortTransaction(String errMessage, Throwable err) { message = errMessage + " in module <" + getSymbolicName() + ">. Message <" + err.getMessage() + ">. Aborting transaction."; getPipeLog().fatal(message); getExceptionHandler().reportException(new ProcessingException(message, err, getSymbolicName())); setTransactionAbort(getTransactionNumber()); } /** * Pass up the error and abort current Transaction * * @param message * @param err */ private void passUpErrorMessageAbortTransaction(String errMessage, Throwable err) { message = errMessage + " in module <" + getSymbolicName() + ">. Message <" + err.getMessage() + ">. Aborting transaction."; getPipeLog().fatal(message); getExceptionHandler().reportException(new ProcessingException(err, getSymbolicName())); setTransactionAbort(getTransactionNumber()); } }