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