/*
* 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 2013-2015 UnboundID Corp.
*/
package com.unboundid.directory.sdk.examples;
import com.unboundid.directory.sdk.broker.api.StoreAdapter;
import com.unboundid.directory.sdk.broker.config.StoreAdapterConfig;
import com.unboundid.directory.sdk.broker.types.IdentityBrokerContext;
import com.unboundid.directory.sdk.broker.types.StoreCreateRequest;
import com.unboundid.directory.sdk.broker.types.StoreDeleteRequest;
import com.unboundid.directory.sdk.broker.types.StoreSearchRequest;
import com.unboundid.directory.sdk.broker.types.StoreUpdateRequest;
import com.unboundid.directory.sdk.common.types.LogSeverity;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.scim.data.AttributeValueResolver;
import com.unboundid.scim.data.BaseResource;
import com.unboundid.scim.data.Meta;
import com.unboundid.scim.marshal.json.JsonMarshaller;
import com.unboundid.scim.marshal.json.JsonUnmarshaller;
import com.unboundid.scim.schema.ResourceDescriptor;
import com.unboundid.scim.sdk.AttributePath;
import com.unboundid.scim.sdk.Diff;
import com.unboundid.scim.sdk.InvalidResourceException;
import com.unboundid.scim.sdk.PageParameters;
import com.unboundid.scim.sdk.ResourceNotFoundException;
import com.unboundid.scim.sdk.Resources;
import com.unboundid.scim.sdk.SCIMException;
import com.unboundid.scim.sdk.SCIMFilter;
import com.unboundid.scim.sdk.SCIMFilterType;
import com.unboundid.scim.sdk.SCIMObject;
import com.unboundid.scim.sdk.SCIMQueryAttributes;
import com.unboundid.scim.sdk.ServerErrorException;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.Validator;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.FileArgument;
import com.unboundid.util.args.StringArgument;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.URI;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import static com.unboundid.scim.sdk.SCIMConstants.SCHEMA_URI_CORE;
import static com.unboundid.scim.data.BaseResource.BASE_RESOURCE_FACTORY;
import static com.unboundid.scim.schema.CoreSchema.USER_DESCRIPTOR;
/**
* Example implementation of a Dataview StoreAdapter persisting to a Java jdbc
* RDBMS backing store. This adapter depends on a jdbc driver jar which must be
* copied into server's lib directory. The defaults in this example work with
* <a href="http://db.apache.org/derby/releases/release-10.10.1.1.cgi">Apache
* derby 10.10.1.1</a>
*/
public class ExampleJDBCStoreAdapter extends StoreAdapter
{
/**
* The name of the argument that will be used to load the jdbc driver class.
*/
public static final String ARG_NAME_JDBC_DRIVER = "jdbc-driver-class";
/**
* The name of the argument that will be used to specify the jdbc url to the
* database instance used by this adapter.
*/
public static final String ARG_NAME_JDBC_URL = "jdbc-url";
/**
* The name of the argument that will be used to specify the file name to
* initialize the database schema used by this adapter.
*/
public static final String ARG_NAME_INIT_SQL_SCHEMA_FILE =
"init-sql-schema-path";
// JDBC driver for the store adapter.
private String jdbcDriverClass;
// JDBC url for the store adapter.
private String jdbcUrl;
// Connection to JDBC database.
private Connection connection;
// Handle to the ServerContext object.
private IdentityBrokerContext serverContext;
private static final AttributeValueResolver<String> SINGULAR =
AttributeValueResolver.STRING_RESOLVER;
/**
* {@inheritDoc}
*/
@Override
public String getExtensionName()
{
return "Simple JDBC Store Adapter";
}
/**
* {@inheritDoc}
*/
@Override
public String[] getExtensionDescription()
{
return new String[] {
"Implements a simple Store Adapter for a Broker DataView which is " +
"persisted in an embeddable JDBC backing store. This example persists " +
"user schema to a single table structure in user-defined tablespace."
};
}
/**
* {@inheritDoc}
*/
@Override
public void defineConfigArguments(final ArgumentParser parser)
throws ArgumentException
{
StringArgument jdbcDriverClassStringArg =
new StringArgument(null, ARG_NAME_JDBC_DRIVER, true, 1,
"{jdbc-driver-class}",
"The JDBC driver class name used by this store adapter.",
"org.apache.derby.jdbc.EmbeddedDriver");
parser.addArgument(jdbcDriverClassStringArg);
StringArgument jdbcUrlStringArg =
new StringArgument(null, ARG_NAME_JDBC_URL, true, 1, "{jdbc-url}",
"The JDBC url to the database instance used by this store adapter.",
"jdbc:derby:storeadapter;create=true");
parser.addArgument(jdbcUrlStringArg);
final FileArgument initSqlSchemaFileArg = new FileArgument(null,
ARG_NAME_INIT_SQL_SCHEMA_FILE, true, 1, "{init-sql-schema-path}",
"The path to the file containing the create table command used to " +
"initialize the database.", false, true, true, false, Arrays.asList(
new File("resource/example-jdbc-store-adapter/create-scim-table.sql")
));
parser.addArgument(initSqlSchemaFileArg);
}
/**
* {@inheritDoc}
*/
@Override
public void initializeStoreAdapter(final IdentityBrokerContext serverContext,
final StoreAdapterConfig config,
final ArgumentParser parser)
throws IOException, LDAPException, SQLException
{
this.serverContext = serverContext;
StringArgument jdbcDriverArgument =
(StringArgument) parser.getNamedArgument(ARG_NAME_JDBC_DRIVER);
jdbcDriverClass = jdbcDriverArgument.getValue();
Validator.ensureNotNull(jdbcDriverClass);
try
{
// Some JDBC drivers require their static and default constructors to be
// called for the driver to be properly registered with the DriverManager.
Class.forName(jdbcDriverClass).newInstance();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
StringArgument jdbcUrlArgument =
(StringArgument) parser.getNamedArgument(ARG_NAME_JDBC_URL);
jdbcUrl = jdbcUrlArgument.getValue();
Validator.ensureNotNull(jdbcUrl);
connection = DriverManager.getConnection(jdbcUrl);
// check to see if the schema exists
FileArgument schemaFileArg =
(FileArgument) parser.getNamedArgument(ARG_NAME_INIT_SQL_SCHEMA_FILE);
File sqlSchemaFile = schemaFileArg.getValue();
if(!sqlSchemaFile.isAbsolute())
{
sqlSchemaFile = new File(serverContext.getServerRoot(),
sqlSchemaFile.getPath());
}
File schemaCreatedFlag = new File(sqlSchemaFile.getParentFile(),
".example-jdbc-schema-created");
if (!schemaCreatedFlag.exists())
{
BufferedReader reader = new BufferedReader(new FileReader(sqlSchemaFile));
StringBuffer buffer = new StringBuffer();
String line;
while ((line = reader.readLine()) != null)
{
if (line.trim().startsWith("--"))
{
continue;
}
buffer.append(line);
}
Statement createSql = connection.createStatement();
createSql.execute(buffer.toString());
schemaCreatedFlag.createNewFile();
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean isAvailable()
{
if (connection != null && serverContext != null)
{
try
{
return connection.isValid(10);
}
catch (SQLException e)
{
serverContext.logMessage(LogSeverity.SEVERE_ERROR,
"Cannot connect to database: " + e.getMessage());
return false;
}
}
else
{
return false;
}
}
/**
* {@inheritDoc}
*/
@Override
public ResourceDescriptor getNativeSchema()
{
return USER_DESCRIPTOR;
}
/**
* {@inheritDoc}
*/
@Override
public Resources<? extends BaseResource> search(
final StoreSearchRequest request) throws SCIMException
{
final SCIMFilter filter = request.getSCIMFilter();
final AttributePath idAttrPath =
new AttributePath(SCHEMA_URI_CORE, "id", null);
try
{
if (filter != null &&
filter.getFilterType().equals(SCIMFilterType.EQUALITY) &&
filter.getFilterAttribute().toString().equals(idAttrPath.toString()))
{
//This is a search by SCIM ID
ResultSet resultSet = queryScimResourceById(filter.getFilterValue(),
true);
if (resultSet.next())
{
BaseResource resource = loadScimResourceFromResultSet(resultSet);
removeSensitiveAttributes(resource.getScimObject());
return new Resources<BaseResource>(
Collections.singletonList(resource));
}
else
{
return new Resources<BaseResource>(
Collections.<BaseResource>emptyList());
}
}
else
{
final PageParameters pageParams = request.getPageParameters();
final List<BaseResource> resultList = new LinkedList<BaseResource>();
int index = 0;
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(ScimSqlStatement.SELECT_ALL
.getStatement());
while (resultSet.next())
{
BaseResource resource = loadScimResourceFromResultSet(resultSet);
if (filter == null || resource.getScimObject().matchesFilter(filter))
{
index++;
if (pageParams != null && pageParams.getStartIndex() > index)
{
continue;
}
if (pageParams != null && index > pageParams.getCount())
{
break;
}
resultList.add(createParedResource(resource.getScimObject(),
request.getSCIMQueryAttributes()));
}
}
return new Resources<BaseResource>(resultList);
}
}
catch (final SQLException se)
{
serverContext.debugCaught(se);
throw new ServerErrorException(StaticUtils.getExceptionMessage(se));
}
}
/**
* {@inheritDoc}
*/
@Override
public BaseResource create(final StoreCreateRequest request)
throws SCIMException
{
try
{
//Populate the SCIM 'id' and 'meta' attributes
final BaseResource resourceToCreate = request.getResource();
resourceToCreate.setId(UUID.randomUUID().toString());
final URI location = URI.create("/" + resourceToCreate.getId());
final Meta meta = new Meta(new Date(), null, location, null);
resourceToCreate.setMeta(meta);
List<String> parameters = getParametersFromResource(resourceToCreate);
PreparedStatement preparedStatement = ScimSqlStatement.INSERT
.getPreparedStatement(connection, parameters
.toArray(new String[parameters.size()]));
preparedStatement.executeUpdate();
return createParedResource(resourceToCreate.getScimObject(),
request.getSCIMQueryAttributes());
}
catch (final Exception e)
{
serverContext.debugCaught(e);
throw new ServerErrorException(StaticUtils.getExceptionMessage(e));
}
}
/**
* {@inheritDoc}
*/
@Override
public BaseResource update(final StoreUpdateRequest request)
throws SCIMException
{
checkIfIdExists(request.getId());
try
{
BaseResource resource = loadScimResourceById(request.getId());
final Diff<BaseResource> diff = Diff.fromPartialResource(
request.getPartialResource(), false);
//Apply the PATCH modifications to the resource
BaseResource resourceToUpdate = diff.apply(resource,
BASE_RESOURCE_FACTORY);
//Update the 'meta.lastModified' attribute
final Meta meta = resource.getMeta();
meta.setLastModified(new Date());
resource.setMeta(meta);
Validator.ensureNotNull(resourceToUpdate);
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try
{
new JsonMarshaller().marshal(resourceToUpdate, outputStream);
List<String> parameters = getParametersFromResource(resource);
parameters.remove(0);
parameters.add(resourceToUpdate.getId());
PreparedStatement preparedStatement = ScimSqlStatement.UPDATE
.getPreparedStatement(connection, parameters.toArray(
new String[parameters.size()]));
preparedStatement.executeUpdate();
}
finally
{
safeCloseStream(outputStream);
}
return createParedResource(resourceToUpdate.getScimObject(),
request.getSCIMQueryAttributes());
}
catch (final Exception e)
{
serverContext.debugCaught(e);
throw new ServerErrorException(StaticUtils.getExceptionMessage(e));
}
}
/**
* {@inheritDoc}
*/
@Override
public void delete(final StoreDeleteRequest request) throws SCIMException
{
checkIfIdExists(request.getId());
try
{
PreparedStatement preparedStatement = ScimSqlStatement.DELETE
.getPreparedStatement(connection, request.getId());
preparedStatement.execute();
}
catch (final SQLException se)
{
serverContext.debugCaught(se);
throw new ServerErrorException(StaticUtils.getExceptionMessage(se));
}
}
/**
* Checks to see if a resource exists.
*
* @param id to check
* @throws ResourceNotFoundException if a resource was not found
*/
private void checkIfIdExists(final String id) throws ResourceNotFoundException
{
ResultSet resultSet = queryScimResourceById(id, false);
try
{
if (!resultSet.next())
{
throw new ResourceNotFoundException(String.format(
"No resource found with ID '%s'", id));
}
}
catch (final SQLException se)
{
serverContext.debugCaught(se);
throw new ResourceNotFoundException(StaticUtils.getExceptionMessage(se));
}
}
/**
* Loads a scim resource by id.
*
* @param id to load from the database
* @return BaseResource from the database
* @throws ResourceNotFoundException if the resource cannot be found
* @throws InvalidResourceException if the resource is invalid
*/
private BaseResource loadScimResourceById(final String id)
throws ResourceNotFoundException, InvalidResourceException
{
ResultSet resultSet = queryScimResourceById(id, true);
try
{
if (!resultSet.next())
{
throw new ResourceNotFoundException(String.format(
"No resource found with ID '%s'", id));
}
return loadScimResourceFromResultSet(resultSet);
}
catch (final SQLException se)
{
serverContext.debugCaught(se);
throw new ResourceNotFoundException(StaticUtils.getExceptionMessage(se));
}
}
/**
* Loads a scim resource from the current database ResultSet.
*
* @param resultSet to load a resource from
* @return BaseResource loaded from the ResultSet
* @throws InvalidResourceException if the resource is invalid
* @throws SQLException if a problem occurred
*/
private BaseResource loadScimResourceFromResultSet(final ResultSet resultSet)
throws InvalidResourceException, SQLException
{
final ResourceDescriptor schema = getNativeSchema();
final JsonUnmarshaller jsonUnmarshaller = new JsonUnmarshaller();
return jsonUnmarshaller.unmarshal(resultSet.getAsciiStream("json"), schema,
BASE_RESOURCE_FACTORY);
}
/**
* Queries the database for a scim resource by id.
*
* @param id to query in the database
* @param loadRecord {@code true} to query full record,
* {@code false} otherwise
* @return ResultSet referencing a found scim resource
* @throws ResourceNotFoundException if the resource cannot be found
*/
private ResultSet queryScimResourceById(final String id,
final boolean loadRecord)
throws ResourceNotFoundException
{
try
{
Statement statement = connection.createStatement();
PreparedStatement preparedStatement;
if (loadRecord)
{
preparedStatement = ScimSqlStatement.SELECT_CONTENT
.getPreparedStatement(connection, id);
}
else
{
preparedStatement = ScimSqlStatement.SELECT_ID
.getPreparedStatement(connection, id);
}
return preparedStatement.executeQuery();
}
catch (final SQLException se)
{
serverContext.debugCaught(se);
throw new ResourceNotFoundException(StaticUtils.getExceptionMessage(se));
}
}
/**
* Creates a new resource from a scim object with specific attributes.
*
* @param scimObject to create a resource from
* @param attributes to include in the resource
* @return BaseResource created from the scim object
*/
private BaseResource createParedResource(final SCIMObject scimObject,
final SCIMQueryAttributes attributes)
{
//Pare the returned resource down to the attributes that were requested
final SCIMObject paredObject = attributes.pareObject(scimObject);
removeSensitiveAttributes(paredObject);
final BaseResource newResource = new BaseResource(
getNativeSchema(), paredObject);
return newResource;
}
/**
* Removes any sensitive attributes from a scim object.
*
* @param scimObject to process
*/
private void removeSensitiveAttributes(final SCIMObject scimObject)
{
scimObject.removeAttribute(SCHEMA_URI_CORE, "password");
}
/**
* Returns a resource as a series of sql parameters.
*
* @param resource to parameterize
* @return resource as parameters
* @throws SCIMException if problem occurs
*/
private List<String> getParametersFromResource(final BaseResource resource)
throws SCIMException
{
final List<String> parameters = new ArrayList<String>();
final JsonMarshaller marshaller = new JsonMarshaller();
ByteArrayOutputStream outputStream = null;
try
{
parameters.add(resource.getId());
parameters.add(resource.getExternalId());
parameters.add(resource.getMeta().toString());
parameters.add(getSingularAttribute(resource, "userName"));
parameters.add(getSingularAttribute(resource, "name.formatted"));
parameters.add(getSingularAttribute(resource, "name.familyName"));
parameters.add(getSingularAttribute(resource, "name.givenName"));
parameters.add(getSingularAttribute(resource, "name.middleName"));
parameters.add(getSingularAttribute(resource, "name.honorificPrefix"));
parameters.add(getSingularAttribute(resource, "name.honorificSuffix"));
parameters.add(getSingularAttribute(resource, "displayName"));
parameters.add(getSingularAttribute(resource, "nickName"));
parameters.add(getSingularAttribute(resource, "profileUrl"));
parameters.add(getSingularAttribute(resource, "title"));
parameters.add(getSingularAttribute(resource, "preferredLanguage"));
parameters.add(getSingularAttribute(resource, "locale"));
parameters.add(getSingularAttribute(resource, "timezone"));
outputStream = new ByteArrayOutputStream();
marshaller.marshal(resource, outputStream);
JSONObject jsonObject = null;
try
{
jsonObject = new JSONObject(outputStream.toString());
parameters.add(getComplexAttribute(resource, jsonObject, "emails"));
parameters.add(getComplexAttribute(resource, jsonObject, "addresses"));
parameters.add(getComplexAttribute(resource, jsonObject,
"phonenumbers"));
parameters.add(getComplexAttribute(resource, jsonObject, "photos"));
parameters.add(getComplexAttribute(resource, jsonObject, "groups"));
parameters.add(getComplexAttribute(resource, jsonObject,
"entitlements"));
parameters.add(getComplexAttribute(resource, jsonObject, "roles"));
}
catch (final JSONException je)
{
throw new RuntimeException(je);
}
parameters.add(getSingularAttribute(resource, "website"));
parameters.add(getSingularAttribute(resource, "gender"));
parameters.add(outputStream.toString());
}
finally
{
safeCloseStream(outputStream);
}
return parameters;
}
/**
* Retrieves a single attribute from a resource.
*
* @param resource to access
* @param attribute to retrieve
* @return attribute value
*/
private String getSingularAttribute(final BaseResource resource,
final String attribute)
{
String value = null;
if (attribute.contains("."))
{
AttributePath attributePath = AttributePath.parse(attribute);
String parentAttributeName = attributePath.getAttributeName();
SCIMObject object = resource.getScimObject();
if (object.hasAttribute(SCHEMA_URI_CORE, parentAttributeName))
{
String subAttribute = attributePath.getSubAttributeName();
value = object.getAttribute(SCHEMA_URI_CORE, parentAttributeName)
.getValue().getSubAttributeValue(subAttribute, SINGULAR);
}
}
else
{
value = resource.getSingularAttributeValue(SCHEMA_URI_CORE,
attribute, SINGULAR);
}
return value == null ? "" : value;
}
/**
* Retrieves a complex or multi-value attribute from a resource.
*
* @param resource to access
* @param object resource in json format
* @param attribute to retrieve
* @return attribute content in json
* @throws JSONException if a problem occurs
*/
private String getComplexAttribute(final BaseResource resource,
final JSONObject object,
final String attribute)
throws JSONException
{
String value = "";
if (resource.getScimObject().hasAttribute(SCHEMA_URI_CORE, attribute))
{
value = object.getJSONObject(attribute).toString();
}
return value;
}
/**
* Closes a stream quietly.
*
* @param stream to close
*/
private void safeCloseStream(final Closeable stream)
{
if (stream != null)
{
try
{
stream.close();
}
catch (final IOException ioe)
{ }
}
}
/**
* Enumerated templates for sql commands. These commands could be moved to
* resources for customization.
*/
enum ScimSqlStatement
{
/**
* Insert into scim resources.
*/
INSERT("insert into %s (id, externalid, meta, username, name, " +
"familyname, givenname, middlename, honorificprefix, " +
"honorificsuffix, displayname, nickname, profileurl, title, " +
"preferredlanguage, locale, timezone, emails, addresses, " +
"phonenumbers, photos, groups, entitlements, roles, website, " +
"gender, json) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
/**
* Update scim resource by id.
*/
UPDATE("update %s set " +
"externalid = ?, meta = ?, username = ?, name = ?, familyname = ?, " +
"givenname = ?, middlename = ?, honorificprefix = ?, " +
"honorificsuffix = ?, displayname = ?, nickname = ?, profileurl = ?, " +
"title = ?, preferredlanguage = ?, locale = ?, timezone = ?, " +
"emails = ?, addresses = ?, phonenumbers = ?, photos = ?, groups = ?, " +
"entitlements = ?, roles = ?, website = ?, gender = ?, json = ? " +
"where id = ?"),
/**
* Delete scim resource by id.
*/
DELETE("delete from %s where id = ?"),
/**
* Select all scim resources.
*/
SELECT_ALL("select * from %s"),
/**
* Load scim resource by id.
*/
SELECT_CONTENT("select id, json from %s where id = ?"),
/**
* Check for scim resource only by id.
*/
SELECT_ID("select id from %s where id = ?");
/**
* Single table containing scim resources. This table is created by the
* sql statement in config/create-scim-table.sql.
*/
public static final String TABLE = "SCIM_RESOURCES";
private final String statement;
/**
* enum constructor.
*
* @param statement embedded statement
*/
ScimSqlStatement(final String statement)
{
this.statement = statement;
}
/**
* Returns a rendered sql statement.
*
* @return String containing the rendered sql statement
*/
public String getStatement()
{
return String.format(statement, TABLE);
}
/**
* Returns a rendered prepared statement from the supplied parameters.
*
* @param connection sql connection
* @param parameters parameters used for substitutions
* @return PreparedStatement rendered result
* @throws SQLException if a problem occurs
*/
public PreparedStatement getPreparedStatement(final Connection connection,
final String... parameters)
throws SQLException
{
final PreparedStatement preparedStatement =
connection.prepareStatement(getStatement());
if (parameters != null)
{
int parameterIndex = 1;
for (final String parameter : parameters)
{
preparedStatement.setString(parameterIndex++, parameter);
}
}
return preparedStatement;
}
}
}
|