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

io.github.wycst.wast.jdbc.executer.EntityExecuter Maven / Gradle / Ivy

package io.github.wycst.wast.jdbc.executer;

import io.github.wycst.wast.common.beans.GregorianDate;
import io.github.wycst.wast.common.utils.ObjectUtils;
import io.github.wycst.wast.common.utils.StringUtils;
import io.github.wycst.wast.jdbc.exception.EntityException;
import io.github.wycst.wast.jdbc.exception.OqlParematerException;
import io.github.wycst.wast.jdbc.exception.SqlExecuteException;
import io.github.wycst.wast.jdbc.query.page.Page;
import io.github.wycst.wast.jdbc.query.sql.Sql;
import io.github.wycst.wast.jdbc.util.StreamCursor;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.*;

/**
 * 实体类执行器
 *
 * @Author wangyunchao
 * @Date 2022/12/3 12:24
 */
public final class EntityExecuter implements OqlExecuter {

    private final DefaultSqlExecuter sqlExecuter;

    EntityExecuter(DefaultSqlExecuter sqlExecuter) {
        this.sqlExecuter = sqlExecuter;
    }

    @Override
    public DefaultSqlExecuter getSqlExecuter() {
        return sqlExecuter;
    }

    @Override
    public  E get(Class entityCls, Serializable id) {
        return get(entityCls, id, false);
    }

    @Override
    public  E get(Class entityCls, Serializable id, boolean fetch) {
        id.getClass();
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);
        E result = entitySqlMapping.getEntityHandler().getById(sqlExecuter, entityCls, id); // getById(entitySqlMapping, entityCls, id);
        if (fetch && result != null) {
            this.handleFetch(entitySqlMapping, result);
        }
        return result;
    }

    @Override
    public  List queryAll(Class cls) {
        return queryBy(cls, (Map) null);
    }

    @Override
    public  E queryOne(Class entityCls, Map params) {
        return executeQueryOne(entityCls, params);
    }

    @Override
    public  E queryOne(Class entityCls, E params) {
        return executeQueryOne(entityCls, params);
    }

    @Override
    public  long queryCount(Class entityCls) {
        return queryCount(entityCls, (Map) null);
    }

    @Override
    public  long queryCount(Class entityCls, Map params) {
        return executeQueryCount(entityCls, params);
    }

    @Override
    public  long queryCount(Class entityCls, E params) {
        return executeQueryCount(entityCls, params);
    }

    public  long queryCount(Class cls, OqlQuery query, Object params) {
        checkEntityClass(cls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(cls);

        Sql countSqlObject = entitySqlMapping.getCountSqlObject(query, params);
        return sqlExecuter.queryValue(countSqlObject.getFormalSql(), long.class, countSqlObject.getParamValues());
    }

    @Override
    public  E queryUnique(Class entityCls, Map params) {
        return executeQueryUnique(entityCls, params);
    }

    @Override
    public  E queryUnique(Class entityCls, E params) {
        return executeQueryUnique(entityCls, params);
    }

    @Override
    public  List queryList(Class entityCls, OqlQuery query, Object params) {
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        Sql sqlObject = entitySqlMapping.getSelectSqlObject(query, params);
        return sqlExecuter.queryList(sqlObject.getFormalSql(), entityCls, sqlObject.getParamValues());
    }

    @Override
    public  List queryList(Class cls, OqlQuery query) {
        return queryList(cls, query, new HashMap());
    }

    @Override
    public  StreamCursor queryStreamBy(Class entityCls, Map params) {
        return executeQueryStreamBy(entityCls, params);
    }

    @Override
    public  StreamCursor queryStreamBy(Class entityCls, E params) {
        return executeQueryStreamBy(entityCls, params);
    }

    @Override
    public  StreamCursor queryStream(Class entityCls, OqlQuery query, Object params) {
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        Sql sqlObject = entitySqlMapping.getSelectSqlObject(query, params);
        return sqlExecuter.queryStream(sqlObject.getFormalSql(), entityCls, sqlObject.getParamValues());
    }

    @Override
    public  List queryBy(Class entityCls, Map params) {
        return executeQueryBy(entityCls, params);
    }

    @Override
    public  List queryBy(Class entityCls, E params) {
        return executeQueryBy(entityCls, params);
    }

    @Override
    public  List queryByIds(Class entityCls, List ids) {
        if (ids == null || ids.isEmpty()) {
            return new ArrayList();
        }
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);
        // select * from t where id in(?, ?, ?)
        String selectInSql = entitySqlMapping.getSelectSqlByIds(ids);
        return sqlExecuter.queryList(selectInSql, entityCls, ids.toArray());
    }

    @Override
    public  List queryByIds(Class entityCls, Serializable... ids) {
        if (ids.length == 0) {
            return new ArrayList();
        }
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);
        // select * from t where id in(?, ?, ?)
        Collection list = Arrays.asList(ids);
        String selectInSql = entitySqlMapping.getSelectSqlByIds(list);
        return sqlExecuter.queryList(selectInSql, entityCls, ids);
    }

    @Override
    public  Page queryPage(Page page, OqlQuery query, Object params) {
        Class entityCls = page.actualType();
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        Sql sqlObject = entitySqlMapping.getSelectSqlObject(query, params, true);
        this.executeQueryPage(page, entityCls, sqlObject);
        return page;
    }

    @Override
    public  Page queryPage(Page page) {
        return queryPage(page, (Map) null);
    }

    @Override
    public  Page queryPage(Page page, Map params) {
        return executeQueryPage(page, params);
    }

    @Override
    public  Page queryPage(Page page, E params) {
        return executeQueryPage(page, params);
    }

    @Override
    public  Serializable insert(E entity) {
        Class entityCls = entity.getClass();
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        Sql sqlObject = entitySqlMapping.getInsertSqlObject(entity);
        return sqlExecuter.insert(sqlObject.getFormalSql(), true, sqlObject.getParamValues());
    }

    @Override
    public  void insertList(List entityList) {
        if (entityList.size() == 0) {
            return;
        }
        Class entityCls = entityList.get(0).getClass();
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        if(entitySqlMapping.isUsePlaceholderOnInsert()) {
            if(isSupportBatchInsert()) {
                Sql batchInsertSqlObject = entitySqlMapping.getBatchInsertSqlObject(entityList);
                sqlExecuter.update(batchInsertSqlObject.getFormalSql(), batchInsertSqlObject.getParamValues());
            } else {
                for (E e : entityList) {
                    Sql sqlObject = entitySqlMapping.getInsertSqlObject(e);
                    sqlExecuter.insert(sqlObject.getFormalSql(), true, sqlObject.getParamValues());
                }
            }
        } else {
            Sql sqlObject = entitySqlMapping.getInsertSqlObjectList(entityList);
            sqlExecuter.updateCollection(sqlObject.getFormalSql(), sqlObject.getParamValuesList());
        }
    }

    @Override
    public  int mysqlBatchInsert(List entityList) {
        if (!isSupportBatchInsert()) {
            throw new SqlExecuteException("当前数据库不支持批量插入");
        }
        if (entityList.size() == 0) {
            return 0;
        }
        Class entityCls = entityList.get(0).getClass();
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        Sql batchInsertSqlObject = entitySqlMapping.getBatchInsertSqlObject(entityList);
        return sqlExecuter.update(batchInsertSqlObject.getFormalSql(), batchInsertSqlObject.getParamValues());
    }

    @Override
    public  int update(E entity) {
        Class entityCls = entity.getClass();
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);
        return entitySqlMapping.getEntityHandler().updateEntity(sqlExecuter, entity);
    }

    @Override
    public  int updateBy(Class entityCls, OqlQuery query, E params, String... fields) {
        return handleUpdateByParams(entityCls, query, params, fields);
    }

    @Override
    public  int updateBy(Class entityCls, OqlQuery query, Map params, String... fields) {
        return handleUpdateByParams(entityCls, query, params, fields);
    }

     int handleUpdateByParams(Class entityCls, OqlQuery query, Object params, String... fields) {
        if(fields.length == 0) return 0;
        if(params == null) {
            throw new SqlExecuteException("params is null");
        }

        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        String sqlStringFormat = getSqlStringFormat(SqlFunctionType.UPDATE_BY_PARAMS);
        Sql sqlObject = entitySqlMapping.getUpdateSqlObject(sqlStringFormat, query, params, fields);
        return sqlExecuter.update(sqlObject.getFormalSql(), sqlObject.getParamValues());
    }

    @Override
    public  int updateFields(E e, String... fields) {
        return updateFields(e, Arrays.asList(fields), false);
    }

    @Override
    public  int updateFields(E e, boolean isExclude, String... fields) {
        return updateFields(e, Arrays.asList(fields), isExclude);
    }

    @Override
    public  int updateFields(E entity, List fields) {
        return updateFields(entity, fields, false);
    }

    @Override
    public  int updateFields(E entity, List fields, boolean isExclude) {
        Class entityCls = entity.getClass();
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);
        String sqlStringFormat = getSqlStringFormat(SqlFunctionType.UPDATE_BY_ID);

        Sql sqlObject = entitySqlMapping.getUpdateSqlObject(sqlStringFormat, entity, fields, isExclude);
        if (sqlObject == null) {
            throw new OqlParematerException("配置错误:" + entityCls + "可能没有定义@Id,请检查配置");
        }
        try {
            return sqlExecuter.update(sqlObject.getFormalSql(), sqlObject.getParamValues());
        } finally {
            entitySqlMapping.getEntityHandler().afterUpdate(sqlExecuter, entity);
        }
    }

    @Override
    public  int deleteAll(Class entityCls) {
        return deleteBy(entityCls, (Map) null);
    }

    @Override
    public  int delete(Class entityCls, Serializable id) {
        return delete(entityCls, id, false);
    }

    @Override
    public  int delete(Class entityCls, Serializable id, boolean cascade) {
        id.getClass();
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);
        if (cascade) {
            // handle cascadeDelete
            this.executeCascadeDelete(entitySqlMapping, id);
        }
        return entitySqlMapping.getEntityHandler().deleteById(sqlExecuter, entityCls, id);
    }

    @Override
    public  int deleteList(List entityList) {
        if (entityList == null) {
            return 0;
        }
        int influenceRows = 0;
        Class entityCls = entityList.get(0).getClass();
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);
        String sqlStringFormat = getSqlStringFormat(SqlFunctionType.DELETE_BY_ID);
        String deleteSql = entitySqlMapping.getDeleteSql(sqlStringFormat);
        if (deleteSql == null) {
            throw new OqlParematerException("配置错误:" + entityCls + "可能没有定义@Id,请检查配置");
        }
        for (Object entity : entityList) {
            Serializable id = entitySqlMapping.getId(entity);
            influenceRows += sqlExecuter.update(deleteSql, id);
        }
        if (influenceRows > 0) {
            entitySqlMapping.getEntityHandler().afterBatchDelete();
        }
        return influenceRows;
    }

    @Override
    public  int deleteByIds(Class entityCls, List ids) {
        if (ids == null || ids.size() == 0) {
            return 0;
        }
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);
        String sqlStringFormat = getSqlStringFormat(SqlFunctionType.DELETE_BY_ID);
        String deleteSql = entitySqlMapping.getDeleteSql(sqlStringFormat);
        if (deleteSql == null) {
            throw new OqlParematerException("配置错误:" + entityCls + "可能没有定义@Id,请检查配置");
        }
        List dataList = new ArrayList();
        for (Serializable id : ids) {
            dataList.add(new Object[]{id});
        }
        sqlExecuter.updateCollection(deleteSql, dataList);
        return ids.size();
    }

    @Override
    public  int deleteByIds(Class cls, Serializable... ids) {
        return deleteByIds(cls, Arrays.asList(ids));
    }

    @Override
    public  int deleteBy(Class entityCls, E params) {
        return handleDeleteBy(entityCls, params);
    }

    @Override
    public  int deleteBy(Class entityCls, Map params) {
        return handleDeleteBy(entityCls, params);
    }

     int handleDeleteBy(Class entityCls, Object params) {
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        String sqlTemplate = getSqlStringFormat(SqlFunctionType.DELETE_BY_PARAMS);
        Sql sqlObject = entitySqlMapping.getDeleteSqlObjectByParams(sqlTemplate, params);
        try {
            return sqlExecuter.update(sqlObject.getFormalSql(), sqlObject.getParamValues());
        } finally {
            entitySqlMapping.getEntityHandler().afterBatchDelete();
        }
    }

    @Override
    public  int deleteBy(Class entityCls, OqlQuery query, Object params) {
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        Sql sqlObject = entitySqlMapping.getSelectSqlObject(query, params);
        List entityList = sqlExecuter.queryList(sqlObject.getFormalSql(), entityCls, sqlObject.getParamValues());
        return deleteList(entityList);
    }

    @Override
    public  String reverseDeleteSQL(E entity) {
        checkEntityClass(entity.getClass());
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entity.getClass());
        return generateDeleteSql(entitySqlMapping, entity);
    }

    @Override
    public  String reverseInsertSQL(E entity) {
        checkEntityClass(entity.getClass());
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entity.getClass());
        return generateInsertSql(entitySqlMapping, entity);
    }

    void checkEntityClass(Class entityCls) {
        EntityManagementFactory.defaultManagementFactory().checkEntityClass(entityCls);
    }

    EntitySqlMapping getEntitySqlMapping(Class entityCls) {
        return EntityManagementFactory.defaultManagementFactory().getEntitySqlMapping(entityCls);
    }

    void handleFetch(EntitySqlMapping entitySqlMapping, Object result) {
        List cascadeFetchMappings = entitySqlMapping.getCascadeFetchMappings();
        for (CascadeFetchMapping cascadeFetchMapping : cascadeFetchMappings) {
            if (!cascadeFetchMapping.isFetch()) {
                continue;
            }
            Map params = getCascadeFetchParams(cascadeFetchMapping, entitySqlMapping, result);
            if (params == null) {
                return;
            }
            int fieldTypeValue = cascadeFetchMapping.getFieldType();
            Class targetEntityClass = cascadeFetchMapping.getTargetEntityClass();
            List list = executeQueryBy(targetEntityClass, params);
            Object fetchFieldVal = null;
            if (fieldTypeValue == 1) {
                if (list.size() > 0) {
                    fetchFieldVal = list.get(0);
                }
            } else {
                fetchFieldVal = list;
            }
            if (fetchFieldVal != null) {
                cascadeFetchMapping.setFetchFieldValue(result, fetchFieldVal);
            }
        }
    }

    Map getCascadeFetchParams(CascadeFetchMapping cascadeFetchMapping, EntitySqlMapping entitySqlMapping, Object result) {
        String fieldName = cascadeFetchMapping.getFieldName();
        FieldColumn fieldColumn = entitySqlMapping.getFieldColumnMapping().get(fieldName);
        if (fieldColumn == null) {
            Field fetchField = cascadeFetchMapping.getCascadeFetchField();
            Class clazz = entitySqlMapping.getEntityClass();
            throw new EntityException(" Entity Class " + clazz + " and field[" + fetchField.getName() + "] is AnnotationPresent @CascadeFetch, but field '" + fieldName + "' is not exist");
        }
        Map params = null;
        Object val = entitySqlMapping.getFieldColumnValue(false, fieldColumn, result);
        if (val != null) {
            String targetFieldName = cascadeFetchMapping.getTargetFieldName();
            params = new HashMap();
            params.put(targetFieldName, val);
        }

        return params;
    }

     E executeQueryOne(Class entityCls, Object params) {
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        Sql sqlObject = entitySqlMapping.getSelectSqlObject(params);
        return sqlExecuter.queryObject(sqlObject.getFormalSql(), entityCls, sqlObject.getParamValues());
    }

    long executeQueryCount(Class entityCls, Object params) {
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        Sql countSqlObject = entitySqlMapping.getCountSqlObject(params);
        return sqlExecuter.queryValue(countSqlObject.getFormalSql(), long.class, countSqlObject.getParamValues());
    }

     E executeQueryUnique(Class entityCls, Object params) {
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        Sql sqlObject = entitySqlMapping.getSelectSqlObject(params);
        return sqlExecuter.queryUniqueObject(sqlObject.getFormalSql(), entityCls, sqlObject.getParamValues());
    }

     StreamCursor executeQueryStreamBy(Class entityCls, Object params) {
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        Sql sqlObject = entitySqlMapping.getSelectSqlObject(params);
        return sqlExecuter.queryStream(sqlObject.getFormalSql(), entityCls, sqlObject.getParamValues());
    }

     List executeQueryBy(Class entityCls, Object params) {
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        return entitySqlMapping.getEntityHandler().executeQueryBy(sqlExecuter, entityCls, params);
    }

     Page executeQueryPage(Page page, Object params) {
        Class entityCls = page.actualType();
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);

        Sql sqlObject = entitySqlMapping.getSelectSqlObject(params, true);
        executeQueryPage(page, entityCls, sqlObject);
        return page;
    }

     void executeQueryPage(Page page, Class entityCls, Sql sqlObject) {
        String totalSql = sqlObject.getTotalSql();
        String formalSql = sqlObject.getFormalSql();
        Object[] paramValues = sqlObject.getParamValues();

        // 分页的sql
        final String limitSql = sqlExecuter.getLimitSql(formalSql, page.getOffset(), page.getPageSize());
        // 列表
        List rows = sqlExecuter.queryList(limitSql, entityCls, paramValues);
        page.setRows(rows);

        long total = sqlExecuter.queryValue(totalSql, long.class, paramValues);
        page.setTotal(total);
    }
    

    String getSqlStringFormat(SqlFunctionType sqlFunctionType) {
        return sqlExecuter.sqlTemplates[sqlFunctionType.ordinal()];
    }

    void executeCascadeDelete(EntitySqlMapping entitySqlMapping, Serializable id) {
        List cascadeFetchMappings = entitySqlMapping.getCascadeFetchMappings();
        Class clazz = entitySqlMapping.getEntityClass();
        Object result = entitySqlMapping.getEntityHandler().getById(sqlExecuter, clazz, id);
        for (CascadeFetchMapping cascadeFetchMapping : cascadeFetchMappings) {
            if (!cascadeFetchMapping.isCascade()) {
                continue;
            }
            Map params = getCascadeFetchParams(cascadeFetchMapping, entitySqlMapping, result);
            if (params == null) {
                return;
            }
            Class entityClass = cascadeFetchMapping.getTargetEntityClass();
            deleteBy(entityClass, params);
        }
    }

    /***
     * 生成删除sql
     *
     * @return
     */
    String generateDeleteSql(EntitySqlMapping entitySqlMapping, Object entity) {
        if (entity == null) {
            return null;
        }
        String template = "DELETE FROM `%s` WHERE %s = ";
        if (sqlExecuter.clickHouse) {
            template = "ALTER TABLE %s DELETE WHERE %s = ";
        }
        Object primaryValue = ObjectUtils.get(entity, entitySqlMapping.getPrimary().getField().getName());
        if (primaryValue instanceof String) {
            primaryValue = "'" + primaryValue + "'";
        }
        String sql = StringUtils.replacePlaceholder(template, "%s", entitySqlMapping.getTableName(),
                entitySqlMapping.getPrimary().getColumnName());
        return sql + primaryValue;
    }

    /**
     * insert sql
     *
     * @return
     */
    private String generateInsertSql(EntitySqlMapping entitySqlMapping, Object entity) {
        if (entity == null) {
            return null;
        }
        String tmpSql = "INSERT INTO `%s` (%s) VALUES (%s)";
        StringBuilder columns = new StringBuilder();
        StringBuilder values = new StringBuilder();
        int index = 0;
        Map fieldColumnMapping = entitySqlMapping.getFieldColumnMapping();
        int columnLength = fieldColumnMapping.size();
        for (Map.Entry entry : fieldColumnMapping.entrySet()) {
            String fieldName = entry.getKey();
            FieldColumn fieldColumn = entry.getValue();
            String columnName = fieldColumn.getColumnName();
            Object value = ObjectUtils.get(entity, fieldName);
            columns.append(columnName);
            if (value == null || value instanceof Number) {
                values.append(value);
            } else {
                if (value instanceof Date) {
                    value = new GregorianDate(((Date) value).getTime()).format();
                } else if (value instanceof String) {
                    // 一个\使用\\\\替换
                    value = ((String) value).replace("\\", "\\\\");
                    // 换行问题
                    value = ((String) value).replace("\n", "\\n");
                    // 处理引号问题
                    value = ((String) value).replace("\"", "\\\"");
                    // 单引号 -> \'
                    value = ((String) value).replace("'", "\\'");
                }
                values.append("'").append(value).append("'");
            }
            if (++index < columnLength) {
                columns.append(", ");
                values.append(", ");
            }
        }

        return StringUtils.replacePlaceholder(tmpSql, "%s", entitySqlMapping.getTableName(), columns.toString(), values.toString());
    }

    public boolean isSupportBatchInsert() {
        return sqlExecuter.isSupportBatchInsert();
    }

    public  void clearCache(Class entityCls) {
        checkEntityClass(entityCls);
        EntitySqlMapping entitySqlMapping = getEntitySqlMapping(entityCls);
        if (entitySqlMapping.isCacheable()) {
            entitySqlMapping.getCacheableEntityHandler().resetCaches();
        }
    }

    public  void clearAllCache() {
        EntityManagementFactory.defaultManagementFactory().clearAllCaches();
    }

    public void beginTransaction() {
        sqlExecuter.beginTransaction();
    }

    public void endTransaction() {
        sqlExecuter.endTransaction();
    }

    public void rollbackTransaction() {
        rollbackTransaction(true);
    }

    public void rollbackTransaction(boolean closeConnection) {
        sqlExecuter.rollbackTransaction(closeConnection);
    }

    public void commitTransaction() {
        sqlExecuter.commitTransaction();
    }

    public void commitTransaction(boolean closeConnection) {
        sqlExecuter.commitTransaction(closeConnection);
    }

    public void close() {
        sqlExecuter.close();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy