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-2016 ForgeRock AS. 015 */ 016package org.forgerock.opendj.rest2ldap; 017 018import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; 019 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.List; 023import java.util.Set; 024 025import org.forgerock.json.JsonPointer; 026import org.forgerock.json.JsonValue; 027import org.forgerock.json.resource.ResourceException; 028import org.forgerock.opendj.ldap.Attribute; 029import org.forgerock.opendj.ldap.AttributeDescription; 030import org.forgerock.opendj.ldap.ByteString; 031import org.forgerock.opendj.ldap.Entry; 032import org.forgerock.opendj.ldap.Filter; 033import org.forgerock.opendj.ldap.schema.AttributeType; 034import org.forgerock.opendj.ldap.schema.Syntax; 035import org.forgerock.services.context.Context; 036import org.forgerock.util.Function; 037import org.forgerock.util.promise.Promise; 038 039import static java.util.Collections.*; 040import static org.forgerock.json.JsonValue.*; 041import static org.forgerock.opendj.ldap.Filter.*; 042import static org.forgerock.opendj.ldap.schema.CoreSchema.*; 043import static org.forgerock.opendj.rest2ldap.Utils.*; 044import static org.forgerock.util.promise.Promises.newResultPromise; 045 046/** An property mapper which provides a simple mapping from a JSON value to a single LDAP attribute. */ 047public final class SimplePropertyMapper extends AbstractLdapPropertyMapper<SimplePropertyMapper> { 048 private Function<ByteString, ?, ? extends Exception> decoder; 049 private Function<Object, ByteString, ? extends Exception> encoder; 050 private JsonValue jsonSchema; 051 052 SimplePropertyMapper(final AttributeDescription ldapAttributeName) { 053 super(ldapAttributeName); 054 } 055 056 /** 057 * Sets the decoder which will be used for converting LDAP attribute values 058 * to JSON values. 059 * 060 * @param f 061 * The function to use for decoding LDAP attribute values. 062 * @return This property mapper. 063 */ 064 public SimplePropertyMapper decoder(final Function<ByteString, ?, ? extends Exception> f) { 065 this.decoder = f; 066 return this; 067 } 068 069 /** 070 * Sets the default JSON value which should be substituted when the LDAP attribute is not found in the LDAP entry. 071 * 072 * @param defaultValue 073 * The default JSON value. 074 * @return This property mapper. 075 */ 076 public SimplePropertyMapper defaultJsonValue(final Object defaultValue) { 077 this.defaultJsonValues = defaultValue != null ? singletonList(defaultValue) : emptyList(); 078 return this; 079 } 080 081 /** 082 * Sets the default JSON values which should be substituted when the LDAP attribute is not found in the LDAP entry. 083 * 084 * @param defaultValues 085 * The default JSON values. 086 * @return This property mapper. 087 */ 088 public SimplePropertyMapper defaultJsonValues(final Collection<?> defaultValues) { 089 this.defaultJsonValues = defaultValues != null ? new ArrayList<>(defaultValues) : emptyList(); 090 return this; 091 } 092 093 /** 094 * Sets the encoder which will be used for converting JSON values to LDAP 095 * attribute values. 096 * 097 * @param f 098 * The function to use for encoding LDAP attribute values. 099 * @return This property mapper. 100 */ 101 public SimplePropertyMapper encoder(final Function<Object, ByteString, ? extends Exception> f) { 102 this.encoder = f; 103 return this; 104 } 105 106 /** 107 * Indicates that JSON values are base 64 encodings of binary data. Calling 108 * this method with the value {@code true} is equivalent to the following: 109 * 110 * <pre> 111 * mapper.decoder(...); // function that converts binary data to base 64 112 * mapper.encoder(...); // function that converts base 64 to binary data 113 * </pre> 114 * 115 * Passing in a value of {@code false} resets the encoding and decoding 116 * functions to the default. 117 * 118 * @param isBinary {@code true} if this property is binary. 119 * @return This property mapper. 120 */ 121 public SimplePropertyMapper isBinary(final boolean isBinary) { 122 if (isBinary) { 123 decoder = byteStringToBase64(); 124 encoder = base64ToByteString(); 125 } else { 126 decoder = null; 127 encoder = null; 128 } 129 return this; 130 } 131 132 /** 133 * Sets the JSON schema corresponding to this simple property mapper. If not {@code null}, 134 * it will be returned by {@link #toJsonSchema()}, otherwise a default JSON schema will be 135 * automatically generated with the information available in this property mapper. 136 * 137 * @param jsonSchema 138 * the JSON schema corresponding to this simple property mapper. Can be {@code null} 139 * @return This property mapper. 140 */ 141 public SimplePropertyMapper jsonSchema(JsonValue jsonSchema) { 142 this.jsonSchema = jsonSchema; 143 return this; 144 } 145 146 @Override 147 public String toString() { 148 return "simple(" + ldapAttributeName + ")"; 149 } 150 151 @Override 152 Promise<Filter, ResourceException> getLdapFilter(final Context context, final Resource resource, 153 final JsonPointer path, final JsonPointer subPath, 154 final FilterType type, final String operator, 155 final Object valueAssertion) { 156 if (subPath.isEmpty()) { 157 try { 158 final ByteString va = valueAssertion != null ? encoder().apply(valueAssertion) : null; 159 return newResultPromise(toFilter(type, ldapAttributeName.toString(), va)); 160 } catch (final Exception e) { 161 // Invalid assertion value - bad request. 162 return newBadRequestException( 163 ERR_ILLEGAL_FILTER_ASSERTION_VALUE.get(String.valueOf(valueAssertion), path), e).asPromise(); 164 } 165 } else { 166 // This property mapper does not support partial filtering. 167 return newResultPromise(alwaysFalse()); 168 } 169 } 170 171 @Override 172 Promise<Attribute, ResourceException> getNewLdapAttributes(final Context context, final Resource resource, 173 final JsonPointer path, final List<Object> newValues) { 174 try { 175 return newResultPromise(jsonToAttribute(newValues, ldapAttributeName, encoder())); 176 } catch (final Exception ex) { 177 return newBadRequestException(ERR_ENCODING_VALUES_FOR_FIELD.get(path, ex.getMessage())).asPromise(); 178 } 179 } 180 181 @Override 182 SimplePropertyMapper getThis() { 183 return this; 184 } 185 186 @SuppressWarnings("fallthrough") 187 @Override 188 Promise<JsonValue, ResourceException> read(final Context context, final Resource resource, 189 final JsonPointer path, final Entry e) { 190 try { 191 final Set<Object> s = e.parseAttribute(ldapAttributeName).asSetOf(decoder(), defaultJsonValues); 192 switch (s.size()) { 193 case 0: 194 return newResultPromise(null); 195 case 1: 196 if (attributeIsSingleValued()) { 197 return newResultPromise(new JsonValue(s.iterator().next())); 198 } 199 // Fall-though: unexpectedly got multiple values. It's probably best to just return them. 200 default: 201 return newResultPromise(new JsonValue(new ArrayList<>(s))); 202 } 203 } catch (final Exception ex) { 204 // The LDAP attribute could not be decoded. 205 return Rest2Ldap.asResourceException(ex).asPromise(); 206 } 207 } 208 209 private Function<ByteString, ?, ? extends Exception> decoder() { 210 return decoder == null ? byteStringToJson(ldapAttributeName) : decoder; 211 } 212 213 private Function<Object, ByteString, ? extends Exception> encoder() { 214 return encoder == null ? jsonToByteString(ldapAttributeName) : encoder; 215 } 216 217 @Override 218 JsonValue toJsonSchema() { 219 return this.jsonSchema != null ? this.jsonSchema : toJsonSchema0(); 220 } 221 222 private JsonValue toJsonSchema0() { 223 final AttributeType attrType = ldapAttributeName.getAttributeType(); 224 225 final JsonValue jsonSchema; 226 if (isMultiValued()) { 227 jsonSchema = json(object( 228 field("type", "array"), 229 // LDAP has set semantics => all items are unique 230 field("uniqueItems", true), 231 field("items", itemsSchema(attrType).getObject()))); 232 } else { 233 jsonSchema = itemsSchema(attrType); 234 } 235 236 final String description = attrType.getDescription(); 237 if (description != null && !"".equals(description)) { 238 jsonSchema.put("title", description); 239 } 240 putWritabilityProperties(jsonSchema); 241 return jsonSchema; 242 } 243 244 private JsonValue itemsSchema(final AttributeType attrType) { 245 final JsonValue itemsSchema = json(object()); 246 putTypeAndFormat(itemsSchema, attrType); 247 return itemsSchema; 248 } 249 250 /** 251 * Puts the type and format corresponding to the provided attribute type on the provided JSON 252 * schema. 253 * 254 * @param jsonSchema 255 * the JSON schema where to put the type and format 256 * @param attrType 257 * the attribute type for which to infer JSON the type and format 258 * @see <a href= 259 * "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types"> 260 * OpenAPI Specification 2.0</a> 261 * @see <a href="https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-7.3"> 262 * draft-fge-json-schema-validation-00 - Semantic validation with "format" - Defined 263 * attributes</a> 264 */ 265 public static void putTypeAndFormat(JsonValue jsonSchema, AttributeType attrType) { 266 if (attrType.isPlaceHolder()) { 267 jsonSchema.put("type", "string"); 268 return; 269 } 270 271 final Syntax syntax = attrType.getSyntax(); 272 if (attrType.hasName("userPassword")) { 273 jsonSchema.put("type", "string"); 274 jsonSchema.put("format", "password"); 275 } else if (attrType.hasName("mail")) { 276 jsonSchema.put("type", "string"); 277 jsonSchema.put("format", "email"); 278 } else if (syntax.equals(getBooleanSyntax())) { 279 jsonSchema.put("type", "boolean"); 280 } else if (syntax.equals(getNumericStringSyntax())) { 281 // credit card numbers are numeric strings whose leading zeros are significant 282 jsonSchema.put("type", "string"); 283 } else if (syntax.equals(getIntegerSyntax())) { 284 jsonSchema.put("type", "integer"); 285 } else if (syntax.equals(getGeneralizedTimeSyntax())) { 286 jsonSchema.put("type", "string"); 287 jsonSchema.put("format", "date-time"); 288 } else if (!syntax.isHumanReadable()) { 289 jsonSchema.put("type", "string"); 290 jsonSchema.put("format", "byte"); 291 } else { 292 jsonSchema.put("type", "string"); 293 } 294 } 295}