diff options
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 |