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}