net.n2oapp.framework.engine.data.json.TestDataProviderEngine Maven / Gradle / Ivy
The newest version!
package net.n2oapp.framework.engine.data.json;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import net.n2oapp.criteria.dataset.DataSet;
import net.n2oapp.criteria.dataset.NestedList;
import net.n2oapp.framework.api.data.MapInvocationEngine;
import net.n2oapp.framework.api.exception.N2oException;
import net.n2oapp.framework.api.metadata.dataprovider.N2oTestDataProvider;
import net.n2oapp.framework.api.metadata.dataprovider.N2oTestDataProvider.PrimaryKeyType;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import jakarta.servlet.http.HttpSession;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.ChronoLocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static net.n2oapp.framework.api.metadata.dataprovider.N2oTestDataProvider.PrimaryKeyType.integer;
/**
* Тестовый провайдер данных из json файла
*/
public class TestDataProviderEngine implements MapInvocationEngine, ResourceLoaderAware {
/**
* Путь к файлу для чтения с диска
*/
private String pathOnDisk;
/**
* Путь к ресурсу в classpath
*/
private String classpathResourcePath;
/**
* Обновление данных в файле на диске
*/
private boolean readonly;
private final Map> repository = new ConcurrentHashMap<>();
private ResourceLoader resourceLoader = new DefaultResourceLoader();
private final Map sequences = new ConcurrentHashMap<>();
private ObjectMapper objectMapper;
public TestDataProviderEngine() {
objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule());
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
}
@Override
public Class extends N2oTestDataProvider> getType() {
return N2oTestDataProvider.class;
}
@Override
public Object invoke(N2oTestDataProvider invocation, Map inParams) {
return execute(invocation, inParams, getData(invocation));
}
public void deleteSessionDataSets(HttpSession session) {
for (Map.Entry> entry : repository.entrySet()) {
if (session.getId().equals(entry.getKey().split("/")[0]))
repository.remove(entry.getKey());
}
}
protected synchronized List getData(N2oTestDataProvider invocation) {
if (invocation.getFile() == null)
return new ArrayList<>();
boolean isInit = getRepositoryData(invocation.getFile()) == null;
if (isInit || (!readonly && fileExistsOnDisk(invocation.getFile()))) {
initRepository(invocation);
}
return repository.get(richKey(invocation.getFile()));
}
protected List getRepositoryData(String key) {
return repository.get(richKey(key));
}
/**
* Заполняет хранилище данных из файла
*/
protected void initRepository(N2oTestDataProvider invocation) {
try {
InputStream inputStream = getResourceInputStream(invocation);
List data = loadJson(inputStream, invocation.getPrimaryKeyType(), invocation.getPrimaryKey());
repository.put(richKey(invocation.getFile()), data);
if (integer.equals(invocation.getPrimaryKeyType())) {
long maxId = data
.stream()
.filter(v -> v.get(invocation.getPrimaryKey()) != null)
.mapToLong(v -> (Long) v.get(invocation.getPrimaryKey()))
.max().orElse(0);
sequences.put(richKey(invocation.getFile()), new AtomicLong(maxId));
}
} catch (IOException e) {
throw new N2oException(e);
}
}
protected InputStream getResourceInputStream(N2oTestDataProvider invocation) throws IOException {
String path = getFullResourcePath(invocation.getFile());
if (fileExistsOnDisk(invocation.getFile())) {
path = "file:" + getFullPathOnDisk(invocation.getFile());
}
return resourceLoader.getResource(path).getInputStream();
}
protected String richKey(String key) {
if (classpathResourcePath != null) return classpathResourcePath + "/" + key;
if (pathOnDisk != null) return pathOnDisk + "/" + key;
return key;
}
/**
* Обновляет содержимое файла на диске
*
* @param filename Имя файла
*/
protected void updateFile(String filename) {
if (!readonly && fileExistsOnDisk(filename)) {
try (FileWriter fileWriter = new FileWriter(getFullPathOnDisk(filename))) {
String mapAsJson = objectMapper.writeValueAsString(getRepositoryData(filename));
fileWriter.write(mapAsJson);
} catch (IOException e) {
throw new N2oException(e);
}
}
}
private Object execute(N2oTestDataProvider invocation,
Map inParams,
List data) {
if (invocation.getOperation() == null)
return findAll(inParams, data);
switch (invocation.getOperation()) {
case create:
return create(invocation, inParams, data);
case findAll:
return findAll(inParams, data);
case findOne:
return findOne(inParams, data);
case update:
return update(invocation, inParams, data);
case updateMany:
return updateMany(invocation, inParams, data);
case updateField:
return updateField(invocation, inParams, data);
case delete:
return delete(invocation, inParams, data);
case deleteMany:
return deleteMany(invocation, inParams, data);
case count:
return count(inParams, data);
case echo:
return inParams;
}
throw new N2oException("Unsupported invocation's operation");
}
private List findAll(Map inParams, List data) {
List sortings = (List) inParams.get("sorting");
List filters = (List) inParams.get("filters");
Integer limit = (Integer) inParams.get("limit");
Integer offset = (Integer) inParams.get("offset");
Integer count = (Integer) inParams.get("count");
List modifiableData = new ArrayList<>(data);
modifiableData = filter(filters, inParams, modifiableData);
sort(sortings, inParams, modifiableData);
return paginate(limit, offset, count, modifiableData);
}
private DataSet findOne(Map inParams, List data) {
List filters = (List) inParams.get("filters");
if (filters == null)
filters = inParams.keySet().stream().map(k -> k + " :eq :" + k).collect(Collectors.toList());
List modifiableData = new ArrayList<>(data);
modifiableData = filter(filters, inParams, modifiableData);
return modifiableData.isEmpty() ? null : modifiableData.get(0);
}
private int count(Map inParams,
List data) {
List filters = (List) inParams.get("filters");
List modifiableData = new ArrayList<>(data);
return filter(filters, inParams, modifiableData).size();
}
private Object create(N2oTestDataProvider invocation,
Map inParams,
List data) {
List modifiableData = new ArrayList<>(data);
DataSet newElement = new DataSet();
newElement.put(invocation.getPrimaryKey(), generateKey(invocation.getPrimaryKeyType(), invocation.getFile()));
updateElement(newElement, inParams.entrySet());
modifiableData.add(0, newElement);
updateRepository(invocation.getFile(), modifiableData);
updateFile(invocation.getFile());
return newElement;
}
private Object update(N2oTestDataProvider invocation,
Map inParams,
List data) {
List modifiableData = new ArrayList<>(data);
if (inParams.get(invocation.getPrimaryKey()) == null)
throw new N2oException("Id is required for operation \"update\"");
DataSet element = modifiableData
.stream()
.filter(buildPredicate(invocation.getPrimaryKeyType(), invocation.getPrimaryKey(), inParams))
.findFirst()
.orElseThrow(() -> new N2oException("No such element"));
updateElement(element, inParams.entrySet());
updateRepository(invocation.getFile(), modifiableData);
updateFile(invocation.getFile());
return element;
}
private Object updateMany(N2oTestDataProvider invocation,
Map inParams,
List data) {
List modifiableData = new ArrayList<>(data);
if (inParams.get(invocation.getPrimaryKeys()) == null)
throw new N2oException("Ids is required for operation \"updateMany\"");
List elements = modifiableData.stream()
.filter(buildListPredicate(invocation.getPrimaryKeyType(), invocation.getPrimaryKey(),
invocation.getPrimaryKeys(), inParams))
.collect(Collectors.toList());
Map fields = new HashMap<>(inParams);
fields.remove(invocation.getPrimaryKeys());
for (DataSet element : elements) {
updateElement(element, fields.entrySet());
}
updateRepository(invocation.getFile(), modifiableData);
updateFile(invocation.getFile());
return modifiableData;
}
private Object updateField(N2oTestDataProvider invocation,
Map inParams,
List data) {
List modifiableData = new ArrayList<>(data);
if (inParams.get(invocation.getPrimaryKey()) == null)
throw new N2oException("Id is required for operation \"updateField\"");
if (!inParams.containsKey("key") || !inParams.containsKey("value"))
throw new N2oException("Should contains parameters \"key\", \"value\" for operation \"updateField\"");
DataSet element = modifiableData
.stream()
.filter(buildPredicate(invocation.getPrimaryKeyType(), invocation.getPrimaryKey(), inParams))
.findFirst()
.orElseThrow(() -> new N2oException("No such element"));
Map fieldData = new HashMap<>();
fieldData.put(invocation.getPrimaryKey(), inParams.get(invocation.getPrimaryKey()));
fieldData.put((String) inParams.get("key"), inParams.get("value"));
if (inParams.containsKey("key2") && inParams.get("key2") != null)
fieldData.put((String) inParams.get("key2"), inParams.get("value2"));
updateElement(element, fieldData.entrySet());
updateRepository(invocation.getFile(), modifiableData);
updateFile(invocation.getFile());
return null;
}
private Object delete(N2oTestDataProvider invocation,
Map inParams,
List data) {
List modifiableData = new ArrayList(data);
if (inParams.get(invocation.getPrimaryKey()) == null)
throw new N2oException("Id is required for operation \"delete\"");
modifiableData.removeIf(buildPredicate(invocation.getPrimaryKeyType(), invocation.getPrimaryKey(), inParams));
updateRepository(invocation.getFile(), modifiableData);
updateFile(invocation.getFile());
return null;
}
private Object deleteMany(N2oTestDataProvider invocation,
Map inParams,
List data) {
List modifiableData = new ArrayList(data);
if (inParams.get(invocation.getPrimaryKeys()) == null)
throw new N2oException("Ids is required for operation \"deleteMany\"");
modifiableData.removeIf(buildListPredicate(invocation.getPrimaryKeyType(), invocation.getPrimaryKey(),
invocation.getPrimaryKeys(), inParams));
updateRepository(invocation.getFile(), modifiableData);
updateFile(invocation.getFile());
return null;
}
private static Predicate buildPredicate(PrimaryKeyType primaryKeyType, String primaryKeyFieldId, Map data) {
if (integer.equals(primaryKeyType))
return obj -> ((Number) data.get(primaryKeyFieldId)).longValue() == ((Number) obj.get(primaryKeyFieldId)).longValue();
else
return obj -> data.get(primaryKeyFieldId).equals(obj.get(primaryKeyFieldId));
}
private static Predicate buildListPredicate(PrimaryKeyType primaryKeyType, String primaryKeyFieldId, String primaryKeysFieldId, Map data) {
if (integer.equals(primaryKeyType)) {
Set list = (Set) ((List) data.get(primaryKeysFieldId)).stream()
.map(o -> ((Number) o).longValue()).collect(Collectors.toSet());
return obj -> list.contains(((Number) obj.get(primaryKeyFieldId)).longValue());
} else {
return obj -> ((List) data.get(primaryKeysFieldId)).contains((obj.get(primaryKeyFieldId)));
}
}
private Object generateKey(PrimaryKeyType primaryKeyType, String fileName) {
if (integer.equals(primaryKeyType)) {
return sequences.get(richKey(fileName)).incrementAndGet();
} else {
return UUID.randomUUID().toString();
}
}
private void updateElement(Map element, Set> fields) {
for (Map.Entry field : fields)
if (field.getValue() instanceof Date)
element.put(field.getKey(), new SimpleDateFormat("dd.MM.yyy HH:mm:ss").format(field.getValue()));
else
element.put(field.getKey(), field.getValue());
}
private List paginate(Integer limit, Integer offset, Integer count, List data) {
Boolean unique = count != null && count.equals(1);
if (limit != null && offset != null && !unique) {
data = data
.stream()
.limit(limit + offset)
.skip(offset).collect(Collectors.toList());
}
return data;
}
private List filter(List filters, Map inParams, List data) {
if (filters == null || filters.isEmpty()) {
if (inParams != null && inParams.containsKey("id")) {
String id = "" + inParams.get("id");
data = data.stream().filter(m -> m.getId().equals(id)).collect(Collectors.toList());
}
return data;
}
for (String filter : filters) {
String[] splittedFilter = filter.replaceAll("[\\s]", "").split(":");
String field = splittedFilter[0];
Object pattern = inParams.get(splittedFilter[2]);
switch (splittedFilter[1]) {
case "eq":
data = eqFilterData(field, pattern, data);
break;
case "notEq":
data = notEqFilterData(field, pattern, data);
break;
case "like":
data = likeFilterData(field, pattern, data);
break;
case "in":
data = inFilterData(field, pattern, data);
break;
case "notIn":
data = notInFilterData(field, pattern, data);
break;
case "more":
data = moreFilterData(field, pattern, data);
break;
case "less":
data = lessFilterData(field, pattern, data);
break;
case "isNull":
data = isNullFilterData(field, data);
break;
case "isNotNull":
data = isNotNullFilterData(field, data);
break;
case "eqOrIsNull":
data = eqOrIsNullFilterData(field, pattern, data);
break;
case "contains":
data = containsFilterData(field, pattern, data);
break;
default:
throw new N2oException("Wrong filter type!");
}
}
return data;
}
private List eqFilterData(String field, Object pattern, List data) {
if (pattern != null) {
data = data
.stream()
.filter(m -> {
if (!m.containsKey(field) || m.get(field) == null)
return false;
if (m.get(field) instanceof Number && pattern instanceof Number)
return ((Long) ((Number) m.get(field)).longValue()).equals(((Number) pattern).longValue());
return m.get(field).toString().equals(pattern.toString());
})
.collect(Collectors.toList());
}
return data;
}
private List notEqFilterData(String field, Object pattern, List data) {
if (pattern != null) {
data = data
.stream()
.filter(m -> {
if (!m.containsKey(field) || m.get(field) == null)
return false;
if (m.get(field) instanceof Number && pattern instanceof Number)
return !((Long) ((Number) m.get(field)).longValue()).equals(((Number) pattern).longValue());
return !m.get(field).toString().equals(pattern.toString());
})
.collect(Collectors.toList());
}
return data;
}
private List likeFilterData(String field, Object pattern, List data) {
if (pattern != null) {
data = data
.stream()
.filter(m -> {
if (!m.containsKey(field) || m.get(field) == null)
return false;
return m.get(field).toString().toLowerCase()
.contains(pattern.toString().toLowerCase());
})
.collect(Collectors.toList());
}
return data;
}
private List inFilterData(String field, Object pattern, List data) {
List patterns = pattern instanceof List ? (List) pattern : Arrays.asList(pattern);
if (patterns != null) {
String[] splittedField = field.split("\\.");
String parent = splittedField[0];
String child = splittedField.length > 1 ? splittedField[1] : null;
data = data
.stream()
.filter(m -> {
if (child != null) {
if (!m.containsKey(parent))
return false;
if (m.get(parent) instanceof NestedList) {
return ((NestedList)m.get(parent)).stream().anyMatch(c ->
((DataSet) c).containsKey(child) && patterns.contains(((DataSet) c).get(child))
);
} else {
return m.containsKey(field) && patterns.contains(m.get(field));
}
}
if (m.get(field) instanceof Number) {
List longPatterns = new ArrayList<>();
patterns.forEach(p -> longPatterns.add(((Number) p).longValue()));
return longPatterns.contains(((Number) m.get(field)).longValue());
}
for (Object p : patterns) {
if (p.toString().equals(m.get(field).toString()))
return true;
}
return false;
})
.collect(Collectors.toList());
}
return data;
}
private List notInFilterData(String field, Object pattern, List data) {
List patterns = pattern instanceof List ? (List) pattern : Arrays.asList(pattern);
if (patterns != null) {
String[] splittedField = field.split("\\.");
String parent = splittedField[0];
String child = splittedField.length > 1 ? splittedField[1] : null;
data = data
.stream()
.filter(m -> {
if (child != null) {
if (!m.containsKey(parent))
return false;
if (m.get(parent) instanceof NestedList) {
return ((NestedList)m.get(parent)).stream().anyMatch(c ->
((DataSet) c).containsKey(child) && !patterns.contains(((DataSet) c).get(child))
);
} else {
return m.containsKey(field) && !patterns.contains(m.get(field));
}
}
if (m.get(field) instanceof Number) {
List longPatterns = new ArrayList<>();
patterns.forEach(p -> longPatterns.add(((Number) p).longValue()));
return !longPatterns.contains(((Number) m.get(field)).longValue());
}
return !patterns.contains(m.get(field).toString());
})
.collect(Collectors.toList());
}
return data;
}
private List containsFilterData(String field, Object pattern, List data) {
List> patterns = pattern instanceof List ? (List>) pattern : Collections.singletonList(pattern);
if (patterns.isEmpty())
return data;
List splittedField = new ArrayList<>(Arrays.asList(field.split("\\.")));
if (splittedField.size() == 1) {
return data
.stream()
.filter(m -> {
if (!m.containsKey(splittedField.get(0)) || !(m.get(splittedField.get(0)) instanceof List))
return false;
return m.getList(splittedField.get(0)).containsAll(patterns);
})
.collect(Collectors.toList());
}
String indexedField = splittedField.remove(splittedField.size() - 1);
String parentField = String.join(".", splittedField);
return data
.stream()
.filter(m -> {
if (!m.containsKey(parentField) || !(m.get(parentField) instanceof List))
return false;
List