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 *
016 */
017package org.forgerock.opendj.rest2ldap;
018
019import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE;
020
021import org.forgerock.api.models.ApiDescription;
022import org.forgerock.http.ApiProducer;
023import org.forgerock.i18n.LocalizableMessage;
024import org.forgerock.i18n.LocalizedIllegalArgumentException;
025import org.forgerock.json.resource.ActionRequest;
026import org.forgerock.json.resource.ActionResponse;
027import org.forgerock.json.resource.BadRequestException;
028import org.forgerock.json.resource.CreateRequest;
029import org.forgerock.json.resource.DeleteRequest;
030import org.forgerock.json.resource.NotFoundException;
031import org.forgerock.json.resource.PatchRequest;
032import org.forgerock.json.resource.QueryRequest;
033import org.forgerock.json.resource.QueryResourceHandler;
034import org.forgerock.json.resource.QueryResponse;
035import org.forgerock.json.resource.ReadRequest;
036import org.forgerock.json.resource.RequestHandler;
037import org.forgerock.json.resource.ResourceException;
038import org.forgerock.json.resource.ResourceResponse;
039import org.forgerock.json.resource.Router;
040import org.forgerock.json.resource.UpdateRequest;
041import org.forgerock.opendj.ldap.DN;
042import org.forgerock.services.context.Context;
043import org.forgerock.util.AsyncFunction;
044import org.forgerock.util.Function;
045import org.forgerock.util.promise.Promise;
046
047/**
048 * Defines a parent-child relationship between a parent resource and one or more child resource(s). Removal of the
049 * parent resource implies that the children (the sub-resources) are also removed. There are two types of
050 * sub-resource:
051 * <ul>
052 * <li>{@link SubResourceSingleton} represents a one-to-one relationship supporting read, update, patch, and action
053 *     requests</li>
054 * <li>{@link SubResourceCollection} represents a one-to-many relationship supporting all requests.</li>
055 * </ul>
056 */
057public abstract class SubResource {
058    private final String resourceId;
059    private DnTemplate dnTemplate;
060
061    String urlTemplate = "";
062    String dnTemplateString = "";
063    boolean isReadOnly = false;
064    Rest2Ldap rest2Ldap;
065    Resource resource;
066
067    SubResource(final String resourceId) {
068        this.resourceId = resourceId;
069    }
070
071    @Override
072    public final boolean equals(final Object o) {
073        return this == o || (o instanceof SubResource && urlTemplate.equals(((SubResource) o).urlTemplate));
074    }
075
076    @Override
077    public final int hashCode() {
078        return urlTemplate.hashCode();
079    }
080
081    @Override
082    public final String toString() {
083        return urlTemplate;
084    }
085
086    final Resource getResource() {
087        return resource;
088    }
089
090    final void build(final Rest2Ldap rest2Ldap, final String parent) {
091        this.rest2Ldap = rest2Ldap;
092        this.resource = rest2Ldap.getResource(resourceId);
093        if (resource == null) {
094            throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE.get(parent, resourceId));
095        }
096        this.dnTemplate = DnTemplate.compileRelative(dnTemplateString);
097    }
098
099    abstract Router addRoutes(Router router);
100
101    /** A 404 indicates that this instance is not also a collection, so return a more helpful message. */
102    static <T> Function<ResourceException, T, ResourceException> convert404To400(final LocalizableMessage msg) {
103        return new Function<ResourceException, T, ResourceException>() {
104            @Override
105            public T apply(final ResourceException e) throws ResourceException {
106                if (e instanceof NotFoundException) {
107                    throw new BadRequestException(msg.toString());
108                }
109                throw e;
110            }
111        };
112    }
113
114    final RequestHandler readOnly(final RequestHandler handler) {
115        return isReadOnly ? new ReadOnlyRequestHandler(handler) : handler;
116    }
117
118    final DN dnFrom(final Context context) {
119        return dnTemplate.format(context);
120    }
121
122    final RequestHandler subResourceRouterFrom(final RoutingContext context) {
123        return context.getType().getSubResourceRouter();
124    }
125
126    abstract Promise<RoutingContext, ResourceException> route(final Context context);
127
128    /**
129     * Responsible for routing requests to sub-resources:
130     * <ul>
131     * <li>of this singleton,</li>
132     * <li>or of instances within a collection<./li>
133     * </ul>
134     * <p>
135     * More specifically, given
136     * <ul>
137     * <li>the URL template /singleton then this handler processes all requests beneath /singleton.</li>
138     * <li>the URL template /collection/{id} then this handler processes all requests beneath /collection/{id},</li>
139     * </ul>
140     */
141    final class SubResourceHandler extends AbstractRequestHandler {
142        @Override
143        public Promise<ActionResponse, ResourceException> handleAction(final Context context,
144                                                                       final ActionRequest request) {
145            return route(context).thenAsync(new AsyncFunction<RoutingContext, ActionResponse, ResourceException>() {
146                @Override
147                public Promise<ActionResponse, ResourceException> apply(final RoutingContext context) {
148                    return subResourceRouterFrom(context).handleAction(context, request);
149                }
150            });
151        }
152
153        @Override
154        public Promise<ResourceResponse, ResourceException> handleCreate(final Context context,
155                                                                         final CreateRequest request) {
156            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
157                @Override
158                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
159                    return subResourceRouterFrom(context).handleCreate(context, request);
160                }
161            });
162        }
163
164        @Override
165        public Promise<ResourceResponse, ResourceException> handleDelete(final Context context,
166                                                                         final DeleteRequest request) {
167            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
168                @Override
169                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
170                    return subResourceRouterFrom(context).handleDelete(context, request);
171                }
172            });
173        }
174
175        @Override
176        public Promise<ResourceResponse, ResourceException> handlePatch(final Context context,
177                                                                        final PatchRequest request) {
178            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
179                @Override
180                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
181                    return subResourceRouterFrom(context).handlePatch(context, request);
182                }
183            });
184        }
185
186        @Override
187        public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request,
188                                                                     final QueryResourceHandler handler) {
189            return route(context).thenAsync(new AsyncFunction<RoutingContext, QueryResponse, ResourceException>() {
190                @Override
191                public Promise<QueryResponse, ResourceException> apply(final RoutingContext context) {
192                    return subResourceRouterFrom(context).handleQuery(context, request, handler);
193                }
194            });
195        }
196
197        @Override
198        public Promise<ResourceResponse, ResourceException> handleRead(final Context context,
199                                                                       final ReadRequest request) {
200            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
201                @Override
202                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
203                    return subResourceRouterFrom(context).handleRead(context, request);
204                }
205            });
206        }
207
208        @Override
209        public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context,
210                                                                         final UpdateRequest request) {
211            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
212                @Override
213                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
214                    return subResourceRouterFrom(context).handleUpdate(context, request);
215                }
216            });
217        }
218
219        @Override
220        public ApiDescription api(ApiProducer<ApiDescription> producer) {
221            return resource.subResourcesApi(producer);
222        }
223    }
224}