/* * 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 legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * * Portions copyright 2011-2015 ForgeRock AS. */ package org.forgerock.openidm.managed; import static org.forgerock.json.resource.Resources.newCollection; import static org.forgerock.json.resource.Router.uriTemplate; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicReference; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.ConfigurationPolicy; import org.apache.felix.scr.annotations.Deactivate; import org.apache.felix.scr.annotations.Modified; import org.apache.felix.scr.annotations.Properties; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.ReferenceCardinality; import org.apache.felix.scr.annotations.ReferencePolicy; import org.apache.felix.scr.annotations.Service; import org.forgerock.json.resource.ResourcePath; import org.forgerock.services.context.Context; import org.forgerock.services.routing.RouteMatcher; import org.forgerock.http.routing.RoutingMode; import org.forgerock.json.JsonPointer; import org.forgerock.json.JsonValue; import org.forgerock.json.resource.ActionRequest; import org.forgerock.json.resource.ActionResponse; import org.forgerock.json.resource.CreateRequest; import org.forgerock.json.resource.DeleteRequest; import org.forgerock.json.resource.PatchRequest; import org.forgerock.json.resource.QueryRequest; import org.forgerock.json.resource.QueryResourceHandler; import org.forgerock.json.resource.QueryResponse; import org.forgerock.json.resource.ReadRequest; import org.forgerock.json.resource.Request; import org.forgerock.json.resource.RequestHandler; import org.forgerock.json.resource.ResourceException; import org.forgerock.json.resource.ResourceResponse; import org.forgerock.json.resource.Router; import org.forgerock.json.resource.UpdateRequest; import org.forgerock.openidm.config.enhanced.EnhancedConfig; import org.forgerock.openidm.core.ServerConstants; import org.forgerock.openidm.crypto.CryptoService; import org.forgerock.openidm.router.IDMConnectionFactory; import org.forgerock.openidm.router.RouteService; import org.forgerock.script.ScriptRegistry; import org.forgerock.util.promise.Promise; import org.osgi.framework.Constants; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.ComponentException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Provides access to managed objects. * */ @Component(name = ManagedObjectService.PID, immediate = true, policy = ConfigurationPolicy.REQUIRE) @Service @Properties({ @Property(name = Constants.SERVICE_DESCRIPTION, value = "OpenIDM managed objects service"), @Property(name = Constants.SERVICE_VENDOR, value = ServerConstants.SERVER_VENDOR_NAME), @Property(name = ServerConstants.ROUTER_PREFIX, value = "/managed*") }) public class ManagedObjectService implements RequestHandler { public static final String PID = "org.forgerock.openidm.managed"; /** * Setup logging for the {@link ManagedObjectService}. */ private final static Logger logger = LoggerFactory.getLogger(ManagedObjectService.class); /** Cryptographic service. */ @Reference(policy = ReferencePolicy.DYNAMIC) protected CryptoService cryptoService; /** Script Registry service. */ @Reference(policy = ReferencePolicy.DYNAMIC) protected ScriptRegistry scriptRegistry; /** * Route service on "sync" endpoint. An aspect of CRUDPAQ on managed objects is to synchronize their * attributes with remote system if configured. As this message is sent over the router, we need to * know if the SynchronizationService is available. This optional reference is used to indicate that * availability. */ @Reference(referenceInterface = RouteService.class, policy = ReferencePolicy.DYNAMIC, bind = "bindSyncRoute", unbind = "unbindSyncRoute", cardinality = ReferenceCardinality.OPTIONAL_UNARY, target = "(" + ServerConstants.ROUTER_PREFIX + "=/sync*)") private final AtomicReference<RouteService> syncRoute = new AtomicReference<RouteService>(); @SuppressWarnings("unused") private void bindSyncRoute(final RouteService service) { syncRoute.set(service); } @SuppressWarnings("unused") private void unbindSyncRoute(final RouteService service) { syncRoute.set(null); } /* The Connection Factory */ @Reference(policy = ReferencePolicy.STATIC) protected IDMConnectionFactory connectionFactory; /** Enhanced configuration service. */ @Reference(policy = ReferencePolicy.DYNAMIC) private EnhancedConfig enhancedConfig; private final ConcurrentMap<String, RouteMatcher<Request>> managedRoutes = new ConcurrentHashMap<String, RouteMatcher<Request>>(); private final Router managedRouter = new Router(); /** * RequestHandler to handle requests for both a {@link ManagedObjectSet} and its nested * {@link RelationshipProvider}s. * * Requests to {@code /} and {@code /{id}} will be directed to the object set. * Requests starting with {@code /{id}/{relationshipField}} will be directed to their relationship provider. */ private class ManagedObjectSetRequestHandler implements RequestHandler { final ManagedObjectSet objectSet; final RequestHandler objectSetRequestHandler; ManagedObjectSetRequestHandler(final ManagedObjectSet objectSet) { this.objectSet = objectSet; this.objectSetRequestHandler = newCollection(objectSet); } private RequestHandler requestHandler(final Request request) { final ResourcePath path = request.getResourcePathObject(); if (path.size() <= 1) { // Either / or /{id}. Use the objectSet return objectSetRequestHandler; } else { // /{id}/{relationshipField}/... use the relationship provider final RelationshipProvider provider = objectSet.getRelationshipProviders().get(new JsonPointer(path.get(1))); if (provider != null) { return provider.asRequestHandler(); } else { // If we don't have a relationship provider for this endpoint send it back ot the objectset // to get a proper 404 response. return objectSetRequestHandler; } } } @Override public Promise<ActionResponse, ResourceException> handleAction(Context context, ActionRequest request) { return requestHandler(request).handleAction(context, request); } @Override public Promise<ResourceResponse, ResourceException> handleCreate(Context context, CreateRequest request) { return requestHandler(request).handleCreate(context, request); } @Override public Promise<ResourceResponse, ResourceException> handleDelete(Context context, DeleteRequest request) { return requestHandler(request).handleDelete(context, request); } @Override public Promise<ResourceResponse, ResourceException> handlePatch(Context context, PatchRequest request) { return requestHandler(request).handlePatch(context, request); } @Override public Promise<QueryResponse, ResourceException> handleQuery(Context context, QueryRequest request, QueryResourceHandler handler) { return requestHandler(request).handleQuery(context, request, handler); } @Override public Promise<ResourceResponse, ResourceException> handleRead(Context context, ReadRequest request) { return requestHandler(request).handleRead(context, request); } @Override public Promise<ResourceResponse, ResourceException> handleUpdate(Context context, UpdateRequest request) { return requestHandler(request).handleUpdate(context, request); } } /** * Activates the component * * @param context the {@link ComponentContext} object for this component. */ @Activate protected void activate(ComponentContext context) throws Exception { JsonValue configuration = enhancedConfig.getConfigurationAsJson(context); for (JsonValue managedObjectConfig : configuration.get("objects").expect(List.class)) { final ManagedObjectSet objectSet = new ManagedObjectSet(scriptRegistry, cryptoService, syncRoute, connectionFactory, managedObjectConfig); if (managedRoutes.containsKey(objectSet.getName())) { throw new ComponentException("Duplicate definition of managed object type: " + objectSet.getName()); } managedRoutes.put(objectSet.getName(), managedRouter.addRoute(RoutingMode.STARTS_WITH, uriTemplate(objectSet.getTemplate()), new ManagedObjectSetRequestHandler(objectSet))); } } /** * Modifies the component * * @param context the {@link ComponentContext} object for this component. */ @Modified protected void modified(ComponentContext context) throws Exception { JsonValue configuration = enhancedConfig.getConfigurationAsJson(context); Set<String> routesToKeep = new HashSet<String>(); for (JsonValue value : configuration.get("objects").expect(List.class)) { ManagedObjectSet objectSet = new ManagedObjectSet(scriptRegistry, cryptoService, syncRoute, connectionFactory, value); if (routesToKeep.contains(objectSet.getName())) { throw new ComponentException("Duplicate definition of managed object type: " + objectSet.getName()); } RouteMatcher<Request> oldRoute = managedRoutes.get(objectSet.getName()); if (null != oldRoute) { managedRouter.removeRoute(oldRoute); } managedRoutes.put(objectSet.getName(), managedRouter.addRoute(RoutingMode.STARTS_WITH, uriTemplate(objectSet.getTemplate()), new ManagedObjectSetRequestHandler(objectSet))); routesToKeep.add(objectSet.getName()); } for (Map.Entry<String, RouteMatcher<Request>> entry : managedRoutes.entrySet()){ //Use ConcurrentMap to avoid ConcurrentModificationException with this iteration if (routesToKeep.contains(entry.getKey())) { continue; } managedRouter.removeRoute(managedRoutes.remove(entry.getKey())); } } /** * Deactivates the component * * @param context the {@link ComponentContext} object for this component. */ @Deactivate protected void deactivate(ComponentContext context) { managedRouter.removeAllRoutes(); managedRoutes.clear(); } @Override public Promise<ActionResponse, ResourceException> handleAction(final Context context, final ActionRequest request) { return managedRouter.handleAction(context, request); } @Override public Promise<ResourceResponse, ResourceException> handleCreate(final Context context, final CreateRequest request) { return managedRouter.handleCreate(context, request); } @Override public Promise<ResourceResponse, ResourceException> handleDelete(final Context context, final DeleteRequest request) { return managedRouter.handleDelete(context, request); } @Override public Promise<ResourceResponse, ResourceException> handlePatch(final Context context, final PatchRequest request) { return managedRouter.handlePatch(context, request); } @Override public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request, QueryResourceHandler queryResourceHandler) { return managedRouter.handleQuery(context, request, queryResourceHandler); } @Override public Promise<ResourceResponse, ResourceException> handleRead(final Context context, final ReadRequest request) { return managedRouter.handleRead(context, request); } @Override public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context, final UpdateRequest request) { return managedRouter.handleUpdate(context, request); } }