001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2016 ForgeRock AS.
015 */
016package org.forgerock.opendj.rest2ldap;
017
018import static org.forgerock.http.routing.RoutingMode.EQUALS;
019import static org.forgerock.http.routing.RoutingMode.STARTS_WITH;
020import static org.forgerock.json.resource.RouteMatchers.requestUriMatcher;
021import static org.forgerock.opendj.ldap.Filter.objectClassPresent;
022import static org.forgerock.opendj.ldap.SearchScope.BASE_OBJECT;
023import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
024import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNSUPPORTED_REQUEST_AGAINST_SINGLETON;
025import static org.forgerock.opendj.rest2ldap.RoutingContext.newRoutingContext;
026import static org.forgerock.util.promise.Promises.newResultPromise;
027
028import org.forgerock.api.models.ApiDescription;
029import org.forgerock.http.ApiProducer;
030import org.forgerock.json.resource.ActionRequest;
031import org.forgerock.json.resource.ActionResponse;
032import org.forgerock.json.resource.BadRequestException;
033import org.forgerock.json.resource.CreateRequest;
034import org.forgerock.json.resource.PatchRequest;
035import org.forgerock.json.resource.QueryRequest;
036import org.forgerock.json.resource.QueryResourceHandler;
037import org.forgerock.json.resource.QueryResponse;
038import org.forgerock.json.resource.ReadRequest;
039import org.forgerock.json.resource.Request;
040import org.forgerock.json.resource.ResourceException;
041import org.forgerock.json.resource.ResourceResponse;
042import org.forgerock.json.resource.Router;
043import org.forgerock.json.resource.UpdateRequest;
044import org.forgerock.opendj.ldap.DN;
045import org.forgerock.opendj.ldap.Entry;
046import org.forgerock.opendj.ldap.requests.SearchRequest;
047import org.forgerock.services.context.Context;
048import org.forgerock.util.AsyncFunction;
049import org.forgerock.util.Function;
050import org.forgerock.util.promise.Promise;
051
052/**
053 * Represents a one to one relationship between a parent resource and a child sub-resource. Removal of the parent
054 * resource implies that the child (the sub-resource) is also removed. Singletons only support read, update, patch, and
055 * action requests.
056 */
057public final class SubResourceSingleton extends SubResource {
058    /**
059     * A simple naming strategy that allows singletons to use the same processing logic as collections. The passed in
060     * resource ID will always be {@code null}.
061     */
062    private static final NamingStrategy SINGLETON_NAMING_STRATEGY = new NamingStrategy() {
063        @Override
064        public SearchRequest createSearchRequest(final DN baseDn, final String resourceId) {
065            return newSearchRequest(baseDn, BASE_OBJECT, objectClassPresent());
066        }
067
068        @Override
069        public String getResourceIdLdapAttribute() {
070            // Nothing to do.
071            return null;
072        }
073
074        @Override
075        public String decodeResourceId(final Entry entry) {
076            // It's safe to return null. The resource response will default to the _id field if present.
077            return null;
078        }
079
080        @Override
081        public void encodeResourceId(final DN baseDn, final String resourceId, final Entry entry)
082                throws ResourceException {
083            // Nothing to do because singletons cannot be created.
084        }
085    };
086
087    SubResourceSingleton(final String resourceId) {
088        super(resourceId);
089    }
090
091    /**
092     * Sets the relative URL template of the single sub-resource. The template must comprise of at least one path
093     * element. Any URL template variables will be substituted into the {@link #dnTemplate(String) DN template}.
094     *
095     * @param urlTemplate
096     *         The relative URL template.
097     * @return A reference to this object.
098     */
099    public SubResourceSingleton urlTemplate(final String urlTemplate) {
100        this.urlTemplate = urlTemplate;
101        return this;
102    }
103
104    /**
105     * Sets the relative DN template of the single sub-resource LDAP entry. The template must comprise of at least one
106     * RDN. Any DN template variables will be substituted using values extracted from the {@link #urlTemplate(String)
107     * URL template}.
108     *
109     * @param dnTemplate
110     *         The relative DN template.
111     * @return A reference to this object.
112     */
113    public SubResourceSingleton dnTemplate(final String dnTemplate) {
114        this.dnTemplateString = dnTemplate;
115        return this;
116    }
117
118    /**
119     * Indicates whether this sub-resource singleton only supports read operations.
120     *
121     * @param readOnly
122     *         {@code true} if this sub-resource singleton is read-only.
123     * @return A reference to this object.
124     */
125    public SubResourceSingleton isReadOnly(final boolean readOnly) {
126        isReadOnly = readOnly;
127        return this;
128    }
129
130    @Override
131    Router addRoutes(final Router router) {
132        router.addRoute(requestUriMatcher(EQUALS, urlTemplate), readOnly(new InstanceHandler()));
133        router.addRoute(requestUriMatcher(STARTS_WITH, urlTemplate), readOnly(new SubResourceHandler()));
134        return router;
135    }
136
137    Promise<RoutingContext, ResourceException> route(final Context context) {
138        return newResultPromise(newRoutingContext(context, dnFrom(context), resource));
139    }
140
141    private SubResourceImpl singleton(final Context context) {
142        return new SubResourceImpl(rest2Ldap, dnFrom(context), null, SINGLETON_NAMING_STRATEGY, resource);
143    }
144
145    /**
146     * Responsible for processing instance requests (RUPA) against this singleton and collection requests (CQ) to
147     * any collections sharing the same base URL as this singleton. More specifically, given the
148     * URL template /singleton/{child} then this handler processes requests against /singleton since it is
149     * both a singleton and also a collection of {child}.
150     */
151    private final class InstanceHandler extends AbstractRequestHandler {
152        @Override
153        public Promise<ActionResponse, ResourceException> handleAction(final Context context,
154                                                                       final ActionRequest request) {
155            return singleton(context).action(context, null, request);
156        }
157
158        @Override
159        public Promise<ResourceResponse, ResourceException> handleCreate(final Context context,
160                                                                         final CreateRequest request) {
161            return route(context)
162                    .thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
163                        @Override
164                        public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
165                            return subResourceRouterFrom(context).handleCreate(context, request);
166                        }
167                    }).thenCatch(this.<ResourceResponse>convert404To400());
168        }
169
170        @Override
171        public Promise<ResourceResponse, ResourceException> handlePatch(final Context context,
172                                                                        final PatchRequest request) {
173            return singleton(context).patch(context, null, request);
174        }
175
176        @Override
177        public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request,
178                                                                     final QueryResourceHandler handler) {
179            return route(context)
180                    .thenAsync(new AsyncFunction<RoutingContext, QueryResponse, ResourceException>() {
181                        @Override
182                        public Promise<QueryResponse, ResourceException> apply(final RoutingContext context) {
183                            return subResourceRouterFrom(context).handleQuery(context, request, handler);
184                        }
185                    }).thenCatch(this.<QueryResponse>convert404To400());
186        }
187
188        @Override
189        public Promise<ResourceResponse, ResourceException> handleRead(final Context context,
190                                                                       final ReadRequest request) {
191            return singleton(context).read(context, null, request);
192        }
193
194        @Override
195        public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context,
196                                                                         final UpdateRequest request) {
197            return singleton(context).update(context, null, request);
198        }
199
200        @Override
201        protected <V> Promise<V, ResourceException> handleRequest(final Context context, final Request request) {
202            return new BadRequestException(ERR_UNSUPPORTED_REQUEST_AGAINST_SINGLETON.get().toString()).asPromise();
203        }
204
205        private <T> Function<ResourceException, T, ResourceException> convert404To400() {
206            return SubResource.convert404To400(ERR_UNSUPPORTED_REQUEST_AGAINST_SINGLETON.get());
207        }
208
209        @Override
210        public ApiDescription api(ApiProducer<ApiDescription> producer) {
211            return getResource().instanceApi(isReadOnly);
212        }
213    }
214}