001package com.box.sdkgen.internal.utils;
002
003import com.box.sdkgen.box.errors.BoxSDKError;
004import com.box.sdkgen.internal.SerializableObject;
005import com.box.sdkgen.serialization.json.EnumWrapper;
006import com.box.sdkgen.serialization.json.JsonManager;
007import com.box.sdkgen.serialization.json.Valuable;
008import com.fasterxml.jackson.databind.JsonNode;
009import com.fasterxml.jackson.databind.ObjectMapper;
010import com.fasterxml.jackson.databind.node.ArrayNode;
011import java.io.ByteArrayInputStream;
012import java.io.ByteArrayOutputStream;
013import java.io.FileNotFoundException;
014import java.io.FileOutputStream;
015import java.io.IOException;
016import java.io.InputStream;
017import java.io.OutputStream;
018import java.math.BigInteger;
019import java.nio.charset.StandardCharsets;
020import java.nio.file.Files;
021import java.nio.file.Paths;
022import java.security.MessageDigest;
023import java.time.OffsetDateTime;
024import java.time.ZoneOffset;
025import java.time.format.DateTimeFormatter;
026import java.time.format.DateTimeFormatterBuilder;
027import java.time.format.DateTimeParseException;
028import java.time.temporal.ChronoField;
029import java.time.temporal.ChronoUnit;
030import java.util.Arrays;
031import java.util.Base64;
032import java.util.HashMap;
033import java.util.Iterator;
034import java.util.List;
035import java.util.Locale;
036import java.util.Map;
037import java.util.NoSuchElementException;
038import java.util.Objects;
039import java.util.Set;
040import java.util.UUID;
041import java.util.function.BiFunction;
042import java.util.stream.Collectors;
043import javax.crypto.Mac;
044import javax.crypto.spec.SecretKeySpec;
045import org.jose4j.jws.JsonWebSignature;
046import org.jose4j.jwt.JwtClaims;
047import org.jose4j.jwt.NumericDate;
048import org.jose4j.lang.JoseException;
049
050public class UtilsManager {
051  private static final int BUFFER_SIZE = 8192;
052
053  private static final DateTimeFormatter OFFSET_DATE_TIME_FORMAT =
054      new DateTimeFormatterBuilder()
055          .appendPattern("yyyy-MM-dd'T'HH:mm:ss")
056          .optionalStart()
057          .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
058          .optionalEnd()
059          .appendOffsetId()
060          .toFormatter();
061  private static final DateTimeFormatter OFFSET_DATE_FORMAT =
062      DateTimeFormatter.ofPattern("yyyy-MM-dd");
063
064  public static <K, V> Map<K, V> mapOf(Entry<K, V>... entries) {
065    return Arrays.stream(entries)
066        .collect(
067            HashMap::new,
068            (map, entry) -> map.put(entry.getKey(), entry.getValue()),
069            HashMap::putAll);
070  }
071
072  public static <V> Set<V> setOf(V... values) {
073    return Arrays.stream(values).collect(Collectors.toSet());
074  }
075
076  public static <K, V> Entry<K, V> entryOf(K key, V value) {
077    return Entry.of(key, value);
078  }
079
080  public static <K, V> Map<K, V> mergeMaps(Map<K, V> map1, Map<K, V> map2) {
081    Map<K, V> mergedMap = new HashMap<>();
082    if (map1 != null) {
083      mergedMap.putAll(map1);
084    }
085    if (map2 != null) {
086      mergedMap.putAll(map2);
087    }
088    return mergedMap;
089  }
090
091  public static Map<String, String> prepareParams(Map<String, String> map) {
092    map.values().removeIf(Objects::isNull);
093    return map;
094  }
095
096  public static String convertToString(Object value) {
097    if (value == null) {
098      return null;
099    }
100    if (value instanceof EnumWrapper) {
101      return ((EnumWrapper<?>) value).getStringValue();
102    }
103    if (value instanceof Valuable) {
104      return ((Valuable) value).getValue();
105    }
106    if (value instanceof List) {
107      List<?> list = (List<?>) value;
108      if (!list.isEmpty() && list.get(0) instanceof SerializableObject) {
109        return JsonManager.serialize(value).toString();
110      } else {
111        return ((List<?>) value)
112            .stream().map(UtilsManager::convertToString).collect(Collectors.joining(","));
113      }
114    }
115    if (value instanceof ArrayNode) {
116      return convertToString(new ObjectMapper().convertValue(value, List.class));
117    }
118    if (value instanceof JsonNode) {
119      return ((JsonNode) value).asText();
120    }
121    if (value instanceof SerializableObject) {
122      return JsonManager.serialize(value).toString();
123    }
124    return value.toString();
125  }
126
127  public static void writeInputStreamToOutputStream(InputStream input, OutputStream output) {
128    try {
129      byte[] buffer = new byte[BUFFER_SIZE];
130      int n = input.read(buffer);
131      while (n != -1) {
132        output.write(buffer, 0, n);
133        n = input.read(buffer);
134      }
135    } catch (IOException e) {
136      throw new RuntimeException(e);
137    } finally {
138      try {
139        input.close();
140        output.close();
141      } catch (IOException e) {
142        throw new RuntimeException(e);
143      }
144    }
145  }
146
147  public static String getUuid() {
148    return UUID.randomUUID().toString();
149  }
150
151  public static byte[] generateByteBuffer(int size) {
152    byte[] bytes = new byte[size];
153    Arrays.fill(bytes, (byte) 0);
154    return bytes;
155  }
156
157  public static InputStream generateByteStream(int size) {
158    byte[] bytes = generateByteBuffer(size);
159    return new ByteArrayInputStream(bytes);
160  }
161
162  public static InputStream generateByteStreamFromBuffer(byte[] buffer) {
163    return new ByteArrayInputStream(buffer);
164  }
165
166  public static byte[] readByteStream(InputStream inputStream) {
167    ByteArrayOutputStream buffer = new ByteArrayOutputStream();
168    byte[] data = new byte[BUFFER_SIZE];
169    int bytesRead;
170    try {
171      while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) {
172        buffer.write(data, 0, bytesRead);
173      }
174    } catch (IOException e) {
175      throw new RuntimeException(e);
176    } finally {
177      try {
178        inputStream.close();
179      } catch (IOException e) {
180        throw new RuntimeException(e);
181      }
182    }
183
184    return buffer.toByteArray();
185  }
186
187  public static boolean bufferEquals(byte[] buffer1, byte[] buffer2) {
188    return Arrays.equals(buffer1, buffer2);
189  }
190
191  public static int bufferLength(byte[] buffer) {
192    return buffer.length;
193  }
194
195  public static InputStream decodeBase64ByteStream(String value) {
196    return new ByteArrayInputStream(Base64.getDecoder().decode(value));
197  }
198
199  public static String decodeBase64(String value) {
200    return new String(Base64.getDecoder().decode(value));
201  }
202
203  public static InputStream stringToByteStream(String value) {
204    return new ByteArrayInputStream(value.getBytes());
205  }
206
207  public static OutputStream getFileOutputStream(String filePath) {
208    try {
209      return new FileOutputStream(filePath);
210    } catch (FileNotFoundException e) {
211      throw new RuntimeException(e);
212    }
213  }
214
215  public static void closeFileOutputStream(OutputStream outputStream) {
216    try {
217      outputStream.close();
218    } catch (IOException e) {
219      throw new RuntimeException(e);
220    }
221  }
222
223  public static byte[] readBufferFromFile(String filePath) {
224    try {
225      InputStream inputStream = Files.newInputStream(Paths.get(filePath));
226      return readByteStream(inputStream);
227    } catch (IOException e) {
228      throw new RuntimeException(e);
229    }
230  }
231
232  public static String getEnvVar(String envVar) {
233    return System.getenv(envVar);
234  }
235
236  public static void delayInSeconds(int seconds) {
237    try {
238      Thread.sleep(seconds * 1000L);
239    } catch (InterruptedException e) {
240      throw new RuntimeException(e);
241    }
242  }
243
244  public static String readTextFromFile(String filePath) {
245    try {
246      return new String(Files.readAllBytes(Paths.get(filePath)));
247    } catch (IOException e) {
248      throw new RuntimeException(e);
249    }
250  }
251
252  public static boolean isBrowser() {
253    return false;
254  }
255
256  public static long getEpochTimeInSeconds() {
257    return System.currentTimeMillis() / 1000;
258  }
259
260  public static String createJwtAssertion(
261      Map<String, Object> claims, JwtKey jwtKey, JwtSignOptions jwtOptions) {
262    JwtClaims jwtClaims = new JwtClaims();
263    jwtClaims.setIssuer(jwtOptions.getIssuer());
264    jwtClaims.setAudience(jwtOptions.getAudience());
265    jwtClaims.setExpirationTime(NumericDate.fromSeconds((Long) claims.get("exp")));
266
267    jwtClaims.setSubject(jwtOptions.getSubject());
268    jwtClaims.setClaim("box_sub_type", claims.get("box_sub_type"));
269    jwtClaims.setGeneratedJwtId(64);
270
271    JsonWebSignature jws = new JsonWebSignature();
272    jws.setPayload(jwtClaims.toJson());
273    jws.setKey(
274        jwtOptions.privateKeyDecryptor.decryptPrivateKey(jwtKey.getKey(), jwtKey.getPassphrase()));
275    jws.setAlgorithmHeaderValue(jwtOptions.getAlgorithm().getValue());
276    jws.setHeader("typ", "JWT");
277    if ((jwtOptions.getKeyid() != null) && !jwtOptions.getKeyid().isEmpty()) {
278      jws.setHeader("kid", jwtOptions.getKeyid());
279    }
280
281    String assertion;
282
283    try {
284      assertion = jws.getCompactSerialization();
285    } catch (JoseException e) {
286      throw new BoxSDKError("Error serializing JSON Web Token assertion.", e);
287    }
288
289    return assertion;
290  }
291
292  public static JsonNode getValueFromObjectRawData(SerializableObject obj, String key) {
293    JsonNode value = obj.getRawData();
294    for (String k : key.split("\\.")) {
295      if (value == null || !value.has(k)) {
296        return null;
297      }
298      value = value.get(k);
299    }
300
301    return value;
302  }
303
304  public static double random(double min, double max) {
305    return Math.random() * (max - min) + min;
306  }
307
308  public static String hexToBase64(String hex) {
309    return Base64.getEncoder().encodeToString(new BigInteger(hex, 16).toByteArray());
310  }
311
312  public static Iterator<InputStream> iterateChunks(
313      InputStream stream, long chunkSize, long fileSize) {
314    return new Iterator<InputStream>() {
315      private InputStream nextChunk;
316      private boolean isNextChunkPrepared = false;
317
318      private void prepareNext() {
319        if (isNextChunkPrepared) {
320          return;
321        }
322        isNextChunkPrepared = true;
323        try {
324          byte[] buffer = new byte[(int) chunkSize];
325          int bytesRead = 0;
326
327          while (bytesRead < chunkSize) {
328            int read = stream.read(buffer, bytesRead, (int) (chunkSize - bytesRead));
329            if (read == -1) {
330              break;
331            }
332            bytesRead += read;
333          }
334
335          if (bytesRead == 0) {
336            nextChunk = null;
337            return;
338          }
339
340          nextChunk = new ByteArrayInputStream(buffer, 0, bytesRead);
341        } catch (IOException e) {
342          throw new RuntimeException("Error reading from stream", e);
343        }
344      }
345
346      @Override
347      public boolean hasNext() {
348        prepareNext();
349        return nextChunk != null;
350      }
351
352      @Override
353      public InputStream next() {
354        prepareNext();
355        if (nextChunk == null) {
356          throw new NoSuchElementException();
357        }
358        InputStream result = nextChunk;
359        nextChunk = null;
360        isNextChunkPrepared = false;
361        return result;
362      }
363    };
364  }
365
366  /**
367   * Reduces an iterator using a reducer function and an initial value.
368   *
369   * @param <Accumulator> The type of the accumulator (result)
370   * @param <T> The type of the items in the iterator
371   * @param iterator The iterator to process
372   * @param reducer The reducer function
373   * @param initialValue The initial value for the accumulator
374   * @return The accumulated result
375   */
376  public static <Accumulator, T> Accumulator reduceIterator(
377      Iterator<T> iterator,
378      BiFunction<Accumulator, T, Accumulator> reducer,
379      Accumulator initialValue) {
380    Accumulator result = initialValue;
381
382    while (iterator.hasNext()) {
383      T item = iterator.next();
384      result = reducer.apply(result, item);
385    }
386
387    return result;
388  }
389
390  public static Map<String, String> sanitizeMap(
391      Map<String, String> dictionary, Map<String, String> keysToSanitize) {
392    return dictionary.entrySet().stream()
393        .collect(
394            Collectors.toMap(
395                Map.Entry::getKey,
396                entry ->
397                    keysToSanitize.containsKey(entry.getKey().toLowerCase(Locale.ROOT))
398                        ? JsonManager.sanitizedValue()
399                        : entry.getValue()));
400  }
401
402  public static OffsetDateTime dateTimeFromString(String dateString) {
403    try {
404      return OffsetDateTime.parse(dateString, OFFSET_DATE_TIME_FORMAT);
405    } catch (DateTimeParseException e) {
406      return null;
407    }
408  }
409
410  public static String dateTimeToString(OffsetDateTime dateTime) {
411    return dateTime.truncatedTo(ChronoUnit.SECONDS).format(OFFSET_DATE_TIME_FORMAT);
412  }
413
414  public static OffsetDateTime dateFromString(String dateString) {
415    try {
416      // For date-only strings, parse as date and convert to OffsetDateTime at start of day UTC
417      if (dateString.matches("\\d{4}-\\d{2}-\\d{2}")) {
418        return OffsetDateTime.parse(dateString + "T00:00:00Z");
419      }
420      // Otherwise try to parse as full OffsetDateTime
421      return dateTimeFromString(dateString);
422    } catch (DateTimeParseException e) {
423      return null;
424    }
425  }
426
427  public static String dateToString(OffsetDateTime date) {
428    return date.format(OFFSET_DATE_FORMAT);
429  }
430
431  public static String escapeUnicode(String value) {
432    if (value == null) {
433      return null;
434    }
435
436    StringBuilder result = new StringBuilder();
437    for (int i = 0; i < value.length(); i++) {
438      char ch = value.charAt(i);
439      if (ch >= 0x007F) {
440        result.append(String.format("\\u%04x", (int) ch));
441      } else if (ch == '\\') {
442        result.append("\\\\");
443      } else if (ch == '\n') {
444        result.append("\\n");
445      } else if (ch == '\r') {
446        result.append("\\r");
447      } else if (ch == '\t') {
448        result.append("\\t");
449      } else if (ch == '/') {
450        if (i == 0 || value.charAt(i - 1) != '\\') {
451          result.append("\\/");
452        } else {
453          result.append(ch);
454        }
455      } else {
456        result.append(ch);
457      }
458    }
459    return result.toString();
460  }
461
462  public static OffsetDateTime epochSecondsToDateTime(long seconds) {
463    return OffsetDateTime.ofInstant(java.time.Instant.ofEpochSecond(seconds), ZoneOffset.UTC);
464  }
465
466  public static long dateTimeToEpochSeconds(OffsetDateTime dateTime) {
467    return dateTime.toEpochSecond();
468  }
469
470  public static boolean compareSignatures(String expectedSignature, String receivedSignature) {
471    if (expectedSignature == null || receivedSignature == null) {
472      return false;
473    }
474    byte[] expectedBytes = expectedSignature.getBytes(StandardCharsets.UTF_8);
475    byte[] receivedBytes = receivedSignature.getBytes(StandardCharsets.UTF_8);
476    return MessageDigest.isEqual(expectedBytes, receivedBytes);
477  }
478
479  public static String computeWebhookSignature(
480      String body, Map<String, String> headers, String signatureKey, boolean escapeBody) {
481    if (signatureKey == null) {
482      return null;
483    }
484    if (!"1".equals(headers.get("box-signature-version"))) {
485      return null;
486    }
487    if (!"HmacSHA256".equals(headers.get("box-signature-algorithm"))) {
488      return null;
489    }
490    if (!headers.containsKey("box-delivery-timestamp")) {
491      return null;
492    }
493
494    try {
495      String escapedBody = escapeBody ? escapeUnicode(body) : body;
496      byte[] encodedSignatureKey = signatureKey.getBytes("UTF-8");
497      byte[] encodedBody = escapedBody.getBytes("UTF-8");
498      byte[] encodedTimestamp = headers.get("box-delivery-timestamp").getBytes("UTF-8");
499      Mac mac = Mac.getInstance("HmacSHA256");
500      SecretKeySpec secretKeySpec = new SecretKeySpec(encodedSignatureKey, "HmacSHA256");
501      mac.init(secretKeySpec);
502      mac.update(encodedBody);
503      mac.update(encodedTimestamp);
504      byte[] hmacDigest = mac.doFinal();
505      return Base64.getEncoder().encodeToString(hmacDigest);
506    } catch (Exception e) {
507      return null;
508    }
509  }
510}