/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at
* docs/licenses/cddl.txt
* or http://www.opensource.org/licenses/cddl1.php.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at
* docs/licenses/cddl.txt. If applicable,
* add the following below this CDDL HEADER, with the fields enclosed
* by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2010-2016 UnboundID Corp.
*/
package com.unboundid.directory.sdk.examples;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.Modification;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.directory.sdk.sync.types.TransactionContext;
import com.unboundid.directory.sdk.sync.types.SyncServerContext;
import com.unboundid.directory.sdk.sync.types.SyncOperation;
import com.unboundid.directory.sdk.sync.api.JDBCSyncDestination;
import com.unboundid.directory.sdk.sync.config.JDBCSyncDestinationConfig;
import com.unboundid.directory.sdk.sync.util.ScriptUtils;
/**
* This class provides a simple example of a JDBC Sync Destination which will
* push changes to a single database table. This implementation does not use any
* configuration arguments.
*/
public final class ExampleJDBCSyncDestination extends JDBCSyncDestination
{
//The server context which can be used for obtaining the server state,
//logging, etc.
private SyncServerContext serverContext;
//The name of the destination data table.
private static final String DATA_TABLE = "DataTable";
//The set of attributes that this simple extension will synchronize.
private static final Set<String> ATTRS = new HashSet<String>();
static
{
ATTRS.add("uid");
ATTRS.add("objectclass");
ATTRS.add("cn");
ATTRS.add("givenname");
ATTRS.add("sn");
ATTRS.add("description");
}
/**
* Retrieves a human-readable name for this extension.
*
* @return A human-readable name for this extension.
*/
@Override
public String getExtensionName()
{
return "Example JDBC Sync Destination";
}
/**
* Retrieves a human-readable description for this extension. Each element
* of the array that is returned will be considered a separate paragraph in
* generated documentation.
*
* @return A human-readable description for this extension, or {@code null}
* or an empty array if no description should be available.
*/
@Override
public String[] getExtensionDescription()
{
return new String[] {
"This extension implements a JDBC Sync Destination which can be used " +
"to synchronize changes to a single data table. " +
"For the sake of simplicity, the columns in the table have " +
"LDAP attribute names (cn, sn, givenname, etc)."
};
}
/**
* Updates the provided argument parser to define any configuration arguments
* which may be used by this extension. The argument parser may also be
* updated to define relationships between arguments (e.g. to specify
* required, exclusive, or dependent argument sets).
* @param parser The argument parser to be updated with the configuration
* arguments which may be used by this extension.
*
* @throws ArgumentException If a problem is encountered while updating the
* provided argument parser.
*/
@Override
public void defineConfigArguments(final ArgumentParser parser)
throws ArgumentException
{
// No arguments will be allowed by default.
}
/**
* This hook is called when a Sync Pipe first starts up, or when the
* <i>resync</i> process first starts up. Any initialization of this sync
* destination should be performed here. This method should generally store
* the {@link SyncServerContext} in a class
* member so that it can be used elsewhere in the implementation.
*
* @param ctx
* a TransactionContext which provides a valid JDBC connection to the
* database.
* @param serverContext A handle to the server context for the server in
* which this extension is running.
* @param config The general configuration for this sync destination.
* @param parser The argument parser which has been initialized from
* the configuration for this JDBC sync destination.
*/
@Override
public void initializeJDBCSyncDestination(
final TransactionContext ctx,
final SyncServerContext serverContext,
final JDBCSyncDestinationConfig config,
final ArgumentParser parser)
{
this.serverContext = serverContext;
}
/**
* This hook is called when a Sync Pipe shuts down, or when the Resync process
* shuts down. Any clean-up should be performed here.
* @param ctx
* a TransactionContext which provides a valid JDBC connection to the
* database.
*/
@Override
public void finalizeJDBCSyncDestination(final TransactionContext ctx)
{
// No cleanup required.
}
/**
* Return a full destination entry (in LDAP form) from the database,
* corresponding to the source {@link Entry} that is passed in.
* This method should perform any queries necessary to gather the latest
* values for all the attributes to be synchronized and return them in an
* Entry.
* <p>
* Note that the if the source entry was renamed (see
* {@link SyncOperation#isModifyDN}), the <code>destEntryMappedFromSrc</code>
* will have the new DN; the old DN can be obtained by calling
* {@link SyncOperation#getDestinationEntryBeforeChange()} and getting the DN
* from there. This method should return the entry in its existing form
* (i.e. with the old DN, before it is changed).
* <p>
* This method <b>must be thread safe</b>, as it will be called repeatedly and
* concurrently by each of the Sync Pipe worker threads as they process
* entries.
* @param ctx
* a TransactionContext which provides a valid JDBC connection to the
* database.
* @param destEntryMappedFromSrc
* the LDAP entry which corresponds to the database "entry" to fetch
* @param operation
* the sync operation for this change
* @return a full LDAP Entry, or null if no such entry exists.
* @throws SQLException
* if there is an error fetching the entry
*/
@Override
public Entry fetchEntry(final TransactionContext ctx,
final Entry destEntryMappedFromSrc,
final SyncOperation operation)
throws SQLException
{
Attribute oc = destEntryMappedFromSrc.getObjectClassAttribute();
Entry entry;
if(ScriptUtils.containsAnyValue(oc, "iNetOrgPerson"))
{
long uid = Long.valueOf(destEntryMappedFromSrc.getAttributeValue("uid"));
String sql = "SELECT * FROM " + DATA_TABLE + " WHERE uid = ?";
PreparedStatement stmt = ctx.prepareStatement(sql);
try
{
stmt.setLong(1, uid);
entry = ctx.searchToRawEntry(stmt, "uid");
}
finally
{
stmt.close();
}
//add an extra attribute that is not found in the database
ScriptUtils.addNumericAttribute(entry, "employeeNumber", uid);
}
else
{
throw new IllegalArgumentException("Unknown entry type: " + oc);
}
return entry;
}
/**
* Creates a full database "entry", corresponding to the LDAP
* {@link Entry} that is passed in. This method should perform any inserts and
* updates necessary to make sure the entry is fully created on the database.
* <p>
* This method <b>must be thread safe</b>, as it will be called repeatedly and
* concurrently by the Sync Pipe worker threads as they process CREATE
* operations.
* @param ctx
* a TransactionContext which provides a valid JDBC connection to the
* database.
* @param entryToCreate
* the LDAP entry which corresponds to the database "entry" to create
* @param operation
* the sync operation for this change
* @throws SQLException
* if there is an error creating the entry
*/
@Override
public void createEntry(final TransactionContext ctx,
final Entry entryToCreate,
final SyncOperation operation)
throws SQLException
{
Attribute oc = entryToCreate.getObjectClassAttribute();
if(ScriptUtils.containsAnyValue(oc, "iNetOrgPerson"))
{
long uid = Long.valueOf(entryToCreate.getAttributeValue("uid"));
String cn = entryToCreate.getAttributeValue("cn");
String givenName = entryToCreate.getAttributeValue("givenname");
String sn = entryToCreate.getAttributeValue("sn");
String description = entryToCreate.getAttributeValue("description");
PreparedStatement stmt = ctx.prepareStatement(
"INSERT INTO " + DATA_TABLE +
" (uid, objectclass, cn, givenname, sn, description)" +
" VALUES (?,?,?,?,?,?)");
stmt.setLong(1, uid);
stmt.setString(2, "iNetOrgPerson");
stmt.setString(3, cn);
stmt.setString(4, givenName);
stmt.setString(5, sn);
if(description != null) //'description' may be null
{
stmt.setString(6, description);
}
else
{
stmt.setNull(6, Types.NULL);
}
stmt.executeUpdate();
stmt.close();
}
else
{
throw new IllegalArgumentException("Unknown entry type: " + oc);
}
}
/**
* Modify an "entry" in the database, corresponding to the LDAP
* {@link Entry} that is passed in. This method may perform multiple updates
* (including inserting or deleting rows) in order to fully synchronize the
* entire entry on the database.
* <p>
* Note that the if the source entry was renamed (see
* {@link SyncOperation#isModifyDN}), the <code>fetchedDestEntry</code> will
* have the old DN; the new DN can be obtained by calling
* {@link SyncOperation#getDestinationEntryAfterChange()} and getting the DN
* from there.
* <p>
* This method <b>must be thread safe</b>, as it will be called repeatedly and
* concurrently by the Sync Pipe worker threads as they process MODIFY
* operations.
* @param ctx
* a TransactionContext which provides a valid JDBC connection to the
* database.
* @param fetchedDestEntry
* the LDAP entry which corresponds to the database "entry" to modify
* @param modsToApply
* a list of Modification objects which should be applied
* @param operation
* the sync operation for this change
* @throws SQLException
* if there is an error modifying the entry
*/
@Override
public void modifyEntry(final TransactionContext ctx,
final Entry fetchedDestEntry,
final List<Modification> modsToApply,
final SyncOperation operation)
throws SQLException
{
//Typically a stored procedure would be used for updates to the database.
//In this simplified example, we'll manually build up an UPDATE statement.
Attribute oc = fetchedDestEntry.getObjectClassAttribute();
if(ScriptUtils.containsAnyValue(oc, "iNetOrgPerson"))
{
long uid = Long.valueOf(fetchedDestEntry.getAttributeValue("uid"));
//Compute the set of columns to update
StringBuilder attrsToUpdate = new StringBuilder();
for(Modification m : modsToApply)
{
String attrName = m.getAttributeName().toLowerCase();
if(!ATTRS.contains(attrName))
{
continue;
}
else if(m.getValues().length == 0)
{
attrsToUpdate.append(attrName).append(" = NULL,");
}
else
{
attrsToUpdate.append(attrName).append(" = ?,");
}
}
//Remove trailing comma
if(attrsToUpdate.length() > 0)
{
attrsToUpdate = attrsToUpdate.deleteCharAt(attrsToUpdate.length()-1);
}
else
{
return;
}
//For a single table, a single update statement is all we need
String sql = "UPDATE " + DATA_TABLE + " SET " + attrsToUpdate.toString() +
" WHERE uid = ?";
PreparedStatement stmt = ctx.prepareStatement(sql);
//Bind the values
int i = 1;
for(Modification m : modsToApply)
{
String attrName = m.getAttributeName().toLowerCase();
if(!ATTRS.contains(attrName) || m.getValues().length == 0)
{
continue;
}
stmt.setString(i, m.getAttribute().getValue());
i++;
}
stmt.setLong(i, uid);
stmt.executeUpdate();
stmt.close();
}
else
{
throw new IllegalArgumentException("Unknown entry type: " + oc);
}
}
/**
* Delete a full "entry" from the database, corresponding to the LDAP
* {@link Entry} that is passed in. This method may perform multiple deletes
* or updates if necessary to fully delete the entry from the database.
* <p>
* This method <b>must be thread safe</b>, as it will be called repeatedly and
* concurrently by the Sync Pipe worker threads as they process DELETE
* operations.
* @param ctx
* a TransactionContext which provides a valid JDBC connection to the
* database.
* @param fetchedDestEntry
* the LDAP entry which corresponds to the database "entry" to delete
* @param operation
* the sync operation for this change
* @throws SQLException
* if there is an error deleting the entry
*/
@Override
public void deleteEntry(final TransactionContext ctx,
final Entry fetchedDestEntry,
final SyncOperation operation)
throws SQLException
{
Attribute oc = fetchedDestEntry.getObjectClassAttribute();
if(ScriptUtils.containsAnyValue(oc, "iNetOrgPerson"))
{
long uid = Long.valueOf(fetchedDestEntry.getAttributeValue("uid"));
PreparedStatement stmt = ctx.prepareStatement("DELETE FROM " +
DATA_TABLE + " WHERE uid = ?");
stmt.setLong(1, uid);
stmt.executeUpdate();
stmt.close();
}
else
{
throw new IllegalArgumentException("Unknown entry type: " +
Arrays.toString(oc.getValues()));
}
}
}
|