001/* 002 * Copyright 2016-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2016-2024 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2016-2024 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.sdk.transformations; 037 038 039 040import java.util.Collections; 041import java.util.HashSet; 042import java.util.Set; 043 044import com.unboundid.ldap.sdk.Attribute; 045import com.unboundid.ldap.sdk.DN; 046import com.unboundid.ldap.sdk.Entry; 047import com.unboundid.ldap.sdk.Filter; 048import com.unboundid.ldap.sdk.SearchScope; 049import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition; 050import com.unboundid.ldap.sdk.schema.Schema; 051import com.unboundid.util.Debug; 052import com.unboundid.util.NotNull; 053import com.unboundid.util.Nullable; 054import com.unboundid.util.StaticUtils; 055import com.unboundid.util.ThreadSafety; 056import com.unboundid.util.ThreadSafetyLevel; 057 058 059 060/** 061 * This class provides an implementation of an entry transformation that will 062 * add a specified attribute with a given set of values to any entry that does 063 * not already contain that attribute and matches a specified set of criteria. 064 */ 065@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 066public final class AddAttributeTransformation 067 implements EntryTransformation 068{ 069 // The attribute to add if appropriate. 070 @NotNull private final Attribute attributeToAdd; 071 072 // Indicates whether we need to check entries against the filter. 073 private final boolean examineFilter; 074 075 // Indicates whether we need to check entries against the scope. 076 private final boolean examineScope; 077 078 // Indicates whether to only add the attribute to entries that do not already 079 // have any values for the associated attribute type. 080 private final boolean onlyIfMissing; 081 082 // The base DN to use to identify entries to which to add the attribute. 083 @NotNull private final DN baseDN; 084 085 // The filter to use to identify entries to which to add the attribute. 086 @NotNull private final Filter filter; 087 088 // The schema to use when processing. 089 @Nullable private final Schema schema; 090 091 // The scope to use to identify entries to which to add the attribute. 092 @NotNull private final SearchScope scope; 093 094 // The names that can be used to reference the target attribute. 095 @NotNull private final Set<String> names; 096 097 098 099 /** 100 * Creates a new add attribute transformation with the provided information. 101 * 102 * @param schema The schema to use in processing. It may be 103 * {@code null} if a default standard schema should be 104 * used. 105 * @param baseDN The base DN to use to identify which entries to 106 * update. If this is {@code null}, it will be 107 * assumed to be the null DN. 108 * @param scope The scope to use to identify which entries to 109 * update. If this is {@code null}, it will be 110 * assumed to be {@link SearchScope#SUB}. 111 * @param filter An optional filter to use to identify which entries 112 * to update. If this is {@code null}, then a default 113 * LDAP true filter (which will match any entry) will 114 * be used. 115 * @param attributeToAdd The attribute to add to entries that match the 116 * criteria and do not already contain any values for 117 * the specified attribute. It must not be 118 * {@code null}. 119 * @param onlyIfMissing Indicates whether the attribute should only be 120 * added to entries that do not already contain it. 121 * If this is {@code false} and an entry that matches 122 * the base, scope, and filter criteria and already 123 * has one or more values for the target attribute 124 * will be updated to include the new values in 125 * addition to the existing values. 126 */ 127 public AddAttributeTransformation(@Nullable final Schema schema, 128 @Nullable final DN baseDN, 129 @Nullable final SearchScope scope, 130 @Nullable final Filter filter, 131 @NotNull final Attribute attributeToAdd, 132 final boolean onlyIfMissing) 133 { 134 this.attributeToAdd = attributeToAdd; 135 this.onlyIfMissing = onlyIfMissing; 136 137 138 // If a schema was provided, then use it. Otherwise, use the default 139 // standard schema. 140 Schema s = schema; 141 if (s == null) 142 { 143 try 144 { 145 s = Schema.getDefaultStandardSchema(); 146 } 147 catch (final Exception e) 148 { 149 // This should never happen. 150 Debug.debugException(e); 151 } 152 } 153 this.schema = s; 154 155 156 // Identify all of the names that can be used to reference the specified 157 // attribute. 158 final HashSet<String> attrNames = 159 new HashSet<>(StaticUtils.computeMapCapacity(5)); 160 final String baseName = 161 StaticUtils.toLowerCase(attributeToAdd.getBaseName()); 162 attrNames.add(baseName); 163 if (s != null) 164 { 165 final AttributeTypeDefinition at = s.getAttributeType(baseName); 166 if (at != null) 167 { 168 attrNames.add(StaticUtils.toLowerCase(at.getOID())); 169 for (final String name : at.getNames()) 170 { 171 attrNames.add(StaticUtils.toLowerCase(name)); 172 } 173 } 174 } 175 names = Collections.unmodifiableSet(attrNames); 176 177 178 // If a base DN was provided, then use it. Otherwise, use the null DN. 179 if (baseDN == null) 180 { 181 this.baseDN = DN.NULL_DN; 182 } 183 else 184 { 185 this.baseDN = baseDN; 186 } 187 188 189 // If a scope was provided, then use it. Otherwise, use a subtree scope. 190 if (scope == null) 191 { 192 this.scope = SearchScope.SUB; 193 } 194 else 195 { 196 this.scope = scope; 197 } 198 199 200 // If a filter was provided, then use it. Otherwise, use an LDAP true 201 // filter. 202 if (filter == null) 203 { 204 this.filter = Filter.createANDFilter(); 205 examineFilter = false; 206 } 207 else 208 { 209 this.filter = filter; 210 if (filter.getFilterType() == Filter.FILTER_TYPE_AND) 211 { 212 examineFilter = (filter.getComponents().length > 0); 213 } 214 else 215 { 216 examineFilter = true; 217 } 218 } 219 220 221 examineScope = 222 (! (this.baseDN.isNullDN() && this.scope == SearchScope.SUB)); 223 } 224 225 226 227 /** 228 * {@inheritDoc} 229 */ 230 @Override() 231 @Nullable() 232 public Entry transformEntry(@NotNull final Entry e) 233 { 234 if (e == null) 235 { 236 return null; 237 } 238 239 240 // If we should only add the attribute to entries that don't already contain 241 // any values for that type, then determine whether the target attribute 242 // already exists in the entry. If so, then just return the original entry. 243 if (onlyIfMissing) 244 { 245 for (final String name : names) 246 { 247 if (e.hasAttribute(name)) 248 { 249 return e; 250 } 251 } 252 } 253 254 255 // Determine whether the entry is within the scope of the inclusion 256 // criteria. If not, then return the original entry. 257 try 258 { 259 if (examineScope && (! e.matchesBaseAndScope(baseDN, scope))) 260 { 261 return e; 262 } 263 } 264 catch (final Exception ex) 265 { 266 // This should only happen if the entry has a malformed DN. In that case, 267 // we'll assume it isn't within the scope and return the provided entry. 268 Debug.debugException(ex); 269 return e; 270 } 271 272 273 // Determine whether the entry matches the suppression filter. If not, then 274 // return the original entry. 275 try 276 { 277 if (examineFilter && (! filter.matchesEntry(e, schema))) 278 { 279 return e; 280 } 281 } 282 catch (final Exception ex) 283 { 284 // If we can't verify whether the entry matches the filter, then assume 285 // it doesn't and return the provided entry. 286 Debug.debugException(ex); 287 return e; 288 } 289 290 291 // If we've gotten here, then we should add the attribute to the entry. 292 final Entry copy = e.duplicate(); 293 final Attribute existingAttribute = 294 copy.getAttribute(attributeToAdd.getName(), schema); 295 if (existingAttribute == null) 296 { 297 copy.addAttribute(attributeToAdd); 298 } 299 else 300 { 301 copy.addAttribute(existingAttribute.getName(), 302 attributeToAdd.getValueByteArrays()); 303 } 304 return copy; 305 } 306 307 308 309 /** 310 * {@inheritDoc} 311 */ 312 @Override() 313 @Nullable() 314 public Entry translate(@NotNull final Entry original, 315 final long firstLineNumber) 316 { 317 return transformEntry(original); 318 } 319 320 321 322 /** 323 * {@inheritDoc} 324 */ 325 @Override() 326 @Nullable() 327 public Entry translateEntryToWrite(@NotNull final Entry original) 328 { 329 return transformEntry(original); 330 } 331}