1 | package com.jsql.view.swing.dialog.translate; | |
2 | ||
3 | import java.io.*; | |
4 | import java.nio.charset.StandardCharsets; | |
5 | import java.util.*; | |
6 | ||
7 | /** | |
8 | * This class provides an alternative to the JDK's {@link Properties} class. It fixes the design flaw of using | |
9 | * inheritance over composition, while keeping up the same APIs as the original class. Keys and values are | |
10 | * guaranteed to be of type {@link String}. | |
11 | * <p/> | |
12 | * This class is not synchronized, contrary to the original implementation. | |
13 | * <p/> | |
14 | * As additional functionality, this class keeps its properties in a well-defined order. By default, the order | |
15 | * is the one in which the individual properties have been added, either through explicit API calls or through | |
16 | * reading them top-to-bottom from a properties file. | |
17 | * <p/> | |
18 | * Also, an optional flag can be set to omit the comment that contains the current date when storing the | |
19 | * properties to a properties file. | |
20 | * <p/> | |
21 | * Currently, this class does not support the concept of default properties, contrary to the original implementation. | |
22 | * <p/> | |
23 | * <strong>Note that this implementation is not synchronized.</strong> If multiple threads access ordered | |
24 | * properties concurrently, and at least one of the threads modifies the ordered properties structurally, it | |
25 | * <em>must</em> be synchronized externally. This is typically accomplished by synchronizing on some object | |
26 | * that naturally encapsulates the properties. | |
27 | * <p/> | |
28 | * Note that the actual (and quite complex) logic of parsing and storing properties from and to a stream | |
29 | * is delegated to the {@link Properties} class from the JDK. | |
30 | * | |
31 | * @see Properties | |
32 | */ | |
33 | public final class OrderedProperties { | |
34 | ||
35 | private Map<String, String> properties; | |
36 | private boolean suppressDate; | |
37 | ||
38 | /** | |
39 | * Creates a new instance that will keep the properties in the order they have been added. Other than | |
40 | * the ordering of the keys, this instance behaves like an instance of the {@link Properties} class. | |
41 | */ | |
42 | public OrderedProperties() { | |
43 | this(new LinkedHashMap<>(), false); | |
44 | } | |
45 | ||
46 | private OrderedProperties(Map<String, String> properties, boolean suppressDate) { | |
47 | | |
48 | this.properties = properties; | |
49 | this.suppressDate = suppressDate; | |
50 | } | |
51 | ||
52 | /** | |
53 | * See {@link Properties#getProperty(String)}. | |
54 | */ | |
55 | public String getProperty(String key) { | |
56 |
1
1. getProperty : replaced return value with "" for com/jsql/view/swing/dialog/translate/OrderedProperties::getProperty → NO_COVERAGE |
return this.properties.get(key); |
57 | } | |
58 | ||
59 | /** | |
60 | * See {@link Properties#getProperty(String, String)}. | |
61 | */ | |
62 | public String getProperty(String key, String defaultValue) { | |
63 | ||
64 | String value = this.properties.get(key); | |
65 |
2
1. getProperty : replaced return value with "" for com/jsql/view/swing/dialog/translate/OrderedProperties::getProperty → NO_COVERAGE 2. getProperty : negated conditional → NO_COVERAGE |
return value == null ? defaultValue : value; |
66 | } | |
67 | ||
68 | /** | |
69 | * See {@link Properties#setProperty(String, String)}. | |
70 | */ | |
71 | public String setProperty(String key, String value) { | |
72 |
1
1. setProperty : replaced return value with "" for com/jsql/view/swing/dialog/translate/OrderedProperties::setProperty → NO_COVERAGE |
return this.properties.put(key, value); |
73 | } | |
74 | ||
75 | /** | |
76 | * Removes the property with the specified key, if it is present. Returns | |
77 | * the value of the property, or <tt>null</tt> if there was no property with | |
78 | * the specified key. | |
79 | * | |
80 | * @param key the key of the property to remove | |
81 | * @return the previous value of the property, or <tt>null</tt> if there was no property with the specified key | |
82 | */ | |
83 | public String removeProperty(String key) { | |
84 |
1
1. removeProperty : replaced return value with "" for com/jsql/view/swing/dialog/translate/OrderedProperties::removeProperty → NO_COVERAGE |
return this.properties.remove(key); |
85 | } | |
86 | ||
87 | /** | |
88 | * Returns <tt>true</tt> if there is a property with the specified key. | |
89 | * | |
90 | * @param key the key whose presence is to be tested | |
91 | */ | |
92 | public boolean containsProperty(String key) { | |
93 |
2
1. containsProperty : replaced boolean return with true for com/jsql/view/swing/dialog/translate/OrderedProperties::containsProperty → NO_COVERAGE 2. containsProperty : replaced boolean return with false for com/jsql/view/swing/dialog/translate/OrderedProperties::containsProperty → NO_COVERAGE |
return this.properties.containsKey(key); |
94 | } | |
95 | ||
96 | /** | |
97 | * See {@link Properties#size()}. | |
98 | */ | |
99 | public int size() { | |
100 |
1
1. size : replaced int return with 0 for com/jsql/view/swing/dialog/translate/OrderedProperties::size → NO_COVERAGE |
return this.properties.size(); |
101 | } | |
102 | ||
103 | /** | |
104 | * See {@link Properties#isEmpty()}. | |
105 | */ | |
106 | public boolean isEmpty() { | |
107 |
2
1. isEmpty : replaced boolean return with false for com/jsql/view/swing/dialog/translate/OrderedProperties::isEmpty → NO_COVERAGE 2. isEmpty : replaced boolean return with true for com/jsql/view/swing/dialog/translate/OrderedProperties::isEmpty → NO_COVERAGE |
return this.properties.isEmpty(); |
108 | } | |
109 | ||
110 | /** | |
111 | * See {@link Properties#propertyNames()}. | |
112 | */ | |
113 | public Enumeration<String> propertyNames() { | |
114 |
1
1. propertyNames : replaced return value with null for com/jsql/view/swing/dialog/translate/OrderedProperties::propertyNames → NO_COVERAGE |
return new Vector<>(this.properties.keySet()).elements(); |
115 | } | |
116 | ||
117 | /** | |
118 | * See {@link Properties#stringPropertyNames()}. | |
119 | */ | |
120 | public Set<String> stringPropertyNames() { | |
121 |
1
1. stringPropertyNames : replaced return value with Collections.emptySet for com/jsql/view/swing/dialog/translate/OrderedProperties::stringPropertyNames → NO_COVERAGE |
return new LinkedHashSet<>(this.properties.keySet()); |
122 | } | |
123 | ||
124 | /** | |
125 | * See {@link Properties#entrySet()}. | |
126 | */ | |
127 | public Set<Map.Entry<String, String>> entrySet() { | |
128 |
1
1. entrySet : replaced return value with Collections.emptySet for com/jsql/view/swing/dialog/translate/OrderedProperties::entrySet → NO_COVERAGE |
return new LinkedHashSet<>(this.properties.entrySet()); |
129 | } | |
130 | ||
131 | /** | |
132 | * See {@link Properties#load(InputStream)}. | |
133 | */ | |
134 | public void load(InputStream stream) throws IOException { | |
135 | | |
136 | var customProperties = new CustomProperties(this.properties); | |
137 |
1
1. load : removed call to com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::load → NO_COVERAGE |
customProperties.load(stream); |
138 | } | |
139 | ||
140 | /** | |
141 | * See {@link Properties#load(Reader)}. | |
142 | */ | |
143 | public void load(Reader reader) throws IOException { | |
144 | | |
145 | var customProperties = new CustomProperties(this.properties); | |
146 |
1
1. load : removed call to com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::load → NO_COVERAGE |
customProperties.load(reader); |
147 | } | |
148 | ||
149 | /** | |
150 | * See {@link Properties#loadFromXML(InputStream)}. | |
151 | */ | |
152 | public void loadFromXML(InputStream stream) throws IOException { | |
153 | | |
154 | var customProperties = new CustomProperties(this.properties); | |
155 |
1
1. loadFromXML : removed call to com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::loadFromXML → NO_COVERAGE |
customProperties.loadFromXML(stream); |
156 | } | |
157 | ||
158 | /** | |
159 | * See {@link Properties#store(OutputStream, String)}. | |
160 | */ | |
161 | public void store(OutputStream stream, String comments) throws IOException { | |
162 | | |
163 | var customProperties = new CustomProperties(this.properties); | |
164 | | |
165 |
1
1. store : negated conditional → NO_COVERAGE |
if (this.suppressDate) { |
166 |
1
1. store : removed call to com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::store → NO_COVERAGE |
customProperties.store(new DateSuppressingPropertiesBufferedWriter(new OutputStreamWriter(stream, StandardCharsets.ISO_8859_1)), comments); |
167 | } else { | |
168 |
1
1. store : removed call to com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::store → NO_COVERAGE |
customProperties.store(stream, comments); |
169 | } | |
170 | } | |
171 | ||
172 | /** | |
173 | * See {@link Properties#store(Writer, String)}. | |
174 | */ | |
175 | public void store(Writer writer, String comments) throws IOException { | |
176 | | |
177 | var customProperties = new CustomProperties(this.properties); | |
178 | | |
179 |
1
1. store : negated conditional → NO_COVERAGE |
if (this.suppressDate) { |
180 |
1
1. store : removed call to com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::store → NO_COVERAGE |
customProperties.store(new DateSuppressingPropertiesBufferedWriter(writer), comments); |
181 | } else { | |
182 |
1
1. store : removed call to com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::store → NO_COVERAGE |
customProperties.store(writer, comments); |
183 | } | |
184 | } | |
185 | ||
186 | /** | |
187 | * See {@link Properties#storeToXML(OutputStream, String)}. | |
188 | */ | |
189 | public void storeToXML(OutputStream stream, String comment) throws IOException { | |
190 | | |
191 | var customProperties = new CustomProperties(this.properties); | |
192 |
1
1. storeToXML : removed call to com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::storeToXML → NO_COVERAGE |
customProperties.storeToXML(stream, comment); |
193 | } | |
194 | ||
195 | /** | |
196 | * See {@link Properties#storeToXML(OutputStream, String, String)}. | |
197 | */ | |
198 | public void storeToXML(OutputStream stream, String comment, String encoding) throws IOException { | |
199 | | |
200 | var customProperties = new CustomProperties(this.properties); | |
201 |
1
1. storeToXML : removed call to com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::storeToXML → NO_COVERAGE |
customProperties.storeToXML(stream, comment, encoding); |
202 | } | |
203 | ||
204 | /** | |
205 | * See {@link Properties#list(PrintStream)}. | |
206 | */ | |
207 | public void list(PrintStream stream) { | |
208 | | |
209 | var customProperties = new CustomProperties(this.properties); | |
210 |
1
1. list : removed call to com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::list → NO_COVERAGE |
customProperties.list(stream); |
211 | } | |
212 | ||
213 | /** | |
214 | * See {@link Properties#list(PrintWriter)}. | |
215 | */ | |
216 | public void list(PrintWriter writer) { | |
217 | | |
218 | var customProperties = new CustomProperties(this.properties); | |
219 |
1
1. list : removed call to com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::list → NO_COVERAGE |
customProperties.list(writer); |
220 | } | |
221 | ||
222 | /** | |
223 | * Convert this instance to a {@link Properties} instance. | |
224 | * | |
225 | * @return the {@link Properties} instance | |
226 | */ | |
227 | public Properties toJdkProperties() { | |
228 | | |
229 | var jdkProperties = new Properties(); | |
230 | | |
231 | for (Map.Entry<String, String> entry: this.entrySet()) { | |
232 | jdkProperties.put(entry.getKey(), entry.getValue()); | |
233 | } | |
234 | | |
235 |
1
1. toJdkProperties : replaced return value with null for com/jsql/view/swing/dialog/translate/OrderedProperties::toJdkProperties → NO_COVERAGE |
return jdkProperties; |
236 | } | |
237 | ||
238 | @Override | |
239 | public boolean equals(Object other) { | |
240 | | |
241 |
1
1. equals : negated conditional → NO_COVERAGE |
if (this == other) { |
242 |
1
1. equals : replaced boolean return with false for com/jsql/view/swing/dialog/translate/OrderedProperties::equals → NO_COVERAGE |
return true; |
243 | } | |
244 | ||
245 |
1
1. equals : negated conditional → NO_COVERAGE |
if ( |
246 | other == null | |
247 |
1
1. equals : negated conditional → NO_COVERAGE |
|| this.getClass() != other.getClass() |
248 | ) { | |
249 |
1
1. equals : replaced boolean return with true for com/jsql/view/swing/dialog/translate/OrderedProperties::equals → NO_COVERAGE |
return false; |
250 | } | |
251 | ||
252 | OrderedProperties that = (OrderedProperties) other; | |
253 | | |
254 |
2
1. equals : replaced boolean return with true for com/jsql/view/swing/dialog/translate/OrderedProperties::equals → NO_COVERAGE 2. equals : replaced boolean return with false for com/jsql/view/swing/dialog/translate/OrderedProperties::equals → NO_COVERAGE |
return Arrays.equals(this.properties.entrySet().toArray(), that.properties.entrySet().toArray()); |
255 | } | |
256 | ||
257 | @Override | |
258 | public int hashCode() { | |
259 |
1
1. hashCode : replaced int return with 0 for com/jsql/view/swing/dialog/translate/OrderedProperties::hashCode → NO_COVERAGE |
return Arrays.hashCode(this.properties.entrySet().toArray()); |
260 | } | |
261 | ||
262 | @SuppressWarnings("unchecked") | |
263 | private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { | |
264 | | |
265 |
1
1. readObject : removed call to java/io/ObjectInputStream::defaultReadObject → NO_COVERAGE |
stream.defaultReadObject(); |
266 | this.properties = (Map<String, String>) stream.readObject(); | |
267 | this.suppressDate = stream.readBoolean(); | |
268 | } | |
269 | ||
270 | /** | |
271 | * See {@link Properties#toString()}. | |
272 | */ | |
273 | @Override | |
274 | public String toString() { | |
275 |
1
1. toString : replaced return value with "" for com/jsql/view/swing/dialog/translate/OrderedProperties::toString → NO_COVERAGE |
return this.properties.toString(); |
276 | } | |
277 | ||
278 | /** | |
279 | * Creates a new instance that will have both the same property entries and | |
280 | * the same behavior as the given source. | |
281 | * <p/> | |
282 | * Note that the source instance and the copy instance will share the same | |
283 | * comparator instance if a custom ordering had been configured on the source. | |
284 | * | |
285 | * @param source the source to copy from | |
286 | * @return the copy | |
287 | */ | |
288 | public static OrderedProperties copyOf(OrderedProperties source) { | |
289 | | |
290 | // create a copy that has the same behaviour | |
291 | var builder = new OrderedPropertiesBuilder(); | |
292 | builder.withSuppressDateInComment(source.suppressDate); | |
293 | | |
294 |
1
1. copyOf : negated conditional → NO_COVERAGE |
if (source.properties instanceof TreeMap) { |
295 | builder.withOrdering(((TreeMap<String, String>) source.properties).comparator()); | |
296 | } | |
297 | | |
298 | OrderedProperties result = builder.build(); | |
299 | ||
300 | // copy the properties from the source to the target | |
301 | for (Map.Entry<String, String> entry: source.entrySet()) { | |
302 | result.setProperty(entry.getKey(), entry.getValue()); | |
303 | } | |
304 | | |
305 |
1
1. copyOf : replaced return value with null for com/jsql/view/swing/dialog/translate/OrderedProperties::copyOf → NO_COVERAGE |
return result; |
306 | } | |
307 | ||
308 | /** | |
309 | * Builder for {@link OrderedProperties} instances. | |
310 | */ | |
311 | public static final class OrderedPropertiesBuilder { | |
312 | ||
313 | private Comparator<? super String> comparator; | |
314 | private boolean suppressDate; | |
315 | ||
316 | /** | |
317 | * Use a custom ordering of the keys. | |
318 | * | |
319 | * @param comparator the ordering to apply on the keys | |
320 | * @return the builder | |
321 | */ | |
322 | public OrderedPropertiesBuilder withOrdering(Comparator<? super String> comparator) { | |
323 | | |
324 | this.comparator = comparator; | |
325 |
1
1. withOrdering : replaced return value with null for com/jsql/view/swing/dialog/translate/OrderedProperties$OrderedPropertiesBuilder::withOrdering → NO_COVERAGE |
return this; |
326 | } | |
327 | ||
328 | /** | |
329 | * Suppress the comment that contains the current date when storing the properties. | |
330 | * | |
331 | * @param suppressDate whether to suppress the comment that contains the current date | |
332 | * @return the builder | |
333 | */ | |
334 | public OrderedPropertiesBuilder withSuppressDateInComment(boolean suppressDate) { | |
335 | | |
336 | this.suppressDate = suppressDate; | |
337 |
1
1. withSuppressDateInComment : replaced return value with null for com/jsql/view/swing/dialog/translate/OrderedProperties$OrderedPropertiesBuilder::withSuppressDateInComment → NO_COVERAGE |
return this; |
338 | } | |
339 | ||
340 | /** | |
341 | * Builds a new {@link OrderedProperties} instance. | |
342 | * | |
343 | * @return the new instance | |
344 | */ | |
345 | public OrderedProperties build() { | |
346 |
1
1. build : negated conditional → NO_COVERAGE |
Map<String, String> properties = this.comparator != null |
347 | ? new TreeMap<>(this.comparator) | |
348 | : new LinkedHashMap<>(); | |
349 | | |
350 |
1
1. build : replaced return value with null for com/jsql/view/swing/dialog/translate/OrderedProperties$OrderedPropertiesBuilder::build → NO_COVERAGE |
return new OrderedProperties(properties, this.suppressDate); |
351 | } | |
352 | } | |
353 | ||
354 | /** | |
355 | * Custom {@link Properties} that delegates reading, writing, and enumerating properties to the | |
356 | * backing {@link OrderedProperties} instance's properties. | |
357 | */ | |
358 | private static final class CustomProperties extends Properties { | |
359 | ||
360 | private final Map<String, String> targetProperties; | |
361 | ||
362 | private CustomProperties(Map<String, String> targetProperties) { | |
363 | this.targetProperties = targetProperties; | |
364 | } | |
365 | ||
366 | @Override | |
367 | public synchronized Object get(Object key) { | |
368 |
1
1. get : replaced return value with null for com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::get → NO_COVERAGE |
return this.targetProperties.get(key); |
369 | } | |
370 | ||
371 | @Override | |
372 | public synchronized Object put(Object key, Object value) { | |
373 |
1
1. put : replaced return value with null for com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::put → NO_COVERAGE |
return this.targetProperties.put((String) key, (String) value); |
374 | } | |
375 | ||
376 | @Override | |
377 | public String getProperty(String key) { | |
378 |
1
1. getProperty : replaced return value with "" for com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::getProperty → NO_COVERAGE |
return this.targetProperties.get(key); |
379 | } | |
380 | ||
381 | @Override | |
382 | public synchronized Enumeration<Object> keys() { | |
383 |
1
1. keys : replaced return value with null for com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::keys → NO_COVERAGE |
return new Vector<Object>(this.targetProperties.keySet()).elements(); |
384 | } | |
385 | ||
386 | @Override | |
387 | public Set<Object> keySet() { | |
388 |
1
1. keySet : replaced return value with Collections.emptySet for com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::keySet → NO_COVERAGE |
return new LinkedHashSet<>(this.targetProperties.keySet()); |
389 | } | |
390 | | |
391 | @Override | |
392 | public synchronized boolean equals(Object o) { | |
393 |
3
1. equals : negated conditional → NO_COVERAGE 2. equals : replaced boolean return with true for com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::equals → NO_COVERAGE 3. equals : negated conditional → NO_COVERAGE |
return super.equals(o) && o instanceof OrderedProperties; |
394 | } | |
395 | | |
396 | @Override | |
397 | public synchronized int hashCode() { | |
398 |
1
1. hashCode : replaced int return with 0 for com/jsql/view/swing/dialog/translate/OrderedProperties$CustomProperties::hashCode → NO_COVERAGE |
return super.hashCode(); |
399 | } | |
400 | } | |
401 | ||
402 | /** | |
403 | * Custom {@link BufferedWriter} for storing properties that will write all leading lines of comments except | |
404 | * the last comment line. Using the JDK Properties class to store properties, the last comment | |
405 | * line always contains the current date which is what we want to filter out. | |
406 | */ | |
407 | private static final class DateSuppressingPropertiesBufferedWriter extends BufferedWriter { | |
408 | ||
409 | private StringBuilder currentComment; | |
410 | private String previousComment; | |
411 | ||
412 | private DateSuppressingPropertiesBufferedWriter(Writer out) { | |
413 | super(out); | |
414 | } | |
415 | ||
416 | @Override | |
417 | public void write(String string) throws IOException { | |
418 |
1
1. write : negated conditional → NO_COVERAGE |
if (this.currentComment != null) { |
419 | | |
420 | this.currentComment.append(string); | |
421 | | |
422 |
1
1. write : negated conditional → NO_COVERAGE |
if (string.endsWith(System.lineSeparator())) { |
423 | | |
424 |
1
1. write : negated conditional → NO_COVERAGE |
if (this.previousComment != null) { |
425 |
1
1. write : removed call to java/io/BufferedWriter::write → NO_COVERAGE |
super.write(this.previousComment); |
426 | } | |
427 | ||
428 | this.previousComment = this.currentComment.toString(); | |
429 | this.currentComment = null; | |
430 | } | |
431 |
1
1. write : negated conditional → NO_COVERAGE |
} else if (string.startsWith("#")) { |
432 | this.currentComment = new StringBuilder(string); | |
433 | } else { | |
434 |
1
1. write : removed call to java/io/BufferedWriter::write → NO_COVERAGE |
super.write(string); |
435 | } | |
436 | } | |
437 | } | |
438 | } | |
439 | ||
Mutations | ||
56 |
1.1 |
|
65 |
1.1 2.2 |
|
72 |
1.1 |
|
84 |
1.1 |
|
93 |
1.1 2.2 |
|
100 |
1.1 |
|
107 |
1.1 2.2 |
|
114 |
1.1 |
|
121 |
1.1 |
|
128 |
1.1 |
|
137 |
1.1 |
|
146 |
1.1 |
|
155 |
1.1 |
|
165 |
1.1 |
|
166 |
1.1 |
|
168 |
1.1 |
|
179 |
1.1 |
|
180 |
1.1 |
|
182 |
1.1 |
|
192 |
1.1 |
|
201 |
1.1 |
|
210 |
1.1 |
|
219 |
1.1 |
|
235 |
1.1 |
|
241 |
1.1 |
|
242 |
1.1 |
|
245 |
1.1 |
|
247 |
1.1 |
|
249 |
1.1 |
|
254 |
1.1 2.2 |
|
259 |
1.1 |
|
265 |
1.1 |
|
275 |
1.1 |
|
294 |
1.1 |
|
305 |
1.1 |
|
325 |
1.1 |
|
337 |
1.1 |
|
346 |
1.1 |
|
350 |
1.1 |
|
368 |
1.1 |
|
373 |
1.1 |
|
378 |
1.1 |
|
383 |
1.1 |
|
388 |
1.1 |
|
393 |
1.1 2.2 3.3 |
|
398 |
1.1 |
|
418 |
1.1 |
|
422 |
1.1 |
|
424 |
1.1 |
|
425 |
1.1 |
|
431 |
1.1 |
|
434 |
1.1 |