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 java.util.Arrays.asList; 020import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ABSTRACT_TYPE_IN_CREATE; 021import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_MISSING_TYPE_PROPERTY_IN_CREATE; 022import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE; 023import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_TYPE_IN_CREATE; 024import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException; 025import static org.forgerock.util.Utils.joinAsString; 026 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collection; 030import java.util.HashSet; 031import java.util.LinkedHashMap; 032import java.util.LinkedHashSet; 033import java.util.List; 034import java.util.Map; 035import java.util.Set; 036 037import org.forgerock.i18n.LocalizedIllegalArgumentException; 038import org.forgerock.json.JsonPointer; 039import org.forgerock.json.JsonValue; 040import org.forgerock.json.resource.RequestHandler; 041import org.forgerock.json.resource.ResourceException; 042import org.forgerock.json.resource.Router; 043import org.forgerock.opendj.ldap.Attribute; 044import org.forgerock.opendj.ldap.Entry; 045import org.forgerock.opendj.ldap.LinkedAttribute; 046 047/** 048 * Defines the characteristics of a resource, including its properties, inheritance, and sub-resources. 049 */ 050public final class Resource { 051 /** The resource ID. */ 052 private final String id; 053 /** {@code true} if only sub-types of this resource can be created. */ 054 private boolean isAbstract; 055 /** The ID of the super-type of this resource, may be {@code null}. */ 056 private String superTypeId; 057 /** The LDAP object classes associated with this resource. */ 058 private final Attribute objectClasses = new LinkedAttribute("objectClass"); 059 /** The possibly empty set of sub-resources. */ 060 private final Set<SubResource> subResources = new LinkedHashSet<>(); 061 /** The set of property mappers associated with this resource, excluding inherited properties. */ 062 private final Map<String, PropertyMapper> declaredProperties = new LinkedHashMap<>(); 063 /** The set of property mappers associated with this resource, including inherited properties. */ 064 private final Map<String, PropertyMapper> allProperties = new LinkedHashMap<>(); 065 /** 066 * A JSON pointer to the primitive JSON property that will be used to convey type information. May be {@code 067 * null} if the type property is defined in a super type or if this resource does not have any sub-types. 068 */ 069 private JsonPointer resourceTypeProperty; 070 /** Set to {@code true} once this Resource has been built. */ 071 private boolean isBuilt = false; 072 /** The resolved super-type. */ 073 private Resource superType; 074 /** The resolved sub-resources (only immediate children). */ 075 private final Set<Resource> subTypes = new LinkedHashSet<>(); 076 /** The property mapper which will map all properties for this resource including inherited properties. */ 077 private final ObjectPropertyMapper propertyMapper = new ObjectPropertyMapper(); 078 /** Routes requests to sub-resources. */ 079 private final Router subResourceRouter = new Router(); 080 private volatile Boolean hasSubTypesWithSubResources = null; 081 /** The set of actions supported by this resource and its sub-types. */ 082 private final Set<Action> supportedActions = new HashSet<>(); 083 084 Resource(final String id) { 085 this.id = id; 086 } 087 088 /** 089 * Returns the resource ID of this resource. 090 * 091 * @return The resource ID of this resource. 092 */ 093 @Override 094 public String toString() { 095 return id; 096 } 097 098 /** 099 * Returns {@code true} if the provided parameter is a {@code Resource} having the same resource ID as this 100 * resource. 101 * 102 * @param o 103 * The object to compare. 104 * @return {@code true} if the provided parameter is a {@code Resource} having the same resource ID as this 105 * resource. 106 */ 107 @Override 108 public boolean equals(final Object o) { 109 return this == o || (o instanceof Resource && id.equals(((Resource) o).id)); 110 } 111 112 @Override 113 public int hashCode() { 114 return id.hashCode(); 115 } 116 117 /** 118 * Specifies the resource ID of the resource which is a super-type of this resource. This resource will inherit 119 * the properties and sub-resources of the super-type, and may optionally override them. 120 * 121 * @param resourceId 122 * The resource ID of the resource which is a super-type of this resource, or {@code null} if there is no 123 * super-type. 124 * @return A reference to this object. 125 */ 126 public Resource superType(final String resourceId) { 127 this.superTypeId = resourceId; 128 return this; 129 } 130 131 /** 132 * Specifies whether this resource is an abstract type and therefore cannot be created. Only non-abstract 133 * sub-types can be created. 134 * 135 * @param isAbstract 136 * {@code true} if this resource is abstract. 137 * @return A reference to this object. 138 */ 139 public Resource isAbstract(final boolean isAbstract) { 140 this.isAbstract = isAbstract; 141 return this; 142 } 143 144 /** 145 * Specifies a mapping for a property contained in this JSON resource. Properties are inherited and sub-types may 146 * override them. Properties are optional: a resource that does not have any properties cannot be created, read, 147 * or modified, and may only be used for accessing sub-resources. These resources usually represent API 148 * "endpoints". 149 * 150 * @param name 151 * The name of the JSON property to be mapped. 152 * @param mapper 153 * The property mapper responsible for mapping the JSON property to LDAP attribute(s). 154 * @return A reference to this object. 155 */ 156 public Resource property(final String name, final PropertyMapper mapper) { 157 declaredProperties.put(name, mapper); 158 return this; 159 } 160 161 /** 162 * Specifies whether all LDAP user attributes should be mapped by default using the default schema based mapping 163 * rules. Individual attributes can be excluded using {@link #excludedDefaultUserAttributes} in order to prevent 164 * attributes with explicit mappings being mapped twice. 165 * 166 * @param include {@code true} if all LDAP user attributes be mapped by default. 167 * @return A reference to this object. 168 */ 169 public Resource includeAllUserAttributesByDefault(final boolean include) { 170 propertyMapper.includeAllUserAttributesByDefault(include); 171 return this; 172 } 173 174 /** 175 * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when 176 * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be 177 * excluded in order to prevent duplication. 178 * 179 * @param attributeNames The list of attributes to be excluded. 180 * @return A reference to this object. 181 */ 182 public Resource excludedDefaultUserAttributes(final String... attributeNames) { 183 return excludedDefaultUserAttributes(Arrays.asList(attributeNames)); 184 } 185 186 /** 187 * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when 188 * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be 189 * excluded in order to prevent duplication. 190 * 191 * @param attributeNames The list of attributes to be excluded. 192 * @return A reference to this object. 193 */ 194 public Resource excludedDefaultUserAttributes(final Collection<String> attributeNames) { 195 propertyMapper.excludedDefaultUserAttributes(attributeNames); 196 return this; 197 } 198 199 /** 200 * Specifies the name of the JSON property which contains the resource's type, whose value is the 201 * resource ID. The resource type property is inherited by sub-types and must be available to any resources 202 * referenced from {@link SubResource sub-resources}. 203 * 204 * @param resourceTypeProperty 205 * The name of the JSON property which contains the resource's type, or {@code null} if this resource does 206 * not have a resource type property or if it should be inherited from a super-type. 207 * @return A reference to this object. 208 */ 209 public Resource resourceTypeProperty(final JsonPointer resourceTypeProperty) { 210 this.resourceTypeProperty = resourceTypeProperty; 211 return this; 212 } 213 214 /** 215 * Specifies an LDAP object class which is to be associated with this resource. Multiple object classes may be 216 * specified. The object classes are used for determining the type of resource being accessed during all requests 217 * other than create. Object classes are inherited by sub-types and must be defined for any resources that are 218 * non-abstract and which can be created. 219 * 220 * @param objectClass 221 * An LDAP object class associated with this resource's LDAP representation. 222 * @return A reference to this object. 223 */ 224 public Resource objectClass(final String objectClass) { 225 this.objectClasses.add(objectClass); 226 return this; 227 } 228 229 /** 230 * Specifies LDAP object classes which are to be associated with this resource. Multiple object classes may be 231 * specified. The object classes are used for determining the type of resource being accessed during all requests 232 * other than create. Object classes are inherited by sub-types and must be defined for any resources that are 233 * non-abstract and which can be created. 234 * 235 * @param objectClasses 236 * The LDAP object classes associated with this resource's LDAP representation. 237 * @return A reference to this object. 238 */ 239 public Resource objectClasses(final String... objectClasses) { 240 this.objectClasses.add((Object[]) objectClasses); 241 return this; 242 } 243 244 /** 245 * Registers an action which should be supported by this resource. By default, no actions are supported. 246 * 247 * @param action 248 * The action supported by this resource. 249 * @return A reference to this object. 250 */ 251 public Resource supportedAction(final Action action) { 252 this.supportedActions.add(action); 253 return this; 254 } 255 256 /** 257 * Registers zero or more actions which should be supported by this resource. By default, no actions are supported. 258 * 259 * @param actions 260 * The actions supported by this resource. 261 * @return A reference to this object. 262 */ 263 public Resource supportedActions(final Action... actions) { 264 this.supportedActions.addAll(Arrays.asList(actions)); 265 return this; 266 } 267 268 /** 269 * Specifies a parent-child relationship with another resource. Sub-resources are inherited by sub-types and may 270 * be overridden. 271 * 272 * @param subResource 273 * The sub-resource definition. 274 * @return A reference to this object. 275 */ 276 public Resource subResource(final SubResource subResource) { 277 this.subResources.add(subResource); 278 return this; 279 } 280 281 /** 282 * Specifies a parent-child relationship with zero or more resources. Sub-resources are inherited by sub-types and 283 * may be overridden. 284 * 285 * @param subResources 286 * The sub-resource definitions. 287 * @return A reference to this object. 288 */ 289 public Resource subResources(final SubResource... subResources) { 290 this.subResources.addAll(asList(subResources)); 291 return this; 292 } 293 294 boolean hasSupportedAction(final Action action) { 295 return supportedActions.contains(action); 296 } 297 298 boolean hasSubTypes() { 299 return !subTypes.isEmpty(); 300 } 301 302 boolean mayHaveSubResources() { 303 return !subResources.isEmpty() || hasSubTypesWithSubResources(); 304 } 305 306 boolean hasSubTypesWithSubResources() { 307 if (hasSubTypesWithSubResources == null) { 308 for (final Resource subType : subTypes) { 309 if (!subType.subResources.isEmpty() || subType.hasSubTypesWithSubResources()) { 310 hasSubTypesWithSubResources = true; 311 return true; 312 } 313 } 314 hasSubTypesWithSubResources = false; 315 } 316 return hasSubTypesWithSubResources; 317 } 318 319 Set<Resource> getSubTypes() { 320 return subTypes; 321 } 322 323 Resource resolveSubTypeFromJson(final JsonValue content) throws ResourceException { 324 if (!hasSubTypes()) { 325 // The resource type is implied because this resource does not have sub-types. In particular, resources 326 // are not required to have type information if they don't have sub-types. 327 return this; 328 } 329 final JsonValue jsonType = content.get(resourceTypeProperty); 330 if (jsonType == null || !jsonType.isString()) { 331 throw newBadRequestException(ERR_MISSING_TYPE_PROPERTY_IN_CREATE.get(resourceTypeProperty)); 332 } 333 final String type = jsonType.asString(); 334 final Resource subType = resolveSubTypeFromString(type); 335 if (subType == null) { 336 throw newBadRequestException(ERR_UNRECOGNIZED_TYPE_IN_CREATE.get(type, getAllowedResourceTypes())); 337 } 338 if (subType.isAbstract) { 339 throw newBadRequestException(ERR_ABSTRACT_TYPE_IN_CREATE.get(type, getAllowedResourceTypes())); 340 } 341 return subType; 342 } 343 344 private String getAllowedResourceTypes() { 345 final List<String> allowedTypes = new ArrayList<>(); 346 getAllowedResourceTypes(allowedTypes); 347 return joinAsString(", ", allowedTypes); 348 } 349 350 private void getAllowedResourceTypes(final List<String> allowedTypes) { 351 if (!isAbstract) { 352 allowedTypes.add(id); 353 } 354 for (final Resource subType : subTypes) { 355 subType.getAllowedResourceTypes(allowedTypes); 356 } 357 } 358 359 Resource resolveSubTypeFromString(final String type) { 360 if (id.equalsIgnoreCase(type)) { 361 return this; 362 } 363 for (final Resource subType : subTypes) { 364 final Resource resolvedSubType = subType.resolveSubTypeFromString(type); 365 if (resolvedSubType != null) { 366 return resolvedSubType; 367 } 368 } 369 return null; 370 } 371 372 Resource resolveSubTypeFromObjectClasses(final Entry entry) { 373 if (!hasSubTypes()) { 374 // This resource does not have sub-types. 375 return this; 376 } 377 final Attribute objectClassesFromEntry = entry.getAttribute("objectClass"); 378 final Resource subType = resolveSubTypeFromObjectClasses(objectClassesFromEntry); 379 if (subType == null) { 380 // Best effort. 381 return this; 382 } 383 return subType; 384 } 385 386 private Resource resolveSubTypeFromObjectClasses(final Attribute objectClassesFromEntry) { 387 if (!objectClassesFromEntry.containsAll(objectClasses)) { 388 return null; 389 } 390 // This resource is a potential match, but sub-types may be better. 391 for (final Resource subType : subTypes) { 392 final Resource resolvedSubType = subType.resolveSubTypeFromObjectClasses(objectClassesFromEntry); 393 if (resolvedSubType != null) { 394 return resolvedSubType; 395 } 396 } 397 return this; 398 } 399 400 Attribute getObjectClassAttribute() { 401 return objectClasses; 402 } 403 404 RequestHandler getSubResourceRouter() { 405 return subResourceRouter; 406 } 407 408 String getResourceId() { 409 return id; 410 } 411 412 void build(final Rest2Ldap rest2Ldap) { 413 // Prevent re-entrant calls. 414 if (isBuilt) { 415 return; 416 } 417 isBuilt = true; 418 419 if (superTypeId != null) { 420 superType = rest2Ldap.getResource(superTypeId); 421 if (superType == null) { 422 throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE.get(id, superTypeId)); 423 } 424 // Inherit content from super-type. 425 superType.build(rest2Ldap); 426 superType.subTypes.add(this); 427 if (resourceTypeProperty == null) { 428 resourceTypeProperty = superType.resourceTypeProperty; 429 } 430 objectClasses.addAll(superType.objectClasses); 431 subResourceRouter.addAllRoutes(superType.subResourceRouter); 432 allProperties.putAll(superType.allProperties); 433 } 434 allProperties.putAll(declaredProperties); 435 for (final Map.Entry<String, PropertyMapper> property : allProperties.entrySet()) { 436 propertyMapper.property(property.getKey(), property.getValue()); 437 } 438 for (final SubResource subResource : subResources) { 439 subResource.build(rest2Ldap, id); 440 subResource.addRoutes(subResourceRouter); 441 } 442 } 443 444 PropertyMapper getPropertyMapper() { 445 return propertyMapper; 446 } 447}