aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAnton Tananaev <anton@traccar.org>2022-11-13 11:48:43 -0800
committerAnton Tananaev <anton@traccar.org>2022-11-13 11:48:43 -0800
commit5877cb1b3f1fa7331c4310b9754a3ec442586497 (patch)
tree6cc3e7193412aa152c1b593bf0ad7e4119687a1e /src
parentff793b2e2872f8ba076e748fab41d944f92b64d4 (diff)
downloadtrackermap-server-5877cb1b3f1fa7331c4310b9754a3ec442586497.tar.gz
trackermap-server-5877cb1b3f1fa7331c4310b9754a3ec442586497.tar.bz2
trackermap-server-5877cb1b3f1fa7331c4310b9754a3ec442586497.zip
Refactor position forwarding
Diffstat (limited to 'src')
-rw-r--r--src/main/java/org/traccar/BasePipelineFactory.java2
-rw-r--r--src/main/java/org/traccar/MainModule.java16
-rw-r--r--src/main/java/org/traccar/PositionForwardingHandler.java141
-rw-r--r--src/main/java/org/traccar/WebDataHandler.java316
-rw-r--r--src/main/java/org/traccar/config/Keys.java17
-rw-r--r--src/main/java/org/traccar/database/NotificationManager.java6
-rw-r--r--src/main/java/org/traccar/forward/EventForwarder.java2
-rw-r--r--src/main/java/org/traccar/forward/EventForwarderJson.java22
-rw-r--r--src/main/java/org/traccar/forward/PositionData.java45
-rw-r--r--src/main/java/org/traccar/forward/PositionForwarder.java20
-rw-r--r--src/main/java/org/traccar/forward/PositionForwarderJson.java86
-rw-r--r--src/main/java/org/traccar/forward/PositionForwarderUrl.java166
-rw-r--r--src/main/java/org/traccar/forward/ResultHandler.java20
-rw-r--r--src/test/java/org/traccar/forward/PositionForwarderUrlTest.java (renamed from src/test/java/org/traccar/WebDataHandlerTest.java)18
14 files changed, 528 insertions, 349 deletions
diff --git a/src/main/java/org/traccar/BasePipelineFactory.java b/src/main/java/org/traccar/BasePipelineFactory.java
index 0d91ec7e4..b184da45c 100644
--- a/src/main/java/org/traccar/BasePipelineFactory.java
+++ b/src/main/java/org/traccar/BasePipelineFactory.java
@@ -140,7 +140,7 @@ public abstract class BasePipelineFactory extends ChannelInitializer<Channel> {
CopyAttributesHandler.class,
EngineHoursHandler.class,
ComputedAttributesHandler.class,
- WebDataHandler.class,
+ PositionForwardingHandler.class,
DefaultDataHandler.class,
MediaEventHandler.class,
CommandResultEventHandler.class,
diff --git a/src/main/java/org/traccar/MainModule.java b/src/main/java/org/traccar/MainModule.java
index 9a5cca4c7..9d450fef7 100644
--- a/src/main/java/org/traccar/MainModule.java
+++ b/src/main/java/org/traccar/MainModule.java
@@ -37,6 +37,9 @@ import org.traccar.database.LdapProvider;
import org.traccar.database.StatisticsManager;
import org.traccar.forward.EventForwarder;
import org.traccar.forward.EventForwarderJson;
+import org.traccar.forward.PositionForwarder;
+import org.traccar.forward.PositionForwarderJson;
+import org.traccar.forward.PositionForwarderUrl;
import org.traccar.geocoder.AddressFormat;
import org.traccar.geocoder.BanGeocoder;
import org.traccar.geocoder.BingMapsGeocoder;
@@ -320,6 +323,19 @@ public class MainModule extends AbstractModule {
@Singleton
@Provides
+ public static PositionForwarder providePositionForwarder(Config config, Client client, ObjectMapper objectMapper) {
+ if (config.hasKey(Keys.FORWARD_URL)) {
+ if (config.getBoolean(Keys.FORWARD_JSON)) {
+ return new PositionForwarderJson(config, client, objectMapper);
+ } else {
+ return new PositionForwarderUrl(config, client, objectMapper);
+ }
+ }
+ return null;
+ }
+
+ @Singleton
+ @Provides
public static VelocityEngine provideVelocityEngine(Config config) {
Properties properties = new Properties();
properties.setProperty("file.resource.loader.path", config.getString(Keys.TEMPLATES_ROOT) + "/");
diff --git a/src/main/java/org/traccar/PositionForwardingHandler.java b/src/main/java/org/traccar/PositionForwardingHandler.java
new file mode 100644
index 000000000..83f91e937
--- /dev/null
+++ b/src/main/java/org/traccar/PositionForwardingHandler.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.channel.ChannelHandler;
+import io.netty.util.Timeout;
+import io.netty.util.Timer;
+import io.netty.util.TimerTask;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.forward.PositionData;
+import org.traccar.forward.PositionForwarder;
+import org.traccar.forward.ResultHandler;
+import org.traccar.model.Device;
+import org.traccar.model.Position;
+import org.traccar.session.cache.CacheManager;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Singleton
+@ChannelHandler.Sharable
+public class PositionForwardingHandler extends BaseDataHandler {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PositionForwardingHandler.class);
+
+ private final CacheManager cacheManager;
+ private final Timer timer;
+
+ private final PositionForwarder positionForwarder;
+
+ private final boolean retryEnabled;
+ private final int retryDelay;
+ private final int retryCount;
+ private final int retryLimit;
+
+ private final AtomicInteger deliveryPending;
+
+ @Inject
+ public PositionForwardingHandler(
+ Config config, CacheManager cacheManager, Timer timer, @Nullable PositionForwarder positionForwarder) {
+
+ this.cacheManager = cacheManager;
+ this.timer = timer;
+ this.positionForwarder = positionForwarder;
+
+ this.retryEnabled = config.getBoolean(Keys.FORWARD_RETRY_ENABLE);
+ this.retryDelay = config.getInteger(Keys.FORWARD_RETRY_DELAY);
+ this.retryCount = config.getInteger(Keys.FORWARD_RETRY_COUNT);
+ this.retryLimit = config.getInteger(Keys.FORWARD_RETRY_LIMIT);
+
+ this.deliveryPending = new AtomicInteger();
+ }
+
+ class AsyncRequestAndCallback implements ResultHandler, TimerTask {
+
+ private final PositionData positionData;
+
+ private int retries = 0;
+
+ AsyncRequestAndCallback(PositionData positionData) {
+ this.positionData = positionData;
+ deliveryPending.incrementAndGet();
+ }
+
+ private void send() {
+ positionForwarder.forward(positionData, this);
+ }
+
+ private void retry(Throwable throwable) {
+ boolean scheduled = false;
+ try {
+ if (retryEnabled && deliveryPending.get() <= retryLimit && retries < retryCount) {
+ schedule();
+ scheduled = true;
+ }
+ } finally {
+ int pending = scheduled ? deliveryPending.get() : deliveryPending.decrementAndGet();
+ LOGGER.warn("Position forwarding failed: " + pending + " pending", throwable);
+ }
+ }
+
+ private void schedule() {
+ timer.newTimeout(this, retryDelay * (long) Math.pow(2, retries++), TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void onResult(boolean success, Throwable throwable) {
+ if (success) {
+ deliveryPending.decrementAndGet();
+ } else {
+ retry(throwable);
+ }
+ }
+
+ @Override
+ public void run(Timeout timeout) {
+ boolean sent = false;
+ try {
+ if (!timeout.isCancelled()) {
+ send();
+ sent = true;
+ }
+ } finally {
+ if (!sent) {
+ deliveryPending.decrementAndGet();
+ }
+ }
+ }
+ }
+
+ @Override
+ protected Position handlePosition(Position position) {
+ if (positionForwarder != null) {
+ PositionData positionData = new PositionData();
+ positionData.setPosition(position);
+ positionData.setDevice(cacheManager.getObject(Device.class, position.getDeviceId()));
+ new AsyncRequestAndCallback(positionData).send();
+ }
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/WebDataHandler.java b/src/main/java/org/traccar/WebDataHandler.java
deleted file mode 100644
index 2d2e3dc8f..000000000
--- a/src/main/java/org/traccar/WebDataHandler.java
+++ /dev/null
@@ -1,316 +0,0 @@
-/*
- * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.traccar;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.netty.channel.ChannelHandler;
-import io.netty.util.Timer;
-import io.netty.util.Timeout;
-import io.netty.util.TimerTask;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.traccar.config.Config;
-import org.traccar.config.Keys;
-import org.traccar.helper.Checksum;
-import org.traccar.model.Device;
-import org.traccar.model.Position;
-import org.traccar.model.Group;
-import org.traccar.session.cache.CacheManager;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.client.Client;
-import javax.ws.rs.client.Entity;
-import javax.ws.rs.client.Invocation;
-import javax.ws.rs.client.InvocationCallback;
-import java.util.HashMap;
-import java.util.Map;
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.util.Calendar;
-import java.util.Formatter;
-import java.util.Locale;
-import java.util.TimeZone;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-@Singleton
-@ChannelHandler.Sharable
-public class WebDataHandler extends BaseDataHandler {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(WebDataHandler.class);
-
- private static final String KEY_POSITION = "position";
- private static final String KEY_DEVICE = "device";
-
- private final CacheManager cacheManager;
- private final ObjectMapper objectMapper;
- private final Client client;
- private final Timer timer;
-
- private final String url;
- private final String header;
- private final boolean json;
- private final boolean urlVariables;
-
- private final boolean retryEnabled;
- private final int retryDelay;
- private final int retryCount;
- private final int retryLimit;
-
- private final AtomicInteger deliveryPending;
-
- @Inject
- public WebDataHandler(
- Config config, CacheManager cacheManager, ObjectMapper objectMapper, Client client, Timer timer) {
-
- this.cacheManager = cacheManager;
- this.objectMapper = objectMapper;
- this.client = client;
- this.timer = timer;
- this.url = config.getString(Keys.FORWARD_URL);
- this.header = config.getString(Keys.FORWARD_HEADER);
- this.json = config.getBoolean(Keys.FORWARD_JSON);
- this.urlVariables = config.getBoolean(Keys.FORWARD_URL_VARIABLES);
-
- this.retryEnabled = config.getBoolean(Keys.FORWARD_RETRY_ENABLE);
- this.retryDelay = config.getInteger(Keys.FORWARD_RETRY_DELAY, 100);
- this.retryCount = config.getInteger(Keys.FORWARD_RETRY_COUNT, 10);
- this.retryLimit = config.getInteger(Keys.FORWARD_RETRY_LIMIT, 100);
-
- this.deliveryPending = new AtomicInteger(0);
- }
-
- private static String formatSentence(Position position) {
-
- StringBuilder s = new StringBuilder("$GPRMC,");
-
- try (Formatter f = new Formatter(s, Locale.ENGLISH)) {
-
- Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ENGLISH);
- calendar.setTimeInMillis(position.getFixTime().getTime());
-
- f.format("%1$tH%1$tM%1$tS.%1$tL,A,", calendar);
-
- double lat = position.getLatitude();
- double lon = position.getLongitude();
-
- f.format("%02d%07.4f,%c,", (int) Math.abs(lat), Math.abs(lat) % 1 * 60, lat < 0 ? 'S' : 'N');
- f.format("%03d%07.4f,%c,", (int) Math.abs(lon), Math.abs(lon) % 1 * 60, lon < 0 ? 'W' : 'E');
-
- f.format("%.2f,%.2f,", position.getSpeed(), position.getCourse());
- f.format("%1$td%1$tm%1$ty,,", calendar);
- }
-
- s.append(Checksum.nmea(s.substring(1)));
-
- return s.toString();
- }
-
- // OpenGTS status code
- private String calculateStatus(Position position) {
- if (position.hasAttribute(Position.KEY_ALARM)) {
- return "0xF841"; // STATUS_PANIC_ON
- } else if (position.getSpeed() < 1.0) {
- return "0xF020"; // STATUS_LOCATION
- } else {
- return "0xF11C"; // STATUS_MOTION_MOVING
- }
- }
-
- public String formatRequest(Position position) throws UnsupportedEncodingException, JsonProcessingException {
-
- Device device = cacheManager.getObject(Device.class, position.getDeviceId());
-
- String request = url
- .replace("{name}", URLEncoder.encode(device.getName(), StandardCharsets.UTF_8.name()))
- .replace("{uniqueId}", device.getUniqueId())
- .replace("{status}", device.getStatus())
- .replace("{deviceId}", String.valueOf(position.getDeviceId()))
- .replace("{protocol}", String.valueOf(position.getProtocol()))
- .replace("{deviceTime}", String.valueOf(position.getDeviceTime().getTime()))
- .replace("{fixTime}", String.valueOf(position.getFixTime().getTime()))
- .replace("{valid}", String.valueOf(position.getValid()))
- .replace("{latitude}", String.valueOf(position.getLatitude()))
- .replace("{longitude}", String.valueOf(position.getLongitude()))
- .replace("{altitude}", String.valueOf(position.getAltitude()))
- .replace("{speed}", String.valueOf(position.getSpeed()))
- .replace("{course}", String.valueOf(position.getCourse()))
- .replace("{accuracy}", String.valueOf(position.getAccuracy()))
- .replace("{statusCode}", calculateStatus(position));
-
- if (position.getAddress() != null) {
- request = request.replace(
- "{address}", URLEncoder.encode(position.getAddress(), StandardCharsets.UTF_8.name()));
- }
-
- if (request.contains("{attributes}")) {
- String attributes = objectMapper.writeValueAsString(position.getAttributes());
- request = request.replace(
- "{attributes}", URLEncoder.encode(attributes, StandardCharsets.UTF_8.name()));
- }
-
- if (request.contains("{gprmc}")) {
- request = request.replace("{gprmc}", formatSentence(position));
- }
-
- if (request.contains("{group}")) {
- String deviceGroupName = "";
- if (device.getGroupId() != 0) {
- Group group = cacheManager.getObject(Group.class, device.getGroupId());
- if (group != null) {
- deviceGroupName = group.getName();
- }
- }
-
- request = request.replace("{group}", URLEncoder.encode(deviceGroupName, StandardCharsets.UTF_8.name()));
- }
-
- return request;
- }
-
- class AsyncRequestAndCallback implements InvocationCallback<Response>, TimerTask {
-
- private int retries = 0;
- private Map<String, Object> payload;
- private final Invocation.Builder requestBuilder;
- private MediaType mediaType = MediaType.APPLICATION_JSON_TYPE;
-
- AsyncRequestAndCallback(Position position) {
-
- String formattedUrl;
- try {
- formattedUrl = json && !urlVariables ? url : formatRequest(position);
- } catch (UnsupportedEncodingException | JsonProcessingException e) {
- throw new RuntimeException("Forwarding formatting error", e);
- }
-
- requestBuilder = client.target(formattedUrl).request();
- if (header != null && !header.isEmpty()) {
- for (String line: header.split("\\r?\\n")) {
- String[] values = line.split(":", 2);
- String headerName = values[0].trim();
- String headerValue = values[1].trim();
- if (headerName.equals(HttpHeaders.CONTENT_TYPE)) {
- mediaType = MediaType.valueOf(headerValue);
- } else {
- requestBuilder.header(headerName, headerValue);
- }
- }
- }
-
- if (json) {
- payload = prepareJsonPayload(position);
- }
-
- deliveryPending.incrementAndGet();
- }
-
- private void send() {
- LOGGER.debug("Position forwarding initiated");
- if (json) {
- try {
- Entity<String> entity = Entity.entity(objectMapper.writeValueAsString(payload), mediaType);
- requestBuilder.async().post(entity, this);
- } catch (JsonProcessingException e) {
- throw new RuntimeException("Failed to serialize location to json", e);
- }
- } else {
- requestBuilder.async().get(this);
- }
- }
-
- private void retry(Throwable throwable) {
- boolean scheduled = false;
- try {
- if (retryEnabled && deliveryPending.get() <= retryLimit && retries < retryCount) {
- schedule();
- scheduled = true;
- }
- } finally {
- int pending = scheduled ? deliveryPending.get() : deliveryPending.decrementAndGet();
- LOGGER.warn("Position forwarding failed: " + pending + " pending", throwable);
- }
- }
-
- private void schedule() {
- timer.newTimeout(this, retryDelay * (long) Math.pow(2, retries++), TimeUnit.MILLISECONDS);
- }
-
- @Override
- public void completed(Response response) {
- if (response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) {
- deliveryPending.decrementAndGet();
- LOGGER.debug("Position forwarding succeeded");
- } else {
- retry(new RuntimeException("Status code 2xx expected"));
- }
- }
-
- @Override
- public void failed(Throwable throwable) {
- retry(throwable);
- }
-
- @Override
- public void run(Timeout timeout) {
- boolean sent = false;
- try {
- if (!timeout.isCancelled()) {
- send();
- sent = true;
- }
- } finally {
- if (!sent) {
- deliveryPending.decrementAndGet();
- }
- }
- }
-
- }
-
- @Override
- protected Position handlePosition(Position position) {
-
- if (url != null) {
- AsyncRequestAndCallback request = new AsyncRequestAndCallback(position);
- request.send();
- }
-
- return position;
- }
-
- private Map<String, Object> prepareJsonPayload(Position position) {
-
- Map<String, Object> data = new HashMap<>();
- Device device = cacheManager.getObject(Device.class, position.getDeviceId());
-
- data.put(KEY_POSITION, position);
-
- if (device != null) {
- data.put(KEY_DEVICE, device);
- }
-
- return data;
- }
-
-}
diff --git a/src/main/java/org/traccar/config/Keys.java b/src/main/java/org/traccar/config/Keys.java
index 1da01518c..b60cd82a0 100644
--- a/src/main/java/org/traccar/config/Keys.java
+++ b/src/main/java/org/traccar/config/Keys.java
@@ -721,14 +721,6 @@ public final class Keys {
List.of(KeyType.CONFIG));
/**
- * Boolean value to enable URL parameters in json mode. For example, {uniqueId} for device identifier,
- * {latitude} and {longitude} for coordinates.
- */
- public static final ConfigKey<Boolean> FORWARD_URL_VARIABLES = new BooleanConfigKey(
- "forward.urlVariables",
- List.of(KeyType.CONFIG));
-
- /**
* Position forwarding retrying enable. When enabled, additional attempts are made to deliver positions. If initial
* delivery fails, because of an unreachable server or an HTTP response different from '2xx', the software waits
* for 'forward.retry.delay' milliseconds to retry delivery. On subsequent failures, this delay is duplicated.
@@ -745,7 +737,8 @@ public final class Keys {
*/
public static final ConfigKey<Integer> FORWARD_RETRY_DELAY = new IntegerConfigKey(
"forward.retry.delay",
- List.of(KeyType.CONFIG));
+ List.of(KeyType.CONFIG),
+ 100);
/**
* Position forwarding retry maximum retries.
@@ -753,7 +746,8 @@ public final class Keys {
*/
public static final ConfigKey<Integer> FORWARD_RETRY_COUNT = new IntegerConfigKey(
"forward.retry.count",
- List.of(KeyType.CONFIG));
+ List.of(KeyType.CONFIG),
+ 10);
/**
* Position forwarding retry pending positions limit.
@@ -761,7 +755,8 @@ public final class Keys {
*/
public static final ConfigKey<Integer> FORWARD_RETRY_LIMIT = new IntegerConfigKey(
"forward.retry.limit",
- List.of(KeyType.CONFIG));
+ List.of(KeyType.CONFIG),
+ 100);
/**
* Events forwarding URL.
diff --git a/src/main/java/org/traccar/database/NotificationManager.java b/src/main/java/org/traccar/database/NotificationManager.java
index 7c82454b2..1eec7e097 100644
--- a/src/main/java/org/traccar/database/NotificationManager.java
+++ b/src/main/java/org/traccar/database/NotificationManager.java
@@ -129,7 +129,11 @@ public class NotificationManager {
if (event.getMaintenanceId() != 0) {
eventData.setMaintenance(cacheManager.getObject(Maintenance.class, event.getMaintenanceId()));
}
- eventForwarder.forward(eventData);
+ eventForwarder.forward(eventData, (success, throwable) -> {
+ if (!success) {
+ LOGGER.warn("Event forwarding failed", throwable);
+ }
+ });
}
}
diff --git a/src/main/java/org/traccar/forward/EventForwarder.java b/src/main/java/org/traccar/forward/EventForwarder.java
index c3ad5efc0..1f991c0b5 100644
--- a/src/main/java/org/traccar/forward/EventForwarder.java
+++ b/src/main/java/org/traccar/forward/EventForwarder.java
@@ -16,5 +16,5 @@
package org.traccar.forward;
public interface EventForwarder {
- void forward(EventData eventData);
+ void forward(EventData eventData, ResultHandler resultHandler);
}
diff --git a/src/main/java/org/traccar/forward/EventForwarderJson.java b/src/main/java/org/traccar/forward/EventForwarderJson.java
index c136fd4e2..7527d568a 100644
--- a/src/main/java/org/traccar/forward/EventForwarderJson.java
+++ b/src/main/java/org/traccar/forward/EventForwarderJson.java
@@ -15,33 +15,29 @@
*/
package org.traccar.forward;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.traccar.config.Config;
import org.traccar.config.Keys;
-import javax.inject.Inject;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.InvocationCallback;
+import javax.ws.rs.core.Response;
public class EventForwarderJson implements EventForwarder {
- private static final Logger LOGGER = LoggerFactory.getLogger(EventForwarderJson.class);
-
private final String url;
private final String header;
private final Client client;
- @Inject
public EventForwarderJson(Config config, Client client) {
this.client = client;
url = config.getString(Keys.EVENT_FORWARD_URL);
header = config.getString(Keys.EVENT_FORWARD_HEADERS);
}
- public void forward(EventData eventData) {
+ @Override
+ public void forward(EventData eventData, ResultHandler resultHandler) {
var requestBuilder = client.target(url).request();
if (header != null && !header.isEmpty()) {
@@ -51,14 +47,20 @@ public class EventForwarderJson implements EventForwarder {
}
}
- requestBuilder.async().post(Entity.json(eventData), new InvocationCallback<>() {
+ requestBuilder.async().post(Entity.json(eventData), new InvocationCallback<Response>() {
@Override
- public void completed(Object o) {
+ public void completed(Response response) {
+ if (response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) {
+ resultHandler.onResult(true, null);
+ } else {
+ int code = response.getStatusInfo().getStatusCode();
+ resultHandler.onResult(false, new RuntimeException("HTTP code " + code));
+ }
}
@Override
public void failed(Throwable throwable) {
- LOGGER.warn("Event forwarding failed", throwable);
+ resultHandler.onResult(false, throwable);
}
});
}
diff --git a/src/main/java/org/traccar/forward/PositionData.java b/src/main/java/org/traccar/forward/PositionData.java
new file mode 100644
index 000000000..784cf52f5
--- /dev/null
+++ b/src/main/java/org/traccar/forward/PositionData.java
@@ -0,0 +1,45 @@
+/*
+ * 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.forward;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.traccar.model.Device;
+import org.traccar.model.Position;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class PositionData {
+
+ private Position position;
+
+ public Position getPosition() {
+ return position;
+ }
+
+ public void setPosition(Position position) {
+ this.position = position;
+ }
+
+ private Device device;
+
+ public Device getDevice() {
+ return device;
+ }
+
+ public void setDevice(Device device) {
+ this.device = device;
+ }
+
+}
diff --git a/src/main/java/org/traccar/forward/PositionForwarder.java b/src/main/java/org/traccar/forward/PositionForwarder.java
new file mode 100644
index 000000000..58bd1dcc7
--- /dev/null
+++ b/src/main/java/org/traccar/forward/PositionForwarder.java
@@ -0,0 +1,20 @@
+/*
+ * 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.forward;
+
+public interface PositionForwarder {
+ void forward(PositionData positionData, ResultHandler resultHandler);
+}
diff --git a/src/main/java/org/traccar/forward/PositionForwarderJson.java b/src/main/java/org/traccar/forward/PositionForwarderJson.java
new file mode 100644
index 000000000..27b96308e
--- /dev/null
+++ b/src/main/java/org/traccar/forward/PositionForwarderJson.java
@@ -0,0 +1,86 @@
+/*
+ * 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.forward;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.InvocationCallback;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+public class PositionForwarderJson implements PositionForwarder {
+
+ private final String url;
+ private final String header;
+
+ private final Client client;
+ private final ObjectMapper objectMapper;
+
+ public PositionForwarderJson(Config config, Client client, ObjectMapper objectMapper) {
+ this.client = client;
+ this.objectMapper = objectMapper;
+ this.url = config.getString(Keys.FORWARD_URL);
+ this.header = config.getString(Keys.FORWARD_HEADER);
+ }
+
+ @Override
+ public void forward(PositionData positionData, ResultHandler resultHandler) {
+ var requestBuilder = client.target(url).request();
+
+ MediaType mediaType = MediaType.APPLICATION_JSON_TYPE;
+ if (header != null && !header.isEmpty()) {
+ for (String line: header.split("\\r?\\n")) {
+ String[] values = line.split(":", 2);
+ String headerName = values[0].trim();
+ String headerValue = values[1].trim();
+ if (headerName.equals(HttpHeaders.CONTENT_TYPE)) {
+ mediaType = MediaType.valueOf(headerValue);
+ } else {
+ requestBuilder.header(headerName, headerValue);
+ }
+ }
+ }
+
+ try {
+ var entity = Entity.entity(objectMapper.writeValueAsString(positionData), mediaType);
+ requestBuilder.async().post(entity, new InvocationCallback<Response>() {
+ @Override
+ public void completed(Response response) {
+ if (response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) {
+ resultHandler.onResult(true, null);
+ } else {
+ int code = response.getStatusInfo().getStatusCode();
+ resultHandler.onResult(false, new RuntimeException("HTTP code " + code));
+ }
+ }
+
+ @Override
+ public void failed(Throwable throwable) {
+ resultHandler.onResult(false, throwable);
+ }
+ });
+ } catch (JsonProcessingException e) {
+ resultHandler.onResult(false, e);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/forward/PositionForwarderUrl.java b/src/main/java/org/traccar/forward/PositionForwarderUrl.java
new file mode 100644
index 000000000..53cc7ad24
--- /dev/null
+++ b/src/main/java/org/traccar/forward/PositionForwarderUrl.java
@@ -0,0 +1,166 @@
+/*
+ * 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.forward;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.helper.Checksum;
+import org.traccar.model.Device;
+import org.traccar.model.Position;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.InvocationCallback;
+import javax.ws.rs.core.Response;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Calendar;
+import java.util.Formatter;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class PositionForwarderUrl implements PositionForwarder {
+
+ private final String url;
+ private final String header;
+
+ private final Client client;
+ private final ObjectMapper objectMapper;
+
+ public PositionForwarderUrl(Config config, Client client, ObjectMapper objectMapper) {
+ this.client = client;
+ this.objectMapper = objectMapper;
+ this.url = config.getString(Keys.FORWARD_URL);
+ this.header = config.getString(Keys.FORWARD_HEADER);
+ }
+
+ @Override
+ public void forward(PositionData positionData, ResultHandler resultHandler) {
+ try {
+ String url = formatRequest(positionData);
+ var requestBuilder = client.target(url).request();
+
+ if (header != null && !header.isEmpty()) {
+ for (String line: header.split("\\r?\\n")) {
+ String[] values = line.split(":", 2);
+ String headerName = values[0].trim();
+ String headerValue = values[1].trim();
+ requestBuilder.header(headerName, headerValue);
+ }
+ }
+
+ requestBuilder.async().get(new InvocationCallback<Response>() {
+ @Override
+ public void completed(Response response) {
+ if (response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) {
+ resultHandler.onResult(true, null);
+ } else {
+ int code = response.getStatusInfo().getStatusCode();
+ resultHandler.onResult(false, new RuntimeException("HTTP code " + code));
+ }
+ }
+
+ @Override
+ public void failed(Throwable throwable) {
+ resultHandler.onResult(false, throwable);
+ }
+ });
+ } catch (UnsupportedEncodingException | JsonProcessingException e) {
+ resultHandler.onResult(false, e);
+ }
+ }
+
+ public String formatRequest(
+ PositionData positionData) throws UnsupportedEncodingException, JsonProcessingException {
+
+ Position position = positionData.getPosition();
+ Device device = positionData.getDevice();
+
+ String request = url
+ .replace("{name}", URLEncoder.encode(device.getName(), StandardCharsets.UTF_8))
+ .replace("{uniqueId}", device.getUniqueId())
+ .replace("{status}", device.getStatus())
+ .replace("{deviceId}", String.valueOf(position.getDeviceId()))
+ .replace("{protocol}", String.valueOf(position.getProtocol()))
+ .replace("{deviceTime}", String.valueOf(position.getDeviceTime().getTime()))
+ .replace("{fixTime}", String.valueOf(position.getFixTime().getTime()))
+ .replace("{valid}", String.valueOf(position.getValid()))
+ .replace("{latitude}", String.valueOf(position.getLatitude()))
+ .replace("{longitude}", String.valueOf(position.getLongitude()))
+ .replace("{altitude}", String.valueOf(position.getAltitude()))
+ .replace("{speed}", String.valueOf(position.getSpeed()))
+ .replace("{course}", String.valueOf(position.getCourse()))
+ .replace("{accuracy}", String.valueOf(position.getAccuracy()))
+ .replace("{statusCode}", calculateStatus(position));
+
+ if (position.getAddress() != null) {
+ request = request.replace(
+ "{address}", URLEncoder.encode(position.getAddress(), StandardCharsets.UTF_8));
+ }
+
+ if (request.contains("{attributes}")) {
+ String attributes = objectMapper.writeValueAsString(position.getAttributes());
+ request = request.replace(
+ "{attributes}", URLEncoder.encode(attributes, StandardCharsets.UTF_8));
+ }
+
+ if (request.contains("{gprmc}")) {
+ request = request.replace("{gprmc}", formatSentence(position));
+ }
+
+ return request;
+ }
+
+ private static String formatSentence(Position position) {
+
+ StringBuilder s = new StringBuilder("$GPRMC,");
+
+ try (Formatter f = new Formatter(s, Locale.ENGLISH)) {
+
+ Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ENGLISH);
+ calendar.setTimeInMillis(position.getFixTime().getTime());
+
+ f.format("%1$tH%1$tM%1$tS.%1$tL,A,", calendar);
+
+ double lat = position.getLatitude();
+ double lon = position.getLongitude();
+
+ f.format("%02d%07.4f,%c,", (int) Math.abs(lat), Math.abs(lat) % 1 * 60, lat < 0 ? 'S' : 'N');
+ f.format("%03d%07.4f,%c,", (int) Math.abs(lon), Math.abs(lon) % 1 * 60, lon < 0 ? 'W' : 'E');
+
+ f.format("%.2f,%.2f,", position.getSpeed(), position.getCourse());
+ f.format("%1$td%1$tm%1$ty,,", calendar);
+ }
+
+ s.append(Checksum.nmea(s.substring(1)));
+
+ return s.toString();
+ }
+
+ // OpenGTS status code
+ private String calculateStatus(Position position) {
+ if (position.hasAttribute(Position.KEY_ALARM)) {
+ return "0xF841"; // STATUS_PANIC_ON
+ } else if (position.getSpeed() < 1.0) {
+ return "0xF020"; // STATUS_LOCATION
+ } else {
+ return "0xF11C"; // STATUS_MOTION_MOVING
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/forward/ResultHandler.java b/src/main/java/org/traccar/forward/ResultHandler.java
new file mode 100644
index 000000000..009daf495
--- /dev/null
+++ b/src/main/java/org/traccar/forward/ResultHandler.java
@@ -0,0 +1,20 @@
+/*
+ * 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.forward;
+
+public interface ResultHandler {
+ void onResult(boolean success, Throwable throwable);
+}
diff --git a/src/test/java/org/traccar/WebDataHandlerTest.java b/src/test/java/org/traccar/forward/PositionForwarderUrlTest.java
index 99dbb83fa..522958052 100644
--- a/src/test/java/org/traccar/WebDataHandlerTest.java
+++ b/src/test/java/org/traccar/forward/PositionForwarderUrlTest.java
@@ -1,19 +1,17 @@
-package org.traccar;
+package org.traccar.forward;
import org.junit.Test;
+import org.traccar.ProtocolTest;
import org.traccar.config.Config;
import org.traccar.config.Keys;
import org.traccar.model.Device;
import org.traccar.model.Position;
-import org.traccar.session.cache.CacheManager;
import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-public class WebDataHandlerTest extends ProtocolTest {
+public class PositionForwarderUrlTest extends ProtocolTest {
@Test
public void testFormatRequest() throws Exception {
@@ -28,14 +26,16 @@ public class WebDataHandlerTest extends ProtocolTest {
when(device.getName()).thenReturn("test");
when(device.getUniqueId()).thenReturn("123456789012345");
when(device.getStatus()).thenReturn(Device.STATUS_ONLINE);
- var cacheManager = mock(CacheManager.class);
- when(cacheManager.getObject(eq(Device.class), anyLong())).thenReturn(device);
- WebDataHandler handler = new WebDataHandler(config, cacheManager, null, null, null);
+ PositionData positionData = new PositionData();
+ positionData.setPosition(position);
+ positionData.setDevice(device);
+
+ PositionForwarderUrl forwarder = new PositionForwarderUrl(config, null, null);
assertEquals(
"http://localhost/?fixTime=1451610123000&gprmc=$GPRMC,010203.000,A,2000.0000,N,03000.0000,E,0.00,0.00,010116,,*05&name=test",
- handler.formatRequest(position));
+ forwarder.formatRequest(positionData));
}