001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2009 Sun Microsystems, Inc. 025 * Portions copyright 2012-2015 ForgeRock AS. 026 */ 027package org.forgerock.opendj.ldap.controls; 028 029import static com.forgerock.opendj.ldap.CoreMessages.*; 030import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage; 031 032import java.io.IOException; 033import java.util.ArrayList; 034import java.util.Collection; 035import java.util.Collections; 036import java.util.LinkedList; 037import java.util.List; 038 039import org.forgerock.i18n.LocalizableMessage; 040import org.forgerock.i18n.LocalizedIllegalArgumentException; 041import org.forgerock.i18n.slf4j.LocalizedLogger; 042import org.forgerock.opendj.io.ASN1; 043import org.forgerock.opendj.io.ASN1Reader; 044import org.forgerock.opendj.io.ASN1Writer; 045import org.forgerock.opendj.io.LDAP; 046import org.forgerock.opendj.ldap.AbstractFilterVisitor; 047import org.forgerock.opendj.ldap.ByteString; 048import org.forgerock.opendj.ldap.ByteStringBuilder; 049import org.forgerock.opendj.ldap.DecodeException; 050import org.forgerock.opendj.ldap.DecodeOptions; 051import org.forgerock.opendj.ldap.Filter; 052import org.forgerock.util.Reject; 053 054/** 055 * The matched values request control as defined in RFC 3876. The matched values 056 * control may be included in a search request to indicate that only attribute 057 * values matching one or more filters contained in the matched values control 058 * should be returned to the client. 059 * <p> 060 * The matched values request control supports a subset of the LDAP filter type 061 * defined in RFC 4511, and is defined as follows: 062 * 063 * <pre> 064 * ValuesReturnFilter ::= SEQUENCE OF SimpleFilterItem 065 * 066 * SimpleFilterItem ::= CHOICE { 067 * equalityMatch [3] AttributeValueAssertion, 068 * substrings [4] SubstringFilter, 069 * greaterOrEqual [5] AttributeValueAssertion, 070 * lessOrEqual [6] AttributeValueAssertion, 071 * present [7] AttributeDescription, 072 * approxMatch [8] AttributeValueAssertion, 073 * extensibleMatch [9] SimpleMatchingAssertion } 074 * 075 * SimpleMatchingAssertion ::= SEQUENCE { 076 * matchingRule [1] MatchingRuleId OPTIONAL, 077 * type [2] AttributeDescription OPTIONAL, 078 * --- at least one of the above must be present 079 * matchValue [3] AssertionValue} 080 * </pre> 081 * 082 * For example Barbara Jensen's entry contains two common name values, Barbara 083 * Jensen and Babs Jensen. The following code retrieves only the latter. 084 * 085 * <pre> 086 * String DN = "uid=bjensen,ou=People,dc=example,dc=com"; 087 * SearchRequest request = Requests.newSearchRequest(DN, 088 * SearchScope.BASE_OBJECT, "(objectclass=*)", "cn") 089 * .addControl(MatchedValuesRequestControl 090 * .newControl(true, "(cn=Babs Jensen)")); 091 * 092 * // Get the entry, retrieving cn: Babs Jensen, not cn: Barbara Jensen 093 * SearchResultEntry entry = connection.searchSingleEntry(request); 094 * </pre> 095 * 096 * @see <a href="http://tools.ietf.org/html/rfc3876">RFC 3876 - Returning 097 * Matched Values with the Lightweight Directory Access Protocol version 3 098 * (LDAPv3) </a> 099 */ 100public final class MatchedValuesRequestControl implements Control { 101 /** 102 * Visitor for validating matched values filters. 103 */ 104 private static final class FilterValidator extends 105 AbstractFilterVisitor<LocalizedIllegalArgumentException, Filter> { 106 107 @Override 108 public LocalizedIllegalArgumentException visitAndFilter(final Filter p, 109 final List<Filter> subFilters) { 110 final LocalizableMessage message = ERR_MVFILTER_BAD_FILTER_AND.get(p.toString()); 111 return new LocalizedIllegalArgumentException(message); 112 } 113 114 @Override 115 public LocalizedIllegalArgumentException visitExtensibleMatchFilter(final Filter p, 116 final String matchingRule, final String attributeDescription, 117 final ByteString assertionValue, final boolean dnAttributes) { 118 if (dnAttributes) { 119 final LocalizableMessage message = ERR_MVFILTER_BAD_FILTER_EXT.get(p.toString()); 120 return new LocalizedIllegalArgumentException(message); 121 } else { 122 return null; 123 } 124 } 125 126 @Override 127 public LocalizedIllegalArgumentException visitNotFilter(final Filter p, 128 final Filter subFilter) { 129 final LocalizableMessage message = ERR_MVFILTER_BAD_FILTER_NOT.get(p.toString()); 130 return new LocalizedIllegalArgumentException(message); 131 } 132 133 @Override 134 public LocalizedIllegalArgumentException visitOrFilter(final Filter p, 135 final List<Filter> subFilters) { 136 final LocalizableMessage message = ERR_MVFILTER_BAD_FILTER_OR.get(p.toString()); 137 return new LocalizedIllegalArgumentException(message); 138 } 139 140 @Override 141 public LocalizedIllegalArgumentException visitUnrecognizedFilter(final Filter p, 142 final byte filterTag, final ByteString filterBytes) { 143 final LocalizableMessage message = 144 ERR_MVFILTER_BAD_FILTER_UNRECOGNIZED.get(p.toString(), filterTag); 145 return new LocalizedIllegalArgumentException(message); 146 } 147 } 148 149 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 150 151 /** 152 * The OID for the matched values request control used to specify which 153 * particular attribute values should be returned in a search result entry. 154 */ 155 public static final String OID = "1.2.826.0.1.3344810.2.3"; 156 157 /** 158 * A decoder which can be used for decoding the matched values request 159 * control. 160 */ 161 public static final ControlDecoder<MatchedValuesRequestControl> DECODER = 162 new ControlDecoder<MatchedValuesRequestControl>() { 163 164 public MatchedValuesRequestControl decodeControl(final Control control, 165 final DecodeOptions options) throws DecodeException { 166 Reject.ifNull(control); 167 168 if (control instanceof MatchedValuesRequestControl) { 169 return (MatchedValuesRequestControl) control; 170 } 171 172 if (!control.getOID().equals(OID)) { 173 final LocalizableMessage message = 174 ERR_MATCHEDVALUES_CONTROL_BAD_OID.get(control.getOID(), OID); 175 throw DecodeException.error(message); 176 } 177 178 if (!control.hasValue()) { 179 // The response control must always have a value. 180 final LocalizableMessage message = ERR_MATCHEDVALUES_NO_CONTROL_VALUE.get(); 181 throw DecodeException.error(message); 182 } 183 184 final ASN1Reader reader = ASN1.getReader(control.getValue()); 185 try { 186 reader.readStartSequence(); 187 if (!reader.hasNextElement()) { 188 final LocalizableMessage message = ERR_MATCHEDVALUES_NO_FILTERS.get(); 189 throw DecodeException.error(message); 190 } 191 192 final LinkedList<Filter> filters = new LinkedList<>(); 193 do { 194 final Filter filter = LDAP.readFilter(reader); 195 try { 196 validateFilter(filter); 197 } catch (final LocalizedIllegalArgumentException e) { 198 throw DecodeException.error(e.getMessageObject()); 199 } 200 filters.add(filter); 201 } while (reader.hasNextElement()); 202 203 reader.readEndSequence(); 204 205 return new MatchedValuesRequestControl(control.isCritical(), Collections 206 .unmodifiableList(filters)); 207 } catch (final IOException e) { 208 logger.debug(LocalizableMessage.raw("%s", e)); 209 210 final LocalizableMessage message = 211 ERR_MATCHEDVALUES_CANNOT_DECODE_VALUE_AS_SEQUENCE 212 .get(getExceptionMessage(e)); 213 throw DecodeException.error(message); 214 } 215 } 216 217 public String getOID() { 218 return OID; 219 } 220 }; 221 222 private static final FilterValidator FILTER_VALIDATOR = new FilterValidator(); 223 224 /** 225 * Creates a new matched values request control with the provided 226 * criticality and list of filters. 227 * 228 * @param isCritical 229 * {@code true} if it is unacceptable to perform the operation 230 * without applying the semantics of this control, or 231 * {@code false} if it can be ignored. 232 * @param filters 233 * The list of filters of which at least one must match an 234 * attribute value in order for the attribute value to be 235 * returned to the client. The list must not be empty. 236 * @return The new control. 237 * @throws LocalizedIllegalArgumentException 238 * If one or more filters failed to conform to the filter 239 * constraints defined in RFC 3876. 240 * @throws IllegalArgumentException 241 * If {@code filters} was empty. 242 * @throws NullPointerException 243 * If {@code filters} was {@code null}. 244 */ 245 public static MatchedValuesRequestControl newControl(final boolean isCritical, 246 final Collection<Filter> filters) { 247 Reject.ifNull(filters); 248 Reject.ifFalse(filters.size() > 0, "filters is empty"); 249 250 List<Filter> copyOfFilters; 251 if (filters.size() == 1) { 252 copyOfFilters = Collections.singletonList(validateFilter(filters.iterator().next())); 253 } else { 254 copyOfFilters = new ArrayList<>(filters.size()); 255 for (final Filter filter : filters) { 256 copyOfFilters.add(validateFilter(filter)); 257 } 258 copyOfFilters = Collections.unmodifiableList(copyOfFilters); 259 } 260 261 return new MatchedValuesRequestControl(isCritical, copyOfFilters); 262 } 263 264 /** 265 * Creates a new matched values request control with the provided 266 * criticality and list of filters. 267 * 268 * @param isCritical 269 * {@code true} if it is unacceptable to perform the operation 270 * without applying the semantics of this control, or 271 * {@code false} if it can be ignored. 272 * @param filters 273 * The list of filters of which at least one must match an 274 * attribute value in order for the attribute value to be 275 * returned to the client. The list must not be empty. 276 * @return The new control. 277 * @throws LocalizedIllegalArgumentException 278 * If one or more filters could not be parsed, or if one or more 279 * filters failed to conform to the filter constraints defined 280 * in RFC 3876. 281 * @throws NullPointerException 282 * If {@code filters} was {@code null}. 283 */ 284 public static MatchedValuesRequestControl newControl(final boolean isCritical, 285 final String... filters) { 286 Reject.ifFalse(filters.length > 0, "filters is empty"); 287 288 final List<Filter> parsedFilters = new ArrayList<>(filters.length); 289 for (final String filter : filters) { 290 parsedFilters.add(validateFilter(Filter.valueOf(filter))); 291 } 292 return new MatchedValuesRequestControl(isCritical, Collections 293 .unmodifiableList(parsedFilters)); 294 } 295 296 private static Filter validateFilter(final Filter filter) { 297 final LocalizedIllegalArgumentException e = filter.accept(FILTER_VALIDATOR, filter); 298 if (e != null) { 299 throw e; 300 } 301 return filter; 302 } 303 304 private final Collection<Filter> filters; 305 306 private final boolean isCritical; 307 308 private MatchedValuesRequestControl(final boolean isCritical, final Collection<Filter> filters) { 309 this.isCritical = isCritical; 310 this.filters = filters; 311 } 312 313 /** 314 * Returns an unmodifiable collection containing the list of filters 315 * associated with this matched values control. 316 * 317 * @return An unmodifiable collection containing the list of filters 318 * associated with this matched values control. 319 */ 320 public Collection<Filter> getFilters() { 321 return filters; 322 } 323 324 /** {@inheritDoc} */ 325 public String getOID() { 326 return OID; 327 } 328 329 /** {@inheritDoc} */ 330 @Override 331 public ByteString getValue() { 332 final ByteStringBuilder buffer = new ByteStringBuilder(); 333 final ASN1Writer writer = ASN1.getWriter(buffer); 334 try { 335 writer.writeStartSequence(); 336 for (final Filter f : filters) { 337 LDAP.writeFilter(writer, f); 338 } 339 writer.writeEndSequence(); 340 return buffer.toByteString(); 341 } catch (final IOException ioe) { 342 // This should never happen unless there is a bug somewhere. 343 throw new RuntimeException(ioe); 344 } 345 } 346 347 /** {@inheritDoc} */ 348 public boolean hasValue() { 349 return true; 350 } 351 352 /** {@inheritDoc} */ 353 public boolean isCritical() { 354 return isCritical; 355 } 356 357 /** {@inheritDoc} */ 358 @Override 359 public String toString() { 360 final StringBuilder builder = new StringBuilder(); 361 builder.append("MatchedValuesRequestControl(oid="); 362 builder.append(getOID()); 363 builder.append(", criticality="); 364 builder.append(isCritical()); 365 builder.append(")"); 366 return builder.toString(); 367 } 368}