/*
* 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-2014 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.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
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.CoreSchema;
import com.unboundid.scim.schema.ResourceDescriptor;
import com.unboundid.scim.sdk.AttributePath;
import com.unboundid.scim.sdk.Diff;
import com.unboundid.scim.sdk.PageParameters;
import com.unboundid.scim.sdk.ResourceNotFoundException;
import com.unboundid.scim.sdk.Resources;
import com.unboundid.scim.sdk.SCIMConstants;
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 java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* This example provides an implementation of a flat-file StoreAdapter
* which stores users in JSON format. Note that this is a simplistic
* implementation for the sake of example; production StoreAdapter
* implementations must store their data in persistent storage that is
* accessible to all Identity Broker instances.
* <p>
* This implementation supports all required SCIM features, but does not
* support certain optional features such as sorting or resource versioning.
*/
public class ExampleStoreAdapter extends StoreAdapter
{
// Handle to the ServerContext object.
private IdentityBrokerContext serverContext;
// The name of the argument that will be used to specify the path to the JSON
// file containing the users.
private static final String ARG_NAME_JSON_FILE_PATH = "json-file-path";
// Handle to the JSON file containing the users.
private File jsonFile;
// An in-memory map of the SCIM resources based on their SCIM ID.
private final ConcurrentHashMap<String, BaseResource> resources =
new ConcurrentHashMap<String, BaseResource>();
/**
* {@inheritDoc}
*/
@Override
public String getExtensionName()
{
return "Example Store Adapter";
}
/**
* {@inheritDoc}
*/
@Override
public String[] getExtensionDescription()
{
return new String[]
{
"This example provides an implementation of a flat-file StoreAdapter " +
"which stores users in JSON format. Note that this is a simplistic " +
"implementation for the sake of example; production StoreAdapter " +
"implementations must store their data in persistent storage that is " +
"accessible to all Identity Broker instances."
};
}
/**
* {@inheritDoc}
*/
@Override
public void defineConfigArguments(final ArgumentParser parser)
throws ArgumentException
{
String description = "The path to the JSON file containing the users.";
List<File> defaultValues = new ArrayList<File>(1);
defaultValues.add(new File("resource/user-database.json"));
final FileArgument jsonFileArg = new FileArgument(null,
ARG_NAME_JSON_FILE_PATH, true, 1, "{file-path}", description,
false, true, true, false, defaultValues);
parser.addArgument(jsonFileArg);
}
/**
* Initializes this store adapter. Any initialization should be performed
* here. This method should generally store the
* {@link com.unboundid.directory.sdk.broker.types.IdentityBrokerContext} in
* a class member so that it can be used elsewhere in the implementation.
*
* @param serverContext A handle to the server context for the server in
* which this extension is running. Extensions should
* typically store this in a class member.
* @param config The general configuration for this object.
* @param parser The argument parser which has been initialized from
* the configuration for this store adapter.
* @throws LDAPException If a problem occurs while initializing this store
* adapter.
*/
@Override
public void initializeStoreAdapter(final IdentityBrokerContext serverContext,
final StoreAdapterConfig config,
final ArgumentParser parser)
throws LDAPException
{
this.serverContext = serverContext;
FileArgument jsonFileArg =
(FileArgument) parser.getNamedArgument(ARG_NAME_JSON_FILE_PATH);
jsonFile = jsonFileArg.getValue();
if(!jsonFile.isAbsolute())
{
jsonFile = new File(serverContext.getServerRoot(), jsonFile.getPath());
}
Resources<BaseResource> resourceList =
new Resources<BaseResource>(Collections.<BaseResource>emptyList());
//Preload the resources from the file if it already exists
if (jsonFile.length() > 0)
{
JsonUnmarshaller unmarshaller = new JsonUnmarshaller();
BufferedInputStream inputStream = null;
try
{
inputStream = new BufferedInputStream(new FileInputStream(jsonFile));
resourceList = unmarshaller.unmarshalResources(inputStream,
getNativeSchema(), BaseResource.BASE_RESOURCE_FACTORY);
}
catch(Exception e)
{
serverContext.debugCaught(e);
throw new LDAPException(ResultCode.LOCAL_ERROR, e);
}
finally
{
safeCloseStream(inputStream);
}
}
else //Otherwise, create the file
{
try
{
jsonFile.createNewFile();
}
catch (IOException ioe)
{
serverContext.debugCaught(ioe);
throw new LDAPException(ResultCode.LOCAL_ERROR, ioe);
}
}
// Populate the resources map
for (BaseResource r : resourceList)
{
resources.put(r.getId(), r);
}
}
/**
* Determines whether this StoreAdapter is currently available and in-service.
* This may return {@code false} in the case that the JSON file is not
* accessible; during this time the DataView will return an appropriate 503
* response code to clients.
*
* @return {@code true} if this StoreAdapter initialized and connected;
* {@code false} otherwise.
*/
@Override
public boolean isAvailable()
{
return jsonFile != null && jsonFile.canWrite();
}
/**
* Gets a ResourceDescriptor that describes the schema for the objects that
* will be returned by this StoreAdapter. The StoreAdapter will always be
* initialized before this method is called.
*
* @return a SCIM ResourceDescriptor object. This may not be {@code null}.
*/
@Override
public ResourceDescriptor getNativeSchema()
{
return CoreSchema.USER_DESCRIPTOR;
}
/**
* Fetches the entries that match the specified criteria.
*
* @param request the search request
* @return a collection of SCIM resources that are in the native schema. This
* may be empty, but it may not be {@code null}.
* @throws SCIMException if there is a problem fulfilling the search request
*/
@Override
public Resources<? extends BaseResource> search(
final StoreSearchRequest request) throws SCIMException
{
final SCIMFilter filter = request.getSCIMFilter();
final AttributePath idAttrPath = new AttributePath(
SCIMConstants.SCHEMA_URI_CORE, "id", null);
if(filter != null && filter.getFilterType().equals(SCIMFilterType.EQUALITY)
&& filter.getFilterAttribute().toString().equals(idAttrPath.toString()))
{
//This is a search by SCIM ID
BaseResource r = resources.get(filter.getFilterValue());
if (r != null)
{
removeSensitiveAttributes(r.getScimObject());
return new Resources<BaseResource>(Collections.singletonList(r));
}
else
{
return new Resources<BaseResource>(
Collections.<BaseResource>emptyList());
}
}
else
{
PageParameters pageParams = request.getPageParameters();
List<BaseResource> resultList = new LinkedList<BaseResource>();
int idx = 0;
for (BaseResource r : resources.values())
{
if (filter == null || r.getScimObject().matchesFilter(filter))
{
idx++;
if (pageParams != null && pageParams.getStartIndex() > idx)
{
continue;
}
if (pageParams != null && idx > pageParams.getCount())
{
break;
}
resultList.add(createParedResource(r.getScimObject(),
request.getSCIMQueryAttributes()));
}
}
return new Resources<BaseResource>(resultList);
}
}
/**
* Create the specified entry in the JSON file.
*
* @param request the create request
* @return the resource that was just created, scoped according to the
* SCIMQueryAttributes contained in the request
* @throws SCIMException if there is a problem creating the resource
*/
@Override
public BaseResource create(final StoreCreateRequest request)
throws SCIMException
{
//All write operations are synchronized to prevent concurrent modifications
//to the JSON file.
synchronized(this)
{
FileOutputStream outputStream = null;
try
{
//Populate the SCIM 'id' and 'meta' attributes
BaseResource resourceToCreate = request.getResource();
resourceToCreate.setId(UUID.randomUUID().toString());
URI location = URI.create(jsonFile.toURI() + "/" +
resourceToCreate.getId());
Meta meta = new Meta(new Date(), null, location, null);
resourceToCreate.setMeta(meta);
List<BaseResource> resourceList =
new LinkedList<BaseResource>(resources.values());
resourceList.add(resourceToCreate);
resources.put(resourceToCreate.getId(), resourceToCreate);
//Write the new set of resources to the JSON file
jsonFile.delete();
outputStream = new FileOutputStream(jsonFile);
writeResources(outputStream, resourceList);
return createParedResource(resourceToCreate.getScimObject(),
request.getSCIMQueryAttributes());
}
catch (Exception e)
{
serverContext.debugCaught(e);
throw new ServerErrorException(StaticUtils.getExceptionMessage(e));
}
finally
{
safeCloseStream(outputStream);
}
}
}
/**
* Update the specified entry in the JSON file.
*
* @param request the update request
* @return the updated resource, scoped according to the SCIMQueryAttributes
* contained in the request
* @throws SCIMException if there is a problem modifying the resource
*/
@Override
public BaseResource update(final StoreUpdateRequest request)
throws SCIMException
{
checkIfIdExists(request.getId());
//All write operations are synchronized to prevent concurrent modifications
//to the JSON file.
synchronized(this)
{
FileOutputStream outputStream = null;
try
{
BaseResource updatedResource = null;
List<BaseResource> resourceList = new LinkedList<BaseResource>();
for (BaseResource r : resources.values())
{
if (r.getId().equals(request.getId()))
{
Diff<BaseResource> diff = Diff.fromPartialResource(
request.getPartialResource(), false);
//Apply the PATCH modifications to the resource
updatedResource = diff.apply(r, BaseResource.BASE_RESOURCE_FACTORY);
//Update the 'meta.lastModified' attribute
Meta meta = updatedResource.getMeta();
meta.setLastModified(new Date());
updatedResource.setMeta(meta);
//Finally, add the resource to the total list of resources
resourceList.add(updatedResource);
}
else {
resourceList.add(r);
}
}
Validator.ensureNotNull(updatedResource);
resources.put(request.getId(), updatedResource);
//Write the updated set of resources to the JSON file
jsonFile.delete();
outputStream = new FileOutputStream(jsonFile);
writeResources(outputStream, resourceList);
return createParedResource(updatedResource.getScimObject(),
request.getSCIMQueryAttributes());
}
catch(Exception e)
{
serverContext.debugCaught(e);
throw new ServerErrorException(StaticUtils.getExceptionMessage(e));
}
finally
{
safeCloseStream(outputStream);
}
}
}
/**
* Delete the specified entry from the JSON file.
*
* @param request the delete request
* @throws SCIMException if there is a problem deleting the resource
*/
@Override
public void delete(final StoreDeleteRequest request) throws SCIMException
{
checkIfIdExists(request.getId());
//All write operations are synchronized to prevent concurrent modifications
//to the JSON file.
synchronized(this)
{
FileOutputStream outputStream = null;
try
{
List<BaseResource> resourceList = new LinkedList<BaseResource>();
resources.remove(request.getId());
resourceList.addAll(resources.values());
//Write the updated set of resources to the JSON file
jsonFile.delete();
outputStream = new FileOutputStream(jsonFile);
writeResources(outputStream, resourceList);
}
catch(Exception e)
{
serverContext.debugCaught(e);
throw new ServerErrorException(StaticUtils.getExceptionMessage(e));
}
finally
{
safeCloseStream(outputStream);
}
}
}
/**
* Closes a stream quietly.
*
* @param stream to close
*/
private void safeCloseStream(final Closeable stream)
{
if (stream != null)
{
try
{
stream.close();
}
catch (final IOException ioe)
{ }
}
}
/**
* 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
{
if (!resources.containsKey(id))
{
throw new ResourceNotFoundException(String.format(
"No resource found with ID '%s'", id));
}
}
/**
* Writes the current resources to an output stream.
*
* @param outputStream where the resources are written
* @param resourceList resources to write
* @throws SCIMException if a problem occurs
*/
private void writeResources(final OutputStream outputStream,
final List<BaseResource> resourceList)
throws SCIMException
{
//Write a set of resources to an output stream
Resources<BaseResource> outputList =
new Resources<BaseResource>(resourceList);
JsonMarshaller marshaller = new JsonMarshaller();
marshaller.marshal(outputList, outputStream);
}
/**
* 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
SCIMObject paredObject = attributes.pareObject(scimObject);
removeSensitiveAttributes(paredObject);
return new BaseResource(getNativeSchema(), paredObject);
}
/**
* Removes any sensitive attributes from a scim object.
*
* @param scimObject to process
*/
private void removeSensitiveAttributes(final SCIMObject scimObject)
{
scimObject.removeAttribute(SCIMConstants.SCHEMA_URI_CORE, "password");
}
}
|