aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/org/traccar/storage
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/org/traccar/storage')
-rw-r--r--src/main/java/org/traccar/storage/DatabaseModule.java103
-rw-r--r--src/main/java/org/traccar/storage/DatabaseStorage.java410
-rw-r--r--src/main/java/org/traccar/storage/MemoryStorage.java79
-rw-r--r--src/main/java/org/traccar/storage/QueryBuilder.java509
-rw-r--r--src/main/java/org/traccar/storage/QueryIgnore.java26
-rw-r--r--src/main/java/org/traccar/storage/Storage.java53
-rw-r--r--src/main/java/org/traccar/storage/StorageException.java32
-rw-r--r--src/main/java/org/traccar/storage/StorageName.java27
-rw-r--r--src/main/java/org/traccar/storage/query/Columns.java81
-rw-r--r--src/main/java/org/traccar/storage/query/Condition.java211
-rw-r--r--src/main/java/org/traccar/storage/query/Limit.java30
-rw-r--r--src/main/java/org/traccar/storage/query/Order.java46
-rw-r--r--src/main/java/org/traccar/storage/query/Request.java58
13 files changed, 1665 insertions, 0 deletions
diff --git a/src/main/java/org/traccar/storage/DatabaseModule.java b/src/main/java/org/traccar/storage/DatabaseModule.java
new file mode 100644
index 000000000..3e3483818
--- /dev/null
+++ b/src/main/java/org/traccar/storage/DatabaseModule.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.storage;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import liquibase.Contexts;
+import liquibase.Liquibase;
+import liquibase.database.Database;
+import liquibase.database.DatabaseFactory;
+import liquibase.exception.LiquibaseException;
+import liquibase.resource.DirectoryResourceAccessor;
+import liquibase.resource.ResourceAccessor;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+
+import javax.inject.Singleton;
+import javax.sql.DataSource;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.URL;
+
+public class DatabaseModule extends AbstractModule {
+
+ @Singleton
+ @Provides
+ public static DataSource provideDataSource(
+ Config config) throws ReflectiveOperationException, IOException, LiquibaseException {
+
+ String driverFile = config.getString(Keys.DATABASE_DRIVER_FILE);
+ if (driverFile != null) {
+ ClassLoader classLoader = ClassLoader.getSystemClassLoader();
+ try {
+ Method method = classLoader.getClass().getDeclaredMethod("addURL", URL.class);
+ method.setAccessible(true);
+ method.invoke(classLoader, new File(driverFile).toURI().toURL());
+ } catch (NoSuchMethodException e) {
+ Method method = classLoader.getClass()
+ .getDeclaredMethod("appendToClassPathForInstrumentation", String.class);
+ method.setAccessible(true);
+ method.invoke(classLoader, driverFile);
+ }
+ }
+
+ String driver = config.getString(Keys.DATABASE_DRIVER);
+ if (driver != null) {
+ Class.forName(driver);
+ }
+
+ HikariConfig hikariConfig = new HikariConfig();
+ hikariConfig.setDriverClassName(driver);
+ hikariConfig.setJdbcUrl(config.getString(Keys.DATABASE_URL));
+ hikariConfig.setUsername(config.getString(Keys.DATABASE_USER));
+ hikariConfig.setPassword(config.getString(Keys.DATABASE_PASSWORD));
+ hikariConfig.setConnectionInitSql(config.getString(Keys.DATABASE_CHECK_CONNECTION));
+ hikariConfig.setIdleTimeout(600000);
+
+ int maxPoolSize = config.getInteger(Keys.DATABASE_MAX_POOL_SIZE);
+ if (maxPoolSize != 0) {
+ hikariConfig.setMaximumPoolSize(maxPoolSize);
+ }
+
+ DataSource dataSource = new HikariDataSource(hikariConfig);
+
+ if (config.hasKey(Keys.DATABASE_CHANGELOG)) {
+
+ ResourceAccessor resourceAccessor = new DirectoryResourceAccessor(new File("."));
+
+ Database database = DatabaseFactory.getInstance().openDatabase(
+ config.getString(Keys.DATABASE_URL),
+ config.getString(Keys.DATABASE_USER),
+ config.getString(Keys.DATABASE_PASSWORD),
+ config.getString(Keys.DATABASE_DRIVER),
+ null, null, null, resourceAccessor);
+
+ String changelog = config.getString(Keys.DATABASE_CHANGELOG);
+
+ try (Liquibase liquibase = new Liquibase(changelog, resourceAccessor, database)) {
+ liquibase.clearCheckSums();
+ liquibase.update(new Contexts());
+ }
+ }
+
+ return dataSource;
+ }
+
+}
diff --git a/src/main/java/org/traccar/storage/DatabaseStorage.java b/src/main/java/org/traccar/storage/DatabaseStorage.java
new file mode 100644
index 000000000..a049a641c
--- /dev/null
+++ b/src/main/java/org/traccar/storage/DatabaseStorage.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.storage;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.traccar.config.Config;
+import org.traccar.model.BaseModel;
+import org.traccar.model.Device;
+import org.traccar.model.Group;
+import org.traccar.model.GroupedModel;
+import org.traccar.model.Permission;
+import org.traccar.storage.query.Columns;
+import org.traccar.storage.query.Condition;
+import org.traccar.storage.query.Order;
+import org.traccar.storage.query.Request;
+
+import javax.inject.Inject;
+import javax.sql.DataSource;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class DatabaseStorage extends Storage {
+
+ private final Config config;
+ private final DataSource dataSource;
+ private final ObjectMapper objectMapper;
+ private final String databaseType;
+
+ @Inject
+ public DatabaseStorage(Config config, DataSource dataSource, ObjectMapper objectMapper) {
+ this.config = config;
+ this.dataSource = dataSource;
+ this.objectMapper = objectMapper;
+
+ try {
+ databaseType = dataSource.getConnection().getMetaData().getDatabaseProductName();
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public <T> List<T> getObjects(Class<T> clazz, Request request) throws StorageException {
+ StringBuilder query = new StringBuilder("SELECT ");
+ if (request.getColumns() instanceof Columns.All) {
+ query.append('*');
+ } else {
+ query.append(formatColumns(request.getColumns().getColumns(clazz, "set"), c -> c));
+ }
+ query.append(" FROM ").append(getStorageName(clazz));
+ query.append(formatCondition(request.getCondition()));
+ query.append(formatOrder(request.getOrder()));
+ try {
+ QueryBuilder builder = QueryBuilder.create(config, dataSource, objectMapper, query.toString());
+ for (Map.Entry<String, Object> variable : getConditionVariables(request.getCondition()).entrySet()) {
+ builder.setValue(variable.getKey(), variable.getValue());
+ }
+ return builder.executeQuery(clazz);
+ } catch (SQLException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
+ public <T> long addObject(T entity, Request request) throws StorageException {
+ List<String> columns = request.getColumns().getColumns(entity.getClass(), "get");
+ StringBuilder query = new StringBuilder("INSERT INTO ");
+ query.append(getStorageName(entity.getClass()));
+ query.append("(");
+ query.append(formatColumns(columns, c -> c));
+ query.append(") VALUES (");
+ query.append(formatColumns(columns, c -> ':' + c));
+ query.append(")");
+ try {
+ QueryBuilder builder = QueryBuilder.create(config, dataSource, objectMapper, query.toString(), true);
+ builder.setObject(entity, columns);
+ return builder.executeUpdate();
+ } catch (SQLException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
+ public <T> void updateObject(T entity, Request request) throws StorageException {
+ List<String> columns = request.getColumns().getColumns(entity.getClass(), "get");
+ StringBuilder query = new StringBuilder("UPDATE ");
+ query.append(getStorageName(entity.getClass()));
+ query.append(" SET ");
+ query.append(formatColumns(columns, c -> c + " = :" + c));
+ query.append(formatCondition(request.getCondition()));
+ try {
+ QueryBuilder builder = QueryBuilder.create(config, dataSource, objectMapper, query.toString());
+ builder.setObject(entity, columns);
+ for (Map.Entry<String, Object> variable : getConditionVariables(request.getCondition()).entrySet()) {
+ builder.setValue(variable.getKey(), variable.getValue());
+ }
+ builder.executeUpdate();
+ } catch (SQLException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
+ public void removeObject(Class<?> clazz, Request request) throws StorageException {
+ StringBuilder query = new StringBuilder("DELETE FROM ");
+ query.append(getStorageName(clazz));
+ query.append(formatCondition(request.getCondition()));
+ try {
+ QueryBuilder builder = QueryBuilder.create(config, dataSource, objectMapper, query.toString());
+ for (Map.Entry<String, Object> variable : getConditionVariables(request.getCondition()).entrySet()) {
+ builder.setValue(variable.getKey(), variable.getValue());
+ }
+ builder.executeUpdate();
+ } catch (SQLException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
+ public List<Permission> getPermissions(
+ Class<? extends BaseModel> ownerClass, long ownerId,
+ Class<? extends BaseModel> propertyClass, long propertyId) throws StorageException {
+ StringBuilder query = new StringBuilder("SELECT * FROM ");
+ query.append(Permission.getStorageName(ownerClass, propertyClass));
+ var conditions = new LinkedList<Condition>();
+ if (ownerId > 0) {
+ conditions.add(new Condition.Equals(Permission.getKey(ownerClass), ownerId));
+ }
+ if (propertyId > 0) {
+ conditions.add(new Condition.Equals(Permission.getKey(propertyClass), propertyId));
+ }
+ Condition combinedCondition = Condition.merge(conditions);
+ query.append(formatCondition(combinedCondition));
+ try {
+ QueryBuilder builder = QueryBuilder.create(config, dataSource, objectMapper, query.toString());
+ for (Map.Entry<String, Object> variable : getConditionVariables(combinedCondition).entrySet()) {
+ builder.setValue(variable.getKey(), variable.getValue());
+ }
+ return builder.executePermissionsQuery();
+ } catch (SQLException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
+ public void addPermission(Permission permission) throws StorageException {
+ StringBuilder query = new StringBuilder("INSERT INTO ");
+ query.append(permission.getStorageName());
+ query.append(" VALUES (");
+ query.append(permission.get().keySet().stream().map(key -> ':' + key).collect(Collectors.joining(", ")));
+ query.append(")");
+ try {
+ QueryBuilder builder = QueryBuilder.create(config, dataSource, objectMapper, query.toString(), true);
+ for (var entry : permission.get().entrySet()) {
+ builder.setLong(entry.getKey(), entry.getValue());
+ }
+ builder.executeUpdate();
+ } catch (SQLException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
+ public void removePermission(Permission permission) throws StorageException {
+ StringBuilder query = new StringBuilder("DELETE FROM ");
+ query.append(permission.getStorageName());
+ query.append(" WHERE ");
+ query.append(permission
+ .get().keySet().stream().map(key -> key + " = :" + key).collect(Collectors.joining(" AND ")));
+ try {
+ QueryBuilder builder = QueryBuilder.create(config, dataSource, objectMapper, query.toString(), true);
+ for (var entry : permission.get().entrySet()) {
+ builder.setLong(entry.getKey(), entry.getValue());
+ }
+ builder.executeUpdate();
+ } catch (SQLException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ private String getStorageName(Class<?> clazz) throws StorageException {
+ StorageName storageName = clazz.getAnnotation(StorageName.class);
+ if (storageName == null) {
+ throw new StorageException("StorageName annotation is missing");
+ }
+ return storageName.value();
+ }
+
+ private Map<String, Object> getConditionVariables(Condition genericCondition) {
+ Map<String, Object> results = new HashMap<>();
+ if (genericCondition instanceof Condition.Compare) {
+ var condition = (Condition.Compare) genericCondition;
+ if (condition.getValue() != null) {
+ results.put(condition.getVariable(), condition.getValue());
+ }
+ } else if (genericCondition instanceof Condition.Between) {
+ var condition = (Condition.Between) genericCondition;
+ results.put(condition.getFromVariable(), condition.getFromValue());
+ results.put(condition.getToVariable(), condition.getToValue());
+ } else if (genericCondition instanceof Condition.Binary) {
+ var condition = (Condition.Binary) genericCondition;
+ results.putAll(getConditionVariables(condition.getFirst()));
+ results.putAll(getConditionVariables(condition.getSecond()));
+ } else if (genericCondition instanceof Condition.Permission) {
+ var condition = (Condition.Permission) genericCondition;
+ if (condition.getOwnerId() > 0) {
+ results.put(Permission.getKey(condition.getOwnerClass()), condition.getOwnerId());
+ } else {
+ results.put(Permission.getKey(condition.getPropertyClass()), condition.getPropertyId());
+ }
+ } else if (genericCondition instanceof Condition.LatestPositions) {
+ var condition = (Condition.LatestPositions) genericCondition;
+ if (condition.getDeviceId() > 0) {
+ results.put("deviceId", condition.getDeviceId());
+ }
+ }
+ return results;
+ }
+
+ private String formatColumns(List<String> columns, Function<String, String> mapper) {
+ return columns.stream().map(mapper).collect(Collectors.joining(", "));
+ }
+
+ private String formatCondition(Condition genericCondition) throws StorageException {
+ return formatCondition(genericCondition, true);
+ }
+
+ private String formatCondition(Condition genericCondition, boolean appendWhere) throws StorageException {
+ StringBuilder result = new StringBuilder();
+ if (genericCondition != null) {
+ if (appendWhere) {
+ result.append(" WHERE ");
+ }
+ if (genericCondition instanceof Condition.Compare) {
+
+ var condition = (Condition.Compare) genericCondition;
+ result.append(condition.getColumn());
+ result.append(" ");
+ result.append(condition.getOperator());
+ result.append(" :");
+ result.append(condition.getVariable());
+
+ } else if (genericCondition instanceof Condition.Between) {
+
+ var condition = (Condition.Between) genericCondition;
+ result.append(condition.getColumn());
+ result.append(" BETWEEN :");
+ result.append(condition.getFromVariable());
+ result.append(" AND :");
+ result.append(condition.getToVariable());
+
+ } else if (genericCondition instanceof Condition.Binary) {
+
+ var condition = (Condition.Binary) genericCondition;
+ result.append(formatCondition(condition.getFirst(), false));
+ result.append(" ");
+ result.append(condition.getOperator());
+ result.append(" ");
+ result.append(formatCondition(condition.getSecond(), false));
+
+ } else if (genericCondition instanceof Condition.Permission) {
+
+ var condition = (Condition.Permission) genericCondition;
+ result.append("id IN (");
+ result.append(formatPermissionQuery(condition));
+ result.append(")");
+
+ } else if (genericCondition instanceof Condition.LatestPositions) {
+
+ var condition = (Condition.LatestPositions) genericCondition;
+ result.append("id IN (");
+ result.append("SELECT positionId FROM ");
+ result.append(getStorageName(Device.class));
+ if (condition.getDeviceId() > 0) {
+ result.append(" WHERE id = :deviceId");
+ }
+ result.append(")");
+
+ }
+ }
+ return result.toString();
+ }
+
+ private String formatOrder(Order order) {
+ StringBuilder result = new StringBuilder();
+ if (order != null) {
+ result.append(" ORDER BY ");
+ result.append(order.getColumn());
+ if (order.getDescending()) {
+ result.append(" DESC");
+ }
+ if (order.getLimit() > 0) {
+ if (databaseType.equals("Microsoft SQL Server")) {
+ result.append(" OFFSET 0 ROWS FETCH FIRST ");
+ result.append(order.getLimit());
+ result.append(" ROWS ONLY");
+ } else {
+ result.append(" LIMIT ");
+ result.append(order.getLimit());
+ }
+ }
+ }
+ return result.toString();
+ }
+
+ private String formatPermissionQuery(Condition.Permission condition) throws StorageException {
+ StringBuilder result = new StringBuilder();
+
+ String outputKey;
+ String conditionKey;
+ if (condition.getOwnerId() > 0) {
+ outputKey = Permission.getKey(condition.getPropertyClass());
+ conditionKey = Permission.getKey(condition.getOwnerClass());
+ } else {
+ outputKey = Permission.getKey(condition.getOwnerClass());
+ conditionKey = Permission.getKey(condition.getPropertyClass());
+ }
+
+ String storageName = Permission.getStorageName(condition.getOwnerClass(), condition.getPropertyClass());
+ result.append("SELECT ");
+ result.append(storageName).append('.').append(outputKey);
+ result.append(" FROM ");
+ result.append(storageName);
+ result.append(" WHERE ");
+ result.append(conditionKey);
+ result.append(" = :");
+ result.append(conditionKey);
+
+ if (condition.getIncludeGroups()) {
+
+ boolean expandDevices;
+ String groupStorageName;
+ if (GroupedModel.class.isAssignableFrom(condition.getOwnerClass())) {
+ expandDevices = Device.class.isAssignableFrom(condition.getOwnerClass());
+ groupStorageName = Permission.getStorageName(Group.class, condition.getPropertyClass());
+ } else {
+ expandDevices = Device.class.isAssignableFrom(condition.getPropertyClass());
+ groupStorageName = Permission.getStorageName(condition.getOwnerClass(), Group.class);
+ }
+
+ result.append(" UNION ");
+
+ result.append("SELECT DISTINCT ");
+ if (!expandDevices) {
+ if (outputKey.equals("groupId")) {
+ result.append("all_groups.");
+ } else {
+ result.append(groupStorageName).append('.');
+ }
+ }
+ result.append(outputKey);
+ result.append(" FROM ");
+ result.append(groupStorageName);
+
+ result.append(" INNER JOIN (");
+ result.append("SELECT id as parentId, id as groupId FROM ");
+ result.append(getStorageName(Group.class));
+ result.append(" UNION ");
+ result.append("SELECT groupId as parentId, id as groupId FROM ");
+ result.append(getStorageName(Group.class));
+ result.append(" WHERE groupId IS NOT NULL");
+ result.append(" UNION ");
+ result.append("SELECT g2.groupId as parentId, g1.id as groupId FROM ");
+ result.append(getStorageName(Group.class));
+ result.append(" AS g2");
+ result.append(" INNER JOIN ");
+ result.append(getStorageName(Group.class));
+ result.append(" AS g1 ON g2.id = g1.groupId");
+ result.append(" WHERE g2.groupId IS NOT NULL");
+ result.append(") AS all_groups ON ");
+ result.append(groupStorageName);
+ result.append(".groupId = all_groups.parentId");
+
+ if (expandDevices) {
+ result.append(" INNER JOIN (");
+ result.append("SELECT groupId as parentId, id as deviceId FROM ");
+ result.append(getStorageName(Device.class));
+ result.append(" WHERE groupId IS NOT NULL");
+ result.append(") AS devices ON all_groups.groupId = devices.parentId");
+ }
+
+ result.append(" WHERE ");
+ result.append(conditionKey);
+ result.append(" = :");
+ result.append(conditionKey);
+
+ }
+
+ return result.toString();
+ }
+
+}
diff --git a/src/main/java/org/traccar/storage/MemoryStorage.java b/src/main/java/org/traccar/storage/MemoryStorage.java
new file mode 100644
index 000000000..f19897ff8
--- /dev/null
+++ b/src/main/java/org/traccar/storage/MemoryStorage.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.storage;
+
+import org.traccar.model.BaseModel;
+import org.traccar.model.Pair;
+import org.traccar.model.Permission;
+import org.traccar.storage.query.Request;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class MemoryStorage extends Storage {
+
+ private final Map<Pair<Class<?>, Class<?>>, Set<Pair<Long, Long>>> permissions = new HashMap<>();
+
+ @Override
+ public <T> List<T> getObjects(Class<T> clazz, Request request) {
+ return null;
+ }
+
+ @Override
+ public <T> long addObject(T entity, Request request) {
+ return 0;
+ }
+
+ @Override
+ public <T> void updateObject(T entity, Request request) {
+ }
+
+ @Override
+ public void removeObject(Class<?> clazz, Request request) {
+ }
+
+ private Set<Pair<Long, Long>> getPermissionsSet(Class<?> ownerClass, Class<?> propertyClass) {
+ return permissions.computeIfAbsent(new Pair<>(ownerClass, propertyClass), k -> new HashSet<>());
+ }
+
+ @Override
+ public List<Permission> getPermissions(
+ Class<? extends BaseModel> ownerClass, long ownerId,
+ Class<? extends BaseModel> propertyClass, long propertyId) {
+ return getPermissionsSet(ownerClass, propertyClass).stream()
+ .filter(pair -> ownerId == 0 || pair.getFirst().equals(ownerId))
+ .filter(pair -> propertyId == 0 || pair.getSecond().equals(propertyId))
+ .map(pair -> new Permission(ownerClass, pair.getFirst(), propertyClass, pair.getSecond()))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public void addPermission(Permission permission) {
+ getPermissionsSet(permission.getOwnerClass(), permission.getPropertyClass())
+ .add(new Pair<>(permission.getOwnerId(), permission.getPropertyId()));
+ }
+
+ @Override
+ public void removePermission(Permission permission) {
+ getPermissionsSet(permission.getOwnerClass(), permission.getPropertyClass())
+ .remove(new Pair<>(permission.getOwnerId(), permission.getPropertyId()));
+ }
+
+}
diff --git a/src/main/java/org/traccar/storage/QueryBuilder.java b/src/main/java/org/traccar/storage/QueryBuilder.java
new file mode 100644
index 000000000..2f4c07406
--- /dev/null
+++ b/src/main/java/org/traccar/storage/QueryBuilder.java
@@ -0,0 +1,509 @@
+/*
+ * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.storage;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.model.Permission;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+@SuppressWarnings("UnusedReturnValue")
+public final class QueryBuilder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(QueryBuilder.class);
+
+ private final Config config;
+ private final ObjectMapper objectMapper;
+
+ private final Map<String, List<Integer>> indexMap = new HashMap<>();
+ private Connection connection;
+ private PreparedStatement statement;
+ private final String query;
+ private final boolean returnGeneratedKeys;
+
+ private QueryBuilder(
+ Config config, DataSource dataSource, ObjectMapper objectMapper,
+ String query, boolean returnGeneratedKeys) throws SQLException {
+ this.config = config;
+ this.objectMapper = objectMapper;
+ this.query = query;
+ this.returnGeneratedKeys = returnGeneratedKeys;
+ if (query != null) {
+ connection = dataSource.getConnection();
+ String parsedQuery = parse(query.trim(), indexMap);
+ try {
+ if (returnGeneratedKeys) {
+ statement = connection.prepareStatement(parsedQuery, Statement.RETURN_GENERATED_KEYS);
+ } else {
+ statement = connection.prepareStatement(parsedQuery);
+ }
+ } catch (SQLException error) {
+ connection.close();
+ throw error;
+ }
+ }
+ }
+
+ private static String parse(String query, Map<String, List<Integer>> paramMap) {
+
+ int length = query.length();
+ StringBuilder parsedQuery = new StringBuilder(length);
+ boolean inSingleQuote = false;
+ boolean inDoubleQuote = false;
+ int index = 1;
+
+ for (int i = 0; i < length; i++) {
+
+ char c = query.charAt(i);
+
+ // String end
+ if (inSingleQuote) {
+ if (c == '\'') {
+ inSingleQuote = false;
+ }
+ } else if (inDoubleQuote) {
+ if (c == '"') {
+ inDoubleQuote = false;
+ }
+ } else {
+
+ // String begin
+ if (c == '\'') {
+ inSingleQuote = true;
+ } else if (c == '"') {
+ inDoubleQuote = true;
+ } else if (c == ':' && i + 1 < length
+ && Character.isJavaIdentifierStart(query.charAt(i + 1))) {
+
+ // Identifier name
+ int j = i + 2;
+ while (j < length && Character.isJavaIdentifierPart(query.charAt(j))) {
+ j++;
+ }
+
+ String name = query.substring(i + 1, j);
+ c = '?';
+ i += name.length();
+ name = name.toLowerCase();
+
+ // Add to list
+ List<Integer> indexList = paramMap.computeIfAbsent(name, k -> new LinkedList<>());
+ indexList.add(index);
+
+ index++;
+ }
+ }
+
+ parsedQuery.append(c);
+ }
+
+ return parsedQuery.toString();
+ }
+
+ public static QueryBuilder create(
+ Config config, DataSource dataSource, ObjectMapper objectMapper, String query) throws SQLException {
+ return new QueryBuilder(config, dataSource, objectMapper, query, false);
+ }
+
+ public static QueryBuilder create(
+ Config config, DataSource dataSource, ObjectMapper objectMapper, String query,
+ boolean returnGeneratedKeys) throws SQLException {
+ return new QueryBuilder(config, dataSource, objectMapper, query, returnGeneratedKeys);
+ }
+
+ private List<Integer> indexes(String name) {
+ name = name.toLowerCase();
+ List<Integer> result = indexMap.get(name);
+ if (result == null) {
+ result = new LinkedList<>();
+ }
+ return result;
+ }
+
+ public QueryBuilder setBoolean(String name, boolean value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ statement.setBoolean(i, value);
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setInteger(String name, int value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ statement.setInt(i, value);
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setLong(String name, long value) throws SQLException {
+ return setLong(name, value, false);
+ }
+
+ public QueryBuilder setLong(String name, long value, boolean nullIfZero) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ if (value == 0 && nullIfZero) {
+ statement.setNull(i, Types.INTEGER);
+ } else {
+ statement.setLong(i, value);
+ }
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setDouble(String name, double value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ statement.setDouble(i, value);
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setString(String name, String value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ if (value == null) {
+ statement.setNull(i, Types.VARCHAR);
+ } else {
+ statement.setString(i, value);
+ }
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setDate(String name, Date value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ if (value == null) {
+ statement.setNull(i, Types.TIMESTAMP);
+ } else {
+ statement.setTimestamp(i, new Timestamp(value.getTime()));
+ }
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setBlob(String name, byte[] value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ if (value == null) {
+ statement.setNull(i, Types.BLOB);
+ } else {
+ statement.setBytes(i, value);
+ }
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setValue(String name, Object value) throws SQLException {
+ if (value instanceof Boolean) {
+ setBoolean(name, (Boolean) value);
+ } else if (value instanceof Integer) {
+ setInteger(name, (Integer) value);
+ } else if (value instanceof Long) {
+ setLong(name, (Long) value);
+ } else if (value instanceof Double) {
+ setDouble(name, (Double) value);
+ } else if (value instanceof String) {
+ setString(name, (String) value);
+ } else if (value instanceof Date) {
+ setDate(name, (Date) value);
+ }
+ return this;
+ }
+
+ public QueryBuilder setObject(Object object, List<String> columns) throws SQLException {
+
+ try {
+ for (String column : columns) {
+ Method method = object.getClass().getMethod(
+ "get" + Character.toUpperCase(column.charAt(0)) + column.substring(1));
+ if (method.getReturnType().equals(boolean.class)) {
+ setBoolean(column, (Boolean) method.invoke(object));
+ } else if (method.getReturnType().equals(int.class)) {
+ setInteger(column, (Integer) method.invoke(object));
+ } else if (method.getReturnType().equals(long.class)) {
+ setLong(column, (Long) method.invoke(object), column.endsWith("Id"));
+ } else if (method.getReturnType().equals(double.class)) {
+ setDouble(column, (Double) method.invoke(object));
+ } else if (method.getReturnType().equals(String.class)) {
+ setString(column, (String) method.invoke(object));
+ } else if (method.getReturnType().equals(Date.class)) {
+ setDate(column, (Date) method.invoke(object));
+ } else if (method.getReturnType().equals(byte[].class)) {
+ setBlob(column, (byte[]) method.invoke(object));
+ } else {
+ setString(column, objectMapper.writeValueAsString(method.invoke(object)));
+ }
+ }
+ } catch (ReflectiveOperationException | JsonProcessingException e) {
+ LOGGER.warn("Set object error", e);
+ }
+
+ return this;
+ }
+
+ private interface ResultSetProcessor<T> {
+ void process(T object, ResultSet resultSet) throws SQLException;
+ }
+
+ private <T> void addProcessors(
+ List<ResultSetProcessor<T>> processors,
+ final Class<?> parameterType, final Method method, final String name) {
+
+ if (parameterType.equals(boolean.class)) {
+ processors.add((object, resultSet) -> {
+ try {
+ method.invoke(object, resultSet.getBoolean(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ });
+ } else if (parameterType.equals(int.class)) {
+ processors.add((object, resultSet) -> {
+ try {
+ method.invoke(object, resultSet.getInt(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ });
+ } else if (parameterType.equals(long.class)) {
+ processors.add((object, resultSet) -> {
+ try {
+ method.invoke(object, resultSet.getLong(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ });
+ } else if (parameterType.equals(double.class)) {
+ processors.add((object, resultSet) -> {
+ try {
+ method.invoke(object, resultSet.getDouble(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ });
+ } else if (parameterType.equals(String.class)) {
+ processors.add((object, resultSet) -> {
+ try {
+ method.invoke(object, resultSet.getString(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ });
+ } else if (parameterType.equals(Date.class)) {
+ processors.add((object, resultSet) -> {
+ try {
+ Timestamp timestamp = resultSet.getTimestamp(name);
+ if (timestamp != null) {
+ method.invoke(object, new Date(timestamp.getTime()));
+ }
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ });
+ } else if (parameterType.equals(byte[].class)) {
+ processors.add((object, resultSet) -> {
+ try {
+ method.invoke(object, resultSet.getBytes(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ });
+ } else {
+ processors.add((object, resultSet) -> {
+ String value = resultSet.getString(name);
+ if (value != null && !value.isEmpty()) {
+ try {
+ method.invoke(object, objectMapper.readValue(value, parameterType));
+ } catch (InvocationTargetException | IllegalAccessException | IOException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ }
+ }
+
+ private void logQuery() {
+ if (config.getBoolean(Keys.LOGGER_QUERIES)) {
+ LOGGER.info(query);
+ }
+ }
+
+ public <T> List<T> executeQuery(Class<T> clazz) throws SQLException {
+ List<T> result = new LinkedList<>();
+
+ if (query != null) {
+
+ try {
+
+ logQuery();
+
+ try (ResultSet resultSet = statement.executeQuery()) {
+
+ ResultSetMetaData resultMetaData = resultSet.getMetaData();
+
+ List<ResultSetProcessor<T>> processors = new LinkedList<>();
+
+ Method[] methods = clazz.getMethods();
+
+ for (final Method method : methods) {
+ if (method.getName().startsWith("set") && method.getParameterTypes().length == 1) {
+
+ final String name = method.getName().substring(3);
+
+ // Check if column exists
+ boolean column = false;
+ for (int i = 1; i <= resultMetaData.getColumnCount(); i++) {
+ if (name.equalsIgnoreCase(resultMetaData.getColumnLabel(i))) {
+ column = true;
+ break;
+ }
+ }
+ if (!column) {
+ continue;
+ }
+
+ addProcessors(processors, method.getParameterTypes()[0], method, name);
+ }
+ }
+
+ while (resultSet.next()) {
+ try {
+ T object = clazz.getDeclaredConstructor().newInstance();
+ for (ResultSetProcessor<T> processor : processors) {
+ processor.process(object, resultSet);
+ }
+ result.add(object);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalArgumentException();
+ }
+ }
+ }
+
+ } finally {
+ statement.close();
+ connection.close();
+ }
+ }
+
+ return result;
+ }
+
+ public long executeUpdate() throws SQLException {
+
+ if (query != null) {
+ try {
+ logQuery();
+ statement.execute();
+ if (returnGeneratedKeys) {
+ ResultSet resultSet = statement.getGeneratedKeys();
+ if (resultSet.next()) {
+ return resultSet.getLong(1);
+ }
+ }
+ } finally {
+ statement.close();
+ connection.close();
+ }
+ }
+ return 0;
+ }
+
+ public List<Permission> executePermissionsQuery() throws SQLException {
+ List<Permission> result = new LinkedList<>();
+ if (query != null) {
+ try {
+ logQuery();
+ try (ResultSet resultSet = statement.executeQuery()) {
+ ResultSetMetaData resultMetaData = resultSet.getMetaData();
+ while (resultSet.next()) {
+ LinkedHashMap<String, Long> map = new LinkedHashMap<>();
+ for (int i = 1; i <= resultMetaData.getColumnCount(); i++) {
+ String label = resultMetaData.getColumnLabel(i);
+ map.put(label, resultSet.getLong(label));
+ }
+ result.add(new Permission(map));
+ }
+ }
+ } finally {
+ statement.close();
+ connection.close();
+ }
+ }
+
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/storage/QueryIgnore.java b/src/main/java/org/traccar/storage/QueryIgnore.java
new file mode 100644
index 000000000..553d4b9fc
--- /dev/null
+++ b/src/main/java/org/traccar/storage/QueryIgnore.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.storage;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface QueryIgnore {
+}
diff --git a/src/main/java/org/traccar/storage/Storage.java b/src/main/java/org/traccar/storage/Storage.java
new file mode 100644
index 000000000..55f5c22c0
--- /dev/null
+++ b/src/main/java/org/traccar/storage/Storage.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.storage;
+
+import org.traccar.model.BaseModel;
+import org.traccar.model.Permission;
+import org.traccar.storage.query.Request;
+
+import java.util.List;
+
+public abstract class Storage {
+
+ public abstract <T> List<T> getObjects(Class<T> clazz, Request request) throws StorageException;
+
+ public abstract <T> long addObject(T entity, Request request) throws StorageException;
+
+ public abstract <T> void updateObject(T entity, Request request) throws StorageException;
+
+ public abstract void removeObject(Class<?> clazz, Request request) throws StorageException;
+
+ public abstract List<Permission> getPermissions(
+ Class<? extends BaseModel> ownerClass, long ownerId,
+ Class<? extends BaseModel> propertyClass, long propertyId) throws StorageException;
+
+ public abstract void addPermission(Permission permission) throws StorageException;
+
+ public abstract void removePermission(Permission permission) throws StorageException;
+
+ public List<Permission> getPermissions(
+ Class<? extends BaseModel> ownerClass,
+ Class<? extends BaseModel> propertyClass) throws StorageException {
+ return getPermissions(ownerClass, 0, propertyClass, 0);
+ }
+
+ public <T> T getObject(Class<T> clazz, Request request) throws StorageException {
+ var objects = getObjects(clazz, request);
+ return objects.isEmpty() ? null : objects.get(0);
+ }
+
+}
diff --git a/src/main/java/org/traccar/storage/StorageException.java b/src/main/java/org/traccar/storage/StorageException.java
new file mode 100644
index 000000000..3f066cae6
--- /dev/null
+++ b/src/main/java/org/traccar/storage/StorageException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.storage;
+
+public class StorageException extends Exception {
+
+ public StorageException(String message) {
+ super(message);
+ }
+
+ public StorageException(Throwable cause) {
+ super(cause);
+ }
+
+ public StorageException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/src/main/java/org/traccar/storage/StorageName.java b/src/main/java/org/traccar/storage/StorageName.java
new file mode 100644
index 000000000..bf824c333
--- /dev/null
+++ b/src/main/java/org/traccar/storage/StorageName.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.storage;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface StorageName {
+ String value();
+}
diff --git a/src/main/java/org/traccar/storage/query/Columns.java b/src/main/java/org/traccar/storage/query/Columns.java
new file mode 100644
index 000000000..a00400b36
--- /dev/null
+++ b/src/main/java/org/traccar/storage/query/Columns.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.storage.query;
+
+import org.traccar.storage.QueryIgnore;
+
+import java.beans.Introspector;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public abstract class Columns {
+
+ public abstract List<String> getColumns(Class<?> clazz, String type);
+
+ protected List<String> getAllColumns(Class<?> clazz, String type) {
+ List<String> columns = new LinkedList<>();
+ Method[] methods = clazz.getMethods();
+ for (Method method : methods) {
+ int parameterCount = type.equals("set") ? 1 : 0;
+ if (method.getName().startsWith(type) && method.getParameterTypes().length == parameterCount
+ && !method.isAnnotationPresent(QueryIgnore.class)
+ && !method.getName().equals("getClass")) {
+ columns.add(Introspector.decapitalize(method.getName().substring(3)));
+ }
+ }
+ return columns;
+ }
+
+ public static class All extends Columns {
+ @Override
+ public List<String> getColumns(Class<?> clazz, String type) {
+ return getAllColumns(clazz, type);
+ }
+ }
+
+ public static class Include extends Columns {
+ private final List<String> columns;
+
+ public Include(String... columns) {
+ this.columns = Arrays.stream(columns).collect(Collectors.toList());
+ }
+
+ @Override
+ public List<String> getColumns(Class<?> clazz, String type) {
+ return columns;
+ }
+ }
+
+ public static class Exclude extends Columns {
+ private final Set<String> columns;
+
+ public Exclude(String... columns) {
+ this.columns = Arrays.stream(columns).collect(Collectors.toSet());
+ }
+
+ @Override
+ public List<String> getColumns(Class<?> clazz, String type) {
+ return getAllColumns(clazz, type).stream()
+ .filter(column -> !columns.contains(column))
+ .collect(Collectors.toList());
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/storage/query/Condition.java b/src/main/java/org/traccar/storage/query/Condition.java
new file mode 100644
index 000000000..08b199052
--- /dev/null
+++ b/src/main/java/org/traccar/storage/query/Condition.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.storage.query;
+
+import org.traccar.model.GroupedModel;
+
+import java.util.List;
+
+public interface Condition {
+
+ static Condition merge(List<Condition> conditions) {
+ Condition result = null;
+ var iterator = conditions.iterator();
+ if (iterator.hasNext()) {
+ result = iterator.next();
+ while (iterator.hasNext()) {
+ result = new Condition.And(result, iterator.next());
+ }
+ }
+ return result;
+ }
+
+ class Equals extends Compare {
+ public Equals(String column, Object value) {
+ super(column, "=", column, value);
+ }
+ }
+
+ class Compare implements Condition {
+ private final String column;
+ private final String operator;
+ private final String variable;
+ private final Object value;
+
+ public Compare(String column, String operator, String variable, Object value) {
+ this.column = column;
+ this.operator = operator;
+ this.variable = variable;
+ this.value = value;
+ }
+
+ public String getColumn() {
+ return column;
+ }
+
+ public String getOperator() {
+ return operator;
+ }
+
+ public String getVariable() {
+ return variable;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+ }
+
+ class Between implements Condition {
+ private final String column;
+ private final String fromVariable;
+ private final Object fromValue;
+ private final String toVariable;
+ private final Object toValue;
+
+ public Between(String column, String fromVariable, Object fromValue, String toVariable, Object toValue) {
+ this.column = column;
+ this.fromVariable = fromVariable;
+ this.fromValue = fromValue;
+ this.toVariable = toVariable;
+ this.toValue = toValue;
+ }
+
+ public String getColumn() {
+ return column;
+ }
+
+ public String getFromVariable() {
+ return fromVariable;
+ }
+
+ public Object getFromValue() {
+ return fromValue;
+ }
+
+ public String getToVariable() {
+ return toVariable;
+ }
+
+ public Object getToValue() {
+ return toValue;
+ }
+ }
+
+ class Or extends Binary {
+ public Or(Condition first, Condition second) {
+ super(first, second, "OR");
+ }
+ }
+
+ class And extends Binary {
+ public And(Condition first, Condition second) {
+ super(first, second, "AND");
+ }
+ }
+
+ class Binary implements Condition {
+ private final Condition first;
+ private final Condition second;
+ private final String operator;
+
+ public Binary(Condition first, Condition second, String operator) {
+ this.first = first;
+ this.second = second;
+ this.operator = operator;
+ }
+
+ public Condition getFirst() {
+ return first;
+ }
+
+ public Condition getSecond() {
+ return second;
+ }
+
+ public String getOperator() {
+ return operator;
+ }
+ }
+
+ class Permission implements Condition {
+ private final Class<?> ownerClass;
+ private final long ownerId;
+ private final Class<?> propertyClass;
+ private final long propertyId;
+ private final boolean excludeGroups;
+
+ private Permission(
+ Class<?> ownerClass, long ownerId, Class<?> propertyClass, long propertyId, boolean excludeGroups) {
+ this.ownerClass = ownerClass;
+ this.ownerId = ownerId;
+ this.propertyClass = propertyClass;
+ this.propertyId = propertyId;
+ this.excludeGroups = excludeGroups;
+ }
+
+ public Permission(Class<?> ownerClass, long ownerId, Class<?> propertyClass) {
+ this(ownerClass, ownerId, propertyClass, 0, false);
+ }
+
+ public Permission(Class<?> ownerClass, Class<?> propertyClass, long propertyId) {
+ this(ownerClass, 0, propertyClass, propertyId, false);
+ }
+
+ public Permission excludeGroups() {
+ return new Permission(this.ownerClass, this.ownerId, this.propertyClass, this.propertyId, true);
+ }
+
+ public Class<?> getOwnerClass() {
+ return ownerClass;
+ }
+
+ public long getOwnerId() {
+ return ownerId;
+ }
+
+ public Class<?> getPropertyClass() {
+ return propertyClass;
+ }
+
+ public long getPropertyId() {
+ return propertyId;
+ }
+
+ public boolean getIncludeGroups() {
+ boolean ownerGroupModel = GroupedModel.class.isAssignableFrom(ownerClass);
+ boolean propertyGroupModel = GroupedModel.class.isAssignableFrom(propertyClass);
+ return (ownerGroupModel || propertyGroupModel) && !excludeGroups;
+ }
+ }
+
+ class LatestPositions implements Condition {
+ private final long deviceId;
+
+ public LatestPositions(long deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ public LatestPositions() {
+ this(0);
+ }
+
+ public long getDeviceId() {
+ return deviceId;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/storage/query/Limit.java b/src/main/java/org/traccar/storage/query/Limit.java
new file mode 100644
index 000000000..9673e5426
--- /dev/null
+++ b/src/main/java/org/traccar/storage/query/Limit.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.storage.query;
+
+public class Limit {
+
+ private final int value;
+
+ public Limit(int value) {
+ this.value = value;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+}
diff --git a/src/main/java/org/traccar/storage/query/Order.java b/src/main/java/org/traccar/storage/query/Order.java
new file mode 100644
index 000000000..f10970381
--- /dev/null
+++ b/src/main/java/org/traccar/storage/query/Order.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.storage.query;
+
+public class Order {
+
+ private final String column;
+ private final boolean descending;
+ private final int limit;
+
+ public Order(String column) {
+ this(column, false, 0);
+ }
+
+ public Order(String column, boolean descending, int limit) {
+ this.column = column;
+ this.descending = descending;
+ this.limit = limit;
+ }
+
+ public String getColumn() {
+ return column;
+ }
+
+ public boolean getDescending() {
+ return descending;
+ }
+
+ public int getLimit() {
+ return limit;
+ }
+
+}
diff --git a/src/main/java/org/traccar/storage/query/Request.java b/src/main/java/org/traccar/storage/query/Request.java
new file mode 100644
index 000000000..b9c2c963c
--- /dev/null
+++ b/src/main/java/org/traccar/storage/query/Request.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.storage.query;
+
+public class Request {
+
+ private final Columns columns;
+ private final Condition condition;
+ private final Order order;
+
+ public Request(Columns columns) {
+ this(columns, null, null);
+ }
+
+ public Request(Condition condition) {
+ this(null, condition, null);
+ }
+
+ public Request(Columns columns, Condition condition) {
+ this(columns, condition, null);
+ }
+
+ public Request(Columns columns, Order order) {
+ this(columns, null, order);
+ }
+
+ public Request(Columns columns, Condition condition, Order order) {
+ this.columns = columns;
+ this.condition = condition;
+ this.order = order;
+ }
+
+ public Columns getColumns() {
+ return columns;
+ }
+
+ public Condition getCondition() {
+ return condition;
+ }
+
+ public Order getOrder() {
+ return order;
+ }
+
+}