diff options
author | Yuriy Piskarev <yuriy.piskarev@gmail.com> | 2023-08-24 14:16:17 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-24 14:16:17 +0300 |
commit | ae406c7b49a72de24d81fd74386d9638342c90ee (patch) | |
tree | 6fbcf557375b98e926c78af9c757e62c79d72a1b /src/main/java/org/traccar/storage | |
parent | 56ff656c908b19feb2fa3dcffa48cc3bcdfe9b3b (diff) | |
parent | 9aeedc90da24848ff97227d6f281eb4d1e1506ef (diff) | |
download | trackermap-server-ae406c7b49a72de24d81fd74386d9638342c90ee.tar.gz trackermap-server-ae406c7b49a72de24d81fd74386d9638342c90ee.tar.bz2 trackermap-server-ae406c7b49a72de24d81fd74386d9638342c90ee.zip |
Merge branch 'traccar:master' into master
Diffstat (limited to 'src/main/java/org/traccar/storage')
-rw-r--r-- | src/main/java/org/traccar/storage/DatabaseModule.java | 103 | ||||
-rw-r--r-- | src/main/java/org/traccar/storage/DatabaseStorage.java | 243 | ||||
-rw-r--r-- | src/main/java/org/traccar/storage/MemoryStorage.java | 146 | ||||
-rw-r--r-- | src/main/java/org/traccar/storage/QueryBuilder.java | 96 | ||||
-rw-r--r-- | src/main/java/org/traccar/storage/QueryExtended.java | 27 | ||||
-rw-r--r-- | src/main/java/org/traccar/storage/Storage.java | 25 | ||||
-rw-r--r-- | src/main/java/org/traccar/storage/StorageException.java | 15 | ||||
-rw-r--r-- | src/main/java/org/traccar/storage/StorageName.java | 15 | ||||
-rw-r--r-- | src/main/java/org/traccar/storage/query/Columns.java | 20 | ||||
-rw-r--r-- | src/main/java/org/traccar/storage/query/Condition.java | 104 | ||||
-rw-r--r-- | src/main/java/org/traccar/storage/query/Limit.java | 15 | ||||
-rw-r--r-- | src/main/java/org/traccar/storage/query/Order.java | 25 | ||||
-rw-r--r-- | src/main/java/org/traccar/storage/query/Request.java | 27 |
13 files changed, 731 insertions, 130 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..9d9e5bd5e --- /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 jakarta.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 index d73dc7b25..d20429319 100644 --- a/src/main/java/org/traccar/storage/DatabaseStorage.java +++ b/src/main/java/org/traccar/storage/DatabaseStorage.java @@ -1,15 +1,37 @@ +/* + * 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.Limit; import org.traccar.storage.query.Order; import org.traccar.storage.query.Request; +import jakarta.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; @@ -17,22 +39,37 @@ 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; - public DatabaseStorage(DataSource dataSource) { + @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 "); - query.append(formatColumns(request.getColumns(), clazz, "get", c -> c)); - query.append(" FROM ").append(getTableName(clazz)); + 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())); - query.append(formatLimit(request.getLimit())); try { - QueryBuilder builder = QueryBuilder.create(dataSource, query.toString()); + 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()); } @@ -44,16 +81,17 @@ public class DatabaseStorage extends Storage { @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(getTableName(entity.getClass())); + query.append(getStorageName(entity.getClass())); query.append("("); - query.append(formatColumns(request.getColumns(), entity.getClass(), "set", c -> c)); + query.append(formatColumns(columns, c -> c)); query.append(") VALUES ("); - query.append(formatColumns(request.getColumns(), entity.getClass(), "set", c -> ':' + c)); + query.append(formatColumns(columns, c -> ':' + c)); query.append(")"); try { - QueryBuilder builder = QueryBuilder.create(dataSource, query.toString(), true); - builder.setObject(entity); + QueryBuilder builder = QueryBuilder.create(config, dataSource, objectMapper, query.toString(), true); + builder.setObject(entity, columns); return builder.executeUpdate(); } catch (SQLException e) { throw new StorageException(e); @@ -62,14 +100,15 @@ public class DatabaseStorage extends Storage { @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(getTableName(entity.getClass())); + query.append(getStorageName(entity.getClass())); query.append(" SET "); - query.append(formatColumns(request.getColumns(), entity.getClass(), "set", c -> c + " = :" + c)); + query.append(formatColumns(columns, c -> c + " = :" + c)); query.append(formatCondition(request.getCondition())); try { - QueryBuilder builder = QueryBuilder.create(dataSource, query.toString()); - builder.setObject(entity); + 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()); } @@ -82,10 +121,10 @@ public class DatabaseStorage extends Storage { @Override public void removeObject(Class<?> clazz, Request request) throws StorageException { StringBuilder query = new StringBuilder("DELETE FROM "); - query.append(getTableName(clazz)); + query.append(getStorageName(clazz)); query.append(formatCondition(request.getCondition())); try { - QueryBuilder builder = QueryBuilder.create(dataSource, query.toString()); + 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()); } @@ -96,11 +135,25 @@ public class DatabaseStorage extends Storage { } @Override - public List<Permission> getPermissions(Class<?> ownerClass, Class<?> propertyClass) throws StorageException { + 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(dataSource, query.toString()); + 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); @@ -115,7 +168,7 @@ public class DatabaseStorage extends Storage { query.append(permission.get().keySet().stream().map(key -> ':' + key).collect(Collectors.joining(", "))); query.append(")"); try { - QueryBuilder builder = QueryBuilder.create(dataSource, query.toString(), true); + QueryBuilder builder = QueryBuilder.create(config, dataSource, objectMapper, query.toString(), true); for (var entry : permission.get().entrySet()) { builder.setLong(entry.getKey(), entry.getValue()); } @@ -133,7 +186,7 @@ public class DatabaseStorage extends Storage { query.append(permission .get().keySet().stream().map(key -> key + " = :" + key).collect(Collectors.joining(" AND "))); try { - QueryBuilder builder = QueryBuilder.create(dataSource, query.toString(), true); + QueryBuilder builder = QueryBuilder.create(config, dataSource, objectMapper, query.toString(), true); for (var entry : permission.get().entrySet()) { builder.setLong(entry.getKey(), entry.getValue()); } @@ -143,7 +196,7 @@ public class DatabaseStorage extends Storage { } } - private String getTableName(Class<?> clazz) throws StorageException { + private String getStorageName(Class<?> clazz) throws StorageException { StorageName storageName = clazz.getAnnotation(StorageName.class); if (storageName == null) { throw new StorageException("StorageName annotation is missing"); @@ -154,32 +207,43 @@ public class DatabaseStorage extends Storage { private Map<String, Object> getConditionVariables(Condition genericCondition) { Map<String, Object> results = new HashMap<>(); if (genericCondition instanceof Condition.Compare) { - Condition.Compare condition = (Condition.Compare) genericCondition; + var condition = (Condition.Compare) genericCondition; if (condition.getValue() != null) { results.put(condition.getVariable(), condition.getValue()); } } else if (genericCondition instanceof Condition.Between) { - Condition.Between condition = (Condition.Between) genericCondition; + var condition = (Condition.Between) genericCondition; results.put(condition.getFromVariable(), condition.getFromValue()); results.put(condition.getToVariable(), condition.getToValue()); } else if (genericCondition instanceof Condition.Binary) { - Condition.Binary condition = (Condition.Binary) genericCondition; + 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( - Columns columns, Class<?> clazz, String type, Function<String, String> mapper) { - return columns.getColumns(clazz, type).stream().map(mapper).collect(Collectors.joining(", ")); + private String formatColumns(List<String> columns, Function<String, String> mapper) { + return columns.stream().map(mapper).collect(Collectors.joining(", ")); } - private String formatCondition(Condition genericCondition) { + private String formatCondition(Condition genericCondition) throws StorageException { return formatCondition(genericCondition, true); } - private String formatCondition(Condition genericCondition, boolean appendWhere) { + private String formatCondition(Condition genericCondition, boolean appendWhere) throws StorageException { StringBuilder result = new StringBuilder(); if (genericCondition != null) { if (appendWhere) { @@ -187,7 +251,7 @@ public class DatabaseStorage extends Storage { } if (genericCondition instanceof Condition.Compare) { - Condition.Compare condition = (Condition.Compare) genericCondition; + var condition = (Condition.Compare) genericCondition; result.append(condition.getColumn()); result.append(" "); result.append(condition.getOperator()); @@ -196,7 +260,7 @@ public class DatabaseStorage extends Storage { } else if (genericCondition instanceof Condition.Between) { - Condition.Between condition = (Condition.Between) genericCondition; + var condition = (Condition.Between) genericCondition; result.append(condition.getColumn()); result.append(" BETWEEN :"); result.append(condition.getFromVariable()); @@ -205,13 +269,31 @@ public class DatabaseStorage extends Storage { } else if (genericCondition instanceof Condition.Binary) { - Condition.Binary condition = (Condition.Binary) genericCondition; + 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(); @@ -225,16 +307,103 @@ public class DatabaseStorage extends Storage { 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 formatLimit(Limit limit) { + private String formatPermissionQuery(Condition.Permission condition) throws StorageException { StringBuilder result = new StringBuilder(); - if (limit != null) { - result.append(" LIMIT "); - result.append(limit.getValue()); + + 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 index 9cfe30a2b..9b5db1209 100644 --- a/src/main/java/org/traccar/storage/MemoryStorage.java +++ b/src/main/java/org/traccar/storage/MemoryStorage.java @@ -1,36 +1,172 @@ +/* + * Copyright 2022 - 2023 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.model.Server; +import org.traccar.storage.query.Condition; import org.traccar.storage.query.Request; +import java.beans.Introspector; +import java.lang.reflect.Method; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; public class MemoryStorage extends Storage { + private final Map<Class<?>, Map<Long, Object>> objects = new HashMap<>(); private final Map<Pair<Class<?>, Class<?>>, Set<Pair<Long, Long>>> permissions = new HashMap<>(); + private final AtomicLong increment = new AtomicLong(); + + public MemoryStorage() { + Server server = new Server(); + server.setId(1); + server.setRegistration(true); + objects.put(Server.class, Map.of(server.getId(), server)); + } + @Override public <T> List<T> getObjects(Class<T> clazz, Request request) { - return null; + return objects.computeIfAbsent(clazz, key -> new HashMap<>()).values().stream() + .filter(object -> checkCondition(request.getCondition(), object)) + .map(object -> (T) object) + .collect(Collectors.toList()); + } + + private boolean checkCondition(Condition genericCondition, Object object) { + if (genericCondition == null) { + return true; + } + + if (genericCondition instanceof Condition.Compare) { + + var condition = (Condition.Compare) genericCondition; + Object value = retrieveValue(object, condition.getVariable()); + int result = ((Comparable) value).compareTo(condition.getValue()); + switch (condition.getOperator()) { + case "<": + return result < 0; + case "<=": + return result <= 0; + case ">": + return result > 0; + case ">=": + return result >= 0; + case "=": + return result == 0; + default: + throw new RuntimeException("Unsupported comparison condition"); + } + + } else if (genericCondition instanceof Condition.Between) { + + var condition = (Condition.Between) genericCondition; + Object fromValue = retrieveValue(object, condition.getFromVariable()); + int fromResult = ((Comparable) fromValue).compareTo(condition.getFromValue()); + Object toValue = retrieveValue(object, condition.getToVariable()); + int toResult = ((Comparable) toValue).compareTo(condition.getToValue()); + return fromResult >= 0 && toResult <= 0; + + } else if (genericCondition instanceof Condition.Binary) { + + var condition = (Condition.Binary) genericCondition; + if (condition.getOperator().equals("AND")) { + return checkCondition(condition.getFirst(), object) && checkCondition(condition.getSecond(), object); + } else if (condition.getOperator().equals("OR")) { + return checkCondition(condition.getFirst(), object) || checkCondition(condition.getSecond(), object); + } + + } else if (genericCondition instanceof Condition.Permission) { + + var condition = (Condition.Permission) genericCondition; + long id = (Long) retrieveValue(object, "id"); + return getPermissionsSet(condition.getOwnerClass(), condition.getPropertyClass()).stream() + .anyMatch(pair -> { + if (condition.getOwnerId() > 0) { + return pair.getFirst() == condition.getOwnerId() && pair.getSecond() == id; + } else { + return pair.getFirst() == id && pair.getSecond() == condition.getPropertyId(); + } + }); + + } else if (genericCondition instanceof Condition.LatestPositions) { + + return false; + + } + + return false; + } + + private Object retrieveValue(Object object, String key) { + try { + Method method = object.getClass().getMethod( + "get" + Character.toUpperCase(key.charAt(0)) + key.substring(1)); + return method.invoke(object); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } } @Override public <T> long addObject(T entity, Request request) { - return 0; + long id = increment.incrementAndGet(); + objects.computeIfAbsent(entity.getClass(), key -> new HashMap<>()).put(id, entity); + return id; } @Override public <T> void updateObject(T entity, Request request) { + Set<String> columns = new HashSet<>(request.getColumns().getColumns(entity.getClass(), "get")); + Collection<Object> items; + if (request.getCondition() != null) { + long id = (Long) ((Condition.Equals) request.getCondition()).getValue(); + items = List.of(objects.computeIfAbsent(entity.getClass(), key -> new HashMap<>()).get(id)); + } else { + items = objects.computeIfAbsent(entity.getClass(), key -> new HashMap<>()).values(); + } + for (Method setter : entity.getClass().getMethods()) { + if (setter.getName().startsWith("set") && setter.getParameterCount() == 1 + && columns.contains(Introspector.decapitalize(setter.getName()))) { + try { + Method getter = entity.getClass().getMethod(setter.getName().replaceFirst("set", "get")); + Object value = getter.invoke(entity); + for (Object object : items) { + setter.invoke(object, value); + } + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + } } @Override public void removeObject(Class<?> clazz, Request request) { + long id = (Long) ((Condition.Equals) request.getCondition()).getValue(); + objects.computeIfAbsent(clazz, key -> new HashMap<>()).remove(id); } private Set<Pair<Long, Long>> getPermissionsSet(Class<?> ownerClass, Class<?> propertyClass) { @@ -38,8 +174,12 @@ public class MemoryStorage extends Storage { } @Override - public List<Permission> getPermissions(Class<?> ownerClass, Class<?> propertyClass) { + 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()); } diff --git a/src/main/java/org/traccar/storage/QueryBuilder.java b/src/main/java/org/traccar/storage/QueryBuilder.java index da8002f0b..2f4c07406 100644 --- a/src/main/java/org/traccar/storage/QueryBuilder.java +++ b/src/main/java/org/traccar/storage/QueryBuilder.java @@ -16,9 +16,11 @@ 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.Context; +import org.traccar.config.Config; +import org.traccar.config.Keys; import org.traccar.model.Permission; import javax.sql.DataSource; @@ -40,17 +42,25 @@ 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(DataSource dataSource, String query, boolean returnGeneratedKeys) throws SQLException { + 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) { @@ -125,13 +135,15 @@ public final class QueryBuilder { return parsedQuery.toString(); } - public static QueryBuilder create(DataSource dataSource, String query) throws SQLException { - return new QueryBuilder(dataSource, query, false); + 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( - DataSource dataSource, String query, boolean returnGeneratedKeys) throws SQLException { - return new QueryBuilder(dataSource, query, returnGeneratedKeys); + 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) { @@ -271,35 +283,32 @@ public final class QueryBuilder { return this; } - public QueryBuilder setObject(Object object) throws SQLException { - - Method[] methods = object.getClass().getMethods(); - - for (Method method : methods) { - if (method.getName().startsWith("get") && method.getParameterTypes().length == 0) { - String name = method.getName().substring(3); - try { - if (method.getReturnType().equals(boolean.class)) { - setBoolean(name, (Boolean) method.invoke(object)); - } else if (method.getReturnType().equals(int.class)) { - setInteger(name, (Integer) method.invoke(object)); - } else if (method.getReturnType().equals(long.class)) { - setLong(name, (Long) method.invoke(object), name.endsWith("Id")); - } else if (method.getReturnType().equals(double.class)) { - setDouble(name, (Double) method.invoke(object)); - } else if (method.getReturnType().equals(String.class)) { - setString(name, (String) method.invoke(object)); - } else if (method.getReturnType().equals(Date.class)) { - setDate(name, (Date) method.invoke(object)); - } else if (method.getReturnType().equals(byte[].class)) { - setBlob(name, (byte[]) method.invoke(object)); - } else { - setString(name, Context.getObjectMapper().writeValueAsString(method.invoke(object))); - } - } catch (IllegalAccessException | InvocationTargetException | JsonProcessingException error) { - LOGGER.warn("Get property error", error); + 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; @@ -309,15 +318,6 @@ public final class QueryBuilder { void process(T object, ResultSet resultSet) throws SQLException; } - public <T> T executeQuerySingle(Class<T> clazz) throws SQLException { - List<T> result = executeQuery(clazz); - if (!result.isEmpty()) { - return result.iterator().next(); - } else { - return null; - } - } - private <T> void addProcessors( List<ResultSetProcessor<T>> processors, final Class<?> parameterType, final Method method, final String name) { @@ -386,7 +386,7 @@ public final class QueryBuilder { String value = resultSet.getString(name); if (value != null && !value.isEmpty()) { try { - method.invoke(object, Context.getObjectMapper().readValue(value, parameterType)); + method.invoke(object, objectMapper.readValue(value, parameterType)); } catch (InvocationTargetException | IllegalAccessException | IOException error) { LOGGER.warn("Set property error", error); } @@ -395,6 +395,12 @@ public final class QueryBuilder { } } + 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<>(); @@ -402,6 +408,8 @@ public final class QueryBuilder { try { + logQuery(); + try (ResultSet resultSet = statement.executeQuery()) { ResultSetMetaData resultMetaData = resultSet.getMetaData(); @@ -457,6 +465,7 @@ public final class QueryBuilder { if (query != null) { try { + logQuery(); statement.execute(); if (returnGeneratedKeys) { ResultSet resultSet = statement.getGeneratedKeys(); @@ -476,6 +485,7 @@ public final class QueryBuilder { List<Permission> result = new LinkedList<>(); if (query != null) { try { + logQuery(); try (ResultSet resultSet = statement.executeQuery()) { ResultSetMetaData resultMetaData = resultSet.getMetaData(); while (resultSet.next()) { diff --git a/src/main/java/org/traccar/storage/QueryExtended.java b/src/main/java/org/traccar/storage/QueryExtended.java deleted file mode 100644 index 3796f1f40..000000000 --- a/src/main/java/org/traccar/storage/QueryExtended.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2017 Anton Tananaev (anton@traccar.org) - * Copyright 2017 Andrey Kunitsyn (andrey@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 QueryExtended { -} diff --git a/src/main/java/org/traccar/storage/Storage.java b/src/main/java/org/traccar/storage/Storage.java index 22c48cae0..55f5c22c0 100644 --- a/src/main/java/org/traccar/storage/Storage.java +++ b/src/main/java/org/traccar/storage/Storage.java @@ -1,5 +1,21 @@ +/* + * 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; @@ -16,12 +32,19 @@ public abstract class Storage { public abstract void removeObject(Class<?> clazz, Request request) throws StorageException; public abstract List<Permission> getPermissions( - Class<?> ownerClass, Class<?> propertyClass) throws StorageException; + 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 index 6c1e9c2ff..3f066cae6 100644 --- a/src/main/java/org/traccar/storage/StorageException.java +++ b/src/main/java/org/traccar/storage/StorageException.java @@ -1,3 +1,18 @@ +/* + * 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 { diff --git a/src/main/java/org/traccar/storage/StorageName.java b/src/main/java/org/traccar/storage/StorageName.java index b6fa55e02..bf824c333 100644 --- a/src/main/java/org/traccar/storage/StorageName.java +++ b/src/main/java/org/traccar/storage/StorageName.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/src/main/java/org/traccar/storage/query/Columns.java b/src/main/java/org/traccar/storage/query/Columns.java index 196d2281c..a00400b36 100644 --- a/src/main/java/org/traccar/storage/query/Columns.java +++ b/src/main/java/org/traccar/storage/query/Columns.java @@ -1,8 +1,23 @@ +/* + * 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.QueryExtended; import org.traccar.storage.QueryIgnore; +import java.beans.Introspector; import java.lang.reflect.Method; import java.util.Arrays; import java.util.LinkedList; @@ -21,9 +36,8 @@ public abstract class Columns { int parameterCount = type.equals("set") ? 1 : 0; if (method.getName().startsWith(type) && method.getParameterTypes().length == parameterCount && !method.isAnnotationPresent(QueryIgnore.class) - && !method.isAnnotationPresent(QueryExtended.class) && !method.getName().equals("getClass")) { - columns.add(method.getName().substring(3).toLowerCase()); + columns.add(Introspector.decapitalize(method.getName().substring(3))); } } return columns; diff --git a/src/main/java/org/traccar/storage/query/Condition.java b/src/main/java/org/traccar/storage/query/Condition.java index 82c8e8479..08b199052 100644 --- a/src/main/java/org/traccar/storage/query/Condition.java +++ b/src/main/java/org/traccar/storage/query/Condition.java @@ -1,14 +1,41 @@ +/* + * 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 { - class Equals extends Compare { - public Equals(String column, String variable) { - this(column, variable, null); + 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; + } - public Equals(String column, String variable, Object value) { - super(column, "=", variable, value); + class Equals extends Compare { + public Equals(String column, Object value) { + super(column, "=", column, value); } } @@ -114,4 +141,71 @@ public interface Condition { } } + 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 index 4a20f0ce2..9673e5426 100644 --- a/src/main/java/org/traccar/storage/query/Limit.java +++ b/src/main/java/org/traccar/storage/query/Limit.java @@ -1,3 +1,18 @@ +/* + * 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 { diff --git a/src/main/java/org/traccar/storage/query/Order.java b/src/main/java/org/traccar/storage/query/Order.java index 6852c5ba8..f10970381 100644 --- a/src/main/java/org/traccar/storage/query/Order.java +++ b/src/main/java/org/traccar/storage/query/Order.java @@ -1,17 +1,34 @@ +/* + * 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(false, column); + this(column, false, 0); } - public Order(boolean descending, String column) { + public Order(String column, boolean descending, int limit) { this.column = column; this.descending = descending; + this.limit = limit; } public String getColumn() { @@ -22,4 +39,8 @@ public class Order { 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 index a98dd48f7..b9c2c963c 100644 --- a/src/main/java/org/traccar/storage/query/Request.java +++ b/src/main/java/org/traccar/storage/query/Request.java @@ -1,3 +1,18 @@ +/* + * 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 { @@ -5,7 +20,6 @@ public class Request { private final Columns columns; private final Condition condition; private final Order order; - private final Limit limit; public Request(Columns columns) { this(columns, null, null); @@ -19,15 +33,14 @@ public class Request { this(columns, condition, null); } - public Request(Columns columns, Condition condition, Order order) { - this(columns, condition, order, null); + public Request(Columns columns, Order order) { + this(columns, null, order); } - public Request(Columns columns, Condition condition, Order order, Limit limit) { + public Request(Columns columns, Condition condition, Order order) { this.columns = columns; this.condition = condition; this.order = order; - this.limit = limit; } public Columns getColumns() { @@ -42,8 +55,4 @@ public class Request { return order; } - public Limit getLimit() { - return limit; - } - } |