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 */ 016package org.forgerock.opendj.rest2ldap; 017 018import static java.util.Collections.emptyList; 019import static java.util.Collections.singletonList; 020import static org.forgerock.json.JsonValue.field; 021import static org.forgerock.json.JsonValue.object; 022import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException; 023import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ENCODING_VALUES_FOR_FIELD; 024import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_PATCH_JSON_INTERNAL_PROPERTY; 025import static org.forgerock.opendj.rest2ldap.Utils.jsonToAttribute; 026import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException; 027import static org.forgerock.opendj.rest2ldap.Utils.newNotSupportedException; 028import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.byteStringToJson; 029import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.jsonToByteString; 030import static org.forgerock.util.promise.Promises.newResultPromise; 031 032import java.util.ArrayList; 033import java.util.Collection; 034import java.util.List; 035import java.util.Set; 036 037import org.forgerock.json.JsonPointer; 038import org.forgerock.json.JsonValue; 039import org.forgerock.json.resource.PatchOperation; 040import org.forgerock.json.resource.ResourceException; 041import org.forgerock.opendj.ldap.Attribute; 042import org.forgerock.opendj.ldap.AttributeDescription; 043import org.forgerock.opendj.ldap.Entry; 044import org.forgerock.opendj.ldap.Filter; 045import org.forgerock.opendj.ldap.Modification; 046import org.forgerock.services.context.Context; 047import org.forgerock.util.promise.Promise; 048import org.forgerock.util.query.QueryFilter; 049 050/** A property mapper which provides a mapping from a JSON value to an LDAP attribute having the JSON syntax. */ 051public final class JsonPropertyMapper extends AbstractLdapPropertyMapper<JsonPropertyMapper> { 052 /** 053 * The default JSON schema for this property. According to json-schema.org the {} schema allows anything. 054 * However, the OpenAPI transformer seems to expect at least a "type" field to be present. 055 */ 056 private static final JsonValue ANY_SCHEMA = new JsonValue(object(field("type", "object"))); 057 private JsonValue jsonSchema = ANY_SCHEMA; 058 059 JsonPropertyMapper(final AttributeDescription ldapAttributeName) { 060 super(ldapAttributeName); 061 } 062 063 /** 064 * Sets the default JSON value which should be substituted when the LDAP attribute is not found in the LDAP entry. 065 * 066 * @param defaultValue 067 * The default JSON value. 068 * @return This property mapper. 069 */ 070 public JsonPropertyMapper defaultJsonValue(final Object defaultValue) { 071 this.defaultJsonValues = defaultValue != null ? singletonList(defaultValue) : emptyList(); 072 return this; 073 } 074 075 /** 076 * Sets the default JSON values which should be substituted when the LDAP attribute is not found in the LDAP entry. 077 * 078 * @param defaultValues 079 * The default JSON values. 080 * @return This property mapper. 081 */ 082 public JsonPropertyMapper defaultJsonValues(final Collection<?> defaultValues) { 083 this.defaultJsonValues = defaultValues != null ? new ArrayList<>(defaultValues) : emptyList(); 084 return this; 085 } 086 087 /** 088 * Sets the JSON schema corresponding to this simple property mapper. If not {@code null}, 089 * it will be returned by {@link #toJsonSchema()}, otherwise a default JSON schema will be 090 * automatically generated with the information available in this property mapper. 091 * 092 * @param jsonSchema 093 * the JSON schema corresponding to this simple property mapper. Can be {@code null} 094 * @return This property mapper. 095 */ 096 public JsonPropertyMapper jsonSchema(JsonValue jsonSchema) { 097 this.jsonSchema = jsonSchema != null ? jsonSchema : ANY_SCHEMA; 098 return this; 099 } 100 101 @Override 102 public String toString() { 103 return "json(" + ldapAttributeName + ")"; 104 } 105 106 @Override 107 Promise<Filter, ResourceException> getLdapFilter(final Context context, final Resource resource, 108 final JsonPointer path, final JsonPointer subPath, 109 final FilterType type, final String operator, 110 final Object valueAssertion) { 111 final QueryFilter<JsonPointer> queryFilter = toQueryFilter(type, subPath, operator, valueAssertion); 112 return newResultPromise(Filter.equality(ldapAttributeName.toString(), queryFilter)); 113 } 114 115 private QueryFilter<JsonPointer> toQueryFilter(final FilterType type, final JsonPointer subPath, 116 final String operator, final Object valueAssertion) { 117 switch (type) { 118 case CONTAINS: 119 return QueryFilter.contains(subPath, valueAssertion); 120 case STARTS_WITH: 121 return QueryFilter.startsWith(subPath, valueAssertion); 122 case EQUAL_TO: 123 return QueryFilter.equalTo(subPath, valueAssertion); 124 case GREATER_THAN: 125 return QueryFilter.greaterThan(subPath, valueAssertion); 126 case GREATER_THAN_OR_EQUAL_TO: 127 return QueryFilter.greaterThanOrEqualTo(subPath, valueAssertion); 128 case LESS_THAN: 129 return QueryFilter.lessThan(subPath, valueAssertion); 130 case LESS_THAN_OR_EQUAL_TO: 131 return QueryFilter.lessThanOrEqualTo(subPath, valueAssertion); 132 case PRESENT: 133 return QueryFilter.present(subPath); 134 case EXTENDED: 135 return QueryFilter.extendedMatch(subPath, operator, valueAssertion); 136 default: 137 return QueryFilter.alwaysFalse(); 138 } 139 } 140 141 @Override 142 Promise<Attribute, ResourceException> getNewLdapAttributes(final Context context, final Resource resource, 143 final JsonPointer path, final List<Object> newValues) { 144 try { 145 return newResultPromise(jsonToAttribute(newValues, ldapAttributeName, jsonToByteString())); 146 } catch (final Exception e) { 147 return newBadRequestException(ERR_ENCODING_VALUES_FOR_FIELD.get(path, e.getMessage())).asPromise(); 148 } 149 } 150 151 @Override 152 JsonPropertyMapper getThis() { 153 return this; 154 } 155 156 /** Intercept attempts to patch internal fields and reject these as unsupported rather than unrecognized. */ 157 @Override 158 Promise<List<Modification>, ResourceException> patch(final Context context, final Resource resource, 159 final JsonPointer path, final PatchOperation operation) { 160 final JsonPointer field = operation.getField(); 161 if (field.isEmpty() || field.size() == 1 && field.get(0).equals("-")) { 162 return super.patch(context, resource, path, operation); 163 } 164 return newNotSupportedException(ERR_PATCH_JSON_INTERNAL_PROPERTY.get(field, path, path)).asPromise(); 165 } 166 167 @SuppressWarnings("fallthrough") 168 @Override 169 Promise<JsonValue, ResourceException> read(final Context context, final Resource resource, final JsonPointer path, 170 final Entry e) { 171 try { 172 final Set<Object> s = e.parseAttribute(ldapAttributeName).asSetOf(byteStringToJson(), defaultJsonValues); 173 switch (s.size()) { 174 case 0: 175 return newResultPromise(null); 176 case 1: 177 if (attributeIsSingleValued()) { 178 return newResultPromise(new JsonValue(s.iterator().next())); 179 } 180 // Fall-though: unexpectedly got multiple values. It's probably best to just return them. 181 default: 182 return newResultPromise(new JsonValue(new ArrayList<>(s))); 183 } 184 } catch (final Exception ex) { 185 // The LDAP attribute could not be decoded. 186 return asResourceException(ex).asPromise(); 187 } 188 } 189 190 @Override 191 JsonValue toJsonSchema() { 192 return jsonSchema; 193 } 194}