opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/CompositeAttributeMapper.java
@@ -91,10 +91,10 @@ if (f == null) { // No mapping so remove. i.remove(); } else if (f == c.getConfig().getFalseFilter()) { return c.getConfig().getFalseFilter(); } else if (f == c.getConfig().getTrueFilter()) { return c.getConfig().getTrueFilter(); } else if (f == c.getConfig().falseFilter()) { return c.getConfig().falseFilter(); } else if (f == c.getConfig().trueFilter()) { return c.getConfig().trueFilter(); } } switch (value.size()) { opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
@@ -15,6 +15,9 @@ */ package org.forgerock.opendj.rest2ldap; import static org.forgerock.opendj.rest2ldap.Config.ReadOnUpdatePolicy.USE_READ_ENTRY_CONTROLS; import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull; import org.forgerock.opendj.ldap.Filter; /** @@ -22,24 +25,200 @@ */ public final class Config { private final Filter trueFilter = Filter.objectClassPresent(); private final Filter falseFilter = Filter.present("1.1"); /** * An interface for incrementally constructing common configuration options. */ public static final class Builder { private Filter falseFilter; private ReadOnUpdatePolicy readOnUpdatePolicy; private Filter trueFilter; private Builder() { // Nothing to do. } /** * Returns a new configuration based on the current state of this * builder. * * @return A new configuration based on the current state of this * builder. */ public Config build() { return new Config(trueFilter, falseFilter, readOnUpdatePolicy); } /** * Sets the absolute false filter which should be used when querying the * LDAP server. * * @param filter * The absolute false filter. * @return A reference to this builder. */ public Builder falseFilter(final Filter filter) { this.trueFilter = ensureNotNull(filter); return this; } /** * Sets the policy which should be used in order to read an entry before * it is deleted, or after it is added or modified. * * @param policy * The policy which should be used in order to read an entry * before it is deleted, or after it is added or modified. * @return A reference to this builder. */ public Builder readOnUpdatePolicy(final ReadOnUpdatePolicy policy) { this.readOnUpdatePolicy = ensureNotNull(policy); return this; } /** * Sets the absolute true filter which should be used when querying the * LDAP server. * * @param filter * The absolute true filter. * @return A reference to this builder. */ public Builder trueFilter(final Filter filter) { this.trueFilter = ensureNotNull(filter); return this; } }; /** * Returns the absolute true filter. * * @return The absolute true filter. * The policy which should be used in order to read an entry before it is * deleted, or after it is added or modified. */ public Filter getTrueFilter() { return trueFilter; public static enum ReadOnUpdatePolicy { /** * The LDAP entry will not be read when an update is performed. More * specifically, the REST resource will not be returned as part of a * create, delete, patch, or update request. */ DISABLED, /** * The LDAP entry will be read atomically using the RFC 4527 read-entry * controls. More specifically, the REST resource will be returned as * part of a create, delete, patch, or update request, and it will * reflect the state of the resource at the time the update was * performed. This policy requires that the LDAP server supports RFC * 4527. */ USE_READ_ENTRY_CONTROLS, /** * The LDAP entry will be read non-atomically using an LDAP search when * an update is performed. More specifically, the REST resource will be * returned as part of a create, delete, patch, or update request, but * it may not reflect the state of the resource at the time the update * was performed. */ USE_SEARCH; } private static final Config DEFAULT = new Builder().trueFilter(Filter.objectClassPresent()) .falseFilter(Filter.present("1.1")).readOnUpdatePolicy(USE_READ_ENTRY_CONTROLS).build(); /** * Returns a new builder which can be used for incrementally constructing * common configuration options. The builder will initially have * {@link #defaultConfig() default} settings. * * @return The new builder. */ public static Builder builder() { return builder(DEFAULT); } /** * Returns the absolute false filter. * Returns a new builder which can be used for incrementally constructing * common configuration options. The builder will initially have the same * settings as the provided configuration. * * @param config * The initial settings. * @return The new builder. */ public static Builder builder(final Config config) { return new Builder().trueFilter(config.trueFilter()).falseFilter(config.falseFilter()) .readOnUpdatePolicy(config.readOnUpdatePolicy()); } /** * Returns the default configuration having the following settings: * <ul> * <li>the absolute true filter {@code (objectClass=*)} * <li>the absolute false filter {@code (1.1=*)} * <li>the read on update policy * {@link ReadOnUpdatePolicy#USE_READ_ENTRY_CONTROLS}. * </ul> * * @return The default configuration. */ public static Config defaultConfig() { return DEFAULT; } private final Filter falseFilter; private final ReadOnUpdatePolicy readOnUpdatePolicy; private final Filter trueFilter; private Config(final Filter trueFilter, final Filter falseFilter, final ReadOnUpdatePolicy readOnUpdatePolicy) { this.trueFilter = trueFilter; this.falseFilter = falseFilter; this.readOnUpdatePolicy = readOnUpdatePolicy; } /** * Returns the absolute false filter which should be used when querying the * LDAP server. * * @return The absolute false filter. */ public Filter getFalseFilter() { public Filter falseFilter() { return falseFilter; } /** * Returns the policy which should be used in order to read an entry before * it is deleted, or after it is added or modified. * * @return The policy which should be used in order to read an entry before * it is deleted, or after it is added or modified. */ public ReadOnUpdatePolicy readOnUpdatePolicy() { return readOnUpdatePolicy; } /** * {@inheritDoc} */ @Override public String toString() { final StringBuilder builder = new StringBuilder(); builder.append("trueFilter="); builder.append(trueFilter); builder.append(", falseFilter="); builder.append(falseFilter); builder.append(", readOnUpdatePolicy="); builder.append(readOnUpdatePolicy); return builder.toString(); } /** * Returns the absolute true filter which should be used when querying the * LDAP server. * * @return The absolute true filter. */ public Filter trueFilter() { return trueFilter; } } opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ConstantAttributeMapper.java
@@ -69,20 +69,20 @@ if (jsonAttribute.size() == 1 && jsonAttribute.get(0).equalsIgnoreCase(jsonAttributeName)) { final Filter filter; if (type == FilterType.PRESENT) { filter = c.getConfig().getTrueFilter(); filter = c.getConfig().trueFilter(); } else if (jsonAttributeValue instanceof String && valueAssertion instanceof String) { final String v1 = toLowerCase((String) jsonAttributeValue); final String v2 = toLowerCase((String) valueAssertion); switch (type) { case CONTAINS: filter = v1.contains(v2) ? c.getConfig().getTrueFilter() : c.getConfig() .getFalseFilter(); v1.contains(v2) ? c.getConfig().trueFilter() : c.getConfig() .falseFilter(); break; case STARTS_WITH: filter = v1.startsWith(v2) ? c.getConfig().getTrueFilter() : c.getConfig() .getFalseFilter(); v1.startsWith(v2) ? c.getConfig().trueFilter() : c.getConfig() .falseFilter(); break; default: filter = compare(c, type, v1, v2); @@ -98,7 +98,7 @@ filter = compare(c, type, v1, v2); } else { // This attribute mapper is a candidate but it does not match. filter = c.getConfig().getFalseFilter(); filter = c.getConfig().falseFilter(); } h.handleResult(filter); } else { @@ -130,30 +130,30 @@ final Filter filter; switch (type) { case EQUAL_TO: filter = v1.equals(v2) ? c.getConfig().getTrueFilter() : c.getConfig().getFalseFilter(); filter = v1.equals(v2) ? c.getConfig().trueFilter() : c.getConfig().falseFilter(); break; case GREATER_THAN: filter = v1.compareTo(v2) > 0 ? c.getConfig().getTrueFilter() : c.getConfig() .getFalseFilter(); v1.compareTo(v2) > 0 ? c.getConfig().trueFilter() : c.getConfig() .falseFilter(); break; case GREATER_THAN_OR_EQUAL_TO: filter = v1.compareTo(v2) >= 0 ? c.getConfig().getTrueFilter() : c.getConfig() .getFalseFilter(); v1.compareTo(v2) >= 0 ? c.getConfig().trueFilter() : c.getConfig() .falseFilter(); break; case LESS_THAN: filter = v1.compareTo(v2) < 0 ? c.getConfig().getTrueFilter() : c.getConfig() .getFalseFilter(); v1.compareTo(v2) < 0 ? c.getConfig().trueFilter() : c.getConfig() .falseFilter(); break; case LESS_THAN_OR_EQUAL_TO: filter = v1.compareTo(v2) <= 0 ? c.getConfig().getTrueFilter() : c.getConfig() .getFalseFilter(); v1.compareTo(v2) <= 0 ? c.getConfig().trueFilter() : c.getConfig() .falseFilter(); break; default: filter = c.getConfig().getFalseFilter(); // Not supported. filter = c.getConfig().falseFilter(); // Not supported. break; } return filter; opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/EntryContainer.java
File was deleted opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -51,14 +51,21 @@ import org.forgerock.opendj.ldap.AssertionFailureException; import org.forgerock.opendj.ldap.AuthenticationException; import org.forgerock.opendj.ldap.AuthorizationException; import org.forgerock.opendj.ldap.Connection; import org.forgerock.opendj.ldap.ConnectionException; import org.forgerock.opendj.ldap.ConnectionFactory; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.Entry; import org.forgerock.opendj.ldap.EntryNotFoundException; import org.forgerock.opendj.ldap.ErrorResultException; import org.forgerock.opendj.ldap.Filter; import org.forgerock.opendj.ldap.Function; import org.forgerock.opendj.ldap.MultipleEntriesFoundException; import org.forgerock.opendj.ldap.SearchResultHandler; import org.forgerock.opendj.ldap.SearchScope; import org.forgerock.opendj.ldap.TimeoutResultException; import org.forgerock.opendj.ldap.requests.Requests; import org.forgerock.opendj.ldap.requests.SearchRequest; import org.forgerock.opendj.ldap.responses.Result; import org.forgerock.opendj.ldap.responses.SearchResultEntry; import org.forgerock.opendj.ldap.responses.SearchResultReference; @@ -68,25 +75,120 @@ * resource collection to LDAP entries beneath a base DN. */ public class LDAPCollectionResourceProvider implements CollectionResourceProvider { private abstract class AbstractRequestCompletionHandler<R, H extends org.forgerock.opendj.ldap.ResultHandler<? super R>> implements org.forgerock.opendj.ldap.ResultHandler<R> { final Connection connection; final H resultHandler; AbstractRequestCompletionHandler(final Connection connection, final H resultHandler) { this.connection = connection; this.resultHandler = resultHandler; } @Override public final void handleErrorResult(final ErrorResultException error) { connection.close(); resultHandler.handleErrorResult(error); } @Override public final void handleResult(final R result) { connection.close(); resultHandler.handleResult(result); } } private abstract class ConnectionCompletionHandler<R> implements org.forgerock.opendj.ldap.ResultHandler<Connection> { private final org.forgerock.opendj.ldap.ResultHandler<? super R> resultHandler; ConnectionCompletionHandler( final org.forgerock.opendj.ldap.ResultHandler<? super R> resultHandler) { this.resultHandler = resultHandler; } @Override public final void handleErrorResult(final ErrorResultException error) { resultHandler.handleErrorResult(error); } @Override public abstract void handleResult(Connection connection); } private final class RequestCompletionHandler<R> extends AbstractRequestCompletionHandler<R, org.forgerock.opendj.ldap.ResultHandler<? super R>> { RequestCompletionHandler(final Connection connection, final org.forgerock.opendj.ldap.ResultHandler<? super R> resultHandler) { super(connection, resultHandler); } } private final class SearchRequestCompletionHandler extends AbstractRequestCompletionHandler<Result, SearchResultHandler> implements SearchResultHandler { SearchRequestCompletionHandler(final Connection connection, final SearchResultHandler resultHandler) { super(connection, resultHandler); } /** * {@inheritDoc} */ @Override public final boolean handleEntry(final SearchResultEntry entry) { return resultHandler.handleEntry(entry); } /** * {@inheritDoc} */ @Override public final boolean handleReference(final SearchResultReference reference) { return resultHandler.handleReference(reference); } } // FIXME: make this configurable. private static final String ETAG_ATTRIBUTE = "etag"; // Dummy exception used for signalling search success. private static final ResourceException SUCCESS = new UncategorizedException(0, null, null); // FIXME: make this configurable, also allow use of DN. private static final String UUID_ATTRIBUTE = "entryUUID"; private final AttributeMapper attributeMapper; private final EntryContainer entryContainer; private final Config config = new Config(); private final DN baseDN; // TODO: support template variables. private final Config config; private final ConnectionFactory factory; /** * Creates a new LDAP resource. * * @param container * The LDAP entry container. * @param baseDN * The parent of all entries contained in this LDAP collection. * @param mapper * The attribute mapper which will be used for mapping LDAP * attributes to JSON attributes. * @param factory * The LDAP connection factory which will be used for performing * LDAP operations. * @param config * Common configuration options. */ public LDAPCollectionResourceProvider(final EntryContainer container, final AttributeMapper mapper) { this.entryContainer = container; public LDAPCollectionResourceProvider(final DN baseDN, final AttributeMapper mapper, final ConnectionFactory factory, final Config config) { this.baseDN = baseDN; this.attributeMapper = mapper; this.factory = factory; this.config = config; } /** @@ -140,8 +242,10 @@ @Override public void queryCollection(final ServerContext context, final QueryRequest request, final QueryResultHandler handler) { // List the entries. final Context c = wrap(context); final Collection<String> ldapAttributes = getLDAPAttributes(c, request.getFieldFilters()); // The handler which will be invoked for each LDAP search result. final SearchResultHandler searchHandler = new SearchResultHandler() { private final AtomicInteger pendingResourceCount = new AtomicInteger(); private final AtomicReference<ResourceException> pendingResult = @@ -161,8 +265,8 @@ // TODO: should the resource or the container define the ID // mapping? final String id = entryContainer.getIDFromEntry(entry); final String revision = entryContainer.getEtagFromEntry(entry); final String id = getIDFromEntry(entry); final String revision = getEtagFromEntry(entry); final ResultHandler<Map<String, Object>> mapHandler = new ResultHandler<Map<String, Object>>() { @Override @@ -224,8 +328,8 @@ } }; final Collection<String> ldapAttributes = getLDAPAttributes(c, request.getFieldFilters()); getLDAPFilter(c, request.getQueryFilter(), new ResultHandler<Filter>() { // The handler which will be invoked once the LDAP filter has been transformed. final ResultHandler<Filter> filterHandler = new ResultHandler<Filter>() { @Override public void handleError(final ResourceException error) { handler.handleError(error); @@ -234,13 +338,32 @@ @Override public void handleResult(final Filter ldapFilter) { // Avoid performing a search if the filter could not be mapped or if it will never match. if (ldapFilter == null || ldapFilter == c.getConfig().getFalseFilter()) { if (ldapFilter == null || ldapFilter == c.getConfig().falseFilter()) { handler.handleResult(new QueryResult()); } else { entryContainer.listEntries(c, ldapFilter, ldapAttributes, searchHandler); final String[] tmp = getSearchAttributes(ldapAttributes); final ConnectionCompletionHandler<Result> outerHandler = new ConnectionCompletionHandler<Result>(searchHandler) { @Override public void handleResult(final Connection connection) { final SearchRequestCompletionHandler innerHandler = new SearchRequestCompletionHandler(connection, searchHandler); final SearchRequest request = Requests.newSearchRequest(baseDN, SearchScope.SINGLE_LEVEL, ldapFilter, tmp); connection.searchAsync(request, null, innerHandler); } }; factory.getConnectionAsync(outerHandler); } } }); }; getLDAPFilter(c, request.getQueryFilter(), filterHandler); } /** @@ -250,7 +373,10 @@ public void readInstance(final ServerContext context, final String resourceId, final ReadRequest request, final ResultHandler<Resource> handler) { final Context c = wrap(context); // @Checkstyle:off final Collection<String> ldapAttributes = getLDAPAttributes(c, request.getFieldFilters()); final String[] tmp = getSearchAttributes(ldapAttributes); // The handler which will be invoked for the LDAP search result. final org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry> searchHandler = new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() { @Override @@ -260,7 +386,7 @@ @Override public void handleResult(final SearchResultEntry entry) { final String revision = entryContainer.getEtagFromEntry(entry); final String revision = getEtagFromEntry(entry); final ResultHandler<Map<String, Object>> mapHandler = new ResultHandler<Map<String, Object>>() { @Override @@ -279,9 +405,25 @@ attributeMapper.toJSON(c, entry, mapHandler); } }; // @Checkstyle:on final Collection<String> ldapAttributes = getLDAPAttributes(c, request.getFieldFilters()); entryContainer.readEntry(c, resourceId, ldapAttributes, searchHandler); // The handler which will be invoked final ConnectionCompletionHandler<SearchResultEntry> outerHandler = new ConnectionCompletionHandler<SearchResultEntry>(searchHandler) { @Override public void handleResult(final Connection connection) { final RequestCompletionHandler<SearchResultEntry> innerHandler = new RequestCompletionHandler<SearchResultEntry>(connection, searchHandler); final SearchRequest request = Requests.newSearchRequest(baseDN, SearchScope.SINGLE_LEVEL, Filter .equality(UUID_ATTRIBUTE, resourceId), tmp); connection.searchSingleEntryAsync(request, innerHandler); } }; factory.getConnectionAsync(outerHandler); } /** @@ -325,6 +467,28 @@ } /** * Returns the ETag for the provided entry. * * @param entry * The entry. * @return The ETag. */ private String getEtagFromEntry(final Entry entry) { return entry.parseAttribute(ETAG_ATTRIBUTE).asString(); } /** * Returns the resource ID for the provided entry. * * @param entry * The entry. * @return The resource ID. */ private String getIDFromEntry(final Entry entry) { return entry.parseAttribute(UUID_ATTRIBUTE).asString(); } /** * Determines the set of LDAP attributes to request in an LDAP read (search, * post-read), based on the provided list of JSON pointers. * @@ -369,16 +533,16 @@ final Filter f = i.next(); if (f == null) { // Filter component did not match any attribute mappers. return c.getConfig().getFalseFilter(); } else if (f == c.getConfig().getFalseFilter()) { return c.getConfig().getFalseFilter(); } else if (f == c.getConfig().getTrueFilter()) { return c.getConfig().falseFilter(); } else if (f == c.getConfig().falseFilter()) { return c.getConfig().falseFilter(); } else if (f == c.getConfig().trueFilter()) { i.remove(); } } switch (value.size()) { case 0: return c.getConfig().getTrueFilter(); return c.getConfig().trueFilter(); case 1: return value.get(0); default: @@ -395,8 +559,8 @@ @Override public Void visitBooleanLiteralFilter(final ResultHandler<Filter> p, final boolean value) { p.handleResult(value ? c.getConfig().getTrueFilter() : c.getConfig() .getFalseFilter()); p.handleResult(value ? c.getConfig().trueFilter() : c.getConfig() .falseFilter()); return null; } @@ -465,11 +629,11 @@ public Filter apply(final Filter value, final Void p) { if (value == null) { // Filter component did not match any attribute mappers. return c.getConfig().getTrueFilter(); } else if (value == c.getConfig().getFalseFilter()) { return c.getConfig().getTrueFilter(); } else if (value == c.getConfig().getTrueFilter()) { return c.getConfig().getFalseFilter(); return c.getConfig().trueFilter(); } else if (value == c.getConfig().falseFilter()) { return c.getConfig().trueFilter(); } else if (value == c.getConfig().trueFilter()) { return c.getConfig().falseFilter(); } else { return Filter.not(value); } @@ -494,15 +658,15 @@ if (f == null) { // Filter component did not match any attribute mappers. i.remove(); } else if (f == c.getConfig().getFalseFilter()) { } else if (f == c.getConfig().falseFilter()) { i.remove(); } else if (f == c.getConfig().getTrueFilter()) { return c.getConfig().getTrueFilter(); } else if (f == c.getConfig().trueFilter()) { return c.getConfig().trueFilter(); } } switch (value.size()) { case 0: return c.getConfig().getFalseFilter(); return c.getConfig().falseFilter(); case 1: return value.get(0); default: @@ -536,7 +700,16 @@ queryFilter.accept(visitor, h); } private Context wrap(ServerContext context) { private String[] getSearchAttributes(final Collection<String> attributes) { // FIXME: who is responsible for adding the UUID and etag attributes to // this search? final String[] tmp = attributes.toArray(new String[attributes.size() + 2]); tmp[tmp.length - 2] = UUID_ATTRIBUTE; tmp[tmp.length - 1] = ETAG_ATTRIBUTE; return tmp; } private Context wrap(final ServerContext context) { return new Context(config, context); } } opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
@@ -173,7 +173,7 @@ break; case EXTENDED: default: filter = c.getConfig().getFalseFilter(); // Not supported. filter = c.getConfig().falseFilter(); // Not supported. break; } return filter; opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Example.java
@@ -50,50 +50,52 @@ */ public static void main(final String[] args) throws Exception { // All LDAP resources will use this connection factory. final ConnectionFactory ldapFactory = newAuthenticatedConnectionFactory( new LDAPConnectionFactory("localhost", 1389), Requests.newSimpleBindRequest( "cn=directory manager", "password".toCharArray())); // Create two entry containers whose members reference each other. final EntryContainer userContainer = new EntryContainer(DN .valueOf("ou=people,dc=example,dc=com"), ldapFactory); final EntryContainer groupContainer = new EntryContainer(DN .valueOf("ou=groups,dc=example,dc=com"), ldapFactory); final ConnectionFactory ldapFactory = newAuthenticatedConnectionFactory(new LDAPConnectionFactory("localhost", 1389), Requests.newSimpleBindRequest("cn=directory manager", "password" .toCharArray())); // Create user resource. final AttributeMapper userMapper = new CompositeAttributeMapper().addMapper( new SimpleAttributeMapper("id", "entryUUID").singleValued(true)).addMapper( new DefaultAttributeMapper().includeAttribute("uid", "isMemberOf", "modifyTimestamp")).addMapper( new ComplexAttributeMapper("name", new DefaultAttributeMapper().includeAttribute( "cn", "sn", "givenName"))).addMapper( new ComplexAttributeMapper("contactInformation", new CompositeAttributeMapper() .addMapper( new SimpleAttributeMapper("telephoneNumber").decoder( Functions.byteStringToString()).singleValued(true)) .addMapper( new SimpleAttributeMapper("emailAddress", "mail") .singleValued(true)))); final LDAPCollectionResourceProvider userResource = new LDAPCollectionResourceProvider( userContainer, userMapper); final AttributeMapper userMapper = new CompositeAttributeMapper().addMapper( new SimpleAttributeMapper("id", "entryUUID").singleValued(true)).addMapper( new DefaultAttributeMapper().includeAttribute("uid", "isMemberOf", "modifyTimestamp")).addMapper( new ComplexAttributeMapper("name", new DefaultAttributeMapper() .includeAttribute("cn", "sn", "givenName"))).addMapper( new ComplexAttributeMapper("contactInformation", new CompositeAttributeMapper().addMapper( new SimpleAttributeMapper("telephoneNumber").decoder( Functions.byteStringToString()).singleValued(true)) .addMapper( new SimpleAttributeMapper("emailAddress", "mail") .singleValued(true)))); final LDAPCollectionResourceProvider userResource = new LDAPCollectionResourceProvider(DN.valueOf("ou=people,dc=example,dc=com"), userMapper, ldapFactory, Config.defaultConfig()); // Create group resource. final AttributeMapper groupMapper = new DefaultAttributeMapper().includeAttribute("cn", "ou", "description", "uniquemember"); final LDAPCollectionResourceProvider groupResource = new LDAPCollectionResourceProvider( groupContainer, groupMapper); final AttributeMapper groupMapper = new DefaultAttributeMapper().includeAttribute("cn", "ou", "description", "uniquemember"); final LDAPCollectionResourceProvider groupResource = new LDAPCollectionResourceProvider(DN.valueOf("ou=groups,dc=example,dc=com"), groupMapper, ldapFactory, Config.defaultConfig()); // Create the router. final Router router = new Router(); router.addRoute("/users", userResource); router.addRoute("/groups", groupResource); final org.forgerock.json.resource.ConnectionFactory resourceFactory = newInternalConnectionFactory(router); final org.forgerock.json.resource.ConnectionFactory resourceFactory = newInternalConnectionFactory(router); final HttpServer httpServer = HttpServer.createSimpleServer("./", PORT); try { final WebappContext ctx = new WebappContext("example", "/example"); final ServletRegistration reg = ctx.addServlet("managed", new HttpServlet( resourceFactory)); final ServletRegistration reg = ctx.addServlet("managed", new HttpServlet(resourceFactory)); reg.addMapping("/managed/*"); ctx.deploy(httpServer);