001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at
010 * docs/licenses/cddl.txt
011 * or http://www.opensource.org/licenses/cddl1.php.
012 * See the License for the specific language governing permissions
013 * and limitations under the License.
014 *
015 * When distributing Covered Code, include this CDDL HEADER in each
016 * file and include the License file at
017 * docs/licenses/cddl.txt.  If applicable,
018 * add the following below this CDDL HEADER, with the fields enclosed
019 * by brackets "[]" replaced with your own identifying information:
020 *      Portions Copyright [yyyy] [name of copyright owner]
021 *
022 * CDDL HEADER END
023 *
024 *
025 *      Portions Copyright 2010-2024 Ping Identity Corporation
026 */
027package com.unboundid.directory.sdk.sync.util;
028
029import com.unboundid.ldap.sdk.Attribute;
030import com.unboundid.ldap.sdk.DN;
031import com.unboundid.ldap.sdk.Entry;
032import com.unboundid.ldap.sdk.RDN;
033import com.unboundid.util.ThreadSafety;
034import com.unboundid.util.ThreadSafetyLevel;
035
036import java.util.ArrayList;
037import java.util.Date;
038import java.util.HashMap;
039import java.util.List;
040import java.util.Map;
041import java.sql.Blob;
042import java.sql.SQLException;
043import java.sql.Timestamp;
044import java.text.ParseException;
045import java.text.SimpleDateFormat;
046import java.util.regex.Pattern;
047
048/**
049 * This class contains various utility methods for working with the
050 * UnboundID LDAP SDK and JDBC objects within script implementations. These
051 * are useful for populating LDAP entries with content from JDBC result sets
052 * and also for working with DNs, Timestamps, and other objects.
053 */
054@ThreadSafety(level = ThreadSafetyLevel.COMPLETELY_THREADSAFE)
055public final class ScriptUtils
056{
057
058  /**
059   * The date format string that will be used to construct and parse dates
060   * represented using generalized time with a four-digit year.
061   */
062  private static final String DATE_FORMAT_GMT_TIME =
063          "yyyyMMddHHmmss'Z'";
064
065  /**
066   * The date format string that will be used to construct and parse dates
067   * represented using generalized time.
068   */
069  private static final String DATE_FORMAT_GENERALIZED_TIME =
070          "yyyyMMddHHmmss.SSS'Z'";
071
072  // DateFormats are not thread-safe, so we provide a thread local
073  private static final ThreadLocal<SimpleDateFormat> gmtTime =
074    new ThreadLocal<SimpleDateFormat>()
075        {
076          @Override
077          protected SimpleDateFormat initialValue()
078          {
079            return new SimpleDateFormat(DATE_FORMAT_GMT_TIME);
080          }
081        };
082
083  private static final ThreadLocal<SimpleDateFormat> generalizedTime =
084    new ThreadLocal<SimpleDateFormat>()
085        {
086          @Override
087          protected SimpleDateFormat initialValue()
088          {
089            return new SimpleDateFormat(DATE_FORMAT_GENERALIZED_TIME);
090          }
091        };
092
093  // Precompiled regular expressions used to remove spaces.
094  private static final Pattern leadingSpaceRE = Pattern.compile("^ *");
095  private static final Pattern trailingSpaceRE = Pattern.compile(" *$");
096
097  /**
098   * Private constructor to enforce non-instantiability.
099   */
100  private ScriptUtils() {}
101
102  /**
103   * Adds an {@link Attribute} with the given attribute name and string value to
104   * the given entry if the value is not null.
105   * @param entry
106   *          an LDAP entry instance. May be null.
107   * @param attrName
108   *          the name of the attribute to add to the entry. May <b>not</b> be
109   *          null.
110   * @param value
111   *          the value for the attribute to add to the entry. May be null.
112   */
113  public static void addStringAttribute(final Entry entry,
114                                        final String attrName,
115                                        final String value)
116  {
117    if(entry != null && value != null)
118    {
119      entry.addAttribute(attrName, value);
120    }
121  }
122
123  /**
124   * Adds an {@link Attribute} with the given attribute name and numeric value
125   * to the given entry if the value is not null.
126   * @param entry
127   *          an LDAP entry instance. May be null.
128   * @param attrName
129   *          the name of the attribute to add to the entry. May <b>not</b> be
130   *          null.
131   * @param value
132   *          the value for the attribute to add to the entry. May be null.
133   */
134  public static void addNumericAttribute(final Entry entry,
135                                         final String attrName,
136                                         final Number value)
137  {
138    if(entry != null && value != null)
139    {
140      entry.addAttribute(attrName, value.toString());
141    }
142  }
143
144  /**
145   * Adds an {@link Attribute} with the given attribute name and boolean value
146   * to the given entry if the value is not null. If the boolean is
147   * <code>true</code>, the attribute value will be the string "true", otherwise
148   * it will be the string "false".
149   * @param entry
150   *          an LDAP entry instance. May be null.
151   * @param attrName
152   *          the name of the attribute to add to the entry. May <b>not</b> be
153   *          null.
154   * @param value
155   *          the value for the attribute to add to the entry. May be null.
156   */
157  public static void addBooleanAttribute(final Entry entry,
158                                         final String attrName,
159                                         final Boolean value)
160  {
161    if(entry != null && value != null)
162    {
163      entry.addAttribute(attrName, value.toString());
164    }
165  }
166
167  /**
168   * Adds an {@link Attribute} with the given attribute name and {@link Date}
169   * value to the given entry if the value is not null. The date is formatted
170   * using the generalized time syntax with a four-digit year and an optional
171   * milliseconds component (i.e. yyyyMMddHHmmss[.SSS]'Z').
172   * @param entry
173   *          entry an LDAP entry instance. May be null.
174   * @param attrName
175   *          the name of the attribute to add to the entry. May <b>not</b> be
176   *          null.
177   * @param date
178   *          the Date value for the attribute to add to the entry. May be null.
179   * @param includeMilliseconds
180   *          whether to include the milliseconds component in the attribute
181   *          value
182   */
183  public static void addDateAttribute(final Entry entry,
184                                      final String attrName,
185                                      final Date date,
186                                      final boolean includeMilliseconds)
187  {
188    if(entry != null && date != null)
189    {
190      String result;
191      if(includeMilliseconds)
192      {
193        result = generalizedTime.get().format(date);
194      }
195      else
196      {
197        result = gmtTime.get().format(date);
198      }
199      entry.addAttribute(attrName, result);
200    }
201  }
202
203  /**
204   * Adds an {@link Attribute} with the given attribute name and {@link Blob}
205   * value to the given entry if the underlying byte array is not null or empty.
206   * This method calls <code>free()</code> on the Blob object after the
207   * attribute has been added to the entry.
208   * @param entry
209   *          an LDAP entry instance. May be null.
210   * @param attrName
211   *          the name of the attribute to add to the entry. May <b>not</b> be
212   *          null.
213   * @param value
214   *          the value for the attribute to add to the entry. May be null.
215   * @param maxBytes
216   *          the maximum number of bytes to extract from the Blob
217   *          and add to the entry.
218   */
219  public static void addBinaryAttribute(final Entry entry,
220                                        final String attrName,
221                                        final Blob value,
222                                        final int maxBytes)
223  {
224    if(entry != null && value != null)
225    {
226      try
227      {
228        byte[] bytes = value.getBytes(1, maxBytes);
229        if(bytes.length > 0)
230        {
231          entry.addAttribute(attrName, bytes);
232        }
233        value.free();
234      }
235      catch(SQLException e)
236      {
237        // suppress
238      }
239    }
240  }
241
242  /**
243   * Returns true if the given attribute is not null and contains any one or
244   * more of the given string values; returns false otherwise. This method
245   * is case-insensitive.
246   * @param attr
247   *          the {@link Attribute} whose values to check
248   * @param values
249   *          the value(s) you are looking for
250   * @return true if any of the values were found in the attribute, false if not
251   */
252  public static boolean containsAnyValue(final Attribute attr,
253                                         final String... values)
254  {
255    if(attr == null)
256    {
257      return false;
258    }
259    for(String attrValue : attr.getValues())
260    {
261      for(String valueToCheck : values)
262      {
263        if(attrValue.equalsIgnoreCase(valueToCheck))
264        {
265          return true;
266        }
267      }
268    }
269    return false;
270  }
271
272  /**
273   * String helper method to check if a value is a null object
274   * or empty string.
275   * @param value
276   *          the String object to check
277   * @return true if the value is null or empty string, false otherwise
278   */
279  public static boolean isNullOrEmpty(final String value)
280  {
281    if(value == null)
282    {
283      return true;
284    }
285    return value.isEmpty();
286  }
287
288  /**
289   * Returns a SQL {@link Timestamp} based on the string value that is passed
290   * in. The string is parsed using generalized time syntax first with and then
291   * without milliseconds (i.e. yyyyMMddHHmmss[.SSS]'Z'). If the string cannot
292   * be parsed, <code>null</code> is returned.
293   * @param value
294   *          a string that represents a timestamp in generalized time format
295   * @return a SQL Timestamp value set to the time that was passed in, or null
296   *         if the value cannot be parsed
297   */
298  public static Timestamp getTimestampFromString(final String value)
299  {
300    Timestamp ts = null;
301    if(value == null)
302    {
303      return ts;
304    }
305
306    // generalized time (with milliseconds) is a more specific format,
307    // so try to parse as that first
308    try
309    {
310      Date d = generalizedTime.get().parse(value);
311      ts = new Timestamp(d.getTime());
312      return ts;
313    }
314    catch(ParseException e)
315    {
316    }
317
318    // try to parse as GMT time
319    try
320    {
321      Date d = gmtTime.get().parse(value);
322      ts = new Timestamp(d.getTime());
323    }
324    catch(ParseException e)
325    {
326    }
327
328    return ts;
329  }
330
331  /**
332   * Takes an identifier string (for example from the database changelog table)
333   * and creates a DN from the components. If there are multiple primary keys
334   * in the identifier, they must be delimited by a unique string with which
335   * the identifier can be split. For example, you could specify a delimiter of
336   * "%%" to handle the following identifier: account_id=123%%group_id=5.
337   * <p>
338   * The resulting DN will contain a RDN per component, and the relative order
339   * of RDNs will be consistent with the order of the components in the original
340   * identifier string. The components here are usually
341   * primary keys for the entry in the database.
342   * @param identifiableInfo
343   *          the identifier string for a given database entry. This cannot be
344   *          null.
345   * @param delimiter
346   *          The delimiter used to split separate components of the
347   *          identifiable info. If this is null, the default of "%%" will be
348   *          used.
349   * @return a DN representing the given identifier.
350   */
351  public static DN idStringToDN(final String identifiableInfo,
352                                final String delimiter)
353  {
354    String defaultDelimiter = delimiter;
355    if(delimiter == null || delimiter.isEmpty())
356    {
357      defaultDelimiter = "%%"; //default delimiter
358    }
359
360    List<RDN> rdnList = new ArrayList<RDN>();
361    String[] pairs = identifiableInfo.split(defaultDelimiter);
362    for(String pair : pairs)
363    {
364      String[] kv = pair.split("=", 2);
365      if(kv.length != 2)
366      {
367        throw new IllegalArgumentException("Malformed identifier component: " +
368                    pair);
369      }
370
371      // Strip of leading and trailing spaces.  This is like String.trim()
372      // except that only spaces are removed.
373      String key = trailingSpaceRE.matcher(
374              leadingSpaceRE.matcher(kv[0]).replaceAll("")).replaceAll("");
375      String value = trailingSpaceRE.matcher(
376              leadingSpaceRE.matcher(kv[1]).replaceAll("")).replaceAll("");
377
378      rdnList.add(new RDN(key, value));
379    }
380
381    if(rdnList.isEmpty())
382    {
383      throw new IllegalArgumentException(
384              "The identifiableInfo parameter is empty.");
385    }
386
387    return new DN(rdnList);
388  }
389
390  /**
391   * Takes an identifier DN (such as the output from
392   * {@link #idStringToDN(String,String)} and
393   * creates a hash map of the components (RDN attributes) to their respective
394   * values. Each RDN will have a separate entry in the resulting map.
395   * <p>
396   * This method is meant to handle database entry identifier DNs, and as such
397   * does not handle DNs with multi-valued RDNs (i.e.
398   * pk1=John+pk2=Doe,groupID=123).
399   * @param identifiableInfo
400   *          the identifier DN for a particular database entry. This cannot be
401   *          null.
402   * @return a map of each RDN name to its value (from the given DN)
403   */
404  public static Map<String, String> dnToMap(final DN identifiableInfo)
405  {
406
407    Map<String, String> ids = new HashMap<String, String>();
408    for(RDN rdn : identifiableInfo.getRDNs())
409    {
410      String[] kv = rdn.toString().split("=", 2);
411      ids.put(kv[0], kv[1]);
412    }
413    return ids;
414  }
415}