From 7982cad794dbf712ab09eef5d14a03d03a6be99a Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 23 Dec 2024 11:29:30 -0600 Subject: [PATCH] Initial build of classes to load meta-data from yaml or json files --- .../loaders/AbstractMetaDataLoader.java | 482 ++++++++++++++++++ .../loaders/ClassDetectingMetaDataLoader.java | 84 +++ .../loaders/MetaDataLoaderHelper.java | 111 ++++ .../loaders/QMetaDataLoaderException.java | 50 ++ .../implementations/QTableMetaDataLoader.java | 57 +++ .../core/model/metadata/QMetaDataObject.java | 34 ++ .../ClassDetectingMetaDataLoaderTest.java | 92 ++++ .../loaders/QTableMetaDataLoaderTest.java | 177 +++++++ 8 files changed, 1087 insertions(+) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoader.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoader.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelper.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/QMetaDataLoaderException.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QTableMetaDataLoader.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMetaDataObject.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoaderTest.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QTableMetaDataLoaderTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoader.java new file mode 100644 index 00000000..3539bca5 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoader.java @@ -0,0 +1,482 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import com.fasterxml.jackson.core.type.TypeReference; +import com.kingsrook.qqq.backend.core.instances.loaders.implementations.QTableMetaDataLoader; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; +import com.kingsrook.qqq.backend.core.utils.ClassPathUtils; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import com.kingsrook.qqq.backend.core.utils.YamlUtils; +import org.apache.commons.io.IOUtils; +import static com.kingsrook.qqq.backend.core.utils.ValueUtils.getValueAsBoolean; +import static com.kingsrook.qqq.backend.core.utils.ValueUtils.getValueAsInteger; +import static com.kingsrook.qqq.backend.core.utils.ValueUtils.getValueAsString; + + +/******************************************************************************* + ** Abstract base class in hierarchy of classes that know how to construct & + ** populate QMetaDataObject instances, based on input streams (e.g., from files). + *******************************************************************************/ +public abstract class AbstractMetaDataLoader +{ + private static final QLogger LOG = QLogger.getLogger(AbstractMetaDataLoader.class); + + private static Map, Class>> registeredLoaders = new HashMap<>(); + + static + { + try + { + List> classesInPackage = ClassPathUtils.getClassesInPackage(QTableMetaDataLoader.class.getPackageName()); + for(Class loaderClass : classesInPackage) + { + Type superClass = loaderClass.getGenericSuperclass(); + if(superClass.getTypeName().startsWith(AbstractMetaDataLoader.class.getName() + "<")) + // if(superClass instanceof Class c && AbstractMetaDataLoader.class.isAssignableFrom(c)) + { + Type actualTypeArgument = ((ParameterizedType) superClass).getActualTypeArguments()[0]; + Class metaDataObjectType = Class.forName(actualTypeArgument.getTypeName()); + registeredLoaders.put(metaDataObjectType, (Class>) loaderClass); + } + } + + System.out.println("Registered loaders: " + registeredLoaders); + } + catch(Exception e) + { + LOG.error("Error in static init block for AbstractMetaDataLoader", e); + } + } + + private String fileName; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public T fileToMetaDataObject(QInstance qInstance, InputStream inputStream, String fileName) throws QMetaDataLoaderException + { + this.fileName = fileName; + Map map = fileToMap(inputStream, fileName); + return (mapToMetaDataObject(qInstance, map)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public abstract T mapToMetaDataObject(QInstance qInstance, Map map) throws QMetaDataLoaderException; + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected Map fileToMap(InputStream inputStream, String fileName) throws QMetaDataLoaderException + { + try + { + String string = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + string = StringUtils.ltrim(string); + if(fileName.toLowerCase().endsWith(".json")) + { + return JsonUtils.toObject(string, new TypeReference<>() {}); + } + else if(fileName.toLowerCase().endsWith(".yaml") || fileName.toLowerCase().endsWith(".yml")) + { + return YamlUtils.toMap(string); + } + + throw (new QMetaDataLoaderException("Unsupported file format (based on file name: " + fileName + ")")); + } + catch(IOException e) + { + throw new QMetaDataLoaderException("Error building map from file: " + fileName, e); + } + } + + + + /*************************************************************************** + * + ***************************************************************************/ + protected void reflectivelyMap(QInstance qInstance, QMetaDataObject targetObject, Map map) + { + Class targetClass = targetObject.getClass(); + + for(Method method : targetClass.getMethods()) + { + try + { + if(method.getName().startsWith("set") && method.getParameterTypes().length == 1) + { + String propertyName = StringUtils.lcFirst(method.getName().substring(3)); + + if(map.containsKey(propertyName)) + { + Class parameterType = method.getParameterTypes()[0]; + Object rawValue = map.get(propertyName); + + try + { + Object mappedValue = reflectivelyMapValue(qInstance, method, parameterType, rawValue); + method.invoke(targetObject, mappedValue); + } + catch(NoValueException nve) + { + /////////////////////// + // don't call setter // + /////////////////////// + } + } + } + } + catch(Exception e) + { + LOG.warn("Error reflectively mapping on " + targetClass.getName() + "." + method.getName(), e); + } + } + } + + + + /*************************************************************************** + * + ***************************************************************************/ + private Object reflectivelyMapValue(QInstance qInstance, Method method, Class parameterType, Object rawValue) throws Exception + { + if(parameterType.equals(String.class)) + { + return (getValueAsString(rawValue)); + } + else if(parameterType.equals(Integer.class)) + { + return (getValueAsInteger(rawValue)); + } + else if(parameterType.equals(Boolean.class)) + { + return (getValueAsBoolean(rawValue)); + } + else if(parameterType.equals(boolean.class)) + { + Boolean valueAsBoolean = getValueAsBoolean(rawValue); + if(valueAsBoolean != null) + { + return (valueAsBoolean); + } + } + else if(parameterType.equals(List.class)) + { + Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0]; + Class actualTypeClass = Class.forName(actualTypeArgument.getTypeName()); + + Object value = rawValue; + if(value instanceof List valueList) + { + List mappedValueList = new ArrayList<>(); + for(Object o : valueList) + { + try + { + Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, o); + mappedValueList.add(mappedValue); + } + catch(NoValueException nve) + { + // leave off list + } + } + return (mappedValueList); + } + } + else if(parameterType.equals(Set.class)) + { + Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0]; + Class actualTypeClass = Class.forName(actualTypeArgument.getTypeName()); + + Object value = rawValue; + if(value instanceof List valueList) + { + Set mappedValueSet = new LinkedHashSet<>(); + for(Object o : valueList) + { + try + { + Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, o); + mappedValueSet.add(mappedValue); + } + catch(NoValueException nve) + { + // leave off list + } + } + return (mappedValueSet); + } + } + else if(parameterType.equals(Map.class)) + { + Type keyType = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0]; + if(!keyType.equals(String.class)) + { + LOG.warn("Unsupported key type for " + method + " (" + keyType + ")"); + throw new NoValueException(); + } + // todo make sure string + + Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[1]; + Class actualTypeClass = Class.forName(actualTypeArgument.getTypeName()); + + Object value = rawValue; + if(value instanceof Map valueMap) + { + Map mappedValueMap = new LinkedHashMap<>(); + for(Object o : valueMap.entrySet()) + { + try + { + Map.Entry entry = (Map.Entry) o; + Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, entry.getValue()); + mappedValueMap.put(entry.getKey(), mappedValue); + } + catch(NoValueException nve) + { + // leave out of map + } + } + return (mappedValueMap); + } + } + else if(parameterType.isEnum()) + { + String value = getValueAsString(rawValue); + for(Object enumConstant : parameterType.getEnumConstants()) + { + if(((Enum) enumConstant).name().equals(value)) + { + return (enumConstant); + } + } + } + else if(registeredLoaders.containsKey(parameterType)) + { + Object value = rawValue; + if(value instanceof Map valueMap) + { + Class> loaderClass = registeredLoaders.get(parameterType); + AbstractMetaDataLoader loader = loaderClass.getConstructor().newInstance(); + QMetaDataObject loadedValue = loader.mapToMetaDataObject(qInstance, valueMap); + return (loadedValue); + } + } + else if(QMetaDataObject.class.isAssignableFrom(parameterType)) + { + Object value = rawValue; + if(value instanceof Map valueMap) + { + QMetaDataObject childObject = (QMetaDataObject) parameterType.getConstructor().newInstance(); + reflectivelyMap(qInstance, childObject, valueMap); + return (childObject); + } + } + else if(parameterType.equals(Serializable.class)) + { + if(rawValue instanceof String + || rawValue instanceof Integer + || rawValue instanceof BigDecimal + || rawValue instanceof Boolean + ) + { + return rawValue; + } + } + else + { + // todo clean up this message/level + LOG.warn("No case for " + parameterType + " (arg to: " + method + ")"); + } + + throw new NoValueException(); + } + + + + /*************************************************************************** + * + ***************************************************************************/ + protected ListOfMapOrMapOfMap getListOfMapOrMapOfMap(Map map, String key) + { + if(map.containsKey(key)) + { + if(map.get(key) instanceof List) + { + return (new ListOfMapOrMapOfMap((List>) map.get(key))); + } + else if(map.get(key) instanceof Map) + { + return (new ListOfMapOrMapOfMap((Map>) map.get(key))); + } + else + { + LOG.warn("Expected list or map under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName())); + } + } + + return (null); + } + + + + /*************************************************************************** + * + ***************************************************************************/ + protected List> getListOfMap(Map map, String key) + { + if(map.containsKey(key)) + { + if(map.get(key) instanceof List) + { + return (List>) map.get(key); + } + else + { + LOG.warn("Expected list under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName())); + } + } + + return (null); + } + + + + /*************************************************************************** + * + ***************************************************************************/ + protected Map> getMapOfMap(Map map, String key) + { + if(map.containsKey(key)) + { + if(map.get(key) instanceof Map) + { + return (Map>) map.get(key); + } + else + { + LOG.warn("Expected map under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName())); + } + } + + return (null); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected record ListOfMapOrMapOfMap(List> listOf, Map> mapOf) + { + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ListOfMapOrMapOfMap(List> listOf) + { + this(listOf, null); + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ListOfMapOrMapOfMap(Map> mapOf) + { + this(null, mapOf); + } + + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected void warnNotImplemented(Map map, String key) + { + if(StringUtils.hasContent(ValueUtils.getValueAsString(map.get(key)))) + { + LOG.warn("Unsupported meta-data attribute [" + key + "] found while processing [" + getClass().getSimpleName() + "] from [" + fileName + "]"); + } + } + + + + /******************************************************************************* + ** Getter for fileName + ** + *******************************************************************************/ + public String getFileName() + { + return fileName; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private static class NoValueException extends Exception + { + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public NoValueException() + { + super("No value"); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoader.java new file mode 100644 index 00000000..e62d8ece --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoader.java @@ -0,0 +1,84 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +import java.io.InputStream; +import java.util.Map; +import com.kingsrook.qqq.backend.core.instances.loaders.implementations.QTableMetaDataLoader; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; + + +/******************************************************************************* + ** Generic implementation of AbstractMetaDataLoader, who "detects" the class + ** of meta data object to be created, then defers to an appropriate subclass + ** to do the work. + *******************************************************************************/ +public class ClassDetectingMetaDataLoader extends AbstractMetaDataLoader +{ + + /*************************************************************************** + * + ***************************************************************************/ + public AbstractMetaDataLoader getLoaderForFile(InputStream inputStream, String fileName) throws QMetaDataLoaderException + { + Map map = fileToMap(inputStream, fileName); + return (getLoaderForMap(map)); + } + + + + /*************************************************************************** + * + ***************************************************************************/ + public AbstractMetaDataLoader getLoaderForMap(Map map) throws QMetaDataLoaderException + { + if(map.containsKey("class")) + { + String _class = ValueUtils.getValueAsString(map.get("class")); + AbstractMetaDataLoader loader = switch(_class) + { + case "QTableMetaData" -> new QTableMetaDataLoader(); + // todo!! case "QTableMetaData" -> new QTableMetaDataLoader(); + default -> throw new QMetaDataLoaderException("Unexpected class [" + _class + "] specified in " + getFileName()); + }; + + return (loader); + } + + throw new QMetaDataLoaderException("Cannot detect meta-data type, because [class] attribute was not specified in file: " + getFileName()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QMetaDataObject mapToMetaDataObject(QInstance qInstance, Map map) throws QMetaDataLoaderException + { + AbstractMetaDataLoader loaderForMap = getLoaderForMap(map); + return loaderForMap.mapToMetaDataObject(qInstance, map); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelper.java new file mode 100644 index 00000000..a872c04d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelper.java @@ -0,0 +1,111 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +import java.io.File; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; +import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface; +import com.kingsrook.qqq.backend.core.utils.Pair; + + +/******************************************************************************* + ** class that loads a directory full of meta data files into meta data objects, + ** and then sets all of them in a QInstance. + *******************************************************************************/ +public class MetaDataLoaderHelper +{ + private static final QLogger LOG = QLogger.getLogger(MetaDataLoaderHelper.class); + + + + /*************************************************************************** + * + ***************************************************************************/ + public static void processAllMetaDataFilesInDirectory(QInstance qInstance, String path) throws QException + { + List>> loaders = new ArrayList<>(); + + File directory = new File(path); + processAllMetaDataFilesInDirectory(loaders, directory); + + // todo - some version of sorting the loaders by type or possibly a sort field within the files (or file names) + + for(Pair> pair : loaders) + { + File file = pair.getA(); + AbstractMetaDataLoader loader = pair.getB(); + try(FileInputStream fileInputStream = new FileInputStream(file)) + { + QMetaDataObject qMetaDataObject = loader.fileToMetaDataObject(qInstance, fileInputStream, file.getName()); + if(qMetaDataObject instanceof TopLevelMetaDataInterface topLevelMetaData) + { + topLevelMetaData.addSelfToInstance(qInstance); + } + else + { + LOG.warn("Received a non-topLevelMetaDataObject from file: " + file.getAbsolutePath()); + } + } + catch(Exception e) + { + LOG.error("Error processing file: " + file.getAbsolutePath(), e); + } + } + } + + + + /*************************************************************************** + * + ***************************************************************************/ + private static void processAllMetaDataFilesInDirectory(List>> loaders, File directory) throws QException + { + for(File file : Objects.requireNonNullElse(directory.listFiles(), new File[0])) + { + if(file.isDirectory()) + { + processAllMetaDataFilesInDirectory(loaders, file); + } + else + { + try(FileInputStream fileInputStream = new FileInputStream(file)) + { + AbstractMetaDataLoader loader = new ClassDetectingMetaDataLoader().getLoaderForFile(fileInputStream, file.getName()); + loaders.add(Pair.of(file, loader)); + } + catch(Exception e) + { + LOG.error("Error processing file: " + file.getAbsolutePath(), e); + } + } + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/QMetaDataLoaderException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/QMetaDataLoaderException.java new file mode 100644 index 00000000..24a3885d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/QMetaDataLoaderException.java @@ -0,0 +1,50 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QMetaDataLoaderException extends Exception +{ + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QMetaDataLoaderException(String message) + { + super(message); + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QMetaDataLoaderException(String message, Throwable cause) + { + super(message, cause); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QTableMetaDataLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QTableMetaDataLoader.java new file mode 100644 index 00000000..963fe93b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QTableMetaDataLoader.java @@ -0,0 +1,57 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders.implementations; + + +import java.util.Map; +import com.kingsrook.qqq.backend.core.instances.loaders.AbstractMetaDataLoader; +import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QTableMetaDataLoader extends AbstractMetaDataLoader +{ + private static final QLogger LOG = QLogger.getLogger(QTableMetaDataLoader.class); + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QTableMetaData mapToMetaDataObject(QInstance qInstance, Map map) throws QMetaDataLoaderException + { + QTableMetaData table = new QTableMetaData(); + + reflectivelyMap(qInstance, table, map); + + // todo - handle QTableBackendDetails, based on backend's type + + return (table); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMetaDataObject.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMetaDataObject.java new file mode 100644 index 00000000..aeec0e2b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMetaDataObject.java @@ -0,0 +1,34 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata; + + +import java.io.Serializable; + + +/******************************************************************************* + ** interface common among all objects that can be considered qqq meta data - + ** e.g., stored in a QInstance. + *******************************************************************************/ +public interface QMetaDataObject extends Serializable +{ +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoaderTest.java new file mode 100644 index 00000000..7eaceb68 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoaderTest.java @@ -0,0 +1,92 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +import java.nio.charset.StandardCharsets; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for ClassDetectingMetaDataLoader + *******************************************************************************/ +class ClassDetectingMetaDataLoaderTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBasicSuccess() throws QMetaDataLoaderException + { + QMetaDataObject qMetaDataObject = new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + class: QTableMetaData + version: 1 + name: myTable + """, StandardCharsets.UTF_8), "myTable.yaml"); + + assertThat(qMetaDataObject).isInstanceOf(QTableMetaData.class); + QTableMetaData qTableMetaData = (QTableMetaData) qMetaDataObject; + assertEquals("myTable", qTableMetaData.getName()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testUnknownClassFails() + { + assertThatThrownBy(() -> new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + class: ya whatever + version: 1 + name: myTable + """, StandardCharsets.UTF_8), "whatever.yaml")) + .isInstanceOf(QMetaDataLoaderException.class) + .hasMessageContaining("Unexpected class"); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMissingClassAttributeFails() + { + assertThatThrownBy(() -> new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + version: 1 + name: myTable + """, StandardCharsets.UTF_8), "aTable.yaml")) + .isInstanceOf(QMetaDataLoaderException.class) + .hasMessageContaining("[class] attribute was not specified"); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QTableMetaDataLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QTableMetaDataLoaderTest.java new file mode 100644 index 00000000..ad7992a6 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QTableMetaDataLoaderTest.java @@ -0,0 +1,177 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +import java.nio.charset.StandardCharsets; +import java.util.List; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.instances.loaders.implementations.QTableMetaDataLoader; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import com.kingsrook.qqq.backend.core.utils.YamlUtils; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +/******************************************************************************* + ** Unit test for QTableMetaDataLoader + *******************************************************************************/ +class QTableMetaDataLoaderTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testToYaml() throws QMetaDataLoaderException + { + QTableMetaData expectedTable = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + String expectedYaml = YamlUtils.toYaml(expectedTable); + QTableMetaData actualTable = new QTableMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(expectedYaml, StandardCharsets.UTF_8), "person.yaml"); + String actualYaml = YamlUtils.toYaml(actualTable); + assertEquals(expectedYaml, actualYaml); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testYaml() throws QMetaDataLoaderException + { + QTableMetaData table = new QTableMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + class: QTableMetaData + version: 1.0 + name: myTable + icon: + name: account_tree + fields: + id: + name: id + type: INTEGER + name: + name: name + type: STRING + uniqueKeys: + - label: Name + fieldNames: + - name + associations: + - name: A1 + associatedTableName: yourTable + joinName: myTableJoinYourTable + - name: A2 + associatedTableName: theirTable + joinName: myTableJoinTheirTable + permissionRules: + level: READ_WRITE_PERMISSIONS + denyBehavior: HIDDEN + permissionBaseName: myTablePermissions + customPermissionChecker: + name: com.kingsrook.SomeChecker + codeType: JAVA + ## todo recordSecurityLocks + ## todo auditRules + ## todo backendDetails + ## todo automationDetails + customizers: + postQueryRecord: + name: com.kingsrook.SomePostQuery + codeType: JAVA + preDeleteRecord: + name: com.kingsrook.SomePreDelete + codeType: JAVA + """, StandardCharsets.UTF_8), "myTable.yaml"); + + assertEquals("myTable", table.getName()); + + assertEquals(2, table.getFields().size()); + // assertEquals("id", table.getFields().get("id").getName()); + assertEquals(QFieldType.INTEGER, table.getFields().get("id").getType()); + // assertEquals("name", table.getFields().get("name").getName()); + assertEquals(QFieldType.STRING, table.getFields().get("name").getType()); + + assertNotNull(table.getIcon()); + assertEquals("account_tree", table.getIcon().getName()); + + assertEquals(1, table.getUniqueKeys().size()); + assertEquals(List.of("name"), table.getUniqueKeys().get(0).getFieldNames()); + assertEquals("Name", table.getUniqueKeys().get(0).getLabel()); + + assertEquals(2, table.getAssociations().size()); + assertEquals("A1", table.getAssociations().get(0).getName()); + assertEquals("theirTable", table.getAssociations().get(1).getAssociatedTableName()); + + assertNotNull(table.getPermissionRules()); + assertEquals(PermissionLevel.READ_WRITE_PERMISSIONS, table.getPermissionRules().getLevel()); + assertEquals(DenyBehavior.HIDDEN, table.getPermissionRules().getDenyBehavior()); + assertEquals("myTablePermissions", table.getPermissionRules().getPermissionBaseName()); + assertNotNull(table.getPermissionRules().getCustomPermissionChecker()); + assertEquals("com.kingsrook.SomeChecker", table.getPermissionRules().getCustomPermissionChecker().getName()); + assertEquals(QCodeType.JAVA, table.getPermissionRules().getCustomPermissionChecker().getCodeType()); + + assertEquals(2, table.getCustomizers().size()); + assertEquals("com.kingsrook.SomePostQuery", table.getCustomizers().get(TableCustomizers.POST_QUERY_RECORD.getRole()).getName()); + assertEquals("com.kingsrook.SomePreDelete", table.getCustomizers().get(TableCustomizers.PRE_DELETE_RECORD.getRole()).getName()); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSimpleJson() throws QMetaDataLoaderException + { + QTableMetaData table = new QTableMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + { + "class": "QTableMetaData", + "version": "1.0", + "name": "myTable", + "fields": + { + "id": {"name": "id", "type": "INTEGER"}, + "name": {"name": "name", "type": "STRING"} + } + } + """, StandardCharsets.UTF_8), "myTable.json"); + + assertEquals("myTable", table.getName()); + assertEquals(2, table.getFields().size()); + assertEquals("id", table.getFields().get("id").getName()); + assertEquals(QFieldType.INTEGER, table.getFields().get("id").getType()); + assertEquals("name", table.getFields().get("name").getName()); + assertEquals(QFieldType.STRING, table.getFields().get("name").getType()); + } + + +} \ No newline at end of file