/* * 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 * * * Portions Copyright 2010-2023 Ping Identity Corporation */ 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())); } } }