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