/*
* DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2012 ForgeRock Inc. All rights reserved.
*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at
* http://forgerock.org/license/CDDLv1.0.html
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at http://forgerock.org/license/CDDLv1.0.html
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*/
package org.forgerock.openicf.connectors.sap;
import com.sap.conn.jco.JCoContext;
import com.sap.conn.jco.JCoException;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.common.script.ScriptExecutor;
import org.identityconnectors.common.script.ScriptExecutorFactory;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeBuilder;
import org.identityconnectors.framework.common.objects.AttributeUtil;
import org.identityconnectors.framework.common.objects.ConnectorObjectBuilder;
import org.identityconnectors.framework.common.objects.ObjectClass;
import org.identityconnectors.framework.common.objects.OperationOptions;
import org.identityconnectors.framework.common.objects.OperationalAttributes;
import org.identityconnectors.framework.common.objects.ResultsHandler;
import org.identityconnectors.framework.common.objects.Schema;
import org.identityconnectors.framework.common.objects.SchemaBuilder;
import org.identityconnectors.framework.common.objects.ScriptContext;
import org.identityconnectors.framework.common.objects.SyncResultsHandler;
import org.identityconnectors.framework.common.objects.SyncToken;
import org.identityconnectors.framework.common.objects.Uid;
import org.identityconnectors.framework.common.objects.filter.FilterTranslator;
import org.identityconnectors.framework.spi.Configuration;
import org.identityconnectors.framework.spi.Connector;
import org.identityconnectors.framework.spi.ConnectorClass;
import org.identityconnectors.framework.spi.operations.CreateOp;
import org.identityconnectors.framework.spi.operations.DeleteOp;
import org.identityconnectors.framework.spi.operations.SchemaOp;
import org.identityconnectors.framework.spi.operations.ScriptOnConnectorOp;
import org.identityconnectors.framework.spi.operations.SearchOp;
import org.identityconnectors.framework.spi.operations.SyncOp;
import org.identityconnectors.framework.spi.operations.TestOp;
import org.identityconnectors.framework.spi.operations.UpdateAttributeValuesOp;
import org.identityconnectors.framework.spi.operations.UpdateOp;
/**
* Main implementation of the SAP Connector
*
* @author Gael Allioux <gael.allioux@forgerock.com>
* @version $Revision$ $Date$
*/
@ConnectorClass(displayNameKey = "SAP.connector.display", configurationClass = SAPConfiguration.class)
public class SAPConnector implements
Connector, CreateOp, DeleteOp, SchemaOp, ScriptOnConnectorOp, SearchOp<Map>, SyncOp, TestOp, UpdateAttributeValuesOp, UpdateOp {
/**
* Setup logging for the {@link SAPConnector}.
*/
private static final Log log = Log.getLog(SAPConnector.class);
/**
* Place holder for the Connection created in the init method
*/
private SAPConnection connection;
private Schema schema;
/**
* Place holder for the {@link Configuration} passed into the init() method
* {@link SAPConnector#init(org.identityconnectors.framework.spi.Configuration)}.
*/
private SAPConfiguration configuration = null;
private ScriptExecutorFactory factory = null;
private ScriptExecutor createExecutor = null;
private ScriptExecutor updateExecutor = null;
private ScriptExecutor deleteExecutor = null;
private ScriptExecutor searchExecutor = null;
private ScriptExecutor searchAllExecutor = null;
private ScriptExecutor syncExecutor = null;
private ScriptExecutor runOnConnectorExecutor = null;
private ScriptExecutor schemaExecutor = null;
private ScriptExecutor testExecutor = null;
/**
* Gets the Configuration context for this connector.
*/
@Override
public Configuration getConfiguration() {
return this.configuration;
}
/**
* Callback method to receive the {@link Configuration}.
*
* @see
* org.identityconnectors.framework.spi.Connector#init(org.identityconnectors.framework.spi.Configuration)
*/
@Override
public void init(Configuration configuration1) {
this.configuration = (SAPConfiguration) configuration1;
this.connection = new SAPConnection(this.configuration);
this.factory = ScriptExecutorFactory.newInstance(this.configuration.getScriptingLanguage());
// We need an executor for each and every script. At least, they'll get
// evaluated and compiled.
// This is a bit expensive.
// TODO: lazy loading of scripts on demand.
createExecutor = getScriptExecutor(configuration.getCreateScriptFileName());
updateExecutor = getScriptExecutor(configuration.getUpdateScriptFileName());
deleteExecutor = getScriptExecutor(configuration.getDeleteScriptFileName());
searchExecutor = getScriptExecutor(configuration.getSearchScriptFileName());
searchAllExecutor = getScriptExecutor(configuration.getSearchAllScriptFileName());
syncExecutor = getScriptExecutor(configuration.getSyncScriptFileName());
schemaExecutor = getScriptExecutor(configuration.getSchemaScriptFileName());
testExecutor = getScriptExecutor(configuration.getTestScriptFileName());
}
/**
* Disposes of the {@link SAPConnector}'s resources.
*
* @see org.identityconnectors.framework.spi.Connector#dispose()
*/
@Override
public void dispose() {
configuration = null;
if (connection != null) {
connection.dispose();
connection = null;
}
}
//
// /**
// * {@inheritDoc}
// */
// @Override
// public void checkAlive() {
// connection.test();
// }
/**
* ****************
* SPI Operations
*
* Implement the following operations using the contract and description
* found in the Javadoc for these methods.
*/
/**
* {@inheritDoc}
*/
@Override
public Schema schema() {
SchemaBuilder scmb = new SchemaBuilder(SAPConnector.class);
if (configuration.isReloadScriptOnExecution()) {
schemaExecutor = getScriptExecutor(configuration.getSchemaScriptFileName());
}
if (schemaExecutor != null) {
Map<String, Object> arguments = new HashMap<String, Object>();
arguments.put("action", "SCHEMA");
arguments.put("log", log);
arguments.put("builder", scmb);
try {
schemaExecutor.execute(arguments);
} catch (Exception e) {
throw new ConnectorException("Schema script error", e);
}
} else {
throw new UnsupportedOperationException("SCHEMA script executor is null. Problem loading Schema script");
}
schema = scmb.build();
return schema;
}
/**
* {@inheritDoc}
*/
@Override
public FilterTranslator<Map> createFilterTranslator(ObjectClass objectClass, OperationOptions options) {
if (objectClass == null) {
throw new IllegalArgumentException("ObjectClass required");
}
return new SAPFilterTranslator();
}
/**
* {@inheritDoc}
*/
@Override
public void executeQuery(ObjectClass objectClass, Map query, ResultsHandler handler, OperationOptions options) {
ScriptExecutor executor = null;
String action = "SEARCH";
if (configuration.isReloadScriptOnExecution()) {
searchExecutor = getScriptExecutor(configuration.getSearchScriptFileName());
searchAllExecutor = getScriptExecutor(configuration.getSearchAllScriptFileName());
}
if (query == null) {
executor = searchAllExecutor;
action = "SEARCHALL";
} else {
executor = searchExecutor;
}
if (executor != null) {
Map<String, Object> arguments = new HashMap<String, Object>();
try {
arguments.put("repository", connection.getDestination().getRepository());
} catch (JCoException jcoe) {
log.error("Can't pass the repository to Groovy script");
}
arguments.put("destination", connection.getDestination());
arguments.put("objectClass", objectClass.getObjectClassValue());
arguments.put("action", action);
arguments.put("log", log);
arguments.put("options", options.getOptions());
arguments.put("query", query);
try {
List<Map> results = (List<Map>) executor.execute(arguments);
processResults(objectClass, results, handler);
} catch (Exception e) {
throw new ConnectorException(action + " script error", e);
}
} else {
throw new UnsupportedOperationException(action + "script executor is null. Problem loading "+action+" script");
}
}
/**
* {@inheritDoc}
*/
@Override
public Uid create(final ObjectClass objectClass, final Set<Attribute> createAttributes, final OperationOptions options) {
if (configuration.isReloadScriptOnExecution()) {
createExecutor = getScriptExecutor(configuration.getCreateScriptFileName());
}
if (createExecutor != null) {
if (createAttributes == null || createAttributes.isEmpty()) {
throw new IllegalArgumentException("Create Attributes required");
}
final Map<String, Object> arguments = new HashMap<String, Object>();
try {
arguments.put("repository", connection.getDestination().getRepository());
} catch (JCoException jcoe) {
log.error("Can't pass the repository to Groovy script");
}
arguments.put("destination", connection.getDestination());
arguments.put("action", "CREATE");
arguments.put("log", log);
arguments.put("objectClass", objectClass.getObjectClassValue());
arguments.put("options", options.getOptions());
// We give the id (name) as an argument, more friendly than dealing with __NAME__
if (AttributeUtil.getNameFromAttributes(createAttributes) != null) {
arguments.put("id", AttributeUtil.getNameFromAttributes(createAttributes).getNameValue());
} else {
throw new IllegalArgumentException("__NAME__ is missing from Create Attributes");
}
Map<String, List> attrMap = new HashMap();
for (Attribute attr : createAttributes) {
attrMap.put(attr.getName(), attr.getValue());
}
// let's get rid of __NAME__
attrMap.remove("__NAME__");
arguments.put("attributes", attrMap);
// Password - if allowed we provide it in clear
if (configuration.getClearTextPasswordToScript()) {
GuardedString gpasswd = AttributeUtil.getPasswordValue(createAttributes);
if (gpasswd != null) {
gpasswd.access(new GuardedString.Accessor() {
@Override
public void access(char[] clearChars) {
arguments.put("password", new String(clearChars));
}
});
} else {
arguments.put("password", null);
}
}
try {
JCoContext.begin(connection.getDestination());
Object uidAfter = createExecutor.execute(arguments);
if (uidAfter instanceof String) {
log.ok("{0} created", uidAfter);
return new Uid((String) uidAfter);
} else {
throw new ConnectorException("Create script didn't return with the __UID__ value");
}
} catch (Exception e) {
throw new ConnectorException("Create script error", e);
} finally {
try {
JCoContext.end(connection.getDestination());
} catch (JCoException jcoe) {
throw new ConnectorException(jcoe);
}
}
} else {
throw new UnsupportedOperationException("CREATE script executor is null. Problem loading Create script");
}
}
/**
* {@inheritDoc}
*/
@Override
public Uid update(ObjectClass objectClass, Uid uid, Set<Attribute> replaceAttributes, OperationOptions options) {
return genericUpdate("UPDATE", objectClass, uid, replaceAttributes, options);
}
/**
* {@inheritDoc}
*/
@Override
public Uid addAttributeValues(ObjectClass objectClass, Uid uid, Set<Attribute> valuesToAdd, OperationOptions options) {
return genericUpdate("ADD_ATTRIBUTE_VALUES", objectClass, uid, valuesToAdd, options);
}
/**
* {@inheritDoc}
*/
@Override
public Uid removeAttributeValues(ObjectClass objectClass, Uid uid, Set<Attribute> valuesToRemove, OperationOptions options) {
return genericUpdate("REMOVE_ATTRIBUTE_VALUES", objectClass, uid, valuesToRemove, options);
}
/**
* {@inheritDoc}
*/
@Override
public void delete(final ObjectClass objectClass,
final Uid uid,
final OperationOptions options) {
if (configuration.isReloadScriptOnExecution()) {
deleteExecutor = getScriptExecutor(configuration.getDeleteScriptFileName());
}
if (deleteExecutor != null) {
if (uid == null || (uid.getUidValue() == null)) {
throw new IllegalArgumentException("Uid required");
}
final String id = uid.getUidValue();
final Map<String, Object> arguments = new HashMap<String, Object>();
try {
arguments.put("repository", connection.getDestination().getRepository());
} catch (JCoException jcoe) {
log.error("Can't pass the repository to Groovy script");
}
arguments.put("destination", connection.getDestination());
arguments.put("action", "DELETE");
arguments.put("log", log);
arguments.put("objectClass", objectClass.getObjectClassValue());
arguments.put("uid", id);
arguments.put("options", options.getOptions());
try {
JCoContext.begin(connection.getDestination());
deleteExecutor.execute(arguments);
log.ok("{0} deleted", id);
} catch (Exception e) {
throw new ConnectorException("Delete script error", e);
} finally {
try {
JCoContext.end(connection.getDestination());
} catch (JCoException jcoe) {
throw new ConnectorException(jcoe);
}
}
} else {
throw new UnsupportedOperationException("DELETE script executor is null. Problem loading Delete script");
}
}
/**
* {@inheritDoc}
*/
@Override
public Object runScriptOnConnector(ScriptContext request, OperationOptions options) {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
public void sync(ObjectClass objectClass, SyncToken token, SyncResultsHandler handler,
final OperationOptions options) {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
public SyncToken getLatestSyncToken(ObjectClass objectClass) {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
@Override
public void test() {
configuration.validate();
if (configuration.isReloadScriptOnExecution()) {
testExecutor = getScriptExecutor(configuration.getTestScriptFileName());
}
if (testExecutor != null) {
Map<String, Object> arguments = new HashMap<String, Object>();
try {
arguments.put("repository", connection.getDestination().getRepository());
} catch (JCoException jcoe) {
log.error("Can't pass the repository to Groovy script");
}
arguments.put("destination", connection.getDestination());
arguments.put("action", "TEST");
arguments.put("log", log);
try {
JCoContext.begin(connection.getDestination());
testExecutor.execute(arguments);
log.ok("Test ok");
} catch (Exception e) {
throw new ConnectorException("Test script error", e);
} finally {
try {
JCoContext.end(connection.getDestination());
} catch (JCoException jcoe) {
throw new ConnectorException(jcoe);
}
}
} else {
throw new UnsupportedOperationException("TEST script executor is null. Problem loading Test script");
}
}
/*
* Private methods
*/
private void processResults(ObjectClass objClass, List<Map> results, ResultsHandler handler) {
// Let's iterate over the results:
for (Map<String, Object> result : results) {
ConnectorObjectBuilder cobld = new ConnectorObjectBuilder();
for (Map.Entry<String, Object> entry : result.entrySet()) {
final String attrName = entry.getKey();
final Object attrValue = entry.getValue();
// Special first
if (attrName.equalsIgnoreCase("__UID__")) {
if (attrValue == null) {
throw new IllegalArgumentException("Uid cannot be null");
}
cobld.setUid(attrValue.toString());
} else if (attrName.equalsIgnoreCase("__NAME__")) {
if (attrValue == null) {
throw new IllegalArgumentException("Name cannot be null");
}
cobld.setName(attrValue.toString());
} else if (attrName.equalsIgnoreCase("password")) {
// is there a chance we fetch password from search?
} else {
if (attrValue instanceof Collection) {
cobld.addAttribute(AttributeBuilder.build(attrName, (Collection) attrValue));
} else if (attrValue != null) {
cobld.addAttribute(AttributeBuilder.build(attrName, attrValue));
} else {
cobld.addAttribute(AttributeBuilder.build(attrName));
}
}
}
cobld.setObjectClass(objClass);
handler.handle(cobld.build());
log.ok("ConnectorObject is built");
}
}
private Uid genericUpdate(String method, ObjectClass objClass, Uid uid, Set<Attribute> attrs, OperationOptions options) {
if (configuration.isReloadScriptOnExecution()) {
updateExecutor = getScriptExecutor(configuration.getUpdateScriptFileName());
}
if (updateExecutor != null) {
if (attrs == null || attrs.isEmpty()) {
throw new IllegalArgumentException("Attribute set required");
}
if (uid == null || (uid.getUidValue() == null)) {
throw new IllegalArgumentException("Uid required");
}
final String id = uid.getUidValue();
final Map<String, Object> arguments = new HashMap<String, Object>();
try {
arguments.put("repository", connection.getDestination().getRepository());
} catch (JCoException jcoe) {
log.error("Can't pass the repository to Groovy script");
}
arguments.put("destination", connection.getDestination());
arguments.put("action", method);
arguments.put("log", log);
arguments.put("objectClass", objClass.getObjectClassValue());
arguments.put("uid", id);
arguments.put("options", options.getOptions());
Map<String, List> attrMap = new HashMap<String, List>();
for (Attribute attr : attrs) {
if (OperationalAttributes.isOperationalAttribute(attr)) {
if (method.equalsIgnoreCase("UPDATE")) {
attrMap.put(attr.getName(), attr.getValue());
}
} else {
attrMap.put(attr.getName(), attr.getValue());
}
}
arguments.put("attributes", attrMap);
// Do we need to update the password?
if (configuration.getClearTextPasswordToScript() && method.equalsIgnoreCase("UPDATE")) {
GuardedString gpasswd = AttributeUtil.getPasswordValue(attrs);
if (gpasswd != null) {
gpasswd.access(new GuardedString.Accessor() {
@Override
public void access(char[] clearChars) {
arguments.put("password", new String(clearChars));
}
});
} else {
arguments.put("password", null);
}
}
try {
JCoContext.begin(connection.getDestination());
Object uidAfter = updateExecutor.execute(arguments);
if (uidAfter instanceof String) {
log.ok("{0} updated ({1})", uidAfter, method);
return new Uid((String) uidAfter);
}
} catch (Exception e) {
throw new ConnectorException("Update(" + method + ") script error", e);
} finally {
try {
JCoContext.end(connection.getDestination());
} catch (JCoException jcoe) {
throw new ConnectorException(jcoe);
}
}
throw new ConnectorException("Update script didn't return with the __UID__ value");
} else {
throw new UnsupportedOperationException("UPDATE script executor is null. Problem loading Update script");
}
}
private String readFile(String filename) {
File file = new File(filename);
StringBuffer contents = new StringBuffer();
BufferedReader reader = null;
String text;
try {
reader = new BufferedReader(new FileReader(file));
while ((text = reader.readLine()) != null) {
contents.append(text).append(System.getProperty("line.separator"));
}
} catch (FileNotFoundException e) {
throw new ConnectorException(filename + " not found", e);
} catch (IOException e) {
throw new ConnectorException(filename, e);
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
throw new ConnectorException(filename, e);
}
}
return contents.toString();
}
private ScriptExecutor getScriptExecutor(String scriptFileName) {
ScriptExecutor scriptExec = null;
String scriptCode = "";
try {
if (scriptFileName != null && scriptFileName.length() > 0) {
scriptCode = readFile(scriptFileName);
}
if (scriptCode.length() > 0) {
scriptExec = factory.newScriptExecutor(getClass().getClassLoader(), scriptCode, true);
log.ok("Script {0} loaded", scriptFileName);
}
} catch (Exception e) {
throw new ConnectorException("Script error", e);
}
return scriptExec;
}
private Map getAttrPerInfotype(OperationOptions opts) {
Map<String, ArrayList<String>> infotypeTables = new HashMap<String, ArrayList<String>>();
java.lang.String[] attrsToGet = opts.getAttributesToGet();
if (attrsToGet != null) {
for (String attr : attrsToGet) {
if (attr.indexOf(":") != -1) {
java.lang.String[] array = attr.split(":");
if (infotypeTables.containsKey(array[0])) {
infotypeTables.get(array[0]).add(array[1]);
} else {
infotypeTables.put(array[0], new ArrayList<String>());
infotypeTables.get(array[0]).add(array[1]);
}
}
}
}
return infotypeTables;
}
}