All Downloads are FREE. Search and download functionalities are using the official Maven repository.

framework.annotation.Config Maven / Gradle / Ivy

package framework.annotation;

import java.io.IOException;
import java.io.Reader;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.nio.charset.Charset;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import app.config.Sys;
import framework.Db;
import framework.Formatter;
import framework.Log;
import framework.Message;
import framework.Reflector;
import framework.Session;
import framework.Tool;
import framework.Try;
import framework.Tuple;

/**
 * config file mapping
 */
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Config {

    /**
     * @return file names(use class name if empty)
     */
    String[] value() default {};

    /**
     * properties inject to static fields
     */
    class Injector {
        /**
         * inject from class
         * 
         * @param clazz target class(get source properties by annotation)
         */
        public static void inject(Class clazz) {

            Properties sourceProperties = inject(clazz, new Properties(), "");

            /* load config files form Config or classname.config */
            String[] fs = Tool.of(clazz.getAnnotation(Config.class))
                .map(Config::value)
                .filter(a -> a.length > 0)
                .orElse(Tool.array(Tool.fullName(clazz)
                    .toLowerCase(Locale.ENGLISH) + ".config"));
            for (String f : fs) {
                sourceProperties.putAll(getProperties(f));
            }

            /* resolve variables */
            for (;;) {
                boolean[] loop = { false };
                Set missings = new LinkedHashSet<>();
                sourceProperties.entrySet()
                    .forEach(pair -> {
                        resolve((String) pair.getValue(), sourceProperties, value -> {
                            sourceProperties.setProperty((String) pair.getKey(), value);
                            loop[0] = true;
                        }, missings::add);
                    });
                if (!loop[0]) {
                    missings.stream()
                        .map(key -> BEGIN + key + END + " cannot resolve.")
                        .forEach(Log::warning);
                    break;
                }
            }

            defaultMap.put(clazz, String.join(Letters.CRLF, dumpConfig(clazz, true)));
            Map propertiesMap = Tool.map("", sourceProperties);
            Stream.of(fs)
                .map(s -> Tuple.of(Tool.getFolder(s), Tool.getName(s), Tool.getExtension(s)))
                .collect(Collectors.groupingBy(t -> t.l))
                .entrySet()
                .forEach(entry -> {
                    String folder = entry.getKey();
                    List> nameExtension = entry.getValue()
                        .stream()
                        .map(t -> Tuple.of(t.r.l, t.r.r))
                        .collect(Collectors.toList());
                    try (Stream list = Tool.getResources(folder)) {
                        list.map(i -> Tuple.of(i, nameExtension.stream()
                            .filter(ne -> i.startsWith(ne.l) && i.endsWith(ne.r))
                            .findFirst()
                            .orElse(null)))
                            .filter(t -> t.r != null)
                            .map(t -> Tuple.of(t.l, folder.length() + t.r.l.length() + 1, t.l.length() - t.r.r.length()))
                            .filter(t -> t.r.l < t.r.r)
                            .forEach(t -> propertiesMap.compute(t.l
                                .substring(t.r.l, t.r.r), (k, v) -> v == null ? getProperties(t.l) : Tool.peek(v, vv -> vv.putAll(getProperties(t.l)))));
                    }
                });
            sourceMap.put(clazz, propertiesMap);

            inject(clazz, getSource(clazz, Session.currentLocale()), "");
        }

        /**
         * Set configuration value
         * 
         * @param name Name
         * @param value Value
         * @param locale Locale
         */
        public static void set(String name, String value, String locale) {
            Tool.of(getField(name))
                .ifPresent(field -> {
                    if (locale.isEmpty() && !Message.class.isAssignableFrom(field.getDeclaringClass())) {
                        set(field, name, value);
                    }
                });
            Tool.of(classCache.get(Tool.splitAt(name, "[.]", 0)))
                .map(clazz -> sourceMap.get(clazz)
                    .computeIfAbsent(locale, k -> new Properties()))
                .ifPresent(map -> map.put(name, value));
        }

        /**
         * Load database config
         */
        public static void loadDb() {
            try (Db db = Db.connect()) {
                String now = Tool.now(14);
                db.from("t_config")
                    .where("start_at", "<=", now)
                    .where("end_at", ">", now)
                    .rows(rs -> {
                        String name = Tool.string(rs.getString("name"))
                            .orElse("");
                        String value = Tool.string(rs.getString("value"))
                            .map(s -> s.replace("\\n", "\n")
                                .replace("\\r", "\r"))
                            .orElse("");
                        String locale = Tool.string(rs.getString("locale"))
                            .orElse("");
                        set(name, value, locale);
                    });
                sourceCache.clear();
            }
        }

        /**
         * Load system properties
         */
        public static void loadSystemProperties() {
            System.getProperties()
                .forEach((name, value) -> {
                    if (configKeys.contains(name) || ((String) name).startsWith("Sys.Db.")) { // overwrite only
                        set((String) name, (String) value, "");
                    }
                });
            sourceCache.clear();
        }

        /**
         * dump config
         * 
         * @param clazz target class
         * @param sort sort if true
         * @return lines
         */
        public static List dumpConfig(Class clazz, boolean sort) {
            return dumpConfig(clazz, "", sort);
        }

        /**
         * @return message dump
         */
        public static String[] dumpMessage() {
            Set locales = sourceMap.entrySet()
                .stream()
                .flatMap(entry -> entry.getValue()
                    .keySet()
                    .stream())
                .map(Locale::forLanguageTag)
                .collect(Collectors.toSet());
            Set> classes = sourceMap.keySet();
            return locales.stream()
                .flatMap(locale -> Stream.concat(Stream.of("[" + Tool.string(locale)
                    .orElse("default") + "]"), classes.stream()
                        .flatMap(clazz -> dumpMessage(getSource(clazz, locale), true).stream())))
                .toArray(String[]::new);
        }

        /**
         * @param name Field full name
         * @return Field
         */
        public static Field getField(String name) {
            return fieldCache.computeIfAbsent(name, fullName -> {
                int classIndex = fullName.indexOf('.');
                int fieldIndex = fullName.lastIndexOf('.');
                if (classIndex < 0 || fieldIndex < 0) {
                    return null;
                }
                Class clazz = classCache.computeIfAbsent(fullName.substring(0, fieldIndex), className -> {
                    Class c = classCache.get(fullName.substring(0, classIndex));
                    if (classIndex < fieldIndex) {
                        for (String i : fullName.substring(classIndex + 1, fieldIndex)
                            .split("[.]")) {
                            if (c == null) {
                                return null;
                            }
                            c = Stream.of(c.getClasses())
                                .filter(j -> i.equals(j.getSimpleName()))
                                .findAny()
                                .orElse(null);
                        }
                    }
                    return c;
                });
                if (clazz == null) {
                    return null;
                }
                try {
                    Field f = clazz.getDeclaredField(fullName.substring(fieldIndex + 1));
                    f.setAccessible(true);
                    return f;
                } catch (NoSuchFieldException | SecurityException e) {
                    return null;
                }
            });
        }

        /**
         * @param  Return type
         * @param name property name
         * @param locale locale
         * @return property value
         */
        @SuppressWarnings("unchecked")
        public static  Optional getValue(String name, Locale locale) {
            Field field = getField(name);
            if (field == null) {
                return Optional.empty();
            }
            try {
                return Tool.of(field.get(null))
                    .map(i -> (T) (i instanceof Message ? ((Message) i).message(locale) : i));
            } catch (IllegalArgumentException | IllegalAccessException e) {
                return Optional.empty();
            }
        }

        /**
         * @param clazz Class
         * @return Default settings
         */
        public static String getDefault(Class clazz) {
            return defaultMap.get(clazz);
        }

        /**
         * @param clazz Class
         * @param locale locale
         * @return Properties
         */
        public static Properties getSource(Class clazz, Locale locale) {
            Map map = Objects.requireNonNull(sourceMap.get(clazz));
            return sourceCache.computeIfAbsent(Tuple.of(clazz, locale), t -> {
                Properties p = new Properties();
                map.entrySet()
                    .stream()
                    .filter(pair -> locale.toString()
                        .startsWith(pair.getKey()))
                    .sorted((a, b) -> a.getKey()
                        .compareTo(b.getKey()))
                    .map(Map.Entry::getValue)
                    .forEach(p::putAll);
                return p;
            });
        }

        /**
         * default value of Separator.prefix
         */
        static final String prefixDefault;
        /**
         * default value of Separator.value
         */
        static final char valueDefault;
        /**
         * default value of Separator.suffix
         */
        static final String suffixDefault;
        /**
         * default value of Separator.value
         */
        static final char pairDefault;

        /**
         * default settings
         */
        static final Map, String> defaultMap = new ConcurrentHashMap<>();

        /**
         * source properties(class: (locale prefix: properties))
         */
        static final Map, Map> sourceMap = new ConcurrentHashMap<>();

        /**
         * source cache
         */
        static final Map, Locale>, Properties> sourceCache = new ConcurrentHashMap<>();

        /**
         * class cache
         */
        static final Map> classCache = new ConcurrentHashMap<>();

        /**
         * field cache
         */
        static final Map fieldCache = new ConcurrentHashMap<>();

        /**
         * Config keys(except message keys)
         */
        static final Set configKeys = new HashSet<>();

        static {
            prefixDefault = Reflector.getDefaultValue(Separator.class, "prefix");
            valueDefault = Reflector.getDefaultValue(Separator.class, "value");
            suffixDefault = Reflector.getDefaultValue(Separator.class, "suffix");
            pairDefault = Reflector.getDefaultValue(Separator.class, "pair");
        }

        /**
         * inner use
         * 
         * @param clazz Target class
         * @param properties Source properties
         * @param prefix Prefix of property-name
         * @return real properties
         */
        static Properties inject(Class clazz, Properties properties, String prefix) {
            Properties realProperties = new Properties();
            realProperties.putAll(properties);
            String newPrefix = prefix + clazz.getSimpleName() + '.';
            classCache.put(newPrefix.substring(0, newPrefix.length() - 1), clazz);
            if (!Enum.class.isAssignableFrom(clazz) || Message.class.isAssignableFrom(clazz)) {
                if (Message.class.isAssignableFrom(clazz)) {
                    Stream.of(clazz.getEnumConstants())
                        .forEach(i -> realProperties.put(newPrefix + ((Enum) i).name(), ((Message) i).defaultMessage()));
                } else {
                    Reflector.fields(clazz).values().stream()
                        .filter(f -> Modifier.isStatic(f.getModifiers()))
                        .forEach(f -> {
                            f.setAccessible(true);
                            String key = newPrefix + f.getName();
                            String raw = properties.getProperty(key);
                            Object value;
                            try {
                                value = f.get(null);
                            } catch (IllegalArgumentException | IllegalAccessException e) {
                                throw new InternalError(e);
                            }
                            if (!Modifier.isFinal(f.getModifiers()) && (properties.containsKey(key) || value == null)) {
                                set(f, key, raw);
                            } else {
                                configKeys.add(key);
                            }
                            realProperties.setProperty(key, toString(f, value));
                        });
                }
            }
            Stream.of(clazz.getClasses())
                .forEach(c -> realProperties.putAll(inject(c, properties, newPrefix)));
            return realProperties;
        }

        /**
         * @param field Field
         * @param key Key
         * @param text Value
         */
        static void set(Field field, String key, String text) {
            configKeys.add(key);
            Class type = field.getType();
            Object value;
            if (type == Optional.class) {
                value = Tool.string(text)
                    .map(s -> getValue(field, Reflector.getGenericParameter(field, 0), s));
            } else if (type.isArray()) {
                Class componentType = type.getComponentType();
                Object[] array = split(text, field.getAnnotation(Separator.class)).map(i -> getValue(field, componentType, i))
                    .toArray();
                value = Array.newInstance(componentType, array.length);
                int i = 0;
                for (Object v : array) {
                    Array.set(value, i, v);
                    i++;
                }
            } else if (type == List.class) {
                value = split(text, field.getAnnotation(Separator.class)).map(i -> getValue(field, Reflector.getGenericParameter(field, 0), i))
                    .collect(Collectors.toList());
            } else if (type == Set.class) {
                value = split(text, field.getAnnotation(Separator.class)).map(i -> getValue(field, Reflector.getGenericParameter(field, 0), i))
                    .collect(LinkedHashSet::new, (set, v) -> set.add(v), Set::addAll);
            } else if (type == Map.class) {
                value = split(text, field.getAnnotation(Separator.class)).map(i -> {
                    String[] pair = i.split(Tool.val(field
                        .getAnnotation(Separator.class), s -> s == null ? prefixDefault + pairDefault + suffixDefault : s.prefix() + s.pair() + s.suffix()));
                    return Tuple.of(getValue(field, Reflector.getGenericParameter(field, 0), pair[0]), getValue(field, Reflector
                        .getGenericParameter(field, 1), pair[1]));
                })
                    .collect(LinkedHashMap::new, (map, tuple) -> map.put(tuple.l, tuple.r), Map::putAll);
            } else {
                value = getValue(field, type, text);
            }
            try {
                field.set(null, value);
            } catch (IllegalArgumentException | IllegalAccessException | SecurityException e) {
                throw new InternalError(e);
            }
        }

        /**
         * inner use
         * 
         * @param clazz Target class
         * @param prefix Prefix of property-name
         * @param sort sort if true
         * @return lines
         */
        static List dumpConfig(Class clazz, String prefix, boolean sort) {
            String newPrefix = prefix + clazz.getSimpleName()
                .replace('$', '.') + '.';
            List lines = new ArrayList<>();
            if (!Enum.class.isAssignableFrom(clazz)) {
                Reflector.fields(clazz).values().stream()
                    .filter(f -> Modifier.isStatic(f.getModifiers()))
                    .forEach(f -> {
                        try {
                            String key = newPrefix + f.getName();
                            Object value = f.get(null);
                            List comments = Tool.of(f.getAnnotation(Help.class))
                                .map(Help::value)
                                .map(Arrays::asList)
                                .orElse(null);
                            if (comments != null) {
                                Collections.reverse(comments);
                            }
                            lines.add('\b' + key + " = " + toString(f, value) + (comments == null ? "" : "\b# " + String.join("\b# ", comments)));
                        } catch (IllegalArgumentException | IllegalAccessException e) {
                            throw new InternalError(e);
                        }
                    });
            }
            Stream.of(clazz.getClasses())
                .forEach(c -> lines.addAll(dumpConfig(c, newPrefix, false)));
            if (sort) {
                Collections.sort(lines);
                return lines.stream()
                    .flatMap(line -> {
                        List reverse = Tool.list(line.split("\b"));
                        Collections.reverse(reverse);
                        return reverse.stream();
                    })
                    .collect(Collectors.toList());
            }
            return lines;
        }

        /**
         * inner use
         * 
         * @param properties Properties
         * @param sort sort if true
         * @return lines
         */
        static List dumpMessage(Properties properties, boolean sort) {
            List lines = new ArrayList<>();
            properties.forEach((key, value) -> {
                if (!configKeys.contains(key)) {
                    lines.add(key + " = " + value);
                }
            });
            if (sort) {
                Collections.sort(lines);
            }
            return lines;
        }

        /**
         * inner use
         * 
         * @param field Field
         * @return DateTimeFormatter or empty
         */
        static Optional getFormat(Field field) {
            return Tool.of(field.getAnnotation(Format.class))
                .map(Format::value)
                .map(DateTimeFormatter::ofPattern);
        }

        /**
         * inner use
         * 
         * @param path Properties file path
         * @return Properties
         */
        static Properties getProperties(String path) {
            Properties p = new Properties();
            Tool.ifPresentOr(Tool.toURL(path), url -> {
                Log.info("config load: " + url);
                try (Reader reader = Tool.newReader(url.openStream())) {
                    p.load(reader);
                } catch (IOException e) {
                    Log.warning(e, () -> "load error");
                }
            }, () -> Log.info("config scan: " + path));
            return p;
        }

        /**
         * inner use
         * 
         * @param field Field
         * @param type Value type
         * @param raw String value
         * @return Value
         */
        @SuppressWarnings({ "unchecked", "rawtypes" })
        static Object getValue(Field field, Class type, String raw) {
            if (type == Integer.class || type == int.class) {
                return raw == null ? 0 : Integer.parseInt(raw);
            } else if (type == Byte.class || type == byte.class) {
                return raw == null ? (byte) 0 : Byte.parseByte(raw);
            } else if (type == Short.class || type == short.class) {
                return raw == null ? (short) 0 : Short.parseShort(raw);
            } else if (type == Long.class || type == long.class) {
                return raw == null ? 0L : Long.parseLong(raw);
            } else if (type == Float.class || type == float.class) {
                return raw == null ? 0f : Float.parseFloat(raw);
            } else if (type == Double.class || type == double.class) {
                return raw == null ? 0.0 : Double.parseDouble(raw);
            } else if (type == Character.class || type == char.class) {
                return raw != null && raw.length() > 0 ? raw.charAt(0) : '\0';
            } else if (type == Boolean.class || type == boolean.class) {
                return Boolean.parseBoolean(raw);
            } else if (type == String.class) {
                return raw == null ? "" : raw;
            } else if (type == LocalDate.class) {
                return raw == null ? LocalDate.now() : LocalDate.parse(raw, getFormat(field).orElse(DateTimeFormatter.ISO_LOCAL_DATE));
            } else if (type == LocalDateTime.class) {
                return raw == null ? LocalDateTime.now() : LocalDateTime.parse(raw, getFormat(field).orElse(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
            } else if (type == LocalTime.class) {
                return raw == null ? LocalTime.now() : LocalTime.parse(raw, getFormat(field).orElse(DateTimeFormatter.ISO_LOCAL_TIME));
            } else if (type == ZonedDateTime.class) {
                return raw == null ? ZonedDateTime.now() : ZonedDateTime.parse(raw, getFormat(field).orElse(DateTimeFormatter.ISO_ZONED_DATE_TIME));
            } else if (type == OffsetDateTime.class) {
                return raw == null ? OffsetDateTime.now() : OffsetDateTime.parse(raw, getFormat(field).orElse(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
            } else if (type == OffsetTime.class) {
                return raw == null ? OffsetTime.now() : OffsetTime.parse(raw, getFormat(field).orElse(DateTimeFormatter.ISO_OFFSET_TIME));
            } else if (Enum.class.isAssignableFrom(type)) {
                return raw == null ? type.getEnumConstants()[0] : Enum.valueOf((Class) type, raw);
            } else if (type == DateTimeFormatter.class) {
                return raw == null ? DateTimeFormatter.BASIC_ISO_DATE : DateTimeFormatter.ofPattern(raw);
            } else if (type == Pattern.class) {
                return Pattern.compile(raw == null ? ".*" : raw);
            } else if (type == Level.class) {
                return raw == null ? Level.INFO : Level.parse(raw);
            } else if (type == Charset.class) {
                return raw == null ? Charset.defaultCharset() : Charset.forName(raw);
            } else if (type == URL.class) {
                return raw == null ? null
                        : Try.f(URL::new)
                            .apply(raw);
            } else {
                return raw;
            }
        }

        /**
         * inner use
         * 
         * @param text Text
         * @param separator Separator(Regular expression)
         * @return Splited text
         */
        static Stream split(String text, Separator separator) {
            if (text == null) {
                return Stream.empty();
            }
            String pattern;
            if (separator == null) {
                pattern = prefixDefault + valueDefault + suffixDefault;
            } else {
                pattern = separator.prefix() + separator.value() + separator.suffix();
            }
            return Stream.of(String.valueOf(text)
                .split(pattern));
        }

        /**
         * inner use
         * 
         * @param field Field
         * @param value Value
         * @return string value
         */
        static String toString(Field field, Object value) {
            if (value == null || value == Optional.empty()) {
                return "";
            }
            Class clazz = field.getType();
            if (clazz == Optional.class) {
                return ((Optional) value).map(String::valueOf)
                    .orElse("");
            }
            char separator = Tool.of(field.getAnnotation(Separator.class))
                .map(Separator::value)
                .orElse(valueDefault);
            if (clazz.isArray()) {
                StringBuilder s = new StringBuilder();
                for (int i = 0, i2 = Array.getLength(value); i < i2; i++) {
                    s.append(separator)
                        .append(Array.get(value, i));
                }
                return s.length() > 0 ? s.substring(1) : "";
            }
            if (clazz == List.class) {
                return ((List) value).stream()
                    .map(String::valueOf)
                    .collect(Collectors.joining(String.valueOf(separator)));
            }
            if (clazz == Set.class) {
                return ((Set) value).stream()
                    .map(String::valueOf)
                    .collect(Collectors.joining(String.valueOf(separator)));
            }
            if (clazz == Map.class) {
                char pairSeparator = Tool.of(field.getAnnotation(Separator.class))
                    .map(Separator::pair)
                    .orElse(pairDefault);
                return Tool.peek(new StringBuilder(), s -> ((Map) value).forEach((k, v) -> s.append(separator)
                    .append(k)
                    .append(pairSeparator)
                    .append(v)))
                    .substring(1);
            }
            if (value instanceof Temporal) {
                Optional formatter = getFormat(field);
                if (formatter.isPresent()) {
                    return formatter.get()
                        .format((Temporal) value);
                }
            }
            if (value instanceof DateTimeFormatter) {
                return Tool.val(((DateTimeFormatter) value).toString(), s -> Tool.formatCache.getOrDefault(s, s));
            }
            return String.valueOf(value);
        }

        /**
         * variable bracket beginning mark
         */
        public static final String BEGIN = "{";

        /**
         * variable bracket ending mark
         */
        public static final String END = "}";

        /**
         * resolve variable
         *
         * @param value value
         * @param source variables
         * @param changed changed action
         * @param missing missing action
         */
        static void resolve(String value, Properties source, Consumer changed, Consumer missing) {
            boolean isChanged = false;
            for (;;) {
                int start = value.indexOf(BEGIN);
                if (start < 0) {
                    break;
                }
                start += BEGIN.length();
                int end = value.indexOf(END, start);
                if (end < 0) {
                    break;
                }
                String key = value.substring(start, end);
                if (!source.containsKey(key)) {
                    if (missing != null) {
                        missing.accept(key);
                    }
                    break;
                }
                String replace = source.getProperty(key);
                int loop = replace.indexOf(BEGIN);
                if (loop >= 0 && loop < replace.indexOf(END)) {
                    if (missing != null) {
                        missing.accept(key);
                    }
                    break;
                }
                isChanged = true;
                value = value.substring(0, start - BEGIN.length()) + replace + value.substring(end + END.length());
            }
            if (isChanged) {
                changed.accept(value);
            }
        }
        
        /**
         * configuration class list
         */
        public static final List> classes = Tool.list();

		/**
		 * @param packages configuration class packages
		 */
		public static void setup(String... packages) {
	        try (Stream> cs = Tool.getClasses(Sys.class.getPackage()
	            .getName())
	            .filter(c -> Tool.fullName(c)
	                .indexOf('.') < 0)) {
	            classes.addAll(cs.peek(Config.Injector::inject)
	                .peek(c -> Formatter.elClassMap.put(c.getSimpleName(), c))
	                .collect(Collectors.toList()));
	        }

	        /* load system properties */
	        Config.Injector.loadSystemProperties();
		}
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy