View Javadoc
1   package com.acumenvelocity.ath.common;
2   
3   import java.io.File;
4   import java.io.FileInputStream;
5   import java.io.IOException;
6   import java.lang.reflect.Method;
7   import java.net.URI;
8   import java.net.URLEncoder;
9   import java.nio.file.Files;
10  import java.nio.file.attribute.PosixFilePermissions;
11  import java.text.DateFormat;
12  import java.time.Instant;
13  import java.util.ArrayList;
14  import java.util.Collections;
15  import java.util.Date;
16  import java.util.List;
17  import java.util.Objects;
18  import java.util.Optional;
19  import java.util.UUID;
20  
21  import com.fasterxml.jackson.databind.JsonNode;
22  
23  import net.sf.okapi.common.BOMAwareInputStream;
24  import net.sf.okapi.common.StreamUtil;
25  import net.sf.okapi.common.Util;
26  
27  /**
28   * Utility class providing commonly used helper methods for
29   * null-safe operations, conversions, and lightweight object handling.
30   * 
31   * @author Acumen Velocity
32   * @version 1.0
33   * @since 1.0
34   */
35  public class AthUtil {
36  
37    /**
38     * Returns the given value if non-null, otherwise returns a default value.
39     *
40     * @param <T>    the type of the values
41     * @param val    the candidate value
42     * @param defVal the default value if {@code val} is {@code null}
43     * @return {@code val} if not {@code null}, otherwise {@code defVal}
44     */
45    public static <T> T fallback(T val, T defVal) {
46      return Optional.ofNullable(val).orElse(defVal);
47    }
48  
49    /**
50     * Converts the given object to its string representation,
51     * or returns a fallback string if the object is null.
52     *
53     * @param val the object to stringify
54     * @return the string representation or {@code defVal} if {@code val} is null
55     */
56    public static String safeToStr(Object val, String defVal) {
57      return val == null ? defVal : val.toString();
58    }
59  
60    /**
61     * Returns the provided string if non-empty, otherwise returns defVal.
62     *
63     * @param val the candidate string
64     * @return the string representation or {@code defVal} if {@code val} is null
65     */
66    public static String safeToStr(String val, String defVal) {
67      return Util.isEmpty(val) ? defVal : val;
68    }
69  
70    /**
71     * Attempts to parse an integer from a string.
72     * Returns the provided default value if parsing fails.
73     *
74     * @param val    the string to parse
75     * @param defVal the default value to return on failure
76     * @return parsed integer or {@code defVal} if parsing fails
77     */
78    public static Integer safeToInt(String val, int defVal) {
79      try {
80        return Integer.valueOf(val);
81  
82      } catch (Exception e) {
83        return defVal;
84      }
85    }
86  
87    public static UUID safeToUuid(String val, UUID defVal) {
88      try {
89        return UUID.fromString(val);
90  
91      } catch (Exception e) {
92        return defVal;
93      }
94    }
95  
96    public static List<UUID> safeToUuidList(Object val, List<UUID> defVal) {
97      if (val == null) {
98        return defVal != null ? defVal : Collections.emptyList();
99      }
100 
101     List<UUID> result = new ArrayList<>();
102 
103     if (val instanceof List) {
104       // Handle multi-valued field from Solr (e.g., List<String>)
105       for (Object item : (List<?>) val) {
106         if (item != null) {
107           UUID uuid = safeToUuid(item.toString(), null);
108 
109           if (uuid != null) {
110             result.add(uuid);
111           }
112         }
113       }
114     } else if (val instanceof String) {
115       // Handle comma-separated string of UUIDs
116       String[] uuidStrings = val.toString().split(",");
117 
118       for (String uuidStr : uuidStrings) {
119         uuidStr = uuidStr.trim();
120 
121         if (!uuidStr.isEmpty()) {
122           UUID uuid = safeToUuid(uuidStr, null);
123 
124           if (uuid != null) {
125             result.add(uuid);
126           }
127         }
128       }
129     }
130 
131     return result.isEmpty() && defVal != null ? defVal : result;
132   }
133 
134   public static <E extends Enum<E>> E safeToEnum(String val, Class<E> enumClass, E defVal) {
135     if (val == null) {
136       return defVal;
137     }
138 
139     try {
140       // First try standard valueOf (matches enum constant name)
141       return Enum.valueOf(enumClass, val);
142 
143     } catch (IllegalArgumentException e) {
144       // If that fails, try to find a fromValue method (common in generated enums)
145       try {
146         Method fromValue = enumClass.getMethod("fromValue", String.class);
147         return enumClass.cast(fromValue.invoke(null, val));
148 
149       } catch (Exception ex) {
150         return defVal;
151       }
152     }
153   }
154 
155   /**
156    * Adds a non-null element to a list.
157    *
158    * @param <E>  the element type
159    * @param list the list to add to
160    * @param elem the element to add (ignored if null)
161    * @return true if the element was added, false if {@code elem} was null
162    */
163   public static <E> boolean safeAddToList(List<E> list, E elem) {
164     if (elem == null) {
165       return false;
166     }
167     return list.add(elem);
168   }
169 
170   /**
171    * Returns the last {@code numChars} characters of a string.
172    *
173    * @param st       the string
174    * @param numChars the number of trailing characters to return
175    * @return substring of last {@code numChars}, or empty string if
176    *         {@code st} is null/empty or shorter than {@code numChars}
177    */
178   public static String lastChars(String st, int numChars) {
179     if (Util.isEmpty(st) || st.length() < numChars) {
180       return "";
181     }
182     return st.substring(st.length() - numChars);
183   }
184 
185   /**
186    * Creates a shallow copy of the given object using JSON serialization.
187    * <p>
188    * This relies on {@link JacksonUtil} for serialization and deserialization.
189    * Note that it is not a deep clone; nested objects may be shared.
190    *
191    * @param <T> the object type
192    * @param obj the object to clone
193    * @param cls the class type
194    * @return a shallow copy of {@code obj}
195    */
196   public static <T> T clone(T obj, Class<T> cls) {
197     return JacksonUtil.fromJson(JacksonUtil.toJson(obj, false), cls);
198   }
199 
200   public static <T> List<T> removeNulls(List<T> list) {
201     list.removeIf(Objects::isNull);
202     return list;
203   }
204 
205   public static String fileAsString(final File file) throws IOException {
206     try (final BOMAwareInputStream bis = new BOMAwareInputStream(new FileInputStream(file),
207         "UTF-8")) {
208       return StreamUtil.streamAsString(bis, bis.detectEncoding());
209     }
210   }
211 
212   /**
213    * Safely converts a JsonNode to a specified class type.
214    * Returns defVal if conversion fails or node is null.
215    */
216   public static <T> T safeFromJsonNode(JsonNode node, Class<T> clazz, T defVal) {
217     if (node == null) {
218       return defVal;
219     }
220     try {
221       return JacksonUtil.fromJsonNode(node, clazz);
222     } catch (Exception e) {
223       return defVal;
224     }
225   }
226 
227   /**
228    * Safely converts a Date object from an object.
229    * Returns defVal if field is null or not a Date.
230    */
231   public static Date safeToDate(Object val, Date defVal) {
232     if (val == null) {
233       return defVal;
234     }
235     try {
236       return (Date) val;
237     } catch (ClassCastException e) {
238       return defVal;
239     }
240   }
241 
242   /**
243    * Safely parses a Long from a string.
244    * Returns defVal if parsing fails or val is null.
245    */
246   public static Long safeToLong(String val, Long defVal) {
247     if (val == null) {
248       return defVal;
249     }
250     try {
251       return Long.parseLong(val);
252     } catch (NumberFormatException e) {
253       return defVal;
254     }
255   }
256 
257   /**
258    * Safely parses a Float from a string or object.
259    * Returns defVal if parsing fails or val is null.
260    */
261   public static Float safeToFloat(Object val, Float defVal) {
262     if (val == null) {
263       return defVal;
264     }
265     try {
266       if (val instanceof Float) {
267         return (Float) val;
268       }
269       return Float.parseFloat(val.toString());
270     } catch (Exception e) {
271       return defVal;
272     }
273   }
274 
275   /**
276    * Safely parses a Float from a string or object.
277    * Returns defVal if parsing fails or val is null.
278    */
279   public static Double safeToDouble(Object val, Double defVal) {
280     if (val == null) {
281       return defVal;
282     }
283     
284     try {
285       if (val instanceof Double) {
286         return (Double) val;
287       }
288       
289       return Double.parseDouble(val.toString());
290       
291     } catch (Exception e) {
292       return defVal;
293     }
294   }
295 
296   /**
297    * Safely parses an ISO-8601 date string from JsonNode.
298    * Returns defVal if parsing fails.
299    */
300   public static Date safeToDateFromIso(String isoDateStr, Date defVal) {
301     if (Util.isEmpty(isoDateStr)) {
302       return defVal;
303     }
304     try {
305       return Date.from(Instant.parse(isoDateStr));
306     } catch (Exception e) {
307       return defVal;
308     }
309   }
310 
311   public static String dateToString(DateFormat dateFormat, Date date) {
312     return dateFormat.format(date);
313   }
314 
315   public static File createTempFile() {
316     File file = null;
317 
318     try {
319       file = File.createTempFile(Const.TEMP_PREFIX, null);
320       file.deleteOnExit();
321 
322       // Set the file permissions to allow only the owner to read and write
323       try {
324         Files.setPosixFilePermissions(file.toPath(),
325             PosixFilePermissions.fromString("rw-------"));
326 
327       } catch (UnsupportedOperationException e) {
328         // Non-POSIX OS like Windows
329         file.setReadable(true);
330         file.setWritable(true);
331       }
332 
333       return file;
334 
335     } catch (Exception e) {
336       Log.warn(AthUtil.class, "Cannot create a temp file: {}", e.getMessage());
337       return null;
338     }
339   }
340 
341   /**
342    * Converts a string to URI, encoding the path component if needed
343    * Handles any URI scheme (gs://, http://, https://, file://, etc.)
344    */
345   public static URI toURI(String uriString) {
346     if (Util.isEmpty(uriString)) {
347       return null;
348     }
349 
350     try {
351       String trimmed = uriString.trim();
352 
353       // Try direct creation first
354       try {
355         return URI.create(trimmed);
356 
357       } catch (IllegalArgumentException e) {
358         // If direct creation fails, try encoding the path
359       }
360 
361       // Parse the URI to encode only the path component
362       int schemeEnd = trimmed.indexOf("://");
363 
364       if (schemeEnd == -1) {
365         // No scheme, treat as path only
366         return URI.create(encodePath(trimmed));
367       }
368 
369       String scheme = trimmed.substring(0, schemeEnd);
370       String rest = trimmed.substring(schemeEnd + 3); // After "://"
371 
372       // Find where authority ends and path begins
373       int pathStart = rest.indexOf('/');
374 
375       if (pathStart == -1) {
376         // No path, just scheme://authority
377         return URI.create(trimmed);
378       }
379 
380       String authority = rest.substring(0, pathStart);
381       String pathAndQuery = rest.substring(pathStart + 1);
382 
383       // Split path from query/fragment
384       int queryStart = pathAndQuery.indexOf('?');
385       int fragmentStart = pathAndQuery.indexOf('#');
386 
387       String path;
388       String suffix = "";
389 
390       if (queryStart != -1) {
391         path = pathAndQuery.substring(0, queryStart);
392         suffix = pathAndQuery.substring(queryStart);
393 
394       } else if (fragmentStart != -1) {
395         path = pathAndQuery.substring(0, fragmentStart);
396         suffix = pathAndQuery.substring(fragmentStart);
397 
398       } else {
399         path = pathAndQuery;
400       }
401 
402       // Encode the path segments
403       String encodedPath = encodePath(path);
404 
405       return URI.create(scheme + "://" + authority + "/" + encodedPath + suffix);
406 
407     } catch (Exception e) {
408       return null;
409     }
410   }
411 
412   /**
413    * Encodes each segment of a path
414    */
415   private static String encodePath(String path) throws Exception {
416     if (path.isEmpty()) {
417       return path;
418     }
419 
420     String[] segments = path.split("/", -1); // -1 to preserve trailing empty strings
421     StringBuilder encoded = new StringBuilder();
422 
423     for (int i = 0; i < segments.length; i++) {
424       if (i > 0)
425         encoded.append("/");
426 
427       if (!segments[i].isEmpty()) {
428         encoded.append(URLEncoder.encode(segments[i], "UTF-8")
429             .replace("+", "%20")); // Use %20 instead of + for spaces
430       }
431     }
432 
433     return encoded.toString();
434   }
435 
436   /**
437    * Extracts the last section after the last slash in a path.
438    * 
439    * @param path the input path string
440    * @return the last section after the last slash, or empty string if no slash found
441    * @throws NullPointerException if the input path is null
442    */
443   public static String extractLastSection(String path) {
444     Objects.requireNonNull(path, "Path cannot be null");
445 
446     int lastSlashIndex = path.lastIndexOf('/');
447 
448     if (lastSlashIndex == -1) {
449       return "";
450     }
451 
452     return path.substring(lastSlashIndex + 1);
453   }
454 
455   /**
456    * Extracts the last section after the last slash, with a fallback value.
457    * 
458    * @param path         the input path string
459    * @param defaultValue the value to return if no section can be extracted
460    * @return the last section after slash, or defaultValue if not found
461    */
462   public static String extractLastSectionOrDefault(String path, String defaultValue) {
463     if (path == null || path.isEmpty()) {
464       return defaultValue;
465     }
466 
467     int lastSlashIndex = path.lastIndexOf('/');
468     if (lastSlashIndex == -1 || lastSlashIndex == path.length() - 1) {
469       return defaultValue;
470     }
471 
472     return path.substring(lastSlashIndex + 1);
473   }
474 }