OrderedProperties.java
package com.jsql.view.swing.dialog.translate;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* This class provides an alternative to the JDK's {@link Properties} class. It fixes the design flaw of using
* inheritance over composition, while keeping up the same APIs as the original class. Keys and values are
* guaranteed to be of type {@link String}.
* <p/>
* This class is not synchronized, contrary to the original implementation.
* <p/>
* As additional functionality, this class keeps its properties in a well-defined order. By default, the order
* is the one in which the individual properties have been added, either through explicit API calls or through
* reading them top-to-bottom from a properties file.
* <p/>
* Also, an optional flag can be set to omit the comment that contains the current date when storing the
* properties to a properties file.
* <p/>
* Currently, this class does not support the concept of default properties, contrary to the original implementation.
* <p/>
* <strong>Note that this implementation is not synchronized.</strong> If multiple threads access ordered
* properties concurrently, and at least one of the threads modifies the ordered properties structurally, it
* <em>must</em> be synchronized externally. This is typically accomplished by synchronizing on some object
* that naturally encapsulates the properties.
* <p/>
* Note that the actual (and quite complex) logic of parsing and storing properties from and to a stream
* is delegated to the {@link Properties} class from the JDK.
*
* @see Properties
*/
public final class OrderedProperties {
private Map<String, String> properties;
private boolean suppressDate;
/**
* Creates a new instance that will keep the properties in the order they have been added. Other than
* the ordering of the keys, this instance behaves like an instance of the {@link Properties} class.
*/
public OrderedProperties() {
this(new LinkedHashMap<>(), false);
}
private OrderedProperties(Map<String, String> properties, boolean suppressDate) {
this.properties = properties;
this.suppressDate = suppressDate;
}
/**
* See {@link Properties#getProperty(String)}.
*/
public String getProperty(String key) {
return this.properties.get(key);
}
/**
* See {@link Properties#getProperty(String, String)}.
*/
public String getProperty(String key, String defaultValue) {
String value = this.properties.get(key);
return value == null ? defaultValue : value;
}
/**
* See {@link Properties#setProperty(String, String)}.
*/
public String setProperty(String key, String value) {
return this.properties.put(key, value);
}
/**
* Removes the property with the specified key, if it is present. Returns
* the value of the property, or <tt>null</tt> if there was no property with
* the specified key.
*
* @param key the key of the property to remove
* @return the previous value of the property, or <tt>null</tt> if there was no property with the specified key
*/
public String removeProperty(String key) {
return this.properties.remove(key);
}
/**
* Returns <tt>true</tt> if there is a property with the specified key.
*
* @param key the key whose presence is to be tested
*/
public boolean containsProperty(String key) {
return this.properties.containsKey(key);
}
/**
* See {@link Properties#size()}.
*/
public int size() {
return this.properties.size();
}
/**
* See {@link Properties#isEmpty()}.
*/
public boolean isEmpty() {
return this.properties.isEmpty();
}
/**
* See {@link Properties#propertyNames()}.
*/
public Enumeration<String> propertyNames() {
return new Vector<>(this.properties.keySet()).elements();
}
/**
* See {@link Properties#stringPropertyNames()}.
*/
public Set<String> stringPropertyNames() {
return new LinkedHashSet<>(this.properties.keySet());
}
/**
* See {@link Properties#entrySet()}.
*/
public Set<Map.Entry<String, String>> entrySet() {
return new LinkedHashSet<>(this.properties.entrySet());
}
/**
* See {@link Properties#load(InputStream)}.
*/
public void load(InputStream stream) throws IOException {
var customProperties = new CustomProperties(this.properties);
customProperties.load(stream);
}
/**
* See {@link Properties#load(Reader)}.
*/
public void load(Reader reader) throws IOException {
var customProperties = new CustomProperties(this.properties);
customProperties.load(reader);
}
/**
* See {@link Properties#loadFromXML(InputStream)}.
*/
public void loadFromXML(InputStream stream) throws IOException {
var customProperties = new CustomProperties(this.properties);
customProperties.loadFromXML(stream);
}
/**
* See {@link Properties#store(OutputStream, String)}.
*/
public void store(OutputStream stream, String comments) throws IOException {
var customProperties = new CustomProperties(this.properties);
if (this.suppressDate) {
customProperties.store(new DateSuppressingPropertiesBufferedWriter(new OutputStreamWriter(stream, StandardCharsets.ISO_8859_1)), comments);
} else {
customProperties.store(stream, comments);
}
}
/**
* See {@link Properties#store(Writer, String)}.
*/
public void store(Writer writer, String comments) throws IOException {
var customProperties = new CustomProperties(this.properties);
if (this.suppressDate) {
customProperties.store(new DateSuppressingPropertiesBufferedWriter(writer), comments);
} else {
customProperties.store(writer, comments);
}
}
/**
* See {@link Properties#storeToXML(OutputStream, String)}.
*/
public void storeToXML(OutputStream stream, String comment) throws IOException {
var customProperties = new CustomProperties(this.properties);
customProperties.storeToXML(stream, comment);
}
/**
* See {@link Properties#storeToXML(OutputStream, String, String)}.
*/
public void storeToXML(OutputStream stream, String comment, String encoding) throws IOException {
var customProperties = new CustomProperties(this.properties);
customProperties.storeToXML(stream, comment, encoding);
}
/**
* See {@link Properties#list(PrintStream)}.
*/
public void list(PrintStream stream) {
var customProperties = new CustomProperties(this.properties);
customProperties.list(stream);
}
/**
* See {@link Properties#list(PrintWriter)}.
*/
public void list(PrintWriter writer) {
var customProperties = new CustomProperties(this.properties);
customProperties.list(writer);
}
/**
* Convert this instance to a {@link Properties} instance.
*
* @return the {@link Properties} instance
*/
public Properties toJdkProperties() {
var jdkProperties = new Properties();
for (Map.Entry<String, String> entry: this.entrySet()) {
jdkProperties.put(entry.getKey(), entry.getValue());
}
return jdkProperties;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (
other == null
|| this.getClass() != other.getClass()
) {
return false;
}
OrderedProperties that = (OrderedProperties) other;
return Arrays.equals(this.properties.entrySet().toArray(), that.properties.entrySet().toArray());
}
@Override
public int hashCode() {
return Arrays.hashCode(this.properties.entrySet().toArray());
}
@SuppressWarnings("unchecked")
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
this.properties = (Map<String, String>) stream.readObject();
this.suppressDate = stream.readBoolean();
}
/**
* See {@link Properties#toString()}.
*/
@Override
public String toString() {
return this.properties.toString();
}
/**
* Creates a new instance that will have both the same property entries and
* the same behavior as the given source.
* <p/>
* Note that the source instance and the copy instance will share the same
* comparator instance if a custom ordering had been configured on the source.
*
* @param source the source to copy from
* @return the copy
*/
public static OrderedProperties copyOf(OrderedProperties source) {
// create a copy that has the same behaviour
var builder = new OrderedPropertiesBuilder();
builder.withSuppressDateInComment(source.suppressDate);
if (source.properties instanceof TreeMap) {
builder.withOrdering(((TreeMap<String, String>) source.properties).comparator());
}
OrderedProperties result = builder.build();
// copy the properties from the source to the target
for (Map.Entry<String, String> entry: source.entrySet()) {
result.setProperty(entry.getKey(), entry.getValue());
}
return result;
}
/**
* Builder for {@link OrderedProperties} instances.
*/
public static final class OrderedPropertiesBuilder {
private Comparator<? super String> comparator;
private boolean suppressDate;
/**
* Use a custom ordering of the keys.
*
* @param comparator the ordering to apply on the keys
* @return the builder
*/
public OrderedPropertiesBuilder withOrdering(Comparator<? super String> comparator) {
this.comparator = comparator;
return this;
}
/**
* Suppress the comment that contains the current date when storing the properties.
*
* @param suppressDate whether to suppress the comment that contains the current date
* @return the builder
*/
public OrderedPropertiesBuilder withSuppressDateInComment(boolean suppressDate) {
this.suppressDate = suppressDate;
return this;
}
/**
* Builds a new {@link OrderedProperties} instance.
*
* @return the new instance
*/
public OrderedProperties build() {
Map<String, String> properties = this.comparator != null
? new TreeMap<>(this.comparator)
: new LinkedHashMap<>();
return new OrderedProperties(properties, this.suppressDate);
}
}
/**
* Custom {@link Properties} that delegates reading, writing, and enumerating properties to the
* backing {@link OrderedProperties} instance's properties.
*/
private static final class CustomProperties extends Properties {
private final Map<String, String> targetProperties;
private CustomProperties(Map<String, String> targetProperties) {
this.targetProperties = targetProperties;
}
@Override
public synchronized Object get(Object key) {
return this.targetProperties.get(key);
}
@Override
public synchronized Object put(Object key, Object value) {
return this.targetProperties.put((String) key, (String) value);
}
@Override
public String getProperty(String key) {
return this.targetProperties.get(key);
}
@Override
public synchronized Enumeration<Object> keys() {
return new Vector<Object>(this.targetProperties.keySet()).elements();
}
@Override
public Set<Object> keySet() {
return new LinkedHashSet<>(this.targetProperties.keySet());
}
@Override
public synchronized boolean equals(Object o) {
return super.equals(o) && o instanceof OrderedProperties;
}
@Override
public synchronized int hashCode() {
return super.hashCode();
}
}
/**
* Custom {@link BufferedWriter} for storing properties that will write all leading lines of comments except
* the last comment line. Using the JDK Properties class to store properties, the last comment
* line always contains the current date which is what we want to filter out.
*/
private static final class DateSuppressingPropertiesBufferedWriter extends BufferedWriter {
private StringBuilder currentComment;
private String previousComment;
private DateSuppressingPropertiesBufferedWriter(Writer out) {
super(out);
}
@Override
public void write(String string) throws IOException {
if (this.currentComment != null) {
this.currentComment.append(string);
if (string.endsWith(System.lineSeparator())) {
if (this.previousComment != null) {
super.write(this.previousComment);
}
this.previousComment = this.currentComment.toString();
this.currentComment = null;
}
} else if (string.startsWith("#")) {
this.currentComment = new StringBuilder(string);
} else {
super.write(string);
}
}
}
}