1414import static java .util .Objects .requireNonNull ;
1515
1616import java .io .Closeable ;
17+ import java .io .EOFException ;
18+ import java .io .FileNotFoundException ;
1719import java .io .IOException ;
1820import java .io .InputStream ;
1921import java .io .UncheckedIOException ;
22+ import java .lang .ref .SoftReference ;
23+ import java .nio .file .Files ;
2024import java .nio .file .Path ;
2125import java .util .HashMap ;
2226import java .util .Map ;
27+ import java .util .Objects ;
28+ import java .util .Optional ;
29+ import java .util .concurrent .atomic .AtomicReference ;
2330import java .util .stream .Stream ;
2431import java .util .zip .ZipEntry ;
2532import java .util .zip .ZipException ;
2633import java .util .zip .ZipFile ;
34+ import java .util .zip .ZipInputStream ;
2735
2836import org .eclipse .rcptt .sherlock .core .model .sherlock .report .Report ;
2937import org .eclipse .rcptt .sherlock .core .streams .SherlockReportFormat ;
3038
31- /** A collection of reports from one execution session grouped in a ZIP file **/
39+ /** A collection of reports from one execution session grouped in a ZIP file
40+ *
41+ * Lazy loading is implemented to prevent OOM on large collections.
42+ * Supports reading of incomplete archive when execution is still in progress or was aborted.
43+ *
44+ * **/
3245public final class IndexedExecutionReport implements Closeable {
33- private final ZipFile zipFile ;
46+ private final Path zipPath ;
3447 private final Map <String , Handle > idIndex = synchronizedMap (new HashMap <>());
3548 private final Map <String , Handle > entryIndex = synchronizedMap (new HashMap <>());
49+ private final AtomicReference <ZipFile > zipFile = new AtomicReference <>(null );
50+
3651 public final class Handle {
3752 private final ZipEntry zipEntry ;
3853 private ReportEntry reportEntry ;
54+ private SoftReference <Report > cachedReport = new SoftReference <Report >(null );
3955 private Handle (ZipEntry entry ) {
4056 this .zipEntry = requireNonNull (entry );
4157 }
4258 public String getId () throws IOException {
4359 return getEntry ().id ;
4460 }
4561 public Report getReport () throws IOException {
46- try (InputStream is = zipFile .getInputStream (zipEntry )) {
47- Report report = SherlockReportFormat .loadReport (is , true , true );
48- reportEntry = ReportEntry .create (report );
49- idIndex .put (reportEntry .id , this );
50- return report ;
62+ var result = this .cachedReport .get ();
63+ if (result != null ) {
64+ return result ;
65+ }
66+ try (var is = readEntry ()) {
67+ result = populate (is );
68+ assert reportEntry != null ;
69+ return result ;
70+ }
71+ }
72+ private Report populate (InputStream is ) throws IOException {
73+ Report report = cachedReport .get ();
74+ if (report != null ) {
75+ return report ;
5176 }
77+ report = SherlockReportFormat .loadReport (is , false , true );
78+ this .cachedReport = new SoftReference <Report >(report );
79+ reportEntry = ReportEntry .create (report );
80+ idIndex .put (reportEntry .id , this );
81+ return report ;
82+ }
83+ private InputStream readEntry () throws IOException {
84+ InputStream result ;
85+ try {
86+ result = openZipFile ().map (f -> {
87+ try {
88+ return f .getInputStream (zipEntry );
89+ } catch (IOException e ) {
90+ throw new UncheckedIOException (e );
91+ }
92+ }).orElseGet (() -> {
93+ try {
94+ String name = zipEntry .getName ();
95+ ZipInputStream zipInputStream = new ZipInputStream (Files .newInputStream (zipPath ));
96+ try {
97+ for (var entry = zipInputStream .getNextEntry (); entry != null ; entry = zipInputStream .getNextEntry ()) {
98+ if (entry .getName ().equals (name )) {
99+ return zipInputStream ;
100+ }
101+ }
102+ zipInputStream .close ();
103+ throw new FileNotFoundException (zipPath + ":" + name );
104+ } catch (Throwable e ) {
105+ zipInputStream .close ();
106+ throw e ;
107+ }
108+ } catch (IOException e ) {
109+ throw new UncheckedIOException (e );
110+ }
111+ });
112+ } catch (UncheckedIOException e ) {
113+ throw e .getCause ();
114+ }
115+ return result ;
52116 }
117+
53118 public ReportEntry getEntry () throws IOException {
54119 if (reportEntry == null ) {
55120 getReport ();
56121 }
57122 assert reportEntry != null ;
58123 return reportEntry ;
59124 }
125+
126+ @ Override
127+ public String toString () {
128+ return "" + zipEntry + (reportEntry == null ? "" : ", " + reportEntry .id );
129+ }
60130 }
61131 public IndexedExecutionReport (Path path ) throws ZipException , IOException {
62- zipFile = new ZipFile ( path . toFile ()) ;
132+ zipPath = path ;
63133 }
134+
64135 public Stream <Handle > read () {
65- if (zipFile .size () == entryIndex .size ()) {
66- return entryIndex .values ().stream ();
136+ try {
137+ Stream <Handle > resultStream = openZipFile ().map (this ::streamZipFile ).orElseGet (this ::streamZipInputStream );
138+ return resultStream ;
139+ } catch (IOException e ) {
140+ throw new UncheckedIOException (e );
141+ }
142+ }
143+
144+ private Stream <Handle > streamZipFile (ZipFile file ) {
145+ return file .stream ().map (entry -> {
146+ var result = entryIndex .computeIfAbsent (entry .getName (), (ignored ) -> new Handle (entry ));
147+ if (result .reportEntry == null ) {
148+ try (var is = file .getInputStream (entry )) {
149+ result .populate (is );
150+ } catch (IOException e ) {
151+ throw new UncheckedIOException (e );
152+ }
153+ }
154+ return result ;
155+ });
156+ }
157+ private Stream <Handle > streamZipInputStream () {
158+ ZipInputStream zipInputStream ;
159+ try {
160+ zipInputStream = new ZipInputStream (Files .newInputStream (zipPath ));
161+ } catch (IOException e ) {
162+ throw new UncheckedIOException (e );
67163 }
68- return zipFile .stream ().map (e -> entryIndex .computeIfAbsent (e .getName (), (ignored ) -> new Handle (e )) );
164+ Stream <Handle > resultStream = Stream .generate (() -> {
165+ try {
166+ synchronized (zipInputStream ) {
167+ ZipEntry entry = zipInputStream .getNextEntry ();
168+ if (entry == null ) {
169+ return null ;
170+ }
171+ var result = entryIndex .computeIfAbsent (entry .getName (), (ignored ) -> new Handle (entry ));
172+ if (result .reportEntry == null ) {
173+ result .populate (zipInputStream );
174+ }
175+ return result ;
176+ }
177+ } catch (EOFException e ) {
178+ // Support unfinished reports from still active or aborted executions
179+ return null ;
180+ } catch (IOException e ) {
181+ throw new UncheckedIOException (e );
182+ }
183+ });
184+ resultStream .onClose (() -> {
185+ try {
186+ zipInputStream .close ();
187+ } catch (IOException e ) {
188+ throw new UncheckedIOException (e );
189+ }
190+ });
191+ resultStream = resultStream .takeWhile (Objects ::nonNull );
192+ return resultStream ;
69193 }
70194 @ Override
71195 public void close () throws IOException {
72196 idIndex .clear ();
73- zipFile .close ();
197+ entryIndex .clear ();
198+ try (var close = zipFile .get ()) {
199+ // closes if not null
200+ }
74201 }
75202 public Handle getById (String id ) {
76203 Handle result = idIndex .get (id );
@@ -87,4 +214,22 @@ public Handle getById(String id) {
87214 }).findAny ().get ();
88215 }
89216 }
217+
218+ @ SuppressWarnings ("resource" )
219+ private Optional <ZipFile > openZipFile () throws IOException {
220+ try {
221+ ZipFile f = zipFile .get ();
222+ if (f == null ) {
223+ f = new ZipFile (zipPath .toFile ());
224+ if (!zipFile .compareAndSet (null , f )) {
225+ f .close ();
226+ f = zipFile .get ();
227+ assert f != null ;
228+ }
229+ }
230+ return Optional .of (f );
231+ } catch (ZipException e ) {
232+ return Optional .empty ();
233+ }
234+ }
90235}
0 commit comments