001/* 002 * Copyright 2009-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2009-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) 2009-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.persist; 037 038 039 040import com.unboundid.ldap.sdk.DN; 041import com.unboundid.ldap.sdk.DNEntrySource; 042import com.unboundid.ldap.sdk.Entry; 043import com.unboundid.ldap.sdk.LDAPInterface; 044import com.unboundid.ldap.sdk.LDAPException; 045import com.unboundid.util.CryptoHelper; 046import com.unboundid.util.NotNull; 047import com.unboundid.util.Nullable; 048import com.unboundid.util.StaticUtils; 049import com.unboundid.util.ThreadSafety; 050import com.unboundid.util.ThreadSafetyLevel; 051import com.unboundid.util.Validator; 052 053import static com.unboundid.ldap.sdk.persist.PersistMessages.*; 054 055 056 057/** 058 * This class provides a set of utilities that may be used in the course of 059 * persistence processing. 060 */ 061@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 062public final class PersistUtils 063{ 064 /** 065 * Prevent this utility class from being instantiated. 066 */ 067 private PersistUtils() 068 { 069 // No implementation required. 070 } 071 072 073 074 /** 075 * Indicates whether the provided string could be used as a valid attribute or 076 * object class name. Numeric OIDs will also be considered acceptable. 077 * 078 * @param s The string for which to make the determination. 079 * @param r A buffer to which the unacceptable reason may be appended. It 080 * must not be {@code null}. 081 * 082 * @return {@code true} if the provided string is acceptable for use as an 083 * LDAP attribute or object class name, or {@code false} if not. 084 */ 085 public static boolean isValidLDAPName(@NotNull final String s, 086 @NotNull final StringBuilder r) 087 { 088 return isValidLDAPName(s, false, r); 089 } 090 091 092 093 /** 094 * Indicates whether the provided string could be used as a valid attribute or 095 * object class name. Numeric OIDs will also be considered acceptable. 096 * 097 * @param s The string for which to make the determination. 098 * @param o Indicates whether the name should be allowed to contain 099 * attribute options (e.g., a semicolon with one or more valid 100 * characters after it). 101 * @param r A buffer to which the unacceptable reason may be appended. It 102 * must not be {@code null}. 103 * 104 * @return {@code true} if the provided string is acceptable for use as an 105 * LDAP attribute or object class name, or {@code false} if not. 106 */ 107 public static boolean isValidLDAPName(@NotNull final String s, 108 final boolean o, 109 @NotNull final StringBuilder r) 110 { 111 int length; 112 if ((s == null) || ((length = s.length()) == 0)) 113 { 114 r.append(ERR_LDAP_NAME_VALIDATOR_EMPTY.get()); 115 return false; 116 } 117 118 final String baseName; 119 final int semicolonPos = s.indexOf(';'); 120 if (semicolonPos > 0) 121 { 122 if (! o) 123 { 124 r.append(ERR_LDAP_NAME_VALIDATOR_INVALID_CHAR.get(s, ';', 125 semicolonPos)); 126 return false; 127 } 128 129 baseName = s.substring(0, semicolonPos); 130 length = baseName.length(); 131 132 final String optionsStr = s.substring(semicolonPos+1); 133 if (! isValidOptionSet(baseName, optionsStr, r)) 134 { 135 return false; 136 } 137 } 138 else 139 { 140 baseName = s; 141 } 142 143 if (StaticUtils.isNumericOID(baseName)) 144 { 145 return true; 146 } 147 148 for (int i=0; i < length; i++) 149 { 150 final char c = baseName.charAt(i); 151 if (((c >= 'a') && (c <= 'z')) || 152 ((c >= 'A') && (c <= 'Z'))) 153 { 154 // This will always be acceptable. 155 } 156 else if (((c >= '0') && (c <= '9')) || (c == '-')) 157 { 158 // This will be acceptable for all but the first character. 159 if (i == 0) 160 { 161 r.append(ERR_LDAP_NAME_VALIDATOR_INVALID_FIRST_CHAR.get(s)); 162 return false; 163 } 164 } 165 else 166 { 167 r.append(ERR_LDAP_NAME_VALIDATOR_INVALID_CHAR.get(s, c, i)); 168 return false; 169 } 170 } 171 172 return true; 173 } 174 175 176 177 /** 178 * Indicates whether the provided string represents a valid set of attribute 179 * options. It should not contain the initial semicolon. 180 * 181 * @param b The base name for the attribute, without the option string or 182 * the semicolon used to delimit the option string from the base 183 * name. 184 * @param o The option string to examine. It must not be {@code null}, and 185 * must not contain the initial semicolon. 186 * @param r A buffer to which the unacceptable reason may be appended. It 187 * must not be {@code null}. 188 * 189 * @return {@code true} if the provided string represents a valid set of 190 * options, or {@code false} if not. 191 */ 192 private static boolean isValidOptionSet(@NotNull final String b, 193 @NotNull final String o, 194 @NotNull final StringBuilder r) 195 { 196 boolean lastWasSemicolon = true; 197 198 for (int i=0; i < o.length(); i++) 199 { 200 final char c = o.charAt(i); 201 if (c == ';') 202 { 203 if (lastWasSemicolon) 204 { 205 r.append( 206 ERR_LDAP_NAME_VALIDATOR_OPTION_WITH_CONSECUTIVE_SEMICOLONS.get( 207 b + ';' + o)); 208 return false; 209 } 210 else 211 { 212 lastWasSemicolon = true; 213 } 214 } 215 else 216 { 217 lastWasSemicolon = false; 218 if (((c >= 'a') && (c <= 'z')) || 219 ((c >= 'A') && (c <= 'Z')) || 220 ((c >= '0') && (c <= '9')) || 221 (c == '-')) 222 { 223 // This will always be acceptable. 224 } 225 else 226 { 227 r.append(ERR_LDAP_NAME_VALIDATOR_INVALID_OPTION_CHAR.get( 228 (b + ';' + o), c, (b.length() + 1 + i))); 229 return false; 230 } 231 } 232 } 233 234 if (lastWasSemicolon) 235 { 236 r.append(ERR_LDAP_NAME_VALIDATOR_ENDS_WITH_SEMICOLON.get(b + ';' + o)); 237 return false; 238 } 239 240 return true; 241 } 242 243 244 245 /** 246 * Indicates whether the provided string could be used as a valid Java 247 * identifier. The identifier must begin with an ASCII letter or underscore, 248 * and must contain only ASCII letters, ASCII digits, and the underscore 249 * character. Even though a dollar sign is technically allowed, it will not 250 * be considered valid for the purpose of this method. Similarly, even though 251 * Java keywords are not allowed, they will not be rejected by this method. 252 * 253 * @param s The string for which to make the determination. It must not be 254 * {@code null}. 255 * @param r A buffer to which the unacceptable reason may be appended. It 256 * must not be {@code null}. 257 * 258 * @return {@code true} if the provided string is acceptable for use as a 259 * Java identifier, or {@code false} if not. 260 */ 261 public static boolean isValidJavaIdentifier(@NotNull final String s, 262 @NotNull final StringBuilder r) 263 { 264 final int length = s.length(); 265 for (int i=0; i < length; i++) 266 { 267 final char c = s.charAt(i); 268 if (((c >= 'a') && (c <= 'z')) || 269 ((c >= 'A') && (c <= 'Z')) || 270 (c == '_')) 271 { 272 // This will always be acceptable. 273 } 274 else if ((c >= '0') && (c <= '9')) 275 { 276 if (i == 0) 277 { 278 r.append(ERR_JAVA_NAME_VALIDATOR_INVALID_FIRST_CHAR_DIGIT.get(s)); 279 return false; 280 } 281 } 282 else 283 { 284 r.append(ERR_JAVA_NAME_VALIDATOR_INVALID_CHAR.get(s, c, i)); 285 return false; 286 } 287 } 288 289 return true; 290 } 291 292 293 294 /** 295 * Transforms the provided string if necessary so that it may be used as a 296 * valid Java identifier. If the provided string is already a valid Java 297 * identifier, then it will be returned as-is. Otherwise, it will be 298 * transformed to make it more suitable. 299 * 300 * @param s The attribute or object class name to be converted to a Java 301 * identifier. 302 * 303 * @return A string that may be used as a valid Java identifier. 304 */ 305 @NotNull() 306 public static String toJavaIdentifier(@NotNull final String s) 307 { 308 final int length; 309 if ((s == null) || ((length = s.length()) == 0)) 310 { 311 // This will be ugly, but safe. 312 return toJavaIdentifier(CryptoHelper.getRandomUUID().toString()); 313 } 314 315 boolean nextUpper = false; 316 final StringBuilder b = new StringBuilder(length); 317 for (int i=0; i < length; i++) 318 { 319 final char c = s.charAt(i); 320 if (((c >= 'a') && (c <= 'z')) || 321 ((c >= 'A') && (c <= 'Z'))) 322 { 323 if (nextUpper) 324 { 325 b.append(Character.toUpperCase(c)); 326 } 327 else 328 { 329 b.append(c); 330 } 331 332 nextUpper = false; 333 } 334 else if ((c >= '0') && (c <= '9')) 335 { 336 if (i == 0) 337 { 338 // Java identifiers can't begin with a digit, but they can begin with 339 // an underscore followed by a digit, so we'll use that instead. 340 b.append('_'); 341 } 342 343 b.append(c); 344 nextUpper = false; 345 } 346 else 347 { 348 // If the provided string was a valid LDAP attribute or object class 349 // name, then this should be a dash, but we'll be safe and take the same 350 // action for any remaining character. 351 nextUpper = true; 352 } 353 } 354 355 if (b.length() == 0) 356 { 357 // This should only happen if the provided string wasn't a valid LDAP 358 // attribute or object class name to start with. 359 return toJavaIdentifier(CryptoHelper.getRandomUUID().toString()); 360 } 361 362 return b.toString(); 363 } 364 365 366 367 /** 368 * Retrieves the entry with the specified DN and decodes it as an object of 369 * the specified type. 370 * 371 * @param <T> The type of object as which to decode the entry. 372 * 373 * @param dn The DN of the entry to retrieve. It must not be 374 * {@code null}. 375 * @param type The type of object as which the entry should be decoded. It 376 * must not be {@code null}, and the class must be marked with 377 * the {@link LDAPObject} annotation type. 378 * @param conn The connection that should be used to retrieve the entry. It 379 * must not be {@code null}. 380 * 381 * @return The object decoded from the specified entry, or {@code null} if 382 * the entry cannot be retrieved (e.g., because it does not exist or 383 * is not readable by the authenticated user). 384 * 385 * @throws LDAPException If a problem occurs while trying to retrieve the 386 * entry or decode it as the specified type of object. 387 */ 388 @Nullable() 389 public static <T> T getEntryAsObject(@NotNull final DN dn, 390 @NotNull final Class<T> type, 391 @NotNull final LDAPInterface conn) 392 throws LDAPException 393 { 394 Validator.ensureNotNull(dn, type, conn); 395 396 final LDAPPersister<T> p = LDAPPersister.getInstance(type); 397 398 final Entry e = conn.getEntry(dn.toString(), 399 p.getObjectHandler().getAttributesToRequest()); 400 if (e == null) 401 { 402 return null; 403 } 404 405 return p.decode(e); 406 } 407 408 409 410 /** 411 * Retrieves and decodes the indicated entries as objects of the specified 412 * type. 413 * 414 * @param <T> The type of object as which to decode the entries. 415 * 416 * @param dns The DNs of the entries to retrieve. It must not be 417 * {@code null}. 418 * @param type The type of object as which the entries should be decoded. 419 * It must not be {@code null}, and the class must be marked 420 * with the {@link LDAPObject} annotation type. 421 * @param conn The connection that should be used to retrieve the entries. 422 * It must not be {@code null}. 423 * 424 * @return A {@code PersistedObjects} result that may be used to access the 425 * objects decoded from the provided set of DNs. 426 * 427 * @throws LDAPPersistException If the requested type cannot be used with 428 * the LDAP SDK persistence framework. 429 */ 430 @NotNull() 431 public static <T> PersistedObjects<T> getEntriesAsObjects( 432 @NotNull final DN[] dns, 433 @NotNull final Class<T> type, 434 @NotNull final LDAPInterface conn) 435 throws LDAPPersistException 436 { 437 Validator.ensureNotNull(dns, type, conn); 438 439 final LDAPPersister<T> p = LDAPPersister.getInstance(type); 440 441 final DNEntrySource entrySource = new DNEntrySource(conn, dns, 442 p.getObjectHandler().getAttributesToRequest()); 443 return new PersistedObjects<>(p, entrySource); 444 } 445}