From 5877cb1b3f1fa7331c4310b9754a3ec442586497 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 13 Nov 2022 11:48:43 -0800 Subject: Refactor position forwarding --- src/main/java/org/traccar/BasePipelineFactory.java | 2 +- src/main/java/org/traccar/MainModule.java | 16 ++ .../org/traccar/PositionForwardingHandler.java | 141 +++++++++ src/main/java/org/traccar/WebDataHandler.java | 316 --------------------- src/main/java/org/traccar/config/Keys.java | 17 +- .../org/traccar/database/NotificationManager.java | 6 +- .../java/org/traccar/forward/EventForwarder.java | 2 +- .../org/traccar/forward/EventForwarderJson.java | 22 +- .../java/org/traccar/forward/PositionData.java | 45 +++ .../org/traccar/forward/PositionForwarder.java | 20 ++ .../org/traccar/forward/PositionForwarderJson.java | 86 ++++++ .../org/traccar/forward/PositionForwarderUrl.java | 166 +++++++++++ .../java/org/traccar/forward/ResultHandler.java | 20 ++ src/test/java/org/traccar/WebDataHandlerTest.java | 42 --- .../traccar/forward/PositionForwarderUrlTest.java | 42 +++ 15 files changed, 561 insertions(+), 382 deletions(-) create mode 100644 src/main/java/org/traccar/PositionForwardingHandler.java delete mode 100644 src/main/java/org/traccar/WebDataHandler.java create mode 100644 src/main/java/org/traccar/forward/PositionData.java create mode 100644 src/main/java/org/traccar/forward/PositionForwarder.java create mode 100644 src/main/java/org/traccar/forward/PositionForwarderJson.java create mode 100644 src/main/java/org/traccar/forward/PositionForwarderUrl.java create mode 100644 src/main/java/org/traccar/forward/ResultHandler.java delete mode 100644 src/test/java/org/traccar/WebDataHandlerTest.java create mode 100644 src/test/java/org/traccar/forward/PositionForwarderUrlTest.java 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 { 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; @@ -318,6 +321,19 @@ public class MainModule extends AbstractModule { return null; } + @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) { 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, TimerTask { - - private int retries = 0; - private Map 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 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 prepareJsonPayload(Position position) { - - Map 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 @@ -720,14 +720,6 @@ public final class Keys { "forward.json", 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 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 @@ -745,7 +737,8 @@ public final class Keys { */ public static final ConfigKey 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 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 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() { @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() { + @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() { + @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/WebDataHandlerTest.java deleted file mode 100644 index 99dbb83fa..000000000 --- a/src/test/java/org/traccar/WebDataHandlerTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.traccar; - -import org.junit.Test; -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 { - - @Test - public void testFormatRequest() throws Exception { - - Config config = new Config(); - config.setString(Keys.FORWARD_URL, "http://localhost/?fixTime={fixTime}&gprmc={gprmc}&name={name}"); - - Position position = position("2016-01-01 01:02:03.000", true, 20, 30); - - var device = mock(Device.class); - when(device.getId()).thenReturn(1L); - 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); - - 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)); - - } - -} diff --git a/src/test/java/org/traccar/forward/PositionForwarderUrlTest.java b/src/test/java/org/traccar/forward/PositionForwarderUrlTest.java new file mode 100644 index 000000000..522958052 --- /dev/null +++ b/src/test/java/org/traccar/forward/PositionForwarderUrlTest.java @@ -0,0 +1,42 @@ +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 static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PositionForwarderUrlTest extends ProtocolTest { + + @Test + public void testFormatRequest() throws Exception { + + Config config = new Config(); + config.setString(Keys.FORWARD_URL, "http://localhost/?fixTime={fixTime}&gprmc={gprmc}&name={name}"); + + Position position = position("2016-01-01 01:02:03.000", true, 20, 30); + + var device = mock(Device.class); + when(device.getId()).thenReturn(1L); + when(device.getName()).thenReturn("test"); + when(device.getUniqueId()).thenReturn("123456789012345"); + when(device.getStatus()).thenReturn(Device.STATUS_ONLINE); + + 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", + forwarder.formatRequest(positionData)); + + } + +} -- cgit v1.2.3