Skip to content

Commit f45c7c3

Browse files
authored
Close file streams in Cache to release resources and prevent crashes; Refactor exists(); Other minor cleanup (#130)
1 parent 4ac8b1d commit f45c7c3

File tree

4 files changed

+118
-80
lines changed

4 files changed

+118
-80
lines changed

datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileCache.java

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import android.support.annotation.NonNull;
2020
import android.support.annotation.Nullable;
21+
import android.support.annotation.VisibleForTesting;
2122

2223
import com.optimizely.ab.android.shared.Cache;
2324

@@ -26,51 +27,51 @@
2627
import org.slf4j.Logger;
2728

2829
/**
29-
* Abstracts the actual data "file" {@link java.io.File}
30+
* Abstracts the actual data "file" {@link java.io.File}.
3031
*/
3132
public class DatafileCache {
3233

33-
private static final String OPTLY_DATA_FILE_NAME = "optly-data-file-%s.json";
34+
private static final String FILENAME = "optly-data-file-%s.json";
3435

3536
@NonNull private final Cache cache;
36-
@NonNull private final String projectId;
37+
@NonNull private final String filename;
3738
@NonNull private final Logger logger;
3839

3940
public DatafileCache(@NonNull String projectId, @NonNull Cache cache, @NonNull Logger logger) {
4041
this.cache = cache;
41-
this.projectId = projectId;
42+
this.filename = String.format(FILENAME, projectId);
4243
this.logger = logger;
4344
}
4445

46+
public boolean delete() {
47+
return cache.delete(filename);
48+
}
49+
50+
public boolean exists() {
51+
return cache.exists(filename);
52+
}
53+
54+
@VisibleForTesting
55+
public String getFileName() {
56+
return filename;
57+
}
58+
4559
@Nullable
4660
public JSONObject load() {
47-
String optlyDatafile = cache.load(getFileName());
61+
String datafile = cache.load(filename);
4862

49-
if (optlyDatafile == null) {
63+
if (datafile == null) {
5064
return null;
5165
}
5266
try {
53-
return new JSONObject(optlyDatafile);
67+
return new JSONObject(datafile);
5468
} catch (JSONException e) {
5569
logger.error("Unable to parse data file", e);
5670
return null;
5771
}
58-
59-
}
60-
61-
public boolean delete() {
62-
return cache.delete(getFileName());
63-
}
64-
65-
public boolean exists() {
66-
return cache.exists(getFileName());
6772
}
6873

6974
public boolean save(String dataFile) {
70-
return cache.save(getFileName(), dataFile);
71-
}
72-
73-
public String getFileName() {
74-
return String.format(OPTLY_DATA_FILE_NAME, projectId);
75+
return cache.save(filename, dataFile);
7576
}
7677
}

datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileLoader.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@
3333
*/
3434
public class DatafileLoader {
3535

36+
@NonNull private final DatafileCache datafileCache;
37+
@NonNull private final DatafileClient datafileClient;
3638
@NonNull private final DatafileService datafileService;
3739
@NonNull private final Executor executor;
3840
@NonNull private final Logger logger;
39-
@NonNull private final DatafileCache datafileCache;
40-
@NonNull private final DatafileClient datafileClient;
4141

4242
private boolean hasNotifiedListener = false;
4343

@@ -54,9 +54,7 @@ public DatafileLoader(@NonNull DatafileService datafileService,
5454

5555
new DatafileServiceConnection("projectId", datafileService.getApplicationContext(), new DatafileLoadedListener() {
5656
public void onDatafileLoaded(@Nullable String dataFile) {}
57-
5857
public void onStop(Context context) {}
59-
6058
});
6159
}
6260

shared/src/androidTest/java/com/optimizely/ab/android/shared/CacheTest.java

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2016, Optimizely, Inc. and contributors *
2+
* Copyright 2016-2017, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -23,16 +23,19 @@
2323
import org.junit.Before;
2424
import org.junit.Test;
2525
import org.junit.runner.RunWith;
26+
import org.mockito.Mockito;
2627
import org.slf4j.Logger;
2728

2829
import java.io.FileNotFoundException;
30+
import java.io.FileOutputStream;
2931
import java.io.IOException;
3032

3133
import static junit.framework.Assert.assertEquals;
3234
import static junit.framework.Assert.assertFalse;
3335
import static junit.framework.Assert.assertNull;
3436
import static junit.framework.Assert.assertTrue;
3537
import static org.mockito.Mockito.mock;
38+
import static org.mockito.Mockito.verify;
3639
import static org.mockito.Mockito.when;
3740

3841
/**
@@ -41,45 +44,56 @@
4144
@RunWith(AndroidJUnit4.class)
4245
public class CacheTest {
4346

44-
private static final String FILE_NAME = "foo.txt";
47+
private static final String FILENAME = "foo.txt";
4548

4649
private Cache cache;
50+
private Logger logger;
4751

4852
@Before
4953
public void setup() {
5054
Context context = InstrumentationRegistry.getTargetContext();
51-
Logger logger = mock(Logger.class);
55+
logger = mock(Logger.class);
5256
cache = new Cache(context, logger);
5357
}
5458

5559
@Test
56-
public void saveLoadAndDelete() throws IOException {
57-
assertTrue(cache.save(FILE_NAME, "bar"));
58-
String data = cache.load(FILE_NAME);
60+
public void testSaveExistsLoadAndDelete() throws IOException {
61+
assertTrue(cache.save(FILENAME, "bar"));
62+
assertTrue(cache.exists(FILENAME));
63+
String data = cache.load(FILENAME);
5964
assertEquals("bar", data);
60-
assertTrue(cache.delete(FILE_NAME));
65+
assertTrue(cache.delete(FILENAME));
6166
}
6267

6368
@Test
64-
public void deleteFileFail() {
65-
assertFalse(cache.delete(FILE_NAME));
69+
public void testDeleteFail() {
70+
assertFalse(cache.delete(FILENAME));
71+
}
72+
73+
@Test
74+
public void testExistsFalse() {
75+
assertFalse(cache.exists(FILENAME));
6676
}
6777

6878
@Test
6979
public void testLoadFileNotFoundExceptionReturnsNull() throws FileNotFoundException {
7080
Context context = mock(Context.class);
71-
Logger logger = mock(Logger.class);
7281
Cache cache = new Cache(context, logger);
73-
when(context.openFileInput(FILE_NAME)).thenThrow(new FileNotFoundException());
74-
assertNull(cache.load(FILE_NAME));
82+
when(context.openFileInput(FILENAME)).thenThrow(new FileNotFoundException());
83+
assertNull(cache.load(FILENAME));
84+
verify(logger).warn("Unable to load file {}.", FILENAME);
7585
}
7686

7787
@Test
78-
public void testSaveFileNotFoundExceptionReturnsFalse() throws FileNotFoundException{
88+
public void testSaveFail() throws IOException {
7989
Context context = mock(Context.class);
80-
Logger logger = mock(Logger.class);
8190
Cache cache = new Cache(context, logger);
82-
when(context.openFileInput(FILE_NAME)).thenThrow(new FileNotFoundException());
83-
assertFalse(cache.save(FILE_NAME, "{}"));
91+
FileOutputStream fileOutputStream = mock(FileOutputStream.class);
92+
93+
String data = "{}";
94+
Mockito.doThrow(new IOException()).when(fileOutputStream).write(data.getBytes());
95+
when(context.openFileOutput(FILENAME, Context.MODE_PRIVATE)).thenReturn(fileOutputStream);
96+
assertFalse(cache.save(FILENAME, data));
97+
verify(logger).error("Error saving file {}.", FILENAME);
8498
}
8599
}
Lines changed: 64 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2016, Optimizely, Inc. and contributors *
2+
* Copyright 2016-2017, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -25,10 +25,13 @@
2525
import java.io.BufferedReader;
2626
import java.io.FileInputStream;
2727
import java.io.FileOutputStream;
28+
import java.io.IOException;
2829
import java.io.InputStreamReader;
30+
import java.util.Arrays;
2931

3032
/**
31-
* Functionality common to all caches
33+
* Functionality common to all caches.
34+
*
3235
* @hide
3336
*/
3437
public class Cache {
@@ -37,7 +40,7 @@ public class Cache {
3740
@NonNull private final Logger logger;
3841

3942
/**
40-
* Create new instances of {@link Cache}
43+
* Create new instance of {@link Cache}.
4144
*
4245
* @param context any {@link Context instance}
4346
* @param logger a {@link Logger} instances
@@ -49,17 +52,44 @@ public Cache(@NonNull Context context, @NonNull Logger logger) {
4952
}
5053

5154
/**
52-
* Load the cache file
55+
* Delete the cache file.
5356
*
54-
* @param fileName the path to the file
57+
* @param filename the path to the file
58+
* @return true if the file was deleted or false otherwise
59+
* @hide
60+
*/
61+
public boolean delete(String filename) {
62+
return context.deleteFile(filename);
63+
}
64+
65+
/**
66+
* Check if the cache file exists.
67+
*
68+
* @param filename the path to the file
69+
* @return true if the file exists or false otherwise
70+
* @hide
71+
*/
72+
public boolean exists(String filename) {
73+
String[] files = context.fileList();
74+
if (files == null) {
75+
return false;
76+
}
77+
return Arrays.asList(files).contains(filename);
78+
}
79+
80+
/**
81+
* Load data from the cache file.
82+
*
83+
* @param filename the path to the file
5584
* @return the loaded cache file as String or null if the file cannot be loaded
5685
* @hide
5786
*/
5887
@Nullable
59-
public String load(String fileName) {
88+
public String load(String filename) {
89+
FileInputStream fileInputStream = null;
6090
try {
61-
FileInputStream fis = context.openFileInput(fileName);
62-
InputStreamReader inputStreamReader = new InputStreamReader(fis);
91+
fileInputStream = context.openFileInput(filename);
92+
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
6393
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
6494
StringBuilder sb = new StringBuilder();
6595
String line;
@@ -68,49 +98,44 @@ public String load(String fileName) {
6898
}
6999
return sb.toString();
70100
} catch (Exception e) {
71-
logger.warn("Unable to load file {}.", fileName);
101+
logger.warn("Unable to load file {}.", filename);
72102
return null;
103+
} finally {
104+
try {
105+
if (fileInputStream != null) {
106+
fileInputStream.close();
107+
}
108+
} catch (IOException e) {
109+
logger.warn("Unable to close file {}.", filename);
110+
}
73111
}
74112
}
75113

76114
/**
77-
* Delete the cache file
78-
* @param fileName the path to the file
79-
* @return true if the file was deleted or false otherwise
80-
* @hide
81-
*/
82-
public boolean delete(String fileName) {
83-
return context.deleteFile(fileName);
84-
}
85-
86-
/**
87-
* Check if the cache file exists
88-
* @param fileName the path to the file
89-
* @return true if the file exists or false otherwise
90-
* @hide
91-
*/
92-
public boolean exists(String fileName) {
93-
String file = load(fileName);
94-
return file != null;
95-
}
96-
97-
/**
98-
* Save the existing cache file with new data
115+
* Save data to the cache file and overwrite any existing data.
99116
*
100-
* The original file will be overwritten.
101-
* @param fileName the path to the file
102-
* @param data the String data to write to the file
117+
* @param filename the path to the file
118+
* @param data the String data to write to the file
103119
* @return true if the file was saved
104120
* @hide
105121
*/
106-
public boolean save(String fileName, String data) {
122+
public boolean save(String filename, String data) {
123+
FileOutputStream fileOutputStream = null;
107124
try {
108-
FileOutputStream fos = context.openFileOutput(fileName, Context.MODE_PRIVATE);
109-
fos.write(data.getBytes());
125+
fileOutputStream = context.openFileOutput(filename, Context.MODE_PRIVATE);
126+
fileOutputStream.write(data.getBytes());
110127
return true;
111128
} catch (Exception e) {
112-
logger.error("Error saving file {}.", fileName);
129+
logger.error("Error saving file {}.", filename);
130+
return false;
131+
} finally {
132+
if (fileOutputStream != null) {
133+
try {
134+
fileOutputStream.close();
135+
} catch (IOException e) {
136+
logger.warn("Unable to close file {}.", filename);
137+
}
138+
}
113139
}
114-
return false;
115140
}
116141
}

0 commit comments

Comments
 (0)