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-2012 UnboundID Corp.
026     */
027    package com.unboundid.directory.sdk.sync.util;
028    
029    import com.unboundid.ldap.sdk.Attribute;
030    import com.unboundid.ldap.sdk.DN;
031    import com.unboundid.ldap.sdk.Entry;
032    import com.unboundid.ldap.sdk.RDN;
033    import com.unboundid.util.ThreadSafety;
034    import com.unboundid.util.ThreadSafetyLevel;
035    
036    import java.util.ArrayList;
037    import java.util.Date;
038    import java.util.HashMap;
039    import java.util.List;
040    import java.util.Map;
041    import java.sql.Blob;
042    import java.sql.SQLException;
043    import java.sql.Timestamp;
044    import java.text.ParseException;
045    import 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)
054    public 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    }