• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Jsondb / jsondb-core / d56bcd4d-0316-447f-9307-7241c8c90305

24 Jun 2026 08:55PM UTC coverage: 79.616% (+1.0%) from 78.597%
d56bcd4d-0316-447f-9307-7241c8c90305

push

circleci

FarooqKhan
Increase unit test coverage from ~75% to ~78% line coverage.

Add 43 meaningful tests across crypto, IO, metadata, schema updates, and
event listener lifecycle. Fix a null-pointer in Util.stampVersion() when
writing to a non-file target fails before streams are opened.

506 of 608 branches covered (83.22%)

9 of 15 new or added lines in 1 file covered. (60.0%)

1535 of 1928 relevant lines covered (79.62%)

0.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.42
/src/main/java/io/jsondb/Util.java
1
/*
2
 * Copyright (c) 2016 Farooq Khan
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining a copy
5
 * of this software and associated documentation files (the "Software"), to
6
 * deal in the Software without restriction, including without limitation the
7
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8
 * sell copies of the Software, and to permit persons to whom the Software is
9
 * furnished to do so, subject to the following conditions:
10
 *
11
 * The above copyright notice and this permission notice shall be included in
12
 * all copies or substantial portions of the Software.
13
 *
14
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
17
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
 */
21
package io.jsondb;
22

23
import java.beans.XMLDecoder;
24
import java.beans.XMLEncoder;
25
import java.io.BufferedWriter;
26
import java.io.ByteArrayInputStream;
27
import java.io.ByteArrayOutputStream;
28
import java.io.File;
29
import java.io.FileOutputStream;
30
import java.io.IOException;
31
import java.io.OutputStreamWriter;
32
import java.lang.reflect.InvocationTargetException;
33
import java.lang.reflect.Method;
34
import java.text.ParseException;
35
import java.util.*;
36

37
import com.fasterxml.jackson.databind.ObjectMapper;
38
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
39
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
40
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
41
import org.slf4j.Logger;
42
import org.slf4j.LoggerFactory;
43

44
import com.fasterxml.jackson.core.JsonProcessingException;
45

46
import io.jsondb.annotation.Document;
47

48
/**
49
 * @author Farooq Khan
50
 * @version 1.0 25-Sep-2016
51
 */
52
public class Util {
×
53
  private static Logger logger = LoggerFactory.getLogger(Util.class);
1✔
54

55
  private static final Collection<String> RESTRICTED_CLASSES;
56
  static {
57

58
    Set<String> restrictedClasses = new HashSet<String>();
1✔
59
    restrictedClasses.add(List.class.getName());
1✔
60
    restrictedClasses.add(Collection.class.getName());
1✔
61
    restrictedClasses.add(Iterator.class.getName());
1✔
62
    restrictedClasses.add(HashSet.class.getName());
1✔
63

64
    RESTRICTED_CLASSES = Collections.unmodifiableCollection(restrictedClasses);
1✔
65
  }
66
  private static ObjectMapper objectMapper = new ObjectMapper()
1✔
67
          .registerModule(new ParameterNamesModule())
1✔
68
          .registerModule(new Jdk8Module())
1✔
69
          .registerModule(new JavaTimeModule());
1✔
70
  
71
  protected static void ensureNotRestricted(Object o) {
72
    if (o.getClass().isArray() || RESTRICTED_CLASSES.contains(o.getClass().getName())) {
1!
73
      throw new InvalidJsonDbApiUsageException("Collection object cannot be inserted, removed, updated or upserted as a single object");
1✔
74
    }
75
  }
1✔
76
  
77
  protected static <T> String determineEntityCollectionName(T obj) {
78
    return determineCollectionName(obj.getClass());
1✔
79
  }
80

81
  /**
82
   * A utility method to determine the collection name for a given entity class.
83
   * This method attempts to find a the annotation {@link io.jsondb.annotation.Document} on this class.
84
   * If found then we know the collection name else it throws a exception
85
   * @param entityClass  class that determines the name of the collection
86
   * @return  name of the class 
87
   */
88
  protected static String determineCollectionName(Class<?> entityClass) {
89
    if (entityClass == null) {
1✔
90
        throw new InvalidJsonDbApiUsageException(
1✔
91
              "No class parameter provided, entity collection can't be determined");
92
    }
93
    Document doc = entityClass.getAnnotation(Document.class);
1✔
94
    if (null == doc) {
1✔
95
      throw new InvalidJsonDbApiUsageException(
1✔
96
          "Entity '" + entityClass.getSimpleName() + "' is not annotated with annotation @Document");
1✔
97
    }
98
    String collectionName = doc.collection();
1✔
99

100
    return collectionName;
1✔
101
  }
102
  
103
  /**
104
   * A utility method to extract the value of field marked by the @Id annotation using its
105
   * getter/accessor method.
106
   * @param document the actual Object representing the POJO we want the Id of.
107
   * @param getterMethodForId the Method that is the accessor for the attributed with @Id annotation
108
   * @return the actual Id or if none exists then a new random UUID
109
   */
110
  protected static Object getIdForEntity(Object document, Method getterMethodForId) {
111
    Object id = null;
1✔
112
    if (null != getterMethodForId) {
1✔
113
      try {
114
        id = getterMethodForId.invoke(document);
1✔
115
      } catch (IllegalAccessException e) {
1✔
116
        logger.error("Failed to invoke getter method for a idAnnotated field due to permissions", e);
1✔
117
        throw new InvalidJsonDbApiUsageException("Failed to invoke getter method for a idAnnotated field due to permissions", e);
1✔
118
      } catch (IllegalArgumentException e) {
×
119
        logger.error("Failed to invoke getter method for a idAnnotated field due to wrong arguments", e);
×
120
        throw new InvalidJsonDbApiUsageException("Failed to invoke getter method for a idAnnotated field due to wrong arguments", e);
×
121
      } catch (InvocationTargetException e) {
1✔
122
        logger.error("Failed to invoke getter method for a idAnnotated field, the method threw a exception", e);
1✔
123
        throw new InvalidJsonDbApiUsageException("Failed to invoke getter method for a idAnnotated field, the method threw a exception", e);
1✔
124
      }
1✔
125
    }
126
    return id;
1✔
127
  }
128

129
  /**
130
   * A utility method to set the value of field marked by the @Id annotation using its
131
   * setter/mutator method.
132
   * TODO: Some day we want to support policies for generation of ID like AutoIncrement etc.
133
   *
134
   * @param document the actual Object representing the POJO we want the Id to be set for.
135
   * @param setterMethodForId the Method that is the mutator for the attributed with @Id annotation
136
   * @return the Id that was generated and set
137
   */
138
  protected static Object setIdForEntity(Object document, Method setterMethodForId) {
139
    Object id = UUID.randomUUID().toString();
1✔
140
    if (null != setterMethodForId) {
1!
141
      try {
142
        id = setterMethodForId.invoke(document, id);
1✔
143
      } catch (IllegalAccessException e) {
1✔
144
        logger.error("Failed to invoke setter method for a idAnnotated field due to permissions", e);
1✔
145
        throw new InvalidJsonDbApiUsageException("Failed to invoke setter method for a idAnnotated field due to permissions", e);
1✔
146
      } catch (IllegalArgumentException e) {
×
147
        logger.error("Failed to invoke setter method for a idAnnotated field due to wrong arguments", e);
×
148
        throw new InvalidJsonDbApiUsageException("Failed to invoke setter method for a idAnnotated field due to wrong arguments", e);
×
149
      } catch (InvocationTargetException e) {
×
150
        logger.error("Failed to invoke setter method for a idAnnotated field, the method threw a exception", e);
×
151
        throw new InvalidJsonDbApiUsageException("Failed to invoke setter method for a idAnnotated field, the method threw a exception", e);
×
152
      }
1✔
153
    }
154
    return id;
1✔
155
  }
156

157
  protected static Object setFieldValueForEntity(Object document, Object newValue, Method setterMethod) {
158
    Object retval = null;
1✔
159
    if (null != setterMethod) {
1!
160
      try {
161
        retval = setterMethod.invoke(document, newValue);
1✔
162
      } catch (IllegalAccessException e) {
×
163
        logger.error("Failed to invoke method due to permissions", e);
×
164
      } catch (IllegalArgumentException e) {
×
165
        logger.error("Failed to invoke method due to wrong arguments", e);
×
166
      } catch (InvocationTargetException e) {
×
167
        logger.error("Failed to invoke method, the method threw a exception", e);
×
168
      }
1✔
169
    }
170
    return retval;
1✔
171
  }
172

173
  /**
174
   * A utility method that creates a deep clone of the specified object.
175
   * There is no other way of doing this reliably.
176
   *
177
   * @param fromBean java bean to be cloned.
178
   * @return a new java bean cloned from fromBean.
179
   */
180
  protected static Object deepCopy(Object fromBean) {
181
    try {
182
      if (fromBean != null) {
1✔
183
        return objectMapper.readValue(objectMapper.writeValueAsString(fromBean), fromBean.getClass());
1✔
184
      }
185
      return null;
1✔
186
    } catch (IOException e) {
×
187
      throw new RuntimeException(e);
×
188
    }
189
  }
190

191
  /**
192
   * Utility to stamp the version into a newly created .json File
193
   * This method is expected to be invoked on a newly created .json file before it is usable.
194
   * So no locking code required.
195
   * 
196
   * @param dbConfig  all the settings used by Json DB
197
   * @param f  the target .json file on which to stamp the version
198
   * @param version  the actual version string to stamp
199
   * @return true if success.
200
   */
201
  public static boolean stampVersion(JsonDBConfig dbConfig, File f, String version) {
202

203
    FileOutputStream fos = null;
1✔
204
    OutputStreamWriter osr = null;
1✔
205
    BufferedWriter writer = null;
1✔
206
    try {
207
      fos = new FileOutputStream(f);
1✔
208
      osr = new OutputStreamWriter(fos, dbConfig.getCharset());
1✔
209
      writer = new BufferedWriter(osr);
1✔
210

211
      String versionData = dbConfig.getObjectMapper().writeValueAsString(new SchemaVersion(version));
1✔
212
      writer.write(versionData);
1✔
213
      writer.newLine();
1✔
214
    } catch (JsonProcessingException e) {
×
215
      logger.error("Failed to serialize SchemaVersion to Json string", e);
×
216
      return false;
×
217
    } catch (IOException e) {
1✔
218
      logger.error("Failed to write SchemaVersion to the new .json file {}", f, e);
1✔
219
      return false;
1✔
220
    } finally {
221
      if (writer != null) {
1✔
222
        try {
223
          writer.close();
1✔
NEW
224
        } catch (IOException e) {
×
NEW
225
          logger.error("Failed to close BufferedWriter for new collection file {}", f, e);
×
226
        }
1✔
227
      }
228
      if (osr != null) {
1✔
229
        try {
230
          osr.close();
1✔
NEW
231
        } catch (IOException e) {
×
NEW
232
          logger.error("Failed to close OutputStreamWriter for new collection file {}", f, e);
×
233
        }
1✔
234
      }
235
      if (fos != null) {
1✔
236
        try {
237
          fos.close();
1✔
NEW
238
        } catch (IOException e) {
×
NEW
239
          logger.error("Failed to close FileOutputStream for new collection file {}", f, e);
×
240
        }
1✔
241
      }
242
    }
243
    return true;
1✔
244
  }
245

246
  /**
247
   * Utility to delete directory recursively
248
   * @param f  File object representing the directory to recursively delete
249
   */
250
  public static void delete(File f) {
251
    if (f.isDirectory()) {
1✔
252
      for (File c : f.listFiles())
1✔
253
        delete(c);
1✔
254
    }
255
    f.delete();
1✔
256
  }
1✔
257

258
  /**
259
   * A utility method to determine the default value to be assigned to i  when it is not specified based on the value of k
260
   * If i is not given it defaults to 0 for k&gt;0 and n-1 for k&lt;0, where n is number elements in slice_target.
261
   *
262
   * @param k actual value of k parsed from slice string
263
   * @param n number of elements in slice_target
264
   * @return default value for i
265
   */
266
  private static int getDefaultIByK(int k, int n) {
267
    if (k > 0) {
1✔
268
      return 0;
1✔
269
    } else if ( k < 0) {
1!
270
      return n-1;
1✔
271
    } else {
272
      throw new IllegalArgumentException("Illegal argument, expected k != 0");
×
273
    }
274
  }
275

276
  /**
277
   * A utility method to determine the default value to be assigned to j when it is not specified based on the value of k
278
   * If j is not given it defaults to n for k&gt;0 and -n-1 for k&lt;0, where n is number elements in slice_target.
279
   *
280
   * @param k actual value of k parsed from slice string
281
   * @param n number of elements in slice_target
282
   * @return default value for j
283
   */
284
  private static int getDefaultJByK(int k, int n) {
285
    if (k > 0) {
1✔
286
      return n;
1✔
287
    } else if ( k < 0) {
1!
288
      return -n-1;
1✔
289
    } else {
290
      throw new IllegalArgumentException("Illegal argument, expected k != 0");
×
291
    }
292
  }
293

294
  /**
295
   * A private utility method used to parse the i|j from part of slice string. If i or j is negative
296
   * it is adjusted to n+i or n+j.
297
   * where n is number elements in slice_target
298
   *
299
   * @param part a non-null non-empty integer part string
300
   * @param n number of elements in slice_target
301
   * @return the parsed integer value, adjusted appropriately if its negative
302
   * @throws IllegalArgumentException a exception of the part string is not a valid number
303
   */
304
  private static int parsePartIJ(String part, int n) throws IllegalArgumentException {
305
    part = part.trim();
1✔
306
    if (part.length() > 0) {
1!
307
      try {
308
        int p = Integer.parseInt(part);
1✔
309
        if (p < 0) {
1✔
310
          p += n;
1✔
311
        }
312
        return p;
1✔
313
      } catch (NumberFormatException e) {
1✔
314
        throw new IllegalArgumentException("Illegal slice argument, expected a integer representing i in i:j:k");
1✔
315
      }
316
    } else {
317
      throw new IllegalArgumentException("Illegal slice argument, expected a integer representing i in i:j:k");
×
318
    }
319
  }
320

321
  /**
322
   * A private utility method used to parse the k from part of slice string.
323
   *
324
   * @param part a non-null integer part string
325
   * @param def value to assign to k if none can be parsed out
326
   * @return the parsed or the default k value
327
   * @throws IllegalArgumentException a exception of the part string is not a valid number
328
   */
329
  private static int parsePartK(String part, int def) throws IllegalArgumentException {
330
    part = part.trim();
1✔
331
    if (part.length() > 0) {
1!
332
      try {
333
        return Integer.parseInt(part);
1✔
334
      } catch (NumberFormatException e) {
×
335
        throw new IllegalArgumentException("Illegal slice argument, expected format is i:j:k");
×
336
      }
337
    }
338
    return def;
×
339
  }
340

341
  public static boolean isSliceable(String slice) {
342
    if (slice == null || slice.length() < 1 || slice.equals(":") || slice.equals("::")) {
1✔
343
      return false;
1✔
344
    }
345
    return true;
1✔
346
  }
347

348
  /**
349
   * Utility method to compute the indexes to select based on slice string
350
   * @param slice select the indices in some slice_target list or array, should be a valid slice string
351
   *
352
   *              The behaviour of this slicing feature is similar to
353
   *              the slicing feature in python or numpy, as much as possible
354
   *              https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.indexing.html
355
   *
356
   *              A slice is a string notation and the basic slice syntax is i:j:k, where i is the starting index,
357
   *              j is the stopping index, and k is the step (k != 0). In other words it is start:stop:step
358
   *              Example slice_target = [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9]
359
   *                      slice = "1:7:2" returns [T1, T3, T5]
360
   *
361
   *              i is inclusive, j is exclusive
362
   *
363
   *              Negative i and j are interpreted as n + i and n + j where n is the number of elements found. Negative
364
   *              k makes stepping go towards smaller indices.
365
   *              Example slice_target = [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9]
366
   *                      slice = "-2:10" returns [T8, T9]
367
   *                      slice = "-3:3:-1" returns [T7, T6, T5, T4]
368
   *
369
   *              Assume n is the number of elements in slice_target. Then, if i is not given it defaults to 0
370
   *              for k&gt;0 and n - 1 for k&lt;0 . If j is not given it defaults to n for k&gt;0 and -n-1 for k&lt;0 .
371
   *              If k is not given it defaults to 1. Note that :: is the same as : and means select all indices
372
   *              from slice_target.
373
   *              Example slice_target = [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9]
374
   *                      slice = "5:" returns [T5, T6, T7, T8, T9]
375
   *
376
   *              Assume n is the number of elements in slice_target. Then, if j&gt;n then it is considered as n, in case
377
   *              of negative j it is considered -n.
378
   * @param size size of the array from which a slice is to extracted
379
   * @return List of indexes to pick from the array to be returned
380
   */
381
  public static List<Integer> getSliceIndexes(String slice, int size) {
382

383
    if (!isSliceable(slice) || size < 1) {
1✔
384
      return null;
1✔
385
    }
386

387
    int i=0,j=0,k = 0;
1✔
388

389
    String[] parts = slice.split(":");
1✔
390
    if (parts.length > 3) {
1✔
391
      throw new IllegalArgumentException("Illegal slice argument, expected format is i:j:k");
1✔
392
    }
393
    if (slice.startsWith("::")) {
1✔
394
      k = parsePartK(parts[2], 1);
1✔
395
      i = getDefaultIByK(k, size);
1✔
396
      j = getDefaultJByK(k, size);
1✔
397
    } else if (slice.startsWith(":")) {
1✔
398
      if (parts.length == 2) {
1✔
399
        j = parsePartIJ(parts[1], size);
1✔
400
        k = 1;
1✔
401
        i = getDefaultIByK(k, size);
1✔
402
      } else if (parts.length == 3) {
1!
403
        j = parsePartIJ(parts[1], size);
1✔
404
        k = parsePartK(parts[2], 1);
1✔
405
        i = getDefaultIByK(k, size);
1✔
406
      }
407
    } else {
408
      if (parts.length == 1) {
1✔
409
        i = parsePartIJ(parts[0], size);
1✔
410
        k = 1;
1✔
411
        j = getDefaultJByK(k, size);
1✔
412
      } else if (parts.length == 2) {
1✔
413
        i = parsePartIJ(parts[0], size);
1✔
414
        j = parsePartIJ(parts[1], size);
1✔
415
        k = 1;
1✔
416
      } else if (parts.length == 3) {
1!
417
        k = parsePartK(parts[2], 1);
1✔
418
        if (parts[0].length() > 0) {
1!
419
          i = parsePartIJ(parts[0], size);
1✔
420
        } else {
421
          i = getDefaultIByK(k, size);
×
422
        }
423
        if (parts[1].length() > 0) {
1✔
424
          j = parsePartIJ(parts[1], size);
1✔
425
        } else {
426
          j = getDefaultJByK(k, size);
1✔
427
        }
428
      }
429
    }
430

431
    List<Integer> indexes = new ArrayList<>();
1✔
432
    if (k == 0) {
1✔
433
      throw new IllegalArgumentException("Illegal slice argument, k cannot be zero");
1✔
434
    } else if (k > 0) {
1✔
435
      int m = i;
1✔
436
      int n = j;
1✔
437
      while (m < n && m < size) { //Second condition prevents ArrayIndexOutOfBounds
1✔
438
        indexes.add(m);
1✔
439
        m += k;
1✔
440
      }
441
    } else if (k < 0) {
1!
442
      int m = i;
1✔
443
      int n = j;
1✔
444
      while (m > n && m > -1) { //Second condition prevents ArrayIndexOutOfBounds
1✔
445
        indexes.add(m);
1✔
446
        m += k;
1✔
447
      }
448
    }
449
    return indexes;
1✔
450
  }
451
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc