Implementing the Operation Interfaces
Important
Connectors continue to be released outside the IDM release. For the latest documentation, refer to the ICF documentation.
The SPI provides several operations. The subset of operations that you implement will depend on the target resource to which you are connecting. Each operation interface defines an action that the connector can perform on the target resource.
The following sections describe the operation interfaces that are provided by the SPI, and provide examples of how they can be implemented in your connector. The sections include the API- and SPI-level rules for each operation.
Authenticate Operation
The authenticate operation authenticates an object on the target system, based on two parameters, usually a unique identifier (username) and a password. If possible, your connector should try to authenticate these credentials natively.
If authentication fails, the connector should throw a runtime exception. The exception must be an IllegalArgumentException
or, if a native exception is available and is of type RuntimeException
, that native runtime exception. If the native exception is not a RuntimeException
, it should be wrapped in a RuntimeException
, and then thrown.
The exception should provide as much detail as possible for logging problems and failed authentication attempts. Several exceptions are provided in the exceptions
package, for this purpose. For example, one of the most common authentication exceptions is the InvalidPasswordException
.
For more information about the common exceptions provided in the OpenICF framework, see "Common Exceptions".
Using the ICF Authenticate Operation
This section shows how your application can use the framework's authentication
operation, and how to write a unit test for this operation, when you are developing your connector.
The authentication
operation throws a RuntimeException
if the credentials do not pass authentication, otherwise returns the UID
.
@Test public void authenticateTest() { logger.info("Running Authentication Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); final OperationOptionsBuilder builder = new OperationOptionsBuilder(); Uid uid = facade.authenticate(ObjectClass.ACCOUNT, "username", new GuardedString("Passw0rd" .toCharArray()), builder.build()); Assert.assertEquals(uid.getUidValue(), "username"); }
Implementing the Authenticate Operation in Your Connector
To implement the authenticate
operation in your connector, add the AuthenticateOp
interface to your connector class, for example:
@ConnectorClass( displayNameKey = "Sample.connector.display", configurationClass = SampleConfiguration.class) public class SampleConnector implements Connector, AuthenticateOp...
For more information, see the AuthenticateOp JavaDoc.
The SPI provides the following detailed exceptions:
UnknownUidException - the UID does not exist on the resource
(org.identityconnectors.framework.common.exceptions.UnknownUidException)
ConnectorSecurityException - base exception for all security-related exceptions
(org.identityconnectors.framework.common.exceptions.ConnectorSecurityException)
InvalidCredentialException - generic invalid credential exception that should be used if the specific error cannot be obtained
(org.identityconnectors.framework.common.exceptions.UnknownUidException)
InvalidPasswordException - the password provided is incorrect
(org.identityconnectors.framework.common.exceptions.InvalidPasswordException)
PasswordExpiredException - the password is correct, but has expired
(org.identityconnectors.framework.common.exceptions.PasswordExpiredException)
PermissionDeniedException - the user can be identified but does not have permission to authenticate
(org.identityconnectors.framework.common.exceptions.PermissionDeniedException)
public Uid authenticate(final ObjectClass objectClass, final String userName, final GuardedString password, final OperationOptions options) { if (ObjectClass.ACCOUNT.equals(objectClass)) { return new Uid(userName); } else { logger.warn("Authenticate of type {0} is not supported", configuration .getConnectorMessages().format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); throw new UnsupportedOperationException("Authenticate of type" + objectClass.getObjectClassValue() + " is not supported"); } }
Create Operation
The create operation interface enables the connector to create objects on the target system. The operation includes one method (create()
). The method takes an ObjectClass
, and any provided attributes, and creates the object and its UID. The connector must return the UID so that the caller can refer to the created object.
The connector should make a best effort to create the object, and should throw an informative RuntimeException
, indicating to the caller why the operation could not be completed. Defaults can be used for any required attributes, as long as the defaults are documented.
The UID is never passed in with the attribute set for this method. If the resource supports a mutable UID, you can create a resource-specific attribute for the ID, such as unix_uid
.
If the create
operation is only partially successful, the connector should attempt to roll back the partial change. If the target system does not allow this, the connector should report the partial success of the create operation and throw a RetryableException
. For example:
public static RetryableException wrap(final String message, final Uid uid) { return new RetryableException(message, new AlreadyExistsException().initUid(Assertions .nullChecked(uid, "Uid"))); }
Using the ICF Create Operation
The following exceptions are thrown by the Create API operation:
IllegalArgumentException
- ifObjectClass}
is missing, or if elements of the set produce duplicate values ofAttribute#getName()
NullPointerException
- if thecreateAttributes
parameter isnull
RuntimeException
- if theConnector
SPI throws a native exception
@Test public void createTest() { logger.info("Running Create Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); final OperationOptionsBuilder builder = new OperationOptionsBuilder(); Set<Attribute> createAttributes = new HashSet<Attribute>(); createAttributes.add(new Name("Foo")); createAttributes.add(AttributeBuilder.buildPassword("Password".toCharArray())); createAttributes.add(AttributeBuilder.buildEnabled(true)); Uid uid = facade.create(ObjectClass.ACCOUNT, createAttributes, builder.build()); Assert.assertEquals(uid.getUidValue(), "foo"); }
Implementing the Create Operation in Your Connector
The SPI provides the following detailed exceptions:
UnsupportedOperationException
- the create operation is not supported for the specified object classInvalidAttributeValueException
- a required attribute is missing, an attribute is present that cannot be created, or a provided attribute has an invalid valueAlreadyExistsException
- an object with the specifiedName
already exits on the target systemPermissionDeniedException
- the target resource will not allow the connector to perform the specified operationConnectorIOException, ConnectionBrokenException, ConnectionFailedException
- a problem as occurred with the connectionRuntimeException
- thrown if anything else goes wrong. You should try to throw a native exception in this case.
public Uid create(final ObjectClass objectClass, final Set<Attribute> createAttributes, final OperationOptions options) { if (ObjectClass.ACCOUNT.equals(objectClass) || ObjectClass.GROUP.equals(objectClass)) { Name name = AttributeUtil.getNameFromAttributes(createAttributes); if (name != null) { // do real create here return new Uid(AttributeUtil.getStringValue(name).toLowerCase()); } else { throw new InvalidAttributeValueException("Name attribute is required"); } } else { logger.warn("Delete of type {0} is not supported", configuration.getConnectorMessages() .format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); throw new UnsupportedOperationException("Delete of type" + objectClass.getObjectClassValue() + " is not supported"); } }
Delete Operation
The delete operation interface enables the connector to delete an object on the target system. The operation includes one method (delete()
). The method takes an ObjectClass
, a Uid
, and any operation options.
The connector should call the native delete methods to remove the object, specified by its unique ID.
Using the ICF Delete Operation
The following exceptions are thrown by the Delete API operation:
UnknownUidException
- the UID does not exist on the resource
@Test public void deleteTest() { logger.info("Running Delete Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); final OperationOptionsBuilder builder = new OperationOptionsBuilder(); facade.delete(ObjectClass.ACCOUNT, new Uid("username"), builder.build()); }
Implementing the Delete Operation in Your Connector
public void delete(final ObjectClass objectClass, final Uid uid, final OperationOptions options) { if (ObjectClass.ACCOUNT.equals(objectClass) || ObjectClass.GROUP.equals(objectClass)) { // do real delete here } else { logger.warn("Delete of type {0} is not supported", configuration.getConnectorMessages() .format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); throw new UnsupportedOperationException("Delete of type" + objectClass.getObjectClassValue() + " is not supported"); } }
Resolve Username Operation
The resolve username operation enables the connector to resolve an object to its UID, based on its username. This operation is similar to the simple authentication operation. However, the resolve username operation does not include a password parameter, and does not attempt to authenticate the credentials. Instead, it returns the UID that corresponds to the supplied username.
The implementation must, however, validate the username (that is, the connector must throw an exception if the username does not correspond to an existing object). If the username validation fails, the the connector should throw a runtime exception, either an IllegalArgumentException
or, if a native exception is available and is of type RuntimeException
, simply throw that exception. If the native exception is not a RuntimeException
, it should be wrapped in a RuntimeException
, and then thrown.
The exception should provide as much detail as possible for logging problems and failed attempts. Several exceptions are provided in the exceptions
package, for this purpose. For example, one of the most common exceptions is the UnknownUidException
.
Using the ICF Resolve Username Operation
The operation throws a RuntimeException
if the username validation fails, otherwise returns the UID
.
@Test public void resolveUsernameTest() { logger.info("Running ResolveUsername Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); final OperationOptionsBuilder builder = new OperationOptionsBuilder(); Uid uid = facade.resolveUsername(ObjectClass.ACCOUNT, "username", builder.build()); Assert.assertEquals(uid.getUidValue(), "username"); }
Implementing the Resolve Username Operation in Your Connector
The SPI provides the following detailed exceptions:
UnknownUidException - the UID does not exist on the resource
public Uid resolveUsername(final ObjectClass objectClass, final String userName, final OperationOptions options) { if (ObjectClass.ACCOUNT.equals(objectClass)) { return new Uid(userName); } else { logger.warn("ResolveUsername of type {0} is not supported", configuration .getConnectorMessages().format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); throw new UnsupportedOperationException("ResolveUsername of type" + objectClass.getObjectClassValue() + " is not supported"); } }
Schema Operation
The Schema Operation interface enables the connector to describe the types of objects that it can handle on the target system, and the operations and options that the connector supports for each object type.
The operation has one method, schema()
, which returns the types of objects on the target system that the connector supports. The method should return the object class name, its description, and a set of attribute definitions.
The implementation of this operation includes a mapping between the native object class and the corresponding connector object. The special Uid
attribute should not be returned, because it is not a true attribute of the object, but a reference to it. For more information about special attributes in ICF, see "ICF Special Attributes".
If your resource object class has a writable unique ID attribute that is different to its Name
, your schema should contain a resource-specific attribute that represents this unique ID. For example, a Unix account object might contain a unix_uid
.
Using the ICF Schema Operation
@Test public void schemaTest() { logger.info("Running Schema Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); Schema schema = facade.schema(); Assert.assertNotNull(schema.findObjectClassInfo(ObjectClass.ACCOUNT_NAME)); }
Implementing the Schema Operation in Your Connector
public Schema schema() { if (null == schema) { final SchemaBuilder builder = new SchemaBuilder(BasicConnector.class); // Account ObjectClassInfoBuilder accountInfoBuilder = new ObjectClassInfoBuilder(); accountInfoBuilder.addAttributeInfo(Name.INFO); accountInfoBuilder.addAttributeInfo(OperationalAttributeInfos.PASSWORD); accountInfoBuilder.addAttributeInfo(PredefinedAttributeInfos.GROUPS); accountInfoBuilder.addAttributeInfo(AttributeInfoBuilder.build("firstName")); accountInfoBuilder.addAttributeInfo(AttributeInfoBuilder.define("lastName") .setRequired(true).build()); builder.defineObjectClass(accountInfoBuilder.build()); // Group ObjectClassInfoBuilder groupInfoBuilder = new ObjectClassInfoBuilder(); groupInfoBuilder.setType(ObjectClass.GROUP_NAME); groupInfoBuilder.addAttributeInfo(Name.INFO); groupInfoBuilder.addAttributeInfo(PredefinedAttributeInfos.DESCRIPTION); groupInfoBuilder.addAttributeInfo(AttributeInfoBuilder.define("members").setCreatable( false).setUpdateable(false).setMultiValued(true).build()); // Only the CRUD operations builder.defineObjectClass(groupInfoBuilder.build(), CreateOp.class, SearchOp.class, UpdateOp.class, DeleteOp.class); // Operation Options builder.defineOperationOption(OperationOptionInfoBuilder.buildAttributesToGet(), SearchOp.class); // Support paged Search builder.defineOperationOption(OperationOptionInfoBuilder.buildPageSize(), SearchOp.class); builder.defineOperationOption(OperationOptionInfoBuilder.buildPagedResultsCookie(), SearchOp.class); // Support to execute operation with provided credentials builder.defineOperationOption(OperationOptionInfoBuilder.buildRunWithUser()); builder.defineOperationOption(OperationOptionInfoBuilder.buildRunWithPassword()); schema = builder.build(); } return schema; }
Script On Connector Operation
The script on connector operation runs a script in the environment of the connector. This is different to the script on resource operation, which runs a script on the target resource that the connector manages.
The corresponding API operation (scriptOnConnectorApiOp
) provides a minimum contract to which the connector must adhere. (See the javadoc for more information). If you do not implement the scriptOnConnector
interface in your connector, the framework provides a default implementation. If you intend your connector to provide more to the script than what is required by this minimum contract, you must implement the scriptOnConnectorOp
interface.
Using the ICF Script on Connector Operation
The API operation allows an application to run a script in the context of any connector.
This operation runs the script in the same JVM or .Net Runtime as the connector. That is, if you are using a local framework, the script runs in your JVM. If you are connected to a remote framework, the script runs in the remote JVM or .Net Runtime.
@Test public void runScriptOnConnectorTest() { logger.info("Running RunScriptOnConnector Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); final OperationOptionsBuilder builder = new OperationOptionsBuilder(); builder.setRunAsUser("admin"); builder.setRunWithPassword(new GuardedString("Passw0rd".toCharArray())); final ScriptContextBuilder scriptBuilder = new ScriptContextBuilder("Groovy", "return argument"); scriptBuilder.addScriptArgument("argument", "value"); Object result = facade.runScriptOnConnector(scriptBuilder.build(), builder.build()); Assert.assertEquals(result, "value"); }
Implementing the Script on Connector Operation in Your Connector
The scriptOnConnector
SPI operation takes the following parameters:
request
- the script and the arguments to be runoptions
- additional options that control how the script is run
The operation returns the result of the script. The return type must be a type that the framework supports for serialization. See the ObjectSerializerFactory javadoc for a list of supported return types.
public Object runScriptOnConnector(ScriptContext request, OperationOptions options) { final ScriptExecutorFactory factory = ScriptExecutorFactory.newInstance(request.getScriptLanguage()); final ScriptExecutor executor = factory.newScriptExecutor(getClass().getClassLoader(), request.getScriptText(), true); if (StringUtil.isNotBlank(options.getRunAsUser())) { String password = SecurityUtil.decrypt(options.getRunWithPassword()); // Use these to execute the script with these credentials } try { return executor.execute(request.getScriptArguments()); } catch (Throwable e) { logger.warn(e, "Failed to execute Script"); throw ConnectorException.wrap(e); } }
Script On Resource Operation
The script on resource operation runs a script directly on the target resource (unlike the "Script On Connector Operation", which runs a script in the context of a specific connector.)
Implement this interface if your connector intends to support the ScriptOnResourceApiOp
API operation. If your connector implements this interface, you must document the script languages that the connector supports, as well as any supported OperationOptions
.
Using the ICF Script on Resource Operation
The contract at the API level is intentionally very loose. Each connector decides what script languages it supports, what running a script on a target resource actually means, and what script options (if any) the connector supports.
@Test public void runScriptOnResourceTest() { logger.info("Running RunScriptOnResource Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); final OperationOptionsBuilder builder = new OperationOptionsBuilder(); builder.setRunAsUser("admin"); builder.setRunWithPassword(new GuardedString("Passw0rd".toCharArray())); final ScriptContextBuilder scriptBuilder = new ScriptContextBuilder("bash", "whoami"); Object result = facade.runScriptOnResource(scriptBuilder.build(), builder.build()); Assert.assertEquals(result, "admin"); }
Implementing the Script on Resource Operation in Your Connector
The scriptOnResource
SPI operation takes the following parameters:
request
- the script and the arguments to be runoptions
- additional options that control how the script is run
The operation returns the result of the script. The return type must be a type that the framework supports for serialization. See the ObjectSerializerFactory javadoc for a list of supported return types.
public Object runScriptOnResource(ScriptContext request, OperationOptions options) { try { // Execute the script on remote resource if (StringUtil.isNotBlank(options.getRunAsUser())) { String password = SecurityUtil.decrypt(options.getRunWithPassword()); // Use these to execute the script with these credentials return options.getRunAsUser(); } throw new UnknownHostException("Failed to connect to remote SSH"); } catch (Throwable e) { logger.warn(e, "Failed to execute Script"); throw ConnectorException.wrap(e); } }
Search Operation
The search operation enables the connector to search for objects on the target system.
The ICF framework handles searches as follows:
The application sends a query, with a search filter, to the OpenICF framework
The framework submits the query, with the filter, to the connector
The connector implements the
createFilterTranslator()
method to obtain aFilterTranslator
objectThe framework then uses this
FilterTranslator
object to transform the filter to a format that theexecuteQuery()
method expects
You can implement the FilterTranslator
object in two ways:
The
FilterTranslator
translates the original filter into one or more native queries.The framework then calls the
executeQuery()
method for each native query.The
FilterTranslator
does not modify the original filter.The framework then calls the
executeQuery()
method with the original ICF filter.Using this second approach enables your connector to distinguish between a search and a get operation and to benefit from the visitor design pattern.
Based on the resultsHandlerConfiguration
, the OpenICF framework can perform additional filtering on the returning results. For more information on the resultsHandlerConfiguration
, see Results Handler Configuration.
The connector facade calls the executeQuery
method once for each native query that the filter translator produces. If the filter translator produces more than one native query, the connector facade merges the results from each query and eliminates any duplicates.
Note that this implies an in-memory data structure that holds a set of UID values. Memory usage, in the event of multiple queries, will be O(N) where N is the number of results. It is therefore important that the filter translator for the connector implement OR
operators, if possible.
Whether the application calls a get
API operation, or a search
API operation, the ICF framework translates that request to a search
request on the connector.
Using the ICF Get Operation
The GetApiOp
returns null
when the UID does not exist on the resource.
@Test public void getObjectTest() { logger.info("Running GetObject Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); final OperationOptionsBuilder builder = new OperationOptionsBuilder(); builder.setAttributesToGet(Name.NAME); ConnectorObject co = facade.getObject(ObjectClass.ACCOUNT, new Uid( "3f50eca0-f5e9-11e3-a3ac-0800200c9a66"), builder.build()); Assert.assertEquals(co.getName().getNameValue(), "Foo"); }
Using the ICF Search Operation
@Test public void searchTest() { logger.info("Running Search Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); final OperationOptionsBuilder builder = new OperationOptionsBuilder(); builder.setPageSize(10); final ResultsHandler handler = new ToListResultsHandler(); SearchResult result = facade.search(ObjectClass.ACCOUNT, FilterBuilder.equalTo(new Name("Foo")), handler, builder.build()); Assert.assertEquals(result.getPagedResultsCookie(), "0"); Assert.assertEquals(((ToListResultsHandler) handler).getObjects().size(), 1); }
Implementing the Search Operation in Your Connector
public FilterTranslator<String> createFilterTranslator(ObjectClass objectClass, OperationOptions options) { return new BasicFilterTranslator(); } public void executeQuery(ObjectClass objectClass, String query, ResultsHandler handler, OperationOptions options) { final ConnectorObjectBuilder builder = new ConnectorObjectBuilder(); builder.setUid("3f50eca0-f5e9-11e3-a3ac-0800200c9a66"); builder.setName("Foo"); builder.addAttribute(AttributeBuilder.buildEnabled(true)); for (ConnectorObject connectorObject : CollectionUtil.newSet(builder.build())) { if (!handler.handle(connectorObject)) { // Stop iterating because the handler stopped processing break; } } if (options.getPageSize() != null && 0 < options.getPageSize()) { logger.info("Paged Search was requested"); ((SearchResultsHandler) handler).handleResult(new SearchResult("0", 0)); } }
Sync Operation
The sync operation polls the target system for synchronization events, that is, native changes to target objects.
The operation has two methods:
sync()
- request synchronization events from the target systemThis method calls the specified handler, once, to pass back each matching synchronization event. When the method returns, it will no longer invoke the specified handler.
getLatestSyncToken()
- returns the token corresponding to the most recent synchronization event
Using the ICF Sync Operation
getLatestSyncToken()
Method), at the API Level@Test public void getLatestSyncTokenTest() { logger.info("Running GetLatestSyncToken Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); SyncToken token = facade.getLatestSyncToken(ObjectClass.ACCOUNT); Assert.assertEquals(token.getValue(), 10); }
The getLatestSyncToken
method throws an IllegalArgumentException
if the objectClass
is null or invalid.
sync()
Method), at the API Level@Test public void syncTest() { logger.info("Running Sync Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); final OperationOptionsBuilder builder = new OperationOptionsBuilder(); builder.setPageSize(10); final SyncResultsHandler handler = new SyncResultsHandler() { public boolean handle(SyncDelta delta) { return false; } }; SyncToken token = facade.sync(ObjectClass.ACCOUNT, new SyncToken(10), handler, builder.build()); Assert.assertEquals(token.getValue(), 10); }
The sync
method throws an IllegalArgumentException
if the objectClass
or handler
is null, or if any argument is invalid.
Implementing the Sync Operation in Your Connector
public void sync(ObjectClass objectClass, SyncToken token, SyncResultsHandler handler, final OperationOptions options) { if (ObjectClass.ALL.equals(objectClass)) { // } else if (ObjectClass.ACCOUNT.equals(objectClass)) { final ConnectorObjectBuilder builder = new ConnectorObjectBuilder(); builder.setUid("3f50eca0-f5e9-11e3-a3ac-0800200c9a66"); builder.setName("Foo"); builder.addAttribute(AttributeBuilder.buildEnabled(true)); final SyncDeltaBuilder deltaBuilder = new SyncDeltaBuilder(); deltaBuilder.setObject(builder.build()); deltaBuilder.setDeltaType(SyncDeltaType.CREATE); deltaBuilder.setToken(new SyncToken(10)); for (SyncDelta connectorObject : CollectionUtil.newSet(deltaBuilder.build())) { if (!handler.handle(connectorObject)) { // Stop iterating because the handler stopped processing break; } } } else { logger.warn("Sync of type {0} is not supported", configuration.getConnectorMessages() .format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); throw new UnsupportedOperationException("Sync of type" + objectClass.getObjectClassValue() + " is not supported"); } ((SyncTokenResultsHandler) handler).handleResult(new SyncToken(10)); } public SyncToken getLatestSyncToken(ObjectClass objectClass) { if (ObjectClass.ACCOUNT.equals(objectClass)) { return new SyncToken(10); } else { logger.warn("Sync of type {0} is not supported", configuration.getConnectorMessages() .format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); throw new UnsupportedOperationException("Sync of type" + objectClass.getObjectClassValue() + " is not supported"); } }
Test Operation
The test operation tests the connector configuration. Unlike validation, testing a configuration verifies that every part of the environment that is referred to by the configuration is available. The operation therefore validates that the connection details that are provided in the configuration are accurate, and that the backend is accessible when using them.
For example, the connector might make a physical connection to the host that is specified in the configuration, to check that it exists and that the credentials supplied in the configuration are valid.
The test operation can be invoked before the configuration has been validated, or can validate the configuration before testing it.
Using the ICF Test Operation
At the API level, the test operation throws a RuntimeException
if the configuration is not valid, or if the test fails. Your connector implementation should throw the most specific exception available. When no specific exception is available, your connector implementation should throw a ConnectorException
.
@Test public void testTest() { logger.info("Running Test Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); facade.test(); }
Implementing the Test Operation in Your Connector
public void test() { logger.ok("Test works well"); }
Update Operation
If your connector will allow an authorized caller to update (modify or replace) objects on the target system, you must implement either the update operation, or the "Update Attribute Values Operation". At the API level update operation calls either the UpdateOp
or the UpdateAttributeValuesOp
, depending on what you have implemented.
The update operation is somewhat simpler to implement than the "Update Attribute Values Operation", because the update attribute values operation must handle any type of update that the caller might specify. However a true implementation of the update attribute values operation offers better performance and atomicity semantics.
Using the ICF Update Operation
At the API level, the update operation returns an UnknownUidException
if the UID does not exist on the target system resource and if the connector does not implement the "Update Attribute Values Operation" interface.
@Test public void updateTest() { logger.info("Running Update Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); final OperationOptionsBuilder builder = new OperationOptionsBuilder(); Set<Attribute> updateAttributes = new HashSet<Attribute>(); updateAttributes.add(new Name("Foo")); Uid uid = facade.update(ObjectClass.ACCOUNT, new Uid("Foo"), updateAttributes, builder .build()); Assert.assertEquals(uid.getUidValue(), "foo"); }
Implementing the Update Operation in Your Connector
At the SPI level, the update operation returns an UnknownUidException
if the UID does not exist on the target system.
public Uid update(ObjectClass objectClass, Uid uid, Set<Attribute> replaceAttributes, OperationOptions options) { AttributesAccessor attributesAccessor = new AttributesAccessor(replaceAttributes); Name newName = attributesAccessor.getName(); Uid uidAfterUpdate = uid; if (newName != null) { logger.info("Rename the object {0}:{1} to {2}", objectClass.getObjectClassValue(), uid .getUidValue(), newName.getNameValue()); uidAfterUpdate = new Uid(newName.getNameValue().toLowerCase()); } if (ObjectClass.ACCOUNT.equals(objectClass)) { } else if (ObjectClass.GROUP.is(objectClass.getObjectClassValue())) { if (attributesAccessor.hasAttribute("members")) { throw new InvalidAttributeValueException( "Requested to update a read only attribute"); } } else { logger.warn("Update of type {0} is not supported", configuration.getConnectorMessages() .format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); throw new UnsupportedOperationException("Update of type" + objectClass.getObjectClassValue() + " is not supported"); } return uidAfterUpdate; }
Suggested Approach for Deleting Attributes and Removing Attribute Values
If the target resource to which you are connecting supports the removal of attributes, you can implement the removal in several ways. All the samples in this document assume the following syntax rules for deleting attributes or removing their values.
Update | Syntax rule | Query filter |
---|---|---|
Set an empty attribute value | [""] (application sends an attribute value that is a list containing one empty string) | equal="" |
Set an attribute value to null | [] (application sends an attribute value that is an empty list) | ispresent search returns 1 |
Removing an attribute | null (application sends an attribute value that is null | ispresent search returns 1 |
Update Attribute Values Operation
The update attribute values operation is an advanced implementation of the update operation. You should implement this operation if you want your connector to offer better performance and atomicity for the following methods:
UpdateApiOp.addAttributeValues(ObjectClass, Uid, Set, OperationOptions)
UpdateApiOp.removeAttributeValues(ObjectClass, Uid, Set, OperationOptions)
@Test public void addAttributeValuesTest() { logger.info("Running AddAttributeValues Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); final OperationOptionsBuilder builder = new OperationOptionsBuilder(); Set<Attribute> updateAttributes = new HashSet<Attribute>(); // add 'group2' to existing groups updateAttributes.add(AttributeBuilder.build(PredefinedAttributes.GROUPS_NAME, "group2")); Uid uid = facade.addAttributeValues(ObjectClass.ACCOUNT, new Uid("Foo"), updateAttributes, builder.build()); Assert.assertEquals(uid.getUidValue(), "foo"); } @Test public void removeAttributeValuesTest() { logger.info("Running RemoveAttributeValues Test"); final ConnectorFacade facade = createConnectorFacade(BasicConnector.class, null); final OperationOptionsBuilder builder = new OperationOptionsBuilder(); Set<Attribute> updateAttributes = new HashSet<Attribute>(); // remove 'group2' from existing groups updateAttributes.add(AttributeBuilder.build(PredefinedAttributes.GROUPS_NAME, "group2")); Uid uid = facade.removeAttributeValues(ObjectClass.ACCOUNT, new Uid("Foo"), updateAttributes, builder.build()); Assert.assertEquals(uid.getUidValue(), "foo"); }
Implementing the Update Attribute Values Operation in Your Connector
At the SPI level, the update attribute values operation returns an UnknownUidException
when the UID does not exist on the resource.
public Uid addAttributeValues(ObjectClass objectClass, Uid uid, Set<Attribute> valuesToAdd, OperationOptions options) { return uid; } public Uid removeAttributeValues(ObjectClass objectClass, Uid uid, Set<Attribute> valuesToRemove, OperationOptions options) { return uid; }