/*
* 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-2016 UnboundID Corp.
*/
package com.unboundid.directory.sdk.examples;
import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.unboundid.directory.sdk.broker.api.StoreAdapter;
import com.unboundid.directory.sdk.broker.config.StoreAdapterConfig;
import com.unboundid.directory.sdk.broker.types.BrokerContext;
import com.unboundid.directory.sdk.broker.types.StoreCreateRequest;
import com.unboundid.directory.sdk.broker.types.StoreDeleteRequest;
import com.unboundid.directory.sdk.broker.types.StoreSearchResultListener;
import com.unboundid.directory.sdk.broker.types.StoreRetrieveRequest;
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.scim2.common.GenericScimResource;
import com.unboundid.scim2.common.exceptions.ResourceNotFoundException;
import com.unboundid.scim2.common.exceptions.ScimException;
import com.unboundid.scim2.common.exceptions.ServerErrorException;
import com.unboundid.scim2.common.filters.Filter;
import com.unboundid.scim2.common.messages.PatchRequest;
import com.unboundid.scim2.common.utils.FilterEvaluator;
import com.unboundid.scim2.common.utils.JsonUtils;
import com.unboundid.util.StaticUtils;
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.util.ArrayList;
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 entries 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.
*/
public class ExampleStoreAdapter extends StoreAdapter
{
// Handle to the ServerContext object.
private BrokerContext serverContext;
// The name of the argument that will be used to specify the path to the JSON
// file containing the entries.
private static final String ARG_NAME_JSON_FILE_PATH = "json-file-path";
// Handle to the JSON file containing the entries.
private File jsonFile;
// An in-memory map of SCIM resources based on their ID attribute.
private final ConcurrentHashMap<String, GenericScimResource> resources =
new ConcurrentHashMap<String, GenericScimResource>();
/**
* {@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 entries 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 Data Broker instances."
};
}
/**
* {@inheritDoc}
*/
@Override
public void defineConfigArguments(final ArgumentParser parser)
throws ArgumentException
{
final String description =
"The path to the JSON file containing the entries.";
final List<File> defaultValues = new ArrayList<File>(1);
defaultValues.add(new File("resource/entry-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 BrokerContext} 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 BrokerContext serverContext,
final StoreAdapterConfig config,
final ArgumentParser parser)
throws LDAPException
{
this.serverContext = serverContext;
final FileArgument jsonFileArg =
(FileArgument) parser.getNamedArgument(ARG_NAME_JSON_FILE_PATH);
jsonFile = jsonFileArg.getValue();
if (!jsonFile.isAbsolute())
{
jsonFile = new File(serverContext.getServerRoot(), jsonFile.getPath());
}
// Preload the entries from the file if it already exists.
if (jsonFile.length() > 0)
{
BufferedInputStream inputStream = null;
try
{
inputStream = new BufferedInputStream(new FileInputStream(jsonFile));
final MappingIterator<GenericScimResource> nodes =
JsonUtils.getObjectReader().readValues(inputStream);
// Populate the resources map.
for (GenericScimResource r : nodes.readAll())
{
resources.put(r.getId(), r);
}
}
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);
}
}
}
/**
* Determines whether this Store Adapter is currently available and
* in-service. This method may return {@code false} in the case that the
* JSON file is not accessible; during this time the SCIMResourceType will
* return an appropriate 503 response code to clients.
*
* @return {@code true} if this Store Adapter is initialized and connected;
* {@code false} otherwise.
*/
@Override
public boolean isAvailable()
{
return jsonFile != null && jsonFile.canWrite();
}
/**
* Fetches the entries that match the specified criteria.
*
* @param request The search request.
* @throws ScimException if there is a problem fulfilling the search request.
*/
@Override
public void search(final StoreSearchRequest request)
throws ScimException
{
final Filter filter = Filter.fromString(request.getSCIMFilter());
final StoreSearchResultListener listener =
request.getStoreSearchResultListener();
int resultCount = 0;
for (GenericScimResource r : resources.values())
{
if (filter == null ||
FilterEvaluator.evaluate(filter, r.getObjectNode()))
{
listener.searchResultReturned(r.toString());
resultCount++;
if (request.getSizeLimit() > 0 && resultCount >= request.getSizeLimit())
{
break;
}
}
}
}
/**
* Fetches the specified entry.
*
* @param request The retrieve request.
* @return The retrieved entry.
* @throws ScimException if there is a problem fulfilling the request.
*/
@Override
public String retrieve(final StoreRetrieveRequest request)
throws ScimException
{
final Filter filter = Filter.fromString(request.getSCIMFilter());
final GenericScimResource r = retrieve(filter);
return r.toString();
}
/**
* Create the specified entry in the JSON file.
*
* @param request The create request.
* @return The entry that was just created;
* @throws ScimException if there is a problem creating the entry.
*/
@Override
public String create(final StoreCreateRequest request)
throws ScimException
{
// All write operations are synchronized to prevent concurrent modifications
// to the JSON file.
synchronized(this)
{
try
{
final GenericScimResource resourceToCreate =
new GenericScimResource(toObjectNode(request.getObjectToCreate()));
// Populate the ID attribute.
resourceToCreate.setId(UUID.randomUUID().toString());
resources.put(resourceToCreate.getId(), resourceToCreate);
// Write the new set of resources to the JSON file.
writeResources();
return resourceToCreate.toString();
}
catch (Exception e)
{
serverContext.debugCaught(e);
throw new ServerErrorException(StaticUtils.getExceptionMessage(e));
}
}
}
/**
* Update the specified entry in the JSON file.
*
* @param request The update request.
* @return The updated entry.
* @throws ScimException if there is a problem modifying the entry.
*/
@Override
public String update(final StoreUpdateRequest request)
throws ScimException
{
final Filter filter = Filter.fromString(request.getSCIMFilter());
final GenericScimResource resourceToUpdate = retrieve(filter);
// All write operations are synchronized to prevent concurrent modifications
// to the JSON file.
synchronized(this)
{
try
{
final ObjectReader reader =
JsonUtils.getObjectReader().forType(PatchRequest.class);
final PatchRequest patchOp =
reader.readValue(request.getPatchRequest());
patchOp.apply(resourceToUpdate);
// Write the updated set of resources to the JSON file.
writeResources();
return resourceToUpdate.toString();
}
catch(Exception e)
{
serverContext.debugCaught(e);
throw new ServerErrorException(StaticUtils.getExceptionMessage(e));
}
}
}
/**
* Delete the specified entry from the JSON file.
*
* @param request The delete request.
* @throws ScimException if there is a problem deleting the entry.
*/
@Override
public void delete(final StoreDeleteRequest request) throws ScimException
{
final Filter filter = Filter.fromString(request.getSCIMFilter());
final GenericScimResource resourceToDelete =
retrieve(filter);
// All write operations are synchronized to prevent concurrent modifications
// to the JSON file.
synchronized(this)
{
try
{
resources.remove(resourceToDelete.getId());
// Write the updated set of resources to the JSON file.
writeResources();
}
catch(Exception e)
{
serverContext.debugCaught(e);
throw new ServerErrorException(StaticUtils.getExceptionMessage(e));
}
}
}
/**
* Closes a stream quietly.
*
* @param stream to close
*/
private void safeCloseStream(final Closeable stream)
{
if (stream != null)
{
try
{
stream.close();
}
catch (final IOException ioe)
{
// Ignore.
}
}
}
/**
* Retrieves a resource identified by a SCIM filter.
*
* @param filter The SCIM filter identifying a resource to retrieve.
* @return The requested resource.
* @throws ScimException If the resource was not found.
*/
private GenericScimResource retrieve(final Filter filter)
throws ScimException
{
for (GenericScimResource r : resources.values())
{
if (FilterEvaluator.evaluate(filter, r.getObjectNode()))
{
return r;
}
}
throw new ResourceNotFoundException(String.format(
"No resource found matching filter '%s'", filter.toString()));
}
/**
* Writes the current resources to the JSON file.
* @throws IOException if the resources could not be written.
*/
private void writeResources()
throws IOException
{
final File file = this.jsonFile;
final File tempFile = new File(file.getAbsolutePath() + ".new");
final File oldFile = new File(file.getAbsolutePath() + ".old");
final OutputStream outputStream = new FileOutputStream(tempFile);
try
{
final ObjectWriter writer = JsonUtils.getObjectWriter();
for (GenericScimResource r : resources.values())
{
writer.writeValue(outputStream, r);
}
}
finally
{
safeCloseStream(outputStream);
}
if (oldFile.exists())
{
oldFile.delete();
}
if (file.exists())
{
file.renameTo(oldFile);
}
tempFile.renameTo(file);
}
/**
* Parse a JSON object in string form into an ObjectNode.
*
* @param jsonString The string containing a JSON object.
* @return The parsed ObjectNode.
* @throws IOException if the object could not be parsed.
*/
private ObjectNode toObjectNode(final String jsonString)
throws IOException
{
return (ObjectNode)JsonUtils.getObjectReader().readTree(jsonString);
}
}
|