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}