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}