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.Rest2Ldap.DECODE_OPTIONS; 020import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE; 021 022import java.util.ArrayList; 023import java.util.List; 024import java.util.Map; 025import java.util.regex.Matcher; 026import java.util.regex.Pattern; 027 028import org.forgerock.http.routing.UriRouterContext; 029import org.forgerock.i18n.LocalizableMessage; 030import org.forgerock.i18n.LocalizedIllegalArgumentException; 031import org.forgerock.json.resource.BadRequestException; 032import org.forgerock.json.resource.NotFoundException; 033import org.forgerock.json.resource.RequestHandler; 034import org.forgerock.json.resource.ResourceException; 035import org.forgerock.json.resource.Router; 036import org.forgerock.opendj.ldap.DN; 037import org.forgerock.opendj.ldap.schema.Schema; 038import org.forgerock.services.context.Context; 039import org.forgerock.util.Function; 040 041/** 042 * Defines a parent-child relationship between a parent resource and one or more child resource(s). Removal of the 043 * parent resource implies that the children (the sub-resources) are also removed. There are two types of 044 * sub-resource: 045 * <ul> 046 * <li>{@link SubResourceSingleton} represents a one-to-one relationship supporting read, update, patch, and action 047 * requests</li> 048 * <li>{@link SubResourceCollection} represents a one-to-many relationship supporting all requests.</li> 049 * </ul> 050 */ 051public abstract class SubResource { 052 private static final Pattern TEMPLATE_KEY_RE = Pattern.compile("\\{([^}]+)\\}"); 053 054 private final String resourceId; 055 private final List<String> dnTemplateVariables = new ArrayList<>(); 056 private String dnTemplateFormatString; 057 058 String urlTemplate = ""; 059 String dnTemplate = ""; 060 boolean isReadOnly = false; 061 Rest2Ldap rest2Ldap; 062 Resource resource; 063 064 SubResource(final String resourceId) { 065 this.resourceId = resourceId; 066 } 067 068 @Override 069 public final boolean equals(final Object o) { 070 return this == o || (o instanceof SubResource && urlTemplate.equals(((SubResource) o).urlTemplate)); 071 } 072 073 @Override 074 public final int hashCode() { 075 return urlTemplate.hashCode(); 076 } 077 078 @Override 079 public final String toString() { 080 return urlTemplate; 081 } 082 083 final Resource getResource() { 084 return resource; 085 } 086 087 final void build(final Rest2Ldap rest2Ldap, final String parent) { 088 this.rest2Ldap = rest2Ldap; 089 this.resource = rest2Ldap.getResource(resourceId); 090 if (resource == null) { 091 throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE.get(parent, resourceId)); 092 } 093 this.dnTemplateFormatString = formatTemplate(dnTemplate, dnTemplateVariables); 094 } 095 096 // Parse the template keys and replace them with %s for formatting. 097 private String formatTemplate(final String template, final List<String> templateVariables) { 098 final Matcher matcher = TEMPLATE_KEY_RE.matcher(template); 099 final StringBuffer buffer = new StringBuffer(template.length()); 100 while (matcher.find()) { 101 matcher.appendReplacement(buffer, "%s"); 102 templateVariables.add(matcher.group(1)); 103 } 104 matcher.appendTail(buffer); 105 return buffer.toString(); 106 } 107 108 abstract Router addRoutes(Router router); 109 110 /** A 404 indicates that this instance is not also a collection, so return a more helpful message. */ 111 static <T> Function<ResourceException, T, ResourceException> convert404To400(final LocalizableMessage msg) { 112 return new Function<ResourceException, T, ResourceException>() { 113 @Override 114 public T apply(final ResourceException e) throws ResourceException { 115 if (e instanceof NotFoundException) { 116 throw new BadRequestException(msg.toString()); 117 } 118 throw e; 119 } 120 }; 121 } 122 123 final RequestHandler readOnly(final RequestHandler handler) { 124 return isReadOnly ? new ReadOnlyRequestHandler(handler) : handler; 125 } 126 127 final DN dnFrom(final Context context) { 128 final DN baseDn = context.containsContext(RoutingContext.class) 129 ? context.asContext(RoutingContext.class).getDn() : DN.rootDN(); 130 131 final Schema schema = rest2Ldap.getOptions().get(DECODE_OPTIONS).getSchemaResolver().resolveSchema(dnTemplate); 132 if (dnTemplateVariables.isEmpty()) { 133 final DN relativeDn = DN.valueOf(dnTemplate, schema); 134 return baseDn.child(relativeDn); 135 } else { 136 final UriRouterContext uriRouterContext = context.asContext(UriRouterContext.class); 137 final Map<String, String> uriTemplateVariables = uriRouterContext.getUriTemplateVariables(); 138 final String[] values = new String[dnTemplateVariables.size()]; 139 for (int i = 0; i < values.length; i++) { 140 final String key = dnTemplateVariables.get(i); 141 values[i] = uriTemplateVariables.get(key); 142 } 143 final DN relativeDn = DN.format(dnTemplateFormatString, schema, (Object[]) values); 144 return baseDn.child(relativeDn); 145 } 146 } 147 148 final RequestHandler subResourceRouterFrom(final RoutingContext context) { 149 return context.getType().getSubResourceRouter(); 150 } 151}