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