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