package main.options; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import hextostring.debug.DebuggingFlags; import hextostring.utils.Charsets; import main.options.annotations.CommandLineArgument; import main.options.annotations.CommandLineValue; import main.options.domain.Domain; import main.options.domain.Values; import main.options.parser.ArgumentParser; import main.utils.GenericSort; import main.utils.ReflectionUtils; import main.utils.StringUtils; /** * Abstract Options. * * The command line manual, the option dialog and the serialization- * deserialization of the options are generated automatically by respecting * the following conventions: * * A member that needs to be serialized but can't is declared transient * and must have a corresponding constant member of type * SerializingFallback in this class. * Naming convention: the member's type in SCREAMING_SNAKE_CASE + "_IO" * A concrete class with such a member must implement read/writeObject * calling the corresponding methods Options.deserialize/serialize * * If the version of the project is XXXXX.YYYYY.ZZZZZ, the serialVersionUID * attribute of serializable option classes must respect the following format: * XXXXX0YYYYY0ZZZZZL * * The usual default value of a member is indicated as follows: * Naming convention: "DEFAULT_" + the member's type in SCREAMING_SNAKE_CASE * * A configurable member must be given a CommandLineArgument annotation and * the appropriate accessor methods. Setters for members containing flags are * managed differently, by following the model setXXXFlags(long, boolean) where * the second argument indicates if the first must be added or substracted * from the current value. * * A configurable member using a custom parser is indicated as follows: * Naming convention: the member's type in SCREAMING_SNAKE_CASE + "_PARSER" * * The domain of a member whose values are constrained is indicated as follows: * Naming convention: the member's type in SCREAMING_SNAKE_CASE + "_DOMAIN" * If the values in the domain have further constraints or in the case of a * member containing flags, the values must be put as public static final * members of a separate class. * These members must be given a CommandLineValue annotation. * Said class is indicated as follows: * Naming convention: the member's type in SCREAMING_SNAKE_CASE + "_VALUE_CLASS" * * A configurable member must be an object, never a primitive type. * * @author Maxime PIA */ public abstract class Options { // used to format usage message private static final int INDENT_LENGTH = 4; private static final int DESC_LINE_LENGTH = 80 - INDENT_LENGTH; public static final String SERIALIAZATION_FILENAME = "config.data"; protected interface SerializingFallback<T> { Charset read(ObjectInputStream in) throws IOException; void write(Object t, ObjectOutputStream out) throws IOException; } public static final SerializingFallback<Charset> CHARSET_IO = new SerializingFallback<Charset>() { @Override public Charset read(ObjectInputStream in) throws IOException { return Charsets.getValidCharset(in.readUTF()); } @Override public void write(Object t, ObjectOutputStream out) throws IOException { out.writeUTF(((Charset) t).name()); } }; /** * Defines how to use command line options. * * @param message * The reason why the usage message is printed. May be null. * @return The usage message. */ public static String usage(String message, Set<Options> subOptions) { StringBuilder usage = new StringBuilder(); if(message != null && !message.isEmpty()) { usage.append(message); usage.append("\n\nPlease use the following options:\n\n"); } try { usage.append(generateUsageMessage(subOptions)); } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException | NoSuchMethodException e) { e.printStackTrace(); } return usage.toString(); } private static String generateUsageMessage(Set<Options> subOptions) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException, NoSuchMethodException { StringBuilder usage = new StringBuilder(); Collection<Options> sortedOptions = GenericSort.apply(subOptions, null); for (Options opt : sortedOptions) { Collection<Field> sortedFields = GenericSort.apply( ReflectionUtils.getAnnotatedFields( opt.getClass(), CommandLineArgument.class ), null ); for (Field argField : sortedFields) { CommandLineArgument argInfo = argField.getAnnotation(CommandLineArgument.class); usage.append(getCmdUsage(opt, argField, argInfo)); usage.append("\n" + getCmdDescription(argInfo)); if (!argInfo.usageExample().isEmpty()) { usage.append("\n" + getCmdExample(argInfo)); } usage.append("\n" + getCmdValueTable(opt, argField)); usage.append("\n\n"); } } usage.append(getHelpCmdUsage()); return usage.toString(); } private static String getCmdDescription(CommandLineArgument argInfo) { return StringUtils.indent( StringUtils.breakText( argInfo.description(), DESC_LINE_LENGTH ), " ", INDENT_LENGTH ); } private static String getCmdExample(CommandLineArgument argInfo) { return StringUtils.indent( "example: " + argInfo.usageExample(), " ", INDENT_LENGTH ); } private static String getCmdUsage(Options opt, Field argField, CommandLineArgument argInfo) throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException { StringBuilder usage = new StringBuilder(); usage.append("--" + argInfo.command() + "="); if (!argInfo.usage().isEmpty()) { usage.append(argInfo.usage()); return usage.toString(); } if (argInfo.flags()) { usage.append(" combination of "); } Domain<?> argDomain = null; try { argDomain = opt.generateArgumentValueDomain(argField); if (argDomain == null) { argDomain = opt.getFieldDomain(argField); } } catch (NoSuchFieldException e) {} Collection<Field> valFields = opt.getValueFields(argField); if (argDomain != null) { usage.append(argDomain); } else { usage.append("<" + argField.getType().getSimpleName() + ">"); } Object argDefault = opt.getFieldDefaultValue(argField); if (argDefault != null) { usage.append(", default="); if (argInfo.flags()) { String flagsStr = DebuggingFlags.longToCmdFlags( (Long) opt.getFieldDefaultValue(argField) ); usage.append(flagsStr.isEmpty() ? "none" : flagsStr); } else if (valFields == null) { usage.append(argDefault); } else { usage.append( getDefaultValueString(argDefault, valFields) ); } } return usage.toString(); } private static String getCmdValueTable(Options opt, Field argField) throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException { Collection<CommandLineValue> valDescs; try { valDescs = opt.getValuesDescriptions( opt.getFieldValueClass(argField) ); return StringUtils.indent( generateValueTable(valDescs), " ", INDENT_LENGTH ); } catch (NoSuchFieldException e) {} return ""; } private static String getDefaultValueString(Object defaultValue, Collection<Field> valFields) throws IllegalArgumentException, IllegalAccessException { for (Field valField : valFields) { if (valField.get(null).toString().equals(defaultValue.toString())) { return valField.getAnnotation(CommandLineValue.class).value(); } } return defaultValue.toString(); } private static String getHelpCmdUsage() { return "--help (cannot be used with other options)\n" + StringUtils.indent("Displays this message.", " ", INDENT_LENGTH); } /** * Getter on the possible values of a configurable member. * * @param argField * The configurable member. * @return The possible values of the configurable member. * @throws IllegalArgumentException * @throws IllegalAccessException * @throws SecurityException */ public Collection<Field> getValueFields(Field argField) throws IllegalArgumentException, IllegalAccessException, SecurityException { Class<?> argValsClass = null; Collection<Field> valFields; try { argValsClass = getFieldValueClass(argField); valFields = ReflectionUtils.getAnnotatedFields( argValsClass, CommandLineValue.class ); } catch (NoSuchFieldException e) { valFields = null; } return valFields; } private Collection<CommandLineValue> getValuesDescriptions( Class<?> valueClass) throws NoSuchMethodException, SecurityException { return valueClass == null ? null : GenericSort.apply( ReflectionUtils.getAnnotations( valueClass, CommandLineValue.class ), CommandLineValue.class.getMethod("value") ); } /** * Maps the command line string values to their actual value. * * @param argField * A configurable member with a restricted number of values. * @return A map of the command line string values to their actual value. * @throws NoSuchMethodException * @throws SecurityException * @throws IllegalArgumentException * @throws IllegalAccessException */ @SuppressWarnings("unchecked") public <T> Map<String, T> getValueDomainToActualValue(Field argField) throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException { Collection<Field> valFields = getValueFields(argField); if (valFields == null) { return null; } Map<String, T> res = new HashMap<>(); for (Field valField : valFields) { CommandLineValue valDesc = valField.getAnnotation(CommandLineValue.class); String key = valDesc.value().isEmpty() ? valField.get(null).toString() : valDesc.value(); res.put(key, (T) valField.get(null)); } return res; } private Values<String> generateArgumentValueDomain(Field argField) throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException { Map<String, Object> valuesMap = getValueDomainToActualValue(argField); if (valuesMap == null) { return null; } return new Values<String>( valuesMap.keySet().toArray(new String[valuesMap.keySet().size()]) ); } private static String generateValueTable( Collection<CommandLineValue> valuesDesc) { final String VALUE = "value"; final String DESCRIPTION = "description"; final String SHORTCUT = "shortcut"; final String CONDITION = "condition"; final String COLUMN_SEPARATOR = " "; final String HEADER_SEPARATOR = "-"; Map<Integer, String> headers = new TreeMap<>(); headers.put(0, VALUE); Map<String, Map<String, String>> content = new TreeMap<>(); for (CommandLineValue desc : valuesDesc) { Map<String, String> lineContent = new HashMap<>(); lineContent.put(DESCRIPTION, desc.description()); headers.put(2, DESCRIPTION); if (desc.shortcut().length() > 0) { lineContent.put(SHORTCUT, "-" + desc.shortcut()); headers.put(1, SHORTCUT); } if (desc.condition().length() > 0) { lineContent.put(CONDITION, desc.condition()); headers.put(3, CONDITION); } content.put(desc.value(), lineContent); } StringBuilder table = new StringBuilder(); int[] widths = new int[headers.size()]; Arrays.fill(widths, 30); widths[0] = 10; if (headers.containsValue(SHORTCUT)) { widths[1] = 10; } int eltCounter = 0; for (String title : headers.values()) { table.append( StringUtils.fillWithSpaces(title, widths[eltCounter++]) ); table.append(COLUMN_SEPARATOR); } table.append("\n"); eltCounter = 0; for (String title : headers.values()) { table.append( StringUtils.fillWithSpaces( title.replaceAll(".", HEADER_SEPARATOR), widths[eltCounter++] ) ); table.append(COLUMN_SEPARATOR); } table.append("\n"); headers.remove(0); for (String value : content.keySet()) { String[] paragraphs = new String[headers.values().size() + 1]; paragraphs[0] = value; int columnIndex = 1; for (String title : headers.values()) { paragraphs[columnIndex++] = content.get(value).get(title); } table.append( StringUtils.putParagraphsSideBySide( paragraphs, widths, COLUMN_SEPARATOR ) ); } return table.toString(); } /** * Getter on the default value of a configurable member. * * @param argField * The configurable member. * @return The default value of the configurable member. * @throws IllegalArgumentException * @throws IllegalAccessException * @throws NoSuchFieldException * @throws SecurityException */ public Object getFieldDefaultValue(Field argField) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { return getFieldAssociatedInformation("DEFAULT_", argField, ""); } /** * Getter on the value domain of a configurable member. * * @param @param argField * The configurable member. * @return The value domain of the configurable member. * @throws IllegalArgumentException * @throws IllegalAccessException * @throws NoSuchFieldException * @throws SecurityException */ public Domain<?> getFieldDomain(Field argField) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { return (Domain<?>) getFieldAssociatedInformation("", argField, "_DOMAIN"); } /** * Getter on the value class of a configurable member. * * @param argField * The configurable member. * @return The value class of the configurable member. * @throws IllegalArgumentException * @throws IllegalAccessException * @throws NoSuchFieldException * @throws SecurityException */ @SuppressWarnings("unchecked") public Class<? extends ValueClass> getFieldValueClass(Field argField) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { return (Class<? extends ValueClass>) getFieldAssociatedInformation("", argField, "_VALUE_CLASS"); } /** * Getter on the value class of a configurable member. * * @param argField * The configurable member. * @return * @throws IllegalArgumentException * @throws IllegalAccessException * @throws NoSuchFieldException * @throws SecurityException */ @SuppressWarnings("unchecked") public Class<? extends ArgumentParser<?>> getFieldParser(Field argField) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { return (Class<? extends ArgumentParser<?>>) getFieldAssociatedInformation("", argField, "_PARSER"); } private Object getFieldAssociatedInformation(String prefix, Field argField, String suffix) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { Field f = getClass().getDeclaredField( prefix + StringUtils.camelToScreamingSnake(argField.getName()) + suffix ); f.setAccessible(true); return f.get(this); } protected synchronized void serialize(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); try { Map<Field, SerializingFallback<?>> fallbacks = getSerializingFallbacks(); for (Field f : fallbacks.keySet()) { fallbacks.get(f).write(f.get(this), out); } } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { throw new IOException(e); } } protected synchronized void deserialize(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); try { Map<Field, SerializingFallback<?>> fallbacks = getSerializingFallbacks(); for (Field f : fallbacks.keySet()) { f.set(this, fallbacks.get(f).read(in)); } } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { throw new IOException(e); } } protected Map<Field, SerializingFallback<?>> getSerializingFallbacks() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { List<Field> nonSerializableFields = ReflectionUtils.getModifiedFields( this.getClass(), Modifier.TRANSIENT ); Map<Field, SerializingFallback<?>> fallbacks = new HashMap<>(); for (Field f : nonSerializableFields) { f.setAccessible(true); String fallbackName = StringUtils.camelToScreamingSnake( f.getType().getSimpleName() ) + "_IO"; fallbacks.put( f, (SerializingFallback<?>) Options.class.getField(fallbackName) .get(this) ); } return fallbacks; } }