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 2012-2015 ForgeRock AS. 015 */ 016package org.forgerock.opendj.rest2ldap; 017 018import java.util.AbstractMap.SimpleImmutableEntry; 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.LinkedHashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025 026import org.forgerock.json.fluent.JsonPointer; 027import org.forgerock.json.fluent.JsonValue; 028import org.forgerock.json.resource.BadRequestException; 029import org.forgerock.json.resource.PatchOperation; 030import org.forgerock.json.resource.ResourceException; 031import org.forgerock.json.resource.ResultHandler; 032import org.forgerock.opendj.ldap.Attribute; 033import org.forgerock.opendj.ldap.Entry; 034import org.forgerock.opendj.ldap.Filter; 035import org.forgerock.opendj.ldap.Modification; 036import org.forgerock.util.Function; 037import org.forgerock.util.promise.NeverThrowsException; 038 039import static org.forgerock.json.resource.PatchOperation.*; 040import static org.forgerock.opendj.ldap.Filter.*; 041import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*; 042import static org.forgerock.opendj.rest2ldap.Utils.*; 043 044/** 045 * An attribute mapper which maps JSON objects to LDAP attributes. 046 */ 047public final class ObjectAttributeMapper extends AttributeMapper { 048 049 private static final class Mapping { 050 private final AttributeMapper mapper; 051 private final String name; 052 053 private Mapping(final String name, final AttributeMapper mapper) { 054 this.name = name; 055 this.mapper = mapper; 056 } 057 058 @Override 059 public String toString() { 060 return name + " -> " + mapper; 061 } 062 } 063 064 private final Map<String, Mapping> mappings = new LinkedHashMap<>(); 065 066 ObjectAttributeMapper() { 067 // Nothing to do. 068 } 069 070 /** 071 * Creates a mapping for an attribute contained in the JSON object. 072 * 073 * @param name 074 * The name of the JSON attribute to be mapped. 075 * @param mapper 076 * The attribute mapper responsible for mapping the JSON 077 * attribute to LDAP attribute(s). 078 * @return A reference to this attribute mapper. 079 */ 080 public ObjectAttributeMapper attribute(final String name, final AttributeMapper mapper) { 081 mappings.put(toLowerCase(name), new Mapping(name, mapper)); 082 return this; 083 } 084 085 @Override 086 public String toString() { 087 return "object(" + mappings.values() + ")"; 088 } 089 090 @Override 091 void create(final Context c, final JsonPointer path, final JsonValue v, 092 final ResultHandler<List<Attribute>> h) { 093 try { 094 /* 095 * First check that the JSON value is an object and that the fields 096 * it contains are known by this mapper. 097 */ 098 final Map<String, Mapping> missingMappings = checkMapping(path, v); 099 100 // Accumulate the results of the subordinate mappings. 101 final ResultHandler<List<Attribute>> handler = accumulator(h); 102 103 // Invoke mappings for which there are values provided. 104 if (v != null && !v.isNull()) { 105 for (final Map.Entry<String, Object> me : v.asMap().entrySet()) { 106 final Mapping mapping = getMapping(me.getKey()); 107 final JsonValue subValue = new JsonValue(me.getValue()); 108 mapping.mapper.create(c, path.child(me.getKey()), subValue, handler); 109 } 110 } 111 112 // Invoke mappings for which there were no values provided. 113 for (final Mapping mapping : missingMappings.values()) { 114 mapping.mapper.create(c, path.child(mapping.name), null, handler); 115 } 116 } catch (final Exception e) { 117 h.handleError(asResourceException(e)); 118 } 119 } 120 121 @Override 122 void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath, 123 final Set<String> ldapAttributes) { 124 if (subPath.isEmpty()) { 125 // Request all subordinate mappings. 126 for (final Mapping mapping : mappings.values()) { 127 mapping.mapper.getLDAPAttributes(c, path.child(mapping.name), subPath, 128 ldapAttributes); 129 } 130 } else { 131 // Request single subordinate mapping. 132 final Mapping mapping = getMapping(subPath); 133 if (mapping != null) { 134 mapping.mapper.getLDAPAttributes(c, path.child(subPath.get(0)), subPath 135 .relativePointer(), ldapAttributes); 136 } 137 } 138 } 139 140 @Override 141 void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath, 142 final FilterType type, final String operator, final Object valueAssertion, 143 final ResultHandler<Filter> h) { 144 final Mapping mapping = getMapping(subPath); 145 if (mapping != null) { 146 mapping.mapper.getLDAPFilter(c, path.child(subPath.get(0)), subPath.relativePointer(), 147 type, operator, valueAssertion, h); 148 } else { 149 /* 150 * Either the filter targeted the entire object (i.e. it was "/"), 151 * or it targeted an unrecognized attribute within the object. 152 * Either way, the filter will never match. 153 */ 154 h.handleResult(alwaysFalse()); 155 } 156 } 157 158 @Override 159 void patch(final Context c, final JsonPointer path, final PatchOperation operation, 160 final ResultHandler<List<Modification>> h) { 161 try { 162 final JsonPointer field = operation.getField(); 163 final JsonValue v = operation.getValue(); 164 165 if (field.isEmpty()) { 166 /* 167 * The patch operation applies to this object. We'll handle this 168 * by allowing the JSON value to be a partial object and 169 * add/remove/replace only the provided values. 170 */ 171 final Map<String, Mapping> missingMappings = checkMapping(path, v); 172 173 // Accumulate the results of the subordinate mappings. 174 final ResultHandler<List<Modification>> handler = 175 accumulator(mappings.size() - missingMappings.size(), h); 176 177 // Invoke mappings for which there are values provided. 178 if (!v.isNull()) { 179 for (final Map.Entry<String, Object> me : v.asMap().entrySet()) { 180 final Mapping mapping = getMapping(me.getKey()); 181 final JsonValue subValue = new JsonValue(me.getValue()); 182 final PatchOperation subOperation = 183 operation(operation.getOperation(), field /* empty */, subValue); 184 mapping.mapper.patch(c, path.child(me.getKey()), subOperation, handler); 185 } 186 } 187 } else { 188 /* 189 * The patch operation targets a subordinate field. Create a new 190 * patch operation targeting the field and forward it to the 191 * appropriate mapper. 192 */ 193 final String fieldName = field.get(0); 194 final Mapping mapping = getMapping(fieldName); 195 if (mapping == null) { 196 throw new BadRequestException(i18n( 197 "The request cannot be processed because it included " 198 + "an unrecognized field '%s'", path.child(fieldName))); 199 } 200 final PatchOperation subOperation = 201 operation(operation.getOperation(), field.relativePointer(), v); 202 mapping.mapper.patch(c, path.child(fieldName), subOperation, h); 203 } 204 } catch (final Exception ex) { 205 h.handleError(asResourceException(ex)); 206 } 207 } 208 209 @Override 210 void read(final Context c, final JsonPointer path, final Entry e, 211 final ResultHandler<JsonValue> h) { 212 /* 213 * Use an accumulator which will aggregate the results from the 214 * subordinate mappers into a single list. On completion, the 215 * accumulator combines the results into a single JSON map object. 216 */ 217 final ResultHandler<Map.Entry<String, JsonValue>> handler = 218 accumulate(mappings.size(), transform( 219 new Function<List<Map.Entry<String, JsonValue>>, JsonValue, NeverThrowsException>() { 220 @Override 221 public JsonValue apply(final List<Map.Entry<String, JsonValue>> value) { 222 if (value.isEmpty()) { 223 /* 224 * No subordinate attributes, so omit the 225 * entire JSON object from the resource. 226 */ 227 return null; 228 } else { 229 // Combine the sub-attributes into a single JSON object. 230 final Map<String, Object> result = new LinkedHashMap<>(value.size()); 231 for (final Map.Entry<String, JsonValue> e : value) { 232 result.put(e.getKey(), e.getValue().getObject()); 233 } 234 return new JsonValue(result); 235 } 236 } 237 }, h)); 238 239 for (final Mapping mapping : mappings.values()) { 240 mapping.mapper.read(c, path.child(mapping.name), e, transform( 241 new Function<JsonValue, Map.Entry<String, JsonValue>, NeverThrowsException>() { 242 @Override 243 public Map.Entry<String, JsonValue> apply(final JsonValue value) { 244 return value != null ? new SimpleImmutableEntry<String, JsonValue>( 245 mapping.name, value) : null; 246 } 247 }, handler)); 248 } 249 } 250 251 @Override 252 void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v, 253 final ResultHandler<List<Modification>> h) { 254 try { 255 /* 256 * First check that the JSON value is an object and that the fields 257 * it contains are known by this mapper. 258 */ 259 final Map<String, Mapping> missingMappings = checkMapping(path, v); 260 261 // Accumulate the results of the subordinate mappings. 262 final ResultHandler<List<Modification>> handler = accumulator(h); 263 264 // Invoke mappings for which there are values provided. 265 if (v != null && !v.isNull()) { 266 for (final Map.Entry<String, Object> me : v.asMap().entrySet()) { 267 final Mapping mapping = getMapping(me.getKey()); 268 final JsonValue subValue = new JsonValue(me.getValue()); 269 mapping.mapper.update(c, path.child(me.getKey()), e, subValue, handler); 270 } 271 } 272 273 // Invoke mappings for which there were no values provided. 274 for (final Mapping mapping : missingMappings.values()) { 275 mapping.mapper.update(c, path.child(mapping.name), e, null, handler); 276 } 277 } catch (final Exception ex) { 278 h.handleError(asResourceException(ex)); 279 } 280 } 281 282 private <T> ResultHandler<List<T>> accumulator(final ResultHandler<List<T>> h) { 283 return accumulator(mappings.size(), h); 284 } 285 286 private <T> ResultHandler<List<T>> accumulator(final int size, final ResultHandler<List<T>> h) { 287 return accumulate(size, transform(new Function<List<List<T>>, List<T>, NeverThrowsException>() { 288 @Override 289 public List<T> apply(final List<List<T>> value) { 290 switch (value.size()) { 291 case 0: 292 return Collections.emptyList(); 293 case 1: 294 return value.get(0); 295 default: 296 final List<T> attributes = new ArrayList<>(value.size()); 297 for (final List<T> a : value) { 298 attributes.addAll(a); 299 } 300 return attributes; 301 } 302 } 303 }, h)); 304 } 305 306 /** 307 * Fail immediately if the JSON value has the wrong type or contains unknown 308 * attributes. 309 */ 310 private Map<String, Mapping> checkMapping(final JsonPointer path, final JsonValue v) 311 throws ResourceException { 312 final Map<String, Mapping> missingMappings = new LinkedHashMap<>(mappings); 313 if (v != null && !v.isNull()) { 314 if (v.isMap()) { 315 for (final String attribute : v.asMap().keySet()) { 316 if (missingMappings.remove(toLowerCase(attribute)) == null) { 317 throw new BadRequestException(i18n( 318 "The request cannot be processed because it included " 319 + "an unrecognized field '%s'", path.child(attribute))); 320 } 321 } 322 } else { 323 throw new BadRequestException(i18n( 324 "The request cannot be processed because it included " 325 + "the field '%s' whose value is the wrong type: " 326 + "an object is expected", path)); 327 } 328 } 329 return missingMappings; 330 } 331 332 private Mapping getMapping(final JsonPointer jsonAttribute) { 333 return jsonAttribute.isEmpty() ? null : getMapping(jsonAttribute.get(0)); 334 } 335 336 private Mapping getMapping(final String jsonAttribute) { 337 return mappings.get(toLowerCase(jsonAttribute)); 338 } 339 340}