
de.tsl2.nano.modelkit.impl.ModelKit Maven / Gradle / Ivy
/*
* File: $HeadURL$
* Id : $Id$
*
* created by: Tom
* created on: 31.03.2017
*
* Copyright: (c) Thomas Schneider 2017, all rights reserved
*/
package de.tsl2.nano.modelkit.impl;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import javax.inject.Named;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.cronutils.descriptor.CronDescriptor;
import com.cronutils.model.CronType;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.model.time.ExecutionTime;
import com.cronutils.parser.CronParser;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.util.StdConverter;
import de.tsl2.nano.modelkit.Configured;
import de.tsl2.nano.modelkit.ExceptionHandler;
import de.tsl2.nano.modelkit.Identified;
import de.tsl2.nano.modelkit.ObjectUtil;
import de.tsl2.nano.modelkit.impl.ModelKitLoader.JsonToMapConverter;
import io.quarkus.scheduler.Scheduled;
import lombok.Getter;
import lombok.Setter;
/**
* full model kit providing all elements to apply a function to a list of objects. a factory is provided
* through #ModelKitLoader.
* to provide test-items to check against on each modelkit change, set the system property 'tsl2.modelkit.test.items.json.file'
*/
@ApplicationScoped
@JsonPropertyOrder({ "name", "cron", "funcName", "description", "env" })
@SuppressWarnings({ "rawtypes", "unchecked" })
public class ModelKit extends AIdentified implements Function, List> {
private static final Logger LOG = LoggerFactory.getLogger(ModelKit.class);
private static boolean testMode;
@JsonDeserialize(converter = JsonToMapConverter.class)
Map, List extends Identified>> env = new HashMap<>();
@Getter @Setter
private String cron;
@Setter
private String description;
@Getter
@Setter
private String funcName;
@JsonIgnore
private String cronDescription;
@JsonIgnore
private static String[] logDebugFields;
/** constructor is used internally on injection - but we have producers */
ModelKit() {
super(null);
}
public ModelKit(String name, String funcName, String cron, String description) {
super(name);
this.cron = cron;
this.description = description;
this.funcName = funcName;
validate();
}
@Override
public List apply(List items) {
if (items.isEmpty()) {
LOG.warn("the given list is empty - nothing to do!");
return items;
}
long start = System.currentTimeMillis();
before(items);
final List newItemList = new ArrayList<>(items.size());
final List passedItems = new ArrayList<>(items);
int passes = getPassCount();
for (int i = 0; i < passes; i++) {
final int ii = i; //workaround to provide a final var in enclosing lambda
forEachGroup(g -> newItemList.addAll(g.apply(ii, passedItems)));
passedItems.retainAll(newItemList);
newItemList.clear();
}
after(newItemList);
logDebug(newItemList, System.currentTimeMillis() - start);
return passedItems;
}
private int getPassCount() {
return get(Group.class).stream()
.max((g1, g2) -> g1.getPassCount().compareTo(g2.getPassCount()))
.get()
.getPassCount();
}
/** to be implemented by extension */
protected void before(List items) {
}
/** to be implemented by extension */
protected void after(List items) {
}
static boolean isTestMode() {
return testMode;
}
@Override
public void validate() {
isActiveNow();
if (env.size() > 0) {
List groups = get(Group.class);
Objects.checkIndex(0, groups.size());
if (!groups.stream().anyMatch(g -> g.getPassCount() > 0)) {
throw new IllegalStateException(
"no group with any function found. at least one group must have a function to be applied!");
}
env.values().forEach(e -> e.forEach(c -> ((Configured) c).validate()));
}
}
public String getDescription() {
return description;
}
public void add(Identified... parts) {
addIdentifiedArray(parts);
}
private void addIdentifiedArray(Identified... parts) {
List list = Arrays.asList(parts);
env.put((Class extends Identified>) parts.getClass().getComponentType(), list);
list.stream().forEach(i -> i.tagNames(this.name));
list.stream().forEach(i -> ((Configured) i).setConfiguration(this));
}
public void add(Fact... parts) {
Fact[] negations = new Fact[parts.length];
for (int i = 0; i < negations.length; i++) {
negations[i] = ((Fact) parts[i].clone()).setNegate();
}
addIdentifiedArray(Stream.concat(Arrays.stream(parts), Arrays.stream(negations)).toArray(Fact[]::new));
}
@Override
public I get(String name, Class type) {
Objects.requireNonNull(name, "name must not be null");
Objects.requireNonNull(type, "type must not be null");
List elements = get(type);
Class tt = type;
while (elements == null) {
elements = get(tt);
tt = tt.getSuperclass();
}
Objects.requireNonNull(elements,
() -> "configuration error: your model kit didn't declare any element of type " + type.getSimpleName()
+ " for name: " + name);
return Identified.get(elements, tag(this.name, name));
}
@Override
public List get(Class type) {
return (List) env.get(type);
}
public List> getEnum(String definitionName) {
return (List>) get(tag(name, definitionName), Def.class).getValue();
}
public E getPrevious(E element) {
return getAt(element, -1);
}
public E getNext(E element) {
return getAt(element, 1);
}
public E getAt(E element, int addIndex) {
List elements = (List) get(element.getClass());
int i = elements.indexOf(element);
return i == -1 || i + addIndex < 0 || i + addIndex >= elements.size() ? null : elements.get(i + addIndex);
}
boolean isActiveNow() {
return isActive(ZonedDateTime.now());
}
boolean isActive(ZonedDateTime time) {
if (cron == null) {
return true;
}
CronParser parser = getCronParser();
ExecutionTime executionTime = ExecutionTime.forCron(parser.parse(cron));
Duration timeToNextExecution = executionTime.timeToNextExecution(time).orElseThrow();
return timeToNextExecution.getSeconds() < 2;
}
private CronParser getCronParser() {
return new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ));
}
private String cronDescription() {
if (cronDescription == null) {
cronDescription = cron != null ? CronDescriptor.instance().describe(getCronParser().parse(cron)) : "active on any time!";
}
return cronDescription;
}
/** convenience to crawl through owned groups (the type is only for compiler generic access) */
public void forEachGroup(Consumer> c) {
get(Group.class).stream().forEach(g -> c.accept(g));
}
public void forEachGroupItem(List items, Consumer c) {
get(Group.class).stream().forEach(g -> g.filter(items).forEach(i -> c.accept((T) i)));
}
public void forEachElement(Consumer c) {
env.values().forEach(e -> e.forEach(c));
}
public String describe() {
String chapter = "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n";
StringBuilder b = new StringBuilder(chapter + toString() + "\n");
forEachGroup(g -> b.append("\t" + g.describe("\t") + "\n"));
b.append(chapter);
return b.toString();
}
public String describeResult() {
StringBuilder b = new StringBuilder("\ncount of calls:\n");
forEachElement(c -> b.append("\n\t" + c.getName() + ": " + ((Configured) c).getVisitorCount()));
b.append("\n\n");
return b.toString();
}
public static void enableDebugLog(String... logDebugFields) {
ModelKit.logDebugFields = logDebugFields;
testMode = true;
}
public void logDebug(List> items, long duration) {
if (testMode || LOG.isDebugEnabled()) {
LOG.info(
"\n" + name + " on " + items.size() + " items (time: " + duration + " msec)\n" +
describeResult() +
ObjectUtil.toString(items, logDebugFields));
}
}
/** optional function to be called, if all configurations are done */
void finalizeOnType() {
if (testMode || LOG.isDebugEnabled()) {
LOG.info(describe());
}
}
// @Override
// public boolean equals(Object obj) {
// // TODO: not performance optimized
// return super.equals(obj) && forEachElement(e -> ((ModelKit)obj).checkExistence(e.getName(), e.getClass()));
// }
@Override
public String toString() {
return getClass().getSimpleName() + "(" + name + ": " + cronDescription() + ")";
}
public void register() {
ModelKitLoader.register(this);
finalizeOnType();
}
@ApplicationScoped
@Produces
@Named("Configured")
public static ModelKit getActiveModelKitNow() {
return ModelKitLoader.getActiveModelKit(ZonedDateTime.now());
}
public static ModelKit getActiveModelKit(ZonedDateTime time) {
return ModelKitLoader.getActiveModelKit(time);
}
public static List getConfigurations() {
return ModelKitLoader.getConfigurations();
}
public static void updateConfiguration(String configName, String property, String value) {
ModelKitLoader.updateConfiguration(configName, property, value);
}
public static void updateConfigurationElement(String configName, String elementType,
String elementAsJSon) {
ModelKitLoader.updateConfigurationElement(configName, elementType, elementAsJSon);
}
public static void saveAsJSon(ModelKit... configs) {
Arrays.stream(configs).forEach(c -> c.validate());
new ModelKitTester().test(configs);
ModelKitLoader.saveAsJSon(configs);
}
public void reset() {
ModelKitLoader.reset();
}
public static void resetAndDelete() {
ModelKitLoader.resetAndDelete();
}
}
/**
* loads all available model kits from json
*
* given by current date, only one model kit will be selected.
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
class ModelKitLoader {
private static final Logger LOG = LoggerFactory.getLogger(ModelKitLoader.class);
private static final String MODELKIT_JSON = "modelkit.json";
/** hard coded modelkits include lambda implementations, to be reused on dynamic loaded model kits. */
private static Map registeredHardConfigurations = new LinkedHashMap<>();
/** loaded dynamic model kits */
private static List configurations;
/** to find simple Class names of elements we provide all registered on reloading from json */
private static Map registeredElements = new LinkedHashMap<>();
/** period look for a changed json configuration file */
private static AtomicLong lastJsonLookupTime = new AtomicLong();
private ModelKitLoader() {
}
public static void register(ModelKit config) {
registeredHardConfigurations.put(config.name, config);
}
/** to be more convenient, a short class name is possible */
static void registereElement(Class extends AIdentified> elementType) {
registeredElements.put(elementType.getSimpleName(), elementType);
}
static Class getElementType(String name) throws ClassNotFoundException {
Class type;
return (type = registeredElements.get(name)) != null ? type : (Class) Class.forName(name);
}
/** to be used on a rest service to change a current configuration */
static void updateConfiguration(String configName, String property, String value) {
ObjectUtil.setValue(getConfig(configName), property, value);
saveAsJSon(getConfigurations().toArray(new ModelKit[0]));
}
/** to be used on a rest service to change a configuration element */
static void updateConfigurationElement(String configName, String elementType,
String elementAsJSon) {
try {
Class type = getElementType(elementType);
I element = createObjectMapper().readValue(elementAsJSon, type);
I origin = (I) getConfig(configName).get(element.getName(), type);
ObjectUtil.update(origin, element);
saveAsJSon(getConfigurations().toArray(new ModelKit[0]));
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
private static ModelKit getConfig(String configName) {
return getConfigurations().stream().filter(c -> c.name.equals(configName)).findFirst().orElseThrow();
}
public static I findRegistered(String kitName, String name, Class type) {
Objects.requireNonNull(registeredHardConfigurations.isEmpty() ? null : "OK",
() -> "configuration error: at least one sort-configuration implementation must be registered");
if (kitName.equals("*")) {
for (ModelKit> config : registeredHardConfigurations.values()) {
I ref = ExceptionHandler.trY(() -> config.get(name, type), IllegalStateException.class);
if (ref != null) {
return ref;
}
}
if (LOG.isDebugEnabled()) {
LOG.warn("no configuration item found for config: " + kitName + "/" + name);
}
return null;
} else {
ModelKit> config = registeredHardConfigurations.get(kitName);
if (config == null && ModelKit.isTestMode()) {
throw new IllegalStateException("no registry entry found for configuration: " + kitName);
}
return config != null ? ExceptionHandler.trY(() -> config.get(name, type), IllegalStateException.class) : null;
}
}
static ModelKit getActiveModelKit(ZonedDateTime time) {
return getActiveModelKit(getConfigurations(), time);
}
static ModelKit getActiveModelKit(List configs, ZonedDateTime time) {
return configs.stream().filter(c -> c.isActive(time)).findFirst().orElseThrow();
}
static List getConfigurations() {
if (configurations == null) {
configurations = loadConfigurations();
}
return configurations;
}
private static List loadConfigurations() {
if (!new File(MODELKIT_JSON).exists()) {
saveAsJSon(registeredHardConfigurations.values().toArray(new ModelKit[0]));
}
List config = readFromJSon();
config.forEach(c -> c.forEachElement(e -> ((Configured) e).setConfiguration(c)));
return config;
}
static List readFromJSon() {
LOG.info("loading configurations from " + MODELKIT_JSON);
lastJsonLookupTime.set(System.currentTimeMillis());
try {
return createObjectMapper().readValue(new File(MODELKIT_JSON),
new TypeReference>() {
});
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
public static void saveAsJSon(ModelKit... configs) {
LOG.info("checking new configuration array");
Arrays.stream(configs).forEach(c -> c.validate());
new ModelKitTester().test(configs);
LOG.info("saving " + MODELKIT_JSON + " on new configuration array");
try {
ObjectMapper mapper = createObjectMapper();
mapper.writeValue(new File(MODELKIT_JSON), configs);
reload();
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
static ObjectMapper createObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
return customizeObjectMapper(mapper);
}
static ObjectMapper customizeObjectMapper(ObjectMapper mapper) {
mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker()
.withFieldVisibility(JsonAutoDetect.Visibility.ANY)
.withGetterVisibility(JsonAutoDetect.Visibility.NONE)
.withSetterVisibility(JsonAutoDetect.Visibility.NONE)
.withCreatorVisibility(JsonAutoDetect.Visibility.NONE));
return mapper.findAndRegisterModules();
}
@Scheduled(cron = "{modelkit.refresh.from.json.file}")
void scheduledRefreshFromJson() {
if (lastJsonLookupTime.get() > 0 && new File(MODELKIT_JSON).lastModified() > lastJsonLookupTime.get()) {
ModelKitLoader.reload();
}
}
static void reload() {
configurations = loadConfigurations();
}
static void reset() {
configurations = null;
}
static void resetAndDelete() {
new File(MODELKIT_JSON).delete();
reset();
}
/**
* Needed to fill the generic attribute 'env' of type LinkedHashMap. The value lists are of different types.
*/
public static class JsonToMapConverter extends StdConverter
© 2015 - 2025 Weber Informatics LLC | Privacy Policy