This chapter discusses writing Java code to implement an event filter, a custom formatter for a built-in handler, or a custom event handler. Specifying custom formatting through a Velocity template is also covered.
The Java package names are compliant with the Oracle standard. You must migrate the any previous release custom formatters, handlers, or filters to the new package names.
This chapter includes the following sections:
By default, all transactions, operations and metadata events are passed to the DataSourceListener
event handlers. An event filter can be implemented to filter the events sent to the handlers. The filter could select certain operations on certain tables containing certain column values, for example
Filters are additive: if more than one filter is set for a handler, then all filters must return true in order for the event to be passed to the handler.
You can configure filters using the Java application properties file:
# handler "foo" only receives certain events gg.handler.one.type=jms gg.handler.one.format=mytemplate.vm gg.handler.one.filter=com.mycompany.MyFilter
To activate the filter, you write the filter and set it on the handler; no additional logic needs to be added to specific handlers.
You can write a custom filter by implementing the oracle.goldengate.datasource.DsEventFilterinterface
filter, which contains filter contracts as in the following example:
package com.mycompany; import oracle.goldengate.datasource.DsConfiguration; import oracle.goldengate.datasource.DsEventFilter; import oracle.goldengate.datasource.DsOperation; import oracle.goldengate.datasource.DsTransaction; import oracle.goldengate.datasource.meta.DsMetaData; import oracle.goldengate.datasource.meta.TableMetaData; public class MyFilter implements DsEventFilter { @Override public void init(DsConfiguration conf) { } @Override public void destroy() { } @Override public boolean doProcess(DsTransaction tx, DsMetaData metaData) {return true// transactionBegin(), transactionCommit(), transactionRollback() will be allowed.} @Override public boolean doProcess(DsOperation op, DsMetaData metaData) {return true// operationAdded() will be allowed.} @Override public boolean doProcess(TableMetaData tbl,DsMetaData meta) {return true// metaDataChanged() will be allowed.} }
You can customize the output format of a built-in handler by:
Writing a custom formatter in Java or
Using a Velocity template
As an alternative to writing Java code for custom formatting, Velocity templates can be a good alternative to quickly prototype formatters. For example, the following template could be specified as the format of a JMS or file handler:
Transaction: numOps='$tx.size' ts='$tx.timestamp' #for each( $op in $tx ) operation: $op.sqlType, on table "$op.tableName": #for each( $col in $op ) $op.tableName, $col.meta.columnName = $col.value #end #end
If the template were named sample.vm, it could be placed in the classpath, for example:
gg_install_dir/dirprm/sample.vm
Note:
If using Velocity templates, the file name must end with the suffix .vm; otherwise the formatter is presumed to be a Java class.
Update the Java application properties file to use the template:
# set properties on 'one' gg.handler.one.type=file gg.handler.one.format=sample.vm gg.handler.one.file=output.xml
When modifying templates, there is no need to recompile any Java source; simply save the template and re-run the Java application. When the application is run, the following output would be generated (assuming a table named SCHEMA.SOMETABLE
, with columns TESTCOLA
and TESTCOLB
):
Transaction: numOps='3' ts='2008-12-31 12:34:56.000' operation: UPDATE, on table "SCHEMA.SOMETABLE": SCHEMA.SOMETABLE, TESTCOLA = value 123 SCHEMA.SOMETABLE, TESTCOLB = value abc operation: UPDATE, on table "SCHEMA.SOMETABLE": SCHEMA.SOMETABLE, TESTCOLA = value 456 SCHEMA.SOMETABLE, TESTCOLB = value def operation: UPDATE, on table "SCHEMA.SOMETABLE": SCHEMA.SOMETABLE, TESTCOLA = value 789 SCHEMA.SOMETABLE, TESTCOLB = value ghi
The preceding examples show a JMS handler and a file output handler using the same formatter (com.mycompany.MyFormatter
). The following is an example of how this formatter may be implemented.
Example 16-1 Custom Formatting Implementation
package com.mycompany.MyFormatter; import oracle.goldengate.datasource.DsOperation; import oracle.goldengate.datasource.DsTransaction; import oracle.goldengate.datasource.format.DsFormatterAdapter; import oracle.goldengate.datasource.meta.ColumnMetaData; import oracle.goldengate.datasource.meta.DsMetaData; import oracle.goldengate.datasource.meta.TableMetaData; import java.io.PrintWriter; public class MyFormatter extends DsFormatterAdapter {
public MyFormatter() { } @Override public void formatTx(DsTransaction tx,
DsMetaData meta, PrintWriter out)
{
out.print("Transaction: " ); out.print("numOps=\'" + tx.getSize() + "\' " ); out.println("ts=\'" + tx.getStartTxTimeAsString() + "\'"); for(DsOperation op: tx.getOperations()) { TableName currTable = op.getTableName(); TableMetaData tMeta = dbMeta.getTableMetaData(currTable); String opType = op.getOperationType().toString(); String table = tMeta.getTableName().getFullName(); out.println(opType + " on table \"" + table + "\":" ); int colNum = 0; for(DsColumn col: op.getColumns()) {
ColumnMetaData cMeta = tMeta.getColumnMetaData( colNum++ ); out.println( cMeta.getColumnName() + " = " + col.getAfterValue() ); }
} @Override public void formatOp(DsTransaction tx,
DsOperation op, TableMetaData tMeta, PrintWriter out)
{
// not used...
}
}
The formatter defines methods for either formatting complete transactions (after they are committed) or individual operations (as they are received, before the commit). If the formatter is in operation mode, then formatOp
(...) is called; otherwise, formatTx
(...) is called at transaction commit.
To compile and use this custom formatter, include the Oracle GoldenGate for Java JARs in the classpath and place the compiled .class
files in gg_install_dir
/dirprm
:
javac -d gg_install_dir/dirprm
-classpath ggjava/ggjava.jar MyFormatter.java
The resulting class files are located in resources/classes
(in correct package structure):
gg_install_dir/dirprm/com/mycompany/MyFormatter.class
Alternatively, the custom classes can be put into a JAR; in this case, either include the JAR file in the JVM classpath using the user exit properties (using java.class.path
in the jvm.bootoptions
property), or by setting the Java application properties file to include your custom JAR:
# set properties on 'one' gg.handler.one.type=file gg.handler.one.format=com.mycompany.MyFormatter gg.handler.one.file=output.xml gg.classpath=/path/to/my.jar,/path/to/directory/of/jars/*
A custom handler can be implemented by extending AbstractHandler
as in the following example:
import oracle.goldengate.datasource.*; import static oracle.goldengate.datasource.GGDataSource.Status; public class SampleHandler extends AbstractHandler { @Override public void init(DsConfiguration conf, DsMetaData metaData) { super.init(conf, metaData); // ... do additional config... } @Override public Status operationAdded(DsEvent e, DsTransaction tx, DsOperation op) { ... } @Override public Status transactionCommit(DsEvent e, DsTransaction tx) { ... } @Override public Status metaDataChanged(DsEvent e, DsMetaData meta) { .... } @Override public void destroy() { /* ... do cleanup ... */ } @Override public String reportStatus() { return "status report..."; } @Override public Status ddlOperation(OpType opType, ObjectType objectType, String objectName, String ddlText) }
The method in AbstractHandler
is not abstract rather it has a body. In the body it performs cached metadata invalidation by marking the metadata object as dirty. It also provides TRACE-level logging of DDL events when the ddlOperation
method is specified. You can override this method in your custom handler implementations. You should always call the super method before any custom handling to ensure the functionality in AbstractHandler
is executed
When a transaction is processed from the Extract, the order of calls into the handler is as follows:
Initialization:
First, the handler is constructed.
Next, all the "setters" are called on the instance with values from the property file.
Finally, the handler is initialized; the init(...)
method is called before any transactions are received. It is important that the init(...)
method call super.init(...)
to properly initialize the base class.
Metadata is thenreceived. If the user exit is processing an operation on a table not yet seen during this run, a metadata event is fired, and the metadataChanged(...)
method is called. Typically, there is no need to implement this method. The DsMetaData
is automatically updated with new data source metadata as it is received.
A transaction is started. A transaction event is fired, causing the transactionBegin(...)
method on the handler to be invoked (this is not shown). This is typically not used, since the transaction has zero operations at this point.
Operations are added to the transaction, one after another. This causes the operationAdded(...)
method to be called on the handler for each operation added. The containing transaction is also passed into the method, along with the data source metadata that contains all processed table metadata. The transaction has not yet been committed, and could be aborted before the commit is received.
Each operation contains the column values from the transaction (possibly just the changed values when Extract is processing with compressed updates.) The column values may contain both before and after values.
For the ddlOperation
method, the options are:
opType
- Is an enumeration that identifies the DDL operation type that is occurring (CREATE
, ALTER
, and so on).
objectType
- Is an enumeration that identifies the type of the target of the DDL (TABLE
, VIEW
, and so on).
objectName
- Is the fully qualified source object name; typically a fully qualified table name.
ddlText
- Is the raw DDL text executed on the source relational database.
The transaction is committed. This causes the transactionCommit(...)
method to be called.
Periodically, reportStatus
may be called; it is also called at process shutdown. Typically, this displays the statistics from processing (the number of operations andtransactions processed and other details).
An example of a simple printer handler, which just prints out very basic event information for transactions, operations and metadata follows. The handler also has a property myoutput
for setting the output file name; this can be set in the Java application properties file as follows:
gg.handlerlist=sample # set properties on 'sample' gg.handler.sample.type=sample.SampleHandler gg.handler.sample.myoutput=out.txt
To use the custom handler,
Compile the class
Include the class in the application classpath,
Add the handler to the list of active handlers in the Java application properties file.
To compile the handler, include the Oracle GoldenGate for Java JARs in the classpath and place the compiled .class
files in gg_install_dir
/javaue/resources/classes
:
javac -d gg_install_dir/dirprm
-classpath ggjava/ggjava.jar SampleHandler.java
The resulting class files would be located in resources/classes
, in correct package structure, such as:
gg_install_dir/dirprm/sample/SampleHandler.class
Note:
For any Java application development beyond hello world examples, either Ant or Maven would be used to compile, test and package the application. The examples showing javac
are for illustration purposes only.
Alternatively, custom classes can be put into a JAR and included in the classpath. Either include the custom JAR file(s) in the JVM classpath using the user exit properties (using java.class.path
in the jvm.bootoptions
property), or by setting the Java application properties file to include your custom JAR:
# set properties on 'one' gg.handler.one.type=sample.SampleHandler gg.handler.one.myoutput=out.txt gg.classpath=/path/to/my.jar,/path/to/directory/of/jars/*
The classpath property can be set on any handler to include additional individual JARs, a directory (which would contain resources or extracted class files) or a whole directory of JARs. To include a whole directory of JARs, use the Java 6 style syntax:
c:/path/to/directory/* (or on UNIX: /path/to/directory/* )
Only the wildcard * can be specified; a file pattern cannot be used. This automatically matches all files in the directory ending with the .jar
suffix. To include multiple JARs or multiple directories, you can use the system-specific path separator (on UNIX, the colon and on Windows the semicolon) or you can use platform-independent commas, as shown in the preceding example.
If the handler requires many properties to be set, just include the property in the parameter file, and your handler's corresponding "setter" will be called. For example:
gg.handler.one.type=com.mycompany.MyHandler gg.handler.one.myOutput=out.txt gg.handler.one.myCustomProperty=12345
The preceding example would invoke the following methods in the custom handler:
public void setMyOutput(String s) {
// use the string...
} public void setMyCustomProperty(int j) {
// use the int...
}
Any standard Java type may be used, such as int, long, String, boolean. For custom types, you may create a custom property editor to convert the String to your custom type.
ExampleFormatter.java
example demonstrates how to:
access and output metadata,
access and output the column data values of the operation, and
ascertain if the operation is a insert, update, delete, or primary key update.
Example 16-2 Custom Formatter for Java Delivery
/* * * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. * */ package oracle.goldengate.datasource.format; //Logging imports import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.PrintWriter; import oracle.goldengate.datasource.DsColumn; import oracle.goldengate.datasource.DsConfiguration; import oracle.goldengate.datasource.DsOperation; import oracle.goldengate.datasource.DsToken; import oracle.goldengate.datasource.DsTransaction; import oracle.goldengate.datasource.TxOpMode; import oracle.goldengate.datasource.meta.ColumnMetaData; import oracle.goldengate.datasource.meta.DsMetaData; import oracle.goldengate.datasource.meta.TableMetaData; import oracle.goldengate.datasource.meta.TableName; import oracle.goldengate.util.CDataUtil; /** * This class provides an example for Oracle GoldenGate Java Adapter customers * to write their own custom formatter which can be plugged into the Java file * writer or Java JMS handler. * This example does NOT work with the new Big Data integrations such as HDFS, Kafka, * or Flume as formatter interface for those has been changed. * This formatter formats data in CSV format, by default. The field and line * delimiters are configurable with the default as a comma and a line feed * respectively. * The goal is to provide an example custom formatter for customers wishing * to develop a custom formatter, to see how it works. The functionality * can be extended and/or altered as needed to fulfill the specific needs of * the customer. * User configures the use of this formatter by setting the follow configuration * in the Oracle GoldenGate Java Adapter properties file. * gg.handler.name.format=oracle.goldengate.datasource.format.ExampleFormatter * @author Tom Campbell * @email thomas.campbell@oracle.com */ public class ExampleFormatter extends DsFormatterAdapter{ private static final Logger logger=LoggerFactory.getLogger(ExampleFormatter.class); //The field delimiter defaults to a comma private String fieldDelimiter = ","; //The line delimiter defaults to the line separator private String lineDelimiter = System.lineSeparator(); /** * Default Constructor */ public ExampleFormatter() { super(TxOpMode.op); //IMPLEMENTERS - Add constructor implementation code below } /** * Constructor with operation mode argument. * @param mode Mode of operation Transaction (tx) or Operation (op) */ public ExampleFormatter(TxOpMode mode) { super(mode); //IMPLEMENTERS - Add constructor implementation code below } /** * Setter method to set the field delimiter. User can configure * generic setters on the formatter than are configured in the Oracle * GoldenGate Java Adapter properties file as follows: * gg.handler.name.format.fieldDelimiter=somevalue * The user is free to generate any setter methods for the formatter. The * configuration initialization code uses Java reflection to find and invoke * the corresponding setter method. If the method is not found an exception * will be thrown and the Oracle GoldenGate process will ABEND. * @param fd The field delimiter value */ void setFieldDelimiter(String fd){ //The CDataUtil.unwrapCData allows the user to configure values that //are considered to be whitespace. Examples are spaces, tabs, carriage //returns, line feeds, etc. To configure white space use the CDATA[] wrapper //as follows. //gg.handler.name.format.fieldDelimiter=CDATA[somevalue] //The configuration value will be "somevalue". //The CDATA[] functionality is needed to preserve whitespace because the //default behavior of the GG config code is to trim whitespace. fieldDelimiter = CDataUtil.unwrapCData(fd); } /** * Setter method to set the line delimiter. User can configure * generic setters on the formatter than are configured in the Oracle * GoldenGate Java Adapter properties file as follows: * gg.handler.name.format.lineDelimiter=somevalue * The user is free to generate any setter methods for the formatter. The * configuration initialization code uses Java reflection to find and invoke * the corresponding setter method. If the method is not found an exception * will be thrown and the Oracle GoldenGate process will ABEND. * @param fd The field delimiter value */ void setLineDelimiter(String ld){ //The CDataUtil.unwrapCData allows the user to configure values that //are considered to be whitespace. Examples are spaces, tabs, carriage //returns, line feeds, etc. To configure whitespace use the CDATA[] wrapper //as follows. //gg.handler.name.format.fieldDelimiter=CDATA[\n] //The configuration value will be a line feed. //The CDATA[] functionality is needed to preserve whitespace because the //default behavior of the GG config code is to trim whitespace. lineDelimiter = CDataUtil.unwrapCData(ld); } /** * Initialization method. This is called once after all of the setter * methods are called to set the explicit configuration methods of the * formatter. * @param conf data source configuration info from properties file * @param meta metadata from data source, describing columns/tables, etc */ @Override public void init(DsConfiguration conf, DsMetaData meta) { //Call the base class first. super.init(conf, meta); //IMPLEMENTERS - Add initialization code below logger.info("Initializing the example formatter."); logger.info(" The field delimiter is configured as [" + fieldDelimiter + "]."); logger.info(" The line delimiter is configured as [" + lineDelimiter + "]."); } /** * The format operation method. This is where the bulk of the formatting * logic will reside. * @param tx The transaction object * @param op The operation object * @param tmeta The table metadata object * @param writer The print writer object to which results should be written */ @Override public void formatOp(DsTransaction tx, DsOperation op, TableMetaData tmeta, PrintWriter writer) { logger.info("Method formatOp called..."); //IMPLEMENTERS - Below is the call the example implementation //comment out and insert custom implementation here. internalFormatOperation(tx, op, tmeta, writer); } @Override public void formatTx(DsTransaction tx, DsMetaData dbmeta, PrintWriter writer) { logger.info("Method formatTx called..."); //In transaction mode (i.e. gg.handler.name.mode=tx) the handler may //be configured to call this method to format data at the end of the //into a single print writer. This is probably not the usual use case. //Using replicat transaction grouping (see GROUPTRANSOPS replicat configuration) //the number of operations in a grouped transaction can be substantial. //Replicat GROUPTRANSOPS is set to 1000 by default meaning as many as //1000 source transactions are getting grouped into a single target //transaction. for (DsOperation op : tx) { TableName currTable = op.getTableName(); TableMetaData tmeta = dbmeta.getTableMetaData(currTable); //Then call the internal formatting internalFormatOperation(tx, op, tmeta, writer); } } private void internalFormatOperation(DsTransaction tx, DsOperation op, TableMetaData tmeta, PrintWriter writer) { //Use a string builder for performance. StringBuilder sb = new StringBuilder(); //IMPLEMENTERS - example code shown below outputs commonly used data. //Modify the code below as need for the specific implementation. //Format the table name formatTableName(op, sb); //Format the operation type. formatOperationType(op, sb); //Format the operation timestamp. formatOperationTimestamp(op, sb); //Format the position formatPosition(op, sb); //Add primary keys formatPrimaryKeys(tmeta, sb); //Format tokens formatGGTokens(op, sb); //Format the column data formatColumnData(op, tmeta, sb); //Transfer the message to the print writer. writer.print(sb.toString()); } /** * This method formats the table name for output * @param op The operation object * @param sb The string builder object to append the data */ private void formatTableName(DsOperation op, StringBuilder sb){ sb.append("TableName"); sb.append(fieldDelimiter); //The original fully qualified table name in the original case sb.append(op.getTableName().getOriginalName()); sb.append(fieldDelimiter); } /** * Method to format the operation type data. Operation types are * insert, update, delete, and primary key update. * @param op The operation object * @param sb The string builder object to append the data */ private void formatOperationType(DsOperation op, StringBuilder sb){ //Output the operation type sb.append("OperationType"); sb.append(fieldDelimiter); if (op.getOperationType().isInsert()){ sb.append("insert"); }else if (op.getOperationType().isDelete()){ sb.append("delete"); }else if (op.getOperationType().isPkUpdate()){ //This is a specialzied use case of update. The isUpdate() call also //returns true for primary key updates. sb.append("primarykeyupdate"); }else if (op.getOperationType().isUpdate()){ sb.append("update"); } sb.append(fieldDelimiter); } /** * Method to format the operation timestamp. This is the transaction commit * time of the transaction that contains the operation. All operations * within a transaction have the same operation timestamp. * @param op The operation object * @param sb The string builder object to append the data */ private void formatOperationTimestamp(DsOperation op, StringBuilder sb){ sb.append("OperationTimestamp"); sb.append(fieldDelimiter); sb.append(op.getTimestampAsString()); sb.append(fieldDelimiter); } /** * Method to format the position. * The position is the concatentated trail file sequence number followed * by the RBA number (offset in the trail file). The two together provide * traceability of the operation back to source trail file. * @param op The operation object * @param sb The string builder object to append the data. */ private void formatPosition(DsOperation op, StringBuilder sb){ sb.append("Position"); sb.append(fieldDelimiter); sb.append(op.getPosition()); sb.append(fieldDelimiter); } /** * Method to format the primary keys. * @param tmeta The table metadata object. * @param sb The string builder object to append the data. */ private void formatPrimaryKeys(TableMetaData tmeta, StringBuilder sb){ sb.append("PrimaryKeys"); for(ColumnMetaData cmeta :tmeta.getColumnMetaData()){ if (cmeta.isKeyCol()){ sb.append(fieldDelimiter); sb.append(cmeta.getOriginalColumnName()); } } } /** * Method to format the GoldenGate token key/value pairs from the source * trail file. * @param op The operation object. * @param sb The string builder object to append the data. */ private void formatGGTokens(DsOperation op, StringBuilder sb){ //If this is false there will not be any tokens if(op.getIncludeTokens()){ sb.append(fieldDelimiter); sb.append("GGTokens"); sb.append(fieldDelimiter); for(DsToken token : op.getTokens().values()){ sb.append(token.getKey()); sb.append(fieldDelimiter); sb.append(token.getValue()); sb.append(fieldDelimiter); } } } /** * Method to output the column value data. This data starts with the column * name, "before", the before image value, "after, and the after image value. * This before and after column values include special handling for missing * and null values. Missing values are output as "MISSING". Null values are * output as "NULL" * @param op The operation object. * @param tmeta The table metadata object * @param sb */ private void formatColumnData(DsOperation op, TableMetaData tmeta, StringBuilder sb){ int cIndex = 0; sb.append(fieldDelimiter); sb.append("TableData"); for(DsColumn col : op.getColumns()) { //Get the column metadata ColumnMetaData cmeta = tmeta.getColumnMetaData(cIndex++); //Output the column name sb.append(fieldDelimiter); sb.append(cmeta.getOriginalColumnName()); sb.append(fieldDelimiter); //Output the before value //Insert operations have no before values //Delete operations generally only have column values for primary //keys and not the complete row. //Updates will have all column values if NOCOMPRESSUPDATES is configured //else updates will have only primary keys and the column values that changed. sb.append("Before"); sb.append(fieldDelimiter); if (col.getBefore() == null){ //The before image value is missing for the column. sb.append("MISSING"); }else if(col.getBefore().isValueNull()){ sb.append("NULL"); }else{ sb.append(col.getBefore().getValue()); } sb.append(fieldDelimiter); //Output the after value //Insert operations have after values //Delete operations have no after values //Update operations will have all column values if NOCOMPRESSUPDATES is configured //else updates will have only primary keys and the column values that changed. sb.append("After"); sb.append(fieldDelimiter); if (col.getAfter() == null){ //The before image value is missing for the column. sb.append("MISSING"); }else if(col.getAfter().isValueNull()){ sb.append("NULL"); }else{ sb.append(col.getAfter().getValue()); } } //Add the line delimiter at the end sb.append(lineDelimiter); } }
Note:
This example does not work with Oracle GoldenGate for Big Data handlers.
This type of custom formatter works well with the JMS and the Java File Writer handlers. Once processed, this outputs operation data in a CSV format. Operation metadata is output first, and then the column data including before and after change column image data. The field and line delimiters are configurable.
Tip:
You can obtain a copy of the maven project that you import to your Java IDE by contacting Oracle Support.
There is Javadoc available for the Java API. The Javadoc has been intentionally reduced to a set of core packages, classes and interfaces in order to only distribute the relevant interfaces and classes useful for customizing and extension.
In each package, some classes have been intentionally omitted for clarity. The important classes are:
oracle.goldengate.datasource.DsTransaction
: represents a database transaction. A transaction contains zero or more operations.
oracle.goldengate.datasource.DsOperation
: represents a database operation (insert, update, delete). An operation contains zero or more column values representing the data-change event. Columns indexes are offset by zero in the Java API.
oracle.goldengate.datasource.DsColumn
: represents a column value. A column value is a composite of a before and an after value. A column value may be 'present' (having a value or be null) or 'missing' (is not included in the source trail).
oracle.goldengate.datasource.DsColumnComposite
is the composite
oracle.goldengate.datasource.DsColumnBeforeValue
is the column value before the operation (this is optional, and may not be included in the operation)
oracle.goldengate.datasource.DsColumnAfterValue
is the value after the operation
oracle.goldengate.datasource.meta.DsMetaData
: represents all database metadata seen; initially, the object is empty. DsMetaData
contains a hash map of zero or more instances of TableMetaData
, using the TableName
as a key.
oracle.goldengate.datasource.meta.TableMetaData
: represents all metadata for a single table; contains zero or more ColumnMetaData
.
oracle.goldengate.datasource.meta.ColumnMetaData
: contains column names and data types, as defined in the database or in the Oracle GoldenGate source definitions file.
See the Javadoc for additional details.