added contents

This commit is contained in:
Mark Minas
2024-09-18 17:04:31 +02:00
parent d28b17eba5
commit 71a4ac8d12
176 changed files with 51198 additions and 0 deletions

View File

@@ -0,0 +1,286 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util.config;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.function.Function;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Abstract base class for representing configurations that can be read from properties files.
* Subclasses can define fields annotated with {@link Property} to specify the configuration keys
* and optionally {@link Separator} to specify custom separators for array values.
*/
public abstract class Config {
private static final String BLANK_SEQ = " *";
/**
* Annotation for specifying the property key for a field.
*/
@Retention(RUNTIME)
@Target(FIELD)
@Documented
protected @interface Property {
/**
* The key of the property.
*/
String value();
}
/**
* Annotation for specifying a custom separator for array values.
*/
@Retention(RUNTIME)
@Target(FIELD)
@Documented
protected @interface Separator {
/**
* The separator for array values.
*/
String value();
}
private static final Logger LOGGER = System.getLogger(Config.class.getName());
private static final Map<Class<?>, Function<String, ?>> CONVERTER_MAP = new HashMap<>();
static {
CONVERTER_MAP.put(String.class, x -> x);
CONVERTER_MAP.put(byte.class, Byte::parseByte);
CONVERTER_MAP.put(short.class, Short::parseShort);
CONVERTER_MAP.put(int.class, Integer::parseInt);
CONVERTER_MAP.put(long.class, Long::parseLong);
CONVERTER_MAP.put(boolean.class, Boolean::parseBoolean);
CONVERTER_MAP.put(float.class, Float::parseFloat);
CONVERTER_MAP.put(double.class, Double::parseDouble);
CONVERTER_MAP.put(Byte.class, Byte::parseByte);
CONVERTER_MAP.put(Short.class, Short::parseShort);
CONVERTER_MAP.put(Integer.class, Integer::parseInt);
CONVERTER_MAP.put(Long.class, Long::parseLong);
CONVERTER_MAP.put(Boolean.class, Boolean::parseBoolean);
CONVERTER_MAP.put(Float.class, Float::parseFloat);
CONVERTER_MAP.put(Double.class, Double::parseDouble);
}
/**
* Reads the specified properties file and sets the values of this config using {@link #readFrom(Properties)}.
*
* @param file the properties file to read
* @throws IOException if an I/O error occurs
* @see #readFrom(Properties)
*/
public void readFrom(File file) throws IOException {
try (Reader reader = new FileReader(file)) {
final Properties properties = new Properties();
properties.load(reader);
readFrom(properties);
}
}
/**
* Sets the values of fields annotated with {@link Property} using the specified properties.
* Array fields can be split into components using the default separator (",") or a custom separator
* specified with {@link Separator}.
*
* @param props the properties to read from
*/
public void readFrom(Properties props) {
for (Class<?> clazz = getClass(); Config.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) {
for (Field field : clazz.getDeclaredFields()) {
final Property keyAnnot = field.getAnnotation(Property.class);
if (keyAnnot != null && props.containsKey(keyAnnot.value())) {
try {
final String text = props.getProperty(keyAnnot.value());
final Object value = createValue(text, field);
setField(field, value);
}
catch (IllegalAccessException ex) {
LOGGER.log(Level.ERROR, "Cannot access " + field, ex); //NON-NLS
}
}
}
}
}
/**
* Reads the specified properties file and sets the values of this config if the file exists,
* otherwise uses default values. This method is a convenience version of {@link #readFrom(File)}
* that checks the existence of the specified file and does nothing if the file does not exist.
*
* @param file the properties file to read, if it exists
*/
public void readFromIfExists(File file) {
if (!file.exists()) {
LOGGER.log(Level.INFO, "There is no config file {0}; using default configuration", //NON-NLS
file.getAbsolutePath());
return;
}
try {
readFrom(file);
LOGGER.log(Level.INFO, "Successfully read config from {0}", file.getAbsolutePath()); //NON-NLS
}
catch (IOException e) {
LOGGER.log(Level.WARNING, "Cannot read config file " + file.getAbsolutePath(), e); //NON-NLS
}
}
/**
* Converts a string value from the properties file into an object that can be assigned to the specified field.
* For array fields, the string value is first split into components.
*
* @param value the string value to convert
* @param field the field to set with the converted value
* @return an object of the appropriate type for the field
*/
private Object createValue(String value, Field field) {
if (!field.getType().isArray())
return convertToType(value, field.getType());
// the field is an array
final Separator sepAnn = field.getDeclaredAnnotation(Separator.class);
final String sep = sepAnn == null ? "," : sepAnn.value();
final String[] split = value.split(BLANK_SEQ + sep + BLANK_SEQ, -1);
final Object array = Array.newInstance(field.getType().componentType(), split.length);
for (int i = 0; i < split.length; i++)
Array.set(array, i, convertToType(split[i], field.getType().componentType()));
return array;
}
/**
* Converts a string value into an object of the specified type.
*
* @param value the string value to convert
* @param targetType the target type to convert to
* @return an object of the specified type
*/
protected Object convertToType(String value, Class<?> targetType) {
Function<String, ?> handler = CONVERTER_MAP.get(targetType);
if (handler != null)
return handler.apply(value);
throw new IllegalArgumentException("Cannot translate " + value + " to " + targetType);
}
/**
* Returns a string representation of the configuration object, including all properties and their values.
*
* @return a string representation of the configuration object
*/
@Override
public String toString() {
final List<String> propertyStrings = getPropertyStrings();
propertyStrings.sort(String.CASE_INSENSITIVE_ORDER);
return "[\n" + String.join(",\n", propertyStrings) + "\n]";
}
/**
* Retrieves all property strings of the configuration object.
*
* @return a list of property strings
*/
private List<String> getPropertyStrings() {
final List<String> propertyStrings = new ArrayList<>();
for (Class<?> clazz = getClass(); Config.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) {
for (Field field : clazz.getDeclaredFields()) {
final String stringRepresentation = getStringRepresentation(field);
if (stringRepresentation != null)
propertyStrings.add(stringRepresentation);
}
}
return propertyStrings;
}
/**
* Retrieves the string representation of a field annotated with {@link Property}.
*
* @param field the field to retrieve the string representation for
* @return the string representation of the field, or null if the field is not annotated with {@link Property}
*/
private String getStringRepresentation(Field field) {
final Property keyAnnotation = field.getAnnotation(Property.class);
if (keyAnnotation != null) {
try {
final Object fieldValue = getField(field);
final String valueString = asString(fieldValue);
return keyAnnotation.value() + " -> " + field.getName() + " = " + valueString;
}
catch (IllegalAccessException e) {
LOGGER.log(Level.ERROR, "Cannot access " + field, e); //NON-NLS
}
}
return null;
}
/**
* Converts an object to its string representation. For arrays, string representations of their components are produced.
*
* @param value the object to convert
* @return the string representation of the object
*/
private String asString(Object value) {
if (value == null)
return "null"; //NON-NLS
if (!value.getClass().isArray())
return value.toString();
final int length = Array.getLength(value);
final List<String> components = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
final Object component = Array.get(value, i);
components.add(asString(component));
}
return "{" + String.join(", ", components) + "}";
}
/**
* Sets the value of a field, making it accessible if necessary.
*
* @param field the field to set
* @param value the value to set
* @throws IllegalAccessException if the field cannot be accessed
*/
private void setField(Field field, Object value) throws IllegalAccessException {
boolean inaccessible = !field.canAccess(this);
if (inaccessible)
field.setAccessible(true);
field.set(this, value);
if (inaccessible)
field.setAccessible(false);
LOGGER.log(Level.TRACE, "Set {0} to {1}", field, value); //NON-NLS
}
/**
* Retrieves the value of a field, making it accessible if necessary.
*
* @param field the field to retrieve the value from
* @return the value of the field
* @throws IllegalAccessException if the field cannot be accessed
*/
private Object getField(Field field) throws IllegalAccessException {
boolean inaccessible = !field.canAccess(this);
if (inaccessible)
field.setAccessible(true);
final Object value = field.get(this);
if (inaccessible)
field.setAccessible(false);
return value;
}
}