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