/*
* 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.groovy;
import java.sql.*;
import com.unboundid.ldap.sdk.*;
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.scripting.ScriptedJDBCSyncDestination;
import com.unboundid.directory.sdk.sync.config.JDBCSyncDestinationConfig;
import com.unboundid.directory.sdk.sync.util.ScriptUtils;
/**
* This class implements the necessary methods to synchronize data to a simple,
* single-table database schema from its LDAP counterpart.
* <p>
* To use this script, place it under
* /lib/groovy-scripted-extensions/com/unboundid/directory/sdk/examples/groovy
* and set the 'script-class' property on the Sync Destination to
* "com.unboundid.directory.sdk.examples.groovy.ExampleJDBCSyncDestination".
*/
public final class ExampleScriptedJDBCSyncDestination extends ScriptedJDBCSyncDestination
{
//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";
/**
* 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.
*/
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"); //may be null
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)
{
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"));
Set<String> allAttrs = new HashSet(["objectclass", "cn", "givenname", "sn", "description"]);
//Compute the set of columns to update
StringBuilder attrsToUpdate = new StringBuilder();
for(Modification m : modsToApply)
{
String attrName = m.getAttributeName().toLowerCase();
if(!allAttrs.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(!allAttrs.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: " + oc);
}
}
}
|