aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.gradle36
-rw-r--r--setup/default.xml1
-rw-r--r--setup/traccar.iss2
-rw-r--r--src/main/java/org/traccar/BasePipelineFactory.java6
-rw-r--r--src/main/java/org/traccar/BaseProtocolDecoder.java3
-rw-r--r--src/main/java/org/traccar/WindowsService.java4
-rw-r--r--src/main/java/org/traccar/api/AsyncSocket.java40
-rw-r--r--src/main/java/org/traccar/api/BaseObjectResource.java10
-rw-r--r--src/main/java/org/traccar/api/resource/AttributeResource.java6
-rw-r--r--src/main/java/org/traccar/api/resource/DeviceResource.java9
-rw-r--r--src/main/java/org/traccar/api/resource/PermissionsResource.java8
-rw-r--r--src/main/java/org/traccar/api/resource/ServerResource.java28
-rw-r--r--src/main/java/org/traccar/api/resource/UserResource.java2
-rw-r--r--src/main/java/org/traccar/broadcast/BaseBroadcastService.java37
-rw-r--r--src/main/java/org/traccar/broadcast/BroadcastInterface.java16
-rw-r--r--src/main/java/org/traccar/broadcast/MulticastBroadcastService.java2
-rw-r--r--src/main/java/org/traccar/broadcast/RedisBroadcastService.java11
-rw-r--r--src/main/java/org/traccar/config/Keys.java63
-rw-r--r--src/main/java/org/traccar/database/DeviceLookupService.java2
-rw-r--r--src/main/java/org/traccar/database/NotificationManager.java5
-rw-r--r--src/main/java/org/traccar/handler/StandardLoggingHandler.java65
-rw-r--r--src/main/java/org/traccar/model/LogRecord.java79
-rw-r--r--src/main/java/org/traccar/notification/NotificationFormatter.java5
-rw-r--r--src/main/java/org/traccar/notificators/NotificatorFirebase.java8
-rw-r--r--src/main/java/org/traccar/notificators/NotificatorMail.java2
-rw-r--r--src/main/java/org/traccar/notificators/NotificatorPushover.java2
-rw-r--r--src/main/java/org/traccar/notificators/NotificatorSms.java2
-rw-r--r--src/main/java/org/traccar/notificators/NotificatorTelegram.java2
-rw-r--r--src/main/java/org/traccar/notificators/NotificatorTraccar.java7
-rw-r--r--src/main/java/org/traccar/notificators/NotificatorWeb.java2
-rw-r--r--src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java54
-rw-r--r--src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java15
-rw-r--r--src/main/java/org/traccar/protocol/MeitrackProtocolDecoder.java3
-rw-r--r--src/main/java/org/traccar/protocol/Minifinder2Protocol.java3
-rw-r--r--src/main/java/org/traccar/protocol/Minifinder2ProtocolEncoder.java7
-rw-r--r--src/main/java/org/traccar/protocol/Mta6ProtocolDecoder.java2
-rw-r--r--src/main/java/org/traccar/protocol/RstProtocolDecoder.java10
-rw-r--r--src/main/java/org/traccar/protocol/SuntechProtocolDecoder.java3
-rw-r--r--src/main/java/org/traccar/schedule/ScheduleManager.java3
-rw-r--r--src/main/java/org/traccar/schedule/TaskExpirations.java130
-rw-r--r--src/main/java/org/traccar/session/ConnectionManager.java65
-rw-r--r--src/main/java/org/traccar/session/Endpoint.java58
-rw-r--r--src/main/java/org/traccar/session/cache/CacheGraph.java139
-rw-r--r--src/main/java/org/traccar/session/cache/CacheKey.java4
-rw-r--r--src/main/java/org/traccar/session/cache/CacheManager.java388
-rw-r--r--src/main/java/org/traccar/session/cache/CacheNode.java40
-rw-r--r--src/main/java/org/traccar/session/cache/CacheValue.java53
-rw-r--r--src/main/java/org/traccar/session/cache/WeakValueMap.java44
-rw-r--r--src/test/java/org/traccar/handler/events/MaintenanceEventHandlerTest.java17
-rw-r--r--src/test/java/org/traccar/protocol/Gl100ProtocolDecoderTest.java3
-rw-r--r--src/test/java/org/traccar/protocol/Gl200TextProtocolDecoderTest.java9
-rw-r--r--src/test/java/org/traccar/protocol/RstProtocolDecoderTest.java4
-rw-r--r--src/test/java/org/traccar/protocol/SuntechProtocolDecoderTest.java18
-rw-r--r--swagger.json2
-rw-r--r--templates/full/deviceExpiration.vm7
-rw-r--r--templates/full/deviceExpirationReminder.vm7
-rw-r--r--templates/full/userExpiration.vm7
-rw-r--r--templates/full/userExpirationReminder.vm7
m---------traccar-web0
59 files changed, 986 insertions, 581 deletions
diff --git a/build.gradle b/build.gradle
index a51ebaff0..41baa7f89 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,7 +14,7 @@ compileJava.options.encoding = "UTF-8"
jar.destinationDirectory = file("$projectDir/target")
checkstyle {
- toolVersion = "10.12.0"
+ toolVersion = "10.12.5"
configFile = "gradle/checkstyle.xml" as File
checkstyleTest.enabled = false
}
@@ -27,11 +27,11 @@ enforce {
ext {
guiceVersion = "7.0.0"
- jettyVersion = "11.0.18"
- jerseyVersion = "3.1.3"
- jacksonVersion = "2.15.2" // same version as jersey-media-json-jackson dependency
- protobufVersion = "3.24.0"
- jxlsVersion = "2.13.0"
+ jettyVersion = "11.0.19"
+ jerseyVersion = "3.1.5"
+ jacksonVersion = "2.15.3" // same version as jersey-media-json-jackson dependency
+ protobufVersion = "3.25.2"
+ jxlsVersion = "2.14.0"
junitVersion = "5.10.1"
}
@@ -45,12 +45,12 @@ dependencies {
implementation "commons-codec:commons-codec:1.16.0"
implementation "com.h2database:h2:2.2.224"
implementation "com.mysql:mysql-connector-j:8.2.0"
- implementation "org.mariadb.jdbc:mariadb-java-client:3.3.0"
- implementation "org.postgresql:postgresql:42.6.0"
+ implementation "org.mariadb.jdbc:mariadb-java-client:3.3.2"
+ implementation "org.postgresql:postgresql:42.7.1"
implementation "com.microsoft.sqlserver:mssql-jdbc:12.4.2.jre11"
implementation "com.zaxxer:HikariCP:5.1.0"
- implementation "io.netty:netty-all:4.1.101.Final"
- implementation "org.slf4j:slf4j-jdk14:2.0.9"
+ implementation "io.netty:netty-all:4.1.104.Final"
+ implementation "org.slf4j:slf4j-jdk14:2.0.11"
implementation "com.google.inject:guice:$guiceVersion"
implementation "com.google.inject.extensions:guice-servlet:$guiceVersion"
implementation "org.owasp.encoder:encoder:1.2.3"
@@ -66,7 +66,7 @@ dependencies {
implementation "org.glassfish.jersey.containers:jersey-container-servlet:$jerseyVersion"
implementation "org.glassfish.jersey.media:jersey-media-json-jackson:$jerseyVersion"
implementation "org.glassfish.jersey.inject:jersey-hk2:$jerseyVersion"
- implementation "org.glassfish.hk2:guice-bridge:3.0.4" // same version as jersey-hk2
+ implementation "org.glassfish.hk2:guice-bridge:3.0.5" // same version as jersey-hk2
implementation "com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:$jacksonVersion"
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jakarta-jsonp:$jacksonVersion"
implementation "org.liquibase:liquibase-core:4.23.2" // upgrade has issues
@@ -79,20 +79,20 @@ dependencies {
implementation "org.mnode.ical4j:ical4j:3.2.14"
implementation "org.locationtech.spatial4j:spatial4j:0.8"
implementation "org.locationtech.jts:jts-core:1.19.0"
- implementation "net.java.dev.jna:jna-platform:5.13.0"
+ implementation "net.java.dev.jna:jna-platform:5.14.0"
implementation "com.github.jnr:jnr-posix:3.1.18"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
- implementation "com.amazonaws:aws-java-sdk-sns:1.12.592"
- implementation "org.apache.kafka:kafka-clients:3.6.0"
+ implementation "com.amazonaws:aws-java-sdk-sns:1.12.636"
+ implementation "org.apache.kafka:kafka-clients:3.6.1"
implementation "com.hivemq:hivemq-mqtt-client:1.3.3"
- implementation "redis.clients:jedis:5.0.2"
+ implementation "redis.clients:jedis:5.1.0"
implementation "com.google.firebase:firebase-admin:9.2.0"
- implementation "com.nimbusds:oauth2-oidc-sdk:11.6"
+ implementation "com.nimbusds:oauth2-oidc-sdk:11.9.1"
implementation "com.rabbitmq:amqp-client:5.20.0"
implementation "com.warrenstrange:googleauth:1.5.0"
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testImplementation "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
- testImplementation "org.mockito:mockito-core:5.7.0"
+ testImplementation "org.mockito:mockito-core:5.8.0"
}
test {
@@ -109,7 +109,7 @@ jar {
manifest {
attributes(
"Main-Class": "org.traccar.Main",
- "Implementation-Version": "5.10",
+ "Implementation-Version": "5.11",
"Class-Path": configurations.runtimeClasspath.files.collect { "lib/$it.name" }.join(" "))
}
}
diff --git a/setup/default.xml b/setup/default.xml
index 48fd8c993..fbe63c873 100644
--- a/setup/default.xml
+++ b/setup/default.xml
@@ -14,6 +14,7 @@
<entry key='web.path'>./modern</entry>
<entry key='web.sanitize'>false</entry>
<entry key='web.persistSession'>false</entry>
+ <entry key='web.showUnknownDevices'>true</entry>
<entry key='geocoder.enable'>true</entry>
<entry key='geocoder.type'>locationiq</entry>
diff --git a/setup/traccar.iss b/setup/traccar.iss
index 23daf6e13..68258dcb5 100644
--- a/setup/traccar.iss
+++ b/setup/traccar.iss
@@ -1,6 +1,6 @@
[Setup]
AppName=Traccar
-AppVersion=5.10
+AppVersion=5.11
DefaultDirName={pf}\Traccar
OutputBaseFilename=traccar-setup
ArchitecturesInstallIn64BitMode=x64
diff --git a/src/main/java/org/traccar/BasePipelineFactory.java b/src/main/java/org/traccar/BasePipelineFactory.java
index 5b48f3d15..ca4a4ae63 100644
--- a/src/main/java/org/traccar/BasePipelineFactory.java
+++ b/src/main/java/org/traccar/BasePipelineFactory.java
@@ -124,7 +124,11 @@ public abstract class BasePipelineFactory extends ChannelInitializer<Channel> {
pipeline.addLast(handler);
}
pipeline.addLast(new NetworkMessageHandler());
- pipeline.addLast(new StandardLoggingHandler(protocol));
+
+ var loggingHandler = new StandardLoggingHandler(protocol);
+ injector.injectMembers(loggingHandler);
+ pipeline.addLast(loggingHandler);
+
if (!connector.isDatagram() && !config.getBoolean(Keys.SERVER_INSTANT_ACKNOWLEDGEMENT)) {
pipeline.addLast(new AcknowledgementHandler());
}
diff --git a/src/main/java/org/traccar/BaseProtocolDecoder.java b/src/main/java/org/traccar/BaseProtocolDecoder.java
index 97762e8ca..4d4086c3c 100644
--- a/src/main/java/org/traccar/BaseProtocolDecoder.java
+++ b/src/main/java/org/traccar/BaseProtocolDecoder.java
@@ -29,7 +29,6 @@ import org.traccar.model.Position;
import org.traccar.session.ConnectionManager;
import org.traccar.session.DeviceSession;
import org.traccar.session.cache.CacheManager;
-import org.traccar.storage.StorageException;
import jakarta.inject.Inject;
import java.net.InetSocketAddress;
@@ -137,7 +136,7 @@ public abstract class BaseProtocolDecoder extends ExtendedObjectDecoder {
public DeviceSession getDeviceSession(Channel channel, SocketAddress remoteAddress, String... uniqueIds) {
try {
return connectionManager.getDeviceSession(protocol, channel, remoteAddress, uniqueIds);
- } catch (StorageException e) {
+ } catch (Exception e) {
throw new RuntimeException(e);
}
}
diff --git a/src/main/java/org/traccar/WindowsService.java b/src/main/java/org/traccar/WindowsService.java
index f233337a7..08eba25a6 100644
--- a/src/main/java/org/traccar/WindowsService.java
+++ b/src/main/java/org/traccar/WindowsService.java
@@ -170,7 +170,7 @@ public abstract class WindowsService {
public abstract void run();
- private class ServiceMain implements SERVICE_MAIN_FUNCTION {
+ private final class ServiceMain implements SERVICE_MAIN_FUNCTION {
public void callback(int dwArgc, Pointer lpszArgv) {
ServiceControl serviceControl = new ServiceControl();
@@ -203,7 +203,7 @@ public abstract class WindowsService {
}
- private class ServiceControl implements HandlerEx {
+ private final class ServiceControl implements HandlerEx {
public int callback(int dwControl, int dwEventType, Pointer lpEventData, Pointer lpContext) {
switch (dwControl) {
diff --git a/src/main/java/org/traccar/api/AsyncSocket.java b/src/main/java/org/traccar/api/AsyncSocket.java
index 5fc4b4412..f5fbcbf62 100644
--- a/src/main/java/org/traccar/api/AsyncSocket.java
+++ b/src/main/java/org/traccar/api/AsyncSocket.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2015 - 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.
@@ -22,16 +22,17 @@ import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.traccar.helper.model.PositionUtil;
-import org.traccar.session.ConnectionManager;
import org.traccar.model.Device;
import org.traccar.model.Event;
+import org.traccar.model.LogRecord;
import org.traccar.model.Position;
+import org.traccar.session.ConnectionManager;
import org.traccar.storage.Storage;
import org.traccar.storage.StorageException;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
public class AsyncSocket extends WebSocketAdapter implements ConnectionManager.UpdateListener {
@@ -41,12 +42,15 @@ public class AsyncSocket extends WebSocketAdapter implements ConnectionManager.U
private static final String KEY_DEVICES = "devices";
private static final String KEY_POSITIONS = "positions";
private static final String KEY_EVENTS = "events";
+ private static final String KEY_LOGS = "logs";
private final ObjectMapper objectMapper;
private final ConnectionManager connectionManager;
private final Storage storage;
private final long userId;
+ private boolean includeLogs;
+
public AsyncSocket(ObjectMapper objectMapper, ConnectionManager connectionManager, Storage storage, long userId) {
this.objectMapper = objectMapper;
this.connectionManager = connectionManager;
@@ -76,29 +80,41 @@ public class AsyncSocket extends WebSocketAdapter implements ConnectionManager.U
}
@Override
+ public void onWebSocketText(String message) {
+ super.onWebSocketText(message);
+
+ try {
+ includeLogs = objectMapper.readTree(message).get("logs").asBoolean();
+ } catch (JsonProcessingException e) {
+ LOGGER.warn("Socket JSON parsing error", e);
+ }
+ }
+
+ @Override
public void onKeepalive() {
sendData(new HashMap<>());
}
@Override
public void onUpdateDevice(Device device) {
- Map<String, Collection<?>> data = new HashMap<>();
- data.put(KEY_DEVICES, Collections.singletonList(device));
- sendData(data);
+ sendData(Map.of(KEY_DEVICES, List.of(device)));
}
@Override
public void onUpdatePosition(Position position) {
- Map<String, Collection<?>> data = new HashMap<>();
- data.put(KEY_POSITIONS, Collections.singletonList(position));
- sendData(data);
+ sendData(Map.of(KEY_POSITIONS, List.of(position)));
}
@Override
public void onUpdateEvent(Event event) {
- Map<String, Collection<?>> data = new HashMap<>();
- data.put(KEY_EVENTS, Collections.singletonList(event));
- sendData(data);
+ sendData(Map.of(KEY_EVENTS, List.of(event)));
+ }
+
+ @Override
+ public void onUpdateLog(LogRecord record) {
+ if (includeLogs) {
+ sendData(Map.of(KEY_LOGS, List.of(record)));
+ }
}
private void sendData(Map<String, Collection<?>> data) {
diff --git a/src/main/java/org/traccar/api/BaseObjectResource.java b/src/main/java/org/traccar/api/BaseObjectResource.java
index ebfa93ff0..2a801221b 100644
--- a/src/main/java/org/traccar/api/BaseObjectResource.java
+++ b/src/main/java/org/traccar/api/BaseObjectResource.java
@@ -67,7 +67,7 @@ public abstract class BaseObjectResource<T extends BaseModel> extends BaseResour
}
@POST
- public Response add(T entity) throws StorageException {
+ public Response add(T entity) throws Exception {
permissionsService.checkEdit(getUserId(), entity, true);
entity.setId(storage.addObject(entity, new Request(new Columns.Exclude("id"))));
@@ -85,7 +85,7 @@ public abstract class BaseObjectResource<T extends BaseModel> extends BaseResour
@Path("{id}")
@PUT
- public Response update(T entity) throws StorageException {
+ public Response update(T entity) throws Exception {
permissionsService.checkEdit(getUserId(), entity, false);
permissionsService.checkPermission(baseClass, getUserId(), entity.getId());
@@ -111,7 +111,7 @@ public abstract class BaseObjectResource<T extends BaseModel> extends BaseResour
new Condition.Equals("id", entity.getId())));
}
}
- cacheManager.updateOrInvalidate(true, entity, ObjectOperation.UPDATE);
+ cacheManager.invalidateObject(true, entity.getClass(), entity.getId(), ObjectOperation.UPDATE);
LogAction.edit(getUserId(), entity);
return Response.ok(entity).build();
@@ -119,12 +119,12 @@ public abstract class BaseObjectResource<T extends BaseModel> extends BaseResour
@Path("{id}")
@DELETE
- public Response remove(@PathParam("id") long id) throws StorageException {
+ public Response remove(@PathParam("id") long id) throws Exception {
permissionsService.checkEdit(getUserId(), baseClass, false);
permissionsService.checkPermission(baseClass, getUserId(), id);
storage.removeObject(baseClass, new Request(new Condition.Equals("id", id)));
- cacheManager.invalidate(baseClass, id);
+ cacheManager.invalidateObject(true, baseClass, id, ObjectOperation.DELETE);
LogAction.remove(getUserId(), baseClass, id);
diff --git a/src/main/java/org/traccar/api/resource/AttributeResource.java b/src/main/java/org/traccar/api/resource/AttributeResource.java
index 44f0ef452..52c4d6324 100644
--- a/src/main/java/org/traccar/api/resource/AttributeResource.java
+++ b/src/main/java/org/traccar/api/resource/AttributeResource.java
@@ -78,21 +78,21 @@ public class AttributeResource extends ExtendedObjectResource<Attribute> {
}
@POST
- public Response add(Attribute entity) throws StorageException {
+ public Response add(Attribute entity) throws Exception {
permissionsService.checkAdmin(getUserId());
return super.add(entity);
}
@Path("{id}")
@PUT
- public Response update(Attribute entity) throws StorageException {
+ public Response update(Attribute entity) throws Exception {
permissionsService.checkAdmin(getUserId());
return super.update(entity);
}
@Path("{id}")
@DELETE
- public Response remove(@PathParam("id") long id) throws StorageException {
+ public Response remove(@PathParam("id") long id) throws Exception {
permissionsService.checkAdmin(getUserId());
return super.remove(id);
}
diff --git a/src/main/java/org/traccar/api/resource/DeviceResource.java b/src/main/java/org/traccar/api/resource/DeviceResource.java
index ebc40a9b1..540450cbb 100644
--- a/src/main/java/org/traccar/api/resource/DeviceResource.java
+++ b/src/main/java/org/traccar/api/resource/DeviceResource.java
@@ -19,6 +19,8 @@ import jakarta.ws.rs.FormParam;
import org.traccar.api.BaseObjectResource;
import org.traccar.api.signature.TokenManager;
import org.traccar.broadcast.BroadcastService;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
import org.traccar.database.MediaManager;
import org.traccar.helper.LogAction;
import org.traccar.model.Device;
@@ -61,6 +63,9 @@ import java.util.List;
public class DeviceResource extends BaseObjectResource<Device> {
@Inject
+ private Config config;
+
+ @Inject
private CacheManager cacheManager;
@Inject
@@ -128,7 +133,7 @@ public class DeviceResource extends BaseObjectResource<Device> {
@Path("{id}/accumulators")
@PUT
- public Response updateAccumulators(DeviceAccumulators entity) throws StorageException {
+ public Response updateAccumulators(DeviceAccumulators entity) throws Exception {
if (permissionsService.notAdmin(getUserId())) {
permissionsService.checkManager(getUserId());
permissionsService.checkPermission(Device.class, getUserId(), entity.getDeviceId());
@@ -212,6 +217,8 @@ public class DeviceResource extends BaseObjectResource<Device> {
share.setExpirationTime(expiration);
share.setTemporary(true);
share.setReadonly(true);
+ share.setLimitCommands(!config.getBoolean(Keys.WEB_SHARE_DEVICE_COMMANDS));
+ share.setDisableReports(!config.getBoolean(Keys.WEB_SHARE_DEVICE_REPORTS));
share.setId(storage.addObject(share, new Request(new Columns.Exclude("id"))));
diff --git a/src/main/java/org/traccar/api/resource/PermissionsResource.java b/src/main/java/org/traccar/api/resource/PermissionsResource.java
index 2a8ac62f7..9e2d21f2c 100644
--- a/src/main/java/org/traccar/api/resource/PermissionsResource.java
+++ b/src/main/java/org/traccar/api/resource/PermissionsResource.java
@@ -64,7 +64,7 @@ public class PermissionsResource extends BaseResource {
@Path("bulk")
@POST
- public Response add(List<LinkedHashMap<String, Long>> entities) throws StorageException, ClassNotFoundException {
+ public Response add(List<LinkedHashMap<String, Long>> entities) throws Exception {
permissionsService.checkRestriction(getUserId(), UserRestrictions::getReadonly);
checkPermissionTypes(entities);
for (LinkedHashMap<String, Long> entity: entities) {
@@ -84,13 +84,13 @@ public class PermissionsResource extends BaseResource {
}
@POST
- public Response add(LinkedHashMap<String, Long> entity) throws StorageException, ClassNotFoundException {
+ public Response add(LinkedHashMap<String, Long> entity) throws Exception {
return add(Collections.singletonList(entity));
}
@DELETE
@Path("bulk")
- public Response remove(List<LinkedHashMap<String, Long>> entities) throws StorageException, ClassNotFoundException {
+ public Response remove(List<LinkedHashMap<String, Long>> entities) throws Exception {
permissionsService.checkRestriction(getUserId(), UserRestrictions::getReadonly);
checkPermissionTypes(entities);
for (LinkedHashMap<String, Long> entity: entities) {
@@ -110,7 +110,7 @@ public class PermissionsResource extends BaseResource {
}
@DELETE
- public Response remove(LinkedHashMap<String, Long> entity) throws StorageException, ClassNotFoundException {
+ public Response remove(LinkedHashMap<String, Long> entity) throws Exception {
return remove(Collections.singletonList(entity));
}
diff --git a/src/main/java/org/traccar/api/resource/ServerResource.java b/src/main/java/org/traccar/api/resource/ServerResource.java
index 59ef642c8..66ecc74e1 100644
--- a/src/main/java/org/traccar/api/resource/ServerResource.java
+++ b/src/main/java/org/traccar/api/resource/ServerResource.java
@@ -107,14 +107,14 @@ public class ServerResource extends BaseResource {
}
@PUT
- public Response update(Server entity) throws StorageException {
+ public Response update(Server server) throws Exception {
permissionsService.checkAdmin(getUserId());
- storage.updateObject(entity, new Request(
+ storage.updateObject(server, new Request(
new Columns.Exclude("id"),
- new Condition.Equals("id", entity.getId())));
- cacheManager.updateOrInvalidate(true, entity, ObjectOperation.UPDATE);
- LogAction.edit(getUserId(), entity);
- return Response.ok(entity).build();
+ new Condition.Equals("id", server.getId())));
+ cacheManager.invalidateObject(true, Server.class, server.getId(), ObjectOperation.UPDATE);
+ LogAction.edit(getUserId(), server);
+ return Response.ok(server).build();
}
@Path("geocode")
@@ -136,11 +136,16 @@ public class ServerResource extends BaseResource {
@Path("file/{path}")
@POST
@Consumes("*/*")
- public Response uploadImage(@PathParam("path") String path, File inputFile) throws IOException, StorageException {
+ public Response uploadFile(@PathParam("path") String path, File inputFile) throws IOException, StorageException {
permissionsService.checkAdmin(getUserId());
String root = config.getString(Keys.WEB_OVERRIDE, config.getString(Keys.WEB_PATH));
- var outputPath = Paths.get(root, path);
+ var rootPath = Paths.get(root).normalize();
+ var outputPath = rootPath.resolve(path).normalize();
+ if (!outputPath.startsWith(rootPath)) {
+ return Response.status(Response.Status.BAD_REQUEST).build();
+ }
+
var directoryPath = outputPath.getParent();
if (directoryPath != null) {
Files.createDirectories(directoryPath);
@@ -152,4 +157,11 @@ public class ServerResource extends BaseResource {
return Response.ok().build();
}
+ @Path("cache")
+ @GET
+ public String cache() throws StorageException {
+ permissionsService.checkAdmin(getUserId());
+ return cacheManager.toString();
+ }
+
}
diff --git a/src/main/java/org/traccar/api/resource/UserResource.java b/src/main/java/org/traccar/api/resource/UserResource.java
index 99537f912..47ea9b07c 100644
--- a/src/main/java/org/traccar/api/resource/UserResource.java
+++ b/src/main/java/org/traccar/api/resource/UserResource.java
@@ -126,7 +126,7 @@ public class UserResource extends BaseObjectResource<User> {
@Path("{id}")
@DELETE
- public Response remove(@PathParam("id") long id) throws StorageException {
+ public Response remove(@PathParam("id") long id) throws Exception {
Response response = super.remove(id);
if (getUserId() == id) {
request.getSession().removeAttribute(SessionResource.USER_ID_KEY);
diff --git a/src/main/java/org/traccar/broadcast/BaseBroadcastService.java b/src/main/java/org/traccar/broadcast/BaseBroadcastService.java
index 1c4660320..01b212c60 100644
--- a/src/main/java/org/traccar/broadcast/BaseBroadcastService.java
+++ b/src/main/java/org/traccar/broadcast/BaseBroadcastService.java
@@ -69,10 +69,8 @@ public abstract class BaseBroadcastService implements BroadcastService {
}
@Override
- public void invalidateObject(
- boolean local,
- Class<? extends BaseModel> clazz, long id,
- ObjectOperation operation) {
+ public <T extends BaseModel> void invalidateObject(
+ boolean local, Class<T> clazz, long id, ObjectOperation operation) {
BroadcastMessage message = new BroadcastMessage();
var invalidateObject = new BroadcastMessage.InvalidateObject();
invalidateObject.setClazz(Permission.getKey(clazz));
@@ -83,11 +81,8 @@ public abstract class BaseBroadcastService implements BroadcastService {
}
@Override
- public void invalidatePermission(
- boolean local,
- Class<? extends BaseModel> clazz1, long id1,
- Class<? extends BaseModel> clazz2, long id2,
- boolean link) {
+ public synchronized <T1 extends BaseModel, T2 extends BaseModel> void invalidatePermission(
+ boolean local, Class<T1> clazz1, long id1, Class<T2> clazz2, long id2, boolean link) {
BroadcastMessage message = new BroadcastMessage();
var invalidatePermission = new BroadcastMessage.InvalidatePermission();
invalidatePermission.setClazz1(Permission.getKey(clazz1));
@@ -101,7 +96,7 @@ public abstract class BaseBroadcastService implements BroadcastService {
protected abstract void sendMessage(BroadcastMessage message);
- protected void handleMessage(BroadcastMessage message) {
+ protected void handleMessage(BroadcastMessage message) throws Exception {
if (message.getDevice() != null) {
listeners.forEach(listener -> listener.updateDevice(false, message.getDevice()));
} else if (message.getPosition() != null) {
@@ -112,17 +107,21 @@ public abstract class BaseBroadcastService implements BroadcastService {
listeners.forEach(listener -> listener.updateCommand(false, message.getCommandDeviceId()));
} else if (message.getInvalidateObject() != null) {
var invalidateObject = message.getInvalidateObject();
- listeners.forEach(listeners -> listeners.invalidateObject(
- false,
- Permission.getKeyClass(invalidateObject.getClazz()), invalidateObject.getId(),
- invalidateObject.getOperation()));
+ for (BroadcastInterface listener : listeners) {
+ listener.invalidateObject(
+ false,
+ Permission.getKeyClass(invalidateObject.getClazz()), invalidateObject.getId(),
+ invalidateObject.getOperation());
+ }
} else if (message.getInvalidatePermission() != null) {
var invalidatePermission = message.getInvalidatePermission();
- listeners.forEach(listener -> listener.invalidatePermission(
- false,
- Permission.getKeyClass(invalidatePermission.getClazz1()), invalidatePermission.getId1(),
- Permission.getKeyClass(invalidatePermission.getClazz2()), invalidatePermission.getId2(),
- invalidatePermission.getLink()));
+ for (BroadcastInterface listener : listeners) {
+ listener.invalidatePermission(
+ false,
+ Permission.getKeyClass(invalidatePermission.getClazz1()), invalidatePermission.getId1(),
+ Permission.getKeyClass(invalidatePermission.getClazz2()), invalidatePermission.getId2(),
+ invalidatePermission.getLink());
+ }
}
}
diff --git a/src/main/java/org/traccar/broadcast/BroadcastInterface.java b/src/main/java/org/traccar/broadcast/BroadcastInterface.java
index 25fdf4d93..d0a491cd2 100644
--- a/src/main/java/org/traccar/broadcast/BroadcastInterface.java
+++ b/src/main/java/org/traccar/broadcast/BroadcastInterface.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ * 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.
@@ -35,16 +35,12 @@ public interface BroadcastInterface {
default void updateCommand(boolean local, long deviceId) {
}
- default void invalidateObject(
- boolean local,
- Class<? extends BaseModel> clazz, long id,
- ObjectOperation operation) {
+ default <T extends BaseModel> void invalidateObject(
+ boolean local, Class<T> clazz, long id, ObjectOperation operation) throws Exception {
}
- default void invalidatePermission(
- boolean local,
- Class<? extends BaseModel> clazz1, long id1,
- Class<? extends BaseModel> clazz2, long id2,
- boolean link) {
+ default <T1 extends BaseModel, T2 extends BaseModel> void invalidatePermission(
+ boolean local, Class<T1> clazz1, long id1, Class<T2> clazz2, long id2, boolean link) throws Exception {
}
+
}
diff --git a/src/main/java/org/traccar/broadcast/MulticastBroadcastService.java b/src/main/java/org/traccar/broadcast/MulticastBroadcastService.java
index 1c02b319b..793c6df36 100644
--- a/src/main/java/org/traccar/broadcast/MulticastBroadcastService.java
+++ b/src/main/java/org/traccar/broadcast/MulticastBroadcastService.java
@@ -103,7 +103,7 @@ public class MulticastBroadcastService extends BaseBroadcastService {
}
publisherSocket = null;
socket.leaveGroup(group, networkInterface);
- } catch (IOException e) {
+ } catch (Exception e) {
throw new RuntimeException(e);
}
}
diff --git a/src/main/java/org/traccar/broadcast/RedisBroadcastService.java b/src/main/java/org/traccar/broadcast/RedisBroadcastService.java
index e87ad5e61..697c45a4a 100644
--- a/src/main/java/org/traccar/broadcast/RedisBroadcastService.java
+++ b/src/main/java/org/traccar/broadcast/RedisBroadcastService.java
@@ -38,7 +38,6 @@ public class RedisBroadcastService extends BaseBroadcastService {
private final ExecutorService service = Executors.newSingleThreadExecutor();
- private final String url;
private final String channel = "traccar";
private Jedis subscriber;
@@ -48,7 +47,7 @@ public class RedisBroadcastService extends BaseBroadcastService {
public RedisBroadcastService(Config config, ObjectMapper objectMapper) throws IOException {
this.objectMapper = objectMapper;
- url = config.getString(Keys.BROADCAST_ADDRESS);
+ String url = config.getString(Keys.BROADCAST_ADDRESS);
try {
subscriber = new Jedis(url);
@@ -69,9 +68,7 @@ public class RedisBroadcastService extends BaseBroadcastService {
try {
String payload = id + ":" + objectMapper.writeValueAsString(message);
publisher.publish(channel, payload);
- } catch (IOException e) {
- LOGGER.warn("Broadcast failed", e);
- } catch (JedisConnectionException e) {
+ } catch (IOException | JedisConnectionException e) {
LOGGER.warn("Broadcast failed", e);
}
}
@@ -114,13 +111,11 @@ public class RedisBroadcastService extends BaseBroadcastService {
if (messageChannel.equals(channel) && parts.length == 2 && !id.equals(parts[0])) {
handleMessage(objectMapper.readValue(parts[1], BroadcastMessage.class));
}
- } catch (IOException e) {
+ } catch (Exception e) {
LOGGER.warn("Broadcast handleMessage failed", e);
}
}
}, channel);
- } catch (JedisConnectionException e) {
- throw new RuntimeException(e);
} catch (JedisException e) {
throw new RuntimeException(e);
}
diff --git a/src/main/java/org/traccar/config/Keys.java b/src/main/java/org/traccar/config/Keys.java
index 3059c4f4b..e79264908 100644
--- a/src/main/java/org/traccar/config/Keys.java
+++ b/src/main/java/org/traccar/config/Keys.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 - 2023 Anton Tananaev (anton@traccar.org)
+ * Copyright 2019 - 2024 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.
@@ -490,14 +490,6 @@ public final class Keys {
List.of(KeyType.CONFIG));
/**
- * By default, server syncs with the database if it encounters and unknown device. This flag allows to disable that
- * behavior to improve performance in some cases.
- */
- public static final ConfigKey<Boolean> DATABASE_IGNORE_UNKNOWN = new BooleanConfigKey(
- "database.ignoreUnknown",
- List.of(KeyType.CONFIG));
-
- /**
* Automatically register unknown devices in the database.
*/
public static final ConfigKey<Boolean> DATABASE_REGISTER_UNKNOWN = new BooleanConfigKey(
@@ -664,7 +656,7 @@ public final class Keys {
/**
* OpenID Connect Authorization URL.
* This can usually be found in the documentation of your identity provider or by using the well-known
- * configuration endpoint, eg. https://auth.example.com//.well-known/openid-configuration
+ * configuration endpoint, e.g. https://auth.example.com//.well-known/openid-configuration
* Required to enable SSO if openid.issuerUrl is not set.
*/
public static final ConfigKey<String> OPENID_AUTH_URL = new StringConfigKey(
@@ -1226,8 +1218,36 @@ public final class Keys {
List.of(KeyType.CONFIG));
/**
+ * Enable user expiration email notification.
+ */
+ public static final ConfigKey<Boolean> NOTIFICATION_EXPIRATION_USER = new BooleanConfigKey(
+ "notification.expiration.user",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * User expiration reminder. Value in milliseconds.
+ */
+ public static final ConfigKey<Long> NOTIFICATION_EXPIRATION_USER_REMINDER = new LongConfigKey(
+ "notification.expiration.user.reminder",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * Enable device expiration email notification.
+ */
+ public static final ConfigKey<Boolean> NOTIFICATION_EXPIRATION_DEVICE = new BooleanConfigKey(
+ "notification.expiration.device",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * Device expiration reminder. Value in milliseconds.
+ */
+ public static final ConfigKey<Long> NOTIFICATION_EXPIRATION_DEVICE_REMINDER = new LongConfigKey(
+ "notification.expiration.device.reminder",
+ List.of(KeyType.CONFIG));
+
+ /**
* Maximum time period for reports in seconds. Can be useful to prevent users to request unreasonably long reports.
- * By default there is no limit.
+ * By default, there is no limit.
*/
public static final ConfigKey<Long> REPORT_PERIOD_LIMIT = new LongConfigKey(
"report.periodLimit",
@@ -1779,6 +1799,27 @@ public final class Keys {
List.of(KeyType.CONFIG));
/**
+ * Show logs from unknown devices.
+ */
+ public static final ConfigKey<Boolean> WEB_SHOW_UNKNOWN_DEVICES = new BooleanConfigKey(
+ "web.showUnknownDevices",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * Enable commands for a shared device.
+ */
+ public static final ConfigKey<Boolean> WEB_SHARE_DEVICE_COMMANDS = new BooleanConfigKey(
+ "web.shareDevice.commands",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * Enable reports for a shared device.
+ */
+ public static final ConfigKey<Boolean> WEB_SHARE_DEVICE_REPORTS = new BooleanConfigKey(
+ "web.shareDevice.reports",
+ List.of(KeyType.CONFIG));
+
+ /**
* Output logging to the standard terminal output instead of a log file.
*/
public static final ConfigKey<Boolean> LOGGER_CONSOLE = new BooleanConfigKey(
diff --git a/src/main/java/org/traccar/database/DeviceLookupService.java b/src/main/java/org/traccar/database/DeviceLookupService.java
index 6ec6841a1..90d23531e 100644
--- a/src/main/java/org/traccar/database/DeviceLookupService.java
+++ b/src/main/java/org/traccar/database/DeviceLookupService.java
@@ -49,7 +49,7 @@ public class DeviceLookupService {
private final boolean throttlingEnabled;
- private static class IdentifierInfo {
+ private static final class IdentifierInfo {
private long lastQuery;
private long delay;
private Timeout timeout;
diff --git a/src/main/java/org/traccar/database/NotificationManager.java b/src/main/java/org/traccar/database/NotificationManager.java
index 79585d67a..65437f0a1 100644
--- a/src/main/java/org/traccar/database/NotificationManager.java
+++ b/src/main/java/org/traccar/database/NotificationManager.java
@@ -29,7 +29,6 @@ import org.traccar.model.Device;
import org.traccar.model.Event;
import org.traccar.model.Geofence;
import org.traccar.model.Maintenance;
-import org.traccar.model.Notification;
import org.traccar.model.Position;
import org.traccar.notification.MessageException;
import org.traccar.notification.NotificatorManager;
@@ -88,7 +87,7 @@ public class NotificationManager {
return;
}
- var notifications = cacheManager.getDeviceObjects(event.getDeviceId(), Notification.class).stream()
+ var notifications = cacheManager.getDeviceNotifications(event.getDeviceId()).stream()
.filter(notification -> notification.getType().equals(event.getType()))
.filter(notification -> {
if (event.getType().equals(Event.TYPE_ALARM)) {
@@ -162,7 +161,7 @@ public class NotificationManager {
try {
cacheManager.addDevice(event.getDeviceId());
updateEvent(event, position);
- } catch (StorageException e) {
+ } catch (Exception e) {
throw new RuntimeException(e);
} finally {
cacheManager.removeDevice(event.getDeviceId());
diff --git a/src/main/java/org/traccar/handler/StandardLoggingHandler.java b/src/main/java/org/traccar/handler/StandardLoggingHandler.java
index 84492e2a5..5978d632e 100644
--- a/src/main/java/org/traccar/handler/StandardLoggingHandler.java
+++ b/src/main/java/org/traccar/handler/StandardLoggingHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2019 - 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.
@@ -20,68 +20,73 @@ import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
+import jakarta.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.traccar.NetworkMessage;
import org.traccar.helper.NetworkUtil;
+import org.traccar.model.LogRecord;
+import org.traccar.session.ConnectionManager;
import java.net.InetSocketAddress;
-import java.net.SocketAddress;
public class StandardLoggingHandler extends ChannelDuplexHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(StandardLoggingHandler.class);
private final String protocol;
+ private ConnectionManager connectionManager;
public StandardLoggingHandler(String protocol) {
this.protocol = protocol;
}
+ @Inject
+ public void setConnectionManager(ConnectionManager connectionManager) {
+ this.connectionManager = connectionManager;
+ }
+
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
- log(ctx, false, msg);
+ LogRecord record = createLogRecord(msg);
+ log(ctx, false, record);
super.channelRead(ctx, msg);
+ if (record != null) {
+ connectionManager.updateLog(record);
+ }
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
- log(ctx, true, msg);
+ log(ctx, true, createLogRecord(msg));
super.write(ctx, msg, promise);
}
- public void log(ChannelHandlerContext ctx, boolean downstream, Object o) {
- if (o instanceof NetworkMessage) {
- NetworkMessage networkMessage = (NetworkMessage) o;
+ private LogRecord createLogRecord(Object msg) {
+ if (msg instanceof NetworkMessage) {
+ NetworkMessage networkMessage = (NetworkMessage) msg;
if (networkMessage.getMessage() instanceof ByteBuf) {
- log(ctx, downstream, networkMessage.getRemoteAddress(), (ByteBuf) networkMessage.getMessage());
+ LogRecord record = new LogRecord();
+ record.setAddress((InetSocketAddress) networkMessage.getRemoteAddress());
+ record.setProtocol(protocol);
+ record.setData(ByteBufUtil.hexDump((ByteBuf) networkMessage.getMessage()));
+ return record;
}
- } else if (o instanceof ByteBuf) {
- log(ctx, downstream, ctx.channel().remoteAddress(), (ByteBuf) o);
}
+ return null;
}
- public void log(ChannelHandlerContext ctx, boolean downstream, SocketAddress remoteAddress, ByteBuf buf) {
- StringBuilder message = new StringBuilder();
-
- message.append("[").append(NetworkUtil.session(ctx.channel())).append(": ");
- message.append(protocol);
- if (downstream) {
- message.append(" > ");
- } else {
- message.append(" < ");
+ private void log(ChannelHandlerContext ctx, boolean downstream, LogRecord record) {
+ if (record != null) {
+ StringBuilder message = new StringBuilder();
+ message.append("[").append(NetworkUtil.session(ctx.channel())).append(": ");
+ message.append(protocol);
+ message.append(downstream ? " > " : " < ");
+ message.append(record.getAddress().getHostString());
+ message.append("] ");
+ message.append(record.getData());
+ LOGGER.info(message.toString());
}
-
- if (remoteAddress instanceof InetSocketAddress) {
- message.append(((InetSocketAddress) remoteAddress).getHostString());
- } else {
- message.append("unknown");
- }
- message.append("] ");
-
- message.append(ByteBufUtil.hexDump(buf));
-
- LOGGER.info(message.toString());
}
}
diff --git a/src/main/java/org/traccar/model/LogRecord.java b/src/main/java/org/traccar/model/LogRecord.java
new file mode 100644
index 000000000..c19163af3
--- /dev/null
+++ b/src/main/java/org/traccar/model/LogRecord.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 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.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import java.net.InetSocketAddress;
+
+public class LogRecord {
+
+ private InetSocketAddress address;
+
+ public void setAddress(InetSocketAddress address) {
+ this.address = address;
+ }
+
+ @JsonIgnore
+ public InetSocketAddress getAddress() {
+ return address;
+ }
+
+ public String getHost() {
+ return address.getHostString();
+ }
+
+ private String protocol;
+
+ public String getProtocol() {
+ return protocol;
+ }
+
+ public void setProtocol(String protocol) {
+ this.protocol = protocol;
+ }
+
+ private String uniqueId;
+
+ public String getUniqueId() {
+ return uniqueId;
+ }
+
+ public void setUniqueId(String uniqueId) {
+ this.uniqueId = uniqueId;
+ }
+
+ private long deviceId;
+
+ public long getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(long deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ private String data;
+
+ public String getData() {
+ return data;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+
+}
diff --git a/src/main/java/org/traccar/notification/NotificationFormatter.java b/src/main/java/org/traccar/notification/NotificationFormatter.java
index e994729c0..7685eac0d 100644
--- a/src/main/java/org/traccar/notification/NotificationFormatter.java
+++ b/src/main/java/org/traccar/notification/NotificationFormatter.java
@@ -23,6 +23,7 @@ import org.traccar.model.Driver;
import org.traccar.model.Event;
import org.traccar.model.Geofence;
import org.traccar.model.Maintenance;
+import org.traccar.model.Notification;
import org.traccar.model.Position;
import org.traccar.model.Server;
import org.traccar.model.User;
@@ -44,13 +45,15 @@ public class NotificationFormatter {
this.textTemplateFormatter = textTemplateFormatter;
}
- public NotificationMessage formatMessage(User user, Event event, Position position, String templatePath) {
+ public NotificationMessage formatMessage(
+ Notification notification, User user, Event event, Position position, String templatePath) {
Server server = cacheManager.getServer();
Device device = cacheManager.getObject(Device.class, event.getDeviceId());
VelocityContext velocityContext = textTemplateFormatter.prepareContext(server, user);
+ velocityContext.put("notification", notification);
velocityContext.put("device", device);
velocityContext.put("event", event);
if (position != null) {
diff --git a/src/main/java/org/traccar/notificators/NotificatorFirebase.java b/src/main/java/org/traccar/notificators/NotificatorFirebase.java
index a39683b2b..89031ba26 100644
--- a/src/main/java/org/traccar/notificators/NotificatorFirebase.java
+++ b/src/main/java/org/traccar/notificators/NotificatorFirebase.java
@@ -24,7 +24,6 @@ import com.google.firebase.messaging.AndroidNotification;
import com.google.firebase.messaging.ApnsConfig;
import com.google.firebase.messaging.Aps;
import com.google.firebase.messaging.FirebaseMessaging;
-import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.MessagingErrorCode;
import com.google.firebase.messaging.MulticastMessage;
import org.slf4j.Logger;
@@ -40,7 +39,6 @@ import org.traccar.notification.MessageException;
import org.traccar.notification.NotificationFormatter;
import org.traccar.session.cache.CacheManager;
import org.traccar.storage.Storage;
-import org.traccar.storage.StorageException;
import org.traccar.storage.query.Columns;
import org.traccar.storage.query.Condition;
import org.traccar.storage.query.Request;
@@ -87,7 +85,7 @@ public class NotificatorFirebase implements Notificator {
public void send(Notification notification, User user, Event event, Position position) throws MessageException {
if (user.hasAttribute("notificationTokens")) {
- var shortMessage = notificationFormatter.formatMessage(user, event, position, "short");
+ var shortMessage = notificationFormatter.formatMessage(notification, user, event, position, "short");
List<String> registrationTokens = new ArrayList<>(
Arrays.asList(user.getString("notificationTokens").split("[, ]")));
@@ -136,9 +134,9 @@ public class NotificatorFirebase implements Notificator {
storage.updateObject(user, new Request(
new Columns.Include("attributes"),
new Condition.Equals("id", user.getId())));
- cacheManager.updateOrInvalidate(true, user, ObjectOperation.UPDATE);
+ cacheManager.invalidateObject(true, User.class, user.getId(), ObjectOperation.UPDATE);
}
- } catch (FirebaseMessagingException | StorageException e) {
+ } catch (Exception e) {
LOGGER.warn("Firebase error", e);
}
}
diff --git a/src/main/java/org/traccar/notificators/NotificatorMail.java b/src/main/java/org/traccar/notificators/NotificatorMail.java
index 3ab050686..11d4c5bae 100644
--- a/src/main/java/org/traccar/notificators/NotificatorMail.java
+++ b/src/main/java/org/traccar/notificators/NotificatorMail.java
@@ -43,7 +43,7 @@ public class NotificatorMail implements Notificator {
@Override
public void send(Notification notification, User user, Event event, Position position) throws MessageException {
try {
- var fullMessage = notificationFormatter.formatMessage(user, event, position, "full");
+ var fullMessage = notificationFormatter.formatMessage(notification, user, event, position, "full");
mailManager.sendMessage(user, false, fullMessage.getSubject(), fullMessage.getBody());
} catch (MessagingException e) {
throw new MessageException(e);
diff --git a/src/main/java/org/traccar/notificators/NotificatorPushover.java b/src/main/java/org/traccar/notificators/NotificatorPushover.java
index 9f2a8c94d..cf4c4026b 100644
--- a/src/main/java/org/traccar/notificators/NotificatorPushover.java
+++ b/src/main/java/org/traccar/notificators/NotificatorPushover.java
@@ -63,7 +63,7 @@ public class NotificatorPushover implements Notificator {
@Override
public void send(Notification notification, User user, Event event, Position position) {
- var shortMessage = notificationFormatter.formatMessage(user, event, position, "short");
+ var shortMessage = notificationFormatter.formatMessage(notification, user, event, position, "short");
Message message = new Message();
message.token = token;
diff --git a/src/main/java/org/traccar/notificators/NotificatorSms.java b/src/main/java/org/traccar/notificators/NotificatorSms.java
index 2b6b20b1b..ce362290e 100644
--- a/src/main/java/org/traccar/notificators/NotificatorSms.java
+++ b/src/main/java/org/traccar/notificators/NotificatorSms.java
@@ -46,7 +46,7 @@ public class NotificatorSms implements Notificator {
@Override
public void send(Notification notification, User user, Event event, Position position) throws MessageException {
if (user.getPhone() != null) {
- var shortMessage = notificationFormatter.formatMessage(user, event, position, "short");
+ var shortMessage = notificationFormatter.formatMessage(notification, user, event, position, "short");
statisticsManager.registerSms();
smsManager.sendMessage(user.getPhone(), shortMessage.getBody(), false);
}
diff --git a/src/main/java/org/traccar/notificators/NotificatorTelegram.java b/src/main/java/org/traccar/notificators/NotificatorTelegram.java
index c91aaa4ff..eaee32810 100644
--- a/src/main/java/org/traccar/notificators/NotificatorTelegram.java
+++ b/src/main/java/org/traccar/notificators/NotificatorTelegram.java
@@ -87,7 +87,7 @@ public class NotificatorTelegram implements Notificator {
@Override
public void send(Notification notification, User user, Event event, Position position) {
- var shortMessage = notificationFormatter.formatMessage(user, event, position, "short");
+ var shortMessage = notificationFormatter.formatMessage(notification, user, event, position, "short");
TextMessage message = new TextMessage();
message.chatId = user.getString("telegramChatId");
diff --git a/src/main/java/org/traccar/notificators/NotificatorTraccar.java b/src/main/java/org/traccar/notificators/NotificatorTraccar.java
index 82e1584a5..c00e3e029 100644
--- a/src/main/java/org/traccar/notificators/NotificatorTraccar.java
+++ b/src/main/java/org/traccar/notificators/NotificatorTraccar.java
@@ -27,7 +27,6 @@ import org.traccar.model.User;
import org.traccar.notification.NotificationFormatter;
import org.traccar.session.cache.CacheManager;
import org.traccar.storage.Storage;
-import org.traccar.storage.StorageException;
import org.traccar.storage.query.Columns;
import org.traccar.storage.query.Condition;
import org.traccar.storage.query.Request;
@@ -88,7 +87,7 @@ public class NotificatorTraccar implements Notificator {
public void send(org.traccar.model.Notification notification, User user, Event event, Position position) {
if (user.hasAttribute("notificationTokens")) {
- var shortMessage = notificationFormatter.formatMessage(user, event, position, "short");
+ var shortMessage = notificationFormatter.formatMessage(notification, user, event, position, "short");
NotificationObject item = new NotificationObject();
item.title = shortMessage.getSubject();
@@ -129,9 +128,9 @@ public class NotificatorTraccar implements Notificator {
storage.updateObject(user, new Request(
new Columns.Include("attributes"),
new Condition.Equals("id", user.getId())));
- cacheManager.updateOrInvalidate(true, user, ObjectOperation.UPDATE);
+ cacheManager.invalidateObject(true, User.class, user.getId(), ObjectOperation.UPDATE);
}
- } catch (StorageException e) {
+ } catch (Exception e) {
LOGGER.warn("Push error", e);
}
}
diff --git a/src/main/java/org/traccar/notificators/NotificatorWeb.java b/src/main/java/org/traccar/notificators/NotificatorWeb.java
index 3a125db3c..2b9030226 100644
--- a/src/main/java/org/traccar/notificators/NotificatorWeb.java
+++ b/src/main/java/org/traccar/notificators/NotificatorWeb.java
@@ -51,7 +51,7 @@ public final class NotificatorWeb implements Notificator {
copy.setMaintenanceId(event.getMaintenanceId());
copy.getAttributes().putAll(event.getAttributes());
- var message = notificationFormatter.formatMessage(user, event, position, "short");
+ var message = notificationFormatter.formatMessage(notification, user, event, position, "short");
copy.set("message", message.getBody());
connectionManager.updateEvent(true, user.getId(), copy);
diff --git a/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java b/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java
index a73981614..0628a06d4 100644
--- a/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java
@@ -61,7 +61,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_ACK = new PatternBuilder()
.text("+ACK:GT")
.expression("...,") // type
- .number("([0-9A-Z]{2}xxxx),") // protocol version
+ .expression("(.{6}|.{10}),") // protocol version
.number("(d{15}|x{14}),") // imei
.any().text(",")
.number("(dddd)(dd)(dd)") // date (yyyymmdd)
@@ -130,7 +130,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_INF = new PatternBuilder()
.text("+").expression("(?:RESP|BUFF):GTINF,")
- .number("[0-9A-Z]{2}xxxx,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("(?:[0-9A-Z]{17},)?") // vin
.expression("(?:[^,]+)?,") // device name
@@ -231,7 +231,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_VER = new PatternBuilder()
.text("+").expression("(?:RESP|BUFF):GTVER,")
- .number("[0-9A-Z]{2}xxxx,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.expression("([^,]*),") // device type
@@ -340,7 +340,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_OBD = new PatternBuilder()
.text("+RESP:GTOBD,")
- .number("[0-9A-Z]{2}xxxx,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("(?:[0-9A-Z]{17})?,") // vin
.expression("[^,]{0,20},") // device name
@@ -636,7 +636,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_FRI = new PatternBuilder()
.text("+").expression("(?:RESP|BUFF):GT...,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("(?:([0-9A-Z]{17}),)?") // vin
.expression("[^,]*,") // device name
@@ -764,7 +764,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_ERI = new PatternBuilder()
.text("+").expression("(?:RESP|BUFF):GTERI,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("(x{8}),") // mask
@@ -905,7 +905,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_IGN = new PatternBuilder()
.text("+").expression("(?:RESP|BUFF):GTIG[NF],")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("d+,") // ignition off duration
@@ -939,7 +939,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_LSW = new PatternBuilder()
.text("+RESP:").expression("GT[LT]SW,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("[01],") // type
@@ -970,7 +970,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_IDA = new PatternBuilder()
.text("+RESP:GTIDA,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,,") // device name
.number("([^,]+),") // rfid
@@ -1006,7 +1006,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_WIF = new PatternBuilder()
.text("+RESP:GTWIF,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("(d+),") // count
@@ -1047,7 +1047,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_GSM = new PatternBuilder()
.text("+RESP:GTGSM,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("(?:STR|CTN|NMR|RTL),") // fix type
.expression("(.*)") // cells
@@ -1086,7 +1086,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_PNA = new PatternBuilder()
.text("+RESP:GT").expression("P[NF]A,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("(dddd)(dd)(dd)") // date (yyyymmdd)
@@ -1112,7 +1112,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_DAR = new PatternBuilder()
.text("+RESP:GTDAR,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("(d),") // warning type
@@ -1151,7 +1151,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_DTT = new PatternBuilder()
.text("+RESP:GTDTT,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,,,") // device name
.number("d,") // data type
@@ -1189,7 +1189,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_BAA = new PatternBuilder()
.text("+RESP:GTBAA,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("x+,") // index
@@ -1245,7 +1245,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_BID = new PatternBuilder()
.text("+RESP:GTBID,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("d,") // count
@@ -1287,7 +1287,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN_LSA = new PatternBuilder()
.text("+RESP:GTLSA,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("d,") // event state 1
@@ -1327,7 +1327,7 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
private static final Pattern PATTERN = new PatternBuilder()
.text("+").expression("(?:RESP|BUFF):GT...,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("d*,")
@@ -1405,15 +1405,19 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
.number("(d{15}|x{14}),") // imei
.any()
.text(",")
- .number("(d{1,2})?,") // hdop
- .number("(d{1,3}.d)?,") // speed
- .number("(d{1,3})?,") // course
- .number("(-?d{1,5}.d)?,") // altitude
- .number("(-?d{1,3}.d{6})?,") // longitude
- .number("(-?d{1,2}.d{6})?,") // latitude
+ .number("(d{1,2}),") // hdop
+ .groupBegin()
+ .number("(d{1,3}.d),") // speed
+ .number("(d{1,3}),") // course
+ .number("(-?d{1,5}.d),") // altitude
+ .number("(-?d{1,3}.d{6}),") // longitude
+ .number("(-?d{1,2}.d{6}),") // latitude
.number("(dddd)(dd)(dd)") // date (yyyymmdd)
- .number("(dd)(dd)(dd)").optional(2) // time (hhmmss)
+ .number("(dd)(dd)(dd)") // time (hhmmss)
.text(",")
+ .or()
+ .text(",,,,,,")
+ .groupEnd()
.number("(d+),") // mcc
.number("(d+),") // mnc
.number("(x+),") // lac
diff --git a/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java b/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java
index 881209120..a102e9e44 100644
--- a/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java
@@ -131,7 +131,10 @@ public class HuabaoProtocolDecoder extends BaseProtocolDecoder {
if (BitUtil.check(value, 8)) {
return Position.ALARM_POWER_OFF;
}
- if (BitUtil.check(value, 17)) {
+ if (BitUtil.check(value, 15)) {
+ return Position.ALARM_VIBRATION;
+ }
+ if (BitUtil.check(value, 16) || BitUtil.check(value, 17)) {
return Position.ALARM_TAMPERING;
}
if (BitUtil.check(value, 20)) {
@@ -140,7 +143,7 @@ public class HuabaoProtocolDecoder extends BaseProtocolDecoder {
if (BitUtil.check(value, 28)) {
return Position.ALARM_MOVEMENT;
}
- if (BitUtil.check(value, 29)) {
+ if (BitUtil.check(value, 29) || BitUtil.check(value, 30)) {
return Position.ALARM_ACCIDENT;
}
return null;
@@ -488,6 +491,14 @@ public class HuabaoProtocolDecoder extends BaseProtocolDecoder {
position.set(Position.KEY_BATTERY_LEVEL, buf.readUnsignedByte() * 10);
buf.readUnsignedByte(); // reserved
break;
+ case 0x57:
+ int alarm = buf.readUnsignedShort();
+ position.set(Position.KEY_ALARM, BitUtil.check(alarm, 8) ? Position.ALARM_ACCELERATION : null);
+ position.set(Position.KEY_ALARM, BitUtil.check(alarm, 9) ? Position.ALARM_BRAKING : null);
+ position.set(Position.KEY_ALARM, BitUtil.check(alarm, 10) ? Position.ALARM_CORNERING : null);
+ buf.readUnsignedShort(); // external switch state
+ buf.skipBytes(4); // reserved
+ break;
case 0x60:
int event = buf.readUnsignedShort();
position.set(Position.KEY_EVENT, event);
diff --git a/src/main/java/org/traccar/protocol/MeitrackProtocolDecoder.java b/src/main/java/org/traccar/protocol/MeitrackProtocolDecoder.java
index 1235ca9fe..c37d1fe47 100644
--- a/src/main/java/org/traccar/protocol/MeitrackProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/MeitrackProtocolDecoder.java
@@ -534,6 +534,9 @@ public class MeitrackProtocolDecoder extends BaseProtocolDecoder {
case 0xA2:
position.set(Position.KEY_FUEL_CONSUMPTION, buf.readUnsignedIntLE() * 0.01);
break;
+ case 0xFEF4:
+ position.set(Position.KEY_HOURS, buf.readUnsignedIntLE() * 60000);
+ break;
default:
buf.readUnsignedIntLE();
break;
diff --git a/src/main/java/org/traccar/protocol/Minifinder2Protocol.java b/src/main/java/org/traccar/protocol/Minifinder2Protocol.java
index c12933b81..082b9146d 100644
--- a/src/main/java/org/traccar/protocol/Minifinder2Protocol.java
+++ b/src/main/java/org/traccar/protocol/Minifinder2Protocol.java
@@ -31,7 +31,8 @@ public class Minifinder2Protocol extends BaseProtocol {
@Inject
public Minifinder2Protocol(Config config) {
setSupportedDataCommands(
- Command.TYPE_FIRMWARE_UPDATE);
+ Command.TYPE_FIRMWARE_UPDATE,
+ Command.TYPE_CONFIGURATION);
addServer(new TrackerServer(config, getName(), false) {
@Override
protected void addProtocolHandlers(PipelineBuilder pipeline, Config config) {
diff --git a/src/main/java/org/traccar/protocol/Minifinder2ProtocolEncoder.java b/src/main/java/org/traccar/protocol/Minifinder2ProtocolEncoder.java
index fab3c3a6d..72ac9db4e 100644
--- a/src/main/java/org/traccar/protocol/Minifinder2ProtocolEncoder.java
+++ b/src/main/java/org/traccar/protocol/Minifinder2ProtocolEncoder.java
@@ -48,6 +48,13 @@ public class Minifinder2ProtocolEncoder extends BaseProtocolEncoder {
@Override
protected Object encodeCommand(Command command) {
+ if (command.getType().equals(Command.TYPE_CONFIGURATION)) {
+ ByteBuf content = Unpooled.buffer();
+ content.writeByte(Minifinder2ProtocolDecoder.MSG_CONFIGURATION);
+ content.writeByte(1); // length
+ content.writeByte(0xF0); // type
+ }
+
Device device = getCacheManager().getObject(Device.class, command.getDeviceId());
if ("Nano".equalsIgnoreCase(device.getModel())) {
ByteBuf content = Unpooled.buffer();
diff --git a/src/main/java/org/traccar/protocol/Mta6ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Mta6ProtocolDecoder.java
index 896c7a2d2..9704cf099 100644
--- a/src/main/java/org/traccar/protocol/Mta6ProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/Mta6ProtocolDecoder.java
@@ -96,7 +96,7 @@ public class Mta6ProtocolDecoder extends BaseProtocolDecoder {
}
- private static class TimeReader extends FloatReader {
+ private static final class TimeReader extends FloatReader {
private long weekNumber;
diff --git a/src/main/java/org/traccar/protocol/RstProtocolDecoder.java b/src/main/java/org/traccar/protocol/RstProtocolDecoder.java
index d53675b7f..2493f0d9f 100644
--- a/src/main/java/org/traccar/protocol/RstProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/RstProtocolDecoder.java
@@ -42,7 +42,7 @@ public class RstProtocolDecoder extends BaseProtocolDecoder {
.expression("(.{5});") // firmware
.number("(d{9});") // serial number
.number("(d+);") // index
- .number("d+;") // type
+ .number("(d+);") // type
.groupBegin()
.number("(dd)-(dd)-(dddd) ") // event date
.number("(dd):(dd):(dd);") // event time
@@ -69,8 +69,10 @@ public class RstProtocolDecoder extends BaseProtocolDecoder {
.number("x{4};") // sensors
.number("(xx);") // status 1
.number("(xx);") // status 2
+ .expression("(.*)") // additional data
.groupEnd("?")
.any()
+ .text("FIM;")
.compile();
@Override
@@ -87,6 +89,7 @@ public class RstProtocolDecoder extends BaseProtocolDecoder {
String firmware = parser.next();
String serial = parser.next();
int index = parser.nextInt();
+ int type = parser.nextInt();
if (channel != null) {
String response = "RST;A;" + model + ";" + firmware + ";" + serial + ";" + index + ";6;FIM;";
@@ -133,6 +136,11 @@ public class RstProtocolDecoder extends BaseProtocolDecoder {
position.set(Position.PREFIX_TEMP + 1, (int) parser.nextHexInt().byteValue());
position.set(Position.KEY_STATUS, (parser.nextHexInt() << 8) + parser.nextHexInt());
+ String[] values = parser.next().split(";");
+ if (type == 55) {
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, values[0]);
+ }
+
return position;
} else {
diff --git a/src/main/java/org/traccar/protocol/SuntechProtocolDecoder.java b/src/main/java/org/traccar/protocol/SuntechProtocolDecoder.java
index 86a8bf6fe..53c4a5d02 100644
--- a/src/main/java/org/traccar/protocol/SuntechProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/SuntechProtocolDecoder.java
@@ -454,9 +454,10 @@ public class SuntechProtocolDecoder extends BaseProtocolDecoder {
if (values.length - index >= 2) {
String driverUniqueId = values[index++];
- if (values[index++].equals("1") && !driverUniqueId.isEmpty()) {
+ if (!driverUniqueId.isEmpty()) {
position.set(Position.KEY_DRIVER_UNIQUE_ID, driverUniqueId);
}
+ index += 1; // registered
}
if (isIncludeTemp(deviceSession.getDeviceId())) {
diff --git a/src/main/java/org/traccar/schedule/ScheduleManager.java b/src/main/java/org/traccar/schedule/ScheduleManager.java
index 38e8f281c..3756d955b 100644
--- a/src/main/java/org/traccar/schedule/ScheduleManager.java
+++ b/src/main/java/org/traccar/schedule/ScheduleManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 - 2023 Anton Tananaev (anton@traccar.org)
+ * Copyright 2020 - 2024 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.
@@ -39,6 +39,7 @@ public class ScheduleManager implements LifecycleObject {
public void start() {
executor = Executors.newSingleThreadScheduledExecutor();
var tasks = List.of(
+ TaskExpirations.class,
TaskDeleteTemporary.class,
TaskReports.class,
TaskDeviceInactivityCheck.class,
diff --git a/src/main/java/org/traccar/schedule/TaskExpirations.java b/src/main/java/org/traccar/schedule/TaskExpirations.java
new file mode 100644
index 000000000..94f855c5f
--- /dev/null
+++ b/src/main/java/org/traccar/schedule/TaskExpirations.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2024 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.schedule;
+
+import jakarta.inject.Inject;
+import jakarta.mail.MessagingException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.mail.MailManager;
+import org.traccar.model.Device;
+import org.traccar.model.Disableable;
+import org.traccar.model.Server;
+import org.traccar.model.User;
+import org.traccar.notification.TextTemplateFormatter;
+import org.traccar.storage.Storage;
+import org.traccar.storage.StorageException;
+import org.traccar.storage.query.Columns;
+import org.traccar.storage.query.Condition;
+import org.traccar.storage.query.Request;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class TaskExpirations implements ScheduleTask {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TaskExpirations.class);
+
+ private static final long CHECK_PERIOD_HOURS = 1;
+
+ private final Config config;
+ private final Storage storage;
+ private final TextTemplateFormatter textTemplateFormatter;
+ private final MailManager mailManager;
+
+ @Inject
+ public TaskExpirations(
+ Config config, Storage storage, TextTemplateFormatter textTemplateFormatter, MailManager mailManager) {
+ this.config = config;
+ this.storage = storage;
+ this.textTemplateFormatter = textTemplateFormatter;
+ this.mailManager = mailManager;
+ }
+
+ @Override
+ public void schedule(ScheduledExecutorService executor) {
+ executor.scheduleAtFixedRate(this, CHECK_PERIOD_HOURS, CHECK_PERIOD_HOURS, TimeUnit.HOURS);
+ }
+
+ private boolean checkTimeTrigger(Disableable disableable, long currentTime, long offsetTime) {
+ if (disableable.getExpirationTime() != null) {
+ long previousTime = currentTime - TimeUnit.HOURS.toMillis(CHECK_PERIOD_HOURS);
+ long expirationTime = disableable.getExpirationTime().getTime() + offsetTime;
+ return previousTime < expirationTime && currentTime >= expirationTime;
+ }
+ return false;
+ }
+
+ private void sendUserExpiration(
+ Server server, User user, String template) throws MessagingException {
+ var velocityContext = textTemplateFormatter.prepareContext(server, user);
+ velocityContext.put("expiration", user.getExpirationTime());
+ var fullMessage = textTemplateFormatter.formatMessage(velocityContext, template, "full");
+ mailManager.sendMessage(user, true, fullMessage.getSubject(), fullMessage.getBody());
+ }
+
+ private void sendDeviceExpiration(
+ Server server, Device device, String template) throws MessagingException, StorageException {
+ var users = storage.getObjects(User.class, new Request(
+ new Columns.All(), new Condition.Permission(User.class, Device.class, device.getId())));
+ for (User user : users) {
+ var velocityContext = textTemplateFormatter.prepareContext(server, user);
+ velocityContext.put("expiration", device.getExpirationTime());
+ velocityContext.put("device", device);
+ var fullMessage = textTemplateFormatter.formatMessage(velocityContext, template, "full");
+ mailManager.sendMessage(user, true, fullMessage.getSubject(), fullMessage.getBody());
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+
+ long currentTime = System.currentTimeMillis();
+ Server server = storage.getObject(Server.class, new Request(new Columns.All()));
+
+ if (config.getBoolean(Keys.NOTIFICATION_EXPIRATION_USER)) {
+ long reminder = config.getLong(Keys.NOTIFICATION_EXPIRATION_USER_REMINDER);
+ var users = storage.getObjects(User.class, new Request(new Columns.All()));
+ for (User user : users) {
+ if (checkTimeTrigger(user, currentTime, 0)) {
+ sendUserExpiration(server, user, "userExpiration");
+ } else if (reminder > 0 && checkTimeTrigger(user, currentTime, -reminder)) {
+ sendUserExpiration(server, user, "userExpirationReminder");
+ }
+ }
+ }
+
+ if (config.getBoolean(Keys.NOTIFICATION_EXPIRATION_DEVICE)) {
+ long reminder = config.getLong(Keys.NOTIFICATION_EXPIRATION_USER_REMINDER);
+ var devices = storage.getObjects(Device.class, new Request(new Columns.All()));
+ for (Device device : devices) {
+ if (checkTimeTrigger(device, currentTime, 0)) {
+ sendDeviceExpiration(server, device, "deviceExpiration");
+ } else if (reminder > 0 && checkTimeTrigger(device, currentTime, -reminder)) {
+ sendDeviceExpiration(server, device, "deviceExpirationReminder");
+ }
+ }
+ }
+
+ } catch (StorageException | MessagingException e) {
+ LOGGER.warn("Failed to check expirations", e);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/session/ConnectionManager.java b/src/main/java/org/traccar/session/ConnectionManager.java
index 0b13a5a72..1461c66ea 100644
--- a/src/main/java/org/traccar/session/ConnectionManager.java
+++ b/src/main/java/org/traccar/session/ConnectionManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2015 - 2024 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.
@@ -30,6 +30,7 @@ import org.traccar.database.NotificationManager;
import org.traccar.model.BaseModel;
import org.traccar.model.Device;
import org.traccar.model.Event;
+import org.traccar.model.LogRecord;
import org.traccar.model.Position;
import org.traccar.model.User;
import org.traccar.session.cache.CacheManager;
@@ -62,9 +63,11 @@ public class ConnectionManager implements BroadcastInterface {
private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionManager.class);
private final long deviceTimeout;
+ private final boolean showUnknownDevices;
private final Map<Long, DeviceSession> sessionsByDeviceId = new ConcurrentHashMap<>();
- private final Map<Endpoint, Map<String, DeviceSession>> sessionsByEndpoint = new ConcurrentHashMap<>();
+ private final Map<SocketAddress, Map<String, DeviceSession>> sessionsByEndpoint = new ConcurrentHashMap<>();
+ private final Map<SocketAddress, String> unknownByEndpoint = new ConcurrentHashMap<>();
private final Config config;
private final CacheManager cacheManager;
@@ -93,6 +96,7 @@ public class ConnectionManager implements BroadcastInterface {
this.broadcastService = broadcastService;
this.deviceLookupService = deviceLookupService;
deviceTimeout = config.getLong(Keys.STATUS_TIMEOUT);
+ showUnknownDevices = config.getBoolean(Keys.WEB_SHOW_UNKNOWN_DEVICES);
broadcastService.registerListener(this);
}
@@ -102,11 +106,10 @@ public class ConnectionManager implements BroadcastInterface {
public DeviceSession getDeviceSession(
Protocol protocol, Channel channel, SocketAddress remoteAddress,
- String... uniqueIds) throws StorageException {
+ String... uniqueIds) throws Exception {
- Endpoint endpoint = new Endpoint(channel, remoteAddress);
Map<String, DeviceSession> endpointSessions = sessionsByEndpoint.getOrDefault(
- endpoint, new ConcurrentHashMap<>());
+ remoteAddress, new ConcurrentHashMap<>());
uniqueIds = Arrays.stream(uniqueIds).filter(Objects::nonNull).toArray(String[]::new);
if (uniqueIds.length > 0) {
@@ -122,30 +125,31 @@ public class ConnectionManager implements BroadcastInterface {
Device device = deviceLookupService.lookup(uniqueIds);
+ String firstUniqueId = uniqueIds[0];
if (device == null && config.getBoolean(Keys.DATABASE_REGISTER_UNKNOWN)) {
- if (uniqueIds[0].matches(config.getString(Keys.DATABASE_REGISTER_UNKNOWN_REGEX))) {
- device = addUnknownDevice(uniqueIds[0]);
+ if (firstUniqueId.matches(config.getString(Keys.DATABASE_REGISTER_UNKNOWN_REGEX))) {
+ device = addUnknownDevice(firstUniqueId);
}
}
if (device != null) {
+ unknownByEndpoint.remove(remoteAddress);
device.checkDisabled();
DeviceSession oldSession = sessionsByDeviceId.remove(device.getId());
if (oldSession != null) {
- Endpoint oldEndpoint = new Endpoint(oldSession.getChannel(), oldSession.getRemoteAddress());
- Map<String, DeviceSession> oldEndpointSessions = sessionsByEndpoint.get(oldEndpoint);
+ Map<String, DeviceSession> oldEndpointSessions = sessionsByEndpoint.get(oldSession.getRemoteAddress());
if (oldEndpointSessions != null && oldEndpointSessions.size() > 1) {
oldEndpointSessions.remove(device.getUniqueId());
} else {
- sessionsByEndpoint.remove(oldEndpoint);
+ sessionsByEndpoint.remove(oldSession.getRemoteAddress());
}
}
DeviceSession deviceSession = new DeviceSession(
device.getId(), device.getUniqueId(), protocol, channel, remoteAddress);
endpointSessions.put(device.getUniqueId(), deviceSession);
- sessionsByEndpoint.put(endpoint, endpointSessions);
+ sessionsByEndpoint.put(remoteAddress, endpointSessions);
sessionsByDeviceId.put(device.getId(), deviceSession);
if (oldSession == null) {
@@ -154,6 +158,7 @@ public class ConnectionManager implements BroadcastInterface {
return deviceSession;
} else {
+ unknownByEndpoint.put(remoteAddress, firstUniqueId);
LOGGER.warn("Unknown device - " + String.join(" ", uniqueIds)
+ " (" + ((InetSocketAddress) remoteAddress).getHostString() + ")");
return null;
@@ -182,8 +187,8 @@ public class ConnectionManager implements BroadcastInterface {
}
public void deviceDisconnected(Channel channel, boolean supportsOffline) {
- Endpoint endpoint = new Endpoint(channel, channel.remoteAddress());
- Map<String, DeviceSession> endpointSessions = sessionsByEndpoint.remove(endpoint);
+ SocketAddress remoteAddress = channel.remoteAddress();
+ Map<String, DeviceSession> endpointSessions = sessionsByEndpoint.remove(remoteAddress);
if (endpointSessions != null) {
for (DeviceSession deviceSession : endpointSessions.values()) {
if (supportsOffline) {
@@ -193,6 +198,7 @@ public class ConnectionManager implements BroadcastInterface {
cacheManager.removeDevice(deviceSession.getDeviceId());
}
}
+ unknownByEndpoint.remove(remoteAddress);
}
public void deviceUnknown(long deviceId) {
@@ -204,8 +210,7 @@ public class ConnectionManager implements BroadcastInterface {
DeviceSession deviceSession = sessionsByDeviceId.remove(deviceId);
if (deviceSession != null) {
cacheManager.removeDevice(deviceId);
- Endpoint endpoint = new Endpoint(deviceSession.getChannel(), deviceSession.getRemoteAddress());
- sessionsByEndpoint.computeIfPresent(endpoint, (e, sessions) -> {
+ sessionsByEndpoint.computeIfPresent(deviceSession.getRemoteAddress(), (e, sessions) -> {
sessions.remove(deviceSession.getUniqueId());
return sessions.isEmpty() ? null : sessions;
});
@@ -327,11 +332,8 @@ public class ConnectionManager implements BroadcastInterface {
}
@Override
- public synchronized void invalidatePermission(
- boolean local,
- Class<? extends BaseModel> clazz1, long id1,
- Class<? extends BaseModel> clazz2, long id2,
- boolean link) {
+ public synchronized <T1 extends BaseModel, T2 extends BaseModel> void invalidatePermission(
+ boolean local, Class<T1> clazz1, long id1, Class<T2> clazz2, long id2, boolean link) {
if (link && clazz1.equals(User.class) && clazz2.equals(Device.class)) {
if (listeners.containsKey(id1)) {
userDevices.get(id1).add(id2);
@@ -340,11 +342,34 @@ public class ConnectionManager implements BroadcastInterface {
}
}
+ public synchronized void updateLog(LogRecord record) {
+ var sessions = sessionsByEndpoint.getOrDefault(record.getAddress(), Map.of());
+ if (sessions.isEmpty()) {
+ String unknownUniqueId = unknownByEndpoint.get(record.getAddress());
+ if (unknownUniqueId != null && showUnknownDevices) {
+ record.setUniqueId(unknownUniqueId);
+ listeners.values().stream()
+ .flatMap(Set::stream)
+ .forEach((listener) -> listener.onUpdateLog(record));
+ }
+ } else {
+ var firstEntry = sessions.entrySet().iterator().next();
+ record.setUniqueId(firstEntry.getKey());
+ record.setDeviceId(firstEntry.getValue().getDeviceId());
+ for (long userId : deviceUsers.getOrDefault(record.getDeviceId(), Set.of())) {
+ for (UpdateListener listener : listeners.getOrDefault(userId, Set.of())) {
+ listener.onUpdateLog(record);
+ }
+ }
+ }
+ }
+
public interface UpdateListener {
void onKeepalive();
void onUpdateDevice(Device device);
void onUpdatePosition(Position position);
void onUpdateEvent(Event event);
+ void onUpdateLog(LogRecord record);
}
public synchronized void addListener(long userId, UpdateListener listener) throws StorageException {
diff --git a/src/main/java/org/traccar/session/Endpoint.java b/src/main/java/org/traccar/session/Endpoint.java
deleted file mode 100644
index 76aac3444..000000000
--- a/src/main/java/org/traccar/session/Endpoint.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.session;
-
-import io.netty.channel.Channel;
-
-import java.net.SocketAddress;
-import java.util.Objects;
-
-public class Endpoint {
-
- private final Channel channel;
- private final SocketAddress remoteAddress;
-
- public Endpoint(Channel channel, SocketAddress remoteAddress) {
- this.channel = channel;
- this.remoteAddress = remoteAddress;
- }
-
- public Channel getChannel() {
- return channel;
- }
-
- public SocketAddress getRemoteAddress() {
- return remoteAddress;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- Endpoint endpoint = (Endpoint) o;
- return channel.equals(endpoint.channel) && remoteAddress.equals(endpoint.remoteAddress);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(channel, remoteAddress);
- }
-
-}
diff --git a/src/main/java/org/traccar/session/cache/CacheGraph.java b/src/main/java/org/traccar/session/cache/CacheGraph.java
new file mode 100644
index 000000000..c99997288
--- /dev/null
+++ b/src/main/java/org/traccar/session/cache/CacheGraph.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 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.session.cache;
+
+import org.traccar.model.BaseModel;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+public class CacheGraph {
+
+ private final Map<CacheKey, CacheNode> roots = new HashMap<>();
+ private final WeakValueMap<CacheKey, CacheNode> nodes = new WeakValueMap<>();
+
+ void addObject(BaseModel value) {
+ CacheKey key = new CacheKey(value);
+ CacheNode node = new CacheNode(value);
+ roots.put(key, node);
+ nodes.put(key, node);
+ }
+
+ void removeObject(Class<? extends BaseModel> clazz, long id) {
+ CacheKey key = new CacheKey(clazz, id);
+ CacheNode node = nodes.remove(key);
+ if (node != null) {
+ node.getAllLinks(false).forEach(child -> child.getLinks(key.getClazz(), true).remove(node));
+ }
+ roots.remove(key);
+ }
+
+ @SuppressWarnings("unchecked")
+ <T extends BaseModel> T getObject(Class<T> clazz, long id) {
+ CacheNode node = nodes.get(new CacheKey(clazz, id));
+ return node != null ? (T) node.getValue() : null;
+ }
+
+ <T extends BaseModel> Stream<T> getObjects(
+ Class<? extends BaseModel> fromClass, long fromId,
+ Class<T> clazz, Set<Class<? extends BaseModel>> proxies, boolean forward) {
+
+ CacheNode rootNode = nodes.get(new CacheKey(fromClass, fromId));
+ if (rootNode != null) {
+ return getObjectStream(rootNode, clazz, proxies, forward);
+ } else {
+ return Stream.empty();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private <T extends BaseModel> Stream<T> getObjectStream(
+ CacheNode rootNode, Class<T> clazz, Set<Class<? extends BaseModel>> proxies, boolean forward) {
+
+ if (proxies.contains(clazz)) {
+ return Stream.empty();
+ }
+
+ var directSteam = rootNode.getLinks(clazz, forward).stream()
+ .map(node -> (T) node.getValue());
+
+ var proxyStream = proxies.stream()
+ .flatMap(proxyClass -> rootNode.getLinks(proxyClass, forward).stream()
+ .flatMap(node -> getObjectStream(node, clazz, proxies, forward)));
+
+ return Stream.concat(directSteam, proxyStream);
+ }
+
+ void updateObject(BaseModel value) {
+ CacheNode node = nodes.get(new CacheKey(value));
+ if (node != null) {
+ node.setValue(value);
+ }
+ }
+
+ boolean addLink(
+ Class<? extends BaseModel> fromClazz, long fromId,
+ BaseModel toValue) {
+ boolean stop = true;
+ CacheNode fromNode = nodes.get(new CacheKey(fromClazz, fromId));
+ if (fromNode != null) {
+ CacheKey toKey = new CacheKey(toValue);
+ CacheNode toNode = nodes.get(toKey);
+ if (toNode == null) {
+ stop = false;
+ toNode = new CacheNode(toValue);
+ nodes.put(toKey, toNode);
+ }
+ fromNode.getLinks(toValue.getClass(), true).add(toNode);
+ toNode.getLinks(fromClazz, false).add(fromNode);
+ }
+ return stop;
+ }
+
+ void removeLink(
+ Class<? extends BaseModel> fromClazz, long fromId,
+ Class<? extends BaseModel> toClazz, long toId) {
+ CacheNode fromNode = nodes.get(new CacheKey(fromClazz, fromId));
+ if (fromNode != null) {
+ CacheNode toNode = nodes.get(new CacheKey(toClazz, toId));
+ if (toNode != null) {
+ fromNode.getLinks(toClazz, true).remove(toNode);
+ toNode.getLinks(fromClazz, false).remove(fromNode);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (CacheNode node : roots.values()) {
+ printNode(stringBuilder, node, "");
+ }
+ return stringBuilder.toString().trim();
+ }
+
+ private void printNode(StringBuilder stringBuilder, CacheNode node, String indentation) {
+ stringBuilder
+ .append('\n')
+ .append(indentation)
+ .append(node.getValue().getClass().getSimpleName())
+ .append('(').append(node.getValue().getId()).append(')');
+ node.getAllLinks(true).forEach(child -> printNode(stringBuilder, child, indentation + " "));
+ }
+
+}
diff --git a/src/main/java/org/traccar/session/cache/CacheKey.java b/src/main/java/org/traccar/session/cache/CacheKey.java
index 23145e34b..f27d5fbf5 100644
--- a/src/main/java/org/traccar/session/cache/CacheKey.java
+++ b/src/main/java/org/traccar/session/cache/CacheKey.java
@@ -33,6 +33,10 @@ class CacheKey {
this.id = id;
}
+ public Class<? extends BaseModel> getClazz() {
+ return clazz;
+ }
+
public boolean classIs(Class<? extends BaseModel> clazz) {
return clazz.equals(this.clazz);
}
diff --git a/src/main/java/org/traccar/session/cache/CacheManager.java b/src/main/java/org/traccar/session/cache/CacheManager.java
index dc9c86ef3..064e5672f 100644
--- a/src/main/java/org/traccar/session/cache/CacheManager.java
+++ b/src/main/java/org/traccar/session/cache/CacheManager.java
@@ -15,11 +15,10 @@
*/
package org.traccar.session.cache;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
import org.traccar.broadcast.BroadcastInterface;
import org.traccar.broadcast.BroadcastService;
-import org.traccar.model.ObjectOperation;
import org.traccar.config.Config;
import org.traccar.model.Attribute;
import org.traccar.model.BaseModel;
@@ -31,6 +30,8 @@ import org.traccar.model.Group;
import org.traccar.model.GroupedModel;
import org.traccar.model.Maintenance;
import org.traccar.model.Notification;
+import org.traccar.model.ObjectOperation;
+import org.traccar.model.Permission;
import org.traccar.model.Position;
import org.traccar.model.Schedulable;
import org.traccar.model.Server;
@@ -41,19 +42,10 @@ import org.traccar.storage.query.Columns;
import org.traccar.storage.query.Condition;
import org.traccar.storage.query.Request;
-import jakarta.inject.Inject;
-import jakarta.inject.Singleton;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.LinkedList;
-import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
@@ -61,10 +53,8 @@ import java.util.stream.Collectors;
@Singleton
public class CacheManager implements BroadcastInterface {
- private static final Logger LOGGER = LoggerFactory.getLogger(CacheManager.class);
- private static final int GROUP_DEPTH_LIMIT = 3;
- private static final Collection<Class<? extends BaseModel>> CLASSES = Arrays.asList(
- Attribute.class, Driver.class, Geofence.class, Maintenance.class, Notification.class);
+ private static final Set<Class<? extends BaseModel>> GROUPED_CLASSES =
+ Set.of(Attribute.class, Driver.class, Geofence.class, Maintenance.class, Notification.class);
private final Config config;
private final Storage storage;
@@ -72,24 +62,26 @@ public class CacheManager implements BroadcastInterface {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
- private final Map<CacheKey, CacheValue> deviceCache = new HashMap<>();
- private final Map<Long, Integer> deviceReferences = new HashMap<>();
- private final Map<Long, Map<Class<? extends BaseModel>, Set<Long>>> deviceLinks = new HashMap<>();
- private final Map<Long, Position> devicePositions = new HashMap<>();
+ private final CacheGraph graph = new CacheGraph();
private Server server;
- private final Map<Long, List<User>> notificationUsers = new HashMap<>();
+ private final Map<Long, Position> devicePositions = new HashMap<>();
+ private final Map<Long, AtomicInteger> deviceReferences = new HashMap<>();
@Inject
public CacheManager(Config config, Storage storage, BroadcastService broadcastService) throws StorageException {
this.config = config;
this.storage = storage;
this.broadcastService = broadcastService;
- invalidateServer();
- invalidateUsers();
+ server = storage.getObject(Server.class, new Request(new Columns.All()));
broadcastService.registerListener(this);
}
+ @Override
+ public String toString() {
+ return graph.toString();
+ }
+
public Config getConfig() {
return config;
}
@@ -97,29 +89,17 @@ public class CacheManager implements BroadcastInterface {
public <T extends BaseModel> T getObject(Class<T> clazz, long id) {
try {
lock.readLock().lock();
- var cacheValue = deviceCache.get(new CacheKey(clazz, id));
- return cacheValue != null ? cacheValue.getValue() : null;
+ return graph.getObject(clazz, id);
} finally {
lock.readLock().unlock();
}
}
- public <T extends BaseModel> List<T> getDeviceObjects(long deviceId, Class<T> clazz) {
+ public <T extends BaseModel> Set<T> getDeviceObjects(long deviceId, Class<T> clazz) {
try {
lock.readLock().lock();
- var links = deviceLinks.get(deviceId);
- if (links != null) {
- return links.getOrDefault(clazz, new LinkedHashSet<>()).stream()
- .map(id -> {
- var cacheValue = deviceCache.get(new CacheKey(clazz, id));
- return cacheValue != null ? cacheValue.<T>getValue() : null;
- })
- .filter(Objects::nonNull)
- .collect(Collectors.toList());
- } else {
- LOGGER.warn("Device {} cache missing", deviceId);
- return Collections.emptyList();
- }
+ return graph.getObjects(Device.class, deviceId, clazz, Set.of(Group.class), true)
+ .collect(Collectors.toUnmodifiableSet());
} finally {
lock.readLock().unlock();
}
@@ -143,30 +123,45 @@ public class CacheManager implements BroadcastInterface {
}
}
- public List<User> getNotificationUsers(long notificationId, long deviceId) {
+ public Set<User> getNotificationUsers(long notificationId, long deviceId) {
try {
lock.readLock().lock();
- var users = deviceLinks.get(deviceId).get(User.class).stream()
+ Set<User> deviceUsers = getDeviceObjects(deviceId, User.class);
+ return graph.getObjects(Notification.class, notificationId, User.class, Set.of(), false)
+ .filter(deviceUsers::contains)
.collect(Collectors.toUnmodifiableSet());
- return notificationUsers.getOrDefault(notificationId, new LinkedList<>()).stream()
- .filter(user -> users.contains(user.getId()))
- .collect(Collectors.toUnmodifiableList());
} finally {
lock.readLock().unlock();
}
}
- public void addDevice(long deviceId) throws StorageException {
+ public Set<Notification> getDeviceNotifications(long deviceId) {
+ try {
+ lock.readLock().lock();
+ var direct = graph.getObjects(Device.class, deviceId, Notification.class, Set.of(Group.class), true)
+ .map(BaseModel::getId)
+ .collect(Collectors.toUnmodifiableSet());
+ return graph.getObjects(Device.class, deviceId, Notification.class, Set.of(Group.class, User.class), true)
+ .filter(notification -> notification.getAlways() || direct.contains(notification.getId()))
+ .collect(Collectors.toUnmodifiableSet());
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ public void addDevice(long deviceId) throws Exception {
try {
lock.writeLock().lock();
- Integer references = deviceReferences.get(deviceId);
- if (references != null) {
- references += 1;
- } else {
- unsafeAddDevice(deviceId);
- references = 1;
+ if (deviceReferences.computeIfAbsent(deviceId, k -> new AtomicInteger()).getAndIncrement() <= 0) {
+ Device device = storage.getObject(Device.class, new Request(
+ new Columns.All(), new Condition.Equals("id", deviceId)));
+ graph.addObject(device);
+ initializeCache(device);
+ if (device.getPositionId() > 0) {
+ devicePositions.put(deviceId, storage.getObject(Position.class, new Request(
+ new Columns.All(), new Condition.Equals("id", device.getPositionId()))));
+ }
}
- deviceReferences.put(deviceId, references);
} finally {
lock.writeLock().unlock();
}
@@ -175,15 +170,10 @@ public class CacheManager implements BroadcastInterface {
public void removeDevice(long deviceId) {
try {
lock.writeLock().lock();
- Integer references = deviceReferences.get(deviceId);
- if (references != null) {
- references -= 1;
- if (references <= 0) {
- unsafeRemoveDevice(deviceId);
- deviceReferences.remove(deviceId);
- } else {
- deviceReferences.put(deviceId, references);
- }
+ if (deviceReferences.computeIfAbsent(deviceId, k -> new AtomicInteger()).incrementAndGet() <= 0) {
+ graph.removeObject(Device.class, deviceId);
+ devicePositions.remove(deviceId);
+ deviceReferences.remove(deviceId);
}
} finally {
lock.writeLock().unlock();
@@ -193,7 +183,7 @@ public class CacheManager implements BroadcastInterface {
public void updatePosition(Position position) {
try {
lock.writeLock().lock();
- if (deviceLinks.containsKey(position.getDeviceId())) {
+ if (deviceReferences.containsKey(position.getDeviceId())) {
devicePositions.put(position.getDeviceId(), position);
}
} finally {
@@ -202,226 +192,140 @@ public class CacheManager implements BroadcastInterface {
}
@Override
- public void invalidateObject(
- boolean local,
- Class<? extends BaseModel> clazz, long id,
- ObjectOperation operation) {
- try {
- var object = storage.getObject(clazz, new Request(
- new Columns.All(), new Condition.Equals("id", id)));
- if (object != null) {
- updateOrInvalidate(local, object, operation);
- } else {
- invalidate(clazz, id);
- }
- } catch (StorageException e) {
- throw new RuntimeException(e);
- }
- }
-
- public <T extends BaseModel> void updateOrInvalidate(
- boolean local, T object, ObjectOperation operation) throws StorageException {
+ public <T extends BaseModel> void invalidateObject(
+ boolean local, Class<T> clazz, long id, ObjectOperation operation) throws Exception {
if (local) {
- broadcastService.invalidateObject(true, object.getClass(), object.getId(), operation);
+ broadcastService.invalidateObject(true, clazz, id, operation);
}
- if (object instanceof Server) {
- invalidateServer();
+ if (operation == ObjectOperation.DELETE) {
+ graph.removeObject(clazz, id);
+ }
+ if (operation != ObjectOperation.UPDATE) {
return;
}
- if (object instanceof User) {
- invalidateUsers();
+
+ if (clazz.equals(Server.class)) {
+ server = storage.getObject(Server.class, new Request(new Columns.All()));
return;
}
- boolean invalidate = false;
- var before = getObject(object.getClass(), object.getId());
+ var after = storage.getObject(clazz, new Request(new Columns.All(), new Condition.Equals("id", id)));
+ if (after == null) {
+ return;
+ }
+ var before = getObject(after.getClass(), after.getId());
if (before == null) {
return;
- } else if (object instanceof GroupedModel) {
- if (((GroupedModel) before).getGroupId() != ((GroupedModel) object).getGroupId()) {
- invalidate = true;
- }
- } else if (object instanceof Schedulable) {
- if (((Schedulable) before).getCalendarId() != ((Schedulable) object).getCalendarId()) {
- invalidate = true;
- }
}
- if (invalidate) {
- invalidate(object.getClass(), object.getId());
- } else {
- try {
- lock.writeLock().lock();
- deviceCache.get(new CacheKey(object.getClass(), object.getId())).setValue(object);
- } finally {
- lock.writeLock().unlock();
+
+ if (after instanceof GroupedModel) {
+ long beforeGroupId = ((GroupedModel) before).getGroupId();
+ long afterGroupId = ((GroupedModel) after).getGroupId();
+ if (beforeGroupId != afterGroupId) {
+ if (beforeGroupId > 0) {
+ invalidatePermission(clazz, id, Group.class, beforeGroupId, false);
+ }
+ if (afterGroupId > 0) {
+ invalidatePermission(clazz, id, Group.class, afterGroupId, true);
+ }
}
+ } else if (after instanceof Schedulable) {
+ long beforeCalendarId = ((Schedulable) before).getCalendarId();
+ long afterCalendarId = ((Schedulable) after).getCalendarId();
+ if (beforeCalendarId != afterCalendarId) {
+ if (beforeCalendarId > 0) {
+ invalidatePermission(clazz, id, Calendar.class, beforeCalendarId, false);
+ }
+ if (afterCalendarId > 0) {
+ invalidatePermission(clazz, id, Calendar.class, afterCalendarId, true);
+ }
+ }
+ // TODO handle notification always change
}
- }
- public <T extends BaseModel> void invalidate(Class<T> clazz, long id) throws StorageException {
- invalidate(new CacheKey(clazz, id));
+ graph.updateObject(after);
}
@Override
- public void invalidatePermission(
- boolean local,
- Class<? extends BaseModel> clazz1, long id1,
- Class<? extends BaseModel> clazz2, long id2,
- boolean link) {
+ public <T1 extends BaseModel, T2 extends BaseModel> void invalidatePermission(
+ boolean local, Class<T1> clazz1, long id1, Class<T2> clazz2, long id2, boolean link) throws Exception {
if (local) {
broadcastService.invalidatePermission(true, clazz1, id1, clazz2, id2, link);
}
- try {
- invalidate(new CacheKey(clazz1, id1), new CacheKey(clazz2, id2));
- } catch (StorageException e) {
- throw new RuntimeException(e);
+ if (clazz1.equals(User.class) && GroupedModel.class.isAssignableFrom(clazz2)) {
+ invalidatePermission(clazz2, id2, clazz1, id1, link);
+ } else {
+ invalidatePermission(clazz1, id1, clazz2, id2, link);
}
}
- private void invalidateServer() throws StorageException {
- server = storage.getObject(Server.class, new Request(new Columns.All()));
- }
+ private <T1 extends BaseModel, T2 extends BaseModel> void invalidatePermission(
+ Class<T1> fromClass, long fromId, Class<T2> toClass, long toId, boolean link) throws Exception {
- private void invalidateUsers() throws StorageException {
- notificationUsers.clear();
- Map<Long, User> users = new HashMap<>();
- storage.getObjects(User.class, new Request(new Columns.All()))
- .forEach(user -> users.put(user.getId(), user));
- storage.getPermissions(User.class, Notification.class).forEach(permission -> {
- long notificationId = permission.getPropertyId();
- var user = users.get(permission.getOwnerId());
- notificationUsers.computeIfAbsent(notificationId, k -> new LinkedList<>()).add(user);
- });
- }
+ boolean groupLink = GroupedModel.class.isAssignableFrom(fromClass) && toClass.equals(Group.class);
+ boolean calendarLink = Schedulable.class.isAssignableFrom(fromClass) && toClass.equals(Calendar.class);
+ boolean userLink = fromClass.equals(User.class) && toClass.equals(Notification.class);
- private void addObject(long deviceId, BaseModel object) {
- deviceCache.computeIfAbsent(new CacheKey(object), k -> new CacheValue(object)).retain(deviceId);
- }
+ boolean groupedLinks = GroupedModel.class.isAssignableFrom(fromClass)
+ && (GROUPED_CLASSES.contains(toClass) || toClass.equals(User.class));
- private void unsafeAddDevice(long deviceId) throws StorageException {
- Map<Class<? extends BaseModel>, Set<Long>> links = new HashMap<>();
-
- Device device = storage.getObject(Device.class, new Request(
- new Columns.All(), new Condition.Equals("id", deviceId)));
- if (device != null) {
- addObject(deviceId, device);
- if (device.getCalendarId() > 0) {
- var calendar = storage.getObject(Calendar.class, new Request(
- new Columns.All(), new Condition.Equals("id", device.getCalendarId())));
- links.computeIfAbsent(Calendar.class, k -> new LinkedHashSet<>()).add(calendar.getId());
- addObject(deviceId, calendar);
- }
+ if (!groupLink && !calendarLink && !userLink && !groupedLinks) {
+ return;
+ }
- int groupDepth = 0;
- long groupId = device.getGroupId();
- while (groupDepth < GROUP_DEPTH_LIMIT && groupId > 0) {
- Group group = storage.getObject(Group.class, new Request(
- new Columns.All(), new Condition.Equals("id", groupId)));
- links.computeIfAbsent(Group.class, k -> new LinkedHashSet<>()).add(group.getId());
- addObject(deviceId, group);
- groupId = group.getGroupId();
- groupDepth += 1;
+ if (link) {
+ BaseModel object = storage.getObject(toClass, new Request(
+ new Columns.All(), new Condition.Equals("id", toId)));
+ if (!graph.addLink(fromClass, fromId, object)) {
+ initializeCache(object);
}
+ } else {
+ graph.removeLink(fromClass, fromId, toClass, toId);
+ }
+ }
- for (Class<? extends BaseModel> clazz : CLASSES) {
- var objects = storage.getObjects(clazz, new Request(
- new Columns.All(), new Condition.Permission(Device.class, deviceId, clazz)));
- links.put(clazz, objects.stream().map(BaseModel::getId).collect(Collectors.toSet()));
- for (var object : objects) {
- addObject(deviceId, object);
- if (object instanceof Schedulable) {
- var scheduled = (Schedulable) object;
- if (scheduled.getCalendarId() > 0) {
- var calendar = storage.getObject(Calendar.class, new Request(
- new Columns.All(), new Condition.Equals("id", scheduled.getCalendarId())));
- links.computeIfAbsent(Calendar.class, k -> new LinkedHashSet<>()).add(calendar.getId());
- addObject(deviceId, calendar);
- }
- }
+ private void initializeCache(BaseModel object) throws Exception {
+ if (object instanceof User) {
+ for (Permission permission : storage.getPermissions(User.class, Notification.class)) {
+ if (permission.getOwnerId() == object.getId()) {
+ invalidatePermission(
+ permission.getOwnerClass(), permission.getOwnerId(),
+ permission.getPropertyClass(), permission.getPropertyId(), true);
}
}
+ } else {
+ if (object instanceof GroupedModel) {
+ long groupId = ((GroupedModel) object).getGroupId();
+ if (groupId > 0) {
+ invalidatePermission(object.getClass(), object.getId(), Group.class, groupId, true);
+ }
- var users = storage.getObjects(User.class, new Request(
- new Columns.All(), new Condition.Permission(User.class, Device.class, deviceId)));
- links.put(User.class, users.stream().map(BaseModel::getId).collect(Collectors.toSet()));
- for (var user : users) {
- addObject(deviceId, user);
- var notifications = storage.getObjects(Notification.class, new Request(
- new Columns.All(),
- new Condition.Permission(User.class, user.getId(), Notification.class))).stream()
- .filter(Notification::getAlways)
- .collect(Collectors.toList());
- for (var notification : notifications) {
- links.computeIfAbsent(Notification.class, k -> new LinkedHashSet<>()).add(notification.getId());
- addObject(deviceId, notification);
- if (notification.getCalendarId() > 0) {
- var calendar = storage.getObject(Calendar.class, new Request(
- new Columns.All(), new Condition.Equals("id", notification.getCalendarId())));
- links.computeIfAbsent(Calendar.class, k -> new LinkedHashSet<>()).add(calendar.getId());
- addObject(deviceId, calendar);
+ for (Permission permission : storage.getPermissions(User.class, object.getClass())) {
+ if (permission.getPropertyId() == object.getId()) {
+ invalidatePermission(
+ object.getClass(), object.getId(), User.class, permission.getOwnerId(), true);
}
}
- }
-
- deviceLinks.put(deviceId, links);
- if (device.getPositionId() > 0) {
- devicePositions.put(deviceId, storage.getObject(Position.class, new Request(
- new Columns.All(), new Condition.Equals("id", device.getPositionId()))));
+ for (Class<? extends BaseModel> clazz : GROUPED_CLASSES) {
+ for (Permission permission : storage.getPermissions(object.getClass(), clazz)) {
+ if (permission.getOwnerId() == object.getId()) {
+ invalidatePermission(
+ object.getClass(), object.getId(), clazz, permission.getPropertyId(), true);
+ }
+ }
+ }
}
- }
- }
-
- private void unsafeRemoveDevice(long deviceId) {
- deviceCache.remove(new CacheKey(Device.class, deviceId));
- deviceLinks.remove(deviceId).forEach((clazz, ids) -> ids.forEach(id -> {
- var key = new CacheKey(clazz, id);
- deviceCache.computeIfPresent(key, (k, value) -> {
- value.release(deviceId);
- return value.getReferences().isEmpty() ? null : value;
- });
- }));
- devicePositions.remove(deviceId);
- }
- private void invalidate(CacheKey... keys) throws StorageException {
- try {
- lock.writeLock().lock();
- unsafeInvalidate(keys);
- } finally {
- lock.writeLock().unlock();
- }
- }
-
- private void unsafeInvalidate(CacheKey[] keys) throws StorageException {
- boolean invalidateServer = false;
- boolean invalidateUsers = false;
- Set<Long> linkedDevices = new HashSet<>();
- for (var key : keys) {
- if (key.classIs(Server.class)) {
- invalidateServer = true;
- } else {
- if (key.classIs(User.class) || key.classIs(Notification.class)) {
- invalidateUsers = true;
+ if (object instanceof Schedulable) {
+ long calendarId = ((Schedulable) object).getCalendarId();
+ if (calendarId > 0) {
+ invalidatePermission(object.getClass(), object.getId(), Calendar.class, calendarId, true);
}
- deviceCache.computeIfPresent(key, (k, value) -> {
- linkedDevices.addAll(value.getReferences());
- return value;
- });
}
}
- for (long deviceId : linkedDevices) {
- unsafeRemoveDevice(deviceId);
- unsafeAddDevice(deviceId);
- }
- if (invalidateServer) {
- invalidateServer();
- }
- if (invalidateUsers) {
- invalidateUsers();
- }
}
}
diff --git a/src/main/java/org/traccar/session/cache/CacheNode.java b/src/main/java/org/traccar/session/cache/CacheNode.java
new file mode 100644
index 000000000..7b584f81a
--- /dev/null
+++ b/src/main/java/org/traccar/session/cache/CacheNode.java
@@ -0,0 +1,40 @@
+package org.traccar.session.cache;
+
+import org.traccar.model.BaseModel;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+public class CacheNode {
+
+ private BaseModel value;
+
+ private final Map<Class<? extends BaseModel>, Set<CacheNode>> links = new HashMap<>();
+ private final Map<Class<? extends BaseModel>, Set<CacheNode>> backlinks = new HashMap<>();
+
+ public CacheNode(BaseModel value) {
+ this.value = value;
+ }
+
+ public BaseModel getValue() {
+ return value;
+ }
+
+ public void setValue(BaseModel value) {
+ this.value = value;
+ }
+
+ public Set<CacheNode> getLinks(Class<? extends BaseModel> clazz, boolean forward) {
+ var map = forward ? links : backlinks;
+ return map.computeIfAbsent(clazz, k -> new HashSet<>());
+ }
+
+ public Stream<CacheNode> getAllLinks(boolean forward) {
+ var map = forward ? links : backlinks;
+ return map.values().stream().flatMap(Set::stream);
+ }
+
+}
diff --git a/src/main/java/org/traccar/session/cache/CacheValue.java b/src/main/java/org/traccar/session/cache/CacheValue.java
deleted file mode 100644
index 1f0383ce5..000000000
--- a/src/main/java/org/traccar/session/cache/CacheValue.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.session.cache;
-
-import org.traccar.model.BaseModel;
-
-import java.util.HashSet;
-import java.util.Set;
-
-class CacheValue {
-
- private BaseModel value;
- private final Set<Long> references = new HashSet<>();
-
- CacheValue(BaseModel value) {
- this.value = value;
- }
-
- public void retain(long deviceId) {
- references.add(deviceId);
- }
-
- public void release(long deviceId) {
- references.remove(deviceId);
- }
-
- @SuppressWarnings("unchecked")
- public <T extends BaseModel> T getValue() {
- return (T) value;
- }
-
- public void setValue(BaseModel value) {
- this.value = value;
- }
-
- public Set<Long> getReferences() {
- return references;
- }
-
-}
diff --git a/src/main/java/org/traccar/session/cache/WeakValueMap.java b/src/main/java/org/traccar/session/cache/WeakValueMap.java
new file mode 100644
index 000000000..8323e2c30
--- /dev/null
+++ b/src/main/java/org/traccar/session/cache/WeakValueMap.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 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.session.cache;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+
+public class WeakValueMap<K, V> {
+
+ private final Map<K, WeakReference<V>> map = new HashMap<>();
+
+ public void put(K key, V value) {
+ map.put(key, new WeakReference<>(value));
+ }
+
+ public V get(K key) {
+ WeakReference<V> weakReference = map.get(key);
+ return (weakReference != null) ? weakReference.get() : null;
+ }
+
+ public V remove(K key) {
+ WeakReference<V> weakReference = map.remove(key);
+ return (weakReference != null) ? weakReference.get() : null;
+ }
+
+ private void clean() {
+ map.entrySet().removeIf(entry -> entry.getValue().get() == null);
+ }
+
+}
diff --git a/src/test/java/org/traccar/handler/events/MaintenanceEventHandlerTest.java b/src/test/java/org/traccar/handler/events/MaintenanceEventHandlerTest.java
index 5320be926..661336d76 100644
--- a/src/test/java/org/traccar/handler/events/MaintenanceEventHandlerTest.java
+++ b/src/test/java/org/traccar/handler/events/MaintenanceEventHandlerTest.java
@@ -6,14 +6,15 @@ import org.traccar.model.Maintenance;
import org.traccar.model.Position;
import org.traccar.session.cache.CacheManager;
-import java.util.Arrays;
import java.util.Date;
+import java.util.Set;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.anyLong;
public class MaintenanceEventHandlerTest extends BaseTest {
@@ -29,7 +30,7 @@ public class MaintenanceEventHandlerTest extends BaseTest {
var maintenance = mock(Maintenance.class);
when(maintenance.getType()).thenReturn(Position.KEY_TOTAL_DISTANCE);
- var maintenances = Arrays.asList(maintenance);
+ var maintenances = Set.of(maintenance);
var cacheManager = mock(CacheManager.class);
when(cacheManager.getDeviceObjects(anyLong(), eq(Maintenance.class))).thenReturn(maintenances);
@@ -48,12 +49,12 @@ public class MaintenanceEventHandlerTest extends BaseTest {
assertTrue(eventHandler.analyzePosition(position).isEmpty());
lastPosition.set(Position.KEY_TOTAL_DISTANCE, 9999);
- position.set(Position.KEY_TOTAL_DISTANCE, 10001);
- assertTrue(eventHandler.analyzePosition(position).size() == 1);
+ position.set(Position.KEY_TOTAL_DISTANCE, 10001);
+ assertEquals(1, eventHandler.analyzePosition(position).size());
lastPosition.set(Position.KEY_TOTAL_DISTANCE, 11999);
- position.set(Position.KEY_TOTAL_DISTANCE, 12001);
- assertTrue(eventHandler.analyzePosition(position).size() == 1);
+ position.set(Position.KEY_TOTAL_DISTANCE, 12001);
+ assertEquals(1, eventHandler.analyzePosition(position).size());
}
diff --git a/src/test/java/org/traccar/protocol/Gl100ProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/Gl100ProtocolDecoderTest.java
index d835f2f27..3701fa772 100644
--- a/src/test/java/org/traccar/protocol/Gl100ProtocolDecoderTest.java
+++ b/src/test/java/org/traccar/protocol/Gl100ProtocolDecoderTest.java
@@ -12,6 +12,9 @@ public class Gl100ProtocolDecoderTest extends ProtocolTest {
var decoder = inject(new Gl100ProtocolDecoder(null));
verifyPosition(decoder, text(
+ "+RESP:GTRTL,359464032011616,1,0,0,0,0.1,0,1662.5,,36.822301,-1.309476,20230706032920,0639,0002,08DF,1F5E,00,095,0101050105,4470"));
+
+ verifyPosition(decoder, text(
"+RESP:GTLGL,359464030492644,1,2,1,0,0.4,0,299.7,1,5.455551,51.449776,20160311083229,0204,0016,03EC,BD94,00,0036,0102090501"));
verifyPosition(decoder, text(
diff --git a/src/test/java/org/traccar/protocol/Gl200TextProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/Gl200TextProtocolDecoderTest.java
index 199012ca0..2c012eb6f 100644
--- a/src/test/java/org/traccar/protocol/Gl200TextProtocolDecoderTest.java
+++ b/src/test/java/org/traccar/protocol/Gl200TextProtocolDecoderTest.java
@@ -11,18 +11,17 @@ public class Gl200TextProtocolDecoderTest extends ProtocolTest {
var decoder = inject(new Gl200TextProtocolDecoder(null));
- verifyAttribute(decoder, buffer(
- "+RESP:GTFRI,710303,868487004352084,GL530MG,0,0,1,1,16.6,0,9.4,121.307910,31.127837,20230815050629,0460,0000,1815,B93B,26,0,8964,90,1,0,26.6,20230815130830,0174$"),
- Position.PREFIX_TEMP + 1, 26.6);
+ verifyPositions(decoder, buffer(
+ "+RESP:GTFRI,8020040305,866314060272661,,,50,1,1,0.0,0,2957.9,-78.691727,-0.951205,20231227162916,,,,,00,0.0,,,,,100,210100,,,,20231227162916,0117$"));
- verifyPosition(decoder, buffer(
+ verifyPositions(decoder, buffer(
"+BUFF:GTFRI,8020040200,866314060249032,,12194,10,1,3,0.0,0,20.1,-71.596533,-33.524718,20230926200338,0730,0001,772A,052B253E,02,0,0.0,,,,,0,420000,,,,20230926200340,1549$"));
verifyAttribute(decoder, buffer(
"+RESP:GTFRI,423037,866884047716519,GT501,0,1,1,5,12,0.1,0,46.8,-95.559173,30.109955,20231110185836,6,0e36c9916485,-50,,,,e831cd5eb79d,-73,,,,ccf4110c4bd5,-79,,,,acdb48973168,-79,,,,80ab4dc323c4,-82,,,,ec8eb5cfa1c6,-89,,,,310,10,711D,81ECF0F,00,,93,20231110185839,0005$"),
Position.KEY_BATTERY_LEVEL, 93);
- verifyPosition(decoder, buffer(
+ verifyPositions(decoder, buffer(
"+RESP:GTFRI,8020040200,866314060109269,,,10,1,1,0.0,0,9.0,-71.596601,-33.524595,20230722145338,0730,0001,772A,052B253E,00,0.0,,,,,100,210100,,,,20230722145341,0F4C$"));
verifyAttributes(decoder, buffer(
diff --git a/src/test/java/org/traccar/protocol/RstProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/RstProtocolDecoderTest.java
index 0e8aefe51..fc932fe9e 100644
--- a/src/test/java/org/traccar/protocol/RstProtocolDecoderTest.java
+++ b/src/test/java/org/traccar/protocol/RstProtocolDecoderTest.java
@@ -11,6 +11,10 @@ public class RstProtocolDecoderTest extends ProtocolTest {
var decoder = inject(new RstProtocolDecoder(null));
+ verifyAttribute(decoder, text(
+ "RST;A;RST-MINIv5;V9.08;009767055;248;55;14-12-2023 19:34:20;14-12-2023 19:34:21;-12.923640;-38.388313;0;14;17;1;4;15;00;B0;00;1A;02;12.18;4.02;65;21;FE;0000;01;C0;001606017031;0002;FIM;"),
+ Position.KEY_DRIVER_UNIQUE_ID, "001606017031");
+
verifyNull(decoder, text(
"RST;A;RST-MINIv2;V7.04;008051261;124;29;04-04-2021 17:27:26;04-04-2021 17:27:26;-1.280811;-47.931755;7353;79;1;14;7315;26;10;0;1855;0;0;0;0;5;5;-1.280821;-47.931747;04-04-2021 17:52:23;6;-1.280863;-47.931770;04-04-2021 18:12:19;5;-1.280844;-47.931763;04-04-2021 17:28:02;5;-1.280900;-47.931770;04-04-2021 19:04:27;4;-1.280843;-47.931747;04-04-2021 18:21:45;04-04-2021 19:29:59;04-04-2021 19:29:59;-1.280770;-47.931595;1;15;0;0;0;0;FIM;"));
diff --git a/src/test/java/org/traccar/protocol/SuntechProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/SuntechProtocolDecoderTest.java
index cbb68132f..d656bba13 100644
--- a/src/test/java/org/traccar/protocol/SuntechProtocolDecoderTest.java
+++ b/src/test/java/org/traccar/protocol/SuntechProtocolDecoderTest.java
@@ -233,13 +233,13 @@ public class SuntechProtocolDecoderTest extends ProtocolTest {
decoder.setIncludeAdc(true);
verifyAttribute(decoder, buffer(
- "ST600STT;008594432;20;492;20200212;18:58:30;060bb0e1;334;20;36bb;45;+19.337897;-099.064489;000.398;000.00;12;1;5049883;13.61;100100;2;1198;013762;4.2;1;4.68"),
+ "ST600STT;008594432;20;492;20200212;18:58:30;060bb0e1;334;20;36bb;45;+19.337897;-099.064489;000.398;000.00;12;1;5049883;13.61;100100;2;1198;013762;4.2;1;4.68"),
Position.PREFIX_ADC + 1, 4.68);
decoder.setIncludeTemp(true);
verifyAttribute(decoder, buffer(
- "ST600STT;008350848;35;523;20191102;13:49:46;0bf14fdb;334;20;2f19;57;+20.466737;-100.825455;000.006;000.00;11;1;10274175;11.36;00000000;1;0300;018353;4.2;1;0.00;;;;00000000000000;0;28EE56B911160234:+13.7;:;:"),
+ "ST600STT;008350848;35;523;20191102;13:49:46;0bf14fdb;334;20;2f19;57;+20.466737;-100.825455;000.006;000.00;11;1;10274175;11.36;00000000;1;0300;018353;4.2;1;0.00;;;;00000000000000;0;28EE56B911160234:+13.7;:;:"),
Position.PREFIX_TEMP + 2, 13.7);
verifyPosition(decoder, buffer(
@@ -262,7 +262,7 @@ public class SuntechProtocolDecoderTest extends ProtocolTest {
decoder.setIncludeRpm(true);
verifyAttribute(decoder, buffer(
- "ST300STT;907131077;04;706;20190227;23:59:34;cc719;-12.963490;-038.499587;000.067;000.00;7;1;57095;12.50;000000;1;0337;000207;0.0;1;0;012E717F010000;1"),
+ "ST300STT;907131077;04;706;20190227;23:59:34;cc719;-12.963490;-038.499587;000.067;000.00;7;1;57095;12.50;000000;1;0337;000207;0.0;1;0;012E717F010000;1"),
Position.KEY_RPM, 0);
}
@@ -275,7 +275,7 @@ public class SuntechProtocolDecoderTest extends ProtocolTest {
decoder.setHbm(true);
verifyAttribute(decoder, buffer(
- "ST300ALT;007239104;40;313;20190112;01:07:16;c99139;+04.703287;-074.148897;000.000;189.72;21;1;425512;12.61;100000;33;003188;4.1;1"),
+ "ST300ALT;007239104;40;313;20190112;01:07:16;c99139;+04.703287;-074.148897;000.000;189.72;21;1;425512;12.61;100000;33;003188;4.1;1"),
Position.KEY_HOURS, 3188 * 60000L);
}
@@ -286,13 +286,19 @@ public class SuntechProtocolDecoderTest extends ProtocolTest {
var decoder = inject(new SuntechProtocolDecoder(null));
verifyAttribute(decoder, buffer(
- "ST300HTE;511050566;45;308;20200909;13:38:38;0;12.50;001354;0.0;1;0;1;1;0;-27.636632;-052.277933;-27.636675;-052.277947;000.000;002.296;0;00000000000000"),
+ "ST300HTE;511050566;45;308;20200909;13:38:38;0;12.50;001354;0.0;1;0;1;1;0;-27.636632;-052.277933;-27.636675;-052.277947;000.000;002.296;0;00000000000000"),
Position.KEY_DRIVER_UNIQUE_ID, "00000000000000");
verifyAttribute(decoder, buffer(
- "ST300HTE;100850001;04;248;20110101;00:13:52;167559;12.28;004005;0.0;1;0;3;3;0;-22.881018;-047.070831;-22.881018;-047.070831;000.000;000.000;0;0;3;0;0;0;01E04D44160000"),
+ "ST300HTE;100850001;04;248;20110101;00:13:52;167559;12.28;004005;0.0;1;0;3;3;0;-22.881018;-047.070831;-22.881018;-047.070831;000.000;000.000;0;0;3;0;0;0;01E04D44160000"),
Position.KEY_DRIVER_UNIQUE_ID, "01E04D44160000");
+ decoder.setHbm(true);
+
+ verifyAttribute(decoder, buffer(
+ "ST300STT;807469112;45;315;20231215;15:25:03;104147;-16.030168;-047.989150;000.000;000.00;19;1;8600;12.14;000010;1;0456;000373;4.1;1;01B54221010000;0"),
+ Position.KEY_DRIVER_UNIQUE_ID, "01B54221010000");
+
}
}
diff --git a/swagger.json b/swagger.json
index 982e1bff1..9268e1a0d 100644
--- a/swagger.json
+++ b/swagger.json
@@ -2,7 +2,7 @@
"openapi": "3.0.1",
"info": {
"title": "Traccar",
- "version": "5.10",
+ "version": "5.11",
"description": "Traccar GPS tracking server API documentation. To use the API you need to have a server instance. For testing purposes you can use one of free [demo servers](https://www.traccar.org/demo-server/). For production use you can install your own server or get a [subscription service](https://www.traccar.org/product/tracking-server/).",
"contact": {
"name": "Traccar Support",
diff --git a/templates/full/deviceExpiration.vm b/templates/full/deviceExpiration.vm
new file mode 100644
index 000000000..879b31778
--- /dev/null
+++ b/templates/full/deviceExpiration.vm
@@ -0,0 +1,7 @@
+#set($subject = "Device expiration")
+<!DOCTYPE html>
+<html>
+<body>
+Your device $device.name has expired.
+</body>
+</html>
diff --git a/templates/full/deviceExpirationReminder.vm b/templates/full/deviceExpirationReminder.vm
new file mode 100644
index 000000000..aa47ac0ed
--- /dev/null
+++ b/templates/full/deviceExpirationReminder.vm
@@ -0,0 +1,7 @@
+#set($subject = "Device expiration reminder")
+<!DOCTYPE html>
+<html>
+<body>
+Your device $device.name will expire on $dateTool.format("YYYY-MM-dd", $expiration, $locale, $timezone).
+</body>
+</html>
diff --git a/templates/full/userExpiration.vm b/templates/full/userExpiration.vm
new file mode 100644
index 000000000..43fe2563e
--- /dev/null
+++ b/templates/full/userExpiration.vm
@@ -0,0 +1,7 @@
+#set($subject = "Account expiration")
+<!DOCTYPE html>
+<html>
+<body>
+Your user account has expired.
+</body>
+</html>
diff --git a/templates/full/userExpirationReminder.vm b/templates/full/userExpirationReminder.vm
new file mode 100644
index 000000000..ecdee0588
--- /dev/null
+++ b/templates/full/userExpirationReminder.vm
@@ -0,0 +1,7 @@
+#set($subject = "Account expiration reminder")
+<!DOCTYPE html>
+<html>
+<body>
+Your user account will expire on $dateTool.format("YYYY-MM-dd", $expiration, $locale, $timezone).
+</body>
+</html>
diff --git a/traccar-web b/traccar-web
-Subproject dc46059f6bfeedca04333c2839872055db066dc
+Subproject d1a9c08571683184ce10b7db5890e2a75bf6179