1 | /*- | |
2 | * #%L | |
3 | * io.earcam.utilitarian.io | |
4 | * %% | |
5 | * Copyright (C) 2017 earcam | |
6 | * %% | |
7 | * SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT) | |
8 | * | |
9 | * You <b>must</b> choose to accept, in full - any individual or combination of | |
10 | * the following licenses: | |
11 | * <ul> | |
12 | * <li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li> | |
13 | * <li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li> | |
14 | * <li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li> | |
15 | * <li><a href="https://opensource.org/licenses/MIT">MIT</a></li> | |
16 | * </ul> | |
17 | * #L% | |
18 | */ | |
19 | package io.earcam.utilitarian.io; | |
20 | ||
21 | import java.io.ByteArrayOutputStream; | |
22 | import java.io.IOException; | |
23 | import java.io.OutputStream; | |
24 | import java.nio.BufferOverflowException; | |
25 | import java.nio.BufferUnderflowException; | |
26 | import java.util.function.Supplier; | |
27 | ||
28 | import javax.annotation.concurrent.NotThreadSafe; | |
29 | ||
30 | /** | |
31 | * <p> | |
32 | * Deals with structured (e.g. XML) or unstructured data. | |
33 | * </p> | |
34 | * | |
35 | * <p> | |
36 | * User code must invoke {@link #beginRecord()} before writing {@code byte}s, and subsequently delimit safe | |
37 | * splitting points by invoking {@link #endRecord()}. The number of {@code byte}s written between the | |
38 | * call to {@link #beginRecord()} and call to {@link #endRecord()} must not exceed | |
39 | * {@link #maxSize(long)} - ({@link #header}{@code .length} + {@link #footer}{@code .length}) | |
40 | * </p> | |
41 | * | |
42 | * <p> | |
43 | * A <i>record</i> is defined as any {@code byte}s written between calls to {@link #beginRecord()} and | |
44 | * {@link #endRecord()}. Should the maximum file size be specified and the length of a single record (plus | |
45 | * header and footer) exceed the maximum then a {@link BufferOverflowException} is throw. | |
46 | * | |
47 | * <p> | |
48 | * Common usage would be splitting files, in this case the {@link Supplier} is expected to <i>keep | |
49 | * track</i> of output file names. | |
50 | * </p> | |
51 | * | |
52 | * <p> | |
53 | * <b>Please note limitation</b>; due to the use of {@link Long} internally, the maximum | |
54 | * size per-file is limited to {@value java.lang.Long#MAX_VALUE} bytes (which is | |
55 | * 9,223PB or 9,223,000,000GB) per split {@link OutputStream} . | |
56 | * | |
57 | */ | |
58 | @SuppressWarnings("squid:S4349") // Sonar: Not applicable IMO | |
59 | @NotThreadSafe | |
60 | public class SplittableOutputStream extends OutputStream implements SplittableOutputStreamBuilder, SplittableOutputStreamBuilder.SplitOutputStreamBuilder { | |
61 | ||
62 | private final Supplier<OutputStream> supplier; | |
63 | private final byte[] header; | |
64 | private final byte[] footer; | |
65 | private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); | |
66 | ||
67 | private OutputStream out = null; | |
68 | private long maxFileSize = Long.MAX_VALUE; | |
69 | private long maxRecordCount = Long.MAX_VALUE; | |
70 | private long bytesCount; | |
71 | private long recordsCount; | |
72 | ||
73 | private boolean inScope; | |
74 | ||
75 | ||
76 | private SplittableOutputStream(Supplier<OutputStream> supplier, byte[] head, byte[] footer) | |
77 | { | |
78 | this.supplier = supplier; | |
79 | this.header = head; | |
80 | this.footer = footer; | |
81 | } | |
82 | ||
83 | ||
84 | /** | |
85 | * Begin building a {@link SplittableOutputStream} | |
86 | * | |
87 | * @param next a {@link Supplier} of the underlying {@link OutputStream}s | |
88 | * @param header written at the start of each {@link OutputStream} (e.g. file header) | |
89 | * @param footer written at the end of each {@link OutputStream} (e.g. file footer) | |
90 | * @return the builder for further construction | |
91 | * @throws IOException rethrows in the unlikely event the underlying ByteArrayOutputStream buffer does | |
92 | */ | |
93 | public static SplittableOutputStreamBuilder splittable(Supplier<OutputStream> next, byte[] head, byte[] footer) throws IOException | |
94 | { | |
95 | @SuppressWarnings("squid:S2095") // false positive - it's being returned | |
96 | SplittableOutputStream splittable = new SplittableOutputStream(next, head, footer); | |
97 |
1
1. splittable : removed call to io/earcam/utilitarian/io/SplittableOutputStream::reset → KILLED |
splittable.reset(); |
98 |
1
1. splittable : mutated return of Object value for io/earcam/utilitarian/io/SplittableOutputStream::splittable to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return splittable; |
99 | } | |
100 | ||
101 | ||
102 | private void reset() throws IOException | |
103 | { | |
104 | bytesCount = recordsCount = 0L; | |
105 | byte[] bytes = buffer.toByteArray(); | |
106 |
1
1. reset : removed call to java/io/ByteArrayOutputStream::reset → KILLED |
buffer.reset(); |
107 |
1
1. reset : removed call to java/io/ByteArrayOutputStream::write → KILLED |
buffer.write(header); |
108 |
1
1. reset : removed call to java/io/ByteArrayOutputStream::write → KILLED |
buffer.write(bytes); |
109 | } | |
110 | ||
111 | ||
112 | @Override | |
113 | public SplitOutputStreamBuilder maxSize(long bytes) | |
114 | { | |
115 | maxFileSize = bytes; | |
116 |
1
1. maxSize : removed call to io/earcam/utilitarian/io/SplittableOutputStream::checkSanity → KILLED |
checkSanity(header, footer, maxFileSize); |
117 |
1
1. maxSize : mutated return of Object value for io/earcam/utilitarian/io/SplittableOutputStream::maxSize to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return this; |
118 | } | |
119 | ||
120 | ||
121 | @Override | |
122 | public SplitOutputStreamBuilder maxCount(long numberOfRecords) | |
123 | { | |
124 |
1
1. maxCount : removed call to io/earcam/utilitarian/io/SplittableOutputStream::requireNaturalNumber → KILLED |
requireNaturalNumber(numberOfRecords); |
125 | maxRecordCount = numberOfRecords; | |
126 |
1
1. maxCount : mutated return of Object value for io/earcam/utilitarian/io/SplittableOutputStream::maxCount to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return this; |
127 | } | |
128 | ||
129 | ||
130 | private void requireNaturalNumber(long number) | |
131 | { | |
132 |
2
1. requireNaturalNumber : changed conditional boundary → KILLED 2. requireNaturalNumber : negated conditional → KILLED |
if(number <= 0) { |
133 | throw new IllegalArgumentException("A positive, non-zero value is required. Received: " + number); | |
134 | } | |
135 | } | |
136 | ||
137 | ||
138 | @Override | |
139 | public SplittableOutputStream outputStream() throws IOException | |
140 | { | |
141 |
1
1. outputStream : mutated return of Object value for io/earcam/utilitarian/io/SplittableOutputStream::outputStream to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return this; |
142 | } | |
143 | ||
144 | ||
145 | private static void checkSanity(byte[] head, byte[] foot, long maxFileSize) | |
146 | { | |
147 |
3
1. checkSanity : changed conditional boundary → SURVIVED 2. checkSanity : Replaced integer addition with subtraction → KILLED 3. checkSanity : negated conditional → KILLED |
if(head.length + foot.length > maxFileSize) { |
148 | throw new IllegalArgumentException("header.length + footer.length > maxFileSize: " + head.length + " + " + foot.length + " > " + maxFileSize); | |
149 | } | |
150 | } | |
151 | ||
152 | ||
153 | @Override | |
154 | public void write(int b) throws IOException | |
155 | { | |
156 |
1
1. write : removed call to io/earcam/utilitarian/io/SplittableOutputStream::checkBeforeWrite → KILLED |
checkBeforeWrite(1); |
157 |
1
1. write : removed call to java/io/ByteArrayOutputStream::write → KILLED |
buffer.write(b); |
158 | } | |
159 | ||
160 | ||
161 | private void checkBeforeWrite(int pendingBytes) | |
162 | { | |
163 |
1
1. checkBeforeWrite : negated conditional → KILLED |
if(!inScope) { |
164 | throw new IllegalStateException("Record scope not started"); | |
165 | } | |
166 |
4
1. checkBeforeWrite : Replaced integer addition with subtraction → SURVIVED 2. checkBeforeWrite : Replaced integer addition with subtraction → SURVIVED 3. checkBeforeWrite : changed conditional boundary → KILLED 4. checkBeforeWrite : negated conditional → KILLED |
if(pendingBytes + header.length + footer.length > maxFileSize) { |
167 | throw new BufferOverflowException(); | |
168 | } | |
169 | } | |
170 | ||
171 | ||
172 | @Override | |
173 | public void write(byte[] bytes) throws IOException | |
174 | { | |
175 |
1
1. write : removed call to io/earcam/utilitarian/io/SplittableOutputStream::checkBeforeWrite → KILLED |
checkBeforeWrite(bytes.length); |
176 |
1
1. write : removed call to java/io/ByteArrayOutputStream::write → KILLED |
buffer.write(bytes); |
177 | } | |
178 | ||
179 | ||
180 | /** | |
181 | * Called to mark the beginning of a <i>record</i> (where a "record" is any block | |
182 | * of bytes that can only be treated atomically; in that it's valid to split content | |
183 | * at the record's boundaries. | |
184 | * | |
185 | * @see #endRecord() | |
186 | */ | |
187 | public void beginRecord() | |
188 | { | |
189 |
1
1. beginRecord : negated conditional → KILLED |
if(inScope) { |
190 | throw new IllegalStateException("Record scope already started"); | |
191 | } | |
192 | inScope = true; | |
193 | } | |
194 | ||
195 | ||
196 | /** | |
197 | * Called to mark the end of a <i>record</i> | |
198 | * | |
199 | * @throws IOException rethrows anything from the underlying {@link OutputStream} | |
200 | * | |
201 | * @see #beginRecord() | |
202 | */ | |
203 | public void endRecord() throws IOException | |
204 | { | |
205 |
1
1. endRecord : removed call to io/earcam/utilitarian/io/SplittableOutputStream::endScope → KILLED |
endScope(); |
206 |
1
1. endRecord : Replaced long addition with subtraction → KILLED |
++recordsCount; |
207 |
2
1. endRecord : negated conditional → KILLED 2. endRecord : negated conditional → KILLED |
if(bufferIsTooLarge() || maxRecordsExceeded()) { |
208 |
1
1. endRecord : removed call to io/earcam/utilitarian/io/SplittableOutputStream::endSplit → KILLED |
endSplit(); |
209 | } | |
210 |
1
1. endRecord : negated conditional → KILLED |
if(recorded()) { |
211 |
1
1. endRecord : removed call to io/earcam/utilitarian/io/SplittableOutputStream::writeBuffer → KILLED |
writeBuffer(); |
212 | } | |
213 | } | |
214 | ||
215 | ||
216 | private void endScope() | |
217 | { | |
218 |
1
1. endScope : negated conditional → KILLED |
if(!inScope) { |
219 | throw new IllegalStateException("Record scope not started, cannot end"); | |
220 | } | |
221 | inScope = false; | |
222 | } | |
223 | ||
224 | ||
225 | private boolean bufferIsTooLarge() | |
226 | { | |
227 |
5
1. bufferIsTooLarge : changed conditional boundary → KILLED 2. bufferIsTooLarge : Replaced long addition with subtraction → KILLED 3. bufferIsTooLarge : Replaced long addition with subtraction → KILLED 4. bufferIsTooLarge : negated conditional → KILLED 5. bufferIsTooLarge : replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED |
return bytesCount + footer.length + buffer.size() > maxFileSize; |
228 | } | |
229 | ||
230 | ||
231 | private boolean maxRecordsExceeded() | |
232 | { | |
233 |
3
1. maxRecordsExceeded : changed conditional boundary → SURVIVED 2. maxRecordsExceeded : negated conditional → KILLED 3. maxRecordsExceeded : replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED |
return recordsCount > maxRecordCount; |
234 | } | |
235 | ||
236 | ||
237 | private void endSplit() throws IOException | |
238 | { | |
239 |
1
1. endSplit : negated conditional → KILLED |
if(out != null) { |
240 |
1
1. endSplit : removed call to java/io/OutputStream::write → KILLED |
out.write(footer); |
241 |
1
1. endSplit : removed call to java/io/OutputStream::close → SURVIVED |
out.close(); |
242 | out = null; | |
243 |
1
1. endSplit : removed call to io/earcam/utilitarian/io/SplittableOutputStream::reset → KILLED |
reset(); |
244 | } | |
245 | } | |
246 | ||
247 | ||
248 | private boolean recorded() | |
249 | { | |
250 |
5
1. recorded : changed conditional boundary → KILLED 2. recorded : negated conditional → KILLED 3. recorded : negated conditional → KILLED 4. recorded : negated conditional → KILLED 5. recorded : replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED |
return (recordsCount == 1 && buffer.size() > header.length) |
251 |
2
1. recorded : changed conditional boundary → KILLED 2. recorded : negated conditional → KILLED |
|| (recordsCount != 1 && buffer.size() > 0); |
252 | } | |
253 | ||
254 | ||
255 | private void writeBuffer() throws IOException | |
256 | { | |
257 |
1
1. writeBuffer : removed call to java/io/OutputStream::write → KILLED |
out().write(buffer.toByteArray()); |
258 |
1
1. writeBuffer : Replaced long addition with subtraction → KILLED |
bytesCount += buffer.size(); |
259 |
1
1. writeBuffer : removed call to java/io/ByteArrayOutputStream::reset → KILLED |
buffer.reset(); |
260 | } | |
261 | ||
262 | ||
263 | private OutputStream out() | |
264 | { | |
265 |
1
1. out : negated conditional → KILLED |
if(out == null) { |
266 | out = supplier.get(); | |
267 | } | |
268 |
1
1. out : mutated return of Object value for io/earcam/utilitarian/io/SplittableOutputStream::out to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return out; |
269 | } | |
270 | ||
271 | ||
272 | @Override | |
273 | public void close() throws IOException | |
274 | { | |
275 |
1
1. close : negated conditional → KILLED |
if(recorded()) { |
276 | throw new BufferUnderflowException(); | |
277 | } | |
278 |
1
1. close : removed call to io/earcam/utilitarian/io/SplittableOutputStream::endSplit → KILLED |
endSplit(); |
279 |
2
1. close : changed conditional boundary → KILLED 2. close : negated conditional → KILLED |
if(buffer.size() > header.length) { |
280 |
1
1. close : removed call to io/earcam/utilitarian/io/SplittableOutputStream::writeBuffer → KILLED |
writeBuffer(); |
281 |
1
1. close : removed call to io/earcam/utilitarian/io/SplittableOutputStream::endSplit → SURVIVED |
endSplit(); |
282 | } | |
283 | } | |
284 | } | |
Mutations | ||
97 |
1.1 |
|
98 |
1.1 |
|
106 |
1.1 |
|
107 |
1.1 |
|
108 |
1.1 |
|
116 |
1.1 |
|
117 |
1.1 |
|
124 |
1.1 |
|
126 |
1.1 |
|
132 |
1.1 2.2 |
|
141 |
1.1 |
|
147 |
1.1 2.2 3.3 |
|
156 |
1.1 |
|
157 |
1.1 |
|
163 |
1.1 |
|
166 |
1.1 2.2 3.3 4.4 |
|
175 |
1.1 |
|
176 |
1.1 |
|
189 |
1.1 |
|
205 |
1.1 |
|
206 |
1.1 |
|
207 |
1.1 2.2 |
|
208 |
1.1 |
|
210 |
1.1 |
|
211 |
1.1 |
|
218 |
1.1 |
|
227 |
1.1 2.2 3.3 4.4 5.5 |
|
233 |
1.1 2.2 3.3 |
|
239 |
1.1 |
|
240 |
1.1 |
|
241 |
1.1 |
|
243 |
1.1 |
|
250 |
1.1 2.2 3.3 4.4 5.5 |
|
251 |
1.1 2.2 |
|
257 |
1.1 |
|
258 |
1.1 |
|
259 |
1.1 |
|
265 |
1.1 |
|
268 |
1.1 |
|
275 |
1.1 |
|
278 |
1.1 |
|
279 |
1.1 2.2 |
|
280 |
1.1 |
|
281 |
1.1 |